Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81144 views
1
/**
2
* Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.
3
*
4
* This can be used with JS designed for browsers to improve reuse of code and
5
* allow the use of existing libraries.
6
*
7
* Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs.
8
*
9
* @author Dan DeFelippi <[email protected]>
10
* @contributor David Ellis <[email protected]>
11
* @license MIT
12
*/
13
14
var Url = require("url");
15
var spawn = require("child_process").spawn;
16
var fs = require("fs");
17
18
exports.XMLHttpRequest = function() {
19
"use strict";
20
21
/**
22
* Private variables
23
*/
24
var self = this;
25
var http = require("http");
26
var https = require("https");
27
28
// Holds http.js objects
29
var request;
30
var response;
31
32
// Request settings
33
var settings = {};
34
35
// Disable header blacklist.
36
// Not part of XHR specs.
37
var disableHeaderCheck = false;
38
39
// Set some default headers
40
var defaultHeaders = {
41
"User-Agent": "node-XMLHttpRequest",
42
"Accept": "*/*",
43
};
44
45
var headers = defaultHeaders;
46
47
// These headers are not user setable.
48
// The following are allowed but banned in the spec:
49
// * user-agent
50
var forbiddenRequestHeaders = [
51
"accept-charset",
52
"accept-encoding",
53
"access-control-request-headers",
54
"access-control-request-method",
55
"connection",
56
"content-length",
57
"content-transfer-encoding",
58
"cookie",
59
"cookie2",
60
"date",
61
"expect",
62
"host",
63
"keep-alive",
64
"origin",
65
"referer",
66
"te",
67
"trailer",
68
"transfer-encoding",
69
"upgrade",
70
"via"
71
];
72
73
// These request methods are not allowed
74
var forbiddenRequestMethods = [
75
"TRACE",
76
"TRACK",
77
"CONNECT"
78
];
79
80
// Send flag
81
var sendFlag = false;
82
// Error flag, used when errors occur or abort is called
83
var errorFlag = false;
84
85
// Event listeners
86
var listeners = {};
87
88
/**
89
* Constants
90
*/
91
92
this.UNSENT = 0;
93
this.OPENED = 1;
94
this.HEADERS_RECEIVED = 2;
95
this.LOADING = 3;
96
this.DONE = 4;
97
98
/**
99
* Public vars
100
*/
101
102
// Current state
103
this.readyState = this.UNSENT;
104
105
// default ready state change handler in case one is not set or is set late
106
this.onreadystatechange = null;
107
108
// Result & response
109
this.responseText = "";
110
this.responseXML = "";
111
this.status = null;
112
this.statusText = null;
113
114
/**
115
* Private methods
116
*/
117
118
/**
119
* Check if the specified header is allowed.
120
*
121
* @param string header Header to validate
122
* @return boolean False if not allowed, otherwise true
123
*/
124
var isAllowedHttpHeader = function(header) {
125
return disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1);
126
};
127
128
/**
129
* Check if the specified method is allowed.
130
*
131
* @param string method Request method to validate
132
* @return boolean False if not allowed, otherwise true
133
*/
134
var isAllowedHttpMethod = function(method) {
135
return (method && forbiddenRequestMethods.indexOf(method) === -1);
136
};
137
138
/**
139
* Public methods
140
*/
141
142
/**
143
* Open the connection. Currently supports local server requests.
144
*
145
* @param string method Connection method (eg GET, POST)
146
* @param string url URL for the connection.
147
* @param boolean async Asynchronous connection. Default is true.
148
* @param string user Username for basic authentication (optional)
149
* @param string password Password for basic authentication (optional)
150
*/
151
this.open = function(method, url, async, user, password) {
152
this.abort();
153
errorFlag = false;
154
155
// Check for valid request method
156
if (!isAllowedHttpMethod(method)) {
157
throw "SecurityError: Request method not allowed";
158
}
159
160
settings = {
161
"method": method,
162
"url": url.toString(),
163
"async": (typeof async !== "boolean" ? true : async),
164
"user": user || null,
165
"password": password || null
166
};
167
168
setState(this.OPENED);
169
};
170
171
/**
172
* Disables or enables isAllowedHttpHeader() check the request. Enabled by default.
173
* This does not conform to the W3C spec.
174
*
175
* @param boolean state Enable or disable header checking.
176
*/
177
this.setDisableHeaderCheck = function(state) {
178
disableHeaderCheck = state;
179
};
180
181
/**
182
* Sets a header for the request.
183
*
184
* @param string header Header name
185
* @param string value Header value
186
*/
187
this.setRequestHeader = function(header, value) {
188
if (this.readyState !== this.OPENED) {
189
throw "INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN";
190
}
191
if (!isAllowedHttpHeader(header)) {
192
console.warn("Refused to set unsafe header \"" + header + "\"");
193
return;
194
}
195
if (sendFlag) {
196
throw "INVALID_STATE_ERR: send flag is true";
197
}
198
headers[header] = value;
199
};
200
201
/**
202
* Gets a header from the server response.
203
*
204
* @param string header Name of header to get.
205
* @return string Text of the header or null if it doesn't exist.
206
*/
207
this.getResponseHeader = function(header) {
208
if (typeof header === "string"
209
&& this.readyState > this.OPENED
210
&& response
211
&& response.headers
212
&& response.headers[header.toLowerCase()]
213
&& !errorFlag
214
) {
215
return response.headers[header.toLowerCase()];
216
}
217
218
return null;
219
};
220
221
/**
222
* Gets all the response headers.
223
*
224
* @return string A string with all response headers separated by CR+LF
225
*/
226
this.getAllResponseHeaders = function() {
227
if (this.readyState < this.HEADERS_RECEIVED || errorFlag) {
228
return "";
229
}
230
var result = "";
231
232
for (var i in response.headers) {
233
// Cookie headers are excluded
234
if (i !== "set-cookie" && i !== "set-cookie2") {
235
result += i + ": " + response.headers[i] + "\r\n";
236
}
237
}
238
return result.substr(0, result.length - 2);
239
};
240
241
/**
242
* Gets a request header
243
*
244
* @param string name Name of header to get
245
* @return string Returns the request header or empty string if not set
246
*/
247
this.getRequestHeader = function(name) {
248
// @TODO Make this case insensitive
249
if (typeof name === "string" && headers[name]) {
250
return headers[name];
251
}
252
253
return "";
254
};
255
256
/**
257
* Sends the request to the server.
258
*
259
* @param string data Optional data to send as request body.
260
*/
261
this.send = function(data) {
262
if (this.readyState !== this.OPENED) {
263
throw "INVALID_STATE_ERR: connection must be opened before send() is called";
264
}
265
266
if (sendFlag) {
267
throw "INVALID_STATE_ERR: send has already been called";
268
}
269
270
var ssl = false, local = false;
271
var url = Url.parse(settings.url);
272
var host;
273
// Determine the server
274
switch (url.protocol) {
275
case "https:":
276
ssl = true;
277
// SSL & non-SSL both need host, no break here.
278
case "http:":
279
host = url.hostname;
280
break;
281
282
case "file:":
283
local = true;
284
break;
285
286
case undefined:
287
case "":
288
host = "localhost";
289
break;
290
291
default:
292
throw "Protocol not supported.";
293
}
294
295
// Load files off the local filesystem (file://)
296
if (local) {
297
if (settings.method !== "GET") {
298
throw "XMLHttpRequest: Only GET method is supported";
299
}
300
301
if (settings.async) {
302
fs.readFile(url.pathname, "utf8", function(error, data) {
303
if (error) {
304
self.handleError(error);
305
} else {
306
self.status = 200;
307
self.responseText = data;
308
setState(self.DONE);
309
}
310
});
311
} else {
312
try {
313
this.responseText = fs.readFileSync(url.pathname, "utf8");
314
this.status = 200;
315
setState(self.DONE);
316
} catch(e) {
317
this.handleError(e);
318
}
319
}
320
321
return;
322
}
323
324
// Default to port 80. If accessing localhost on another port be sure
325
// to use http://localhost:port/path
326
var port = url.port || (ssl ? 443 : 80);
327
// Add query string if one is used
328
var uri = url.pathname + (url.search ? url.search : "");
329
330
// Set the Host header or the server may reject the request
331
headers.Host = host;
332
if (!((ssl && port === 443) || port === 80)) {
333
headers.Host += ":" + url.port;
334
}
335
336
// Set Basic Auth if necessary
337
if (settings.user) {
338
if (typeof settings.password === "undefined") {
339
settings.password = "";
340
}
341
var authBuf = new Buffer(settings.user + ":" + settings.password);
342
headers.Authorization = "Basic " + authBuf.toString("base64");
343
}
344
345
// Set content length header
346
if (settings.method === "GET" || settings.method === "HEAD") {
347
data = null;
348
} else if (data) {
349
headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
350
351
if (!headers["Content-Type"]) {
352
headers["Content-Type"] = "text/plain;charset=UTF-8";
353
}
354
} else if (settings.method === "POST") {
355
// For a post with no data set Content-Length: 0.
356
// This is required by buggy servers that don't meet the specs.
357
headers["Content-Length"] = 0;
358
}
359
360
var options = {
361
host: host,
362
port: port,
363
path: uri,
364
method: settings.method,
365
headers: headers,
366
agent: false
367
};
368
369
// Reset error flag
370
errorFlag = false;
371
372
// Handle async requests
373
if (settings.async) {
374
// Use the proper protocol
375
var doRequest = ssl ? https.request : http.request;
376
377
// Request is being sent, set send flag
378
sendFlag = true;
379
380
// As per spec, this is called here for historical reasons.
381
self.dispatchEvent("readystatechange");
382
383
// Handler for the response
384
var responseHandler = function responseHandler(resp) {
385
// Set response var to the response we got back
386
// This is so it remains accessable outside this scope
387
response = resp;
388
// Check for redirect
389
// @TODO Prevent looped redirects
390
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) {
391
// Change URL to the redirect location
392
settings.url = response.headers.location;
393
var url = Url.parse(settings.url);
394
// Set host var in case it's used later
395
host = url.hostname;
396
// Options for the new request
397
var newOptions = {
398
hostname: url.hostname,
399
port: url.port,
400
path: url.path,
401
method: response.statusCode === 303 ? "GET" : settings.method,
402
headers: headers
403
};
404
405
// Issue the new request
406
request = doRequest(newOptions, responseHandler).on("error", errorHandler);
407
request.end();
408
// @TODO Check if an XHR event needs to be fired here
409
return;
410
}
411
412
response.setEncoding("utf8");
413
414
setState(self.HEADERS_RECEIVED);
415
self.status = response.statusCode;
416
417
response.on("data", function(chunk) {
418
// Make sure there's some data
419
if (chunk) {
420
self.responseText += chunk;
421
}
422
// Don't emit state changes if the connection has been aborted.
423
if (sendFlag) {
424
setState(self.LOADING);
425
}
426
});
427
428
response.on("end", function() {
429
if (sendFlag) {
430
// Discard the end event if the connection has been aborted
431
setState(self.DONE);
432
sendFlag = false;
433
}
434
});
435
436
response.on("error", function(error) {
437
self.handleError(error);
438
});
439
};
440
441
// Error handler for the request
442
var errorHandler = function errorHandler(error) {
443
self.handleError(error);
444
};
445
446
// Create the request
447
request = doRequest(options, responseHandler).on("error", errorHandler);
448
449
// Node 0.4 and later won't accept empty data. Make sure it's needed.
450
if (data) {
451
request.write(data);
452
}
453
454
request.end();
455
456
self.dispatchEvent("loadstart");
457
} else { // Synchronous
458
// Create a temporary file for communication with the other Node process
459
var contentFile = ".node-xmlhttprequest-content-" + process.pid;
460
var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
461
fs.writeFileSync(syncFile, "", "utf8");
462
// The async request the other Node process executes
463
var execString = "var http = require('http'), https = require('https'), fs = require('fs');"
464
+ "var doRequest = http" + (ssl ? "s" : "") + ".request;"
465
+ "var options = " + JSON.stringify(options) + ";"
466
+ "var responseText = '';"
467
+ "var req = doRequest(options, function(response) {"
468
+ "response.setEncoding('utf8');"
469
+ "response.on('data', function(chunk) {"
470
+ " responseText += chunk;"
471
+ "});"
472
+ "response.on('end', function() {"
473
+ "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText}}), 'utf8');"
474
+ "fs.unlinkSync('" + syncFile + "');"
475
+ "});"
476
+ "response.on('error', function(error) {"
477
+ "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');"
478
+ "fs.unlinkSync('" + syncFile + "');"
479
+ "});"
480
+ "}).on('error', function(error) {"
481
+ "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');"
482
+ "fs.unlinkSync('" + syncFile + "');"
483
+ "});"
484
+ (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"")
485
+ "req.end();";
486
// Start the other Node Process, executing this string
487
var syncProc = spawn(process.argv[0], ["-e", execString]);
488
while(fs.existsSync(syncFile)) {
489
// Wait while the sync file is empty
490
}
491
var resp = JSON.parse(fs.readFileSync(contentFile, 'utf8'));
492
// Kill the child process once the file has data
493
syncProc.stdin.end();
494
// Remove the temporary file
495
fs.unlinkSync(contentFile);
496
497
if (resp.err) {
498
self.handleError(resp.err);
499
} else {
500
response = resp.data;
501
self.status = resp.data.statusCode;
502
self.responseText = resp.data.text;
503
setState(self.DONE);
504
}
505
}
506
};
507
508
/**
509
* Called when an error is encountered to deal with it.
510
*/
511
this.handleError = function(error) {
512
this.status = 503;
513
this.statusText = error;
514
this.responseText = error.stack;
515
errorFlag = true;
516
setState(this.DONE);
517
};
518
519
/**
520
* Aborts a request.
521
*/
522
this.abort = function() {
523
if (request) {
524
request.abort();
525
request = null;
526
}
527
528
headers = defaultHeaders;
529
this.responseText = "";
530
this.responseXML = "";
531
532
errorFlag = true;
533
534
if (this.readyState !== this.UNSENT
535
&& (this.readyState !== this.OPENED || sendFlag)
536
&& this.readyState !== this.DONE) {
537
sendFlag = false;
538
setState(this.DONE);
539
}
540
this.readyState = this.UNSENT;
541
};
542
543
/**
544
* Adds an event listener. Preferred method of binding to events.
545
*/
546
this.addEventListener = function(event, callback) {
547
if (!(event in listeners)) {
548
listeners[event] = [];
549
}
550
// Currently allows duplicate callbacks. Should it?
551
listeners[event].push(callback);
552
};
553
554
/**
555
* Remove an event callback that has already been bound.
556
* Only works on the matching funciton, cannot be a copy.
557
*/
558
this.removeEventListener = function(event, callback) {
559
if (event in listeners) {
560
// Filter will return a new array with the callback removed
561
listeners[event] = listeners[event].filter(function(ev) {
562
return ev !== callback;
563
});
564
}
565
};
566
567
/**
568
* Dispatch any events, including both "on" methods and events attached using addEventListener.
569
*/
570
this.dispatchEvent = function(event) {
571
if (typeof self["on" + event] === "function") {
572
self["on" + event]();
573
}
574
if (event in listeners) {
575
for (var i = 0, len = listeners[event].length; i < len; i++) {
576
listeners[event][i].call(self);
577
}
578
}
579
};
580
581
/**
582
* Changes readyState and calls onreadystatechange.
583
*
584
* @param int state New state
585
*/
586
var setState = function(state) {
587
if (state == self.LOADING || self.readyState !== state) {
588
self.readyState = state;
589
590
if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) {
591
self.dispatchEvent("readystatechange");
592
}
593
594
if (self.readyState === self.DONE && !errorFlag) {
595
self.dispatchEvent("load");
596
// @TODO figure out InspectorInstrumentation::didLoadXHR(cookie)
597
self.dispatchEvent("loadend");
598
}
599
}
600
};
601
};
602
603