Skip to content
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

Added pfx to the "HTTPS options restore" section #1

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/migration-guides.md
Expand Up @@ -75,6 +75,7 @@ To use streams, just call `got.stream(url, options)` or `got(url, {isStream: tru

- The `json` option is not a `boolean`, it's an `Object`. It will be stringified and used as a body.
- The `form` option is an `Object`. It can be a plain object or a [`form-data` instance](https://github.com/sindresorhus/got/#form-data).
- Got will lowercase all custom headers, even if they are specified to not be.
- No `oauth`/`hawk`/`aws`/`httpSignature` option. To sign requests, you need to create a [custom instance](advanced-creation.md#signing-requests).
- No `agentClass`/`agentOptions`/`pool` option.
- No `forever` option. You need to use [forever-agent](https://github.com/request/forever-agent).
Expand Down
103 changes: 81 additions & 22 deletions readme.md
Expand Up @@ -699,8 +699,6 @@ Default: `[]`

Called with [normalized](source/core/index.ts) [request options](#options). Got will make no further changes to the request before it is sent. This is especially useful in conjunction with [`got.extend()`](#instances) when you want to create an API client that, for example, uses HMAC-signing.

See the [AWS section](#aws) for an example.

**Tip:** You can override the `request` function by returning a [`ClientRequest`-like](https://nodejs.org/api/http.html#http_class_http_clientrequest) instance or a [`IncomingMessage`-like](https://nodejs.org/api/http.html#http_class_http_incomingmessage) instance. This is very useful when creating a custom cache mechanism.

###### hooks.beforeRedirect
Expand Down Expand Up @@ -985,7 +983,24 @@ Type: `string`

The passphrase to decrypt the `options.https.key` (if different keys have different passphrases refer to `options.https.key` documentation).

##### Examples for `https.key`, `https.certificate` and `https.passphrase`
##### https.pfx

Type: `string | Buffer | Array<string | Buffer | object>`

[PFX or PKCS12](https://en.wikipedia.org/wiki/PKCS_12) encoded private key and certificate chain. Using `options.https.pfx` is an alternative to providing `options.https.key` and `options.https.certificate` individually. A PFX is usually encrypted, and if it is, `options.https.passphrase` will be used to decrypt it.

Multiple PFX's can be be provided as an array of unencrypted buffers or an array of objects like:

```ts
{
buffer: string | Buffer,
passphrase?: string
}
```

This object form can only occur in an array. If the provided buffers are encrypted, `object.passphrase` can be used to decrypt them. If `object.passphrase` is not provided, `options.https.passphrase` will be used for decryption.

##### Examples for `https.key`, `https.certificate`, `https.passphrase`, and `https.pfx`

```js
// Single key with certificate
Expand Down Expand Up @@ -1032,6 +1047,45 @@ got('https://example.com', {
]
}
});

// Single encrypted PFX with passphrase
got('https://example.com', {
https: {
pfx: fs.readFileSync('./fake.pfx'),
passphrase: 'passphrase'
}
});

// Multiple encrypted PFX's with different passphrases
got('https://example.com', {
https: {
pfx: [
{
buffer: fs.readFileSync('./key1.pfx'),
passphrase: 'passphrase1'
},
{
buffer: fs.readFileSync('./key2.pfx'),
passphrase: 'passphrase2'
}
]
}
});

// Multiple encrypted PFX's with single passphrase
got('https://example.com', {
https: {
passphrase: 'passphrase',
pfx: [
{
buffer: fs.readFileSync('./key1.pfx')
},
{
buffer: fs.readFileSync('./key2.pfx')
}
]
}
});
```

##### https.rejectUnauthorized
Expand Down Expand Up @@ -1750,6 +1804,26 @@ got('https://sindresorhus.com', {
});
```

