pixel-serve-server
Version:
A robust Node.js utility for handling and processing images. This package provides features like resizing, format conversion and etc.
3 lines • 15.8 kB
JavaScript
import v from'path';import*as A from'fs/promises';import {readFile}from'fs/promises';import {createHash}from'crypto';import M from'sharp';import {fileURLToPath}from'url';import*as _ from'dns/promises';import*as K from'http';import*as X from'https';import {isIP}from'net';import be from'axios';import {z as z$1}from'zod';var xe=v.dirname(fileURLToPath(import.meta.url)),G=e=>v.join(xe,"assets",e),we=G("noimage.jpg"),ye=G("noavatar.png"),g={normal:async()=>readFile(we),avatar:async()=>readFile(ye)},Q=/^\/api\/v1\//,B=["jpeg","jpg","png","webp","gif","tiff","avif"],H={jpeg:"image/jpeg",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",tiff:"image/tiff",avif:"image/avif"};var x=(e,t,n,r)=>{if(e)try{e(t,{phase:n,src:r});}catch{}},ve=4096,J=async(e,t)=>{try{if(!e||!t||typeof t!="string"||t.length>ve||t.includes("\0")||t.includes("\\")||v.isAbsolute(t)||!/^[^\x00-\x1F\x7F]+$/.test(t))return !1;let n=v.resolve(e),r=v.resolve(n,t),[i,u]=await Promise.all([A.realpath(n),A.realpath(r)]);if(!(await A.stat(i)).isDirectory()||!(await A.stat(u)).isFile())return !1;let p=i+v.sep,c=(u+v.sep).startsWith(p)||u===i,o=v.relative(i,u);return !o.startsWith("..")&&!v.isAbsolute(o)&&c}catch{return false}},F=e=>{let t=isIP(e);if(t===0)return true;if(t===4){let r=e.split(".").map(s=>Number(s));if(r.length!==4||r.some(s=>Number.isNaN(s)))return true;let[i,u]=r;return i===0||i===10||i===127||i===169&&u===254||i===172&&u>=16&&u<=31||i===192&&u===168||i===192&&u===0||i===198&&(u===18||u===19)||i===198&&u===51&&r[2]===100||i===203&&u===0&&r[2]===113||i>=224&&i<=239||i>=240}let n=e.toLowerCase();if(n==="::"||n==="::0"||n==="::1")return true;if(n.startsWith("::ffff:")){let r=n.slice(7);return isIP(r)===4?F(r):true}return !!(/^fe[89ab][0-9a-f]?:/i.test(n)||/^f[cd][0-9a-f]{0,2}:/i.test(n)||n.startsWith("ff"))},Pe=async e=>{if(!e)return false;let t=e.replace(/^\[|\]$/g,"");if(isIP(t)!==0)return !F(t);try{let n=await _.lookup(t,{all:!0,verbatim:!0});return n.length?n.every(r=>!F(r.address)):!1}catch{return false}},Z=async e=>{if(!e)return null;let t=e.replace(/^\[|\]$/g,"");if(isIP(t)!==0){if(F(t))return null;let n=isIP(t)===6?6:4;return {address:t,family:n}}try{let n=await _.lookup(t,{all:!0,verbatim:!0});if(!n.length||n.some(i=>F(i.address)))return null;let r=n[0];return {address:r.address,family:r.family===6?6:4}}catch{return null}},Ie=(e,t)=>(n,r,i)=>{i(null,e,t);},Y=(e,t)=>{let n=Ie(e,t);return {httpAgent:new K.Agent({lookup:n}),httpsAgent:new X.Agent({lookup:n})}},ee=(e,t,n)=>n.includes(e)||n.includes(t),Ae=async(e,t,n,r)=>{try{return await be.get(e,{responseType:"arraybuffer",timeout:t,maxContentLength:n,maxBodyLength:n,maxRedirects:0,httpAgent:r.httpAgent,httpsAgent:r.httpsAgent,validateStatus:i=>i>=200&&i<300||i>=300&&i<400})}catch(i){let u=i;return u?.response?u.response:null}},Se=async(e,t="normal",{timeoutMs:n,maxBytes:r,allowedNetworkList:i,maxRedirects:u,onError:s})=>{try{let l=e;for(let p=0;p<=u;p++){let m;try{m=new URL(l);}catch(P){return x(s,P,"fetch",l),await g[t]()}if(!["http:","https:"].includes(m.protocol))return x(s,new Error(`disallowed protocol ${m.protocol}`),"fetch",l),await g[t]();if(!ee(m.hostname,m.host,i))return x(s,new Error(`host ${m.hostname} not in allowedNetworkList`),"fetch",l),await g[t]();let c=await Z(m.hostname);if(!c)return x(s,new Error(`host ${m.hostname} resolves to a private IP or DNS lookup failed`),"fetch",l),await g[t]();let o=Y(c.address,c.family),h=await Ae(l,n,r,o);if(!h)return x(s,new Error("network request returned no response"),"fetch",l),await g[t]();if(h.status>=300&&h.status<400){let P=h.headers?.location;if(!P)return x(s,new Error("redirect response missing Location header"),"fetch",l),await g[t]();try{l=new URL(P,l).toString();}catch(y){return x(s,y,"fetch",P),await g[t]()}continue}if(h.status<200||h.status>=300)return x(s,new Error(`non-2xx status ${h.status}`),"fetch",l),await g[t]();let f=h.headers?.["content-type"]?.toLowerCase()?.split(";")[0]?.trim(),w=Object.values(H);return f&&w.includes(f)?Buffer.from(h.data):(x(s,new Error(`disallowed content-type ${f??"missing"} for ${l}`),"fetch",l),await g[t]())}return x(s,new Error(`exceeded maxRedirects=${u}`),"fetch",e),await g[t]()}catch(l){return x(s,l,"fetch",e),await g[t]()}},W=async(e,t,n="normal",r,i)=>{if(!await J(t,e))return x(i,new Error(`invalid local path: ${e}`),"fs",e),await g[n]();try{let s=v.resolve(t,e);if(r){let l=await A.stat(s);if(l.size>r)return x(i,new Error(`local file ${e} exceeds maxDownloadBytes (${l.size} > ${r})`),"fs",e),await g[n]()}return await A.readFile(s)}catch(s){return x(i,s,"fs",e),await g[n]()}},te=(e,t,n)=>n!==void 0?e.startsWith(n)?e.slice(n.length):e:e.replace(t,""),re=(e,t,n,r="normal",i,u=[],{timeoutMs:s,maxBytes:l,maxRedirects:p=3,onError:m,apiPrefix:c})=>{try{let o=new URL(e);if(n!==void 0&&[n,`www.${n}`].includes(o.hostname)){let w=te(o.pathname,i,c);return W(w,t,r,l,m)}return ee(o.hostname,o.host,u)?["http:","https:"].includes(o.protocol)?Se(e,r,{timeoutMs:s,maxBytes:l,allowedNetworkList:u,maxRedirects:p,onError:m}):(x(m,new Error(`disallowed protocol ${o.protocol}`),"fetch",e),g[r]()):(x(m,new Error(`host ${o.hostname} not in allowedNetworkList`),"fetch",e),g[r]())}catch(o){return x(m,o,"fetch",e),W(e,t,r,l,m)}};var Ee=z$1.enum(B),Fe=z$1.enum(["avatar","normal"]),k=z$1.object({src:z$1.preprocess((e,t)=>{if(e==null||typeof e=="string")return e;let n=Array.isArray(e)?"array":typeof e;return t.addIssue({code:z$1.ZodIssueCode.custom,message:`src must be a string (received ${n})`}),z$1.NEVER},z$1.string().optional()).optional(),format:z$1.string().optional().transform(e=>{let t=e?.toLowerCase();return t&&Ee.options.includes(t)?t:void 0}).optional(),width:z$1.union([z$1.number(),z$1.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(z$1.number().int().min(50,"width too small").max(4e3,"width too large").optional()),height:z$1.union([z$1.number(),z$1.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(z$1.number().int().min(50,"height too small").max(4e3,"height too large").optional()),quality:z$1.union([z$1.number(),z$1.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(z$1.number().int().min(1).max(100).default(80)),folder:z$1.enum(["public","private"]).default("public"),type:Fe.default("normal"),userId:z$1.union([z$1.string(),z$1.number()]).optional().transform(e=>e==null?void 0:String(e).trim()).pipe(z$1.string().min(1,"userId cannot be empty").max(128,"userId too long").optional())}).strict(),q=z$1.object({baseDir:z$1.string().min(1,"baseDir is required"),idHandler:z$1.custom(e=>typeof e=="function",{message:"idHandler must be a function"}).optional(),getUserFolder:z$1.custom(e=>typeof e=="function",{message:"getUserFolder must be a function"}).optional(),getUserFolderRootDir:z$1.string().min(1).optional(),websiteURL:z$1.union([z$1.url(),z$1.string().regex(/^(?!-)[A-Za-z0-9-]{1,63}(\.(?!-)[A-Za-z0-9-]{1,63})*$/)]).optional(),apiRegex:z$1.instanceof(RegExp).default(Q),apiPrefix:z$1.string().min(1,"apiPrefix cannot be empty").optional(),allowedNetworkList:z$1.array(z$1.string().transform(e=>e.trim()).pipe(z$1.string().min(1,"allowedNetworkList entries cannot be empty").regex(/^[a-z0-9.-]+$/i,"allowedNetworkList entry is not a valid hostname"))).transform(e=>e.map(t=>t.toLowerCase())).default([]),cacheControl:z$1.string().optional(),etag:z$1.boolean().default(true),minWidth:z$1.number().int().positive().default(50),maxWidth:z$1.number().int().positive().default(4e3),minHeight:z$1.number().int().positive().default(50),maxHeight:z$1.number().int().positive().default(4e3),defaultQuality:z$1.number().int().min(1).max(100).default(80),requestTimeoutMs:z$1.number().int().positive().default(5e3),idHandlerTimeoutMs:z$1.number().int().positive().optional(),maxDownloadBytes:z$1.number().int().positive().default(5e6),maxRedirects:z$1.number().int().min(0).max(10).default(3),maxInputPixels:z$1.number().int().positive().default(16e3*16e3),allowSvgInput:z$1.boolean().default(false),onError:z$1.custom(e=>typeof e=="function",{message:"onError must be a function"}).optional(),onComplete:z$1.custom(e=>typeof e=="function",{message:"onComplete must be a function"}).optional()}).strict().refine(e=>e.minWidth<=e.maxWidth,{message:"minWidth must be less than or equal to maxWidth",path:["minWidth"]}).refine(e=>e.minHeight<=e.maxHeight,{message:"minHeight must be less than or equal to maxHeight",path:["minHeight"]});var ne=e=>q.parse(e),ie=(e,t)=>{let n=k.parse(e),r=(i,u,s)=>{if(i!==void 0)return Math.min(Math.max(i,u),s)};return {...n,width:r(n.width,t.minWidth,t.maxWidth),height:r(n.height,t.minHeight,t.maxHeight),quality:n.quality??t.defaultQuality,format:n.format??"jpeg"}};var I=(e,t,n)=>{if(e)try{e(t,n);}catch{}},j=(e,t)=>{if(e)try{e(t);}catch{}},z=e=>{let t=process.hrtime.bigint()-e,n=Number(t/1000000n),r=Number(t%1000000n)/1e6;return n+r},Re=100,se="image",le=(e,t)=>{let n=t,r=(e??"").split("#")[0].split("?")[0],i=v.basename(r),u=i&&i!=="/"&&i!=="\\"?v.basename(i,v.extname(i)):"",s=u.replace(/[^\x20-\x7E]/g,"_").replace(/["\\\x00-\x1F\x7F]/g,"_").replace(/_+/g,"_");s.startsWith("_")&&(s=s.slice(1)),s.endsWith("_")&&(s=s.slice(0,-1));let l=s.length>0?s:se,p=Math.max(1,Re-n.length-1),c=`${l.slice(0,p)}.${n}`,o=u.length>0?u:se,f=encodeURIComponent(o).replace(/['()*]/g,y=>`%${y.charCodeAt(0).toString(16).toUpperCase()}`).slice(0,p),w=f.lastIndexOf("%");for(w>=0&&f.length-w<3&&(f=f.slice(0,w));f.length>=3;){let y=f.slice(-3);if(y[0]!=="%")break;let C=parseInt(y.slice(1),16);if(C>=192&&C<=253){f=f.slice(0,-3);break}if(C>=128&&C<=191){f=f.slice(0,-3);continue}break}let P=`${f}.${n}`;return {asciiFilename:c,encodedFilename:P}},ue=async(e,t)=>{if(!e)return null;if(e.startsWith("http://")||e.startsWith("https://"))return `url:${e}`;try{let n=v.resolve(t,e),r=await A.stat(n);return `file:${r.mtimeMs}:${r.size}`}catch{return null}},me=(e,t)=>{let n=JSON.stringify({src:e.src??"",w:e.width??"",h:e.height??"",f:e.format,q:e.quality,t:e.type,fo:e.folder,u:e.parsedUserId??"",sid:t});return `"${createHash("sha256").update(n).digest("hex")}"`},oe=async(e,t,n)=>{let r;try{return await Promise.race([e,new Promise((i,u)=>{r=setTimeout(()=>u(new Error(`${n} timed out after ${t}ms`)),t);})])}finally{r!==void 0&&clearTimeout(r);}},de=async(e,t,n)=>{if(!e||!t)return false;let r;if(n!==void 0)r=n;else try{r=await A.realpath(v.resolve(e));}catch{r=v.resolve(e);}let i=v.resolve(t),u;try{u=await A.realpath(i);}catch{u=i;}if(r===u)return true;let s=v.relative(r,u);return s===""||s==="."?true:!s.startsWith("..")&&!v.isAbsolute(s)},fe=async e=>{try{return await A.realpath(v.resolve(e))}catch{return v.resolve(e)}},ce=e=>{if(!e||e.length===0)return false;let t=0;for(;t<e.length&&(e[t]===9||e[t]===10||e[t]===13||e[t]===32);)t++;if(e.length>=t+2&&(e[t]===254&&e[t+1]===255||e[t]===255&&e[t+1]===254)){let r=e[t]===255,i=Math.min(e.length,t+2+4096),u;if(r)u=e.subarray(t+2,i).toString("utf16le");else {let l=e.subarray(t+2,i),p=Buffer.alloc(l.length-l.length%2);for(let m=0;m+1<l.length;m+=2)p[m]=l[m+1],p[m+1]=l[m];u=p.toString("utf16le");}let s=u.trimStart().toLowerCase();return s.startsWith("<svg")?true:s.startsWith("<?xml")||s.startsWith("<!--")?/<svg[\s>]/.test(s):/<svg[\s>]/.test(s)}e.length>=t+3&&e[t]===239&&e[t+1]===187&&e[t+2]===191&&(t+=3);let n=e.subarray(t,Math.min(e.length,t+4096)).toString("latin1").trimStart().toLowerCase();return n.startsWith("<svg")?true:n.startsWith("<?xml")||n.startsWith("<!--")?/<svg[\s>]/.test(n):false},Ce=async(e,t,n,r,i)=>{let u=process.hrtime.bigint(),s="normal",l=r.onError,p=r.onComplete,m,c;try{let o;try{o=ie(e.query,{minWidth:r.minWidth,maxWidth:r.maxWidth,minHeight:r.minHeight,maxHeight:r.maxHeight,defaultQuality:r.defaultQuality});}catch(d){throw I(l,d,{phase:"validation"}),d}m=o.src,c=o.userId,s=o.type??"normal";let h=r.baseDir,f;if(o.userId&&(f=o.userId,r.idHandler)){let d=o.userId,b=r.idHandlerTimeoutMs??r.requestTimeoutMs;try{let E=Promise.resolve().then(()=>r.idHandler(d)),D=await oe(E,b,"idHandler");f=typeof D=="string"?D:d,typeof D!="string"&&I(l,new Error(`idHandler returned a non-string value (${typeof D})`),{phase:"idHandler",src:m,userId:d});}catch(E){f=d,I(l,E,{phase:"idHandler",src:m,userId:d});}c=f;}if(o.folder==="private"&&r.getUserFolder)try{let d=Promise.resolve().then(()=>r.getUserFolder(e,f)),b=await oe(d,r.requestTimeoutMs,"getUserFolder");b&&(r.getUserFolderRootDir?await de(r.getUserFolderRootDir,b,i)?h=b:I(l,new Error(`getUserFolder returned path "${b}" outside getUserFolderRootDir "${r.getUserFolderRootDir}"`),{phase:"getUserFolder",src:m,userId:c}):h=b);}catch(d){I(l,d,{phase:"getUserFolder",src:m,userId:c});}let w=B.includes(o.format)?o.format:"jpeg",P=await ue(o.src,h),y;if(r.etag&&P&&(y=me({src:o.src,width:o.width,height:o.height,format:w,quality:o.quality,type:o.type,folder:o.folder,parsedUserId:f},P),e.headers["if-none-match"]===y)){t.status(304).end(),j(p,{src:m,userId:c,format:w,outputBytes:0,cached:!0,durationMs:z(u)});return}let N=await(async()=>o.src?o.src.startsWith("http://")||o.src.startsWith("https://")?re(o.src,h,r.websiteURL,o.type,r.apiRegex,r.allowedNetworkList,{timeoutMs:r.requestTimeoutMs,maxBytes:r.maxDownloadBytes,maxRedirects:r.maxRedirects,onError:l,apiPrefix:r.apiPrefix}):W(o.src,h,o.type,r.maxDownloadBytes,l):g[o.type]())();if(!r.allowSvgInput&&ce(N)){let d=new Error("svg input rejected");throw I(l,d,{phase:"sharp",src:m,userId:c}),d}let L;try{let d=M(N,{failOn:"warning",limitInputPixels:r.maxInputPixels,sequentialRead:!0,unlimited:!1}),b=await d.metadata();if(b.width&&b.height&&b.width*b.height>r.maxInputPixels)throw new Error("input exceeds maxInputPixels");if(!r.allowSvgInput&&b.format==="svg")throw new Error("svg input rejected");if(d=M(N,{failOn:"warning",limitInputPixels:r.maxInputPixels,sequentialRead:!0,unlimited:!1}).rotate(),o.width||o.height){let E={width:o.width??void 0,height:o.height??void 0,fit:M.fit.cover,withoutEnlargement:!0};d=d.resize(E);}L=await d.toFormat(w,{quality:o.quality}).toBuffer();}catch(d){throw I(l,d,{phase:"sharp",src:m,userId:c}),d}if(r.etag&&!y&&(y=`"${createHash("sha256").update(L).digest("hex")}"`,e.headers["if-none-match"]===y)){t.status(304).end(),j(p,{src:m,userId:c,format:w,outputBytes:0,cached:!0,durationMs:z(u)});return}let{asciiFilename:ge,encodedFilename:pe}=le(o.src,w);t.type(H[w]),t.setHeader("Content-Disposition",`inline; filename="${ge}"; filename*=UTF-8''${pe}`),t.setHeader("Vary","Accept-Encoding"),t.setHeader("Cache-Control",r.cacheControl??"public, max-age=86400, stale-while-revalidate=604800"),y&&t.setHeader("ETag",y),t.setHeader("Content-Length",L.length.toString()),t.send(L),j(p,{src:m,userId:c,format:w,outputBytes:L.length,cached:!1,durationMs:z(u)});}catch{if(t.headersSent){let o=new Error("response already flushed");I(l,o,{phase:"fs",src:m,userId:c}),n(o);return}try{let h=await g[s==="avatar"?"avatar":"normal"]();t.type(H.jpeg),t.setHeader("Content-Disposition",'inline; filename="fallback.jpeg"'),t.setHeader("Vary","Accept-Encoding"),t.setHeader("Cache-Control","public, max-age=60"),t.send(h);}catch(o){I(l,o,{phase:"fs",src:m,userId:c}),n(o);}}},Le=e=>{let t;try{t=ne(e);}catch(s){throw I(e.onError,s,{phase:"schema"}),s}let n,r=false,i,u=async s=>r&&n!==void 0?n:(i||(i=fe(s).then(l=>(n=l,r=true,l))),i);return async(s,l,p)=>{let m;return t.getUserFolderRootDir&&(m=await u(t.getUserFolderRootDir)),Ce(s,l,p,t,m)}},Te=Le;
export{me as buildDeterministicEtag,le as buildFilename,Y as buildPinnedAgents,ue as buildSourceIdentifier,de as isInsideRoot,F as isPrivateIp,Pe as isPublicHost,J as isValidPath,ce as looksLikeSvg,q as optionsSchema,Te as registerServe,Z as resolvePinnedAddress,fe as resolveRootDir,te as stripApiPrefix,k as userDataSchema};//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map