From 4392035512796675a52f454b54f29b079834657a Mon Sep 17 00:00:00 2001 From: FullGreaM Date: Sat, 11 Jan 2025 10:07:16 +0300 Subject: [PATCH] Add support of encryption and HTTP Auth --- components/_component.js | 10 ++++ components/http-reverse.js | 101 ++++++++++++++++++++++++++++--------- components/index.js | 2 + package-lock.json | 22 +++++++- package.json | 3 +- routemap-parser.js | 25 ++++++++- routemap.txt | 6 +-- tests/priv.key | 27 ++++++++++ tests/pub.crt | 18 +++++++ 9 files changed, 183 insertions(+), 31 deletions(-) create mode 100644 tests/priv.key create mode 100644 tests/pub.crt diff --git a/components/_component.js b/components/_component.js index 8475a92..ec702c8 100644 --- a/components/_component.js +++ b/components/_component.js @@ -4,5 +4,15 @@ module.exports = class Component { this.syntax = syntaxTree; } + // Run server run () {} + + // Working with domains + domainAction (context, domain) {} + + // Call this function where need works with domain (DO NOT REWRITE) + callDomain (domain, domainST) { + const component = new domainST.Type(this.adr, domainST); + return { argv: (...argv) => component.domainAction(this, domain, ...argv) }; + } }; diff --git a/components/http-reverse.js b/components/http-reverse.js index 113312d..bddab2e 100644 --- a/components/http-reverse.js +++ b/components/http-reverse.js @@ -1,22 +1,82 @@ const Component = require("./_component"); +const https = require("https"); const express = require("express"); const bodyParser = require("body-parser"); const bodyHand = require('body'); const url = require('url'); const path = require('path'); +const fs = require('fs'); const fetch = require('node-fetch'); function logger (...p) { - return; + //return; console.log("[DEBUG.main]:", ...p); } +function auth (self, req, res, next) { + if (self.syntax.auth) { + const ACCESS = "Basic " + Buffer.from(`${self.syntax.auth.login}:${self.syntax.auth.password}`).toString('base64'); + if (req.headers['authorization'] === ACCESS) { + return next(); + } + else { + if (self.syntax.auth.mode === "strict" && req.headers['authorization'] !== undefined) { + return res.status(403).send("

Permission denied


Permission denied.

"); + } + res.set("WWW-Authenticate", "Basic"); + return res.status(401).send(); + } + } + next(); +} + +function fetchProxy (req, res, reverseTo) { + const params = { method: req.method.toUpperCase(), headers: Object.assign({}, req.headers), redirect: "follow", follow: 20, compress: false }; + delete params.headers.host; + delete params.headers.referer; + if (!["GET", "HEAD"].includes(params.method)) + params.body = req.body; + + const reqAddress = url.resolve(reverseTo, path.join("/", req.originalUrl)); + fetch(reqAddress, params) + .then(async result => { + const content = await result.arrayBuffer(); + //logger("result:", result); + //logger("result.arrayBuffer():", content); + //logger("result.Buffer:", Buffer.from(content).toString()); + + const headers = Object.fromEntries(result.headers); + + //logger("headers Headers", result.headers); + //logger("headers Headers.raw()", result.headers.raw()); + logger("headers object", headers); + + Object.entries(headers).forEach(([header, value]) => { + res.set(header, value); + }); + + const finalContent = Buffer.from(content); + res.status(result.status).send(finalContent); + }) + .catch(err => { + console.error(err); + res.status(500).send("Error

Error:


Error catched. Check logs

"); + }); +} + module.exports = class HTTPReverse extends Component { constructor (adr, syntaxTree) { super(adr, syntaxTree); } + domainAction (context, domain, req, res) { + logger("DOMAIN ACT:", this.syntax); + if (!(context instanceof HTTPReverse)) + throw new SyntaxError("You can't call revHTTP domain from unsupported type. Supported types: revHTTP, simpleHTTP, redirectHTTP"); + fetchProxy(req, res, this.syntax.target); + } + run () { this.app = express(); @@ -38,39 +98,32 @@ module.exports = class HTTPReverse extends Component { }); }); + this.app.use((req, res, next) => auth(this, req, res, next)); + this.app.use("*", async (req, res) => { logger("REQ HEADERS:>", req.headers); logger("originalUrl:>", req.originalUrl); const currentDomain = req.hostname; - const params = { method: req.method.toUpperCase(), headers: Object.assign({}, req.headers), redirect: "follow", follow: 20 }; - delete params.headers.host; - delete params.headers.referer; - if (!["GET", "HEAD"].includes(params.method)) - params.body = req.body; logger("hostname:", currentDomain); - const reverseToLoc = !this.syntax.domains[currentDomain] ? reverseTo : this.syntax.domains[currentDomain].target; - const reqAddress = url.resolve(reverseToLoc, path.join("/", req.originalUrl)); - fetch(reqAddress, params) - .then(async result => { - const content = await result.text(); - logger("result:", result); - logger("result.text():", content); - Object.entries(result?.headers ? result.headers : new Object()).forEach(([header, value]) => { - res.set(header, value); - }); - res.status(result.status).send(content); - }) - .catch(err => { - console.error(err); - res.status(500).send("Error