Otherwise, you can use the [`hpagent`](https://github.com/delvedor/hpagent) package, which keeps the internal sockets alive to be reused.

```js
const got = require('got');
const {HttpsProxyAgent} = require('hpagent');

got('https://sindresorhus.com', {
agent: {
https: new HttpsProxyAgent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 256,
scheduling: 'lifo',
proxy: 'https://localhost:8080'
})
}
});
```

Alternatively, use [`global-agent`](https://github.com/gajus/global-agent) to configure a global proxy for all HTTP/HTTPS traffic in your program.

Read the [`http2-wrapper`](https://github.com/szmarczak/http2-wrapper/#proxy-support) docs to learn about proxying for HTTP/2.
Expand Down Expand Up @@ -1840,29 +1914,14 @@ got('unix:/var/run/docker.sock:/containers/json');

## AWS

Requests to AWS services need to have their headers signed. This can be accomplished by using the [`aws4`](https://www.npmjs.com/package/aws4) package. This is an example for querying an ["API Gateway"](https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/) with a signed request.
Requests to AWS services need to have their headers signed. This can be accomplished by using the [`got4aws`](https://www.npmjs.com/package/got4aws) package. This is an example for querying an ["API Gateway"](https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/) with a signed request.

```js
const got = require('got');
const AWS = require('aws-sdk');
const aws4 = require('aws4');

const chain = new AWS.CredentialProviderChain();
const got4aws = require('got4aws');;

// Create a Got instance to use relative paths and signed requests
const awsClient = got.extend({
prefixUrl: 'https://<api-id>.execute-api.<api-region>.amazonaws.com/<stage>/',
hooks: {
beforeRequest: [
async options => {
const credentials = await chain.resolvePromise();
aws4.sign(options, credentials);
}
]
}
});
const awsClient = got4aws();

const response = await awsClient('endpoint/path', {
const response = await awsClient('https://<api-id>.execute-api.<api-region>.amazonaws.com/<stage>/endpoint/path', {
// Request-specific options
});
```
Expand Down
45 changes: 44 additions & 1 deletion source/core/index.ts
Expand Up @@ -28,6 +28,8 @@ import {DnsLookupIpVersion, isDnsLookupIpVersion, dnsLookupIpVersionToFamily} fr
import deprecationWarning from '../utils/deprecation-warning';
import {PromiseOnly} from '../as-promise/types';

const globalDnsCache = new CacheableLookup();

type HttpRequestFunction = typeof httpRequest;
type Error = NodeJS.ErrnoException;

Expand Down Expand Up @@ -187,6 +189,7 @@ export interface HTTPSOptions {
key?: SecureContextOptions['key'];
certificate?: SecureContextOptions['cert'];
passphrase?: SecureContextOptions['passphrase'];
pfx?: SecureContextOptions['pfx'];
}

interface NormalizedPlainOptions extends PlainOptions {
Expand Down Expand Up @@ -684,6 +687,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
assert.any([is.string, is.object, is.array, is.undefined], options.https.key);
assert.any([is.string, is.object, is.array, is.undefined], options.https.certificate);
assert.any([is.string, is.undefined], options.https.passphrase);
assert.any([is.string, is.buffer, is.array, is.undefined], options.https.pfx);
}

// `options.method`
Expand Down Expand Up @@ -888,7 +892,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {

// `options.dnsCache`
if (options.dnsCache === true) {
options.dnsCache = new CacheableLookup();
options.dnsCache = globalDnsCache;
} else if (!is.undefined(options.dnsCache) && !options.dnsCache.lookup) {
throw new TypeError(`Parameter \`dnsCache\` must be a CacheableLookup instance or a boolean, got ${is(options.dnsCache)}`);
}
Expand Down Expand Up @@ -975,6 +979,10 @@ export default class Request extends Duplex implements RequestEvents<Request> {
deprecationWarning('"options.passphrase" was never documented, please use "options.https.passphrase"');
}

if ('pfx' in options) {
deprecationWarning('"options.pfx" was never documented, please use "options.https.pfx"');
}

// Other options
if ('followRedirects' in options) {
throw new TypeError('The `followRedirects` option does not exist. Use `followRedirect` instead.');
Expand Down Expand Up @@ -1527,6 +1535,10 @@ export default class Request extends Duplex implements RequestEvents<Request> {
if (options.https.passphrase) {
requestOptions.passphrase = options.https.passphrase;
}

if (options.https.pfx) {
requestOptions.pfx = options.https.pfx;
}
}

try {
Expand All @@ -1541,6 +1553,37 @@ export default class Request extends Duplex implements RequestEvents<Request> {
options.timeout = timeout;
options.agent = agent;

// HTTPS options restore
if (options.https) {
if ('rejectUnauthorized' in options.https) {
delete requestOptions.rejectUnauthorized;
}

if (options.https.checkServerIdentity) {
delete requestOptions.checkServerIdentity;
}

if (options.https.certificateAuthority) {
delete requestOptions.ca;
}

if (options.https.certificate) {
delete requestOptions.cert;
}

if (options.https.key) {
delete requestOptions.key;
}

if (options.https.passphrase) {
delete requestOptions.passphrase;
}

if (options.https.pfx) {
delete requestOptions.pfx;
}
}

if (isClientRequest(requestOrResponse)) {
this._onRequest(requestOrResponse);

Expand Down
19 changes: 9 additions & 10 deletions source/create.ts
Expand Up @@ -38,7 +38,7 @@ import {
StreamOptions
} from './types';
import createRejection from './as-promise/create-rejection';
import Request, {kIsNormalizedAlready, setNonEnumerableProperties} from './core';
import Request, {kIsNormalizedAlready, setNonEnumerableProperties, Defaults} from './core';
import deepFreeze from './utils/deep-freeze';

const errors = {
Expand Down Expand Up @@ -114,7 +114,7 @@ const create = (defaults: InstanceDefaults): Got => {
}));

// Got interface
const got: Got = ((url: string | URL, options?: Options): GotReturn => {
const got: Got = ((url: string | URL, options?: Options, _defaults?: Defaults): GotReturn => {
let iteration = 0;
const iterateHandlers = (newOptions: NormalizedOptions): GotReturn => {
return defaults.handlers[iteration++](
Expand Down Expand Up @@ -147,7 +147,7 @@ const create = (defaults: InstanceDefaults): Got => {
}

// Normalize options & call handlers
const normalizedOptions = normalizeArguments(url, options, defaults.options);
const normalizedOptions = normalizeArguments(url, options, _defaults ?? defaults.options);
normalizedOptions[kIsNormalizedAlready] = true;

if (initHookError) {
Expand Down Expand Up @@ -199,7 +199,7 @@ const create = (defaults: InstanceDefaults): Got => {
};

// Pagination
const paginateEach = (async function * <T, R>(url: string | URL, options?: OptionsWithPagination<T, R>) {
const paginateEach = (async function * <T, R>(url: string | URL, options?: OptionsWithPagination<T, R>): AsyncIterableIterator<T> {
// TODO: Remove this `@ts-expect-error` when upgrading to TypeScript 4.
// Error: Argument of type 'Merge<Options, PaginationOptions<T, R>> | undefined' is not assignable to parameter of type 'Options | undefined'.
// @ts-expect-error
Expand All @@ -222,9 +222,10 @@ const create = (defaults: InstanceDefaults): Got => {
await delay(pagination.backoff);
}

// @ts-expect-error FIXME!
// TODO: Throw when result is not an instance of Response
// eslint-disable-next-line no-await-in-loop
const result = (await got(normalizedOptions)) as Response;
const result = (await got(undefined, undefined, normalizedOptions)) as Response;

// eslint-disable-next-line no-await-in-loop
const parsed = await pagination.transform(result);
Expand All @@ -236,7 +237,7 @@ const create = (defaults: InstanceDefaults): Got => {
return;
}

yield item;
yield item as T;

if (pagination.stackAllItems) {
all.push(item as T);
Expand Down Expand Up @@ -266,14 +267,12 @@ const create = (defaults: InstanceDefaults): Got => {
}
});

got.paginate = (<T, R>(url: string | URL, options?: OptionsWithPagination<T, R>) => {
return paginateEach(url, options);
}) as GotPaginate;
got.paginate = paginateEach as GotPaginate;

got.paginate.all = (async <T, R>(url: string | URL, options?: OptionsWithPagination<T, R>) => {
const results: T[] = [];

for await (const item of got.paginate<T, R>(url, options)) {
for await (const item of paginateEach<T, R>(url, options)) {
results.push(item);
}

Expand Down
12 changes: 12 additions & 0 deletions test/create.ts
Expand Up @@ -312,3 +312,15 @@ test('async handlers can throw', async t => {

await t.throwsAsync(instance('https://example.com'), {message});
});

test('setting dnsCache to true points to global cache', t => {
const a = got.extend({
dnsCache: true
});

const b = got.extend({
dnsCache: true
});

t.is(a.defaults.options.dnsCache, b.defaults.options.dnsCache);
});