mirror of
https://github.com/nodejs/node.git
synced 2025-05-07 08:00:26 +00:00

Users too often would forget to add socket.on('end', function () { socket.end(); }); Which is a mistake. Therefore we default to this behavior and only optionally let people handle the 'end' case themselves.
1150 lines
31 KiB
JavaScript
Executable File
1150 lines
31 KiB
JavaScript
Executable File
var util = require('util');
|
|
|
|
var debug;
|
|
var debugLevel = parseInt(process.env.NODE_DEBUG, 16);
|
|
if (debugLevel & 0x4) {
|
|
debug = function (x) { util.error('HTTP: ' + x); };
|
|
} else {
|
|
debug = function () { };
|
|
}
|
|
|
|
var net = require('net');
|
|
var stream = require('stream');
|
|
|
|
var FreeList = require('freelist').FreeList;
|
|
var HTTPParser = process.binding('http_parser').HTTPParser;
|
|
|
|
var parsers = new FreeList('parsers', 1000, function () {
|
|
var parser = new HTTPParser('request');
|
|
|
|
parser.onMessageBegin = function () {
|
|
parser.incoming = new IncomingMessage(parser.socket);
|
|
parser.field = null;
|
|
parser.value = null;
|
|
};
|
|
|
|
// Only servers will get URL events.
|
|
parser.onURL = function (b, start, len) {
|
|
var slice = b.toString('ascii', start, start+len);
|
|
if (parser.incoming.url) {
|
|
parser.incoming.url += slice;
|
|
} else {
|
|
// Almost always will branch here.
|
|
parser.incoming.url = slice;
|
|
}
|
|
};
|
|
|
|
parser.onHeaderField = function (b, start, len) {
|
|
var slice = b.toString('ascii', start, start+len).toLowerCase();
|
|
if (parser.value != undefined) {
|
|
parser.incoming._addHeaderLine(parser.field, parser.value);
|
|
parser.field = null;
|
|
parser.value = null;
|
|
}
|
|
if (parser.field) {
|
|
parser.field += slice;
|
|
} else {
|
|
parser.field = slice;
|
|
}
|
|
};
|
|
|
|
parser.onHeaderValue = function (b, start, len) {
|
|
var slice = b.toString('ascii', start, start+len);
|
|
if (parser.value) {
|
|
parser.value += slice;
|
|
} else {
|
|
parser.value = slice;
|
|
}
|
|
};
|
|
|
|
parser.onHeadersComplete = function (info) {
|
|
if (parser.field && (parser.value != undefined)) {
|
|
parser.incoming._addHeaderLine(parser.field, parser.value);
|
|
parser.field = null;
|
|
parser.value = null;
|
|
}
|
|
|
|
parser.incoming.httpVersionMajor = info.versionMajor;
|
|
parser.incoming.httpVersionMinor = info.versionMinor;
|
|
parser.incoming.httpVersion = info.versionMajor
|
|
+ '.'
|
|
+ info.versionMinor ;
|
|
|
|
if (info.method) {
|
|
// server only
|
|
parser.incoming.method = info.method;
|
|
} else {
|
|
// client only
|
|
parser.incoming.statusCode = info.statusCode;
|
|
}
|
|
|
|
parser.incoming.upgrade = info.upgrade;
|
|
|
|
var isHeadResponse = false;
|
|
|
|
if (!info.upgrade) {
|
|
// For upgraded connections, we'll emit this after parser.execute
|
|
// so that we can capture the first part of the new protocol
|
|
isHeadResponse = parser.onIncoming(parser.incoming, info.shouldKeepAlive);
|
|
}
|
|
|
|
return isHeadResponse;
|
|
};
|
|
|
|
parser.onBody = function (b, start, len) {
|
|
// TODO body encoding?
|
|
var slice = b.slice(start, start+len);
|
|
if (parser.incoming._decoder) {
|
|
var string = parser.incoming._decoder.write(slice);
|
|
if (string.length) parser.incoming.emit('data', string);
|
|
} else {
|
|
parser.incoming.emit('data', slice);
|
|
}
|
|
};
|
|
|
|
parser.onMessageComplete = function () {
|
|
this.incoming.complete = true;
|
|
if (parser.field && (parser.value != undefined)) {
|
|
parser.incoming._addHeaderLine(parser.field, parser.value);
|
|
}
|
|
if (!parser.incoming.upgrade) {
|
|
// For upgraded connections, also emit this after parser.execute
|
|
parser.incoming.emit("end");
|
|
}
|
|
};
|
|
|
|
return parser;
|
|
});
|
|
exports.parsers = parsers;
|
|
|
|
|
|
var CRLF = "\r\n";
|
|
var STATUS_CODES = exports.STATUS_CODES = {
|
|
100 : 'Continue',
|
|
101 : 'Switching Protocols',
|
|
102 : 'Processing', // RFC 2518, obsoleted by RFC 4918
|
|
200 : 'OK',
|
|
201 : 'Created',
|
|
202 : 'Accepted',
|
|
203 : 'Non-Authoritative Information',
|
|
204 : 'No Content',
|
|
205 : 'Reset Content',
|
|
206 : 'Partial Content',
|
|
207 : 'Multi-Status', // RFC 4918
|
|
300 : 'Multiple Choices',
|
|
301 : 'Moved Permanently',
|
|
302 : 'Moved Temporarily',
|
|
303 : 'See Other',
|
|
304 : 'Not Modified',
|
|
305 : 'Use Proxy',
|
|
307 : 'Temporary Redirect',
|
|
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',
|
|
416 : 'Requested Range Not Satisfiable',
|
|
417 : 'Expectation Failed',
|
|
418 : 'I\'m a teapot', // RFC 2324
|
|
422 : 'Unprocessable Entity', // RFC 4918
|
|
423 : 'Locked', // RFC 4918
|
|
424 : 'Failed Dependency', // RFC 4918
|
|
425 : 'Unordered Collection', // RFC 4918
|
|
426 : 'Upgrade Required', // RFC 2817
|
|
500 : 'Internal Server Error',
|
|
501 : 'Not Implemented',
|
|
502 : 'Bad Gateway',
|
|
503 : 'Service Unavailable',
|
|
504 : 'Gateway Time-out',
|
|
505 : 'HTTP Version not supported',
|
|
506 : 'Variant Also Negotiates', // RFC 2295
|
|
507 : 'Insufficient Storage', // RFC 4918
|
|
509 : 'Bandwidth Limit Exceeded',
|
|
510 : 'Not Extended' // RFC 2774
|
|
};
|
|
|
|
var connectionExpression = /Connection/i;
|
|
var transferEncodingExpression = /Transfer-Encoding/i;
|
|
var closeExpression = /close/i;
|
|
var chunkExpression = /chunk/i;
|
|
var contentLengthExpression = /Content-Length/i;
|
|
var expectExpression = /Expect/i;
|
|
var continueExpression = /100-continue/i;
|
|
|
|
|
|
/* Abstract base class for ServerRequest and ClientResponse. */
|
|
function IncomingMessage (socket) {
|
|
stream.Stream.call(this);
|
|
|
|
// TODO Remove one of these eventually.
|
|
this.socket = socket;
|
|
this.connection = socket;
|
|
|
|
this.httpVersion = null;
|
|
this.complete = false;
|
|
this.headers = {};
|
|
this.trailers = {};
|
|
|
|
this.readable = true;
|
|
|
|
// request (server) only
|
|
this.url = "";
|
|
|
|
this.method = null;
|
|
|
|
// response (client) only
|
|
this.statusCode = null;
|
|
this.client = this.socket;
|
|
}
|
|
util.inherits(IncomingMessage, stream.Stream);
|
|
exports.IncomingMessage = IncomingMessage;
|
|
|
|
|
|
IncomingMessage.prototype.destroy = function (error) {
|
|
this.socket.destroy(error);
|
|
};
|
|
|
|
|
|
IncomingMessage.prototype.setEncoding = function (encoding) {
|
|
var StringDecoder = require("string_decoder").StringDecoder; // lazy load
|
|
this._decoder = new StringDecoder(encoding);
|
|
};
|
|
|
|
IncomingMessage.prototype.pause = function () {
|
|
this.socket.pause();
|
|
};
|
|
|
|
IncomingMessage.prototype.resume = function () {
|
|
this.socket.resume();
|
|
};
|
|
|
|
// Add the given (field, value) pair to the message
|
|
//
|
|
// Per RFC2616, section 4.2 it is acceptable to join multiple instances of the
|
|
// same header with a ', ' if the header in question supports specification of
|
|
// multiple values this way. If not, we declare the first instance the winner
|
|
// and drop the second. Extended header fields (those beginning with 'x-') are
|
|
// always joined.
|
|
IncomingMessage.prototype._addHeaderLine = function (field, value) {
|
|
var dest;
|
|
if (this.complete) {
|
|
dest = this.trailers;
|
|
} else {
|
|
dest = this.headers;
|
|
}
|
|
switch (field) {
|
|
// Array headers:
|
|
case 'set-cookie':
|
|
if (field in dest) {
|
|
dest[field].push(value);
|
|
} else {
|
|
dest[field] = [value];
|
|
}
|
|
break;
|
|
|
|
// Comma separate. Maybe make these arrays?
|
|
case 'accept':
|
|
case 'accept-charset':
|
|
case 'accept-encoding':
|
|
case 'accept-language':
|
|
case 'connection':
|
|
case 'cookie':
|
|
if (field in dest) {
|
|
dest[field] += ', ' + value;
|
|
} else {
|
|
dest[field] = value;
|
|
}
|
|
break;
|
|
|
|
|
|
default:
|
|
if (field.slice(0,2) == 'x-') {
|
|
// except for x-
|
|
if (field in dest) {
|
|
dest[field] += ', ' + value;
|
|
} else {
|
|
dest[field] = value;
|
|
}
|
|
} else {
|
|
// drop duplicates
|
|
if (!(field in dest)) dest[field] = value;
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
function OutgoingMessage (socket) {
|
|
stream.Stream.call(this);
|
|
|
|
// TODO Remove one of these eventually.
|
|
this.socket = socket;
|
|
this.connection = socket;
|
|
|
|
this.output = [];
|
|
this.outputEncodings = [];
|
|
|
|
this.writable = true;
|
|
|
|
this._last = false;
|
|
this.chunkedEncoding = false;
|
|
this.shouldKeepAlive = true;
|
|
this.useChunkedEncodingByDefault = true;
|
|
|
|
this._hasBody = true;
|
|
this._trailer = '';
|
|
|
|
this.finished = false;
|
|
}
|
|
util.inherits(OutgoingMessage, stream.Stream);
|
|
exports.OutgoingMessage = OutgoingMessage;
|
|
|
|
|
|
OutgoingMessage.prototype.destroy = function (error) {
|
|
this.socket.destroy(error);
|
|
};
|
|
|
|
|
|
// This abstract either writing directly to the socket or buffering it.
|
|
OutgoingMessage.prototype._send = function (data, encoding) {
|
|
// This is a shameful hack to get the headers and first body chunk onto
|
|
// the same packet. Future versions of Node are going to take care of
|
|
// this at a lower level and in a more general way.
|
|
if (!this._headerSent) {
|
|
if (typeof data === 'string') {
|
|
data = this._header + data;
|
|
} else {
|
|
this.output.unshift(this._header);
|
|
this.outputEncodings.unshift('ascii');
|
|
}
|
|
this._headerSent = true;
|
|
}
|
|
return this._writeRaw(data, encoding);
|
|
};
|
|
|
|
OutgoingMessage.prototype._writeRaw = function(data, encoding) {
|
|
if (this.connection._outgoing[0] === this && this.connection.writable) {
|
|
// There might be pending data in the this.output buffer.
|
|
while (this.output.length) {
|
|
if (!this.connection.writable) {
|
|
this._buffer(data, encoding);
|
|
return false;
|
|
}
|
|
var c = this.output.shift();
|
|
var e = this.outputEncodings.shift();
|
|
this.connection.write(c, e);
|
|
}
|
|
|
|
// Directly write to socket.
|
|
return this.connection.write(data, encoding);
|
|
} else {
|
|
this._buffer(data, encoding);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
OutgoingMessage.prototype._buffer = function (data, encoding) {
|
|
if (data.length === 0) return;
|
|
|
|
var length = this.output.length;
|
|
|
|
if (length === 0 || typeof data != 'string') {
|
|
this.output.push(data);
|
|
encoding = encoding || "ascii";
|
|
this.outputEncodings.push(encoding);
|
|
return false;
|
|
}
|
|
|
|
var lastEncoding = this.outputEncodings[length-1];
|
|
var lastData = this.output[length-1];
|
|
|
|
if ((lastEncoding === encoding) ||
|
|
(!encoding && data.constructor === lastData.constructor)) {
|
|
this.output[length-1] = lastData + data;
|
|
return false;
|
|
}
|
|
|
|
this.output.push(data);
|
|
encoding = encoding || "ascii";
|
|
this.outputEncodings.push(encoding);
|
|
|
|
return false;
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype._storeHeader = function (firstLine, headers) {
|
|
var sentConnectionHeader = false;
|
|
var sentContentLengthHeader = false;
|
|
var sentTransferEncodingHeader = false;
|
|
var sentExpect = false;
|
|
|
|
// firstLine 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 messageHeader = firstLine;
|
|
var field, value;
|
|
var self = this;
|
|
|
|
function store(field, value) {
|
|
messageHeader += field + ": " + value + CRLF;
|
|
|
|
if (connectionExpression.test(field)) {
|
|
sentConnectionHeader = true;
|
|
if (closeExpression.test(value)) {
|
|
self._last = true;
|
|
} else {
|
|
self.shouldKeepAlive = true;
|
|
}
|
|
|
|
} else if (transferEncodingExpression.test(field)) {
|
|
sentTransferEncodingHeader = true;
|
|
if (chunkExpression.test(value)) self.chunkedEncoding = true;
|
|
|
|
} else if (contentLengthExpression.test(field)) {
|
|
sentContentLengthHeader = true;
|
|
|
|
} else if (expectExpression.test(field)) {
|
|
sentExpect = true;
|
|
|
|
}
|
|
}
|
|
|
|
if (headers) {
|
|
var keys = Object.keys(headers);
|
|
var isArray = (Array.isArray(headers));
|
|
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
var key = keys[i];
|
|
if (isArray) {
|
|
field = headers[key][0];
|
|
value = headers[key][1];
|
|
} else {
|
|
field = key;
|
|
value = headers[key];
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
for (var j = 0; j < value.length; j++) {
|
|
store(field, value[j]);
|
|
}
|
|
} else {
|
|
store(field, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// keep-alive logic
|
|
if (sentConnectionHeader == false) {
|
|
if (this.shouldKeepAlive &&
|
|
(sentContentLengthHeader || this.useChunkedEncodingByDefault)) {
|
|
messageHeader += "Connection: keep-alive\r\n";
|
|
} else {
|
|
this._last = true;
|
|
messageHeader += "Connection: close\r\n";
|
|
}
|
|
}
|
|
|
|
if (sentContentLengthHeader == false && sentTransferEncodingHeader == false) {
|
|
if (this._hasBody) {
|
|
if (this.useChunkedEncodingByDefault) {
|
|
messageHeader += "Transfer-Encoding: chunked\r\n";
|
|
this.chunkedEncoding = true;
|
|
} else {
|
|
this._last = true;
|
|
}
|
|
} else {
|
|
// Make sure we don't end the 0\r\n\r\n at the end of the message.
|
|
this.chunkedEncoding = false;
|
|
}
|
|
}
|
|
|
|
this._header = messageHeader + CRLF;
|
|
this._headerSent = false;
|
|
// wait until the first body chunk, or close(), is sent to flush,
|
|
// UNLESS we're sending Expect: 100-continue.
|
|
if (sentExpect) {
|
|
this._send("");
|
|
}
|
|
};
|
|
|
|
OutgoingMessage.prototype.write = function (chunk, encoding) {
|
|
if (!this._header) {
|
|
throw new Error("You have to call writeHead() before write()");
|
|
}
|
|
|
|
if (!this._hasBody) {
|
|
console.error("This type of response MUST NOT have a body. Ignoring write() calls.");
|
|
return true;
|
|
}
|
|
|
|
if (typeof chunk !== "string"
|
|
&& !Buffer.isBuffer(chunk)
|
|
&& !Array.isArray(chunk)) {
|
|
throw new TypeError("first argument must be a string, Array, or Buffer");
|
|
}
|
|
|
|
if (chunk.length === 0) return false;
|
|
|
|
var len, ret;
|
|
if (this.chunkedEncoding) {
|
|
if (typeof(chunk) === 'string') {
|
|
len = Buffer.byteLength(chunk, encoding);
|
|
var chunk = len.toString(16) + CRLF + chunk + CRLF;
|
|
debug('string chunk = ' + util.inspect(chunk));
|
|
ret = this._send(chunk, encoding);
|
|
} else {
|
|
// buffer
|
|
len = chunk.length;
|
|
this._send(len.toString(16) + CRLF);
|
|
this._send(chunk);
|
|
ret = this._send(CRLF);
|
|
}
|
|
} else {
|
|
ret = this._send(chunk, encoding);
|
|
}
|
|
|
|
debug('write ret = ' + ret);
|
|
return ret;
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype.addTrailers = function (headers) {
|
|
this._trailer = "";
|
|
var keys = Object.keys(headers);
|
|
var isArray = (Array.isArray(headers));
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
var key = keys[i];
|
|
if (isArray) {
|
|
field = headers[key][0];
|
|
value = headers[key][1];
|
|
} else {
|
|
field = key;
|
|
value = headers[key];
|
|
}
|
|
|
|
this._trailer += field + ": " + value + CRLF;
|
|
}
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype.end = function (data, encoding) {
|
|
var ret;
|
|
|
|
var hot = this._headerSent === false
|
|
&& typeof(data) === "string"
|
|
&& data.length > 0
|
|
&& this.output.length === 0
|
|
&& this.connection.writable
|
|
&& this.connection._outgoing[0] === this
|
|
;
|
|
|
|
if (hot) {
|
|
// Hot path. They're doing
|
|
// res.writeHead();
|
|
// res.end(blah);
|
|
// HACKY.
|
|
if (this.chunkedEncoding) {
|
|
var l = Buffer.byteLength(data, encoding).toString(16);
|
|
ret = this.connection.write( this._header
|
|
+ l
|
|
+ CRLF
|
|
+ data
|
|
+ "\r\n0\r\n"
|
|
+ this._trailer
|
|
+ "\r\n"
|
|
, encoding
|
|
);
|
|
} else {
|
|
ret = this.connection.write(this._header + data, encoding);
|
|
}
|
|
this._headerSent = true;
|
|
|
|
} else if (data) {
|
|
// Normal body write.
|
|
ret = this.write(data, encoding);
|
|
}
|
|
|
|
if (!hot) {
|
|
if (this.chunkedEncoding) {
|
|
ret = this._send('0\r\n' + this._trailer + '\r\n'); // Last chunk.
|
|
} else {
|
|
// Force a flush, HACK.
|
|
ret = this._send('');
|
|
}
|
|
}
|
|
|
|
this.finished = true;
|
|
|
|
// There is the first message on the outgoing queue, and we've sent
|
|
// everything to the socket.
|
|
if (this.output.length === 0 && this.connection._outgoing[0] === this) {
|
|
debug('outgoing message end. shifting because was flushed');
|
|
this.connection._onOutgoingSent();
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
|
|
function ServerResponse (req) {
|
|
OutgoingMessage.call(this, req.socket);
|
|
|
|
if (req.method === 'HEAD') this._hasBody = false;
|
|
|
|
if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
|
|
this.useChunkedEncodingByDefault = false;
|
|
this.shouldKeepAlive = false;
|
|
}
|
|
}
|
|
util.inherits(ServerResponse, OutgoingMessage);
|
|
exports.ServerResponse = ServerResponse;
|
|
|
|
ServerResponse.prototype.writeContinue = function () {
|
|
this._writeRaw("HTTP/1.1 100 Continue" + CRLF + CRLF, 'ascii');
|
|
this._sent100 = true;
|
|
};
|
|
|
|
ServerResponse.prototype.writeHead = function (statusCode) {
|
|
var reasonPhrase, headers, headerIndex;
|
|
|
|
if (typeof arguments[1] == 'string') {
|
|
reasonPhrase = arguments[1];
|
|
headerIndex = 2;
|
|
} else {
|
|
reasonPhrase = STATUS_CODES[statusCode] || "unknown";
|
|
headerIndex = 1;
|
|
}
|
|
|
|
if (typeof arguments[headerIndex] == 'object') {
|
|
headers = arguments[headerIndex];
|
|
} else {
|
|
headers = {};
|
|
}
|
|
|
|
var statusLine = "HTTP/1.1 " + statusCode.toString() + " "
|
|
+ reasonPhrase + CRLF;
|
|
|
|
if ( statusCode === 204
|
|
|| statusCode === 304
|
|
|| (statusCode >= 100 && statusCode <= 199)
|
|
) {
|
|
// RFC 2616, 10.2.5:
|
|
// The 204 response MUST NOT include a message-body, and thus is always
|
|
// terminated by the first empty line after the header fields.
|
|
// RFC 2616, 10.3.5:
|
|
// The 304 response MUST NOT contain a message-body, and thus is always
|
|
// terminated by the first empty line after the header fields.
|
|
// RFC 2616, 10.1 Informational 1xx:
|
|
// This class of status code indicates a provisional response,
|
|
// consisting only of the Status-Line and optional headers, and is
|
|
// terminated by an empty line.
|
|
this._hasBody = false;
|
|
}
|
|
|
|
// don't keep alive connections where the client expects 100 Continue
|
|
// but we sent a final status; they may put extra bytes on the wire.
|
|
if (this._expect_continue && ! this._sent100) {
|
|
this.shouldKeepAlive = false;
|
|
}
|
|
|
|
this._storeHeader(statusLine, headers);
|
|
};
|
|
|
|
|
|
ServerResponse.prototype.writeHeader = function () {
|
|
this.writeHead.apply(this, arguments);
|
|
};
|
|
|
|
function ClientRequest (socket, method, url, headers) {
|
|
OutgoingMessage.call(this, socket);
|
|
|
|
this.method = method = method.toUpperCase();
|
|
this.shouldKeepAlive = false;
|
|
if (method === "GET" || method === "HEAD") {
|
|
this.useChunkedEncodingByDefault = false;
|
|
} else {
|
|
this.useChunkedEncodingByDefault = true;
|
|
}
|
|
this._last = true;
|
|
|
|
this._storeHeader(method + " " + url + " HTTP/1.1\r\n", headers);
|
|
}
|
|
util.inherits(ClientRequest, OutgoingMessage);
|
|
exports.ClientRequest = ClientRequest;
|
|
|
|
function outgoingFlush (socket) {
|
|
// This logic is probably a bit confusing. Let me explain a bit:
|
|
//
|
|
// In both HTTP servers and clients it is possible to queue up several
|
|
// outgoing messages. This is easiest to imagine in the case of a client.
|
|
// Take the following situation:
|
|
//
|
|
// req1 = client.request('GET', '/');
|
|
// req2 = client.request('POST', '/');
|
|
//
|
|
// The question is what happens when the user does
|
|
//
|
|
// req2.write("hello world\n");
|
|
//
|
|
// It's possible that the first request has not been completely flushed to
|
|
// the socket yet. Thus the outgoing messages need to be prepared to queue
|
|
// up data internally before sending it on further to the socket's queue.
|
|
//
|
|
// This function, outgoingFlush(), is called by both the Server
|
|
// implementation and the Client implementation to attempt to flush any
|
|
// pending messages out to the socket.
|
|
var message = socket._outgoing[0];
|
|
|
|
if (!message) return;
|
|
|
|
var ret;
|
|
|
|
while (message.output.length) {
|
|
if (!socket.writable) return; // XXX Necessary?
|
|
|
|
var data = message.output.shift();
|
|
var encoding = message.outputEncodings.shift();
|
|
|
|
ret = socket.write(data, encoding);
|
|
}
|
|
|
|
if (message.finished) {
|
|
socket._onOutgoingSent();
|
|
} else if (ret) {
|
|
message.emit('drain');
|
|
}
|
|
}
|
|
|
|
|
|
function httpSocketSetup (socket) {
|
|
// An array of outgoing messages for the socket. In pipelined connections
|
|
// we need to keep track of the order they were sent.
|
|
socket._outgoing = [];
|
|
socket.__destroyOnDrain = false;
|
|
|
|
// NOTE: be sure not to use ondrain elsewhere in this file!
|
|
socket.ondrain = function () {
|
|
var message = socket._outgoing[0];
|
|
if (message) message.emit('drain');
|
|
if (socket.__destroyOnDrain) socket.destroy();
|
|
};
|
|
}
|
|
|
|
|
|
|
|
function Server (requestListener) {
|
|
if (!(this instanceof Server)) return new Server(requestListener);
|
|
net.Server.call(this, { allowHalfOpen: true });
|
|
|
|
if (requestListener) {
|
|
this.addListener("request", requestListener);
|
|
}
|
|
|
|
this.addListener("connection", connectionListener);
|
|
}
|
|
util.inherits(Server, net.Server);
|
|
|
|
Server.prototype.setSecure = function (credentials) {
|
|
this.secure = true;
|
|
this.credentials = credentials;
|
|
};
|
|
|
|
exports.Server = Server;
|
|
|
|
exports.createServer = function (requestListener) {
|
|
return new Server(requestListener);
|
|
};
|
|
|
|
function connectionListener (socket) {
|
|
var self = this;
|
|
|
|
debug("new http connection");
|
|
|
|
httpSocketSetup(socket);
|
|
|
|
socket.setTimeout(2*60*1000); // 2 minute timeout
|
|
socket.addListener('timeout', function () {
|
|
socket.destroy();
|
|
});
|
|
|
|
var parser = parsers.alloc();
|
|
parser.reinitialize('request');
|
|
parser.socket = socket;
|
|
|
|
if (self.secure) {
|
|
socket.setSecure(self.credentials);
|
|
}
|
|
|
|
socket.addListener('error', function (e) {
|
|
self.emit('clientError', e);
|
|
});
|
|
|
|
socket.ondata = function (d, start, end) {
|
|
var ret = parser.execute(d, start, end - start);
|
|
if (ret instanceof Error) {
|
|
socket.destroy(ret);
|
|
} else if (parser.incoming && parser.incoming.upgrade) {
|
|
var bytesParsed = ret;
|
|
socket.ondata = null;
|
|
socket.onend = null;
|
|
|
|
var req = parser.incoming;
|
|
|
|
// This is start + byteParsed + 1 due to the error of getting \n
|
|
// in the upgradeHead from the closing lines of the headers
|
|
var upgradeHead = d.slice(start + bytesParsed + 1, end);
|
|
|
|
if (self.listeners("upgrade").length) {
|
|
self.emit('upgrade', req, req.socket, upgradeHead);
|
|
} else {
|
|
// Got upgrade header, but have no handler.
|
|
socket.destroy();
|
|
}
|
|
}
|
|
};
|
|
|
|
socket.onend = function () {
|
|
parser.finish();
|
|
|
|
if (socket._outgoing.length) {
|
|
socket._outgoing[socket._outgoing.length-1]._last = true;
|
|
outgoingFlush(socket);
|
|
} else {
|
|
socket.end();
|
|
}
|
|
};
|
|
|
|
socket.addListener('close', function () {
|
|
// unref the parser for easy gc
|
|
parsers.free(parser);
|
|
});
|
|
|
|
// At the end of each response message, after it has been flushed to the
|
|
// socket. Here we insert logic about what to do next.
|
|
socket._onOutgoingSent = function (message) {
|
|
var message = socket._outgoing.shift();
|
|
if (message._last) {
|
|
// No more messages to be pushed out.
|
|
|
|
// HACK: need way to do this with socket interface
|
|
if (socket._writeQueue.length) {
|
|
socket.__destroyOnDrain = true; //socket.end();
|
|
} else {
|
|
socket.destroy();
|
|
}
|
|
|
|
} else if (socket._outgoing.length) {
|
|
// Push out the next message.
|
|
outgoingFlush(socket);
|
|
}
|
|
};
|
|
|
|
// The following callback is issued after the headers have been read on a
|
|
// new message. In this callback we setup the response object and pass it
|
|
// to the user.
|
|
parser.onIncoming = function (req, shouldKeepAlive) {
|
|
var res = new ServerResponse(req);
|
|
debug('server response shouldKeepAlive: ' + shouldKeepAlive);
|
|
res.shouldKeepAlive = shouldKeepAlive;
|
|
socket._outgoing.push(res);
|
|
|
|
if ('expect' in req.headers
|
|
&& (req.httpVersionMajor == 1 && req.httpVersionMinor == 1)
|
|
&& continueExpression.test(req.headers['expect'])) {
|
|
res._expect_continue = true;
|
|
if (self.listeners("checkContinue").length) {
|
|
self.emit("checkContinue", req, res);
|
|
} else {
|
|
res.writeContinue();
|
|
self.emit('request', req, res);
|
|
}
|
|
} else {
|
|
self.emit('request', req, res);
|
|
}
|
|
return false; // Not a HEAD response. (Not even a response!)
|
|
};
|
|
}
|
|
|
|
|
|
function Client ( ) {
|
|
if (!(this instanceof Client)) return new Client();
|
|
net.Stream.call(this, { allowHalfOpen: true });
|
|
var self = this;
|
|
|
|
httpSocketSetup(self);
|
|
|
|
function onData(d, start, end) {
|
|
if (!self.parser) {
|
|
throw new Error("parser not initialized prior to Client.ondata call");
|
|
}
|
|
var ret = self.parser.execute(d, start, end - start);
|
|
if (ret instanceof Error) {
|
|
self.destroy(ret);
|
|
} else if (self.parser.incoming && self.parser.incoming.upgrade) {
|
|
var bytesParsed = ret;
|
|
self.ondata = null;
|
|
self.onend = null;
|
|
|
|
var req = self.parser.incoming;
|
|
|
|
var upgradeHead = d.slice(start + bytesParsed + 1, end);
|
|
|
|
if (self.listeners('upgrade').length) {
|
|
self.emit('upgrade', req, self, upgradeHead);
|
|
} else {
|
|
self.destroy();
|
|
}
|
|
}
|
|
};
|
|
|
|
self.addListener("connect", function () {
|
|
debug('client connected');
|
|
|
|
self.ondata = onData;
|
|
self.onend = onEnd;
|
|
|
|
if (this.https) {
|
|
this.setSecure(this.credentials);
|
|
} else {
|
|
self._initParser();
|
|
debug('requests: ' + util.inspect(self._outgoing));
|
|
outgoingFlush(self);
|
|
}
|
|
});
|
|
|
|
self.addListener("secure", function () {
|
|
self._initParser();
|
|
debug('requests: ' + util.inspect(self._outgoing));
|
|
outgoingFlush(self);
|
|
});
|
|
|
|
function onEnd() {
|
|
if (self.parser) self.parser.finish();
|
|
debug("self got end closing. readyState = " + self.readyState);
|
|
self.end();
|
|
};
|
|
|
|
self.addListener("close", function (e) {
|
|
if (e) return;
|
|
|
|
debug("HTTP CLIENT onClose. readyState = " + self.readyState);
|
|
|
|
// finally done with the request
|
|
self._outgoing.shift();
|
|
|
|
// If there are more requests to handle, reconnect.
|
|
if (self._outgoing.length) {
|
|
self._reconnect();
|
|
} else if (self.parser) {
|
|
parsers.free(self.parser);
|
|
self.parser = null;
|
|
}
|
|
});
|
|
};
|
|
util.inherits(Client, net.Stream);
|
|
|
|
exports.Client = Client;
|
|
|
|
exports.createClient = function (port, host, https, credentials) {
|
|
var c = new Client();
|
|
c.port = port;
|
|
c.host = host;
|
|
c.https = https;
|
|
c.credentials = credentials;
|
|
return c;
|
|
};
|
|
|
|
|
|
Client.prototype._initParser = function () {
|
|
var self = this;
|
|
if (!self.parser) self.parser = parsers.alloc();
|
|
self.parser.reinitialize('response');
|
|
self.parser.socket = self;
|
|
self.parser.onIncoming = function (res) {
|
|
debug("incoming response!");
|
|
|
|
var req = self._outgoing[0];
|
|
|
|
// Responses to HEAD requests are AWFUL. Ask Ryan.
|
|
// A major oversight in HTTP. Hence this nastiness.
|
|
var isHeadResponse = req.method == "HEAD";
|
|
debug('isHeadResponse ' + isHeadResponse);
|
|
|
|
if (res.statusCode == 100) {
|
|
// restart the parser, as this is a continue message.
|
|
req.emit("continue");
|
|
return true;
|
|
}
|
|
|
|
if (req.shouldKeepAlive && res.headers.connection === 'close') {
|
|
req.shouldKeepAlive = false;
|
|
}
|
|
|
|
res.addListener('end', function ( ) {
|
|
debug("request complete disconnecting. readyState = " + self.readyState);
|
|
// For the moment we reconnect for every request. FIXME!
|
|
// All that should be required for keep-alive is to not reconnect,
|
|
// but outgoingFlush instead.
|
|
if (req.shouldKeepAlive) {
|
|
outgoingFlush(self);
|
|
self._outgoing.shift();
|
|
outgoingFlush(self);
|
|
} else {
|
|
self.end();
|
|
}
|
|
});
|
|
|
|
req.emit("response", res);
|
|
|
|
return isHeadResponse;
|
|
};
|
|
};
|
|
|
|
|
|
// This is called each time a request has been pushed completely to the
|
|
// socket. The message that was sent is still sitting at client._outgoing[0]
|
|
// it is our responsibility to shift it off.
|
|
//
|
|
// We have to be careful when it we shift it because once we do any writes
|
|
// to other requests will be flushed directly to the socket.
|
|
//
|
|
// At the moment we're implement a client which connects and disconnects on
|
|
// each request/response cycle so we cannot shift off the request from
|
|
// client._outgoing until we're completely disconnected after the response
|
|
// comes back.
|
|
Client.prototype._onOutgoingSent = function (message) {
|
|
// We've just finished a message. We don't end/shutdown the connection here
|
|
// because HTTP servers typically cannot handle half-closed connections
|
|
// (Node servers can).
|
|
//
|
|
// Instead, we just check if the connection is closed, and if so
|
|
// reconnect if we have pending messages.
|
|
if (this._outgoing.length && this.readyState == "closed") {
|
|
debug("HTTP client request flush. reconnect. readyState = " + this.readyState);
|
|
this._reconnect();
|
|
}
|
|
};
|
|
|
|
|
|
Client.prototype._reconnect = function () {
|
|
if (this.readyState === "closed") {
|
|
debug("HTTP CLIENT: reconnecting readyState = " + this.readyState);
|
|
this.connect(this.port, this.host);
|
|
}
|
|
};
|
|
|
|
Client.prototype.request = function (method, url, headers) {
|
|
if (typeof(url) != "string") {
|
|
// assume method was omitted, shift arguments
|
|
headers = url;
|
|
url = method;
|
|
method = "GET";
|
|
}
|
|
var req = new ClientRequest(this, method, url, headers);
|
|
this._outgoing.push(req);
|
|
if (this.readyState === 'closed') this._reconnect();
|
|
return req;
|
|
};
|
|
|
|
|
|
exports.cat = function (url, encoding_, headers_) {
|
|
var encoding = 'utf8',
|
|
headers = {},
|
|
callback = null;
|
|
|
|
// parse the arguments for the various options... very ugly
|
|
if (typeof(arguments[1]) == 'string') {
|
|
encoding = arguments[1];
|
|
if (typeof(arguments[2]) == 'object') {
|
|
headers = arguments[2];
|
|
if (typeof(arguments[3]) == 'function') callback = arguments[3];
|
|
} else {
|
|
if (typeof(arguments[2]) == 'function') callback = arguments[2];
|
|
}
|
|
} else {
|
|
// didn't specify encoding
|
|
if (typeof(arguments[1]) == 'object') {
|
|
headers = arguments[1];
|
|
callback = arguments[2];
|
|
} else {
|
|
callback = arguments[1];
|
|
}
|
|
}
|
|
|
|
var url = require("url").parse(url);
|
|
|
|
var hasHost = false;
|
|
if (Array.isArray(headers)) {
|
|
for (var i = 0, l = headers.length; i < l; i++) {
|
|
if (headers[i][0].toLowerCase() === 'host') {
|
|
hasHost = true;
|
|
break;
|
|
}
|
|
}
|
|
} else if (typeof headers === "Object") {
|
|
var keys = Object.keys(headers);
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
var key = keys[i];
|
|
if (key.toLowerCase() == 'host') {
|
|
hasHost = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!hasHost) headers["Host"] = url.hostname;
|
|
|
|
var content = "";
|
|
|
|
var client = exports.createClient(url.port || 80, url.hostname);
|
|
var req = client.request((url.pathname || "/")+(url.search || "")+(url.hash || ""), headers);
|
|
|
|
if (url.protocol=="https:") {
|
|
client.https = true;
|
|
}
|
|
|
|
var callbackSent = false;
|
|
|
|
req.addListener('response', function (res) {
|
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
if (callback && !callbackSent) {
|
|
callback(res.statusCode);
|
|
callbackSent = true;
|
|
}
|
|
client.end();
|
|
return;
|
|
}
|
|
res.setEncoding(encoding);
|
|
res.addListener('data', function (chunk) { content += chunk; });
|
|
res.addListener('end', function () {
|
|
if (callback && !callbackSent) {
|
|
callback(null, content);
|
|
callbackSent = true;
|
|
}
|
|
});
|
|
});
|
|
|
|
client.addListener("error", function (err) {
|
|
if (callback && !callbackSent) {
|
|
callback(err);
|
|
callbackSent = true;
|
|
}
|
|
});
|
|
|
|
client.addListener("close", function () {
|
|
if (callback && !callbackSent) {
|
|
callback(new Error('Connection closed unexpectedly'));
|
|
callbackSent = true;
|
|
}
|
|
});
|
|
req.end();
|
|
};
|