mirror of
https://github.com/nodejs/node.git
synced 2025-05-20 02:55:51 +00:00

Change the http.Client API so that it provides a single request() method taking an optional parameter to specify the HTTP method (defaulting to "GET"), instead of the five methods get(), head(), post(), del() and put().
663 lines
17 KiB
JavaScript
663 lines
17 KiB
JavaScript
var sys = require('sys');
|
|
|
|
var CRLF = "\r\n";
|
|
var STATUS_CODES = {
|
|
100 : 'Continue',
|
|
101 : 'Switching Protocols',
|
|
200 : 'OK',
|
|
201 : 'Created',
|
|
202 : 'Accepted',
|
|
203 : 'Non-Authoritative Information',
|
|
204 : 'No Content',
|
|
205 : 'Reset Content',
|
|
206 : 'Partial Content',
|
|
300 : 'Multiple Choices',
|
|
301 : 'Moved Permanently',
|
|
302 : 'Moved Temporarily',
|
|
303 : 'See Other',
|
|
304 : 'Not Modified',
|
|
305 : 'Use Proxy',
|
|
400 : 'Bad Request',
|
|
401 : 'Unauthorized',
|
|
402 : 'Payment Required',
|
|
403 : 'Forbidden',
|
|
404 : 'Not Found',
|
|
405 : 'Method Not Allowed',
|
|
406 : 'Not Acceptable',
|
|
407 : 'Proxy Authentication Required',
|
|
408 : 'Request Time-out',
|
|
409 : 'Conflict',
|
|
410 : 'Gone',
|
|
411 : 'Length Required',
|
|
412 : 'Precondition Failed',
|
|
413 : 'Request Entity Too Large',
|
|
414 : 'Request-URI Too Large',
|
|
415 : 'Unsupported Media Type',
|
|
500 : 'Internal Server Error',
|
|
501 : 'Not Implemented',
|
|
502 : 'Bad Gateway',
|
|
503 : 'Service Unavailable',
|
|
504 : 'Gateway Time-out',
|
|
505 : 'HTTP Version not supported'
|
|
};
|
|
|
|
/*
|
|
parseUri 1.2.1
|
|
(c) 2007 Steven Levithan <stevenlevithan.com>
|
|
MIT License
|
|
*/
|
|
|
|
function decode (s) {
|
|
return decodeURIComponent(s.replace(/\+/g, ' '));
|
|
}
|
|
|
|
exports.parseUri = function (str) {
|
|
var o = exports.parseUri.options,
|
|
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
|
|
uri = {},
|
|
i = 14;
|
|
|
|
while (i--) uri[o.key[i]] = m[i] || "";
|
|
|
|
uri[o.q.name] = {};
|
|
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
|
|
if ($1) {
|
|
try {
|
|
var key = decode($1);
|
|
var val = decode($2);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
uri[o.q.name][key] = val;
|
|
}
|
|
});
|
|
uri.toString = function () { return str; };
|
|
|
|
for (i = o.key.length - 1; i >= 0; i--){
|
|
if (uri[o.key[i]] == "") delete uri[o.key[i]];
|
|
}
|
|
|
|
return uri;
|
|
};
|
|
|
|
exports.parseUri.options = {
|
|
strictMode: false,
|
|
key: [
|
|
"source",
|
|
"protocol",
|
|
"authority",
|
|
"userInfo",
|
|
"user",
|
|
"password",
|
|
"host",
|
|
"port",
|
|
"relative",
|
|
"path",
|
|
"directory",
|
|
"file",
|
|
"query",
|
|
"anchor"
|
|
],
|
|
q: {
|
|
name: "params",
|
|
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
|
|
},
|
|
parser: {
|
|
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
|
|
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
|
|
}
|
|
};
|
|
|
|
|
|
var connection_expression = /Connection/i;
|
|
var transfer_encoding_expression = /Transfer-Encoding/i;
|
|
var close_expression = /close/i;
|
|
var chunk_expression = /chunk/i;
|
|
var content_length_expression = /Content-Length/i;
|
|
|
|
|
|
/* Abstract base class for ServerRequest and ClientResponse. */
|
|
function IncomingMessage (connection) {
|
|
process.EventEmitter.call(this);
|
|
|
|
this.connection = connection;
|
|
this.httpVersion = null;
|
|
this.headers = {};
|
|
|
|
// request (server) only
|
|
this.uri = {
|
|
full: "",
|
|
queryString: "",
|
|
fragment: "",
|
|
path: "",
|
|
params: {}
|
|
};
|
|
|
|
this.method = null;
|
|
|
|
// response (client) only
|
|
this.statusCode = null;
|
|
this.client = this.connection;
|
|
}
|
|
sys.inherits(IncomingMessage, process.EventEmitter);
|
|
exports.IncomingMessage = IncomingMessage;
|
|
|
|
IncomingMessage.prototype._parseQueryString = function () {
|
|
var parts = this.uri.queryString.split('&');
|
|
for (var j = 0; j < parts.length; j++) {
|
|
var i = parts[j].indexOf('=');
|
|
if (i < 0) continue;
|
|
try {
|
|
var key = decode(parts[j].slice(0,i))
|
|
var value = decode(parts[j].slice(i+1));
|
|
this.uri.params[key] = value;
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
};
|
|
|
|
IncomingMessage.prototype.setBodyEncoding = function (enc) {
|
|
// TODO: Find a cleaner way of doing this.
|
|
this.connection.setEncoding(enc);
|
|
};
|
|
|
|
IncomingMessage.prototype.pause = function () {
|
|
this.connection.readPause();
|
|
};
|
|
|
|
IncomingMessage.prototype.resume = function () {
|
|
this.connection.readResume();
|
|
};
|
|
|
|
IncomingMessage.prototype._addHeaderLine = function (field, value) {
|
|
if (field in this.headers) {
|
|
// TODO Certain headers like 'Content-Type' should not be concatinated.
|
|
// See https://www.google.com/reader/view/?tab=my#overview-page
|
|
this.headers[field] += ", " + value;
|
|
} else {
|
|
this.headers[field] = value;
|
|
}
|
|
};
|
|
|
|
function OutgoingMessage () {
|
|
process.EventEmitter.call(this);
|
|
|
|
this.output = [];
|
|
this.outputEncodings = [];
|
|
|
|
this.closeOnFinish = false;
|
|
this.chunked_encoding = false;
|
|
this.should_keep_alive = true;
|
|
this.use_chunked_encoding_by_default = true;
|
|
|
|
this.flushing = false;
|
|
|
|
this.finished = false;
|
|
}
|
|
sys.inherits(OutgoingMessage, process.EventEmitter);
|
|
exports.OutgoingMessage = OutgoingMessage;
|
|
|
|
OutgoingMessage.prototype.send = function (data, encoding) {
|
|
var length = this.output.length;
|
|
|
|
if (length === 0) {
|
|
this.output.push(data);
|
|
encoding = encoding || "ascii";
|
|
this.outputEncodings.push(encoding);
|
|
return;
|
|
}
|
|
|
|
var lastEncoding = this.outputEncodings[length-1];
|
|
var lastData = this.output[length-1];
|
|
|
|
if ((lastEncoding === encoding) ||
|
|
(!encoding && data.constructor === lastData.constructor)) {
|
|
if (lastData.constructor === String) {
|
|
this.output[length-1] = lastData + data;
|
|
} else {
|
|
this.output[length-1] = lastData.concat(data);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.output.push(data);
|
|
encoding = encoding || "ascii";
|
|
this.outputEncodings.push(encoding);
|
|
};
|
|
|
|
OutgoingMessage.prototype.sendHeaderLines = function (first_line, headers) {
|
|
var sent_connection_header = false;
|
|
var sent_content_length_header = false;
|
|
var sent_transfer_encoding_header = false;
|
|
|
|
// first_line in the case of request is: "GET /index.html HTTP/1.1\r\n"
|
|
// in the case of response it is: "HTTP/1.1 200 OK\r\n"
|
|
var message_header = first_line;
|
|
var field, value;
|
|
for (var i in headers) {
|
|
if (headers[i] instanceof Array) {
|
|
field = headers[i][0];
|
|
value = headers[i][1];
|
|
} else {
|
|
if (!headers.hasOwnProperty(i)) continue;
|
|
field = i;
|
|
value = headers[i];
|
|
}
|
|
|
|
message_header += field + ": " + value + CRLF;
|
|
|
|
if (connection_expression.exec(field)) {
|
|
sent_connection_header = true;
|
|
if (close_expression.exec(value)) this.closeOnFinish = true;
|
|
|
|
} else if (transfer_encoding_expression.exec(field)) {
|
|
sent_transfer_encoding_header = true;
|
|
if (chunk_expression.exec(value)) this.chunked_encoding = true;
|
|
|
|
} else if (content_length_expression.exec(field)) {
|
|
sent_content_length_header = true;
|
|
|
|
}
|
|
}
|
|
|
|
// keep-alive logic
|
|
if (sent_connection_header == false) {
|
|
if (this.should_keep_alive) {
|
|
message_header += "Connection: keep-alive\r\n";
|
|
} else {
|
|
this.closeOnFinish = true;
|
|
message_header += "Connection: close\r\n";
|
|
}
|
|
}
|
|
|
|
if (sent_content_length_header == false && sent_transfer_encoding_header == false) {
|
|
if (this.use_chunked_encoding_by_default) {
|
|
message_header += "Transfer-Encoding: chunked\r\n";
|
|
this.chunked_encoding = true;
|
|
}
|
|
}
|
|
|
|
message_header += CRLF;
|
|
|
|
this.send(message_header);
|
|
// wait until the first body chunk, or finish(), is sent to flush.
|
|
};
|
|
|
|
OutgoingMessage.prototype.sendBody = function (chunk, encoding) {
|
|
if (this.chunked_encoding) {
|
|
this.send(process._byteLength(chunk, encoding).toString(16));
|
|
this.send(CRLF);
|
|
this.send(chunk, encoding);
|
|
this.send(CRLF);
|
|
} else {
|
|
this.send(chunk, encoding);
|
|
}
|
|
|
|
if (this.flushing) {
|
|
this.flush();
|
|
} else {
|
|
this.flushing = true;
|
|
}
|
|
};
|
|
|
|
OutgoingMessage.prototype.flush = function () {
|
|
this.emit("flush");
|
|
};
|
|
|
|
OutgoingMessage.prototype.finish = function () {
|
|
if (this.chunked_encoding) this.send("0\r\n\r\n"); // last chunk
|
|
this.finished = true;
|
|
this.flush();
|
|
};
|
|
|
|
|
|
function ServerResponse (req) {
|
|
OutgoingMessage.call(this);
|
|
|
|
this.should_keep_alive = true;
|
|
this.use_chunked_encoding_by_default = true;
|
|
|
|
if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
|
|
this.use_chunked_encoding_by_default = false;
|
|
this.should_keep_alive = false;
|
|
}
|
|
}
|
|
sys.inherits(ServerResponse, OutgoingMessage);
|
|
exports.ServerResponse = ServerResponse;
|
|
|
|
ServerResponse.prototype.sendHeader = function (statusCode, headers) {
|
|
var reason = STATUS_CODES[statusCode] || "unknown";
|
|
var status_line = "HTTP/1.1 " + statusCode.toString() + " " + reason + CRLF;
|
|
this.sendHeaderLines(status_line, headers);
|
|
};
|
|
|
|
|
|
function ClientRequest (method, uri, headers) {
|
|
OutgoingMessage.call(this);
|
|
|
|
this.should_keep_alive = false;
|
|
if (method === "GET" || method === "HEAD") {
|
|
this.use_chunked_encoding_by_default = false;
|
|
} else {
|
|
this.use_chunked_encoding_by_default = true;
|
|
}
|
|
this.closeOnFinish = true;
|
|
|
|
this.sendHeaderLines(method + " " + uri + " HTTP/1.1\r\n", headers);
|
|
}
|
|
sys.inherits(ClientRequest, OutgoingMessage);
|
|
exports.ClientRequest = ClientRequest;
|
|
|
|
ClientRequest.prototype.finish = function (responseListener) {
|
|
this.addListener("response", responseListener);
|
|
OutgoingMessage.prototype.finish.call(this);
|
|
};
|
|
|
|
|
|
function createIncomingMessageStream (connection, incoming_listener) {
|
|
var stream = new process.EventEmitter();
|
|
|
|
stream.addListener("incoming", incoming_listener);
|
|
|
|
var incoming, field, value;
|
|
|
|
connection.addListener("messageBegin", function () {
|
|
incoming = new IncomingMessage(connection);
|
|
field = null;
|
|
value = null;
|
|
});
|
|
|
|
// Only servers will get URI events.
|
|
connection.addListener("url", function (data) {
|
|
incoming.uri.full += data;
|
|
});
|
|
|
|
connection.addListener("path", function (data) {
|
|
incoming.uri.path += data;
|
|
});
|
|
|
|
connection.addListener("fragment", function (data) {
|
|
incoming.uri.fragment += data;
|
|
});
|
|
|
|
connection.addListener("queryString", function (data) {
|
|
incoming.uri.queryString += data;
|
|
});
|
|
|
|
connection.addListener("headerField", function (data) {
|
|
if (value) {
|
|
incoming._addHeaderLine(field, value);
|
|
field = null;
|
|
value = null;
|
|
}
|
|
if (field) {
|
|
field += data;
|
|
} else {
|
|
field = data;
|
|
}
|
|
});
|
|
|
|
connection.addListener("headerValue", function (data) {
|
|
if (value) {
|
|
value += data;
|
|
} else {
|
|
value = data;
|
|
}
|
|
});
|
|
|
|
connection.addListener("headerComplete", function (info) {
|
|
if (field && value) {
|
|
incoming._addHeaderLine(field, value);
|
|
}
|
|
|
|
incoming.httpVersion = info.httpVersion;
|
|
incoming.httpVersionMajor = info.versionMajor;
|
|
incoming.httpVersionMinor = info.versionMinor;
|
|
|
|
if (info.method) {
|
|
// server only
|
|
incoming.method = info.method;
|
|
|
|
if (incoming.uri.queryString.length > 0) {
|
|
incoming._parseQueryString();
|
|
}
|
|
} else {
|
|
// client only
|
|
incoming.statusCode = info.statusCode;
|
|
}
|
|
|
|
stream.emit("incoming", incoming, info.should_keep_alive);
|
|
});
|
|
|
|
connection.addListener("body", function (chunk) {
|
|
incoming.emit("body", chunk);
|
|
});
|
|
|
|
connection.addListener("messageComplete", function () {
|
|
incoming.emit("complete");
|
|
});
|
|
|
|
return stream;
|
|
}
|
|
|
|
/* Returns true if the message queue is finished and the connection
|
|
* should be closed. */
|
|
function flushMessageQueue (connection, queue) {
|
|
while (queue[0]) {
|
|
var message = queue[0];
|
|
|
|
while (message.output.length > 0) {
|
|
if (connection.readyState !== "open" && connection.readyState !== "writeOnly") {
|
|
return true;
|
|
}
|
|
|
|
var data = message.output.shift();
|
|
var encoding = message.outputEncodings.shift();
|
|
|
|
connection.send(data, encoding);
|
|
}
|
|
|
|
if (!message.finished) break;
|
|
|
|
message.emit("sent");
|
|
queue.shift();
|
|
|
|
if (message.closeOnFinish) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
exports.createServer = function (requestListener, options) {
|
|
var server = new process.http.Server();
|
|
//server.setOptions(options);
|
|
server.addListener("request", requestListener);
|
|
server.addListener("connection", connectionListener);
|
|
return server;
|
|
};
|
|
|
|
function connectionListener (connection) {
|
|
// An array of responses for each connection. In pipelined connections
|
|
// we need to keep track of the order they were sent.
|
|
var responses = [];
|
|
|
|
connection.resetParser();
|
|
|
|
// is this really needed?
|
|
connection.addListener("eof", function () {
|
|
if (responses.length == 0) {
|
|
connection.close();
|
|
} else {
|
|
responses[responses.length-1].closeOnFinish = true;
|
|
}
|
|
});
|
|
|
|
|
|
createIncomingMessageStream(connection, function (incoming, should_keep_alive) {
|
|
var req = incoming;
|
|
|
|
var res = new ServerResponse(req);
|
|
res.should_keep_alive = should_keep_alive;
|
|
res.addListener("flush", function () {
|
|
if (flushMessageQueue(connection, responses)) {
|
|
connection.close();
|
|
}
|
|
});
|
|
responses.push(res);
|
|
|
|
connection.server.emit("request", req, res);
|
|
});
|
|
}
|
|
|
|
|
|
exports.createClient = function (port, host) {
|
|
var client = new process.http.Client();
|
|
var secure_credentials={ secure : false };
|
|
|
|
var requests = [];
|
|
|
|
client._pushRequest = function (req) {
|
|
req.addListener("flush", function () {
|
|
if (client.readyState == "closed") {
|
|
//sys.debug("HTTP CLIENT request flush. reconnect. readyState = " + client.readyState);
|
|
client.connect(port, host); // reconnect
|
|
if (secure_credentials.secure) {
|
|
client.tcpSetSecure(secure_credentials.format_type,
|
|
secure_credentials.ca_certs,
|
|
secure_credentials.crl_list,
|
|
secure_credentials.private_key,
|
|
secure_credentials.certificate);
|
|
}
|
|
return;
|
|
}
|
|
//sys.debug("client flush readyState = " + client.readyState);
|
|
if (req == requests[0]) flushMessageQueue(client, [req]);
|
|
});
|
|
requests.push(req);
|
|
};
|
|
|
|
client.tcpSetSecure = client.setSecure;
|
|
client.setSecure = function(format_type, ca_certs, crl_list, private_key, certificate) {
|
|
secure_credentials.secure = true;
|
|
secure_credentials.format_type = format_type;
|
|
secure_credentials.ca_certs = ca_certs;
|
|
secure_credentials.crl_list = crl_list;
|
|
secure_credentials.private_key = private_key;
|
|
secure_credentials.certificate = certificate;
|
|
}
|
|
|
|
client.addListener("connect", function () {
|
|
client.resetParser();
|
|
requests[0].flush();
|
|
});
|
|
|
|
client.addListener("eof", function () {
|
|
//sys.debug("client got eof closing. readyState = " + client.readyState);
|
|
client.close();
|
|
});
|
|
|
|
client.addListener("close", function (had_error) {
|
|
if (had_error) {
|
|
client.emit("error");
|
|
return;
|
|
}
|
|
|
|
//sys.debug("HTTP CLIENT onClose. readyState = " + client.readyState);
|
|
|
|
// If there are more requests to handle, reconnect.
|
|
if (requests.length > 0 && client.readyState != "opening") {
|
|
//sys.debug("HTTP CLIENT: reconnecting readyState = " + client.readyState);
|
|
client.connect(port, host); // reconnect
|
|
if (secure_credentials.secure) {
|
|
client.tcpSetSecure(secure_credentials.format_type,
|
|
secure_credentials.ca_certs,
|
|
secure_credentials.crl_list,
|
|
secure_credentials.private_key,
|
|
secure_credentials.certificate);
|
|
}
|
|
}
|
|
});
|
|
|
|
createIncomingMessageStream(client, function (res) {
|
|
//sys.debug("incoming response!");
|
|
|
|
res.addListener("complete", function ( ) {
|
|
//sys.debug("request complete disconnecting. readyState = " + client.readyState);
|
|
client.close();
|
|
});
|
|
|
|
var req = requests.shift();
|
|
req.emit("response", res);
|
|
});
|
|
|
|
return client;
|
|
};
|
|
|
|
process.http.Client.prototype.get = function () {
|
|
throw new Error("client.get(...) is now client.request('GET', ...)");
|
|
};
|
|
|
|
process.http.Client.prototype.head = function () {
|
|
throw new Error("client.head(...) is now client.request('HEAD', ...)");
|
|
};
|
|
|
|
process.http.Client.prototype.post = function () {
|
|
throw new Error("client.post(...) is now client.request('POST', ...)");
|
|
};
|
|
|
|
process.http.Client.prototype.del = function () {
|
|
throw new Error("client.del(...) is now client.request('DELETE', ...)");
|
|
};
|
|
|
|
process.http.Client.prototype.put = function () {
|
|
throw new Error("client.put(...) is now client.request('PUT', ...)");
|
|
};
|
|
|
|
process.http.Client.prototype.request = function (method, uri, headers) {
|
|
if (typeof(uri) != "string") { // assume method was omitted, shift arguments
|
|
headers = uri;
|
|
uri = method;
|
|
method = null;
|
|
}
|
|
var req = new ClientRequest(method || "GET", uri, headers);
|
|
this._pushRequest(req);
|
|
return req;
|
|
};
|
|
|
|
|
|
exports.cat = function (url, encoding, headers) {
|
|
var promise = new process.Promise();
|
|
|
|
encoding = encoding || "utf8";
|
|
|
|
var uri = exports.parseUri(url);
|
|
headers = headers || {};
|
|
if (!headers["Host"] && uri.host) {
|
|
headers["Host"] = uri.host;
|
|
}
|
|
|
|
var client = exports.createClient(uri.port || 80, uri.host);
|
|
var req = client.request(uri.path || "/", headers);
|
|
|
|
client.addListener("error", function () {
|
|
promise.emitError();
|
|
});
|
|
|
|
var content = "";
|
|
|
|
req.finish(function (res) {
|
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
promise.emitError(res.statusCode);
|
|
return;
|
|
}
|
|
res.setBodyEncoding(encoding);
|
|
res.addListener("body", function (chunk) { content += chunk; });
|
|
res.addListener("complete", function () {
|
|
promise.emitSuccess(content);
|
|
});
|
|
});
|
|
|
|
return promise;
|
|
};
|