Error:


Error catched. Check logs

"); - }); + const isNotDomained = !this.syntax.domains[currentDomain]; + if (!isNotDomained) { + return this + .callDomain(currentDomain, this.syntax.domains[currentDomain]) + .argv(req, res); + } + return fetchProxy(req, res, reverseTo); }); - this.app.listen(this.adr.port, address, (err) => { + const server = !this.syntax.encryption ? this.app : https.createServer({ + cert : fs.readFileSync(this.syntax.encryption.public, 'utf-8'), + key : fs.readFileSync(this.syntax.encryption.private, 'utf-8') + }, this.app); + + server.listen(this.adr.port, address, (err) => { if (err) { throw err; } - console.log(`Koboldcpp reverse proxy server listened at ${ + console.log(`HTTP reverse proxy server listened at ${ address }:${this.adr.port}`); }) diff --git a/components/index.js b/components/index.js index 515bb0e..14c0914 100644 --- a/components/index.js +++ b/components/index.js @@ -1,3 +1,5 @@ module.exports = { "revHTTP": require("./http-reverse"), + "simpleHTTP": require("./http-reverse"), + "redirectHTTP": require("./http-reverse"), }; diff --git a/package-lock.json b/package-lock.json index a87ef5a..05caf17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "body": "^5.1.0", "body-parser": "^1.20.3", "express": "^4.21.2", - "node-fetch": "^2.6.7" + "node-fetch": "^2.6.7", + "sse": "^0.0.8" }, "devDependencies": { "nodemon": "^3.1.9" @@ -816,6 +817,14 @@ "node": ">= 0.8" } }, + "node_modules/options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha512-bOj3L1ypm++N+n7CEbbe473A414AB7z+amKYshRb//iuL3MpdDCLhPnw6aVTdKB9g5ZRVHIEp8eUln6L2NUStg==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1083,6 +1092,17 @@ "node": ">=10" } }, + "node_modules/sse": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/sse/-/sse-0.0.8.tgz", + "integrity": "sha512-cviG7JH31TUhZeaEVhac3zTzA+2FwA7qvHziAHpb7mC7RNVJ/RbHN+6LIGsS2ugP4o2H15DWmrSMK+91CboIcg==", + "dependencies": { + "options": "0.0.6" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index bbafe4d..e1c5237 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "body": "^5.1.0", "body-parser": "^1.20.3", "express": "^4.21.2", - "node-fetch": "^2.6.7" + "node-fetch": "^2.6.7", + "sse": "^0.0.8" }, "devDependencies": { "nodemon": "^3.1.9" diff --git a/routemap-parser.js b/routemap-parser.js index bda0569..9eb1d40 100644 --- a/routemap-parser.js +++ b/routemap-parser.js @@ -6,14 +6,14 @@ const components = require("./components"); // const FIND_ADDRESS_INSTR_REGEX = /([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]{1,5}\s{0,}\{[\s\n\r0-9a-z;:\/\."',]{0,}\}/gmi; const DOMAINER_REGEX = /domain\s{1,}[a-z0-9\.]{1,}\s{1,}\{[\s\n\r0-9a-z;:\/\."',]{0,}\}/gmi; const DOMAINER_VALUE_FIND_REGEX = /domain\s{1,}[a-z0-9\.]{1,}\s{1,}\{/gmi; -const ADDRESS_WITH_PORT_REGEX = /([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]{1,5}/gmi +const ADDRESS_WITH_PORT_REGEX = /([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]{1,5}/mi; const FIND_ADDRESS_INSTR_REGEX = /([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]{1,5}\s{0,}\{([\s\n\r0-9a-z;:\/\."',]|domain\s{1,}[a-z0-9\.]{1,}\s{1,}\{[\s\n\r0-9a-z;:\/\."',]{0,}\}){0,}\}/gmi; const COMMENT_REGEX = /#.{0,}(\n|\r|$)/gmi; const CONTENT_INTO_CURLY_BRACES = /\{[\s\n\r0-9a-z;:\/\."',]{0,}\}{0,}\}/gmi; // Instruction regexps function readInstructions (instructions, fullCode, forDomains=false) { - const syntaxTree = { firewall: false }; + const syntaxTree = { firewall: false, encryption: {} }; for (let instruction of instructions) { if (!instruction) { @@ -39,6 +39,22 @@ function readInstructions (instructions, fullCode, forDomains=false) { syntaxTree.target = instructionParts[1]; break; case "cert": + if (instructionParts.length < 3) + throw new SyntaxError("Invalid syntax of operator `cert`: cert "); + if (!["public", "private"].includes(instructionParts[1])) + throw new SyntaxError("Cert type can be only private or public, not " + instructionParts[1]); + syntaxTree.encryption[instructionParts[1]] = instructionParts.slice(2,).join(" "); + break; + case "auth": + if (instructionParts.length < 4 || instructionParts.length < 2) + throw new SyntaxError("Invalid syntax of operator `auth`: auth OR auth null"); + if (!["loopback", "strict", "null"].includes(instructionParts[1])) + throw new SyntaxError("Invalid mode: " + instructionParts[1] + ", valid modes: loopback, strict, null"); + syntaxTree.auth = instructionParts[1] !== "null" ? { + mode: instructionParts[1], + login: instructionParts[2], + password: instructionParts.slice(3,).join(" ") + } : null; break; case "firewall": break; @@ -51,6 +67,11 @@ function readInstructions (instructions, fullCode, forDomains=false) { } } + if (Object.keys(syntaxTree.encryption).length === 0) + syntaxTree.encryption = null; + else if (Object.keys(syntaxTree.encryption).length === 1) + throw new SyntaxError("No private or public key specified"); + if (!forDomains) { syntaxTree.domains = new Object(); const allDomains = Object.fromEntries((fullCode.match(DOMAINER_REGEX) ?? []) diff --git a/routemap.txt b/routemap.txt index 12b4a4a..a34bbe5 100644 --- a/routemap.txt +++ b/routemap.txt @@ -1,6 +1,5 @@ 127.0.0.1:9889 { type revHTTP; - target https://example.org; # RRR domain localhost { firewall whitelist GLOBAL; @@ -12,6 +11,7 @@ 127.0.0.1:7889 { type revHTTP; target https://google.com; - cert public /path/to/cert.pub; - cert private /path/to/cert.priv; + auth loopback admin qwerty; + cert public ./tests/pub.crt; + cert private ./tests/priv.key; } diff --git a/tests/priv.key b/tests/priv.key new file mode 100644 index 0000000..d7b4c01 --- /dev/null +++ b/tests/priv.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA2tGTaIEgV/Ar5hF5gL60pUJPYQuQ/8qCrKIB65i2KCTd/EIF +TmlcXVF+drSKyfBaPxee8FuuxR9gcw4mzPqYwRrqC1OPfANYEs+VUzBYRdI6uBgU +FWNVvWsDqpmHcGWgiZ+g/GRrfGBwN3xqpk+jPkTuXunBztZcvSyGCJnbpgKTbBrd +niRT6KX3KyZmwxSqvOQA7+ebUOUSrFY+t1Bf+BXD/QrECJ6RWbddrbFGSPE/QDei +8AlOWLVY8SHMtcC/0T4NSonR0yVVJ4NxkUO4P+kvdrqaKkY8UYXV4LLWt80J1Pky +YqhGq4g9nNnggpuDt4Xgp4uMI2hsb8H7Hn3JhwIDAQABAoIBAFEsnfqOrGjy99YJ +dYGFPY5w4vGlUM0TWxafowa7SBfsVU1XUJCeFV2o9bkjkLkJKPFu2c9gs5z83tLP +1SUFcdVxHpHFFd1zgCC6UTLjJFwCeTYB+LFWgGAcaufGvK+9g6iKRzZaKmWhgP+U +cBvyPyTCfYtIPFV2wECNW8ulCA61BgC8ecggCLkE3BBCVpz9pTcIKbNH/698EIIJ +EnS6VB2FBu0j1UGQ9cYOa5i1u+AW7GTfJxmO6zcdh5aNsWZQkshDmpyCbL3xHELK +okHvgbcoJDvNjW25UXcW78UJ+jHJoZJ3x61xVKFMaJ67ZlGNljlIhumhik2QPbvG +KLK8DAECgYEA+0EQN0lFOCIngowB/5QOxzHAg7UAQ7bXKsaAbZmJ5XGXrK3OltRi +jvxTw8z4BtW3+sW+5fkTmdCoz8Q9++Cg7eWecq5BWiToW8IB6hU73rpGoMTyLYX+ +xmt3+QY84qVWFMHQfBB4tD3JHJc0otkyD4DbOOcxLE7pPO7dtlkEj8ECgYEA3vOr +xV3zO4Wn7nXh3eHmcdtcbTBFktOiP9zOGphgNFtELC2CvjSXE+WO4dORhfr8fAki +RJSstGI32RMOsYm11jUuMGKG3/a0p06wZZKWgsVMrtT5xme5nKTZeoFnDSKusApi +Tv1ueeM/fOSU7gElpP/XNxolbmmn7BIT0O80q0cCgYAPfBAKyDo63mf+9K/+11od +zqQludb9VVxnZ3psxsSn3ltRqhiMVSSJ2WU0MQKsMSm4i81bEQGzPhfZTiffyRVB +9B/Sw4iFjLbBSPCjQ5HABUzrBZhDU/nCssbfExJXiYcHmnZF3yIBQbmOSIdW7fnW +2MMp+52dNHPNbI8/eLb+QQKBgQCKMyFWxlXvn3npJslLcKwYB5HTXQjDasw4KfbY +JXv8+FXZzJwuqwq7GqDm+u5JzrqTaFYFjONBLWDQ/i8tfd9pWK248FoKmjKBQxW0 +4JjLluFhvkLV6ZX0s3rbpTrruYPvO4CU1iOVCnhgQpDOt4dOMAiH730kY/njK5F9 +Acn0IQKBgQCzZzRFkRDWn8H+79rW32arVSG+RBxrG3gPL4kLla5EHODeHYMlUf5I +qNypqfsw/wGTn9TWYyfx3SB4Of1B68T//BXghXMjm8YCWSGXJytBu/rFMgBpW5da +8OY8LpNaIIpmEKd8esq6B+VtEUEOoSlN1Yv84EG6yjCkHBNqpwqC4g== +-----END RSA PRIVATE KEY----- diff --git a/tests/pub.crt b/tests/pub.crt new file mode 100644 index 0000000..ddb3ea5 --- /dev/null +++ b/tests/pub.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgIEb2ZnLjANBgkqhkiG9w0BAQsFADBbMScwJQYDVQQDDB5SZWdlcnkgU2Vs +Zi1TaWduZWQgQ2VydGlmaWNhdGUxIzAhBgNVBAoMGlJlZ2VyeSwgaHR0cHM6Ly9yZWdlcnkuY29t +MQswCQYDVQQGEwJVQTAgFw0yNTAxMTEwMDAwMDBaGA8yMTI1MDExMTA2MzUxN1owRjESMBAGA1UE +AwwJbG9jYWxob3N0MSMwIQYDVQQKDBpSZWdlcnksIGh0dHBzOi8vcmVnZXJ5LmNvbTELMAkGA1UE +BhMCVUEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa0ZNogSBX8CvmEXmAvrSlQk9h +C5D/yoKsogHrmLYoJN38QgVOaVxdUX52tIrJ8Fo/F57wW67FH2BzDibM+pjBGuoLU498A1gSz5VT +MFhF0jq4GBQVY1W9awOqmYdwZaCJn6D8ZGt8YHA3fGqmT6M+RO5e6cHO1ly9LIYImdumApNsGt2e +JFPopfcrJmbDFKq85ADv55tQ5RKsVj63UF/4FcP9CsQInpFZt12tsUZI8T9AN6LwCU5YtVjxIcy1 +wL/RPg1KidHTJVUng3GRQ7g/6S92upoqRjxRhdXgsta3zQnU+TJiqEariD2c2eCCm4O3heCni4wj +aGxvwfsefcmHAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1Ud +DgQWBBTrZLnKR4WyMdxMIHutC14pjwAfIzAfBgNVHSMEGDAWgBTrZLnKR4WyMdxMIHutC14pjwAf +IzANBgkqhkiG9w0BAQsFAAOCAQEAFxch816B5Y3qNnKSBiynDhFwZJoRGln4bcq/U4IACGczuzbI +3BMtFah6I3FHDF2rrLfzhwnWct5GRqfIQSdqR4jImXbIvDVxJh1EW/5IhnmRGc/xpVr8Pb2CgwU5 +dh2DiIfQzEGId+BuP1M/t55x4rjI1bdRwmb1MdZ4sQbVXX+IY+zQ6rOx+BAJ5ne+TQMJYDQbhc3O +AETJ77uvSaROghKPYZ+S1GKQEXSPI+MgPV2SBRL6x5cCiV+Vwio2ld6v+E+V9m5dkQxv1Z1MMv6Z +vqmLsM3tUE1zB7+vnZcu/rZcm1OkTZ7lQLYm9KQQeeJ9q70SBSwqrLTOd1w5fEkKBw== +-----END CERTIFICATE-----