Migrate From Bun to Express

This commit is contained in:
Caileb 2025-05-27 16:00:15 -05:00
parent b525cc0dd0
commit d2c014e744
8 changed files with 3054 additions and 668 deletions

187
bun.lock
View file

@ -1,187 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bunbun",
"dependencies": {
"@iarna/toml": "^2.2.5",
"cookie": "^1.0.2",
"dotenv": "^16.5.0",
"express-http-proxy": "^2.1.1",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^3.0.5",
"level": "^10.0.0",
"level-ttl": "^3.1.1",
"maxmind": "^4.3.25",
"string-dsa": "^2.1.0",
"tar": "^7.4.3",
"tar-stream": "^3.1.7",
"toml": "^3.0.0",
},
"devDependencies": {
"prettier": "^2.8.8",
},
},
},
"packages": {
"@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@types/http-proxy": ["@types/http-proxy@1.17.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w=="],
"@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
"abstract-level": ["abstract-level@3.1.0", "", { "dependencies": { "buffer": "^6.0.3", "is-buffer": "^2.0.5", "level-supports": "^6.2.0", "level-transcoder": "^1.0.1", "maybe-combine-errors": "^1.0.0", "module-error": "^1.0.1" } }, "sha512-j2e+TsAxy7Ri+0h7dJqwasymgt0zHBWX4+nMk3XatyuqgHfdstBJ9wsMfbiGwE1O+QovRyPcVAqcViMYdyPaaw=="],
"after": ["after@0.8.2", "", {}, "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA=="],
"b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="],
"bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browser-level": ["browser-level@3.0.0", "", { "dependencies": { "abstract-level": "^3.1.0" } }, "sha512-kGXtLh29jMwqKaskz5xeDLtCtN1KBz/DbQSqmvH7QdJiyGRC7RAM8PPg6gvUiNMa+wVnaxS9eSmEtP/f5ajOVw=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"classic-level": ["classic-level@3.0.0", "", { "dependencies": { "abstract-level": "^3.1.0", "module-error": "^1.0.1", "napi-macros": "^2.2.2", "node-gyp-build": "^4.3.0" } }, "sha512-yGy8j8LjPbN0Bh3+ygmyYvrmskVita92pD/zCoalfcC9XxZj6iDtZTAnz+ot7GG8p9KLTG+MZ84tSA4AhkgVZQ=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="],
"es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="],
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"express-http-proxy": ["express-http-proxy@2.1.1", "", { "dependencies": { "debug": "^3.0.1", "es6-promise": "^4.1.1", "raw-body": "^2.3.0" } }, "sha512-4aRQRqDQU7qNPV5av0/hLcyc0guB9UP71nCYrQEYml7YphTo8tmWf3nDZWdTJMMjFikyz9xKXaURor7Chygdwg=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="],
"http-proxy-middleware": ["http-proxy-middleware@3.0.5", "", { "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" } }, "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"level": ["level@10.0.0", "", { "dependencies": { "abstract-level": "^3.1.0", "browser-level": "^3.0.0", "classic-level": "^3.0.0" } }, "sha512-aZJvdfRr/f0VBbSRF5C81FHON47ZsC2TkGxbBezXpGGXAUEL/s6+GP73nnhAYRSCIqUNsmJjfeOF4lzRDKbUig=="],
"level-supports": ["level-supports@6.2.0", "", {}, "sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w=="],
"level-transcoder": ["level-transcoder@1.0.1", "", { "dependencies": { "buffer": "^6.0.3", "module-error": "^1.0.1" } }, "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w=="],
"level-ttl": ["level-ttl@3.1.1", "", { "dependencies": { "after": ">=0.8.1 <0.9.0-0", "list-stream": ">=1.0.0 <1.1.0-0", "lock": "~0.1.2", "xtend": ">=4.0.0 <4.1.0-0" } }, "sha512-OeiHOD2IPkmdLqqU4feVJL7mnZX/Q03WEClrQi5t9558alkajVaecCgwJQZVVL/zFR9q74n5pWN1eozifa1Ghw=="],
"list-stream": ["list-stream@1.0.1", "", { "dependencies": { "readable-stream": "~2.0.5", "xtend": "~4.0.1" } }, "sha512-XheYsTtN+/nay6Co4N9NlTjQzo1ohknNlDJfxTeH0tvvssxBINUXwmjqPtj8+7rYMBwTRb3kO8C8d6ogeRwD1A=="],
"lock": ["lock@0.1.4", "", {}, "sha512-IcEe2R+NA7WgM622ppgmJFCFZl20f2owsA1YiJg7qpvO0wdOgOuZdfhQMxCYXdESVX+QIF/eikE4hB5ZPM2ipA=="],
"maxmind": ["maxmind@4.3.25", "", { "dependencies": { "mmdb-lib": "2.2.0", "tiny-lru": "11.2.11" } }, "sha512-u7L6LrbXZUtpdoovTVHo/l4/EoWUT2eHfCKWDMNNTsW9BaLa7h0jCHjqVx5ZeS5aWorLGZSsZwqxcpoollBw1g=="],
"maybe-combine-errors": ["maybe-combine-errors@1.0.0", "", {}, "sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"mmdb-lib": ["mmdb-lib@2.2.0", "", {}, "sha512-V6DDh3v8tfZFWbeH6fsL5uBIlWL7SvRgGDaAZWFC5kjQ2xP5dl/mLpWwJQ1Ho6ZbEKVp/351QF1JXYTAmeZ/zA=="],
"module-error": ["module-error@1.0.2", "", {}, "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"napi-macros": ["napi-macros@2.2.2", "", {}, "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g=="],
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
"process-nextick-args": ["process-nextick-args@1.0.7", "", {}, "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw=="],
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
"readable-stream": ["readable-stream@2.0.6", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "~1.0.0", "process-nextick-args": "~1.0.6", "string_decoder": "~0.10.x", "util-deprecate": "~1.0.1" } }, "sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw=="],
"requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="],
"string-dsa": ["string-dsa@2.1.0", "", {}, "sha512-ht+H83VtdA0JXmZsRfhQYzUSwqK3T7STPqiD/u3bIvYUHLEw8zzZyvP9WI3l9uKbK/2IpU+ZdshAe5BoRil3wA=="],
"string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
"tiny-lru": ["tiny-lru@11.2.11", "", {}, "sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"http-proxy-middleware/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
}
}

