mirror of
https://github.com/danbulant/jose
synced 2026-05-24 12:35:36 +00:00
fix: node runtime json fetch handles connection errors properly
This commit is contained in:
parent
c037e5907c
commit
fc584b2efd
2 changed files with 72 additions and 24 deletions
|
|
@ -1,5 +1,8 @@
|
||||||
import { get as http, ClientRequest } from 'http'
|
import { get as http } from 'http'
|
||||||
import { get as https, RequestOptions } from 'https'
|
import { get as https } from 'https'
|
||||||
|
import { once } from 'events'
|
||||||
|
import type { ClientRequest, IncomingMessage } from 'http'
|
||||||
|
import type { RequestOptions } from 'https'
|
||||||
|
|
||||||
import type { FetchFunction } from '../interfaces.d'
|
import type { FetchFunction } from '../interfaces.d'
|
||||||
import { JOSEError } from '../../util/errors.js'
|
import { JOSEError } from '../../util/errors.js'
|
||||||
|
|
@ -16,12 +19,20 @@ const fetch: FetchFunction = async (url: URL, timeout: number, options: Accepted
|
||||||
if (protocols[url.protocol] === undefined) {
|
if (protocols[url.protocol] === undefined) {
|
||||||
throw new TypeError('Unsupported URL protocol.')
|
throw new TypeError('Unsupported URL protocol.')
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const { agent } = options
|
const { agent } = options
|
||||||
protocols[url.protocol](url.href, { agent, timeout }, async (response) => {
|
const req = protocols[url.protocol](url.href, {
|
||||||
|
agent,
|
||||||
|
timeout,
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/keyword-spacing
|
||||||
|
const [response] = <[IncomingMessage]>await once(req, 'response')
|
||||||
|
|
||||||
if (response.statusCode !== 200) {
|
if (response.statusCode !== 200) {
|
||||||
reject(new JOSEError('Expected 200 OK from the JSON Web Key Set HTTP response'))
|
throw new JOSEError('Expected 200 OK from the JSON Web Key Set HTTP response')
|
||||||
} else {
|
}
|
||||||
|
|
||||||
const parts = []
|
const parts = []
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for await (const part of response) {
|
for await (const part of response) {
|
||||||
|
|
@ -29,13 +40,10 @@ const fetch: FetchFunction = async (url: URL, timeout: number, options: Accepted
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resolve(JSON.parse(decoder.decode(concat(...parts))))
|
return JSON.parse(decoder.decode(concat(...parts)))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(new JOSEError('Failed to parse the JSON Web Key Set HTTP response as JSON'))
|
throw new JOSEError('Failed to parse the JSON Web Key Set HTTP response as JSON')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).on('error', reject)
|
|
||||||
}) as Promise<any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetch
|
export default fetch
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
import timekeeper from 'timekeeper';
|
import timekeeper from 'timekeeper';
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import { once } from 'events';
|
||||||
|
|
||||||
let root;
|
let root;
|
||||||
let keyRoot;
|
let keyRoot;
|
||||||
|
|
@ -29,16 +31,25 @@ Promise.all([
|
||||||
]) => {
|
]) => {
|
||||||
const now = 1604416038;
|
const now = 1604416038;
|
||||||
|
|
||||||
test.beforeEach(() => {
|
test.before(async (t) => {
|
||||||
timekeeper.freeze(now * 1000);
|
nock.disableNetConnect();
|
||||||
|
t.context.server = createServer().listen(3000);
|
||||||
|
t.context.server.removeAllListeners('request');
|
||||||
|
await once(t.context.server, 'listening');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async (t) => {
|
||||||
|
nock.enableNetConnect();
|
||||||
|
await new Promise((resolve) => t.context.server.close(resolve));
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach((t) => {
|
test.afterEach((t) => {
|
||||||
|
t.context.server.removeAllListeners('request');
|
||||||
t.true(nock.isDone());
|
t.true(nock.isDone());
|
||||||
nock.cleanAll();
|
nock.cleanAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach(timekeeper.reset);
|
test.afterEach(() => timekeeper.reset());
|
||||||
|
|
||||||
test.serial('RemoteJWKSet', async (t) => {
|
test.serial('RemoteJWKSet', async (t) => {
|
||||||
const keys = [
|
const keys = [
|
||||||
|
|
@ -174,6 +185,7 @@ Promise.all([
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('refreshes the JWKS once off cooldown', async (t) => {
|
test.serial('refreshes the JWKS once off cooldown', async (t) => {
|
||||||
|
timekeeper.freeze(now * 1000);
|
||||||
let jwk = {
|
let jwk = {
|
||||||
crv: 'P-256',
|
crv: 'P-256',
|
||||||
x: 'fqCXPnWs3sSfwztvwYU9SthmRdoT4WCXxS8eD8icF6U',
|
x: 'fqCXPnWs3sSfwztvwYU9SthmRdoT4WCXxS8eD8icF6U',
|
||||||
|
|
@ -263,6 +275,34 @@ Promise.all([
|
||||||
message: 'Failed to parse the JSON Web Key Set HTTP response as JSON',
|
message: 'Failed to parse the JSON Web Key Set HTTP response as JSON',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.serial('handles ENOTFOUND', async (t) => {
|
||||||
|
nock.enableNetConnect();
|
||||||
|
const url = new URL('https://op.example.com/jwks');
|
||||||
|
const JWKS = createRemoteJWKSet(url);
|
||||||
|
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||||
|
code: 'ENOTFOUND',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('handles ECONNREFUSED', async (t) => {
|
||||||
|
const url = new URL('http://localhost:3001/jwks');
|
||||||
|
const JWKS = createRemoteJWKSet(url);
|
||||||
|
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||||
|
code: 'ECONNREFUSED',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('handles ECONNRESET', async (t) => {
|
||||||
|
const url = new URL('http://localhost:3000/jwks');
|
||||||
|
t.context.server.once('connection', (socket) => {
|
||||||
|
socket.destroy();
|
||||||
|
});
|
||||||
|
const JWKS = createRemoteJWKSet(url);
|
||||||
|
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||||
|
code: 'ECONNRESET',
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
test.serial('failed to import', (t) => {
|
test.serial('failed to import', (t) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue