-
Notifications
You must be signed in to change notification settings - Fork 42
/
Http11Server.ts
219 lines (189 loc) · 11.5 KB
/
Http11Server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import ModuleTemp from 'module';
import * as http from 'http';
import { AddressInfo, Socket } from 'net';
import * as path from 'path';
import * as stream from 'stream';
import InvocationRequest from '../../../InvocationData/InvocationRequest';
import ModuleSourceType from '../../../InvocationData/ModuleSourceType';
import { getTempIdentifier, respondWithError, setup } from './Shared';
// The typings for module are incomplete and can't be augmented, so import as any.
const Module = ModuleTemp as any;
// Setup
const [args, projectDir, moduleResolutionPaths] = setup();
// Start
startServer();
function startServer(): void {
// Create server
const server: http.Server = http.createServer(serverOnRequestListener);
// The timeouts below are designed to manage network instability. Since we're using the HTTP protocol on a local machine, we can disable them
// to avoid their overhead and stability issues.
// By default, on older versions of Node.js, request handling times out after 120 seconds.
// This timeout is disabled by default on Node.js v13+.
// Becuase of the older versions, we explicitly disable it.
server.setTimeout(0);
// By default, a socket is destroyed if it receives no incoming data for 5 seconds: https://nodejs.org/api/http.html#http_server_keepalivetimeout.
// This is good practice when making external requests because DNS records may change: https://github.com/dotnet/runtime/issues/18348.
// Since we're using the HTTP protocol on a local machine, it's safe and more efficient to keep sockets alive indefinitely.
server.keepAliveTimeout = 0;
// By default, a socket is destroyed if its incoming headers take longer than 60 seconds: https://nodejs.org/api/http.html#http_server_headerstimeout.
// In early versions of Node.js, even if setTimeout() was specified with a non-zero value, the server would wait indefinitely for headers.
// This timeout was added to deal with that issue. We specify setTimeout(0), so this timeout is of no use to us.
//
// Note that while 0 disables this timeout in node 12.17+, in earlier versions it causes requests to time out immediately, so set to max positive int 32.
server.headersTimeout = 2147483647;
// Log timed out connections for debugging
server.on('timeout', serverOnTimeout);
// Send client error details to client for debugging
server.on('clientError', serverOnClientError);
// Start server
server.listen(parseInt(args.port), 'localhost', () => {
// Signal to HttpNodeHost which loopback IP address (IPv4 or IPv6) and port it should make its HTTP connections on
// and that we are ready to process invocations.
let info = server.address() as AddressInfo;
console.log(`[Jering.Javascript.NodeJS: HttpVersion - HTTP/1.1 Listening on IP - ${info.address} Port - ${info.port}]`);
});
}
function serverOnRequestListener(req: http.IncomingMessage, res: http.ServerResponse): void {
const bodyChunks = [];
req.
on('data', chunk => bodyChunks.push(chunk)).
on('end', async () => {
try {
// Create InvocationRequest
const body: string = Buffer.concat(bodyChunks).toString();
let invocationRequest: InvocationRequest;
if (req.headers['content-type'] === 'multipart/mixed') {
const parts: string[] = body.split('--Uiw6+hXl3k+5ia0cUYGhjA==');
invocationRequest = JSON.parse(parts[0]);
invocationRequest.moduleSource = parts[1];
} else {
invocationRequest = JSON.parse(body);
}
// Get exports of module specified by InvocationRequest.moduleSource
let exports: any;
if (invocationRequest.moduleSourceType === ModuleSourceType.Cache) {
const cachedModule = Module._cache[invocationRequest.moduleSource];
// Cache miss
if (cachedModule == null) {
res.statusCode = 404;
res.end();
return;
}
exports = cachedModule.exports;
} else if (invocationRequest.moduleSourceType === ModuleSourceType.Stream ||
invocationRequest.moduleSourceType === ModuleSourceType.String) {
// Check if already cached
if (invocationRequest.cacheIdentifier != null) {
const cachedModule = Module._cache[invocationRequest.cacheIdentifier];
if (cachedModule != null) {
exports = cachedModule.exports;
}
}
// Not cached
if (exports == null) {
const newModule = new Module('', null);
// Specify paths where child modules may be.
newModule.paths = moduleResolutionPaths;
// Node.js exposes a method for loading a module from a file: Module.load - https://github.com/nodejs/node/blob/6726246dbb83e3251f080fc4729154d492f7e340/lib/internal/modules/cjs/loader.js#L942.
// Since we're loading a module in string form, we can't use it. Instead we call Module._compile - https://github.com/nodejs/node/blob/6726246dbb83e3251f080fc4729154d492f7e340/lib/internal/modules/cjs/loader.js#L1043,
// which Module.load calls internally.
newModule._compile(invocationRequest.moduleSource, 'anonymous');
if (invocationRequest.cacheIdentifier != null) {
// Notes on module caching:
// When a module is required using require, it is cached in Module._cache using its absolute file path as its key.
// When Module._load tries to load the same module again, it first resolves the absolute file path of the module, then it
// checks if the module exists in the cache. Custom keys for in memory modules cause an error at the file resolution step.
// To make modules with custom keys requirable by other modules, require must be monkey patched.
Module._cache[invocationRequest.cacheIdentifier] = newModule;
}
exports = newModule.exports;
}
} else if (invocationRequest.moduleSourceType === ModuleSourceType.File) {
const resolvedPath = path.resolve(projectDir, invocationRequest.moduleSource);
exports = await import('file:///' + resolvedPath.replaceAll('\\', '/'));
} else {
respondWithError(res, `Invalid module source type: ${invocationRequest.moduleSourceType}.`);
return;
}
if (exports == null || typeof exports === 'object' && Object.keys(exports).length === 0) {
respondWithError(res, `The module ${getTempIdentifier(invocationRequest)} has no exports. Ensure that the module assigns a function or an object containing functions to module.exports.`);
return;
}
// Get function to invoke
let functionToInvoke: Function;
if (invocationRequest.exportName != null) {
functionToInvoke = exports[invocationRequest.exportName] ?? exports.default?.[invocationRequest.exportName];
if (functionToInvoke == null) {
let availableExports = Object.keys(exports).join(', ');
respondWithError(res, `The module ${getTempIdentifier(invocationRequest)} has no export named ${invocationRequest.exportName}. Available exports are: ${availableExports}`);
return;
}
if (!(typeof functionToInvoke === 'function')) {
respondWithError(res, `The export named ${invocationRequest.exportName} from module ${getTempIdentifier(invocationRequest)} is not a function.`);
return;
}
} else if (typeof exports === 'function') {
functionToInvoke = exports;
} else if (typeof exports.default === 'function') { // .mjs default export
functionToInvoke = exports.default;
} else {
respondWithError(res, `The module ${getTempIdentifier(invocationRequest)} does not export a function.`);
return;
}
let callbackCalled = false;
const callback = (error: Error | string, result: any, responseAction?: (response: http.ServerResponse) => boolean) => {
if (callbackCalled) {
return;
}
callbackCalled = true;
if (error != null) {
respondWithError(res, error);
}
if (responseAction?.(res)) {
return;
}
if (result instanceof stream.Readable) {
// By default, res is ended when result ends - https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
result.pipe(res);
} else if (typeof result === 'string') {
// String - can bypass JSON-serialization altogether
res.end(result);
} else {
// Arbitrary object/number/etc - JSON-serialize it
let responseJson: string;
try {
responseJson = JSON.stringify(result);
} catch (err) {
// JSON serialization error - pass it back to .NET
respondWithError(res, err);
return;
}
res.end(responseJson);
}
}
// Invoke function
if (functionToInvoke.constructor.name === "AsyncFunction") {
callback(null, await functionToInvoke.apply(null, invocationRequest.args));
} else {
const args: object[] = [callback];
functionToInvoke.apply(null, args.concat(invocationRequest.args));
}
} catch (error) {
respondWithError(res, error);
}
});
}
// Send error details to client for debugging - https://nodejs.org/api/http.html#http_event_clienterror
function serverOnClientError(error: Error, socket: stream.Duplex): void {
let errorJson = JSON.stringify({
errorMessage: error.message,
errorStack: error.stack
});
let httpResponseMessage = `HTTP/1.1 500 Internal Server Error\r\nContent-Length: ${Buffer.byteLength(errorJson, 'utf8')}\r\nContent-Type: text/html\r\n\r\n${errorJson}`;
socket.end(httpResponseMessage);
}
// Send timeout details to client for debugging - this shouldn't fire but there have been various node http server timeout issues in the past.
// The socket won't actually get closed (the timeout function needs to do that manually).
function serverOnTimeout(socket: Socket): void {
console.log(`[Node.js HTTP server] Ignoring unexpected socket timeout for address ${socket.remoteAddress}, port ${socket.remotePort}`);
}