Migrate From Bun to Express
This commit is contained in:
parent
b525cc0dd0
commit
d2c014e744
8 changed files with 3054 additions and 668 deletions
187
bun.lock
187
bun.lock
|
|
@ -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=="],
|
||||
}
|
||||
}
|
||||
245
checkpoint.js
245
checkpoint.js
|
|
@ -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
133
index.js
|
|
@ -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
2656
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
const clientIP = getRealIP(request);
|
||||
logs.plugin('ipfilter', `Incoming request from IP: ${clientIP}`);
|
||||
const [blocked, blockType, blockValue, customPage, asnOrgName] = isBlockedIPExtended(clientIP);
|
||||
|
||||
if (url.pathname.startsWith('/api')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
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 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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
295
plugins/proxy.js
295
plugins/proxy.js
|
|
@ -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',
|
||||
];
|
||||
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);
|
||||
});
|
||||
|
||||
// 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'));
|
||||
};
|
||||
// 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;
|
||||
const router = express.Router();
|
||||
|
||||
// Skip checkpoint endpoints
|
||||
if (path.startsWith('/api/challenge') || path.startsWith('/api/verify')) return undefined;
|
||||
// Skip checkpoint endpoints
|
||||
router.use('/api/challenge', (req, res, next) => next('route'));
|
||||
router.use('/api/verify', (req, res, next) => next('route'));
|
||||
|
||||
// Skip static assets
|
||||
if (path.startsWith('/webfont/') || path.startsWith('/js/')) return undefined;
|
||||
// Skip static assets (already handled by static middleware)
|
||||
router.use('/webfont/', (req, res, next) => next('route'));
|
||||
router.use('/js/', (req, res, next) => next('route'));
|
||||
|
||||
// Get the hostname from the request
|
||||
const hostname = request.headers.get('host')?.split(':')[0];
|
||||
const target = proxyMappings[hostname];
|
||||
if (!target) return undefined;
|
||||
// Create a proxy instance for each host
|
||||
const proxyInstances = {};
|
||||
Object.entries(proxyMappings).forEach(([host, target]) => {
|
||||
proxyInstances[host] = createProxyForHost(target);
|
||||
});
|
||||
|
||||
// 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');
|
||||
// Main proxy handler
|
||||
router.use((req, res, next) => {
|
||||
const hostname = req.hostname || req.headers.host?.split(':')[0];
|
||||
const proxyInstance = proxyInstances[hostname];
|
||||
|
||||
// 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;
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
// For any other routes, do not handle
|
||||
return undefined;
|
||||
const apiHandled = await handleStatsAPI(req, res);
|
||||
if (apiHandled) return;
|
||||
|
||||
// For any other routes, do not handle
|
||||
return next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue