@schoolofmotion/rocket-booster
Version:
Serverless reverse proxy and load balancing library built for Cloudflare Workers.
1 lines • 7.77 kB
JavaScript
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["rocket-booster"]=t():e["rocket-booster"]=t()}(self,(function(){return(()=>{"use strict";var e={d:(t,r)=>{for(var o in r)e.o(r,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:r[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{default:()=>E});const r=new Set(["country","continent","asn","ip","hostname","user-agent"]),o=new Set(["equal","not equal","greater","less","in","not in","contain","not contain","match","not match"]),n=({field:e,operator:t,value:n})=>{if(void 0===e||void 0===t||void 0===n)throw new Error("Invalid 'firewall' field in the option object");if(!1===r.has(e))throw new Error("Invalid 'firewall' field in the option object");if(!1===o.has(t))throw new Error("Invalid 'firewall' field in the option object")},s=(e,t)=>{const r=e.cf;switch(t){case"asn":return r.asn;case"continent":return r.continent||"";case"country":return r.country;case"hostname":return e.headers.get("host")||"";case"ip":return e.headers.get("cf-connecting-ip")||"";case"user-agent":return e.headers.get("user-agent")||"";default:return}},a=(e,t)=>{if(!(t instanceof RegExp))throw new Error("You must use 'new RegExp('...')' for 'value' in firewall configuration to use 'match' or 'not match' operator");return t.test(e.toString())},i=(e,t)=>{if("string"!=typeof e||"string"!=typeof t)throw new Error("You must use string for 'value' in firewall configuration to use 'contain' or 'not contain' operator");return e.includes(t)},c=(e,t)=>{if(!Array.isArray(t))throw new Error("You must use an Array for 'value' in firewall configuration to use 'in' or 'not in' operator");return t.some((t=>t===e))},u={match:a,contain:i,equal:(e,t)=>e===t,in:c,greater:(e,t)=>{if("number"!=typeof e||"number"!=typeof t)throw new Error("You must use number for 'value' in firewall configuration to use 'greater' or 'less' operator");return e>t},less:(e,t)=>{if("number"!=typeof e||"number"!=typeof t)throw new Error("You must use number for 'value' in firewall configuration to use 'greater' or 'less' operator");return e<t},"not match":(e,t)=>!a(e,t),"not contain":(e,t)=>!i(e,t),"not equal":(e,t)=>e!==t,"not in":(e,t)=>!c(e,t)},l=async(e,t)=>{const{request:r,options:o}=e;if(void 0!==o.firewall){o.firewall.forEach(n);for(const{field:e,operator:t,value:n}of o.firewall){const o=s(r,e);if(void 0!==o&&u[t](o,n))throw new Error("You don't have permission to access this service.")}await t()}else await t()},d=async(e,t)=>{const{request:r,options:o}=e;if(void 0===o.headers)return void await t();const n=new Headers(r.headers);if((e=>{e.set("X-Forwarded-Proto","https");const t=e.get("Host");null!==t&&e.set("X-Forwarded-Host",t);const r=e.get("cf-connecting-ip"),o=e.get("X-Forwarded-For");null!==r&&null===o&&e.set("X-Forwarded-For",r)})(n),void 0!==o.headers.request)for(const[e,t]of Object.entries(o.headers.request))n.set(e,t);e.request=new Request(r.url,{body:r.body,method:r.method,headers:n}),await t();const{response:s}=e,a=new Headers(s.headers);if(void 0!==o.headers.response)for(const[e,t]of Object.entries(o.headers.response))a.set(e,t);e.response=new Response(s.body,{status:s.status,statusText:s.statusText,headers:a})},f=e=>{if(void 0===e.domain)throw new Error("Invalid 'upstream' field in the option object")},h=e=>{const t=e.map((e=>void 0===e.weight?1:e.weight)),r=t.reduce(((e,r,o)=>{const n=e+r;return t[o]=n,n}));if(0===r)throw new Error("Total weights should be greater than 0.");const o=Math.random()*r;for(const r of t.keys())if(t[r]>=o)return e[r];return e[Math.floor(Math.random()*e.length)]},p={random:h,"ip-hash":(e,t)=>e[(t.headers.get("cf-connecting-ip")||"0.0.0.0").split(".").map(((e,t,r)=>parseInt(e,10)*256**(r.length-t-1))).reduce(((e,t)=>e+t))%e.length]},w=async(e,t)=>{const{request:r,options:o}=e,{upstream:n,loadBalancing:s}=o;if(void 0===n)throw new Error("The required 'upstream' field in the option object is missing");Array.isArray(n)?n.forEach(f):f(n);const a=Array.isArray(n)?n:[n];if(void 0===s)return e.upstream=h(a),void await t();const i=s.policy||"random",c=p[i];e.upstream=c(a,r),await t()},m=async(e,t)=>{const{request:r,upstream:o}=e;if(null===o)return void await t();const n=((e,t)=>{const r={body:t.body,method:t.method,headers:t.headers};return new Request(e,r)})(((e,t)=>{const r=new URL(e),{domain:o,port:n,protocol:s}=t;return r.hostname=o,void 0!==n&&(r.port=n.toString()),void 0!==s&&(r.protocol=`${s}:`),r.href})(r.url,o),r);e.response=await(async(e,t)=>{const r=t.timeout||1e4,o=t.redirect||"manual",n=setTimeout((()=>{throw new Error("Fetch Timeout")}),r),s=await fetch(e,{redirect:o});return clearTimeout(n),s})(n,o),await t()},g=async(e,t)=>{await t();const{request:r,response:o,options:n}=e,s=n.cors;if(void 0===s)return;const{origin:a,methods:i,exposedHeaders:c,allowedHeaders:u,credentials:l,maxAge:d}=s,f=r.headers.get("origin");if(null===f||!1===a)return;const h=new Headers(o.headers);if(!0===a?h.set("Access-Control-Allow-Origin",f):Array.isArray(a)?a.includes(f)&&h.set("Access-Control-Allow-Origin",f):"*"===a&&h.set("Access-Control-Allow-Origin","*"),Array.isArray(i))h.set("Access-Control-Allow-Methods",i.join(","));else if("*"===i)h.set("Access-Control-Allow-Methods","*");else{const e=r.headers.get("Access-Control-Request-Method");null!==e&&h.set("Access-Control-Allow-Methods",e)}if(Array.isArray(c)?h.set("Access-Control-Expose-Headers",c.join(",")):"*"===c&&h.set("Access-Control-Expose-Headers","*"),Array.isArray(u))h.set("Access-Control-Allow-Headers",u.join(","));else if("*"===u)h.set("Access-Control-Allow-Headers","*");else{const e=r.headers.get("Access-Control-Request-Headers");null!==e&&h.set("Access-Control-Allow-Headers",e)}!0===l&&h.set("Access-Control-Allow-Credentials","true"),void 0!==d&&Number.isInteger(d)&&h.set("Access-Control-Max-Age",d.toString()),e.response=new Response(o.body,{status:o.status,statusText:o.statusText,headers:h})},y=async(e,t)=>{const{request:r,options:o}=e;if(void 0===o.rewrite)return void await t();const n=new URL(r.url);n.pathname=((e,t)=>{if(void 0===t.path)return e;for(const[r,o]of Object.entries(t.path)){const t=new RegExp(r);if(t.test(e))return e.replace(t,o)}return e})(n.pathname,o.rewrite),e.request=new Request(n.href,{body:r.body,method:r.method,headers:r.headers}),await t()};class A{constructor(){this.get=async e=>{const t=await DATABASE.get(e);if("string"==typeof t)return JSON.parse(t)},this.put=async(e,t)=>{await DATABASE.put(e,JSON.stringify(t))},this.delete=async e=>{await DATABASE.delete(e)}}}const v=(e,t)=>new Response(e,{status:t}),b=e=>new URL(e.url).host;function E(e){const t=((...e)=>{const t=[...e];return{push:(...e)=>{t.push(...e)},execute:async e=>{const r=async(o,n)=>{if(n===o)throw new Error("next() called multiple times");if(n>=t.length)return;const s=t[n];await s(e,(async()=>r(n,n+1)))};await r(-1,0)}}})(l,w,d,g,y,m),r=[];return{use:(t,o)=>{r.push({pattern:t,options:{...e,...o}})},apply:async e=>{const o=((e,t)=>{const r=new URL(e.url);for(const{pattern:o,options:n}of t)if((void 0===n.methods||n.methods.includes(e.method))&&new RegExp(`^${o.replace(/\*/g,".*")}`).test(r.pathname))return n;return null})(e,r);if(null===o)return v("Failed to find a route that matches the path and method of the current request",500);const n={request:e,options:o,hostname:b(e),response:new Response("Unhandled response"),storage:new A,upstream:null};try{await t.execute(n)}catch(e){e instanceof Error&&(n.response=v(e.message,500))}return n.response}}}return t})()}));