diff --git a/join.js b/join.js index 1b0a43f7..018b192e 100644 --- a/join.js +++ b/join.js @@ -3,7 +3,6 @@ const camp = require('camp'); const http = require('http'); -const nodepath = require('path'); const nodeurl = require('url'); const boot = require('./lib/boot'); @@ -187,7 +186,7 @@ async function ensureOAuth2Access (request, response, next) { } // Proxy a request to a local Docker container. -async function proxyRequest (request, response) { +async function proxyRequest (request, response, head) { const { session } = request; let { container, port } = request.query; if (!container || !port) { @@ -216,7 +215,7 @@ async function proxyRequest (request, response) { // container port is mapped to. This also ensures that the container exists // on this host, and that the authenticated user is allowed to access it. const data = await getMappedPort(oauth2Tokens[session.id], container, port); - routeRequest({ port: data.port, proxy: data.proxy }, request, response); + routeRequest({ port: data.port, proxy: data.proxy }, request, response, head); } catch (error) { log('[fail] getting mapped port', error); response.statusCode = 404; // Not Found @@ -225,10 +224,9 @@ async function proxyRequest (request, response) { } // Route a request to the given port, using the given proxy type. -function routeRequest (proxyParameters, request, response) { - const path = nodepath.normalize(request.url); - if (path[0] !== '/') { - log('[fail] invalid proxy path:', path); +function routeRequest (proxyParameters, request, response, head) { + if (request.url[0] !== '/') { + log('[fail] invalid proxy path:', request.url); response.statusCode = 500; // Internal Server Error response.end(); return; @@ -237,11 +235,17 @@ function routeRequest (proxyParameters, request, response) { const { port, proxy } = proxyParameters; switch (proxy) { case 'https': - routes.webProxy(request, response, { port, path }); + routes.webProxy(request, response, head, { port }); break; case 'none': - routes.redirect(response, 'https://' + hostname + ':' + port + path); + if (!(response instanceof http.ServerResponse)) { + const error = new Error('Unsupported response type (e.g. WebSocket)'); + log('[fail] proxy=none redirect', error); + response.end(); + return; + } + routes.redirect(response, 'https://' + hostname + ':' + port + request.url); break; default: diff --git a/lib/routes.js b/lib/routes.js index e539699b..70814a92 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -3,6 +3,8 @@ const camp = require('camp'); const http = require('http'); +const httpProxy = require('http-proxy'); +const lruCache = require('lru-cache'); const timeago = require('timeago.js'); const configurations = require('./configurations'); @@ -639,51 +641,29 @@ exports.notFoundPageNew = function (response, user) { ]); }; +const proxyCache = lruCache({ + max: 1024, + dispose: (key, item) => item.close(), +}); + // Local web proxy. -exports.webProxy = function (request, response, parameters) { +exports.webProxy = function (request, response, head, parameters) { // Proxy request to the local port and path. - const options = { - hostname: 'localhost', - port: parameters.port, - path: parameters.path, - method: request.method, - headers: request.headers, - }; - const proxy = http.request(options); - - proxy.on('response', res => { - response.writeHead(res.statusCode, res.headers); - res.pipe(response, { end: true }); - }); - - proxy.on('upgrade', (res, socket) => { - // Rebuild the WebSocket handshake reply from `res`. - let head = 'HTTP/1.1 ' + res.statusCode + ' ' + res.statusMessage + '\r\n'; - - res.rawHeaders.forEach((header, i) => { - head += header + (i % 2 ? '\r\n' : ': '); + const cacheKey = JSON.stringify(parameters.port); + let proxy = proxyCache.get(cacheKey); + if (proxy === undefined) { + proxy = httpProxy.createServer({ + target: { + host: 'localhost', + port: parameters.port, + } }); - - response.write(head + '\r\n'); - - // WebSocket handshake complete, the data transfer begins. - socket.pipe(response, { end: true }); - response.pipe(socket, { end: true }); - }); - - proxy.on('error', error => { - if (error) { - log('[fail] could not process the request', error); - } - - response.statusCode = 503; // Service Unavailable - response.end(); - }); - - // If we already consumed some request data, re-send it through the proxy. - if (request.savedChunks) { - proxy.write(request.savedChunks); + proxyCache.set(cacheKey, proxy); } - request.pipe(proxy, { end: true }); + if (response instanceof http.ServerResponse) { + proxy.web(request, response); + } else { + proxy.ws(request, response, head); + } }; diff --git a/package.json b/package.json index 79796e64..1084971f 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,9 @@ "email-login": "1.3.1", "fast-json-patch": "2.0.6", "fleau": "16.2.0", + "http-proxy": "^1.16.2", "le-acme-core": "2.1.1", + "lru-cache": "^4.1.2", "ms-rest-azure": "^2.5.2", "node-forge": "0.7.4", "oauth": "0.9.15",