Comparing version 0.2.0 to 0.3.0
@@ -1,2 +0,2 @@ | ||
declare type FETCH_OPT = { | ||
export declare type FETCH_OPT = { | ||
method?: string; | ||
@@ -12,5 +12,10 @@ type?: 'text' | 'json' | 'bytes'; | ||
referrer: boolean; | ||
sslAllowSelfSigned: boolean; | ||
sslPinnedCertificates?: string[]; | ||
_redirectCount: number; | ||
}; | ||
export declare class InvalidCertError extends Error { | ||
readonly fingerprint256: string; | ||
constructor(msg: string, fingerprint256: string); | ||
} | ||
export default function fetchUrl(url: string, options?: FETCH_OPT): Promise<any>; | ||
export {}; |
80
index.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InvalidCertError = void 0; | ||
const DEFAULT_OPT = Object.freeze({ | ||
@@ -11,4 +12,12 @@ redirect: true, | ||
referrer: false, | ||
sslAllowSelfSigned: false, | ||
_redirectCount: 0, | ||
}); | ||
class InvalidCertError extends Error { | ||
constructor(msg, fingerprint256) { | ||
super(msg); | ||
this.fingerprint256 = fingerprint256; | ||
} | ||
} | ||
exports.InvalidCertError = InvalidCertError; | ||
function detectType(b, type) { | ||
@@ -36,3 +45,3 @@ if (!type || type === 'text' || type === 'json') { | ||
} | ||
let _httpAgent, _httpsAgent; | ||
let agents = {}; | ||
function fetchNode(url, options = DEFAULT_OPT) { | ||
@@ -45,7 +54,2 @@ options = { ...DEFAULT_OPT, ...options }; | ||
const { resolve: urlResolve } = require('url'); | ||
const agentOpt = { keepAlive: true, keepAliveMsecs: 30 * 1000, maxFreeSockets: 1024 }; | ||
if (!_httpAgent) | ||
_httpAgent = new http.Agent(agentOpt); | ||
if (!_httpsAgent) | ||
_httpsAgent = new https.Agent(agentOpt); | ||
const isSecure = !!/^https/.test(url); | ||
@@ -56,4 +60,17 @@ let opts = { | ||
}; | ||
if (options.keepAlive) | ||
opts.agent = isSecure ? _httpsAgent : _httpAgent; | ||
const compactFP = (s) => s.replace(/:| /g, '').toLowerCase(); | ||
if (options.keepAlive) { | ||
const agentOpt = { | ||
keepAlive: true, | ||
keepAliveMsecs: 30 * 1000, | ||
maxFreeSockets: 1024, | ||
maxCachedSessions: 1024, | ||
}; | ||
const agentKey = [ | ||
isSecure, | ||
isSecure && options.sslPinnedCertificates?.map((i) => compactFP(i)).sort(), | ||
].join(); | ||
opts.agent = | ||
agents[agentKey] || (agents[agentKey] = new (isSecure ? https : http).Agent(agentOpt)); | ||
} | ||
if (options.type === 'json') | ||
@@ -67,2 +84,4 @@ opts.headers['Content-Type'] = 'application/json'; | ||
opts.headers = { ...opts.headers, ...options.headers }; | ||
if (options.sslAllowSelfSigned) | ||
opts.rejectUnauthorized = false; | ||
const handleRes = async (res) => { | ||
@@ -95,4 +114,17 @@ const status = res.statusCode; | ||
return new Promise((resolve, reject) => { | ||
const handleError = async (err) => { | ||
if (err && err.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { | ||
try { | ||
await fetchNode(url, { ...options, sslAllowSelfSigned: true, sslPinnedCertificates: [] }); | ||
} | ||
catch (e) { | ||
if (e && e.fingerprint256) { | ||
err = new InvalidCertError(`Self-signed SSL certificate: ${e.fingerprint256}`, e.fingerprint256); | ||
} | ||
} | ||
} | ||
reject(err); | ||
}; | ||
const req = (isSecure ? https : http).request(url, opts, (res) => { | ||
res.on('error', reject); | ||
res.on('error', handleError); | ||
(async () => { | ||
@@ -107,2 +139,24 @@ try { | ||
}); | ||
req.on('error', handleError); | ||
const pinned = options.sslPinnedCertificates?.map((i) => compactFP(i)); | ||
const mfetchSecureConnect = (socket) => { | ||
const fp256 = compactFP(socket.getPeerCertificate()?.fingerprint256 || ''); | ||
if (!fp256 && socket.isSessionReused()) | ||
return; | ||
if (pinned.includes(fp256)) | ||
return; | ||
req.emit('error', new InvalidCertError(`Invalid SSL certificate: ${fp256} Expected: ${pinned}`, fp256)); | ||
return req.abort(); | ||
}; | ||
if (options.sslPinnedCertificates) { | ||
req.on('socket', (socket) => { | ||
const hasListeners = socket | ||
.listeners('secureConnect') | ||
.map((i) => (i.name || '').replace('bound ', '')) | ||
.includes('mfetchSecureConnect'); | ||
if (hasListeners) | ||
return; | ||
socket.on('secureConnect', mfetchSecureConnect.bind(null, socket)); | ||
}); | ||
} | ||
if (options.keepAlive) | ||
@@ -124,2 +178,10 @@ req.setNoDelay(true); | ||
headers.set('Content-Type', 'application/json'); | ||
let parsed = new URL(url); | ||
if (parsed.username) { | ||
const auth = btoa(`${parsed.username}:${parsed.password}`); | ||
headers.set('Authorization', `Basic ${auth}`); | ||
parsed.username = ''; | ||
parsed.password = ''; | ||
} | ||
url = '' + parsed; | ||
for (let k in options.headers) { | ||
@@ -126,0 +188,0 @@ const name = k.toLowerCase(); |
{ | ||
"name": "micro-ftch", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Wraps nodejs built-in modules and browser fetch into one function.", | ||
@@ -27,5 +27,5 @@ "main": "index.js", | ||
"devDependencies": { | ||
"@types/node": "^14.0.14", | ||
"typescript": "^4.3.2" | ||
"@types/node": "^16.0", | ||
"typescript": "^4.3.5" | ||
} | ||
} |
@@ -16,4 +16,26 @@ # micro-ftch | ||
## Options | ||
The list of options that can be supplied as second argument to fetch(url, opts): | ||
```typescript | ||
export type FETCH_OPT = { | ||
method?: string; | ||
type?: 'text' | 'json' | 'bytes'; // Response encoding (auto-detect if empty) | ||
redirect: boolean; // Follow redirects | ||
expectStatusCode?: number | false; // Expect this status code | ||
headers: Record<string, string>; | ||
data?: object; // POST/PUT/DELETE request data | ||
full: boolean; // Return full request {headers, status, body} | ||
keepAlive: boolean; // Enable keep-alive (node only) | ||
cors: boolean; // Allow CORS safe-listed headers (browser-only) | ||
referrer: boolean; // Send referrer (browser-only) | ||
sslAllowSelfSigned: boolean; // Allow self-signed ssl certs (node only) | ||
sslPinnedCertificates?: string[]; // Verify fingerprint of certificate (node only) | ||
_redirectCount: number; | ||
}; | ||
``` | ||
## License | ||
MIT License (c) 2020, Paul Miller (https://paulmillr.com) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
11639
230
40