fix: fetchT() now handles self-signed HTTPS certs
- Node.js native fetch() (undici) cannot use rejectUnauthorized:false - Added _httpsFetch() using raw https module for internal .sami endpoints - Fixes quality profile fetch for Sonarr/Radarr (was returning "fetch failed") - Also fixed corrupted proces..._KEY -> process.env.PYLON_KEY in pylon Coderbot fix #1
This commit is contained in:
@@ -2,11 +2,26 @@
|
|||||||
* HTTP utilities - Fetch helpers and HTTP operations
|
* HTTP utilities - Fetch helpers and HTTP operations
|
||||||
*/
|
*/
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
const { TIMEOUTS } = require('../../constants');
|
const { TIMEOUTS } = require('../../constants');
|
||||||
|
|
||||||
|
// HTTPS agent that trusts internal CA certs (self-signed .sami TLD etc.)
|
||||||
|
// Lazy-initialized singleton to avoid creating a new agent per request.
|
||||||
|
let _internalHttpsAgent;
|
||||||
|
function getInternalHttpsAgent() {
|
||||||
|
if (!_internalHttpsAgent) {
|
||||||
|
_internalHttpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
}
|
||||||
|
return _internalHttpsAgent;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch with automatic timeout
|
* Fetch with automatic timeout
|
||||||
* Drop-in replacement for fetch() with AbortSignal timeout
|
* Drop-in replacement for fetch() with AbortSignal timeout
|
||||||
|
*
|
||||||
|
* Handles two cases where native fetch() doesn't work:
|
||||||
|
* 1. Caddy admin API (:2019) - rejects undici fetch
|
||||||
|
* 2. HTTPS with self-signed certs (.sami TLD) - undici can't use Node's tls agent
|
||||||
*/
|
*/
|
||||||
function fetchT(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
function fetchT(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
||||||
// Caddy admin API rejects Node.js undici fetch - use raw http.request
|
// Caddy admin API rejects Node.js undici fetch - use raw http.request
|
||||||
@@ -14,6 +29,12 @@ function fetchT(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
|||||||
return _httpFetch(url, opts, timeoutMs);
|
return _httpFetch(url, opts, timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPS with self-signed certs: use raw https.request with rejectUnauthorized:false
|
||||||
|
// Node.js native fetch() (undici) ignores the `agent` option and always validates certs.
|
||||||
|
if (url.startsWith('https://')) {
|
||||||
|
return _httpsFetch(url, opts, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
if (!opts.signal) {
|
if (!opts.signal) {
|
||||||
opts = { ...opts, signal: AbortSignal.timeout(timeoutMs) };
|
opts = { ...opts, signal: AbortSignal.timeout(timeoutMs) };
|
||||||
}
|
}
|
||||||
@@ -21,6 +42,65 @@ function fetchT(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
|||||||
return fetch(url, opts);
|
return fetch(url, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw https.request wrapper for self-signed cert support
|
||||||
|
* Node.js native fetch() (undici) cannot be configured with rejectUnauthorized:false,
|
||||||
|
* so we use the standard https module for internal HTTPS endpoints.
|
||||||
|
*/
|
||||||
|
function _httpsFetch(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const options = {
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || 443,
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method: (opts.method || 'GET').toUpperCase(),
|
||||||
|
headers: { ...opts.headers },
|
||||||
|
timeout: timeoutMs,
|
||||||
|
agent: getInternalHttpsAgent(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.body && !options.headers['Content-Length']) {
|
||||||
|
options.headers['Content-Length'] = Buffer.byteLength(opts.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
let size = 0;
|
||||||
|
|
||||||
|
res.on('data', chunk => {
|
||||||
|
size += chunk.length;
|
||||||
|
if (size > MAX_RESPONSE_SIZE) {
|
||||||
|
res.destroy();
|
||||||
|
reject(new Error(`Response from ${url} exceeded ${MAX_RESPONSE_SIZE} bytes`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve({
|
||||||
|
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||||
|
status: res.statusCode,
|
||||||
|
statusText: res.statusMessage,
|
||||||
|
json: () => Promise.resolve(JSON.parse(data)),
|
||||||
|
text: () => Promise.resolve(data),
|
||||||
|
headers: { get: (k) => res.headers[k.toLowerCase()] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error(`Request to ${url} timed out after ${timeoutMs}ms`));
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
if (opts.body) req.write(opts.body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw http.request wrapper for Caddy admin API
|
* Raw http.request wrapper for Caddy admin API
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user