diff --git a/bin/http-server b/bin/http-server index 5de9deab..db2d1fb9 100755 --- a/bin/http-server +++ b/bin/http-server @@ -59,6 +59,7 @@ if (argv.h || argv.help) { '', ' -P --proxy Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com', ' --proxy-options Pass options to proxy using nested dotted objects. e.g.: --proxy-options.secure false', + ' --websocket Enable websocket proxy', '', ' --username Username for basic authentication [none]', ' Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME', @@ -84,6 +85,7 @@ var port = argv.p || argv.port || parseInt(process.env.PORT, 10), sslPassphrase = process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE, proxy = argv.P || argv.proxy, proxyOptions = argv['proxy-options'], + websocket = argv.websocket, utc = argv.U || argv.utc, version = argv.v || argv.version, baseDir = argv['base-dir'], @@ -181,6 +183,14 @@ function listen(port) { } } + if (websocket) { + if (!proxy) { + logger.warning(colors.yellow('WebSocket proxy will not be enabled because proxy is not enabled')); + } else { + options.websocket = true; + } + } + if (argv.cors) { options.cors = true; if (typeof argv.cors === 'string') { diff --git a/lib/http-server.js b/lib/http-server.js index d40b6094..7e4e86ae 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -147,12 +147,13 @@ function HttpServer(options) { if (typeof options.proxy === 'string') { var proxyOptions = options.proxyOptions || {}; - var proxy = httpProxy.createProxyServer(proxyOptions); + var proxy = httpProxy.createProxyServer({ + ...proxyOptions, + target: options.proxy, + changeOrigin: true, + }); before.push(function (req, res) { - proxy.web(req, res, { - target: options.proxy, - changeOrigin: true - }, function (err, req, res) { + proxy.web(req, res, {}, function (err, req, res) { if (options.logFn) { options.logFn(req, res, { message: err.message, @@ -188,7 +189,7 @@ function HttpServer(options) { this.server.setTimeout(options.timeout); } - if (typeof options.proxy === 'string') { + if (typeof options.proxy === 'string' && options.websocket) { this.server.on('upgrade', function (request, socket, head) { proxy.ws(request, socket, head, { target: options.proxy, diff --git a/test/websocket-proxy.test.js b/test/websocket-proxy.test.js new file mode 100644 index 00000000..bf651304 --- /dev/null +++ b/test/websocket-proxy.test.js @@ -0,0 +1,214 @@ +const test = require('tap').test +const path = require('path') +const http = require('http') +const httpServer = require('../lib/http-server') +const WebSocket = require('ws') + +// Prevent errors from being swallowed +process.on('uncaughtException', console.error) + +test('websocket proxy functionality', (t) => { + new Promise((resolve) => { + // Create a target server that will handle websocket connections + const targetServer = http.createServer() + const targetWss = new WebSocket.Server({ server: targetServer }) + + targetWss.on('connection', (ws) => { + ws.on('message', (message) => { + // Echo the message back + ws.send(`Echo: ${message}`) + }) + }) + + targetServer.listen(0, () => { + const targetPort = targetServer.address().port + const targetUrl = `http://localhost:${targetPort}` + + // Create http-server with websocket proxy enabled + const proxyServer = httpServer.createServer({ + proxy: targetUrl, + websocket: true, + root: path.join(__dirname, 'fixtures') + }) + + proxyServer.listen(0, async () => { + const proxyPort = proxyServer.server.address().port + const proxyUrl = `http://localhost:${proxyPort}` + + try { + // Test 1: Verify websocket proxy is enabled when both proxy and websocket options are set + t.ok(proxyServer.server.listeners('upgrade').length > 0, 'upgrade event listener should be registered') + + // Test 2: Test websocket connection through proxy + await new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://localhost:${proxyPort}`) + + ws.on('open', () => { + t.pass('websocket connection should be established through proxy') + + // Send a test message + ws.send('Hello WebSocket!') + }) + + ws.on('message', (data) => { + t.equal(data.toString(), 'Echo: Hello WebSocket!', 'should receive echoed message') + ws.close() + }) + + ws.on('close', () => { + t.pass('websocket connection should close properly') + resolve() + }) + + ws.on('error', (err) => { + t.fail(`websocket error: ${err.message}`) + reject(err) + }) + + // Set timeout to prevent hanging + setTimeout(() => { + ws.close() + reject(new Error('WebSocket test timeout')) + }, 5000) + }) + + } catch (err) { + t.fail(`websocket proxy test failed: ${err.message}`) + } finally { + proxyServer.close() + targetServer.close() + resolve() + } + }) + }) + }) + .then(() => t.end()) + .catch(err => { + t.fail(err.toString()) + t.end() + }) +}) + +test('websocket proxy without proxy configuration', (t) => { + new Promise((resolve) => { + // Create http-server with websocket enabled but no proxy + const server = httpServer.createServer({ + websocket: true, + root: path.join(__dirname, 'fixtures') + }) + + server.listen(0, () => { + try { + // Test: Verify no upgrade event listener is registered when proxy is not set + t.equal(server.server.listeners('upgrade').length, 0, 'no upgrade event listener should be registered when proxy is not set') + t.pass('websocket option should be ignored when proxy is not configured') + } catch (err) { + t.fail(`test failed: ${err.message}`) + } finally { + server.close() + resolve() + } + }) + }) + .then(() => t.end()) + .catch(err => { + t.fail(err.toString()) + t.end() + }) +}) + +test('ensure websocket proxy is not enabled when \'websocket\' is not set', (t) => { + new Promise((resolve) => { + // Create a target server that will handle websocket connections + const targetServer = http.createServer() + const targetWss = new WebSocket.Server({ server: targetServer }) + + targetWss.on('connection', (ws) => { + ws.on('message', (message) => { + // Echo the message back + ws.send(`Echo: ${message}`) + }) + }) + + targetServer.listen(0, () => { + const targetPort = targetServer.address().port + const targetUrl = `http://localhost:${targetPort}` + + const proxyServer = httpServer.createServer({ + proxy: targetUrl, + root: path.join(__dirname, 'fixtures') + }) + try { + t.equal(proxyServer.server.listeners('upgrade').length, 0, 'no upgrade event listener should be registered when websocket is not set') + } catch (err) { + t.fail(`test failed: ${err.message}`) + } finally { + proxyServer.close() + targetServer.close() + resolve() + } + }) + }) + .then(() => t.end()) + .catch(err => { + t.fail(err.toString()) + t.end() + }) +}); + +test('websocket proxy error handling', (t) => { + new Promise((resolve) => { + // Create http-server with invalid proxy target + const proxyServer = httpServer.createServer({ + proxy: 'http://localhost:99999', // Invalid port + websocket: true, + root: path.join(__dirname, 'fixtures') + }) + + proxyServer.listen(0, async () => { + const proxyPort = proxyServer.server.address().port + + try { + // Test: Verify websocket proxy handles connection errors gracefully + t.ok(proxyServer.server.listeners('upgrade').length > 0, 'upgrade event listener should be registered even with invalid proxy') + + // Test websocket connection to invalid proxy target + await new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://localhost:${proxyPort}`) + + ws.on('open', () => { + t.fail('websocket should not connect to invalid proxy target') + ws.close() + resolve() + }) + + ws.on('error', (err) => { + t.pass('websocket should error when proxy target is invalid') + resolve() // This is expected + }) + + ws.on('close', () => { + t.pass('websocket should close on error') + resolve() + }) + + setTimeout(() => { + ws.close() + resolve() // Timeout is acceptable for this test + }, 2000) + }) + + } catch (err) { + t.fail(`websocket proxy error handling test failed: ${err.message}`) + } finally { + proxyServer.close() + resolve() + } + }) + }) + .then(() => t.end()) + .catch(err => { + t.fail(err.toString()) + t.end() + }) +}) \ No newline at end of file