@api-client/core 0.3.2 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/browser.d.ts +1 -0
- package/build/browser.js +1 -0
- package/build/browser.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/src/models/ErrorResponse.d.ts +5 -4
- package/build/src/models/ErrorResponse.js +18 -5
- package/build/src/models/ErrorResponse.js.map +1 -1
- package/build/src/models/SerializableError.d.ts +30 -0
- package/build/src/models/SerializableError.js +63 -0
- package/build/src/models/SerializableError.js.map +1 -0
- package/build/src/runtime/http-engine/ArcEngine.js +8 -4
- package/build/src/runtime/http-engine/ArcEngine.js.map +1 -1
- package/build/src/runtime/http-engine/HttpEngine.d.ts +3 -3
- package/build/src/runtime/http-engine/HttpEngine.js +3 -3
- package/build/src/runtime/http-engine/HttpEngine.js.map +1 -1
- package/build/src/runtime/http-engine/NodeEngine.js +9 -4
- package/build/src/runtime/http-engine/NodeEngine.js.map +1 -1
- package/build/src/runtime/http-engine/NodeEngineDirect.js +8 -2
- package/build/src/runtime/http-engine/NodeEngineDirect.js.map +1 -1
- package/build/src/runtime/node/ProjectRunner.d.ts +2 -2
- package/build/src/runtime/node/ProjectRunner.js +9 -2
- package/build/src/runtime/node/ProjectRunner.js.map +1 -1
- package/build/src/runtime/store/StoreSdk.js +10 -15
- package/build/src/runtime/store/StoreSdk.js.map +1 -1
- package/package.json +1 -1
- package/src/data/DataReader.ts +11 -0
- package/src/data/DataUtils.ts +108 -0
- package/src/data/JmesparthReader.ts +26 -0
- package/src/data/Json2Xml.ts +190 -0
- package/src/data/JsonReader.ts +41 -0
- package/src/data/PayloadPointer.ts +48 -0
- package/src/data/RequestDataExtractor.ts +133 -0
- package/src/data/UrlEncodedReader.ts +20 -0
- package/src/data/XmlReader.ts +103 -0
- package/src/events/BaseEvents.ts +259 -0
- package/src/events/CustomEvent.ts +27 -0
- package/src/events/EventTypes.ts +19 -0
- package/src/events/Events.ts +19 -0
- package/src/events/authorization/AuthorizationEventTypes.ts +22 -0
- package/src/events/authorization/AuthorizationEvents.ts +61 -0
- package/src/events/cookies/CookieEventTypes.ts +13 -0
- package/src/events/cookies/CookieEvents.ts +157 -0
- package/src/events/encryption/EncryptionEventTypes.ts +4 -0
- package/src/events/encryption/EncryptionEvents.ts +51 -0
- package/src/events/environment/EnvironmentEventTypes.ts +3 -0
- package/src/events/environment/EnvironmentEvents.ts +24 -0
- package/src/events/models/ClientCertificateEvents.ts +87 -0
- package/src/events/models/ModelEventTypes.ts +47 -0
- package/src/events/models/ModelEvents.ts +7 -0
- package/src/events/models/ProjectEvents.ts +331 -0
- package/src/events/process/ProcessEventTypes.ts +5 -0
- package/src/events/process/ProcessEvents.ts +76 -0
- package/src/events/readme.md +22 -0
- package/src/events/reporting/ReportingEventTypes.ts +3 -0
- package/src/events/reporting/ReportingEvents.ts +28 -0
- package/src/events/telemetry/TelemetryEventTypes.ts +10 -0
- package/src/events/telemetry/TelemetryEvents.ts +156 -0
- package/src/lib/cookies/Cookie.ts +312 -0
- package/src/lib/cookies/Cookies.ts +326 -0
- package/src/lib/cookies/Utils.ts +168 -0
- package/src/lib/headers/Headers.ts +219 -0
- package/src/lib/logging/DefaultLogger.ts +19 -0
- package/src/lib/logging/DummyLogger.ts +21 -0
- package/src/lib/logging/Logger.ts +16 -0
- package/src/lib/transformers/PayloadSerializer.ts +332 -0
- package/src/lib/transformers/Utils.ts +18 -0
- package/src/lib/uuid.ts +40 -0
- package/src/mocking/LegacyInterfaces.ts +52 -0
- package/src/mocking/LegacyMock.ts +37 -0
- package/src/mocking/legacy/Authorization.ts +39 -0
- package/src/mocking/legacy/Certificates.ts +145 -0
- package/src/mocking/legacy/Cookies.ts +51 -0
- package/src/mocking/legacy/HostRules.ts +43 -0
- package/src/mocking/legacy/Http.ts +236 -0
- package/src/mocking/legacy/HttpResponse.ts +106 -0
- package/src/mocking/legacy/RestApi.ts +68 -0
- package/src/mocking/legacy/Urls.ts +44 -0
- package/src/mocking/legacy/Variables.ts +53 -0
- package/src/models/ArcResponse.ts +166 -0
- package/src/models/Authorization.ts +481 -0
- package/src/models/AuthorizationData.ts +60 -0
- package/src/models/Backend.ts +107 -0
- package/src/models/ClientCertificate.ts +68 -0
- package/src/models/Environment.ts +279 -0
- package/src/models/ErrorResponse.ts +113 -0
- package/src/models/HistoryIndex.ts +76 -0
- package/src/models/HistoryRequest.ts +28 -0
- package/src/models/HostRule.ts +163 -0
- package/src/models/HttpCookie.ts +285 -0
- package/src/models/HttpProject.ts +1294 -0
- package/src/models/HttpProjectListItem.ts +23 -0
- package/src/models/HttpRequest.ts +124 -0
- package/src/models/HttpResponse.ts +143 -0
- package/src/models/License.ts +113 -0
- package/src/models/ProjectDefinitionProperty.ts +40 -0
- package/src/models/ProjectFolder.ts +439 -0
- package/src/models/ProjectItem.ts +135 -0
- package/src/models/ProjectParent.ts +113 -0
- package/src/models/ProjectRequest.ts +277 -0
- package/src/models/ProjectSchema.ts +202 -0
- package/src/models/Property.ts +423 -0
- package/src/models/Provider.ts +98 -0
- package/src/models/README.md +20 -0
- package/src/models/Request.ts +452 -0
- package/src/models/RequestActions.ts +163 -0
- package/src/models/RequestAuthorization.ts +115 -0
- package/src/models/RequestConfig.ts +317 -0
- package/src/models/RequestLog.ts +159 -0
- package/src/models/RequestTime.ts +108 -0
- package/src/models/RequestUiMeta.ts +258 -0
- package/src/models/RequestsSize.ts +65 -0
- package/src/models/ResponseAuthorization.ts +104 -0
- package/src/models/ResponseRedirect.ts +158 -0
- package/src/models/RevisionInfo.ts +37 -0
- package/src/models/SentRequest.ts +125 -0
- package/src/models/SerializableError.ts +80 -0
- package/src/models/SerializablePayload.ts +68 -0
- package/src/models/Server.ts +153 -0
- package/src/models/Thing.ts +110 -0
- package/src/models/Url.ts +90 -0
- package/src/models/User.ts +120 -0
- package/src/models/WebApi.ts +234 -0
- package/src/models/WebApiIndex.ts +122 -0
- package/src/models/Workspace.ts +182 -0
- package/src/models/actions/Action.ts +213 -0
- package/src/models/actions/ActionView.ts +40 -0
- package/src/models/actions/Condition.ts +207 -0
- package/src/models/actions/ConditionView.ts +42 -0
- package/src/models/actions/Enums.ts +29 -0
- package/src/models/actions/RunnableAction.ts +144 -0
- package/src/models/actions/runnable/DeleteCookieAction.ts +113 -0
- package/src/models/actions/runnable/Runnable.ts +9 -0
- package/src/models/actions/runnable/SetCookieAction.ts +216 -0
- package/src/models/actions/runnable/SetVariableAction.ts +81 -0
- package/src/models/legacy/DataExport.ts +172 -0
- package/src/models/legacy/Normalizer.ts +110 -0
- package/src/models/legacy/actions/Actions.ts +269 -0
- package/src/models/legacy/authorization/Authorization.ts +572 -0
- package/src/models/legacy/models/ApiTypes.ts +202 -0
- package/src/models/legacy/models/ArcLegacyProject.ts +39 -0
- package/src/models/legacy/models/AuthData.ts +17 -0
- package/src/models/legacy/models/ClientCertificate.ts +95 -0
- package/src/models/legacy/models/Cookies.ts +52 -0
- package/src/models/legacy/models/HostRule.ts +35 -0
- package/src/models/legacy/models/RestApi.ts +49 -0
- package/src/models/legacy/models/UrlHistory.ts +37 -0
- package/src/models/legacy/models/Variable.ts +43 -0
- package/src/models/legacy/models/base.d.ts +95 -0
- package/src/models/legacy/request/ArcRequest.ts +405 -0
- package/src/models/legacy/request/ArcResponse.ts +177 -0
- package/src/models/legacy/request/HistoryData.ts +47 -0
- package/src/models/legacy/request/Legacy.ts +45 -0
- package/src/models/legacy/request/RequestBody.ts +87 -0
- package/src/models/transformers/ArcDexieTransformer.ts +323 -0
- package/src/models/transformers/ArcLegacyNormalizer.ts +85 -0
- package/src/models/transformers/ArcLegacyTransformer.ts +200 -0
- package/src/models/transformers/ArcPouchTransformer.ts +184 -0
- package/src/models/transformers/BaseTransformer.ts +116 -0
- package/src/models/transformers/ImportUtils.ts +141 -0
- package/src/models/transformers/LegacyDataExportToApiProject.ts +76 -0
- package/src/models/transformers/LegacyExportProcessor.ts +252 -0
- package/src/models/transformers/PostmanBackupTransformer.ts +306 -0
- package/src/models/transformers/PostmanDataTransformer.ts +50 -0
- package/src/models/transformers/PostmanTransformer.ts +106 -0
- package/src/models/transformers/PostmanV21Transformer.ts +311 -0
- package/src/models/transformers/PostmanV2Transformer.ts +308 -0
- package/src/models/transformers/har.ts +865 -0
- package/src/runtime/actions/ActionRunner.ts +83 -0
- package/src/runtime/actions/ConditionRunner.ts +194 -0
- package/src/runtime/actions/RunnableCondition.ts +57 -0
- package/src/runtime/actions/runnable/ActionRunnable.ts +19 -0
- package/src/runtime/actions/runnable/DeleteCookieRunnable.ts +39 -0
- package/src/runtime/actions/runnable/SetCookieRunnable.ts +92 -0
- package/src/runtime/actions/runnable/SetVariableRunnable.ts +53 -0
- package/src/runtime/http-engine/ArcEngine.ts +1068 -0
- package/src/runtime/http-engine/FormData.ts +85 -0
- package/src/runtime/http-engine/HttpEngine.ts +874 -0
- package/src/runtime/http-engine/HttpErrorCodes.ts +270 -0
- package/src/runtime/http-engine/NodeEngine.ts +792 -0
- package/src/runtime/http-engine/NodeEngineDirect.ts +482 -0
- package/src/runtime/http-engine/PayloadSupport.ts +84 -0
- package/src/runtime/http-engine/RequestUtils.ts +164 -0
- package/src/runtime/http-engine/ntlm/Des.ts +345 -0
- package/src/runtime/http-engine/ntlm/MD4.ts +135 -0
- package/src/runtime/http-engine/ntlm/NtlmAuth.ts +186 -0
- package/src/runtime/http-engine/ntlm/NtlmMessage.ts +57 -0
- package/src/runtime/modules/BasicAuthCache.ts +133 -0
- package/src/runtime/modules/ExecutionResponse.ts +4 -0
- package/src/runtime/modules/ModulesRegistry.ts +136 -0
- package/src/runtime/modules/RequestAuthorization.ts +110 -0
- package/src/runtime/modules/RequestCookies.ts +145 -0
- package/src/runtime/node/ProjectRunner.ts +281 -0
- package/src/runtime/node/RequestFactory.ts +422 -0
- package/src/runtime/node/VariablesStore.ts +25 -0
- package/src/runtime/store/StoreSdk.ts +838 -0
- package/src/runtime/variables/Cache.ts +53 -0
- package/src/runtime/variables/EvalFunctions.ts +132 -0
- package/src/runtime/variables/ProjectVariables.ts +6 -0
- package/src/runtime/variables/VariablesProcessor.ts +543 -0
- package/src/runtime/variables/VariablesTokenizer.ts +55 -0
- package/build/src/runtime/http-engine/Errors.d.ts +0 -10
- package/build/src/runtime/http-engine/Errors.js +0 -14
- package/build/src/runtime/http-engine/Errors.js.map +0 -1
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import tls from 'tls';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import { HttpEngine, HttpEngineOptions, HeadersReceivedDetail } from './HttpEngine.js';
|
|
6
|
+
import { IRequestLog } from 'src/models/RequestLog.js';
|
|
7
|
+
import { IHttpRequest } from '../../models/HttpRequest.js';
|
|
8
|
+
import { ArcResponse } from '../../models/ArcResponse.js';
|
|
9
|
+
import { SerializableError } from '../../models/SerializableError.js';
|
|
10
|
+
import { Headers } from '../../lib/headers/Headers.js';
|
|
11
|
+
import { PayloadSupport } from './PayloadSupport.js';
|
|
12
|
+
import { addContentLength, getPort } from './RequestUtils.js';
|
|
13
|
+
import { INtlmAuthorization } from '../../models/Authorization.js';
|
|
14
|
+
import { NtlmAuth, INtlmAuthConfig } from './ntlm/NtlmAuth.js';
|
|
15
|
+
import { PayloadSerializer } from '../../lib/transformers/PayloadSerializer.js';
|
|
16
|
+
import { ResponseRedirect } from '../../models/ResponseRedirect.js';
|
|
17
|
+
|
|
18
|
+
const nlBuffer = Buffer.from([13, 10]);
|
|
19
|
+
const nlNlBuffer = Buffer.from([13, 10, 13, 10]);
|
|
20
|
+
|
|
21
|
+
export enum RequestState {
|
|
22
|
+
Status,
|
|
23
|
+
Headers,
|
|
24
|
+
Body,
|
|
25
|
+
Done,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ResponseInfo {
|
|
29
|
+
contentLength?: number;
|
|
30
|
+
chunked: boolean;
|
|
31
|
+
body?: Buffer;
|
|
32
|
+
chunk?: Buffer;
|
|
33
|
+
chunkSize?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ARC's HTTP engine.
|
|
38
|
+
* An HTTP 1.1 engine working directly on the socket. It communicates with the remote machine and
|
|
39
|
+
* collects stats about the request and response.
|
|
40
|
+
*/
|
|
41
|
+
export class ArcEngine extends HttpEngine {
|
|
42
|
+
state = RequestState.Status;
|
|
43
|
+
rawHeaders?: Buffer;
|
|
44
|
+
_hostTestReg = /^\s*host\s*:/im;
|
|
45
|
+
|
|
46
|
+
responseInfo: ResponseInfo;
|
|
47
|
+
hasProxy: boolean;
|
|
48
|
+
isProxyTunnel: boolean;
|
|
49
|
+
isProxySsl: boolean;
|
|
50
|
+
|
|
51
|
+
constructor(request: IHttpRequest, opts: HttpEngineOptions = {}) {
|
|
52
|
+
super(request, opts);
|
|
53
|
+
this.responseInfo = {
|
|
54
|
+
chunked: false,
|
|
55
|
+
};
|
|
56
|
+
this.hasProxy = !!this.opts.proxy;
|
|
57
|
+
this.isProxyTunnel = this.hasProxy && this.request.url.startsWith('https:');
|
|
58
|
+
this.isProxySsl = !!this.opts.proxy && this.opts.proxy.startsWith('https:');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sends the request
|
|
63
|
+
*/
|
|
64
|
+
async send(): Promise<IRequestLog> {
|
|
65
|
+
const promise = this.wrapExecution();
|
|
66
|
+
this.sendRequest();
|
|
67
|
+
return promise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async sendRequest(): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
if (this.hasProxy) {
|
|
73
|
+
await this.connectProxy();
|
|
74
|
+
} else {
|
|
75
|
+
await this.connect();
|
|
76
|
+
}
|
|
77
|
+
if (!this.socket) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const message = await this.prepareMessage();
|
|
81
|
+
await this.writeMessage(message);
|
|
82
|
+
} catch (cause) {
|
|
83
|
+
const e = cause as any;
|
|
84
|
+
const err = new SerializableError(e.message, { cause: e });
|
|
85
|
+
if (e.code || e.code === 0) {
|
|
86
|
+
err.code = e.code as string;
|
|
87
|
+
}
|
|
88
|
+
this.abort();
|
|
89
|
+
this._errorRequest({
|
|
90
|
+
message: err.message,
|
|
91
|
+
code: err.code,
|
|
92
|
+
});
|
|
93
|
+
throw cause;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Cleans the state after finished.
|
|
99
|
+
*/
|
|
100
|
+
_cleanUp(): void {
|
|
101
|
+
super._cleanUp();
|
|
102
|
+
this.state = RequestState.Status;
|
|
103
|
+
this.rawHeaders = undefined;
|
|
104
|
+
this.responseInfo = {
|
|
105
|
+
chunked: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Cleans up the state for redirect.
|
|
111
|
+
*/
|
|
112
|
+
_cleanUpRedirect(): void {
|
|
113
|
+
super._cleanUpRedirect();
|
|
114
|
+
this.state = RequestState.Status;
|
|
115
|
+
this.rawHeaders = undefined;
|
|
116
|
+
this.responseInfo = {
|
|
117
|
+
chunked: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Prepares an HTTP message from ARC's request object.
|
|
123
|
+
*
|
|
124
|
+
* @returns Resolved promise to an `ArrayBuffer`.
|
|
125
|
+
*/
|
|
126
|
+
async prepareMessage(): Promise<Buffer> {
|
|
127
|
+
let payload = this.request.payload;
|
|
128
|
+
if (['get', 'head'].includes(this.request.method.toLowerCase())) {
|
|
129
|
+
payload = undefined;
|
|
130
|
+
}
|
|
131
|
+
const headers = new Headers(this.request.headers);
|
|
132
|
+
this.prepareHeaders(headers);
|
|
133
|
+
const auth = this.hasProxy && !this.isProxyTunnel ? this._proxyAuthHeader() : undefined;
|
|
134
|
+
if (auth) {
|
|
135
|
+
headers.set('proxy-authorization', auth);
|
|
136
|
+
}
|
|
137
|
+
let buffer: Buffer | undefined;
|
|
138
|
+
if (payload) {
|
|
139
|
+
buffer = await PayloadSupport.payloadToBuffer(payload, headers);
|
|
140
|
+
if (buffer) {
|
|
141
|
+
addContentLength(this.request.method || 'GET', buffer, headers);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this._handleAuthorization(headers);
|
|
146
|
+
this.sentRequest.headers = headers.toString();
|
|
147
|
+
const message = this._prepareMessage(headers, buffer);
|
|
148
|
+
if (this.auth) {
|
|
149
|
+
// This restores altered by authorization original headers
|
|
150
|
+
// so it can be safe to use when redirecting
|
|
151
|
+
if (this.auth.headers) {
|
|
152
|
+
this.request.headers = this.auth.headers;
|
|
153
|
+
delete this.auth.headers;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return message;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Sends a data to a socket.
|
|
161
|
+
*
|
|
162
|
+
* @param buffer HTTP message to send
|
|
163
|
+
*/
|
|
164
|
+
writeMessage(buffer: Buffer): Promise<void> {
|
|
165
|
+
this.logger.debug(`Writing the message to the socket...`);
|
|
166
|
+
let msg = buffer.toString();
|
|
167
|
+
const limit = this.opts.sentMessageLimit;
|
|
168
|
+
if (limit && limit > 0 && msg.length >= limit) {
|
|
169
|
+
msg = msg.substr(0, limit);
|
|
170
|
+
msg += ' ...';
|
|
171
|
+
}
|
|
172
|
+
this.sentRequest.httpMessage = msg;
|
|
173
|
+
const startTime = Date.now();
|
|
174
|
+
this.stats.startTime = startTime;
|
|
175
|
+
this.sentRequest.startTime = startTime;
|
|
176
|
+
|
|
177
|
+
this.stats.messageStart = Date.now();
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
this.socket?.write(buffer, () => {
|
|
180
|
+
this.logger.debug(`The message has been sent.`);
|
|
181
|
+
this.stats.sentTime = Date.now();
|
|
182
|
+
try {
|
|
183
|
+
this.emit('loadstart');
|
|
184
|
+
} catch (_) {
|
|
185
|
+
//
|
|
186
|
+
}
|
|
187
|
+
resolve();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Connects to a server and sends the message.
|
|
194
|
+
*
|
|
195
|
+
* @returns Promise resolved when socket is connected.
|
|
196
|
+
*/
|
|
197
|
+
async connect(): Promise<net.Socket> {
|
|
198
|
+
const port = getPort(this.uri.port, this.uri.protocol);
|
|
199
|
+
const host = this.uri.hostname;
|
|
200
|
+
let socket;
|
|
201
|
+
if (port === 443 || this.uri.protocol === 'https:') {
|
|
202
|
+
socket = await this._connectTls(port, host);
|
|
203
|
+
} else {
|
|
204
|
+
socket = await this._connect(port, host);
|
|
205
|
+
}
|
|
206
|
+
const { timeout } = this;
|
|
207
|
+
if (timeout > 0) {
|
|
208
|
+
socket.setTimeout(timeout);
|
|
209
|
+
}
|
|
210
|
+
this.socket = socket;
|
|
211
|
+
this._addSocketListeners(socket);
|
|
212
|
+
socket.resume();
|
|
213
|
+
return socket;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Connects to a server and writes a message using insecure connection.
|
|
218
|
+
*
|
|
219
|
+
* @param port A port number to connect to.
|
|
220
|
+
* @param host A host name to connect to
|
|
221
|
+
* @returns A promise resolved when the message was sent to a server
|
|
222
|
+
*/
|
|
223
|
+
_connect(port: number, host: string): Promise<net.Socket> {
|
|
224
|
+
this.logger.debug('Opening an HTTP connection...');
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
this.stats.connectionTime = Date.now();
|
|
227
|
+
const isIp = net.isIP(host);
|
|
228
|
+
if (isIp) {
|
|
229
|
+
this.stats.lookupTime = Date.now();
|
|
230
|
+
}
|
|
231
|
+
const client = net.createConnection(port, host, () => {
|
|
232
|
+
this.logger.debug('HTTP connection established.');
|
|
233
|
+
this.stats.connectedTime = Date.now();
|
|
234
|
+
resolve(client);
|
|
235
|
+
});
|
|
236
|
+
client.pause();
|
|
237
|
+
if (!isIp) {
|
|
238
|
+
client.once('lookup', () => {
|
|
239
|
+
this.stats.lookupTime = Date.now();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
client.once('error', (err) => reject(err));
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Connects to a server and writes a message using secured connection.
|
|
248
|
+
*
|
|
249
|
+
* @param port A port number to connect to.
|
|
250
|
+
* @param host A host name to connect to
|
|
251
|
+
* @returns A promise resolved when the message was sent to a server
|
|
252
|
+
*/
|
|
253
|
+
_connectTls(port: number, host: string): Promise<tls.TLSSocket> {
|
|
254
|
+
this.logger.debug('Opening an SSL connection...');
|
|
255
|
+
const { opts } = this;
|
|
256
|
+
const options: tls.ConnectionOptions = {};
|
|
257
|
+
const isIp = net.isIP(host);
|
|
258
|
+
if (!isIp) {
|
|
259
|
+
options.servername = host;
|
|
260
|
+
}
|
|
261
|
+
if (opts.validateCertificates) {
|
|
262
|
+
options.checkServerIdentity = this._checkServerIdentity.bind(this);
|
|
263
|
+
} else {
|
|
264
|
+
options.rejectUnauthorized = false;
|
|
265
|
+
// target.requestOCSP = false;
|
|
266
|
+
}
|
|
267
|
+
const certs = this.opts.certificates;
|
|
268
|
+
if (Array.isArray(certs)) {
|
|
269
|
+
certs.forEach(cert => this._addClientCertificate(cert, options));
|
|
270
|
+
}
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const time = Date.now();
|
|
273
|
+
this.stats.connectionTime = time;
|
|
274
|
+
if (isIp) {
|
|
275
|
+
this.stats.lookupTime = time;
|
|
276
|
+
}
|
|
277
|
+
const client = tls.connect(port, host, options, () => {
|
|
278
|
+
this.logger.debug('SSL connection established.');
|
|
279
|
+
const connectTime = Date.now();
|
|
280
|
+
this.stats.connectedTime = connectTime;
|
|
281
|
+
this.stats.secureStartTime = connectTime;
|
|
282
|
+
resolve(client);
|
|
283
|
+
});
|
|
284
|
+
client.pause();
|
|
285
|
+
client.once('error', (e) => reject(e));
|
|
286
|
+
if (!isIp) {
|
|
287
|
+
client.once('lookup', () => {
|
|
288
|
+
this.stats.lookupTime = Date.now();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
client.once('secureConnect', () => {
|
|
292
|
+
this.stats.secureConnectedTime = Date.now();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Prepares a full HTTP message body
|
|
299
|
+
*
|
|
300
|
+
* @param httpHeaders The list ogf headers to append.
|
|
301
|
+
* @param buffer The buffer with the HTTP message
|
|
302
|
+
* @returns The Buffer of the HTTP message
|
|
303
|
+
*/
|
|
304
|
+
_prepareMessage(httpHeaders: Headers, buffer?: Buffer): Buffer {
|
|
305
|
+
this.logger.debug('Preparing an HTTP message...');
|
|
306
|
+
const headers = [];
|
|
307
|
+
// const search = this.uri.search;
|
|
308
|
+
// let path = this.uri.pathname;
|
|
309
|
+
// if (search) {
|
|
310
|
+
// path += search;
|
|
311
|
+
// }
|
|
312
|
+
// headers.push(`${this.arcRequest.method} ${path} HTTP/1.1`);
|
|
313
|
+
const status = this._createHttpStatus();
|
|
314
|
+
this.logger.debug(`Created message status: ${status}`);
|
|
315
|
+
headers.push(status);
|
|
316
|
+
if (this._hostRequired()) {
|
|
317
|
+
this.logger.debug(`Adding the "host" header: ${this.hostHeader}`);
|
|
318
|
+
headers.push(`Host: ${this.hostHeader}`);
|
|
319
|
+
}
|
|
320
|
+
let str = headers.join('\r\n');
|
|
321
|
+
const addHeaders = httpHeaders.toString();
|
|
322
|
+
if (addHeaders) {
|
|
323
|
+
this.logger.debug(`Adding headers to the request...`);
|
|
324
|
+
str += '\r\n';
|
|
325
|
+
str += PayloadSupport.normalizeString(addHeaders);
|
|
326
|
+
}
|
|
327
|
+
const startBuffer = Buffer.from(str, 'utf8');
|
|
328
|
+
const endBuffer = Buffer.from(new Uint8Array([13, 10, 13, 10]));
|
|
329
|
+
let body;
|
|
330
|
+
let sum = startBuffer.length + endBuffer.length;
|
|
331
|
+
if (buffer) {
|
|
332
|
+
sum += buffer.length;
|
|
333
|
+
body = Buffer.concat([startBuffer, endBuffer, buffer], sum);
|
|
334
|
+
} else {
|
|
335
|
+
body = Buffer.concat([startBuffer, endBuffer], sum);
|
|
336
|
+
}
|
|
337
|
+
this.logger.debug(`The message is ready.`);
|
|
338
|
+
return body;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Creates an HTTP status line for the message.
|
|
343
|
+
* For proxy connections it, depending whether target is SSL or not, sets the path
|
|
344
|
+
* as the full URL or just the authority.
|
|
345
|
+
* @returns The generates status message.
|
|
346
|
+
*/
|
|
347
|
+
_createHttpStatus(): string {
|
|
348
|
+
const { request, uri, hasProxy, isProxyTunnel } = this;
|
|
349
|
+
const parts = [request.method];
|
|
350
|
+
if (hasProxy && !isProxyTunnel) {
|
|
351
|
+
// if (isProxyTunnel) {
|
|
352
|
+
// // when a tunnel then the target is over SSL so the default port is 443.
|
|
353
|
+
// parts.push(`${uri.hostname}:${uri.port || 443}`);
|
|
354
|
+
// } else {
|
|
355
|
+
// parts.push(arcRequest.url);
|
|
356
|
+
// }
|
|
357
|
+
parts.push(request.url);
|
|
358
|
+
} else {
|
|
359
|
+
let path = uri.pathname;
|
|
360
|
+
if (uri.search) {
|
|
361
|
+
path += uri.search;
|
|
362
|
+
}
|
|
363
|
+
parts.push(path);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
parts.push('HTTP/1.1');
|
|
367
|
+
return parts.join(' ');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Tests if current connection is required to add `host` header.
|
|
372
|
+
* It returns `false` only if `host` header has been already provided.
|
|
373
|
+
*
|
|
374
|
+
* @returns True when the `host` header should be added to the headers list.
|
|
375
|
+
*/
|
|
376
|
+
_hostRequired(): boolean {
|
|
377
|
+
const headers = this.request.headers;
|
|
378
|
+
if (typeof headers !== 'string') {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
return !this._hostTestReg.test(headers);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Alters authorization header depending on the `auth` object
|
|
386
|
+
* @param headers A headers object where to append headers when needed
|
|
387
|
+
*/
|
|
388
|
+
_handleAuthorization(headers: Headers): void {
|
|
389
|
+
const { authorization } = this.opts;
|
|
390
|
+
const enabled = Array.isArray(authorization) ? authorization.filter((i) => i.enabled) : [];
|
|
391
|
+
if (!enabled.length) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const ntlm = enabled.find((i) => i.type === 'ntlm');
|
|
395
|
+
if (ntlm) {
|
|
396
|
+
this._authorizeNtlm(ntlm.config as INtlmAuthorization, headers);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Authorize the request with NTLM
|
|
402
|
+
* @param authData Credentials to use
|
|
403
|
+
* @param headers A headers object where to append headers if needed
|
|
404
|
+
*/
|
|
405
|
+
_authorizeNtlm(authData: INtlmAuthorization, headers: Headers): void {
|
|
406
|
+
const init = { ...authData, url: this.request.url } as INtlmAuthConfig;
|
|
407
|
+
const auth = new NtlmAuth(init);
|
|
408
|
+
if (!this.auth) {
|
|
409
|
+
this.auth = {
|
|
410
|
+
method: 'ntlm',
|
|
411
|
+
state: 0,
|
|
412
|
+
headers: headers.toString(),
|
|
413
|
+
};
|
|
414
|
+
const msg = auth.createMessage1(this.uri.host);
|
|
415
|
+
headers.set('Authorization', `NTLM ${msg.toBase64()}`);
|
|
416
|
+
headers.set('Connection', 'keep-alive');
|
|
417
|
+
} else if (this.auth && this.auth.state === 1) {
|
|
418
|
+
const msg = auth.createMessage3(this.auth.challengeHeader!, this.uri.host);
|
|
419
|
+
this.auth.state = 2;
|
|
420
|
+
headers.set('Authorization', `NTLM ${msg.toBase64()}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Add event listeners to existing socket.
|
|
426
|
+
* @param socket An instance of the socket.
|
|
427
|
+
* @return The same socket. Used for chaining.
|
|
428
|
+
*/
|
|
429
|
+
_addSocketListeners(socket: net.Socket): net.Socket {
|
|
430
|
+
let received = false;
|
|
431
|
+
socket.on('data', (data) => {
|
|
432
|
+
this.logger.debug(`Received server data from the socket...`);
|
|
433
|
+
if (!received) {
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
this.stats.firstReceiveTime = now;
|
|
436
|
+
this.emit('firstbyte');
|
|
437
|
+
received = true;
|
|
438
|
+
}
|
|
439
|
+
data = Buffer.from(data);
|
|
440
|
+
try {
|
|
441
|
+
this._processSocketMessage(data);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
const err = e as Error;
|
|
444
|
+
this._errorRequest({
|
|
445
|
+
message: err.message || 'Unknown error occurred',
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
socket.once('timeout', () => {
|
|
451
|
+
this.state = RequestState.Done;
|
|
452
|
+
this._errorRequest(new Error('Connection timeout.'));
|
|
453
|
+
socket.destroy();
|
|
454
|
+
});
|
|
455
|
+
socket.on('end', () => {
|
|
456
|
+
this.logger.debug(`Server connection ended.`);
|
|
457
|
+
socket.removeAllListeners('timeout');
|
|
458
|
+
socket.removeAllListeners('error');
|
|
459
|
+
const endTime = Date.now();
|
|
460
|
+
this.stats.lastReceivedTime = endTime;
|
|
461
|
+
this.sentRequest.endTime = endTime;
|
|
462
|
+
if (this.state !== RequestState.Done) {
|
|
463
|
+
if (!this.currentResponse) {
|
|
464
|
+
this.logger.error(`Connection closed without receiving any data.`);
|
|
465
|
+
// The parser haven't found the end of message so there was no message!
|
|
466
|
+
const e = new SerializableError('Connection closed without receiving any data', 100);
|
|
467
|
+
this._errorRequest(e);
|
|
468
|
+
} else {
|
|
469
|
+
// There is an issue with the response. Size mismatch? Anyway,
|
|
470
|
+
// it tries to create a response from current data.
|
|
471
|
+
this.emit('loadend');
|
|
472
|
+
this._publishResponse();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
socket.once('error', (err) => {
|
|
477
|
+
socket.removeAllListeners('timeout');
|
|
478
|
+
this._errorRequest(err);
|
|
479
|
+
});
|
|
480
|
+
return socket;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Processes response message chunk
|
|
485
|
+
* @param buffer Message buffer
|
|
486
|
+
*/
|
|
487
|
+
_processResponse(buffer: Buffer): void {
|
|
488
|
+
this._processSocketMessage(buffer);
|
|
489
|
+
this._reportResponse();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Reports response after processing it.
|
|
494
|
+
*/
|
|
495
|
+
_reportResponse(): void {
|
|
496
|
+
this._clearSocketEventListeners();
|
|
497
|
+
const { aborted, currentResponse } = this;
|
|
498
|
+
if (aborted || !currentResponse) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const { status } = currentResponse;
|
|
502
|
+
|
|
503
|
+
const endTime = Date.now();
|
|
504
|
+
this.stats.lastReceivedTime = endTime;
|
|
505
|
+
this.sentRequest.endTime = endTime;
|
|
506
|
+
|
|
507
|
+
if (status >= 300 && status < 400) {
|
|
508
|
+
if (this.followRedirects && this._reportRedirect(status)) {
|
|
509
|
+
this.closeClient();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
} else if (status === 401 && this.auth) {
|
|
513
|
+
switch (this.auth.method) {
|
|
514
|
+
case 'ntlm': this.handleNtlmResponse(); return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
this.closeClient();
|
|
518
|
+
this.emit('loadend');
|
|
519
|
+
this._publishResponse();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Generate response object and publish it to the listeners.
|
|
524
|
+
*/
|
|
525
|
+
_publishResponse(): Promise<void> {
|
|
526
|
+
this.state = RequestState.Done;
|
|
527
|
+
if (!this._rawBody) {
|
|
528
|
+
if (this.responseInfo.body) {
|
|
529
|
+
this._rawBody = this.responseInfo.body;
|
|
530
|
+
} else if (this.responseInfo.chunk) {
|
|
531
|
+
this._rawBody = this.responseInfo.chunk;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return super._publishResponse();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* @param location The redirect location.
|
|
539
|
+
* @return Redirect response object
|
|
540
|
+
*/
|
|
541
|
+
async _createRedirectResponse(location: string): Promise<ResponseRedirect> {
|
|
542
|
+
const { currentResponse = new ArcResponse() } = this;
|
|
543
|
+
this.currentResponse = currentResponse;
|
|
544
|
+
if (!this.currentResponse.payload) {
|
|
545
|
+
if (this._rawBody) {
|
|
546
|
+
this.currentResponse.payload = PayloadSerializer.stringifyBuffer(this._rawBody);
|
|
547
|
+
} else if (this.responseInfo.body) {
|
|
548
|
+
this.currentResponse.payload = PayloadSerializer.stringifyBuffer(this.responseInfo.body);
|
|
549
|
+
} else if (this.responseInfo.chunk) {
|
|
550
|
+
this.currentResponse.payload = PayloadSerializer.stringifyBuffer(this.responseInfo.chunk);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return super._createRedirectResponse(location);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* closes the connection, if any
|
|
558
|
+
*/
|
|
559
|
+
closeClient(): void {
|
|
560
|
+
if (this.socket && !this.socket.destroyed) {
|
|
561
|
+
this.socket.destroy();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Handles the response with NTLM authorization
|
|
567
|
+
*/
|
|
568
|
+
handleNtlmResponse(): void {
|
|
569
|
+
const { auth } = this;
|
|
570
|
+
if (!auth) {
|
|
571
|
+
throw new Error('No auth data.')
|
|
572
|
+
}
|
|
573
|
+
if (auth.state === 0) {
|
|
574
|
+
if (this.currentHeaders.has('www-authenticate')) {
|
|
575
|
+
auth.state = 1;
|
|
576
|
+
auth.challengeHeader = this.currentHeaders.get('www-authenticate');
|
|
577
|
+
this._cleanUpRedirect();
|
|
578
|
+
this.prepareMessage().then((message) => this.writeMessage(message));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
delete this.auth;
|
|
583
|
+
this.emit('loadend');
|
|
584
|
+
this._publishResponse();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Process received message.
|
|
589
|
+
*
|
|
590
|
+
* @param data Received message.
|
|
591
|
+
*/
|
|
592
|
+
_processSocketMessage(data: Buffer): void {
|
|
593
|
+
if (this.aborted) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (this.state === RequestState.Done) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
let remaining: Buffer | undefined = data;
|
|
600
|
+
if (this.state === RequestState.Status) {
|
|
601
|
+
remaining = this._processStatus(remaining);
|
|
602
|
+
if (!remaining) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (this.state === RequestState.Headers) {
|
|
607
|
+
remaining = this._processHeaders(remaining);
|
|
608
|
+
if (!remaining) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (this.state === RequestState.Body) {
|
|
613
|
+
this._processBody(remaining);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Read status line from the response.
|
|
620
|
+
* This function will set `status` and `statusText` fields
|
|
621
|
+
* and then will set `state` to HEADERS.
|
|
622
|
+
*
|
|
623
|
+
* @param data The received data
|
|
624
|
+
*/
|
|
625
|
+
_processStatus(data?: Buffer): Buffer | undefined {
|
|
626
|
+
if (this.aborted) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const response = ArcResponse.fromValues(0);
|
|
630
|
+
response.loadingTime = 0;
|
|
631
|
+
this.currentResponse = response;
|
|
632
|
+
if (!data) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
this.logger.info('Processing status');
|
|
637
|
+
const index = data.indexOf(nlBuffer);
|
|
638
|
+
let statusLine = data.slice(0, index).toString();
|
|
639
|
+
data = data.slice(index + 2);
|
|
640
|
+
statusLine = statusLine.replace(/HTTP\/\d(\.\d)?\s/, '');
|
|
641
|
+
const delimiterPos = statusLine.indexOf(' ');
|
|
642
|
+
let status;
|
|
643
|
+
let msg = '';
|
|
644
|
+
if (delimiterPos === -1) {
|
|
645
|
+
status = statusLine;
|
|
646
|
+
} else {
|
|
647
|
+
status = statusLine.substr(0, delimiterPos);
|
|
648
|
+
msg = statusLine.substr(delimiterPos + 1);
|
|
649
|
+
}
|
|
650
|
+
let typedStatus = Number(status);
|
|
651
|
+
if (Number.isNaN(typedStatus)) {
|
|
652
|
+
typedStatus = 0;
|
|
653
|
+
}
|
|
654
|
+
if (msg && msg.indexOf('\n') !== -1) {
|
|
655
|
+
msg = msg.split('\n')[0];
|
|
656
|
+
}
|
|
657
|
+
this.currentResponse!.status = typedStatus;
|
|
658
|
+
this.currentResponse!.statusText = msg;
|
|
659
|
+
this.logger.info('Received status', typedStatus, msg);
|
|
660
|
+
this.state = RequestState.Headers;
|
|
661
|
+
return data;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Read headers from the received data.
|
|
666
|
+
*
|
|
667
|
+
* @param data Received data
|
|
668
|
+
* @returns Remaining data in the buffer.
|
|
669
|
+
*/
|
|
670
|
+
_processHeaders(data?: Buffer): Buffer | undefined {
|
|
671
|
+
if (this.aborted) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (!data) {
|
|
675
|
+
this._parseHeaders();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
this.logger.info('Processing headers');
|
|
679
|
+
// Looking for end of headers section
|
|
680
|
+
let index = data.indexOf(nlNlBuffer);
|
|
681
|
+
let padding = 4;
|
|
682
|
+
if (index === -1) {
|
|
683
|
+
// It can also be 2x ASCII 10
|
|
684
|
+
const _index = data.indexOf(Buffer.from([10, 10]));
|
|
685
|
+
if (_index !== -1) {
|
|
686
|
+
index = _index;
|
|
687
|
+
padding = 2;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// https://github.com/jarrodek/socket-fetch/issues/3
|
|
692
|
+
const enterIndex = data.indexOf(nlBuffer);
|
|
693
|
+
if (index === -1 && enterIndex !== 0) {
|
|
694
|
+
// end in next chunk
|
|
695
|
+
if (!this.rawHeaders) {
|
|
696
|
+
this.rawHeaders = data;
|
|
697
|
+
} else {
|
|
698
|
+
const sum = this.rawHeaders.length + data.length;
|
|
699
|
+
this.rawHeaders = Buffer.concat([this.rawHeaders, data], sum);
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (enterIndex !== 0) {
|
|
704
|
+
const headersArray = data.slice(0, index);
|
|
705
|
+
if (!this.rawHeaders) {
|
|
706
|
+
this.rawHeaders = headersArray;
|
|
707
|
+
} else {
|
|
708
|
+
const sum = this.rawHeaders.length + headersArray.length;
|
|
709
|
+
this.rawHeaders = Buffer.concat([this.rawHeaders, headersArray], sum);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
this._parseHeaders(this.rawHeaders);
|
|
713
|
+
delete this.rawHeaders;
|
|
714
|
+
this.state = RequestState.Body;
|
|
715
|
+
const start = index === -1 ? 0 : index;
|
|
716
|
+
const move = enterIndex === 0 ? 2 : padding;
|
|
717
|
+
data = data.slice(start + move);
|
|
718
|
+
return this._postHeaders(data);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Check the response headers and end the request if necessary.
|
|
723
|
+
* @param data Current response data buffer
|
|
724
|
+
*/
|
|
725
|
+
_postHeaders(data: Buffer): Buffer|undefined {
|
|
726
|
+
if (this.request.method === 'HEAD') {
|
|
727
|
+
this._reportResponse();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (data.length === 0) {
|
|
731
|
+
if (this.currentHeaders.has('Content-Length')) {
|
|
732
|
+
// If the server do not close the connection and clearly indicate that
|
|
733
|
+
// there are no further data to receive the app can close the connection
|
|
734
|
+
// and prepare the response.
|
|
735
|
+
const length = Number(this.currentHeaders.get('Content-Length'));
|
|
736
|
+
// NaN never equals NaN. This is faster.
|
|
737
|
+
if (!Number.isNaN(length) && length === 0) {
|
|
738
|
+
this._reportResponse();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// See: https://github.com/advanced-rest-client/arc-electron/issues/106
|
|
743
|
+
// The client should wait until the connection is closed instead of assuming it should end the request.
|
|
744
|
+
|
|
745
|
+
// else if (!this.currentHeaders.has('Transfer-Encoding') || !this.currentHeaders.get('Transfer-Encoding')) {
|
|
746
|
+
// // Fix for https://github.com/jarrodek/socket-fetch/issues/6
|
|
747
|
+
// // There is no body in the response.
|
|
748
|
+
// // this._reportResponse();
|
|
749
|
+
// return;
|
|
750
|
+
// }
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
return data;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* This function assumes that all headers has been read and it's
|
|
758
|
+
* just before changing the status to BODY.
|
|
759
|
+
*/
|
|
760
|
+
_parseHeaders(buffer?: Buffer): void {
|
|
761
|
+
let raw = '';
|
|
762
|
+
if (buffer) {
|
|
763
|
+
raw = buffer.toString();
|
|
764
|
+
}
|
|
765
|
+
this.currentResponse!.headers = raw;
|
|
766
|
+
this.logger.info('Received headers list', raw);
|
|
767
|
+
const headers = new Headers(raw);
|
|
768
|
+
this.currentHeaders = headers;
|
|
769
|
+
if (headers.has('Content-Length')) {
|
|
770
|
+
this.responseInfo.contentLength = Number(headers.get('Content-Length'));
|
|
771
|
+
}
|
|
772
|
+
if (headers.has('Transfer-Encoding')) {
|
|
773
|
+
const tr = headers.get('Transfer-Encoding');
|
|
774
|
+
if (tr === 'chunked') {
|
|
775
|
+
this.responseInfo.chunked = true;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const rawHeaders = headers.toString();
|
|
779
|
+
const detail: HeadersReceivedDetail = {
|
|
780
|
+
returnValue: true,
|
|
781
|
+
value: rawHeaders,
|
|
782
|
+
};
|
|
783
|
+
this.emit('headersreceived', detail);
|
|
784
|
+
if (!detail.returnValue) {
|
|
785
|
+
this.abort();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* @param data A data to process
|
|
791
|
+
*/
|
|
792
|
+
_processBody(data?: Buffer): void {
|
|
793
|
+
if (this.aborted || !data) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (this.responseInfo.chunked) {
|
|
797
|
+
this._processBodyChunked(data);
|
|
798
|
+
} else {
|
|
799
|
+
this._processBodyContentLength(data);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
_processBodyContentLength(data: Buffer): void {
|
|
804
|
+
if (typeof this.responseInfo.contentLength === 'undefined') {
|
|
805
|
+
this._errorRequest(new Error(`The content-length header of the response is missing.`));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (!this.responseInfo.body) {
|
|
809
|
+
this.responseInfo.body = data;
|
|
810
|
+
if (data.length >= this.responseInfo.contentLength) {
|
|
811
|
+
this._reportResponse();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const sum = this.responseInfo.body.length + data.length;
|
|
817
|
+
this.responseInfo.body = Buffer.concat([this.responseInfo.body, data], sum);
|
|
818
|
+
if (this.responseInfo.body.length >= this.responseInfo.contentLength) {
|
|
819
|
+
this._reportResponse();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* @param data A latest data to process
|
|
826
|
+
*/
|
|
827
|
+
_processBodyChunked(data?: Buffer): void {
|
|
828
|
+
if (!data) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (this.responseInfo.chunk) {
|
|
832
|
+
data = Buffer.concat([this.responseInfo.chunk, data], this.responseInfo.chunk.length + data.length);
|
|
833
|
+
this.responseInfo.chunk = undefined;
|
|
834
|
+
}
|
|
835
|
+
// eslint-disable-next-line no-constant-condition
|
|
836
|
+
while (true) {
|
|
837
|
+
if (this.responseInfo.chunkSize === 0 && data.indexOf(nlNlBuffer) === 0) {
|
|
838
|
+
this._reportResponse();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (!this.responseInfo.chunkSize) {
|
|
842
|
+
data = this.readChunkSize(data);
|
|
843
|
+
if (!data) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (!this.responseInfo.chunkSize && this.responseInfo.chunkSize !== 0) {
|
|
847
|
+
// It may happen that node's buffer cuts the data
|
|
848
|
+
// just before the chunk size.
|
|
849
|
+
// It should proceed it in next portion of the data.
|
|
850
|
+
this.responseInfo.chunk = data;
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (!this.responseInfo.chunkSize) {
|
|
854
|
+
this._reportResponse();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const size = Math.min(this.responseInfo.chunkSize, data.length);
|
|
859
|
+
const sliced = data.slice(0, size);
|
|
860
|
+
if (!this._rawBody) {
|
|
861
|
+
this._rawBody = sliced;
|
|
862
|
+
} else {
|
|
863
|
+
const sum = size + this._rawBody.length;
|
|
864
|
+
this._rawBody = Buffer.concat([this._rawBody, sliced], sum);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
this.responseInfo.chunkSize -= size;
|
|
868
|
+
if (data.length === 0) {
|
|
869
|
+
// this.logger.warn('Next chunk will start with CRLF!');
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
data = data.slice(size + 2); // + CR
|
|
873
|
+
if (data.length === 0) {
|
|
874
|
+
// this.logger.info('No more data here. Waiting for new chunk');
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* If response's Transfer-Encoding is 'chunked' read until next CR.
|
|
882
|
+
* Everything before it is a chunk size.
|
|
883
|
+
*/
|
|
884
|
+
readChunkSize(array: Buffer): Buffer|undefined {
|
|
885
|
+
if (this.aborted) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
let index = array.indexOf(nlBuffer);
|
|
889
|
+
if (index === -1) {
|
|
890
|
+
// not found in this portion of data.
|
|
891
|
+
return array;
|
|
892
|
+
}
|
|
893
|
+
if (index === 0) {
|
|
894
|
+
// Node's buffer cuts CRLF after the end of chunk data, without last CLCR,
|
|
895
|
+
// here's to fix it.
|
|
896
|
+
// It can be either new line from the last chunk or end of
|
|
897
|
+
// the message where
|
|
898
|
+
// the rest of the array is [13, 10, 48, 13, 10, 13, 10]
|
|
899
|
+
if (array.indexOf(nlNlBuffer) === 0) {
|
|
900
|
+
this.responseInfo.chunkSize = 0;
|
|
901
|
+
return Buffer.alloc(0);
|
|
902
|
+
}
|
|
903
|
+
array = array.slice(index + 2);
|
|
904
|
+
index = array.indexOf(nlBuffer);
|
|
905
|
+
}
|
|
906
|
+
// this.logger.info('Size index: ', index);
|
|
907
|
+
const chunkSize = parseInt(array.slice(0, index).toString(), 16);
|
|
908
|
+
if (Number.isNaN(chunkSize)) {
|
|
909
|
+
this.responseInfo.chunkSize = undefined;
|
|
910
|
+
return array.slice(index + 2);
|
|
911
|
+
}
|
|
912
|
+
this.responseInfo.chunkSize = chunkSize;
|
|
913
|
+
return array.slice(index + 2);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Connects to a server through a proxy. Depending on the proxy type the returned socket
|
|
918
|
+
* is a socket created after creating a tunnel (SSL) or the proxy socket.
|
|
919
|
+
*
|
|
920
|
+
* @returns Promise resolved when socket is connected.
|
|
921
|
+
*/
|
|
922
|
+
async connectProxy(): Promise<net.Socket|undefined> {
|
|
923
|
+
let socket;
|
|
924
|
+
if (this.isProxyTunnel) {
|
|
925
|
+
socket = await this.connectTunnel(this.isProxySsl);
|
|
926
|
+
} else {
|
|
927
|
+
socket = await this.proxyHttp(this.isProxySsl);
|
|
928
|
+
}
|
|
929
|
+
if (!socket) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const { timeout } = this;
|
|
933
|
+
if (timeout > 0) {
|
|
934
|
+
socket.setTimeout(timeout);
|
|
935
|
+
}
|
|
936
|
+
this.socket = socket;
|
|
937
|
+
this._addSocketListeners(socket);
|
|
938
|
+
socket.resume();
|
|
939
|
+
return socket;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Creates a tunnel to a Proxy for SSL connections.
|
|
944
|
+
* The returned socket is the one created after the tunnel is established.
|
|
945
|
+
* @param proxyIsSsl Whether the proxy is an SSL connection.
|
|
946
|
+
* @returns Promise resolved when socket is connected.
|
|
947
|
+
*/
|
|
948
|
+
async connectTunnel(proxyIsSsl=false): Promise<net.Socket | undefined> {
|
|
949
|
+
this.logger.debug(`Creating a tunnel through the proxy...`);
|
|
950
|
+
const { proxy } = this.opts;
|
|
951
|
+
const { url } = this.request;
|
|
952
|
+
if (!proxy) {
|
|
953
|
+
throw new Error(`No proxy configuration found.`);
|
|
954
|
+
}
|
|
955
|
+
let proxyUrl = proxy;
|
|
956
|
+
if (proxyIsSsl && !proxyUrl.startsWith('https:')) {
|
|
957
|
+
proxyUrl = `https://${proxyUrl}`;
|
|
958
|
+
} else if (!proxyIsSsl && !proxyUrl.startsWith('http:')) {
|
|
959
|
+
proxyUrl = `http://${proxyUrl}`;
|
|
960
|
+
}
|
|
961
|
+
const proxyUri = new URL(proxyUrl);
|
|
962
|
+
const targetUrl = new URL(url);
|
|
963
|
+
const proxyPort = proxyUri.port || (proxyIsSsl ? 443 : 80);
|
|
964
|
+
const targetPort = targetUrl.port || 443; // target is always SSL so 443.
|
|
965
|
+
const authority = `${targetUrl.hostname}:${targetPort}`;
|
|
966
|
+
const connectOptions: https.RequestOptions = {
|
|
967
|
+
host: proxyUri.hostname,
|
|
968
|
+
port: proxyPort,
|
|
969
|
+
method: 'CONNECT',
|
|
970
|
+
path: authority,
|
|
971
|
+
headers: {
|
|
972
|
+
host: authority,
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
if (proxyIsSsl) {
|
|
976
|
+
connectOptions.rejectUnauthorized = false;
|
|
977
|
+
// @ts-ignore
|
|
978
|
+
connectOptions.requestOCSP = false;
|
|
979
|
+
}
|
|
980
|
+
const auth = this._proxyAuthHeader();
|
|
981
|
+
if (auth) {
|
|
982
|
+
this.logger.debug(`Adding proxy authorization.`);
|
|
983
|
+
connectOptions.headers!['proxy-authorization'] = auth;
|
|
984
|
+
}
|
|
985
|
+
const lib = proxyIsSsl ? https : http;
|
|
986
|
+
return new Promise((resolve, reject) => {
|
|
987
|
+
this.stats.connectionTime = Date.now();
|
|
988
|
+
const connectRequest = lib.request(connectOptions);
|
|
989
|
+
connectRequest.once('socket', (socket) => {
|
|
990
|
+
socket.on('lookup', () => {
|
|
991
|
+
this.stats.lookupTime = Date.now();
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
connectRequest.on('connect', async (res, socket, head) => {
|
|
995
|
+
const time = Date.now();
|
|
996
|
+
this.stats.connectedTime = time;
|
|
997
|
+
this.stats.secureStartTime = time;
|
|
998
|
+
if (typeof this.stats.lookupTime === 'undefined') {
|
|
999
|
+
this.stats.lookupTime = time;
|
|
1000
|
+
}
|
|
1001
|
+
if (res.statusCode === 401) {
|
|
1002
|
+
this.currentHeaders = new Headers(res.headers);
|
|
1003
|
+
const currentResponse = ArcResponse.fromValues(res.statusCode, res.statusMessage, this.currentHeaders.toString());
|
|
1004
|
+
currentResponse.loadingTime = 0;
|
|
1005
|
+
this.currentResponse = currentResponse;
|
|
1006
|
+
if (head.length) {
|
|
1007
|
+
this._rawBody = head;
|
|
1008
|
+
currentResponse.payload = PayloadSerializer.stringifyBuffer(head);
|
|
1009
|
+
}
|
|
1010
|
+
connectRequest.destroy();
|
|
1011
|
+
resolve(undefined);
|
|
1012
|
+
setTimeout(() => {
|
|
1013
|
+
// const e = new NetError('The proxy requires authentication.', 127);
|
|
1014
|
+
this._publishResponse();
|
|
1015
|
+
});
|
|
1016
|
+
} else if (res.statusCode !== 200) {
|
|
1017
|
+
this.logger.debug(`The proxy tunnel ended with ${res.statusCode} status code. Erroring request.`);
|
|
1018
|
+
connectRequest.destroy();
|
|
1019
|
+
const e = new SerializableError('A tunnel connection through the proxy could not be established', 111);
|
|
1020
|
+
reject(e);
|
|
1021
|
+
} else {
|
|
1022
|
+
this.logger.debug(`Established a proxy tunnel.`);
|
|
1023
|
+
this.logger.debug(`Upgrading connection to SSL...`);
|
|
1024
|
+
const tlsSocket = tls.connect({ socket, rejectUnauthorized: false }, () => {
|
|
1025
|
+
this.logger.debug(`Connection upgraded to SSL.`);
|
|
1026
|
+
resolve(tlsSocket);
|
|
1027
|
+
});
|
|
1028
|
+
tlsSocket.once('secureConnect', () => {
|
|
1029
|
+
this.stats.secureConnectedTime = Date.now();
|
|
1030
|
+
})
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
connectRequest.end();
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Creates connection to a proxy for an HTTP (non-SSL) transport.
|
|
1039
|
+
* This is the same as calling _connect or _connectTls but the target is the proxy and not the
|
|
1040
|
+
* target URL. The message sent to the proxy server is different than the one sent
|
|
1041
|
+
* to the target.
|
|
1042
|
+
* @param proxyIsSsl
|
|
1043
|
+
* @returns Promise resolved when socket is connected.
|
|
1044
|
+
*/
|
|
1045
|
+
async proxyHttp(proxyIsSsl=false): Promise<net.Socket> {
|
|
1046
|
+
this.logger.debug('Proxying an HTTP request...');
|
|
1047
|
+
const { proxy } = this.opts;
|
|
1048
|
+
if (!proxy) {
|
|
1049
|
+
throw new Error(`No proxy configuration found.`);
|
|
1050
|
+
}
|
|
1051
|
+
let proxyUrl = proxy;
|
|
1052
|
+
if (proxyIsSsl && !proxyUrl.startsWith('https:')) {
|
|
1053
|
+
proxyUrl = `https://${proxyUrl}`;
|
|
1054
|
+
} else if (!proxyIsSsl && !proxyUrl.startsWith('http:')) {
|
|
1055
|
+
proxyUrl = `http://${proxyUrl}`;
|
|
1056
|
+
}
|
|
1057
|
+
const proxyUri = new URL(proxyUrl);
|
|
1058
|
+
const port = Number(proxyUri.port || 443);
|
|
1059
|
+
const host = proxyUri.hostname;
|
|
1060
|
+
let socket;
|
|
1061
|
+
if (proxyIsSsl) {
|
|
1062
|
+
socket = await this._connectTls(port, host);
|
|
1063
|
+
} else {
|
|
1064
|
+
socket = await this._connect(port, host);
|
|
1065
|
+
}
|
|
1066
|
+
return socket;
|
|
1067
|
+
}
|
|
1068
|
+
}
|