diff --git a/docs/proxy.md b/docs/proxy.md new file mode 100644 index 00000000000..401c4325379 --- /dev/null +++ b/docs/proxy.md @@ -0,0 +1,123 @@ +# Connecting through a proxy + +Conneting through a proxy is possible by properly configuring the `Client` or `Pool` constructor and request. + +The proxy url should be passed to the `Client` or `Pool` constructor, while the upstream server url +should be added to every request call in the `path`. +For instance, if you need to send a request to the `/hello` route of your upstream server, +the `path` should be `path: 'http://upstream.server:port/hello?foo=bar'`. + +If you proxy requires basic authentication, you can send it via the `proxy-authorization` header. + +### Connect without authentication + +```js +import { Client } from 'undici' +import { createServer } from 'http' +import proxy from 'proxy' + +const server = await buildServer() +const proxy = await buildProxy() + +const serverUrl = `http://localhost:${server.address().port}` +const proxyUrl = `http://localhost:${proxy.address().port}` + +server.on('request', (req, res) => { + console.log(req.url) // '/hello?foo=bar' + res.setHeader('content-type', 'application/json') + res.end(JSON.stringify({ hello: 'world' })) +}) + +const client = new Client(proxyUrl) + +const response = await client.request({ + method: 'GET', + path: serverUrl + '/hello?foo=bar' +}) + +response.body.setEncoding('utf8') +let data = '' +for await (const chunk of response.body) { + data += chunk +} +console.log(response.statusCode) // 200 +console.log(JSON.parse(data)) // { hello: 'world' } + +server.close() +proxy.close() +client.close() + +function buildServer () { + return new Promise((resolve, reject) => { + const server = createServer() + server.listen(0, () => resolve(server)) + }) +} + +function buildProxy () { + return new Promise((resolve, reject) => { + const server = proxy(createServer()) + server.listen(0, () => resolve(server)) + }) +} +``` + +### Connect with authentication + +```js +import { Client } from 'undici' +import { createServer } from 'http' +import proxy from 'proxy' + +const server = await buildServer() +const proxy = await buildProxy() + +const serverUrl = `http://localhost:${server.address().port}` +const proxyUrl = `http://localhost:${proxy.address().port}` + +proxy.authenticate = function (req, fn) { + fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`) +} + +server.on('request', (req, res) => { + console.log(req.url) // '/hello?foo=bar' + res.setHeader('content-type', 'application/json') + res.end(JSON.stringify({ hello: 'world' })) +}) + +const client = new Client(proxyUrl) + +const response = await client.request({ + method: 'GET', + path: serverUrl + '/hello?foo=bar', + headers: { + 'proxy-authorization': `Basic ${Buffer.from('user:pass').toString('base64')}` + } +}) + +response.body.setEncoding('utf8') +let data = '' +for await (const chunk of response.body) { + data += chunk +} +console.log(response.statusCode) // 200 +console.log(JSON.parse(data)) // { hello: 'world' } + +server.close() +proxy.close() +client.close() + +function buildServer () { + return new Promise((resolve, reject) => { + const server = createServer() + server.listen(0, () => resolve(server)) + }) +} + +function buildProxy () { + return new Promise((resolve, reject) => { + const server = proxy(createServer()) + server.listen(0, () => resolve(server)) + }) +} +``` diff --git a/lib/core/request.js b/lib/core/request.js index 8a17bb7560b..b524b86a0be 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -9,10 +9,6 @@ const assert = require('assert') const kHandler = Symbol('handler') -// Borrowed from https://gist.github.com/dperini/729294 -// eslint-disable-next-line no-control-regex -const REGEXP_ABSOLUTE_URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x00a1-\xffff0-9]+-?)*[a-z\x00a1-\xffff0-9]+)(?:\.(?:[a-z\x00a1-\xffff0-9]+-?)*[a-z\x00a1-\xffff0-9]+)*(?:\.(?:[a-z\x00a1-\xffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/ius - class Request { constructor ({ path, @@ -24,7 +20,7 @@ class Request { }, handler) { if (typeof path !== 'string') { throw new InvalidArgumentError('path must be a string') - } else if (path[0] !== '/' && !REGEXP_ABSOLUTE_URL.test(path)) { + } else if (path[0] !== '/' && !(path.startsWith('http://') || path.startsWith('https://'))) { throw new InvalidArgumentError('path must be an absolute URL or start with a slash') } diff --git a/package.json b/package.json index b6e1f042b3c..d53d71a201c 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "https-pem": "^2.0.0", "jest": "^26.4.0", "pre-commit": "^1.2.2", + "proxy": "^1.0.2", "proxyquire": "^2.0.1", "snazzy": "^8.0.0", "standard": "^14.3.4", diff --git a/test/proxy.js b/test/proxy.js new file mode 100644 index 00000000000..ba22e1826f8 --- /dev/null +++ b/test/proxy.js @@ -0,0 +1,132 @@ +'use strict' + +const { test } = require('tap') +const { Client, Pool } = require('..') +const { createServer } = require('http') +const proxy = require('proxy') + +test('connect through proxy', async (t) => { + t.plan(3) + + const server = await buildServer() + const proxy = await buildProxy() + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = `http://localhost:${proxy.address().port}` + + server.on('request', (req, res) => { + t.strictEqual(req.url, '/hello?foo=bar') + res.setHeader('content-type', 'application/json') + res.end(JSON.stringify({ hello: 'world' })) + }) + + const client = new Client(proxyUrl) + + const response = await client.request({ + method: 'GET', + path: serverUrl + '/hello?foo=bar' + }) + + response.body.setEncoding('utf8') + let data = '' + for await (const chunk of response.body) { + data += chunk + } + t.strictEqual(response.statusCode, 200) + t.deepEqual(JSON.parse(data), { hello: 'world' }) + + server.close() + proxy.close() + client.close() +}) + +test('connect through proxy with auth', async (t) => { + t.plan(3) + + const server = await buildServer() + const proxy = await buildProxy() + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = `http://localhost:${proxy.address().port}` + + proxy.authenticate = function (req, fn) { + fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`) + } + + server.on('request', (req, res) => { + t.strictEqual(req.url, '/hello?foo=bar') + res.setHeader('content-type', 'application/json') + res.end(JSON.stringify({ hello: 'world' })) + }) + + const client = new Client(proxyUrl) + + const response = await client.request({ + method: 'GET', + path: serverUrl + '/hello?foo=bar', + headers: { + 'proxy-authorization': `Basic ${Buffer.from('user:pass').toString('base64')}` + } + }) + + response.body.setEncoding('utf8') + let data = '' + for await (const chunk of response.body) { + data += chunk + } + t.strictEqual(response.statusCode, 200) + t.deepEqual(JSON.parse(data), { hello: 'world' }) + + server.close() + proxy.close() + client.close() +}) + +test('connect through proxy (with pool)', async (t) => { + t.plan(3) + + const server = await buildServer() + const proxy = await buildProxy() + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = `http://localhost:${proxy.address().port}` + + server.on('request', (req, res) => { + t.strictEqual(req.url, '/hello?foo=bar') + res.setHeader('content-type', 'application/json') + res.end(JSON.stringify({ hello: 'world' })) + }) + + const pool = new Pool(proxyUrl) + + const response = await pool.request({ + method: 'GET', + path: serverUrl + '/hello?foo=bar' + }) + + response.body.setEncoding('utf8') + let data = '' + for await (const chunk of response.body) { + data += chunk + } + t.strictEqual(response.statusCode, 200) + t.deepEqual(JSON.parse(data), { hello: 'world' }) + + server.close() + proxy.close() + pool.close() +}) + +function buildServer () { + return new Promise((resolve, reject) => { + const server = createServer() + server.listen(0, () => resolve(server)) + }) +} + +function buildProxy () { + return new Promise((resolve, reject) => { + const server = proxy(createServer()) + server.listen(0, () => resolve(server)) + }) +}