-
Notifications
You must be signed in to change notification settings - Fork 8.3k
/
proxy_request.ts
108 lines (96 loc) · 3.18 KB
/
proxy_request.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import http from 'http';
import https from 'https';
import net from 'net';
import stream from 'stream';
import Boom from '@hapi/boom';
import { URL } from 'url';
interface Args {
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head';
agent: http.Agent;
uri: URL;
payload: stream.Stream;
timeout: number;
headers: http.OutgoingHttpHeaders;
rejectUnauthorized?: boolean;
}
/**
* Node http request library does not expect there to be trailing "[" or "]"
* characters in ipv6 host names.
*/
const sanitizeHostname = (hostName: string): string =>
hostName.trim().replace(/^\[/, '').replace(/\]$/, '');
// We use a modified version of Hapi's Wreck because Hapi, Axios, and Superagent don't support GET requests
// with bodies, but ES APIs do. Similarly with DELETE requests with bodies. Another library, `request`
// diverged too much from current behaviour.
export const proxyRequest = ({
method,
headers,
agent,
uri,
timeout,
payload,
rejectUnauthorized,
}: Args) => {
const { hostname, port, protocol, pathname, search } = uri;
const client = uri.protocol === 'https:' ? https : http;
let resolved = false;
let resolve: (res: http.IncomingMessage) => void;
let reject: (res: unknown) => void;
const reqPromise = new Promise<http.IncomingMessage>((res, rej) => {
resolve = res;
reject = rej;
});
const finalUserHeaders = { ...headers };
const hasHostHeader = Object.keys(finalUserHeaders).some((key) => key.toLowerCase() === 'host');
if (!hasHostHeader) {
finalUserHeaders.host = hostname;
}
const req = client.request({
method: method.toUpperCase(),
// We support overriding this on a per request basis to support legacy proxy config. See ./proxy_config.
rejectUnauthorized: typeof rejectUnauthorized === 'boolean' ? rejectUnauthorized : undefined,
host: sanitizeHostname(hostname),
port: port === '' ? undefined : parseInt(port, 10),
protocol,
path: `${pathname}${search || ''}`,
headers: {
...finalUserHeaders,
'content-type': 'application/json',
'transfer-encoding': 'chunked',
},
agent,
});
req.once('response', (res) => {
resolved = true;
resolve(res);
});
req.once('socket', (socket: net.Socket) => {
if (!socket.connecting) {
payload.pipe(req);
} else {
socket.once('connect', () => {
payload.pipe(req);
});
}
});
const onError = (e: Error) => reject(e);
req.once('error', onError);
const timeoutPromise = new Promise<any>((timeoutResolve, timeoutReject) => {
setTimeout(() => {
if (!req.aborted && !req.socket) req.abort();
if (!resolved) {
timeoutReject(Boom.gatewayTimeout('Client request timeout'));
} else {
timeoutResolve(undefined);
}
}, timeout);
});
return Promise.race<http.IncomingMessage>([reqPromise, timeoutPromise]);
};