Files
worldmonitor/scripts/_proxy-utils.cjs
Elie Habib bf27e474c2 fix(seeder): TLS proxy CONNECT — fixes FRED fetch failures (FSI, yield curve, macro) (#2538)
* fix(seeder): use TLS for proxy CONNECT tunnel to fix FRED fetch failures

Decodo gate.decodo.com:10001 requires TLS. Previous code used http.request
(plain TCP) which received SOCKS5 rejection bytes instead of HTTP 200.

Two issues fixed:
1. Replace http.request CONNECT with tls.connect + manual CONNECT handshake.
   Node.js http.request also auto-sets Host to the proxy hostname; Decodo
   rejects this and responds with SOCKS5 bytes (0x05 0xff). Manual CONNECT
   over a raw TLS socket avoids both issues.
2. Handle https:// and plain "user:pass@host:port" proxy URL formats — always
   uses TLS regardless of PROXY_URL prefix.

_proxy-utils.cjs: resolveProxyStringConnect now preserves https:// prefix
from PROXY_URL so callers can detect TLS proxies explicitly.

All 24 FRED series (BAMLH0A0HYM2, FEDFUNDS, DGS10, etc.) confirmed working
locally via gate.decodo.com:10001.

* fix(seeder): respect http:// proxy scheme + buffer full CONNECT response

Two protocol-correctness fixes:

1. http:// proxies used plain TCP before; always-TLS regressed them.
   Now: bare/undeclared format → TLS (Decodo requires it), explicit
   http:// → plain net.connect, explicit https:// → TLS.

2. CONNECT response buffered until \r\n\r\n instead of acting on the
   first data chunk. Fragmented proxy responses (headers split across
   packets) could corrupt the TLS handshake by leaving header bytes
   on the wire when tls.connect() was called too early.

Verified locally: BAMLH0A0HYM2 → { date: 2026-03-26, value: 3.21 }

* chore(seeder): remove unused http import, fix stale JSDoc

- Drop `import * as http from 'node:http'` — no longer used after
  replacing http.request CONNECT with tls.connect + manual handshake
- Update resolveProxyStringConnect() JSDoc: https.request → tls.connect
2026-03-30 11:06:48 +04:00

97 lines
3.3 KiB
JavaScript

'use strict';
/**
* Shared proxy configuration parser used by ais-relay.cjs and _seed-utils.mjs.
*
* Supported formats for PROXY_URL:
* - http://user:pass@host:port (standard URL)
* - host:port:user:pass (Decodo/Smartproxy)
*
* Returns { host, port, auth: 'user:pass' } or null.
*/
function parseProxyConfig(raw) {
if (!raw) return null;
// Standard URL format: http://user:pass@host:port or https://user:pass@host:port
try {
const u = new URL(raw);
if (u.hostname) {
return {
host: u.hostname,
port: parseInt(u.port, 10),
auth: u.username ? `${decodeURIComponent(u.username)}:${decodeURIComponent(u.password)}` : null,
tls: u.protocol === 'https:',
};
}
} catch { /* fall through */ }
// Froxy/OREF format: user:pass@host:port
if (raw.includes('@')) {
const atIdx = raw.lastIndexOf('@');
const auth = raw.slice(0, atIdx);
const hostPort = raw.slice(atIdx + 1);
const colonIdx = hostPort.lastIndexOf(':');
if (colonIdx !== -1) {
const host = hostPort.slice(0, colonIdx);
const port = parseInt(hostPort.slice(colonIdx + 1), 10);
if (host && port && auth) return { host, port, auth };
}
}
// Decodo/Smartproxy format: host:port:user:pass
const parts = raw.split(':');
if (parts.length >= 4) {
const host = parts[0];
const port = parseInt(parts[1], 10);
const user = parts[2];
const pass = parts.slice(3).join(':');
if (host && port && user) return { host, port, auth: `${user}:${pass}` };
}
return null;
}
/**
* Resolve proxy from PROXY_URL only. Returns { host, port, auth } or null.
* Use this for sources where OREF (IL-exit) proxy must NOT be used (e.g. USNI).
*/
function resolveProxyConfig() {
return parseProxyConfig(process.env.PROXY_URL || '');
}
/**
* Resolve proxy from PROXY_URL with fallback to OREF_PROXY_AUTH.
* Use this for general seeders (fear-greed, disease-outbreaks, etc.).
*/
function resolveProxyConfigWithFallback() {
return parseProxyConfig(process.env.PROXY_URL || process.env.OREF_PROXY_AUTH || '');
}
/**
* Returns proxy as "user:pass@host:port" string for use with curl -x.
* Decodo: gate.decodo.com → us.decodo.com (curl endpoint differs from CONNECT endpoint).
* Returns empty string if no proxy configured.
*/
function resolveProxyString() {
const cfg = resolveProxyConfigWithFallback();
if (!cfg) return '';
const host = cfg.host.replace(/^gate\./, 'us.');
return cfg.auth ? `${cfg.auth}@${host}:${cfg.port}` : `${host}:${cfg.port}`;
}
/**
* Returns proxy as "user:pass@host:port" string for use with HTTP CONNECT tunneling.
* Does NOT replace gate.decodo.com → us.decodo.com; CONNECT endpoint is gate.decodo.com.
* When PROXY_URL uses https:// (TLS proxy), returns "https://user:pass@host:port" so
* httpsProxyFetchJson uses tls.connect to the proxy instead of plain net.connect.
* Returns empty string if no proxy configured.
*/
function resolveProxyStringConnect() {
const cfg = resolveProxyConfigWithFallback();
if (!cfg) return '';
const base = cfg.auth ? `${cfg.auth}@${cfg.host}:${cfg.port}` : `${cfg.host}:${cfg.port}`;
return cfg.tls ? `https://${base}` : base;
}
module.exports = { parseProxyConfig, resolveProxyConfig, resolveProxyConfigWithFallback, resolveProxyString, resolveProxyStringConnect };