react-native-nitro-net 0.2.0 → 0.3.0
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/README.md +70 -12
- package/android/libs/arm64-v8a/librust_c_net.so +0 -0
- package/android/libs/armeabi-v7a/librust_c_net.so +0 -0
- package/android/libs/x86/librust_c_net.so +0 -0
- package/android/libs/x86_64/librust_c_net.so +0 -0
- package/cpp/HybridHttpParser.hpp +67 -0
- package/cpp/HybridNetDriver.hpp +6 -0
- package/cpp/HybridNetServerDriver.hpp +7 -0
- package/cpp/HybridNetSocketDriver.hpp +27 -0
- package/cpp/NetBindings.hpp +15 -0
- package/ios/Frameworks/RustCNet.xcframework/Info.plist +5 -5
- package/ios/Frameworks/RustCNet.xcframework/ios-arm64/RustCNet.framework/RustCNet +0 -0
- package/ios/Frameworks/RustCNet.xcframework/ios-arm64_x86_64-simulator/RustCNet.framework/RustCNet +0 -0
- package/lib/Net.nitro.d.ts +19 -0
- package/lib/http.d.ts +203 -0
- package/lib/http.js +1138 -0
- package/lib/https.d.ts +24 -0
- package/lib/https.js +144 -0
- package/lib/index.d.ts +46 -8
- package/lib/index.js +133 -26
- package/lib/tls.d.ts +21 -0
- package/lib/tls.js +74 -4
- package/nitrogen/generated/android/RustCNet+autolinking.cmake +2 -0
- package/nitrogen/generated/android/RustCNetOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JHybridHttpParserSpec.cpp +54 -0
- package/nitrogen/generated/android/c++/JHybridHttpParserSpec.hpp +65 -0
- package/nitrogen/generated/android/c++/JHybridNetDriverSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JHybridNetDriverSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JHybridNetServerDriverSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JHybridNetServerDriverSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JHybridNetSocketDriverSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JHybridNetSocketDriverSpec.hpp +2 -0
- package/nitrogen/generated/android/c++/JNetConfig.hpp +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridHttpParserSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetDriverSpec.kt +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetServerDriverSpec.kt +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetSocketDriverSpec.kt +8 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/NetConfig.kt +6 -3
- package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Bridge.hpp +26 -0
- package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Umbrella.hpp +5 -0
- package/nitrogen/generated/ios/c++/HybridHttpParserSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridHttpParserSpecSwift.hpp +79 -0
- package/nitrogen/generated/ios/c++/HybridNetDriverSpecSwift.hpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNetServerDriverSpecSwift.hpp +6 -0
- package/nitrogen/generated/ios/c++/HybridNetSocketDriverSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/swift/HybridHttpParserSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridHttpParserSpec_cxx.swift +131 -0
- package/nitrogen/generated/ios/swift/HybridNetDriverSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridNetDriverSpec_cxx.swift +15 -0
- package/nitrogen/generated/ios/swift/HybridNetServerDriverSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridNetServerDriverSpec_cxx.swift +11 -0
- package/nitrogen/generated/ios/swift/HybridNetSocketDriverSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridNetSocketDriverSpec_cxx.swift +36 -0
- package/nitrogen/generated/ios/swift/NetConfig.swift +19 -1
- package/nitrogen/generated/shared/c++/HybridHttpParserSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridHttpParserSpec.hpp +63 -0
- package/nitrogen/generated/shared/c++/HybridNetDriverSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridNetDriverSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/HybridNetServerDriverSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridNetServerDriverSpec.hpp +1 -0
- package/nitrogen/generated/shared/c++/HybridNetSocketDriverSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridNetSocketDriverSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/NetConfig.hpp +6 -2
- package/package.json +3 -3
- package/src/Net.nitro.ts +17 -0
- package/src/http.ts +1304 -0
- package/src/https.ts +127 -0
- package/src/index.ts +149 -18
- package/src/tls.ts +82 -6
package/lib/http.js
ADDED
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClientRequest = exports.globalAgent = exports.Agent = exports.Server = exports.ServerResponse = exports.OutgoingMessage = exports.IncomingMessage = exports.METHODS = exports.STATUS_CODES = void 0;
|
|
4
|
+
exports.createServer = createServer;
|
|
5
|
+
exports.request = request;
|
|
6
|
+
exports.get = get;
|
|
7
|
+
const readable_stream_1 = require("readable-stream");
|
|
8
|
+
const eventemitter3_1 = require("eventemitter3");
|
|
9
|
+
const Driver_1 = require("./Driver");
|
|
10
|
+
const index_1 = require("./index");
|
|
11
|
+
const tls_1 = require("./tls");
|
|
12
|
+
const react_native_nitro_buffer_1 = require("react-native-nitro-buffer");
|
|
13
|
+
function debugLog(message) {
|
|
14
|
+
if ((0, index_1.isVerbose)()) {
|
|
15
|
+
const timestamp = new Date().toISOString().split('T')[1].split('Z')[0];
|
|
16
|
+
console.log(`[HTTP DEBUG ${timestamp}] ${message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// ========== STATUS_CODES ==========
|
|
20
|
+
exports.STATUS_CODES = {
|
|
21
|
+
100: 'Continue',
|
|
22
|
+
101: 'Switching Protocols',
|
|
23
|
+
102: 'Processing',
|
|
24
|
+
200: 'OK',
|
|
25
|
+
201: 'Created',
|
|
26
|
+
202: 'Accepted',
|
|
27
|
+
203: 'Non-Authoritative Information',
|
|
28
|
+
204: 'No Content',
|
|
29
|
+
205: 'Reset Content',
|
|
30
|
+
206: 'Partial Content',
|
|
31
|
+
300: 'Multiple Choices',
|
|
32
|
+
301: 'Moved Permanently',
|
|
33
|
+
302: 'Found',
|
|
34
|
+
303: 'See Other',
|
|
35
|
+
304: 'Not Modified',
|
|
36
|
+
307: 'Temporary Redirect',
|
|
37
|
+
308: 'Permanent Redirect',
|
|
38
|
+
400: 'Bad Request',
|
|
39
|
+
401: 'Unauthorized',
|
|
40
|
+
402: 'Payment Required',
|
|
41
|
+
403: 'Forbidden',
|
|
42
|
+
404: 'Not Found',
|
|
43
|
+
405: 'Method Not Allowed',
|
|
44
|
+
406: 'Not Acceptable',
|
|
45
|
+
407: 'Proxy Authentication Required',
|
|
46
|
+
408: 'Request Timeout',
|
|
47
|
+
409: 'Conflict',
|
|
48
|
+
410: 'Gone',
|
|
49
|
+
411: 'Length Required',
|
|
50
|
+
412: 'Precondition Failed',
|
|
51
|
+
413: 'Payload Too Large',
|
|
52
|
+
414: 'URI Too Long',
|
|
53
|
+
415: 'Unsupported Media Type',
|
|
54
|
+
416: 'Range Not Satisfiable',
|
|
55
|
+
417: 'Expectation Failed',
|
|
56
|
+
418: "I'm a teapot",
|
|
57
|
+
421: 'Misdirected Request',
|
|
58
|
+
422: 'Unprocessable Entity',
|
|
59
|
+
423: 'Locked',
|
|
60
|
+
424: 'Failed Dependency',
|
|
61
|
+
425: 'Too Early',
|
|
62
|
+
426: 'Upgrade Required',
|
|
63
|
+
428: 'Precondition Required',
|
|
64
|
+
429: 'Too Many Requests',
|
|
65
|
+
431: 'Request Header Fields Too Large',
|
|
66
|
+
451: 'Unavailable For Legal Reasons',
|
|
67
|
+
500: 'Internal Server Error',
|
|
68
|
+
501: 'Not Implemented',
|
|
69
|
+
502: 'Bad Gateway',
|
|
70
|
+
503: 'Service Unavailable',
|
|
71
|
+
504: 'Gateway Timeout',
|
|
72
|
+
505: 'HTTP Version Not Supported',
|
|
73
|
+
506: 'Variant Also Negotiates',
|
|
74
|
+
507: 'Insufficient Storage',
|
|
75
|
+
508: 'Loop Detected',
|
|
76
|
+
510: 'Not Extended',
|
|
77
|
+
511: 'Network Authentication Required',
|
|
78
|
+
};
|
|
79
|
+
exports.METHODS = [
|
|
80
|
+
'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD',
|
|
81
|
+
'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL',
|
|
82
|
+
'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH',
|
|
83
|
+
'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE',
|
|
84
|
+
'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE'
|
|
85
|
+
];
|
|
86
|
+
// ========== IncomingMessage ==========
|
|
87
|
+
class IncomingMessage extends readable_stream_1.Readable {
|
|
88
|
+
constructor(socket) {
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
super({ autoDestroy: false });
|
|
91
|
+
this.httpVersion = '1.1';
|
|
92
|
+
this.httpVersionMajor = 1;
|
|
93
|
+
this.httpVersionMinor = 1;
|
|
94
|
+
this.headers = {};
|
|
95
|
+
this.rawHeaders = [];
|
|
96
|
+
this.aborted = false;
|
|
97
|
+
this.complete = false;
|
|
98
|
+
this.trailers = {};
|
|
99
|
+
this.socket = socket;
|
|
100
|
+
}
|
|
101
|
+
_read() {
|
|
102
|
+
this.socket.resume();
|
|
103
|
+
}
|
|
104
|
+
setTimeout(msecs, callback) {
|
|
105
|
+
this.socket.setTimeout(msecs, callback);
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
destroy(error) {
|
|
109
|
+
super.destroy(error);
|
|
110
|
+
this.socket.destroy();
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
setNoDelay(noDelay = true) {
|
|
114
|
+
this.socket.setNoDelay(noDelay);
|
|
115
|
+
}
|
|
116
|
+
setKeepAlive(enable = false, initialDelay = 0) {
|
|
117
|
+
this.socket.setKeepAlive(enable, initialDelay);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.IncomingMessage = IncomingMessage;
|
|
121
|
+
// ========== OutgoingMessage ==========
|
|
122
|
+
class OutgoingMessage extends readable_stream_1.Writable {
|
|
123
|
+
constructor() {
|
|
124
|
+
// @ts-ignore - disable autoDestroy to prevent socket from being destroyed when stream ends
|
|
125
|
+
super({ autoDestroy: false });
|
|
126
|
+
this.headersSent = false;
|
|
127
|
+
this._headers = {};
|
|
128
|
+
this._headerNames = {};
|
|
129
|
+
this.socket = null;
|
|
130
|
+
this.chunkedEncoding = false;
|
|
131
|
+
this._hasBody = true;
|
|
132
|
+
this._sendHeadersSent = false;
|
|
133
|
+
this.aborted = false;
|
|
134
|
+
this._trailers = null;
|
|
135
|
+
this.once('finish', () => {
|
|
136
|
+
this.emit('close');
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
destroy(error) {
|
|
140
|
+
super.destroy(error);
|
|
141
|
+
if (this.socket) {
|
|
142
|
+
this.socket.destroy();
|
|
143
|
+
}
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
setHeader(name, value) {
|
|
147
|
+
if (this.headersSent)
|
|
148
|
+
throw new Error('Cannot set headers after they are sent');
|
|
149
|
+
const key = name.toLowerCase();
|
|
150
|
+
this._headers[key] = value;
|
|
151
|
+
this._headerNames[key] = name;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
getHeader(name) {
|
|
155
|
+
return this._headers[name.toLowerCase()];
|
|
156
|
+
}
|
|
157
|
+
removeHeader(name) {
|
|
158
|
+
if (this.headersSent)
|
|
159
|
+
throw new Error('Cannot remove headers after they are sent');
|
|
160
|
+
const key = name.toLowerCase();
|
|
161
|
+
delete this._headers[key];
|
|
162
|
+
delete this._headerNames[key];
|
|
163
|
+
}
|
|
164
|
+
hasHeader(name) {
|
|
165
|
+
return name.toLowerCase() in this._headers;
|
|
166
|
+
}
|
|
167
|
+
getHeaderNames() {
|
|
168
|
+
return Object.values(this._headerNames);
|
|
169
|
+
}
|
|
170
|
+
setTimeout(ms, callback) {
|
|
171
|
+
if (this.socket) {
|
|
172
|
+
this.socket.setTimeout(ms, () => {
|
|
173
|
+
this.emit('timeout');
|
|
174
|
+
if (callback)
|
|
175
|
+
callback();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
this.once('socket', (s) => {
|
|
180
|
+
s.setTimeout(ms, () => {
|
|
181
|
+
this.emit('timeout');
|
|
182
|
+
if (callback)
|
|
183
|
+
callback();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
_renderHeaders(firstLine) {
|
|
190
|
+
let headerStr = firstLine + '\r\n';
|
|
191
|
+
for (const key in this._headers) {
|
|
192
|
+
const name = this._headerNames[key];
|
|
193
|
+
const value = this._headers[key];
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
for (const v of value) {
|
|
196
|
+
headerStr += `${name}: ${v}\r\n`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
headerStr += `${name}: ${value}\r\n`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
headerStr += '\r\n';
|
|
204
|
+
return headerStr;
|
|
205
|
+
}
|
|
206
|
+
_sendHeaders(firstLine) {
|
|
207
|
+
if (this.headersSent)
|
|
208
|
+
return;
|
|
209
|
+
// Check for Chunked Encoding
|
|
210
|
+
if (!this.hasHeader('Content-Length') && this._hasBody) {
|
|
211
|
+
this.setHeader('Transfer-Encoding', 'chunked');
|
|
212
|
+
this.chunkedEncoding = true;
|
|
213
|
+
}
|
|
214
|
+
this.headersSent = true;
|
|
215
|
+
const headerStr = this._renderHeaders(firstLine);
|
|
216
|
+
debugLog(`OutgoingMessage._sendHeaders: writing ${headerStr.length} bytes to socket (socket=${!!this.socket})`);
|
|
217
|
+
this.socket.write(react_native_nitro_buffer_1.Buffer.from(headerStr));
|
|
218
|
+
}
|
|
219
|
+
_write(chunk, encoding, callback) {
|
|
220
|
+
if (!this.socket) {
|
|
221
|
+
callback(new Error('Socket not assigned'));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (this.chunkedEncoding) {
|
|
225
|
+
const len = typeof chunk === 'string' ? react_native_nitro_buffer_1.Buffer.byteLength(chunk, encoding) : chunk.length;
|
|
226
|
+
const header = len.toString(16) + '\r\n';
|
|
227
|
+
this.socket.write(react_native_nitro_buffer_1.Buffer.from(header));
|
|
228
|
+
// Note: We don't return the backpressure status here because we are doing multiple writes
|
|
229
|
+
// The final write determines the callback.
|
|
230
|
+
this.socket.write(chunk, encoding, (err) => {
|
|
231
|
+
if (err)
|
|
232
|
+
return callback(err);
|
|
233
|
+
this.socket.write(react_native_nitro_buffer_1.Buffer.from('\r\n'), undefined, callback);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
this.socket.write(chunk, encoding, callback);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
write(chunk, encoding, callback) {
|
|
241
|
+
const ret = super.write(chunk, encoding, callback);
|
|
242
|
+
// If writableLength is too high, return false
|
|
243
|
+
// But since we are proxying to socket, we should also check socket backpressure
|
|
244
|
+
if (this.socket && this.socket._writableState) {
|
|
245
|
+
// This is a bit hacky but if we have a real Node-like socket, we respect its state
|
|
246
|
+
return ret && !this.socket._writableState.needDrain;
|
|
247
|
+
}
|
|
248
|
+
return ret;
|
|
249
|
+
}
|
|
250
|
+
// _final is called by the stream when all writes are complete before 'finish' event
|
|
251
|
+
_final(callback) {
|
|
252
|
+
if (this.chunkedEncoding && this.socket) {
|
|
253
|
+
let terminator = '0\r\n';
|
|
254
|
+
if (this._trailers) {
|
|
255
|
+
for (const [key, value] of Object.entries(this._trailers)) {
|
|
256
|
+
terminator += `${key}: ${value}\r\n`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
terminator += '\r\n';
|
|
260
|
+
this.socket.write(react_native_nitro_buffer_1.Buffer.from(terminator), undefined, (err) => {
|
|
261
|
+
callback(err);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
callback();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
addTrailers(headers) {
|
|
269
|
+
if (this.headersSent && !this.chunkedEncoding) {
|
|
270
|
+
throw new Error('Trailers can only be used with chunked encoding');
|
|
271
|
+
}
|
|
272
|
+
this._trailers = headers;
|
|
273
|
+
}
|
|
274
|
+
end(chunk, encoding, callback) {
|
|
275
|
+
debugLog(`OutgoingMessage.end() called, already ending: ${this._writableState?.ending}, chunk: ${!!chunk}`);
|
|
276
|
+
if (chunk) {
|
|
277
|
+
this.write(chunk, encoding);
|
|
278
|
+
}
|
|
279
|
+
super.end(undefined, undefined, callback);
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
setNoDelay(noDelay = true) {
|
|
283
|
+
this.socket?.setNoDelay(noDelay);
|
|
284
|
+
}
|
|
285
|
+
setSocketKeepAlive(enable = false, initialDelay = 0) {
|
|
286
|
+
this.socket?.setKeepAlive(enable, initialDelay);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
exports.OutgoingMessage = OutgoingMessage;
|
|
290
|
+
// ========== ServerResponse ==========
|
|
291
|
+
class ServerResponse extends OutgoingMessage {
|
|
292
|
+
constructor(socket) {
|
|
293
|
+
super();
|
|
294
|
+
this.statusCode = 200;
|
|
295
|
+
this.socket = socket;
|
|
296
|
+
}
|
|
297
|
+
writeHead(statusCode, statusMessage, headers) {
|
|
298
|
+
if (this.headersSent)
|
|
299
|
+
throw new Error('Cannot write headers after they are sent');
|
|
300
|
+
this.statusCode = statusCode;
|
|
301
|
+
if (typeof statusMessage === 'object') {
|
|
302
|
+
headers = statusMessage;
|
|
303
|
+
statusMessage = undefined;
|
|
304
|
+
}
|
|
305
|
+
if (statusMessage)
|
|
306
|
+
this.statusMessage = statusMessage;
|
|
307
|
+
if (headers) {
|
|
308
|
+
for (const key in headers) {
|
|
309
|
+
this.setHeader(key, headers[key]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Note: Do NOT send headers here. They will be sent on first write/end
|
|
313
|
+
// when Content-Length can be determined.
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
_sendResponseHeaders() {
|
|
317
|
+
if (this.headersSent)
|
|
318
|
+
return;
|
|
319
|
+
const firstLine = `HTTP/1.1 ${this.statusCode} ${this.statusMessage || exports.STATUS_CODES[this.statusCode] || 'OK'}`;
|
|
320
|
+
this._sendHeaders(firstLine);
|
|
321
|
+
}
|
|
322
|
+
_write(chunk, encoding, callback) {
|
|
323
|
+
if (!this.headersSent)
|
|
324
|
+
this._sendResponseHeaders();
|
|
325
|
+
super._write(chunk, encoding, callback);
|
|
326
|
+
}
|
|
327
|
+
write(chunk, encoding, callback) {
|
|
328
|
+
return super.write(chunk, encoding, callback);
|
|
329
|
+
}
|
|
330
|
+
end(chunk, encoding, callback) {
|
|
331
|
+
if (!this.headersSent) {
|
|
332
|
+
// If we have a single chunk and no headers sent yet, we can add Content-Length
|
|
333
|
+
// to avoid chunked encoding for simple responses.
|
|
334
|
+
if (chunk) {
|
|
335
|
+
const len = typeof chunk === 'string' ? react_native_nitro_buffer_1.Buffer.byteLength(chunk, encoding) : chunk.length;
|
|
336
|
+
this.setHeader('Content-Length', len);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
this.setHeader('Content-Length', 0);
|
|
340
|
+
}
|
|
341
|
+
this._sendResponseHeaders();
|
|
342
|
+
}
|
|
343
|
+
// super.end will trigger _write if chunk was provided.
|
|
344
|
+
super.end(chunk, encoding, () => {
|
|
345
|
+
if (callback)
|
|
346
|
+
callback();
|
|
347
|
+
});
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
exports.ServerResponse = ServerResponse;
|
|
352
|
+
class Server extends eventemitter3_1.EventEmitter {
|
|
353
|
+
constructor(options, requestListener) {
|
|
354
|
+
super();
|
|
355
|
+
this._httpConnections = new Set();
|
|
356
|
+
this.maxHeaderSize = 16384;
|
|
357
|
+
this.maxRequestsPerSocket = 0;
|
|
358
|
+
this.headersTimeout = 60000;
|
|
359
|
+
this.requestTimeout = 300000;
|
|
360
|
+
this.keepAliveTimeout = 5000;
|
|
361
|
+
// Use net.Server from index.ts
|
|
362
|
+
const { Server: NetServer } = require('./index');
|
|
363
|
+
this._netServer = new NetServer();
|
|
364
|
+
let listener;
|
|
365
|
+
if (typeof options === 'function') {
|
|
366
|
+
listener = options;
|
|
367
|
+
}
|
|
368
|
+
else if (options) {
|
|
369
|
+
if (options.keepAliveTimeout !== undefined)
|
|
370
|
+
this.keepAliveTimeout = options.keepAliveTimeout;
|
|
371
|
+
if (options.requestTimeout !== undefined)
|
|
372
|
+
this.requestTimeout = options.requestTimeout;
|
|
373
|
+
if (options.headersTimeout !== undefined)
|
|
374
|
+
this.headersTimeout = options.headersTimeout;
|
|
375
|
+
if (options.maxHeaderSize !== undefined)
|
|
376
|
+
this.maxHeaderSize = options.maxHeaderSize;
|
|
377
|
+
if (options.maxRequestsPerSocket !== undefined)
|
|
378
|
+
this.maxRequestsPerSocket = options.maxRequestsPerSocket;
|
|
379
|
+
listener = requestListener;
|
|
380
|
+
}
|
|
381
|
+
if (listener) {
|
|
382
|
+
this.on('request', listener);
|
|
383
|
+
}
|
|
384
|
+
// Forward net.Server events
|
|
385
|
+
this._netServer.on('listening', () => this.emit('listening'));
|
|
386
|
+
this._netServer.on('close', () => this.emit('close'));
|
|
387
|
+
this._netServer.on('error', (err) => this.emit('error', err));
|
|
388
|
+
this._netServer.on('connection', (socket) => {
|
|
389
|
+
this._setupHttpConnection(socket);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
_setupHttpConnection(socket) {
|
|
393
|
+
this._httpConnections.add(socket);
|
|
394
|
+
let req = null;
|
|
395
|
+
let res = null;
|
|
396
|
+
const parser = Driver_1.Driver.createHttpParser(0); // 0 = Request mode
|
|
397
|
+
// @ts-ignore
|
|
398
|
+
let bodyBytesRead = 0;
|
|
399
|
+
// @ts-ignore
|
|
400
|
+
let contentLength = -1;
|
|
401
|
+
// headersTimeout logic
|
|
402
|
+
let headersTimer = null;
|
|
403
|
+
if (this.headersTimeout > 0) {
|
|
404
|
+
headersTimer = setTimeout(() => {
|
|
405
|
+
debugLog(`Server: headersTimeout reached for socket, destroying`);
|
|
406
|
+
socket.destroy();
|
|
407
|
+
}, this.headersTimeout);
|
|
408
|
+
}
|
|
409
|
+
const onData = (data) => {
|
|
410
|
+
const handleParsedResult = (result) => {
|
|
411
|
+
if (result.startsWith('ERROR:')) {
|
|
412
|
+
if (headersTimer)
|
|
413
|
+
clearTimeout(headersTimer);
|
|
414
|
+
this.emit('error', new Error(result));
|
|
415
|
+
socket.destroy();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const parsed = JSON.parse(result);
|
|
419
|
+
if (parsed.is_headers) {
|
|
420
|
+
if (headersTimer) {
|
|
421
|
+
clearTimeout(headersTimer);
|
|
422
|
+
headersTimer = null;
|
|
423
|
+
}
|
|
424
|
+
// Handle CONNECT method (HTTP Tunneling)
|
|
425
|
+
if (parsed.is_connect) {
|
|
426
|
+
const req = new IncomingMessage(socket);
|
|
427
|
+
req.method = parsed.method;
|
|
428
|
+
req.url = parsed.path;
|
|
429
|
+
req.httpVersion = '1.' + parsed.version;
|
|
430
|
+
req.headers = parsed.headers;
|
|
431
|
+
// Remove our data listener to stop feeding the parser
|
|
432
|
+
// The user is responsible for handling the socket data stream from now on
|
|
433
|
+
socket.removeListener('data', onData);
|
|
434
|
+
debugLog(`Server: CONNECT request received, emitting 'connect' event`);
|
|
435
|
+
// TODO: retrieve any remaining body from parser as 'head'
|
|
436
|
+
const head = react_native_nitro_buffer_1.Buffer.alloc(0);
|
|
437
|
+
if (this.listenerCount('connect') > 0) {
|
|
438
|
+
this.emit('connect', req, socket, head);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
// Default behavior: close connection if no listener
|
|
442
|
+
socket.destroy();
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const currentReq = new IncomingMessage(socket);
|
|
447
|
+
currentReq.method = parsed.method;
|
|
448
|
+
currentReq.url = parsed.path;
|
|
449
|
+
currentReq.httpVersion = '1.' + parsed.version;
|
|
450
|
+
currentReq.headers = parsed.headers;
|
|
451
|
+
req = currentReq;
|
|
452
|
+
const currentRes = new ServerResponse(socket);
|
|
453
|
+
res = currentRes;
|
|
454
|
+
// Support Keep-Alive: reset state once response is done
|
|
455
|
+
currentRes.on('finish', () => {
|
|
456
|
+
req = null;
|
|
457
|
+
res = null;
|
|
458
|
+
// The parser should already be reset in Rust
|
|
459
|
+
});
|
|
460
|
+
const upgrade = req.headers['upgrade'];
|
|
461
|
+
if (upgrade && this.listenerCount('upgrade') > 0) {
|
|
462
|
+
debugLog(`Server: Upgrade request received, emitting 'upgrade' event`);
|
|
463
|
+
this.emit('upgrade', req, socket, react_native_nitro_buffer_1.Buffer.alloc(0));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const expect = req.headers['expect'];
|
|
467
|
+
if (expect && (typeof expect === 'string' && expect.toLowerCase() === '100-continue')) {
|
|
468
|
+
if (this.listenerCount('checkContinue') > 0) {
|
|
469
|
+
this.emit('checkContinue', req, res);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
socket.write(react_native_nitro_buffer_1.Buffer.from('HTTP/1.1 100 Continue\r\n\r\n'));
|
|
473
|
+
this.emit('request', req, res);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
debugLog(`Server: Emitting 'request' for ${req.method} ${req.url}`);
|
|
478
|
+
this.emit('request', req, res);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (req && parsed.body && parsed.body.length > 0) {
|
|
482
|
+
req.push(react_native_nitro_buffer_1.Buffer.from(parsed.body));
|
|
483
|
+
}
|
|
484
|
+
if (req && parsed.complete) {
|
|
485
|
+
req.complete = true;
|
|
486
|
+
if (parsed.trailers) {
|
|
487
|
+
req.trailers = parsed.trailers;
|
|
488
|
+
}
|
|
489
|
+
req.push(null);
|
|
490
|
+
}
|
|
491
|
+
// For Keep-Alive, try to parse remaining buffer in case of pipelining
|
|
492
|
+
if (parsed.complete && !req) {
|
|
493
|
+
// This case is handled by the feed loop if multiple messages in data
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
let input = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
497
|
+
let iterations = 0;
|
|
498
|
+
const maxIterations = 100; // Safety limit
|
|
499
|
+
while (iterations < maxIterations) {
|
|
500
|
+
iterations++;
|
|
501
|
+
const result = parser.feed(input);
|
|
502
|
+
if (!result || result === '' || result.startsWith('ERROR:')) {
|
|
503
|
+
// Empty result (partial) or error - exit loop
|
|
504
|
+
if (result && result.startsWith('ERROR:')) {
|
|
505
|
+
console.log(`[HTTP] Server: Parser error: ${result}`);
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
handleParsedResult(result);
|
|
510
|
+
input = new ArrayBuffer(0); // Continue with empty input to drain Rust buffer
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
socket.on('data', onData);
|
|
514
|
+
// CRITICAL: Ensure server-side socket starts flowing!
|
|
515
|
+
socket.resume();
|
|
516
|
+
socket.on('close', () => {
|
|
517
|
+
if (headersTimer)
|
|
518
|
+
clearTimeout(headersTimer);
|
|
519
|
+
this._httpConnections.delete(socket);
|
|
520
|
+
if (req && !req.readableEnded) {
|
|
521
|
+
req.push(null);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
socket.on('error', (err) => {
|
|
525
|
+
if (req)
|
|
526
|
+
req.emit('error', err);
|
|
527
|
+
else
|
|
528
|
+
this.emit('error', err);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
listen(...args) {
|
|
532
|
+
this._netServer.listen(...args);
|
|
533
|
+
return this;
|
|
534
|
+
}
|
|
535
|
+
close(callback) {
|
|
536
|
+
this._netServer.close(callback);
|
|
537
|
+
return this;
|
|
538
|
+
}
|
|
539
|
+
// @ts-ignore
|
|
540
|
+
async [Symbol.asyncDispose]() {
|
|
541
|
+
return new Promise((resolve) => {
|
|
542
|
+
this.close(() => resolve());
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
address() {
|
|
546
|
+
return this._netServer.address();
|
|
547
|
+
}
|
|
548
|
+
get listening() {
|
|
549
|
+
return this._netServer.listening;
|
|
550
|
+
}
|
|
551
|
+
setTimeout(ms, callback) {
|
|
552
|
+
this._netServer.setTimeout(ms, callback);
|
|
553
|
+
return this;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
exports.Server = Server;
|
|
557
|
+
class Agent extends eventemitter3_1.EventEmitter {
|
|
558
|
+
/**
|
|
559
|
+
* Gets the proxy URL for the given request options.
|
|
560
|
+
* Checks HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables.
|
|
561
|
+
*
|
|
562
|
+
* @param options Request options to determine if proxy should be used
|
|
563
|
+
* @returns Proxy URL or null if no proxy should be used
|
|
564
|
+
*/
|
|
565
|
+
getProxy(options) {
|
|
566
|
+
// If explicitly set on agent, use that
|
|
567
|
+
if (this.proxy)
|
|
568
|
+
return this.proxy;
|
|
569
|
+
// Check environment variables (React Native may not have process.env)
|
|
570
|
+
const env = typeof process !== 'undefined' && process.env ? process.env : {};
|
|
571
|
+
const isHttps = options.protocol === 'https:';
|
|
572
|
+
const host = options.hostname || options.host || 'localhost';
|
|
573
|
+
// Check NO_PROXY first
|
|
574
|
+
const noProxy = env.NO_PROXY || env.no_proxy;
|
|
575
|
+
if (noProxy) {
|
|
576
|
+
const noProxyList = noProxy.split(',').map(s => s.trim().toLowerCase());
|
|
577
|
+
const hostLower = host.toLowerCase();
|
|
578
|
+
for (const pattern of noProxyList) {
|
|
579
|
+
if (pattern === '*')
|
|
580
|
+
return null;
|
|
581
|
+
if (pattern.startsWith('.') && hostLower.endsWith(pattern))
|
|
582
|
+
return null;
|
|
583
|
+
if (hostLower === pattern)
|
|
584
|
+
return null;
|
|
585
|
+
if (hostLower.endsWith('.' + pattern))
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Get proxy URL based on protocol
|
|
590
|
+
const proxyUrl = isHttps
|
|
591
|
+
? (env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy)
|
|
592
|
+
: (env.HTTP_PROXY || env.http_proxy);
|
|
593
|
+
return proxyUrl || null;
|
|
594
|
+
}
|
|
595
|
+
constructor(options) {
|
|
596
|
+
super();
|
|
597
|
+
this.maxSockets = Infinity;
|
|
598
|
+
this.maxTotalSockets = Infinity;
|
|
599
|
+
this.maxFreeSockets = 256;
|
|
600
|
+
this.keepAlive = false;
|
|
601
|
+
this.keepAliveMsecs = 1000;
|
|
602
|
+
this.maxCachedSessions = 100;
|
|
603
|
+
this.scheduling = 'lifo';
|
|
604
|
+
this.requests = {};
|
|
605
|
+
this.sockets = {};
|
|
606
|
+
this.freeSockets = {};
|
|
607
|
+
this._totalSockets = 0;
|
|
608
|
+
this.proxy = null;
|
|
609
|
+
if (options?.maxSockets)
|
|
610
|
+
this.maxSockets = options.maxSockets;
|
|
611
|
+
if (options?.maxTotalSockets)
|
|
612
|
+
this.maxTotalSockets = options.maxTotalSockets;
|
|
613
|
+
if (options?.maxFreeSockets)
|
|
614
|
+
this.maxFreeSockets = options.maxFreeSockets;
|
|
615
|
+
if (options?.keepAlive)
|
|
616
|
+
this.keepAlive = options.keepAlive;
|
|
617
|
+
if (options?.keepAliveMsecs)
|
|
618
|
+
this.keepAliveMsecs = options.keepAliveMsecs;
|
|
619
|
+
if (options?.scheduling)
|
|
620
|
+
this.scheduling = options.scheduling;
|
|
621
|
+
if (options?.maxCachedSessions !== undefined)
|
|
622
|
+
this.maxCachedSessions = options.maxCachedSessions;
|
|
623
|
+
}
|
|
624
|
+
getName(options) {
|
|
625
|
+
let name = `${options.host || options.hostname || 'localhost'}:${options.port || (options.protocol === 'https:' ? 443 : 80)}:`;
|
|
626
|
+
if (options.localAddress)
|
|
627
|
+
name += `${options.localAddress}:`;
|
|
628
|
+
if (options.family)
|
|
629
|
+
name += `${options.family}:`;
|
|
630
|
+
return name;
|
|
631
|
+
}
|
|
632
|
+
addRequest(req, options) {
|
|
633
|
+
const name = this.getName(options);
|
|
634
|
+
debugLog(`Agent.addRequest: name=${name}, totalSockets=${this._totalSockets}`);
|
|
635
|
+
// 1. Check if there's an idle socket in freeSockets
|
|
636
|
+
if (this.freeSockets[name] && this.freeSockets[name].length > 0) {
|
|
637
|
+
const socket = this.scheduling === 'lifo'
|
|
638
|
+
? this.freeSockets[name].pop()
|
|
639
|
+
: this.freeSockets[name].shift();
|
|
640
|
+
if (this.freeSockets[name].length === 0)
|
|
641
|
+
delete this.freeSockets[name];
|
|
642
|
+
// Re-use socket
|
|
643
|
+
if (!this.sockets[name])
|
|
644
|
+
this.sockets[name] = [];
|
|
645
|
+
this.sockets[name].push(socket);
|
|
646
|
+
req.onSocket(socket);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
// 2. Check if we can create a new connection
|
|
650
|
+
const currentSockets = (this.sockets[name]?.length || 0);
|
|
651
|
+
if (currentSockets < this.maxSockets && this._totalSockets < this.maxTotalSockets) {
|
|
652
|
+
if (!this.sockets[name])
|
|
653
|
+
this.sockets[name] = [];
|
|
654
|
+
// Increment total sockets early
|
|
655
|
+
this._totalSockets++;
|
|
656
|
+
// ClientRequest handles connection but we signal it to proceed
|
|
657
|
+
req.onSocket(null);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
// 3. Queue the request
|
|
661
|
+
if (!this.requests[name])
|
|
662
|
+
this.requests[name] = [];
|
|
663
|
+
this.requests[name].push(req);
|
|
664
|
+
}
|
|
665
|
+
createConnection(options, callback) {
|
|
666
|
+
const name = this.getName(options);
|
|
667
|
+
const isHttps = options.protocol === 'https:';
|
|
668
|
+
const port = options.port || (isHttps ? 443 : 80);
|
|
669
|
+
const host = options.hostname || options.host || 'localhost';
|
|
670
|
+
debugLog(`Agent.createConnection: name=${name}, isHttps=${isHttps}, host=${host}, port=${port}`);
|
|
671
|
+
// Build clean connection options - DO NOT pass HTTP path as it will be confused with Unix socket path
|
|
672
|
+
const connectOptions = {
|
|
673
|
+
host: host,
|
|
674
|
+
port: port,
|
|
675
|
+
};
|
|
676
|
+
if (isHttps) {
|
|
677
|
+
connectOptions.servername = options.servername || host;
|
|
678
|
+
connectOptions.rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
679
|
+
if (options.ca)
|
|
680
|
+
connectOptions.ca = options.ca;
|
|
681
|
+
if (options.cert)
|
|
682
|
+
connectOptions.cert = options.cert;
|
|
683
|
+
if (options.key)
|
|
684
|
+
connectOptions.key = options.key;
|
|
685
|
+
}
|
|
686
|
+
const socket = isHttps ? new tls_1.TLSSocket(connectOptions) : new index_1.Socket();
|
|
687
|
+
// Re-emit keylog events from TLSSockets
|
|
688
|
+
if (isHttps) {
|
|
689
|
+
socket.on('keylog', (line) => {
|
|
690
|
+
// @ts-ignore - Agent is an EventEmitter via Node-like inheritance or internal use
|
|
691
|
+
this.emit('keylog', line, socket);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
let called = false;
|
|
695
|
+
const onConnected = () => {
|
|
696
|
+
if (called)
|
|
697
|
+
return;
|
|
698
|
+
called = true;
|
|
699
|
+
debugLog(`Agent.createConnection: socket ${isHttps ? 'SECURE_CONNECTED' : 'CONNECTED'} for ${name}`);
|
|
700
|
+
callback(null, socket);
|
|
701
|
+
};
|
|
702
|
+
if (isHttps) {
|
|
703
|
+
socket.on('secureConnect', onConnected);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
socket.on('connect', onConnected);
|
|
707
|
+
}
|
|
708
|
+
socket.on('error', (err) => {
|
|
709
|
+
if (called)
|
|
710
|
+
return;
|
|
711
|
+
called = true;
|
|
712
|
+
debugLog(`Agent.createConnection: socket ERROR for ${name}: ${err.message}`);
|
|
713
|
+
this._totalSockets--;
|
|
714
|
+
if (this.sockets[name]) {
|
|
715
|
+
const idx = this.sockets[name].indexOf(socket);
|
|
716
|
+
if (idx !== -1)
|
|
717
|
+
this.sockets[name].splice(idx, 1);
|
|
718
|
+
}
|
|
719
|
+
callback(err, null);
|
|
720
|
+
});
|
|
721
|
+
socket.connect(connectOptions);
|
|
722
|
+
if (!this.sockets[name])
|
|
723
|
+
this.sockets[name] = [];
|
|
724
|
+
this.sockets[name].push(socket);
|
|
725
|
+
return socket;
|
|
726
|
+
}
|
|
727
|
+
releaseSocket(socket, options) {
|
|
728
|
+
const name = this.getName(options);
|
|
729
|
+
// Remove from active sockets
|
|
730
|
+
if (this.sockets[name]) {
|
|
731
|
+
const idx = this.sockets[name].indexOf(socket);
|
|
732
|
+
if (idx !== -1)
|
|
733
|
+
this.sockets[name].splice(idx, 1);
|
|
734
|
+
if (this.sockets[name].length === 0)
|
|
735
|
+
delete this.sockets[name];
|
|
736
|
+
}
|
|
737
|
+
const onClose = () => {
|
|
738
|
+
debugLog(`Agent: socket closed while in pool, removing from ${name}`);
|
|
739
|
+
this._removeSocket(socket, name);
|
|
740
|
+
};
|
|
741
|
+
socket.once('close', onClose);
|
|
742
|
+
socket.once('error', onClose);
|
|
743
|
+
socket._agentOnClose = onClose;
|
|
744
|
+
// Check if there are pending requests - ALWAYS reuse if something is waiting
|
|
745
|
+
if (this.requests[name] && this.requests[name].length > 0) {
|
|
746
|
+
const req = this.requests[name].shift();
|
|
747
|
+
if (this.requests[name].length === 0)
|
|
748
|
+
delete this.requests[name];
|
|
749
|
+
if (!this.sockets[name])
|
|
750
|
+
this.sockets[name] = [];
|
|
751
|
+
this.sockets[name].push(socket);
|
|
752
|
+
this.reuseSocket(socket, req);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (this.keepAlive && this.keepSocketAlive(socket)) {
|
|
756
|
+
// Return to free pool
|
|
757
|
+
if (!this.freeSockets[name])
|
|
758
|
+
this.freeSockets[name] = [];
|
|
759
|
+
if (this.freeSockets[name].length < this.maxFreeSockets) {
|
|
760
|
+
this.freeSockets[name].push(socket);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
this._totalSockets--;
|
|
764
|
+
socket.end();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
this._totalSockets--;
|
|
769
|
+
socket.end();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
keepSocketAlive(_socket) {
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
reuseSocket(socket, req) {
|
|
776
|
+
debugLog(`Agent.reuseSocket: reusing socket for ${req.method} ${req.path}`);
|
|
777
|
+
// Remove agent listeners before reusing
|
|
778
|
+
if (socket._agentOnClose) {
|
|
779
|
+
socket.removeListener('close', socket._agentOnClose);
|
|
780
|
+
socket.removeListener('error', socket._agentOnClose);
|
|
781
|
+
delete socket._agentOnClose;
|
|
782
|
+
}
|
|
783
|
+
req.onSocket(socket);
|
|
784
|
+
}
|
|
785
|
+
_removeSocket(socket, name) {
|
|
786
|
+
if (this.sockets[name]) {
|
|
787
|
+
const idx = this.sockets[name].indexOf(socket);
|
|
788
|
+
if (idx !== -1) {
|
|
789
|
+
this.sockets[name].splice(idx, 1);
|
|
790
|
+
this._totalSockets--;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (this.freeSockets[name]) {
|
|
794
|
+
const idx = this.freeSockets[name].indexOf(socket);
|
|
795
|
+
if (idx !== -1) {
|
|
796
|
+
this.freeSockets[name].splice(idx, 1);
|
|
797
|
+
this._totalSockets--;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
destroy() {
|
|
802
|
+
for (const name in this.sockets) {
|
|
803
|
+
for (const socket of this.sockets[name]) {
|
|
804
|
+
socket.destroy();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
for (const name in this.freeSockets) {
|
|
808
|
+
for (const socket of this.freeSockets[name]) {
|
|
809
|
+
socket.destroy();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
exports.Agent = Agent;
|
|
815
|
+
exports.globalAgent = new Agent();
|
|
816
|
+
class ClientRequest extends OutgoingMessage {
|
|
817
|
+
constructor(options, callback) {
|
|
818
|
+
super();
|
|
819
|
+
this._connected = false;
|
|
820
|
+
this._pendingWrites = [];
|
|
821
|
+
this._ended = false;
|
|
822
|
+
this._expectContinue = false;
|
|
823
|
+
this._continueReceived = false;
|
|
824
|
+
this._options = options;
|
|
825
|
+
this.method = options.method || 'GET';
|
|
826
|
+
this.path = options.path || '/';
|
|
827
|
+
this.host = options.hostname || options.host || 'localhost';
|
|
828
|
+
if (['GET', 'HEAD'].includes(this.method.toUpperCase())) {
|
|
829
|
+
this._hasBody = false;
|
|
830
|
+
}
|
|
831
|
+
if (options.headers) {
|
|
832
|
+
for (const key in options.headers) {
|
|
833
|
+
this.setHeader(key, options.headers[key]);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (callback) {
|
|
837
|
+
this.once('response', callback);
|
|
838
|
+
}
|
|
839
|
+
const expect = this.getHeader('expect');
|
|
840
|
+
if (expect && typeof expect === 'string' && expect.toLowerCase() === '100-continue') {
|
|
841
|
+
this._expectContinue = true;
|
|
842
|
+
}
|
|
843
|
+
if (options.timeout) {
|
|
844
|
+
this.setTimeout(options.timeout);
|
|
845
|
+
}
|
|
846
|
+
const agent = options.agent === false ? new Agent() : (options.agent instanceof Agent ? options.agent : exports.globalAgent);
|
|
847
|
+
// Use setImmediate or setTimeout for React Native compatibility
|
|
848
|
+
const nextTick = typeof setImmediate !== 'undefined' ? setImmediate : (fn) => setTimeout(fn, 0);
|
|
849
|
+
nextTick(() => {
|
|
850
|
+
debugLog(`ClientRequest: nextTick fired for ${this.method} ${this.host}${this.path}`);
|
|
851
|
+
agent.addRequest(this, this._options);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
/** @internal */
|
|
855
|
+
onSocket(socket) {
|
|
856
|
+
if (socket) {
|
|
857
|
+
this.socket = socket;
|
|
858
|
+
this._connected = true;
|
|
859
|
+
this.emit('socket', this.socket);
|
|
860
|
+
this._sendRequest();
|
|
861
|
+
this._flushPendingWrites();
|
|
862
|
+
this._attachSocketListeners();
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
this._connect();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
_connect() {
|
|
869
|
+
const agent = this._options.agent === false ? new Agent() : (this._options.agent instanceof Agent ? this._options.agent : exports.globalAgent);
|
|
870
|
+
const connectCallback = (err, socket) => {
|
|
871
|
+
if (err) {
|
|
872
|
+
debugLog(`ClientRequest._connect: ERROR: ${err.message}`);
|
|
873
|
+
this.emit('error', err);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
debugLog(`ClientRequest._connect: Socket connected! socket=${!!socket}, socket._driver=${!!socket._driver}`);
|
|
877
|
+
console.log(`[HTTP] _connect: Socket connected!`);
|
|
878
|
+
this.socket = socket;
|
|
879
|
+
this._connected = true;
|
|
880
|
+
this.emit('socket', this.socket);
|
|
881
|
+
debugLog(`ClientRequest._connect: Calling _sendRequest`);
|
|
882
|
+
this._sendRequest();
|
|
883
|
+
this._flushPendingWrites();
|
|
884
|
+
this._attachSocketListeners();
|
|
885
|
+
};
|
|
886
|
+
this.socket = agent.createConnection(this._options, connectCallback);
|
|
887
|
+
}
|
|
888
|
+
_attachSocketListeners() {
|
|
889
|
+
if (!this.socket)
|
|
890
|
+
return;
|
|
891
|
+
const parser = Driver_1.Driver.createHttpParser(1); // 1 = Response mode
|
|
892
|
+
const onData = (data) => {
|
|
893
|
+
const handleParsedResult = (result) => {
|
|
894
|
+
if (result.startsWith('ERROR:')) {
|
|
895
|
+
this.emit('error', new Error(result));
|
|
896
|
+
this.socket.destroy();
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const parsed = JSON.parse(result);
|
|
900
|
+
console.log(`[HTTP] _connect: Parser result: ${parsed.is_headers ? 'HEADERS' : 'DATA'}${parsed.complete ? ' (COMPLETE)' : ''}`);
|
|
901
|
+
if (parsed.is_headers) {
|
|
902
|
+
const status = parsed.status || 0;
|
|
903
|
+
if (status >= 100 && status < 200 && status !== 101) {
|
|
904
|
+
const info = {
|
|
905
|
+
httpVersion: '1.' + parsed.version,
|
|
906
|
+
httpVersionMajor: 1,
|
|
907
|
+
httpVersionMinor: parsed.version,
|
|
908
|
+
statusCode: status,
|
|
909
|
+
statusMessage: exports.STATUS_CODES[status] || '',
|
|
910
|
+
headers: parsed.headers,
|
|
911
|
+
rawHeaders: []
|
|
912
|
+
};
|
|
913
|
+
if (status === 100) {
|
|
914
|
+
this._continueReceived = true;
|
|
915
|
+
this.emit('continue');
|
|
916
|
+
this._flushPendingWrites();
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
this.emit('information', info);
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
this._res = new IncomingMessage(this.socket);
|
|
924
|
+
this._res.statusCode = status;
|
|
925
|
+
this._res.httpVersion = '1.' + parsed.version;
|
|
926
|
+
this._res.headers = parsed.headers;
|
|
927
|
+
if (status === 101) {
|
|
928
|
+
debugLog(`ClientRequest: 101 Switching Protocols received, detaching parser`);
|
|
929
|
+
this.socket.removeListener('data', onData);
|
|
930
|
+
this.socket.removeListener('error', onError);
|
|
931
|
+
this.emit('upgrade', this._res, this.socket, react_native_nitro_buffer_1.Buffer.alloc(0));
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
// Handle CONNECT method response (HTTP Tunneling)
|
|
935
|
+
if (this.method.toUpperCase() === 'CONNECT' && status >= 200 && status < 300) {
|
|
936
|
+
debugLog(`ClientRequest: CONNECT tunnel established (status=${status}), emitting 'connect' event`);
|
|
937
|
+
this.socket.removeListener('data', onData);
|
|
938
|
+
this.socket.removeListener('error', onError);
|
|
939
|
+
this.emit('connect', this._res, this.socket, react_native_nitro_buffer_1.Buffer.alloc(0));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
this.emit('response', this._res);
|
|
943
|
+
}
|
|
944
|
+
if (this._res && parsed.body && parsed.body.length > 0) {
|
|
945
|
+
this._res.push(react_native_nitro_buffer_1.Buffer.from(parsed.body));
|
|
946
|
+
}
|
|
947
|
+
if (this._res && parsed.complete) {
|
|
948
|
+
this._res.complete = true;
|
|
949
|
+
if (parsed.trailers) {
|
|
950
|
+
this._res.trailers = parsed.trailers;
|
|
951
|
+
}
|
|
952
|
+
this._res.push(null);
|
|
953
|
+
this._finishResponse();
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
let input = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
957
|
+
let iterations = 0;
|
|
958
|
+
const maxIterations = 100; // Safety limit
|
|
959
|
+
while (iterations < maxIterations) {
|
|
960
|
+
iterations++;
|
|
961
|
+
const result = parser.feed(input);
|
|
962
|
+
if (!result || result === '' || result.startsWith('ERROR:')) {
|
|
963
|
+
// Empty result (partial) or error - exit loop
|
|
964
|
+
if (result && result.startsWith('ERROR:')) {
|
|
965
|
+
console.log(`[HTTP] ClientRequest: Parser error: ${result}`);
|
|
966
|
+
}
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
handleParsedResult(result);
|
|
970
|
+
input = new ArrayBuffer(0); // Continue with empty input to drain Rust buffer
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
const onError = (err) => {
|
|
974
|
+
console.log(`[HTTP] _connect: Socket error: ${err.message}`);
|
|
975
|
+
this.emit('error', err);
|
|
976
|
+
this._cleanupSocket();
|
|
977
|
+
};
|
|
978
|
+
const onClose = () => {
|
|
979
|
+
console.log(`[HTTP] _connect: Socket closed`);
|
|
980
|
+
if (this._res && !this._res.readableEnded)
|
|
981
|
+
this._res.push(null);
|
|
982
|
+
this.emit('close');
|
|
983
|
+
this._cleanupSocket();
|
|
984
|
+
};
|
|
985
|
+
this.socket.on('data', onData);
|
|
986
|
+
this.socket.on('error', onError);
|
|
987
|
+
this.socket.on('close', onClose);
|
|
988
|
+
this._socketCleanup = () => {
|
|
989
|
+
this.socket?.removeListener('data', onData);
|
|
990
|
+
this.socket?.removeListener('error', onError);
|
|
991
|
+
this.socket?.removeListener('close', onClose);
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
_cleanupSocket() {
|
|
995
|
+
if (this._socketCleanup)
|
|
996
|
+
this._socketCleanup();
|
|
997
|
+
this._socketCleanup = undefined;
|
|
998
|
+
this.socket = null;
|
|
999
|
+
this._connected = false;
|
|
1000
|
+
}
|
|
1001
|
+
_finishResponse() {
|
|
1002
|
+
// Release socket back to agent
|
|
1003
|
+
const agent = this._options.agent === false ? new Agent() : (this._options.agent instanceof Agent ? this._options.agent : exports.globalAgent);
|
|
1004
|
+
const socket = this.socket;
|
|
1005
|
+
this._cleanupSocket();
|
|
1006
|
+
if (socket)
|
|
1007
|
+
agent.releaseSocket(socket, this._options);
|
|
1008
|
+
}
|
|
1009
|
+
_flushPendingWrites() {
|
|
1010
|
+
if (!this.socket)
|
|
1011
|
+
return;
|
|
1012
|
+
if (!this.headersSent)
|
|
1013
|
+
this._sendRequest();
|
|
1014
|
+
// If we are waiting for 100-continue, don't flush yet
|
|
1015
|
+
if (this._expectContinue && !this._continueReceived) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const writes = this._pendingWrites;
|
|
1019
|
+
this._pendingWrites = [];
|
|
1020
|
+
for (const pending of writes) {
|
|
1021
|
+
this._write(pending.chunk, pending.encoding, pending.callback);
|
|
1022
|
+
}
|
|
1023
|
+
if (this._ended) {
|
|
1024
|
+
this._finishRequest();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
_finishRequest() {
|
|
1028
|
+
if (!this._ended)
|
|
1029
|
+
return;
|
|
1030
|
+
super.end();
|
|
1031
|
+
}
|
|
1032
|
+
_sendRequest() {
|
|
1033
|
+
debugLog(`ClientRequest._sendRequest: headersSent=${this.headersSent}, socket=${!!this.socket}`);
|
|
1034
|
+
if (this.headersSent)
|
|
1035
|
+
return;
|
|
1036
|
+
if (!this.hasHeader('host')) {
|
|
1037
|
+
this.setHeader('Host', this.host);
|
|
1038
|
+
}
|
|
1039
|
+
const firstLine = `${this.method} ${this.path} HTTP/1.1`;
|
|
1040
|
+
debugLog(`ClientRequest._sendRequest: sending firstLine=${firstLine}`);
|
|
1041
|
+
this._sendHeaders(firstLine);
|
|
1042
|
+
}
|
|
1043
|
+
_write(chunk, encoding, callback) {
|
|
1044
|
+
if (!this._connected) {
|
|
1045
|
+
this._pendingWrites.push({ chunk, encoding, callback });
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (!this.headersSent)
|
|
1049
|
+
this._sendRequest();
|
|
1050
|
+
super._write(chunk, encoding, callback);
|
|
1051
|
+
}
|
|
1052
|
+
write(chunk, encoding, callback) {
|
|
1053
|
+
if (!this._connected) {
|
|
1054
|
+
this._pendingWrites.push({ chunk, encoding, callback });
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
if (!this.headersSent)
|
|
1058
|
+
this._sendRequest();
|
|
1059
|
+
return super.write(chunk, encoding, callback);
|
|
1060
|
+
}
|
|
1061
|
+
end(chunk, encoding, callback) {
|
|
1062
|
+
debugLog(`ClientRequest.end() called, connected=${this._connected}, headersSent=${this.headersSent}`);
|
|
1063
|
+
if (chunk) {
|
|
1064
|
+
this.write(chunk, encoding);
|
|
1065
|
+
}
|
|
1066
|
+
this._ended = true;
|
|
1067
|
+
// If connected, we can send request and end immediately
|
|
1068
|
+
if (this._connected) {
|
|
1069
|
+
if (!this.headersSent) {
|
|
1070
|
+
this._sendRequest();
|
|
1071
|
+
}
|
|
1072
|
+
// Call super.end only when connected
|
|
1073
|
+
super.end(undefined, undefined, callback);
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
// Socket not connected yet - _flushPendingWrites will handle ending
|
|
1077
|
+
// Store callback if provided
|
|
1078
|
+
if (callback) {
|
|
1079
|
+
this.once('finish', callback);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return this;
|
|
1083
|
+
}
|
|
1084
|
+
abort() {
|
|
1085
|
+
if (this.aborted)
|
|
1086
|
+
return;
|
|
1087
|
+
this.aborted = true;
|
|
1088
|
+
this.emit('abort');
|
|
1089
|
+
this.destroy();
|
|
1090
|
+
}
|
|
1091
|
+
flushHeaders() {
|
|
1092
|
+
if (this._connected && !this.headersSent) {
|
|
1093
|
+
this._sendRequest();
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
exports.ClientRequest = ClientRequest;
|
|
1098
|
+
function createServer(optionsOrListener, requestListener) {
|
|
1099
|
+
return new Server(optionsOrListener, requestListener);
|
|
1100
|
+
}
|
|
1101
|
+
function request(urlOrOptions, optionsOrCallback, callback) {
|
|
1102
|
+
let opts = {};
|
|
1103
|
+
let cb = callback;
|
|
1104
|
+
if (typeof urlOrOptions === 'string') {
|
|
1105
|
+
const url = new URL(urlOrOptions);
|
|
1106
|
+
opts = {
|
|
1107
|
+
protocol: url.protocol,
|
|
1108
|
+
hostname: url.hostname,
|
|
1109
|
+
path: url.pathname + url.search,
|
|
1110
|
+
port: url.port ? parseInt(url.port) : undefined
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
else if (urlOrOptions instanceof URL) {
|
|
1114
|
+
opts = {
|
|
1115
|
+
protocol: urlOrOptions.protocol,
|
|
1116
|
+
hostname: urlOrOptions.hostname,
|
|
1117
|
+
path: urlOrOptions.pathname + urlOrOptions.search,
|
|
1118
|
+
port: urlOrOptions.port ? parseInt(urlOrOptions.port) : undefined
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
opts = urlOrOptions;
|
|
1123
|
+
}
|
|
1124
|
+
// Handle (url, options, callback) or (url, callback) signatures
|
|
1125
|
+
if (typeof optionsOrCallback === 'function') {
|
|
1126
|
+
cb = optionsOrCallback;
|
|
1127
|
+
}
|
|
1128
|
+
else if (optionsOrCallback) {
|
|
1129
|
+
// Merge options
|
|
1130
|
+
opts = { ...opts, ...optionsOrCallback };
|
|
1131
|
+
}
|
|
1132
|
+
return new ClientRequest(opts, cb);
|
|
1133
|
+
}
|
|
1134
|
+
function get(urlOrOptions, optionsOrCallback, callback) {
|
|
1135
|
+
const req = request(urlOrOptions, optionsOrCallback, callback);
|
|
1136
|
+
req.end();
|
|
1137
|
+
return req;
|
|
1138
|
+
}
|