-
Notifications
You must be signed in to change notification settings - Fork 261
Add Tiingo local websocket test #4712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # Tiingo WS Failover — Manual Integration Tests | ||
|
|
||
| These scripts test end-to-end WebSocket failover behaviour by running two local | ||
| proxies between the EA and the real Tiingo upstream, then artificially triggering | ||
| abnormal closures via the proxy control API. | ||
|
|
||
| ## What is tested | ||
|
|
||
| - All four transports subscribe and receive live data (IEX, crypto, crypto-lwba, forex) | ||
| - Abnormal WS closures (code 1006 / TCP terminate) increment the failover counter | ||
| - The 2:1 primary/secondary cycle is respected across 6 rounds of closures: | ||
| - counter=1 → cycle=1 → primary | ||
| - counter=2 → cycle=2 → **SECONDARY** (failover) | ||
| - counter=3 → cycle=0 → primary (failback) | ||
| - counter=4 → cycle=1 → primary | ||
| - counter=5 → cycle=2 → **SECONDARY** (failover again) | ||
| - counter=6 → cycle=0 → primary (failback again) | ||
| - IEX always stays on primary (its URL is hardcoded; it does not participate in failover) | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| 1. Build the Tiingo EA dist: | ||
|
|
||
| ```bash | ||
| yarn workspace @chainlink/tiingo-adapter build | ||
| ``` | ||
|
|
||
| 2. Export your Tiingo API key: | ||
|
|
||
| ```bash | ||
| export TIINGO_API_KEY=<your-key> | ||
| ``` | ||
|
|
||
| 3. `curl`, `python3`, and `npm` must be on your PATH. | ||
| On first run the script installs the `ws` package into `/tmp/tiingo-proxy-modules` | ||
| (outside the repo) so the proxy can run without interfering with Yarn PnP. | ||
|
|
||
| ## Running | ||
|
|
||
| ```bash | ||
| export TIINGO_API_KEY=<your-key> | ||
| bash test/local-ws-test/test-failover.sh | ||
| ``` | ||
|
|
||
| Optional environment overrides: | ||
| | Variable | Default | Description | | ||
| |-----------------------|---------|------------------------------------| | ||
| | `EA_PORT` | 8181 | Port for the local EA HTTP server | | ||
| | `PRIMARY_PORT` | 9001 | Port for the primary WS proxy | | ||
| | `PRIMARY_CTRL` | 9002 | Control HTTP port for primary proxy | | ||
| | `SECONDARY_PORT` | 9003 | Port for the secondary WS proxy | | ||
| | `SECONDARY_CTRL` | 9004 | Control HTTP port for secondary proxy | | ||
| | `PRIMARY_ATTEMPTS` | 2 | Attempts on primary per cycle | | ||
| | `SECONDARY_ATTEMPTS` | 1 | Attempts on secondary per cycle | | ||
|
|
||
| ## Proxy control API | ||
|
|
||
| While the proxy is running you can query and control it directly: | ||
|
|
||
| ```bash | ||
| # List open connections | ||
| curl http://localhost:9002/status | ||
|
|
||
| # Close all connections abruptly (simulates code 1005 / no status received) | ||
| curl -X POST "http://localhost:9002/close?code=1005" | ||
|
|
||
| # Close only the IEX connection | ||
| curl -X POST "http://localhost:9002/close?code=1005&path=/iex" | ||
|
|
||
| # Normal close | ||
| curl -X POST "http://localhost:9002/close?code=1000" | ||
| ``` | ||
|
|
||
| ## Log files | ||
|
|
||
| After a run, full logs are available at: | ||
|
|
||
| - `/tmp/tiingo-ea.log` — EA output | ||
| - `/tmp/proxy-primary.log` — primary proxy | ||
| - `/tmp/proxy-secondary.log` — secondary proxy |
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,151 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env node | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * WebSocket proxy for Tiingo failover testing. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Forwards WS connections from the EA to a real upstream, and exposes an HTTP | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * control server to trigger artificial closes and inspect open connections. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Usage: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * UPSTREAM_WS_URL=wss://api.tiingo.com PROXY_PORT=9001 CONTROL_PORT=9002 node proxy.js | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Control endpoints: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GET http://localhost:$CONTROL_PORT/status – list open connections | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * POST http://localhost:$CONTROL_PORT/close?code=1005 – close all connections | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * POST http://localhost:$CONTROL_PORT/close?code=1005&path=/iex – close by path | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use strict' | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const WebSocket = require('ws') | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const http = require('http') | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = require('url') | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const PROXY_PORT = parseInt(process.env.PROXY_PORT || '9001', 10) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CONTROL_PORT = parseInt(process.env.CONTROL_PORT || String(PROXY_PORT + 1), 10) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const UPSTREAM_WS_URL = process.env.UPSTREAM_WS_URL || 'wss://api.tiingo.com' | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const activePairs = [] // { id, clientWs, upstreamWs, path } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let connectionCounter = 0 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ── WebSocket proxy server ──────────────────────────────────────────────────── | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wss = new WebSocket.Server({ port: PROXY_PORT }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[proxy] Listening on ws://localhost:${PROXY_PORT}`) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[proxy] Forwarding to ${UPSTREAM_WS_URL}`) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wss.on('connection', (clientWs, req) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const id = ++connectionCounter | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = req.url || '' | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const upstreamUrl = `${UPSTREAM_WS_URL}${path}` | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[proxy][${id}] EA connected, opening upstream: ${upstreamUrl}`) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const upstreamWs = new WebSocket(upstreamUrl) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pair = { id, clientWs, upstreamWs, path } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| activePairs.push(pair) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Buffer messages from EA that arrive before upstream is ready | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pendingMessages = [] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upstreamWs.on('open', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `[proxy][${id}] Upstream connected — flushing ${pendingMessages.length} buffered message(s)`, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const msg of pendingMessages) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upstreamWs.send(msg.toString('utf8')) // send as text frame, not binary | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingMessages.length = 0 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upstreamWs.on('message', (data) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientWs.on('message', (data) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (upstreamWs.readyState === WebSocket.OPEN) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upstreamWs.send(data) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingMessages.push(data) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upstreamWs.on('close', (code, reason) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[proxy][${id}] Upstream closed: code=${code} reason=${reason?.toString() || ''}`) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (clientWs.readyState === WebSocket.OPEN) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 1005/1006 cannot be sent in a close frame — terminate the TCP connection instead | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (code === 1005 || code === 1006) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientWs.terminate() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientWs.close(code, reason) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| removePair(id) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upstreamWs.on('error', (err) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[proxy][${id}] Upstream error: ${err.message}`) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientWs.on('close', (code, reason) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[proxy][${id}] EA closed: code=${code} reason=${reason?.toString() || ''}`) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (upstreamWs.readyState === WebSocket.OPEN) upstreamWs.close() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| removePair(id) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientWs.on('error', (err) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[proxy][${id}] EA error: ${err.message}`) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function removePair(id) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const idx = activePairs.findIndex((p) => p.id === id) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (idx !== -1) activePairs.splice(idx, 1) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ── Control HTTP server ─────────────────────────────────────────────────────── | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const controlServer = http.createServer((req, res) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = url.parse(req.url, true) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (req.method === 'GET' && parsed.pathname === '/status') { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(200, { 'Content-Type': 'application/json' }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSON.stringify({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openConnections: activePairs.length, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connections: activePairs.map((p) => ({ id: p.id, path: p.path })), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (req.method === 'POST' && parsed.pathname === '/close') { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const code = parseInt(parsed.query.code || '1005', 10) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const reason = parsed.query.reason || '' | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pathFilter = parsed.query.path || null | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const targets = [...activePairs].filter((p) => !pathFilter || p.path === pathFilter) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `[control] Closing ${targets.length} connection(s) with code=${code}` + | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (pathFilter ? ` path=${pathFilter}` : ''), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+126
to
+127
Check warningCode scanning / CodeQL Log injection Medium test
Log entry depends on a
user-provided value Error loading related location Loading
Copilot AutofixAI about 2 months ago To fix the problem, any user-controlled data included in log messages should be sanitized before logging. For plain text logs, the key step is to remove or neutralize newline characters ( In this file, the only problematic user-controlled value in the highlighted log call is Concretely:
No new external libraries are necessary;
Suggested changeset
1
packages/sources/tiingo/test/local-ws-test/proxy.js
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const pair of targets) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pair.clientWs.readyState === WebSocket.OPEN) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (code === 1005 || code === 1006) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pair.clientWs.terminate() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pair.clientWs.close(code, reason) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(200, { 'Content-Type': 'application/json' }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ closed: targets.length, code, path: pathFilter })) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(404) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end('Not found') | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| controlServer.listen(CONTROL_PORT, () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[control] HTTP on http://localhost:${CONTROL_PORT}`) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warning
Code scanning / CodeQL
Log injection Medium test
Copilot Autofix
AI about 2 months ago
In general, to fix log injection when logging user input, sanitize the data before logging by removing or neutralizing characters that can alter the log structure (such as
\r,\n, and other control characters). For plain text logs, stripping CR/LF is usually sufficient; for HTML logs, HTML-encoding is needed. It is also good practice to clearly delimit or quote user-provided data in the log message.For this specific code, the best minimal fix is to ensure that
upstreamUrlis sanitized before it is interpolated into the log message on line 39. We can do this by creating a sanitized version ofupstreamUrlspecifically for logging, e.g.safeUpstreamUrl, which strips carriage return and newline characters (and optionally other control characters) usingString.prototype.replacewith a regular expression. We should only change the log line (and possibly introduce a one-line local variable) without altering the behavior of the proxy logic: the actualupstreamUrlused to establish the WebSocket connection must remain unchanged. No additional imports are required; we can rely on built-in string methods.Concretely, inside the
wss.on('connection', ...)handler inpackages/sources/tiingo/test/local-ws-test/proxy.js, after computingconst upstreamUrl = \${UPSTREAM_WS_URL}${path}`, we introduceconst safeUpstreamUrl = upstreamUrl.replace(/[\r\n]/g, '')and then change theconsole.logcall to logsafeUpstreamUrlinstead ofupstreamUrl`. This keeps functionality identical (the upstream connection still uses the original URL) while ensuring logs cannot be structurally modified via CR/LF injection.