View file

@ -658,113 +658,164 @@ async function handleTokenRedirect(request) {
}
function CheckpointMiddleware() {
return async (request) => {
// Check if checkpoint is enabled
if (checkpointConfig.Enabled === false) {
return undefined;
}
const urlObj = new URL(request.url);
const host = request.headers.get('host')?.split(':')[0];
const userAgent = request.headers.get('user-agent') || '';
// 1) Bypass via query keys
for (const { Key, Value, Domains } of checkpointConfig.BypassQueryKeys) {
if (urlObj.searchParams.get(Key) === Value) {
if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) {
return undefined;
}
// Return Express-compatible middleware
return {
middleware: async (req, res, next) => {
// Check if checkpoint is enabled
if (checkpointConfig.Enabled === false) {
return next();
}
}
// 2) Bypass via header keys
for (const { Name, Value, Domains } of checkpointConfig.BypassHeaderKeys) {
const headerVal = request.headers.get(Name);
if (headerVal === Value) {
if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) {
return undefined;
}
}
}
// Convert Express request to the format expected by checkpoint logic
const request = {
url: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
method: req.method,
headers: {
get: (name) => req.get(name),
entries: () => Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v])
},
json: () => new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
resolve(JSON.parse(body));
} catch (e) {
reject(e);
}
});
req.on('error', reject);
})
};
// Handle token redirect for URL-token login
const tokenResponse = await handleTokenRedirect(request);
if (tokenResponse) return tokenResponse;
const urlObj = new URL(request.url);
const host = request.headers.get('host')?.split(':')[0];
const userAgent = request.headers.get('user-agent') || '';
// Setup request context
const url = new URL(request.url);
let path = url.pathname;
if (checkpointConfig.SanitizeURLs) {
path = sanitizePath(path);
}
const method = request.method;
// Always allow challenge & verify endpoints
if (method === 'GET' && path === '/api/challenge') {
return handleGetCheckpointChallenge(request);
}
if (method === 'POST' && path === '/api/verify') {
return handleVerifyCheckpoint(request);
}
// Check new exclusion rules
if (checkpointConfig.ExclusionRules && checkpointConfig.ExclusionRules.length > 0) {
for (const rule of checkpointConfig.ExclusionRules) {
// Check if path matches
if (!rule.Path || !path.startsWith(rule.Path)) {
continue;
}
// Check if host matches (if specified)
if (rule.Hosts && rule.Hosts.length > 0 && !rule.Hosts.includes(host)) {
continue;
}
// Check if user agent matches (if specified)
if (rule.UserAgents && rule.UserAgents.length > 0) {
const matchesUA = rule.UserAgents.some((ua) => userAgent.includes(ua));
if (!matchesUA) {
continue;
// 1) Bypass via query keys
for (const { Key, Value, Domains } of checkpointConfig.BypassQueryKeys) {
if (urlObj.searchParams.get(Key) === Value) {
if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) {
return next();
}
}
// All conditions match - exclude this request
return undefined;
}
}
// Check file extensions
const ext = path.includes('.') ? path.slice(path.lastIndexOf('.')) : '';
// First check excluded extensions
if (ext && checkpointConfig.HTMLCheckpointExcludedExtensions.includes(ext)) {
return undefined;
}
// Then check if we should only include specific extensions
if (checkpointConfig.HTMLCheckpointIncludedExtensions.length > 0) {
// If extension list is specified and current extension is not in it, skip
if (!checkpointConfig.HTMLCheckpointIncludedExtensions.includes(ext)) {
return undefined;
// 2) Bypass via header keys
for (const { Name, Value, Domains } of checkpointConfig.BypassHeaderKeys) {
const headerVal = request.headers.get(Name);
if (headerVal === Value) {
if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) {
return next();
}
}
}
// Handle token redirect for URL-token login
const tokenResponse = await handleTokenRedirect(request);
if (tokenResponse) {
// Convert Response to Express response
res.status(tokenResponse.status);
tokenResponse.headers.forEach((value, key) => {
res.setHeader(key, value);
});
const body = await tokenResponse.text();
return res.send(body);
}
// Setup request context
const url = new URL(request.url);
let path = url.pathname;
if (checkpointConfig.SanitizeURLs) {
path = sanitizePath(path);
}
const method = request.method;
// Always allow challenge & verify endpoints
if (method === 'GET' && path === '/api/challenge') {
const response = await handleGetCheckpointChallenge(request);
res.status(response.status);
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
const body = await response.text();
return res.send(body);
}
if (method === 'POST' && path === '/api/verify') {
const response = await handleVerifyCheckpoint(request);
res.status(response.status);
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
const body = await response.text();
return res.send(body);
}
// Check new exclusion rules
if (checkpointConfig.ExclusionRules && checkpointConfig.ExclusionRules.length > 0) {
for (const rule of checkpointConfig.ExclusionRules) {
// Check if path matches
if (!rule.Path || !path.startsWith(rule.Path)) {
continue;
}
// Check if host matches (if specified)
if (rule.Hosts && rule.Hosts.length > 0 && !rule.Hosts.includes(host)) {
continue;
}
// Check if user agent matches (if specified)
if (rule.UserAgents && rule.UserAgents.length > 0) {
const matchesUA = rule.UserAgents.some((ua) => userAgent.includes(ua));
if (!matchesUA) {
continue;
}
}
// All conditions match - exclude this request
return next();
}
}
// Check file extensions
const ext = path.includes('.') ? path.slice(path.lastIndexOf('.')) : '';
// First check excluded extensions
if (ext && checkpointConfig.HTMLCheckpointExcludedExtensions.includes(ext)) {
return next();
}
// Then check if we should only include specific extensions
if (checkpointConfig.HTMLCheckpointIncludedExtensions.length > 0) {
// If extension list is specified and current extension is not in it, skip
if (!checkpointConfig.HTMLCheckpointIncludedExtensions.includes(ext)) {
return next();
}
}
// Validate session token
const cookies = cookie.parse(request.headers.get('cookie') || '');
const tokenCookie = cookies[checkpointConfig.CookieName];
const validation = await validateToken(tokenCookie, request);
if (validation) {
// Active session: bypass checkpoint
return next();
}
// Log new checkpoint flow
console.log(`checkpoint: incoming ${method} ${request.url}`);
console.log(`checkpoint: tokenCookie=${tokenCookie}`);
console.log(`checkpoint: validateToken => ${validation}`);
// Serve interstitial challenge
const response = await serveInterstitial(request);
res.status(response.status);
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
const body = await response.text();
return res.send(body);
}
// Validate session token
const cookies = cookie.parse(request.headers.get('cookie') || '');
const tokenCookie = cookies[checkpointConfig.CookieName];
const validation = await validateToken(tokenCookie, request);
if (validation) {
// Active session: bypass checkpoint
return undefined;
}
// Log new checkpoint flow
console.log(`checkpoint: incoming ${method} ${request.url}`);
console.log(`checkpoint: tokenCookie=${tokenCookie}`);
console.log(`checkpoint: validateToken => ${validation}`);
// Serve interstitial challenge
return serveInterstitial(request);
};
}

