Skip to content

qiwi/qorsproxy

Repository files navigation

qorsproxy

Cors http(s) proxy for dev any purposes

CI Maintainability Test Coverage npm (tag)

Install

# npm
npm i qorsproxy --save-dev

# yarn
yarn add -D qorsproxy

or just run via npx / npm exec:

npx qorsproxy [options]

Start

qorsproxy -p 8080 -c /Users/a.golub/repo/qorsproxy/config/qorsproxy.dev.qiwi.tools.json

or via runners:

npm start -- --config=path
pm2 start npm --name qorsproxy -- start -- --port=8080 --config=/Users/a.golub/repo/qorsproxy/config/qorsproxy.dev.qiwi.tools.json
npm run start:pm2 -- -- --port=8080
{"message":"[email protected] is loading...","level":"info"}
{"message":"argv={}","level":"info"}
{"message":"Config path=<empty>","level":"info"}
{"message":"Config ready.","level":"info"}
{"message":"Container configured.","level":"info"}
{"message":"Container is online: http://localhost:9292, https://localhost:9293","level":"info"}

Usage

curl 'http://127.0.0.1:9292/http://example.com' -H 'origin:http://localhost'<!doctype html> ...

Configuration

JS/TS API

import {App} from 'qorsproxy'

const options = {
  config: 'path/to/config.json',  // or plain object
  watch: true,                    // if true, config would be reloaded on change
  port: 9292,                     // http port. Optional
  host: 'localhost',              // http host. Optional
}
const orchestrator = App.main(options)

// later
orchestrator.config.update({server: {port: 8080}, ...rest})
// ...
await orchestrator.container.stop()

CLI

  • --host, -h DNS name or IP address. Defaults to localhost.
  • --port, -p defines exposed port. Defaults to 9292.
  • --secure.port, defines exposed secure port. Defaults to 9293.
  • --secure.cert, path to SSL certificate. Defaults to certificate in ssl/cert.pem.
  • --secure.key, path to SSL private key. Defaults to key in ssl/cert.pem.
  • --config, -c sets path to the custom config.
  • --watch, -w if defined sets fs.watchFile interval for config update. If port or host has been changed, the server would be restarted. If config becomes invalid, the prev working version remains applied.

JSON / YAML

At the top level config describes server, log and proxy rules sections.

rules is the main one

It declares allowed connections and their side-effects like mutations, interceptions, customAuthorization and etc. Qorsproxy applies the first matched rule to the request, therefore declaration order matters. rules may be declared as a map:

{
  "rules": {
    "localhost": {
      "from": [
        "*"
      ],
      "to": [
        "example.com"
      ],
      "paths": [
        "/"
      ],
      "mutations": [
        {
          "direction": "to",
          "headers": [
            {
              "name": "origin",
              "value": "localhost"
            }
          ]
        },
        {
          "direction": "from",
          "headers": [
            {
              "name": "set-cookie",
              "value": {
                "from": "/;Domain.+;/",
                "to": ";Domain: foobar.com;"
              }
            }
          ]
        }
      ]
    }
  }
}

array syntax is suitable too:

{
  "rules": [
    {
      "from": [
        "*"
      ],
      "to": [
        "example.com"
      ]
    }
  ]
}

log

Winston is under the hood and you're able to set some parameters:

{
  "log": {
    "dir": "./logs/",
    "filename": "qors-%DATE%.log",
    "datePattern": "YYYY-MM-DD",
    "size": 52428800,
    "level": "info"
  }
}

server

{
  "server": {
    "host": "127.0.0.1",
    "port": 8080,
    "cert": "/path/to/cert.pem", // Defaults to ./ssl/cert.pem
    "key": "/path/to/key.pem"    // and ./ssl/key.pem
  }
}

Pre-flight

If you need support for OPTIONS request, extend target rule:

"interceptions": [
  {
    "req": {
      "method": "OPTIONS"
    },
    "res": {
      "status": 200
    }
  }
],

Authorization

If intermediate authorization is required (change auth for JWT) add customAuthorization to the target rule. See details at schema and impl.

"customAuthorization": {
    "targetUrl": "example.com",
    "authorizationUrl": "example-authorization.com",
    "headers": ["authorization", "cookie"],
    "authPath": "Edge.Headers.Authorization[0]"
}

Cypress

Cypress has a trouble with Transfer-Encoding: chunked header, so in this case you may use a workaround:

{
  "mutations": [
    {
      "direction": "from",
      "headers": [
        {
          "name": "transfer-encoding",
          "value": null
        }
      ]
    }
  ]
}

Monitoring

There are several features to clarify what's going on with proxy.

GET /health

Exposes liveness probe.

{
  "status":"UP",
  "critical":true,
  "deps":{
    "corsproxy":{
      "status":"UP",
      "critical":true
    }
  }
}

GET /metrics

Uptime, CPU and memory usage, request counter:

{
  "process": {
    "uptime": "00:10:29",
    "memory": {"rss": 96956416, "heapTotal": 56356864, "heapUsed": 47617368, "external": 10413906},
    "cpu": {"user": 2229086, "system": 585411}
  },
  "servlets": {
    "corsproxy": {
      "count": 3,
      "traffic": 1270
    }
  }
}

GET /info

Common app info: version, name, etc.

{
  "name": "qorsproxy",
  "version": "1.5.4",
  "description": "Cors proxy for dev purposes",
  "repository": "[email protected]:qiwi/qorsproxy.git"
}

Alternatives

const http = require('http');
http.createServer(handler).listen(3000);

function handler(req, res) {
	console.log('serve: ' + req.url);

	const options = {
		hostname: 'example.com',
		port: 80,
		path: req.url,
		method: req.method
	};

	const proxy = http.request(options, _res => {
		_res.pipe(res, {
			end: true
		});
	});

	req.pipe(proxy, {
		end: true
	});
}

License

MIT