diff --git a/lib/http2.js b/lib/http2.js new file mode 100644 index 00000000000..846a4c31f20 --- /dev/null +++ b/lib/http2.js @@ -0,0 +1,425 @@ +var sys = require('./sys'); +var net = require('./net'); +var events = require('events'); + +var CRLF = "\r\n"; +var STATUS_CODES = exports.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' +}; + +var connectionExpression = /Connection/i; +var transferEncodingExpression = /Transfer-Encoding/i; +var closeExpression = /close/i; +var chunkExpression = /chunk/i; +var contentLengthExpression = /Content-Length/i; + + +/* Abstract base class for ServerRequest and ClientResponse. */ +function IncomingMessage (socket) { + events.EventEmitter.call(this); + + this.socket = socket; + this.httpVersion = null; + this.headers = {}; + + this.method = null; + + // response (client) only + this.statusCode = null; +} +sys.inherits(IncomingMessage, events.EventEmitter); +exports.IncomingMessage = IncomingMessage; + +IncomingMessage.prototype._parseQueryString = function () { + throw new Error("_parseQueryString is deprecated. Use require(\"querystring\") to parse query strings.\n"); +}; + +IncomingMessage.prototype.setBodyEncoding = function (enc) { + // TODO: Find a cleaner way of doing this. + this.socket.setEncoding(enc); +}; + +IncomingMessage.prototype.pause = function () { + this.socket.readPause(); +}; + +IncomingMessage.prototype.resume = function () { + this.socket.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 () { + events.EventEmitter.call(this); + + this.output = []; + this.outputEncodings = []; + + this.closeOnFinish = false; + this.chunkEncoding = false; + this.shouldKeepAlive = true; + this.useChunkedEncodingByDefault = true; + + this.flushing = false; + + this.finished = false; +} +sys.inherits(OutgoingMessage, events.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 sentConnectionHeader = false; + var sendContentLengthHeader = false; + var sendTransferEncodingHeader = 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 messageHeader = 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]; + } + + messageHeader += field + ": " + value + CRLF; + + if (connectionExpression.test(field)) { + sentConnectionHeader = true; + if (closeExpression.test(value)) this.closeOnFinish = true; + + } else if (transferEncodingExpression.test(field)) { + sendTransferEncodingHeader = true; + if (chunkExpression.test(value)) this.chunkEncoding = true; + + } else if (contentLengthExpression.test(field)) { + sendContentLengthHeader = true; + + } + } + + // keep-alive logic + if (sentConnectionHeader == false) { + if (this.shouldKeepAlive && + (sendContentLengthHeader || this.useChunkedEncodingByDefault)) { + messageHeader += "Connection: keep-alive\r\n"; + } else { + this.closeOnFinish = true; + messageHeader += "Connection: close\r\n"; + } + } + + if (sendContentLengthHeader == false && sendTransferEncodingHeader == false) { + if (this.useChunkedEncodingByDefault) { + messageHeader += "Transfer-Encoding: chunked\r\n"; + this.chunkEncoding = true; + } + else { + this.closeOnFinish = true; + } + } + + messageHeader += CRLF; + + this.send(messageHeader); + // wait until the first body chunk, or finish(), is sent to flush. +}; + +OutgoingMessage.prototype.sendBody = function (chunk, encoding) { + encoding = encoding || "ascii"; + if (this.chunkEncoding) { + 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.chunkEncoding) this.send("0\r\n\r\n"); // last chunk + this.finished = true; + this.flush(); +}; + + +function ServerResponse (req) { + OutgoingMessage.call(this); + + if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { + this.useChunkedEncodingByDefault = false; + this.shouldKeepAlive = 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, url, headers) { + OutgoingMessage.call(this); + + this.shouldKeepAlive = false; + if (method === "GET" || method === "HEAD") { + this.useChunkedEncodingByDefault = false; + } else { + this.useChunkedEncodingByDefault = true; + } + this.closeOnFinish = true; + + this.sendHeaderLines(method + " " + url + " 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 (socket, cb) { + var incoming, field, value; + + socket._parser.onMessageBegin = function () { + incoming = new IncomingMessage(socket); + field = null; + value = null; + }; + + // Only servers will get URL events. + socket._parser.onURL = function (b, start, len) { + var slice = b.asciiSlice(start, start+len); + if (incoming.url) { + incoming.url += slice; + } else { + // Almost always will branch here. + incoming.url = slice; + } + }; + + socket._parser.onHeaderField = function (b, start, len) { + var slice = b.asciiSlice(start, start+len).toLowerCase(); + if (value) { + incoming._addHeaderLine(field, value); + field = null; + value = null; + } + if (field) { + field += slice; + } else { + field = slice; + } + }; + + socket._parser.onHeaderValue = function (b, start, len) { + var slice = b.asciiSlice(start, start+len); + if (value) { + value += slice; + } else { + value = slice; + } + }; + + socket._parser.onHeadersComplete = function (info) { + if (field && value) { + incoming._addHeaderLine(field, value); + } + + incoming.httpVersionMajor = info.versionMajor; + incoming.httpVersionMinor = info.versionMinor; + + if (info.method) { + // server only + incoming.method = info.method; + } else { + // client only + incoming.statusCode = info.statusCode; + } + + cb(incoming, info.shouldKeepAlive); + }; + + socket._parser.onBody = function (b, start, len) { + incoming.emit("data", b.slice(start, start+len)); + }; + + socket._parser.onMessageComplete = function () { + incoming.emit("eof"); + }; +} + + +/* Returns true if the message queue is finished and the socket + * should be closed. */ +function flushMessageQueue (socket, queue) { + while (queue[0]) { + var message = queue[0]; + + while (message.output.length > 0) { + if (!socket.writable) return true; + + var data = message.output.shift(); + var encoding = message.outputEncodings.shift(); + + socket.send(data, encoding); + } + + if (!message.finished) break; + + message.emit("sent"); + queue.shift(); + + if (message.closeOnFinish) return true; + } + return false; +} + + +function connectionListener (socket) { + var self = this; + if (socket._parser) throw new Error("socket already has a parser?"); + socket._parser = new process.HTTPParser('request'); + // An array of responses for each socket. In pipelined connections + // we need to keep track of the order they were sent. + var responses = []; + + socket.addListener('data', function (d) { + socket._parser.execute(d, 0, d.length); + }); + + // is this really needed? + socket.addListener('eof', function () { + socket._parser.finish(); + // unref the parser for easy gc + socket._parser.host = null; + socket._parser = null; + + if (responses.length == 0) { + socket.close(); + } else { + responses[responses.length-1].closeOnFinish = true; + } + }); + + createIncomingMessageStream(socket, function (incoming, shouldKeepAlive) { + var req = incoming; + + var res = new ServerResponse(req); + res.shouldKeepAlive = shouldKeepAlive; + res.addListener('flush', function () { + if (flushMessageQueue(socket, responses)) { + socket.close(); + } + }); + responses.push(res); + + self.emit('request', req, res); + }); +} + + +function Server (requestListener, options) { + net.Server.call(this, connectionListener); + //server.setOptions(options); + this.addListener('request', requestListener); +} +process.inherits(Server, net.Server); +exports.Server = Server; +exports.createServer = function (requestListener, options) { + return new Server(requestListener, options); +}; + +