133
index.js
View file

@ -4,6 +4,10 @@ import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { secureImportModule } from './utils/plugins.js';
import * as logs from './utils/logs.js';
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { spawn } from 'child_process';
// Load environment variables from .env file
import dotenv from 'dotenv';
@ -45,10 +49,9 @@ if (process.argv.includes('-d')) {
}
// Spawn new background process
const args = process.argv.slice(1).filter((arg) => arg !== '-d');
const cp = Bun.spawn({
cmd: [process.argv[0], ...args],
const cp = spawn(process.argv[0], args, {
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
stdio: 'ignore'
});
cp.unref();
writeFileSync(pidFile, cp.pid.toString(), 'utf8');
@ -105,21 +108,14 @@ async function initDataDirectories() {
}
function staticFileMiddleware() {
return async (request) => {
const url = new URL(request.url);
const pathname = url.pathname;
if (pathname.startsWith('/webfont/') || pathname.startsWith('/js/')) {
const filePath = join(rootDir, 'pages/interstitial', pathname.slice(1));
try {
return new Response(Bun.file(filePath), {
headers: { 'Cache-Control': 'public, max-age=604800' },
});
} catch {
return new Response('Not Found', { status: 404 });
}
}
return undefined;
};
const router = express.Router();
router.use('/webfont', express.static(join(rootDir, 'pages/interstitial/webfont'), {
maxAge: '7d'
}));
router.use('/js', express.static(join(rootDir, 'pages/interstitial/js'), {
maxAge: '7d'
}));
return router;
}
async function main() {
@ -132,7 +128,19 @@ async function main() {
logs.config('stats', 'loaded');
logs.section('OPERATIONS');
let wsHandler;
const app = express();
const server = createServer(app);
// Trust proxy headers (important for proper protocol detection)
app.set('trust proxy', true);
// Initialize WebSocket server
const wss = new WebSocketServer({ noServer: true });
// Store WebSocket handlers
let wsHandlers = {};
try {
await secureImportModule('checkpoint.js');
} catch (e) {
@ -146,7 +154,9 @@ async function main() {
try {
await secureImportModule('plugins/proxy.js');
const mod = await import('./plugins/proxy.js');
wsHandler = mod.proxyWebSocketHandler;
if (mod.proxyWebSocketHandler) {
wsHandlers = mod.proxyWebSocketHandler;
}
} catch (e) {
logs.error('proxy', `Failed to load proxy plugin: ${e}`);
}
@ -156,7 +166,8 @@ async function main() {
logs.error('stats', `Failed to load stats plugin: ${e}`);
}
registerPlugin('static', staticFileMiddleware());
// Register static middleware
app.use(staticFileMiddleware());
logs.section('PLUGINS');
// Ensure ipfilter runs first by moving it to front of the registry
@ -169,28 +180,76 @@ async function main() {
logs.section('SYSTEM');
freezePlugins();
logs.section('SERVER');
const portNumber = Number(process.env.PORT || 3000);
// Apply all plugin middlewares to Express
const middlewareHandlers = loadPlugins();
logs.server(`🚀 Server is up and running on port ${portNumber}...`);
logs.section('REQ LOGS');
Bun.serve({
port: portNumber,
async fetch(request, server) {
for (const handler of middlewareHandlers) {
middlewareHandlers.forEach(handler => {
if (typeof handler === 'function') {
// Wrap plugin handlers to work with Express
app.use(async (req, res, next) => {
try {
const resp = await handler(request, server);
if (resp instanceof Response) return resp;
const result = await handler(req, { upgrade: () => false });
if (result instanceof Response) {
// Convert Response to Express response
res.status(result.status);
result.headers.forEach((value, key) => {
res.setHeader(key, value);
});
const body = await result.text();
res.send(body);
} else {
next();
}
} catch (err) {
logs.error('server', `Handler error: ${err}`);
next(err);
}
});
} else if (handler && handler.middleware) {
// If plugin exports Express middleware directly
app.use(handler.middleware);
}
});
// 404 handler
app.use((req, res) => {
res.status(404).send('Not Found');
});
// Error handler
app.use((err, req, res, next) => {
logs.error('server', `Server error: ${err.message}`);
res.status(500).send(`Server Error: ${err.message}`);
});
// Handle WebSocket upgrades
server.on('upgrade', (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
});
// WebSocket connection handler
if (wsHandlers.open) {
wss.on('connection', (ws, request) => {
ws.data = {};
if (wsHandlers.open) wsHandlers.open(ws);
if (wsHandlers.message) {
ws.on('message', (message) => wsHandlers.message(ws, message));
}
return new Response('Not Found', { status: 404 });
},
websocket: wsHandler,
error(err) {
return new Response(`Server Error: ${err.message}`, { status: 500 });
},
if (wsHandlers.close) {
ws.on('close', (code, reason) => wsHandlers.close(ws, code, reason));
}
if (wsHandlers.error) {
ws.on('error', (err) => wsHandlers.error(ws, err));
}
});
}
logs.section('SERVER');
const portNumber = Number(process.env.PORT || 3000);
server.listen(portNumber, () => {
logs.server(`🚀 Server is up and running on port ${portNumber}...`);
logs.section('REQ LOGS');
});
}

2656
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,27 +1,36 @@
{
"name": "checkpoint",
"module": "index.js",
"private": true,
"type": "module",
"scripts": {
"start": "bun index.js"
"start": "node index.js",
"dev": "nodemon index.js",
"daemon": "pm2 start index.js --name checkpoint",
"stop": "pm2 stop checkpoint",
"restart": "pm2 restart checkpoint",
"logs": "pm2 logs checkpoint"
},
"devDependencies": {
"nodemon": "^3.0.2",
"prettier": "^2.8.8"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"cookie": "^1.0.2",
"dotenv": "^16.5.0",
"express-http-proxy": "^2.1.1",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^3.0.5",
"express": "^4.18.2",
"http-proxy-middleware": "^2.0.6",
"level": "^10.0.0",
"level-ttl": "^3.1.1",
"maxmind": "^4.3.25",
"pm2": "^5.3.0",
"string-dsa": "^2.1.0",
"tar": "^7.4.3",
"tar-stream": "^3.1.7",
"toml": "^3.0.0"
"toml": "^3.0.0",
"ws": "^8.16.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View file

@ -381,83 +381,82 @@ function isBlockedIPExtended(ip) {
}
function IPBlockMiddleware() {
return async (request, server) => {
const clientIP = getRealIP(request, server);
logs.plugin('ipfilter', `Incoming request from IP: ${clientIP}`);
const [blocked, blockType, blockValue, customPage, asnOrgName] = isBlockedIPExtended(clientIP);
return {
middleware: async (req, res, next) => {
// Convert Express request to the format expected by ipfilter logic
const request = {
url: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
headers: {
get: (name) => req.get(name),
entries: () => Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v])
}
};
const clientIP = getRealIP(request);
logs.plugin('ipfilter', `Incoming request from IP: ${clientIP}`);
const [blocked, blockType, blockValue, customPage, asnOrgName] = isBlockedIPExtended(clientIP);
if (blocked) {
recordEvent('ipfilter.block', {
type: blockType,
value: blockValue,
asn_org: asnOrgName,
ip: clientIP, // Include the IP address for stats
});
const url = new URL(request.url);
if (blocked) {
recordEvent('ipfilter.block', {
type: blockType,
value: blockValue,
asn_org: asnOrgName,
ip: clientIP, // Include the IP address for stats
});
const url = new URL(request.url);
if (url.pathname.startsWith('/api')) {
return new Response(
JSON.stringify({
if (url.pathname.startsWith('/api')) {
return res.status(403).json({
error: 'Access denied from your location or network.',
reason: 'geoip',
type: blockType,
value: blockValue,
asn_org: asnOrgName,
}),
{
status: 403,
headers: { 'Content-Type': 'application/json' },
},
});
}
// Normalize page paths by stripping leading slash
const cleanCustomPage = customPage.replace(/^\/+/, '');
const cleanDefaultPage = defaultBlockPage.replace(/^\/+/, '');
let html = '';
logs.plugin(
'ipfilter',
`Block pages: custom="${cleanCustomPage}", default="${cleanDefaultPage}"`,
);
}
logs.plugin('ipfilter', 'Searching for block page in the following locations:');
const paths = [
// allow absolute paths relative to project root first
join(rootDir, cleanCustomPage),
];
// Fallback to default block page if custom page isn't found
if (customPage !== defaultBlockPage) {
paths.push(
// check default page at root directory
join(rootDir, cleanDefaultPage),
);
}
// Normalize page paths by stripping leading slash
const cleanCustomPage = customPage.replace(/^\/+/, '');
const cleanDefaultPage = defaultBlockPage.replace(/^\/+/, '');
for (const p of paths) {
logs.plugin('ipfilter', `Trying block page at: ${p}`);
const content = await loadBlockPage(p);
logs.plugin('ipfilter', `Load result for ${p}: ${content ? 'FOUND' : 'NOT FOUND'}`);
if (content) {
html = content;
break;
}
}
let html = '';
logs.plugin(
'ipfilter',
`Block pages: custom="${cleanCustomPage}", default="${cleanDefaultPage}"`,
);
logs.plugin('ipfilter', 'Searching for block page in the following locations:');
const paths = [
// allow absolute paths relative to project root first
join(rootDir, cleanCustomPage),
];
// Fallback to default block page if custom page isn't found
if (customPage !== defaultBlockPage) {
paths.push(
// check default page at root directory
join(rootDir, cleanDefaultPage),
);
}
for (const p of paths) {
logs.plugin('ipfilter', `Trying block page at: ${p}`);
const content = await loadBlockPage(p);
logs.plugin('ipfilter', `Load result for ${p}: ${content ? 'FOUND' : 'NOT FOUND'}`);
if (content) {
html = content;
break;
if (html) {
const output = html.replace('{{.ASNName}}', asnOrgName || 'Blocked Network');
return res.status(403).type('html').send(output);
} else {
return res.status(403).type('text').send('Access denied from your location or network.');
}
}
if (html) {
const output = html.replace('{{.ASNName}}', asnOrgName || 'Blocked Network');
return new Response(output, {
status: 403,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
} else {
return new Response('Access denied from your location or network.', {
status: 403,
headers: { 'Content-Type': 'text/plain' },
});
}
return next();
}
return undefined;
};
}

View file

@ -1,5 +1,7 @@
import { registerPlugin, loadConfig } from '../index.js';
import * as logs from '../utils/logs.js';
import { createProxyMiddleware } from 'http-proxy-middleware';
import express from 'express';
const proxyConfig = {};
await loadConfig('proxy', proxyConfig);
@ -17,222 +19,113 @@ proxyConfig.Mapping.forEach(mapping => {
logs.plugin('proxy', `Proxy mappings loaded: ${JSON.stringify(proxyMappings)}`);
const HOP_BY_HOP_HEADERS = [
'connection',
'keep-alive',
'proxy-authenticate',
'proxy-authorization',
'te',
'trailer',
'transfer-encoding',
'upgrade',
];
// Connect to upstream WebSocket with handshake timeout
async function connectUpstreamWebSocket(url, headers) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(url, { headers });
const timer = setTimeout(() => {
ws.close();
reject(new Error('timeout'));
}, wsTimeout);
ws.onopen = () => {
clearTimeout(timer);
resolve(ws);
};
ws.onerror = (err) => {
clearTimeout(timer);
reject(err);
};
ws.onclose = () => {
clearTimeout(timer);
reject(new Error('closed'));
};
function createProxyForHost(target) {
return createProxyMiddleware({
target,
changeOrigin: true,
ws: true, // Enable WebSocket support
timeout: upstreamTimeout,
proxyTimeout: upstreamTimeout,
onProxyReq: (proxyReq, req, res) => {
// Remove undefined headers
const headersToRemove = ['x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-for'];
headersToRemove.forEach(header => {
proxyReq.removeHeader(header);
});
// Set proper forwarded headers
const forwarded = {
for: req.ip || req.connection.remoteAddress,
host: req.get('host'),
proto: req.protocol
};
proxyReq.setHeader('X-Forwarded-For', forwarded.for);
proxyReq.setHeader('X-Forwarded-Host', forwarded.host);
proxyReq.setHeader('X-Forwarded-Proto', forwarded.proto);
// Log the proxied request
const startTime = Date.now();
res.on('finish', () => {
const latency = Date.now() - startTime;
logs.plugin('proxy', `Proxied request to: ${target}${req.url} (${res.statusCode}) (${latency}ms)`);
});
},
onProxyReqWs: (proxyReq, req, socket, options, head) => {
// Set WebSocket timeout
socket.setTimeout(wsTimeout);
logs.plugin('proxy', `WebSocket proxied to: ${target}${req.url}`);
},
onError: (err, req, res) => {
logs.error('proxy', `Proxy error: ${err.message}`);
if (!res.headersSent) {
res.status(502).send('Bad Gateway');
}
},
// Handle SSE and streaming responses properly
onProxyRes: (proxyRes, req, res) => {
// For SSE responses, ensure proper headers
const contentType = proxyRes.headers['content-type'];
if (contentType && contentType.includes('text/event-stream')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('X-Accel-Buffering', 'no');
// Remove compression for SSE
delete proxyRes.headers['content-encoding'];
// Force connection keep-alive
res.setHeader('Connection', 'keep-alive');
}
},
// Advanced options for better compatibility
followRedirects: false,
preserveHeaderKeyCase: true,
autoRewrite: true,
protocolRewrite: 'http',
cookieDomainRewrite: {
"*": "" // Remove domain restrictions from cookies
}
});
}
async function createProxyResponse(targetURL, request) {
try {
const url = new URL(request.url);
const targetPathAndQuery = url.pathname + url.search;
const fullTargetURL = new URL(targetPathAndQuery, targetURL).toString();
const startTime = Date.now();
const outgoingHeaders = new Headers(request.headers);
outgoingHeaders.delete('host');
// Set proper host header for the target
const targetHost = new URL(targetURL).host;
outgoingHeaders.set('host', targetHost);
// Forward the original host as X-Forwarded-Host for applications that need it
outgoingHeaders.set('x-forwarded-host', request.headers.get('host'));
outgoingHeaders.set('x-forwarded-proto', url.protocol.replace(':', ''));
const options = {
method: request.method,
headers: outgoingHeaders,
// Always use manual redirect to let client handle it
redirect: 'manual',
// Don't decode compressed responses - let the client handle it
decompress: false,
};
// Handle request body
if (request.body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) {
options.body = request.body;
}
// Add timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => {
logs.warn('proxy', `Upstream request to ${fullTargetURL} timed out after ${upstreamTimeout}ms`);
controller.abort();
}, upstreamTimeout);
let response;
try {
response = await fetch(fullTargetURL, { ...options, signal: controller.signal });
} catch (fetchErr) {
clearTimeout(timeoutId);
if (fetchErr.name === 'AbortError') {
logs.error('proxy', `Upstream fetch aborted for ${fullTargetURL} (likely due to timeout)`);
return new Response('Gateway Timeout', { status: 504 });
}
logs.error('proxy', `Fetch error: ${fetchErr.message}`);
return new Response('Bad Gateway', { status: 502 });
}
clearTimeout(timeoutId);
const latency = Date.now() - startTime;
logs.plugin('proxy', `Proxied request to: ${fullTargetURL} (${response.status} ${response.statusText}) (${latency}ms)`);
const responseHeaders = new Headers(response.headers);
// Remove hop-by-hop headers
HOP_BY_HOP_HEADERS.forEach((h) => responseHeaders.delete(h));
// IMPORTANT: Don't remove content-encoding or modify the body
// Let the response stream through as-is for SSE compatibility
// Add proxy information
responseHeaders.set('X-Proxy-Latency', `${latency}ms`);
// Handle Set-Cookie headers - rewrite domain if needed
const setCookieHeaders = response.headers.getSetCookie ? response.headers.getSetCookie() : [];
if (setCookieHeaders.length > 0) {
responseHeaders.delete('set-cookie');
setCookieHeaders.forEach(cookieStr => {
let modifiedCookie = cookieStr;
// Remove domain restrictions
modifiedCookie = modifiedCookie.replace(/;\s*domain=[^;]*/gi, '');
// Handle SameSite for local development
if (url.protocol === 'http:' && modifiedCookie.match(/samesite\s*=\s*none/i)) {
modifiedCookie = modifiedCookie.replace(/;\s*samesite=[^;]*/gi, '; SameSite=Lax');
modifiedCookie = modifiedCookie.replace(/;\s*secure/gi, '');
}
responseHeaders.append('set-cookie', modifiedCookie);
});
}
// Return response with original body stream
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
} catch (err) {
logs.error('proxy', `Proxy error processing ${request.method} ${request.url}: ${err.message}`);
logs.error('proxy', `Full error details: ${err.stack}`);
return new Response('Bad Gateway', { status: 502 });
}
}
function proxyMiddleware() {
return async (request, server) => {
const url = new URL(request.url);
const path = url.pathname;
// Skip checkpoint endpoints
if (path.startsWith('/api/challenge') || path.startsWith('/api/verify')) return undefined;
// Skip static assets
if (path.startsWith('/webfont/') || path.startsWith('/js/')) return undefined;
// Get the hostname from the request
const hostname = request.headers.get('host')?.split(':')[0];
const target = proxyMappings[hostname];
if (!target) return undefined;
// Handle WebSocket upgrade requests
const upgradeHeader = request.headers.get('upgrade')?.toLowerCase();
if (upgradeHeader === 'websocket') {
const targetUrl = new URL(url.pathname + url.search, target);
targetUrl.protocol = targetUrl.protocol.replace(/^http/, 'ws');
// Forward important headers for WebSocket
const wsHeaders = {};
['cookie', 'authorization', 'origin', 'sec-websocket-protocol', 'sec-websocket-extensions']
.forEach(header => {
const value = request.headers.get(header);
if (value) wsHeaders[header] = value;
});
let upstream;
try {
upstream = await connectUpstreamWebSocket(targetUrl.toString(), wsHeaders);
} catch (err) {
logs.error('proxy', `Upstream WebSocket connection failed: ${err}`);
return new Response('Bad Gateway', { status: 502 });
}
// Upgrade incoming client connection and attach upstream socket
const ok = server.upgrade(request, { data: { upstream } });
if (!ok) {
logs.error('proxy', 'WebSocket upgrade failed');
upstream.close();
return new Response('Bad Gateway', { status: 502 });
}
logs.plugin('proxy', `WebSocket proxied to: ${targetUrl.toString()}`);
return;
const router = express.Router();
// Skip checkpoint endpoints
router.use('/api/challenge', (req, res, next) => next('route'));
router.use('/api/verify', (req, res, next) => next('route'));
// Skip static assets (already handled by static middleware)
router.use('/webfont/', (req, res, next) => next('route'));
router.use('/js/', (req, res, next) => next('route'));
// Create a proxy instance for each host
const proxyInstances = {};
Object.entries(proxyMappings).forEach(([host, target]) => {
proxyInstances[host] = createProxyForHost(target);
});
// Main proxy handler
router.use((req, res, next) => {
const hostname = req.hostname || req.headers.host?.split(':')[0];
const proxyInstance = proxyInstances[hostname];
if (proxyInstance) {
proxyInstance(req, res, next);
} else {
next();
}
return createProxyResponse(target, request);
};
});
return { middleware: router };
}
// WebSocket handlers for proxying messages between client and upstream
// Export WebSocket handler for compatibility
export const proxyWebSocketHandler = {
open(ws) {
const upstream = ws.data.upstream;
upstream.onopen = () => logs.plugin('proxy', 'Upstream WebSocket connected');
// Forward messages from target to client
upstream.onmessage = (event) => ws.send(event.data);
upstream.onerror = (err) => {
logs.error('proxy', `Upstream WebSocket error: ${err}`);
ws.close(1011, 'Upstream error');
};
upstream.onclose = ({ code, reason }) => ws.close(code, reason);
},
message(ws, message) {
const upstream = ws.data.upstream;
// Forward messages from client to target
upstream.send(message);
},
close(ws, code, reason) {
const upstream = ws.data.upstream;
upstream.close(code, reason);
},
error(ws, err) {
logs.error('proxy', `WebSocket proxy error: ${err}`);
const upstream = ws.data.upstream;
upstream.close();
},
// http-proxy-middleware handles WebSocket internally
// These are kept for compatibility but won't be used
open: () => {},
message: () => {},
close: () => {},
error: () => {}
};
if (enabled) {

View file

@ -72,24 +72,23 @@ function recordEvent(metric, data = {}) {
}
// Handler for serving the stats HTML UI
async function handleStatsPage(request) {
const url = new URL(request.url);
if (url.pathname !== statsUIPath) return undefined;
async function handleStatsPage(req, res) {
const url = new URL(`${req.protocol}://${req.get('host')}${req.originalUrl}`);
if (url.pathname !== statsUIPath) return false;
try {
const html = await fs.readFile(path.join(__dirname, 'stats.html'), 'utf8');
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
res.status(200).type('html').send(html);
return true;
} catch (e) {
return new Response('Stats UI not found', { status: 404 });
res.status(404).send('Stats UI not found');
return true;
}
}
// Handler for stats API
async function handleStatsAPI(request) {
const url = new URL(request.url);
if (url.pathname !== statsAPIPath) return undefined;
async function handleStatsAPI(req, res) {
const url = new URL(`${req.protocol}://${req.get('host')}${req.originalUrl}`);
if (url.pathname !== statsAPIPath) return false;
const metric = url.searchParams.get('metric');
const start = parseInt(url.searchParams.get('start') || '0', 10);
const end = parseInt(url.searchParams.get('end') || `${Date.now()}`, 10);
@ -101,23 +100,24 @@ async function handleStatsAPI(request) {
})) {
result.push(value);
}
return new Response(JSON.stringify(result), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
res.status(200).json(result);
return true;
}
// Middleware for stats plugin
function StatsMiddleware() {
return async (request) => {
// Always serve stats UI and API first, bypassing auth
const pageResp = await handleStatsPage(request);
if (pageResp) return pageResp;
const apiResp = await handleStatsAPI(request);
if (apiResp) return apiResp;
return {
middleware: async (req, res, next) => {
// Always serve stats UI and API first, bypassing auth
const pageHandled = await handleStatsPage(req, res);
if (pageHandled) return;
const apiHandled = await handleStatsAPI(req, res);
if (apiHandled) return;
// For any other routes, do not handle
return undefined;
// For any other routes, do not handle
return next();
}
};
}