diff --git a/lib/http.js b/lib/http.js index dc83ec6925..829eb2798c 100644 --- a/lib/http.js +++ b/lib/http.js @@ -37,58 +37,51 @@ if (process.env.NODE_DEBUG && /http/.test(process.env.NODE_DEBUG)) { 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; - } + var parser = new HTTPParser(HTTPParser.REQUEST); + + parser._headers = []; + parser._url = ''; + + // Only called in the slow case where slow means + // that the request headers were either fragmented + // across multiple TCP packets or too large to be + // processed in a single run. This method is also + // called to process trailing HTTP headers. + parser.onHeaders = function(headers, url) { + parser._headers = parser._headers.concat(headers); + parser._url += url; }; - 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; - } - }; + // info.headers and info.url are set only if .onHeaders() + // has not been called for this request. + // + // info.url is not set for response parsers but that's not + // applicable here since all our parsers are request parsers. + parser.onHeadersComplete = function(info) { + var headers = info.headers; + var url = info.url; - parser.onHeaderValue = function(b, start, len) { - var slice = b.toString('ascii', start, start + len); - if (parser.value) { - parser.value += slice; - } else { - parser.value = slice; + if (!headers) { + headers = parser._headers; + parser._headers = []; } - }; - parser.onHeadersComplete = function(info) { - if (parser.field && (parser.value != undefined)) { - parser.incoming._addHeaderLine(parser.field, parser.value); - parser.field = null; - parser.value = null; + if (!url) { + url = parser._url; + parser._url = ''; } + parser.incoming = new IncomingMessage(parser.socket); parser.incoming.httpVersionMajor = info.versionMajor; parser.incoming.httpVersionMinor = info.versionMinor; parser.incoming.httpVersion = info.versionMajor + '.' + info.versionMinor; + parser.incoming.url = url; + + for (var i = 0, n = headers.length; i < n; i += 2) { + var k = headers[i]; + var v = headers[i + 1]; + parser.incoming._addHeaderLine(k.toLowerCase(), v); + } if (info.method) { // server only @@ -96,6 +89,7 @@ var parsers = new FreeList('parsers', 1000, function() { } else { // client only parser.incoming.statusCode = info.statusCode; + // CHECKME dead code? we're always a request parser } parser.incoming.upgrade = info.upgrade; @@ -123,10 +117,20 @@ var parsers = new FreeList('parsers', 1000, function() { }; parser.onMessageComplete = function() { - this.incoming.complete = true; - if (parser.field && (parser.value != undefined)) { - parser.incoming._addHeaderLine(parser.field, parser.value); + parser.incoming.complete = true; + + // Emit any trailing headers. + var headers = parser._headers; + if (headers) { + for (var i = 0, n = headers.length; i < n; i += 2) { + var k = headers[i]; + var v = headers[i + 1]; + parser.incoming._addHeaderLine(k.toLowerCase(), v); + } + parser._headers = []; + parser._url = ''; } + if (!parser.incoming.upgrade) { // For upgraded connections, also emit this after parser.execute parser.incoming.readable = false; @@ -1088,7 +1092,7 @@ ClientRequest.prototype.onSocket = function(socket) { var parser = parsers.alloc(); req.socket = socket; req.connection = socket; - parser.reinitialize('response'); + parser.reinitialize(HTTPParser.RESPONSE); parser.socket = socket; parser.incoming = null; req.parser = parser; @@ -1346,7 +1350,7 @@ function connectionListener(socket) { }); var parser = parsers.alloc(); - parser.reinitialize('request'); + parser.reinitialize(HTTPParser.REQUEST); parser.socket = socket; parser.incoming = null; diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index 8922c9ed1a..1cc2029f9a 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -51,10 +51,7 @@ namespace node { using namespace v8; -static Persistent on_message_begin_sym; -static Persistent on_url_sym; -static Persistent on_header_field_sym; -static Persistent on_header_value_sym; +static Persistent on_headers_sym; static Persistent on_headers_complete_sym; static Persistent on_body_sym; static Persistent on_message_complete_sym; @@ -92,6 +89,8 @@ static Persistent version_major_sym; static Persistent version_minor_sym; static Persistent should_keep_alive_sym; static Persistent upgrade_sym; +static Persistent headers_sym; +static Persistent url_sym; static struct http_parser_settings settings; @@ -104,43 +103,29 @@ static char* current_buffer_data; static size_t current_buffer_len; -// Callback prototype for http_cb -#define DEFINE_HTTP_CB(name) \ - static int name(http_parser *p) { \ - Parser *parser = static_cast(p->data); \ - Local cb_value = parser->handle_->Get(name##_sym); \ - if (!cb_value->IsFunction()) return 0; \ - Local cb = Local::Cast(cb_value); \ - Local ret = cb->Call(parser->handle_, 0, NULL); \ - if (ret.IsEmpty()) { \ - parser->got_exception_ = true; \ - return -1; \ - } else { \ - return 0; \ - } \ - } +#if defined(__GNUC__) +#define always_inline __attribute__((always_inline)) +#elif defined(_MSC_VER) +#define always_inline __forceinline +#else +#define always_inline +#endif -// Callback prototype for http_data_cb -#define DEFINE_HTTP_DATA_CB(name) \ - static int name(http_parser *p, const char *at, size_t length) { \ - Parser *parser = static_cast(p->data); \ - assert(current_buffer); \ - Local cb_value = parser->handle_->Get(name##_sym); \ - if (!cb_value->IsFunction()) return 0; \ - Local cb = Local::Cast(cb_value); \ - Local argv[3] = { *current_buffer \ - , Integer::New(at - current_buffer_data) \ - , Integer::New(length) \ - }; \ - Local ret = cb->Call(parser->handle_, 3, argv); \ - assert(current_buffer); \ - if (ret.IsEmpty()) { \ - parser->got_exception_ = true; \ - return -1; \ - } else { \ - return 0; \ - } \ - } + +#define HTTP_CB(name) \ + static int name(http_parser* p_) { \ + Parser* self = container_of(p_, Parser, parser_); \ + return self->name##_(); \ + } \ + int always_inline name##_() + + +#define HTTP_DATA_CB(name) \ + static int name(http_parser* p_, const char* at, size_t length) { \ + Parser* self = container_of(p_, Parser, parser_); \ + return self->name##_(at, length); \ + } \ + int always_inline name##_(const char* at, size_t length) static inline Persistent @@ -175,90 +160,246 @@ method_to_str(unsigned short m) { } +// helper class for the Parser +struct StringPtr { + StringPtr() { + on_heap_ = false; + Reset(); + } + + + ~StringPtr() { + Reset(); + } + + + void Reset() { + if (on_heap_) { + delete[] str_; + on_heap_ = false; + } + + str_ = NULL; + size_ = 0; + } + + + void Update(const char* str, size_t size) { + if (str_ == NULL) + str_ = str; + else if (on_heap_ || str_ + size != str) { + // Non-consecutive input, make a copy on the heap. + // TODO Use slab allocation, O(n) allocs is bad. + char* s = new char[size_ + size]; + memcpy(s, str_, size_); + memcpy(s + size_, str, size); + + if (on_heap_) + delete[] str_; + else + on_heap_ = true; + + str_ = s; + } + size_ += size; + } + + + Handle ToString() const { + if (str_) + return String::New(str_, size_); + else + return String::Empty(); + } + + + const char* str_; + bool on_heap_; + size_t size_; +}; + + class Parser : public ObjectWrap { - public: +public: Parser(enum http_parser_type type) : ObjectWrap() { Init(type); } + ~Parser() { } - DEFINE_HTTP_CB(on_message_begin) - DEFINE_HTTP_CB(on_message_complete) - DEFINE_HTTP_DATA_CB(on_url) - DEFINE_HTTP_DATA_CB(on_header_field) - DEFINE_HTTP_DATA_CB(on_header_value) - DEFINE_HTTP_DATA_CB(on_body) + HTTP_CB(on_message_begin) { + num_fields_ = num_values_ = -1; + url_.Reset(); + return 0; + } + + + HTTP_DATA_CB(on_url) { + url_.Update(at, length); + return 0; + } + - static int on_headers_complete(http_parser *p) { - Parser *parser = static_cast(p->data); + HTTP_DATA_CB(on_header_field) { + if (num_fields_ == num_values_) { + // start of new field name + if (++num_fields_ == ARRAY_SIZE(fields_)) { + Flush(); + num_fields_ = 0; + num_values_ = -1; + } + fields_[num_fields_].Reset(); + } - Local cb_value = parser->handle_->Get(on_headers_complete_sym); - if (!cb_value->IsFunction()) return 0; - Local cb = Local::Cast(cb_value); + assert(num_fields_ < (int)ARRAY_SIZE(fields_)); + assert(num_fields_ == num_values_ + 1); + fields_[num_fields_].Update(at, length); + + return 0; + } + + + HTTP_DATA_CB(on_header_value) { + if (num_values_ != num_fields_) { + // start of new header value + values_[++num_values_].Reset(); + } + + assert(num_values_ < (int)ARRAY_SIZE(values_)); + assert(num_values_ == num_fields_); + + values_[num_values_].Update(at, length); + + return 0; + } + + + HTTP_CB(on_headers_complete) { + Local cb = handle_->Get(on_headers_complete_sym); + + if (!cb->IsFunction()) + return 0; Local message_info = Object::New(); + if (have_flushed_) { + // Slow case, flush remaining headers. + Flush(); + } + else { + // Fast case, pass headers and URL to JS land. + message_info->Set(headers_sym, CreateHeaders()); + if (parser_.type == HTTP_REQUEST) + message_info->Set(url_sym, url_.ToString()); + } + num_fields_ = num_values_ = -1; + // METHOD - if (p->type == HTTP_REQUEST) { - message_info->Set(method_sym, method_to_str(p->method)); + if (parser_.type == HTTP_REQUEST) { + message_info->Set(method_sym, method_to_str(parser_.method)); } // STATUS - if (p->type == HTTP_RESPONSE) { - message_info->Set(status_code_sym, Integer::New(p->status_code)); + if (parser_.type == HTTP_RESPONSE) { + message_info->Set(status_code_sym, Integer::New(parser_.status_code)); } // VERSION - message_info->Set(version_major_sym, Integer::New(p->http_major)); - message_info->Set(version_minor_sym, Integer::New(p->http_minor)); + message_info->Set(version_major_sym, Integer::New(parser_.http_major)); + message_info->Set(version_minor_sym, Integer::New(parser_.http_minor)); message_info->Set(should_keep_alive_sym, - http_should_keep_alive(p) ? True() : False()); + http_should_keep_alive(&parser_) ? True() : False()); - message_info->Set(upgrade_sym, p->upgrade ? True() : False()); + message_info->Set(upgrade_sym, parser_.upgrade ? True() : False()); Local argv[1] = { message_info }; - Local head_response = cb->Call(parser->handle_, 1, argv); + Local head_response = + Local::Cast(cb)->Call(handle_, 1, argv); if (head_response.IsEmpty()) { - parser->got_exception_ = true; + got_exception_ = true; + return -1; + } + + return head_response->IsTrue() ? 1 : 0; + } + + + HTTP_DATA_CB(on_body) { + HandleScope scope; + + Local cb = handle_->Get(on_body_sym); + if (!cb->IsFunction()) + return 0; + + Handle argv[3] = { + *current_buffer, + Integer::New(at - current_buffer_data), + Integer::New(length) + }; + + Local r = Local::Cast(cb)->Call(handle_, 3, argv); + + if (r.IsEmpty()) { + got_exception_ = true; return -1; - } else { - return head_response->IsTrue() ? 1 : 0; } + + return 0; } - static Handle New(const Arguments& args) { + + HTTP_CB(on_message_complete) { HandleScope scope; - String::Utf8Value type(args[0]->ToString()); + if (num_fields_ != -1) + Flush(); // Flush trailing HTTP headers. - Parser *parser; + Local cb = handle_->Get(on_message_complete_sym); - if (0 == strcasecmp(*type, "request")) { - parser = new Parser(HTTP_REQUEST); - } else if (0 == strcasecmp(*type, "response")) { - parser = new Parser(HTTP_RESPONSE); - } else { - return ThrowException(Exception::Error( - String::New("Constructor argument be 'request' or 'response'"))); + if (!cb->IsFunction()) + return 0; + + Local r = Local::Cast(cb)->Call(handle_, 0, NULL); + + if (r.IsEmpty()) { + got_exception_ = true; + return -1; + } + + return 0; + } + + + static Handle New(const Arguments& args) { + HandleScope scope; + + http_parser_type type = + static_cast(args[0]->Int32Value()); + + if (type != HTTP_REQUEST && type != HTTP_RESPONSE) { + return ThrowException(Exception::Error(String::New( + "Argument must be HTTPParser.REQUEST or HTTPParser.RESPONSE"))); } + Parser* parser = new Parser(type); parser->Wrap(args.This()); return args.This(); } + // var bytesParsed = parser->execute(buffer, off, len); static Handle Execute(const Arguments& args) { HandleScope scope; - Parser *parser = ObjectWrap::Unwrap(args.This()); + Parser* parser = ObjectWrap::Unwrap(args.This()); assert(!current_buffer); assert(!current_buffer_data); @@ -321,10 +462,11 @@ class Parser : public ObjectWrap { } } + static Handle Finish(const Arguments& args) { HandleScope scope; - Parser *parser = ObjectWrap::Unwrap(args.This()); + Parser* parser = ObjectWrap::Unwrap(args.This()); assert(!current_buffer); parser->got_exception_ = false; @@ -343,33 +485,83 @@ class Parser : public ObjectWrap { return Undefined(); } + static Handle Reinitialize(const Arguments& args) { HandleScope scope; - Parser *parser = ObjectWrap::Unwrap(args.This()); - String::Utf8Value type(args[0]->ToString()); + http_parser_type type = + static_cast(args[0]->Int32Value()); - if (0 == strcasecmp(*type, "request")) { - parser->Init(HTTP_REQUEST); - } else if (0 == strcasecmp(*type, "response")) { - parser->Init(HTTP_RESPONSE); - } else { - return ThrowException(Exception::Error( - String::New("Argument be 'request' or 'response'"))); + if (type != HTTP_REQUEST && type != HTTP_RESPONSE) { + return ThrowException(Exception::Error(String::New( + "Argument must be HTTPParser.REQUEST or HTTPParser.RESPONSE"))); } + + Parser* parser = ObjectWrap::Unwrap(args.This()); + parser->Init(type); + return Undefined(); } - private: +private: - void Init (enum http_parser_type type) { + Local CreateHeaders() { + // num_values_ is either -1 or the entry # of the last header + // so num_values_ == 0 means there's a single header + Local headers = Array::New(2 * (num_values_ + 1)); + + for (int i = 0; i < num_values_ + 1; ++i) { + headers->Set(2 * i, fields_[i].ToString()); + headers->Set(2 * i + 1, values_[i].ToString()); + } + + return headers; + } + + + // spill headers and request path to JS land + void Flush() { + HandleScope scope; + + Local cb = handle_->Get(on_headers_sym); + + if (!cb->IsFunction()) + return; + + Handle argv[2] = { + CreateHeaders(), + url_.ToString() + }; + + Local r = Local::Cast(cb)->Call(handle_, 2, argv); + + if (r.IsEmpty()) + got_exception_ = true; + + url_.Reset(); + have_flushed_ = true; + } + + + void Init(enum http_parser_type type) { http_parser_init(&parser_, type); - parser_.data = this; + url_.Reset(); + num_fields_ = -1; + num_values_ = -1; + have_flushed_ = false; + got_exception_ = false; } - bool got_exception_; + http_parser parser_; + StringPtr fields_[32]; // header fields + StringPtr values_[32]; // header values + StringPtr url_; + int num_fields_; + int num_values_; + bool have_flushed_; + bool got_exception_; }; @@ -380,16 +572,17 @@ void InitHttpParser(Handle target) { t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(String::NewSymbol("HTTPParser")); + PropertyAttribute attrib = (PropertyAttribute) (ReadOnly | DontDelete); + t->Set(String::NewSymbol("REQUEST"), Integer::New(HTTP_REQUEST), attrib); + t->Set(String::NewSymbol("RESPONSE"), Integer::New(HTTP_RESPONSE), attrib); + NODE_SET_PROTOTYPE_METHOD(t, "execute", Parser::Execute); NODE_SET_PROTOTYPE_METHOD(t, "finish", Parser::Finish); NODE_SET_PROTOTYPE_METHOD(t, "reinitialize", Parser::Reinitialize); target->Set(String::NewSymbol("HTTPParser"), t->GetFunction()); - on_message_begin_sym = NODE_PSYMBOL("onMessageBegin"); - on_url_sym = NODE_PSYMBOL("onURL"); - on_header_field_sym = NODE_PSYMBOL("onHeaderField"); - on_header_value_sym = NODE_PSYMBOL("onHeaderValue"); + on_headers_sym = NODE_PSYMBOL("onHeaders"); on_headers_complete_sym = NODE_PSYMBOL("onHeadersComplete"); on_body_sym = NODE_PSYMBOL("onBody"); on_message_complete_sym = NODE_PSYMBOL("onMessageComplete"); @@ -427,6 +620,8 @@ void InitHttpParser(Handle target) { version_minor_sym = NODE_PSYMBOL("versionMinor"); should_keep_alive_sym = NODE_PSYMBOL("shouldKeepAlive"); upgrade_sym = NODE_PSYMBOL("upgrade"); + headers_sym = NODE_PSYMBOL("headers"); + url_sym = NODE_PSYMBOL("url"); settings.on_message_begin = Parser::on_message_begin; settings.on_url = Parser::on_url; diff --git a/test/simple/test-http-parser.js b/test/simple/test-http-parser.js index c85be88a74..df880f50ac 100644 --- a/test/simple/test-http-parser.js +++ b/test/simple/test-http-parser.js @@ -22,55 +22,513 @@ var common = require('../common'); var assert = require('assert'); +var HTTPParser = process.binding('http_parser').HTTPParser; + +var CRLF = "\r\n"; +var REQUEST = HTTPParser.REQUEST; +var RESPONSE = HTTPParser.RESPONSE; + // The purpose of this test is not to check HTTP compliance but to test the // binding. Tests for pathological http messages should be submitted // upstream to http://github.com/ry/http-parser for inclusion into // deps/http-parser/test.c -var HTTPParser = process.binding('http_parser').HTTPParser; -var parser = new HTTPParser('request'); +function newParser(type) { + var parser = new HTTPParser(type); + + parser.headers = []; + parser.url = ''; + + parser.onHeaders = function(headers, url) { + parser.headers = parser.headers.concat(headers); + parser.url += url; + }; + + parser.onHeadersComplete = function(info) { + }; + + parser.onBody = function(b, start, len) { + assert.ok(false, 'Function should not be called.'); + }; + + parser.onMessageComplete = function() { + }; + + return parser; +} + + +function mustCall(f, times) { + var actual = 0; + + process.setMaxListeners(256); + process.on('exit', function() { + assert.equal(actual, times || 1); + }); + + return function() { + actual++; + return f.apply(this, Array.prototype.slice.call(arguments)); + }; +} + + +function expectBody(expected) { + return mustCall(function(buf, start, len) { + var body = '' + buf.slice(start, start + len); + assert.equal(body, expected); + }); +} + + +// +// Simple request test. +// +(function() { + var request = Buffer( + 'GET /hello HTTP/1.1' + CRLF + + CRLF + ); + + var parser = newParser(REQUEST); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'GET'); + assert.equal(info.url || parser.url, '/hello'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + }); + + parser.execute(request, 0, request.length); + + // + // Check that if we throw an error in the callbacks that error will be + // thrown from parser.execute() + // + + parser.onHeadersComplete = function(info) { + throw new Error('hello world'); + }; + + parser.reinitialize(HTTPParser.REQUEST); + + assert.throws(function() { + parser.execute(request, 0, request.length); + }, Error, 'hello world'); +})(); -var Buffer = require('buffer').Buffer; -var buffer = new Buffer(1024); -var request = 'GET /hello HTTP/1.1\r\n\r\n'; +// +// Simple response test. +// +(function() { + var request = Buffer( + 'HTTP/1.1 200 OK' + CRLF + + 'Content-Type: text/plain' + CRLF + + 'Content-Length: 4' + CRLF + + CRLF + + 'pong' + ); + + var parser = newParser(RESPONSE); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, undefined); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + assert.equal(info.statusCode, 200); + }); + + parser.onBody = mustCall(function(buf, start, len) { + var body = '' + buf.slice(start, start + len); + assert.equal(body, 'pong'); + }); + + parser.execute(request, 0, request.length); +})(); + -buffer.write(request, 0, 'ascii'); +// +// Trailing headers. +// +(function() { + var request = Buffer( + 'POST /it HTTP/1.1' + CRLF + + 'Transfer-Encoding: chunked' + CRLF + + CRLF + + '4' + CRLF + + 'ping' + CRLF + + '0' + CRLF + + 'Vary: *' + CRLF + + 'Content-Type: text/plain' + CRLF + + CRLF + ); + + var seen_body = false; + + function onHeaders(headers, url) { + assert.ok(seen_body); // trailers should come after the body + assert.deepEqual(headers, + ['Vary', '*', 'Content-Type', 'text/plain']); + } -var callbacks = 0; + var parser = newParser(REQUEST); -parser.onMessageBegin = function() { - console.log('message begin'); - callbacks++; -}; + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'POST'); + assert.equal(info.url || parser.url, '/it'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + // expect to see trailing headers now + parser.onHeaders = mustCall(onHeaders); + }); -parser.onHeadersComplete = function(info) { - console.log('headers complete: ' + JSON.stringify(info)); - assert.equal('GET', info.method); - assert.equal(1, info.versionMajor); - assert.equal(1, info.versionMinor); - callbacks++; -}; + parser.onBody = mustCall(function(buf, start, len) { + var body = '' + buf.slice(start, start + len); + assert.equal(body, 'ping'); + seen_body = true; + }); -parser.onURL = function(b, off, len) { - //throw new Error('hello world'); - callbacks++; -}; + parser.execute(request, 0, request.length); +})(); -parser.execute(buffer, 0, request.length); -assert.equal(3, callbacks); // -// Check that if we throw an error in the callbacks that error will be -// thrown from parser.execute() +// Test header ordering. +// +(function() { + var request = Buffer( + 'GET / HTTP/1.0' + CRLF + + 'X-Filler: 1337' + CRLF + + 'X-Filler: 42' + CRLF + + 'X-Filler2: 42' + CRLF + + CRLF + ); + + var parser = newParser(REQUEST); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'GET'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 0); + assert.deepEqual(info.headers || parser.headers, + ['X-Filler', '1337', + 'X-Filler', '42', + 'X-Filler2', '42']); + }); + + parser.execute(request, 0, request.length); +})(); + + +// +// Test large number of headers +// +(function() { + // 256 X-Filler headers + var lots_of_headers = 'X-Filler: 42' + CRLF; + for (var i = 0; i < 8; ++i) lots_of_headers += lots_of_headers; + + var request = Buffer( + 'GET /foo/bar/baz?quux=42#1337 HTTP/1.0' + CRLF + + lots_of_headers + + CRLF + ); + + var parser = newParser(REQUEST); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'GET'); + assert.equal(info.url || parser.url, '/foo/bar/baz?quux=42#1337'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 0); + + var headers = info.headers || parser.headers; + + assert.equal(headers.length, 2 * 256); // 256 key/value pairs + for (var i = 0; i < headers.length; i += 2) { + assert.equal(headers[i], 'X-Filler'); + assert.equal(headers[i + 1], '42'); + } + }); + + parser.execute(request, 0, request.length); +})(); + + // +// Test request body +// +(function() { + var request = Buffer( + 'POST /it HTTP/1.1' + CRLF + + 'Content-Type: application/x-www-form-urlencoded' + CRLF + + 'Content-Length: 15' + CRLF + + CRLF + + 'foo=42&bar=1337' + ); + + var parser = newParser(REQUEST); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'POST'); + assert.equal(info.url || parser.url, '/it'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + }); + + parser.onBody = mustCall(function(buf, start, len) { + var body = '' + buf.slice(start, start + len); + assert.equal(body, 'foo=42&bar=1337'); + }); + + parser.execute(request, 0, request.length); +})(); + + +// +// Test chunked request body +// +(function() { + var request = Buffer( + 'POST /it HTTP/1.1' + CRLF + + 'Content-Type: text/plain' + CRLF + + 'Transfer-Encoding: chunked' + CRLF + + CRLF + + '3' + CRLF + + '123' + CRLF + + '6' + CRLF + + '123456' + CRLF + + 'A' + CRLF + + '1234567890' + CRLF + + '0' + CRLF + ); + + var parser = newParser(REQUEST); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'POST'); + assert.equal(info.url || parser.url, '/it'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + }); + + var body_part = 0, body_parts = ['123', '123456', '1234567890']; + + function onBody(buf, start, len) { + var body = '' + buf.slice(start, start + len); + assert.equal(body, body_parts[body_part++]); + } + + parser.onBody = mustCall(onBody, body_parts.length); + parser.execute(request, 0, request.length); +})(); + + +// +// Test chunked request body spread over multiple buffers (packets) +// +(function() { + var request = Buffer( + 'POST /it HTTP/1.1' + CRLF + + 'Content-Type: text/plain' + CRLF + + 'Transfer-Encoding: chunked' + CRLF + + CRLF + + '3' + CRLF + + '123' + CRLF + + '6' + CRLF + + '123456' + CRLF + ); + + var parser = newParser(REQUEST); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'POST'); + assert.equal(info.url || parser.url, '/it'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + }); + + var body_part = 0, body_parts = [ + '123', '123456', '123456789', + '123456789ABC', '123456789ABCDEF' ]; + + function onBody(buf, start, len) { + var body = '' + buf.slice(start, start + len); + assert.equal(body, body_parts[body_part++]); + } + + parser.onBody = mustCall(onBody, body_parts.length); + parser.execute(request, 0, request.length); + + request = Buffer( + '9' + CRLF + + '123456789' + CRLF + + 'C' + CRLF + + '123456789ABC' + CRLF + + 'F' + CRLF + + '123456789ABCDEF' + CRLF + + '0' + CRLF + ); + + parser.execute(request, 0, request.length); +})(); + + +// +// Stress test. +// +(function() { + var request = Buffer( + 'POST /it HTTP/1.1' + CRLF + + 'Content-Type: text/plain' + CRLF + + 'Transfer-Encoding: chunked' + CRLF + + CRLF + + '3' + CRLF + + '123' + CRLF + + '6' + CRLF + + '123456' + CRLF + + '9' + CRLF + + '123456789' + CRLF + + 'C' + CRLF + + '123456789ABC' + CRLF + + 'F' + CRLF + + '123456789ABCDEF' + CRLF + + '0' + CRLF + ); + + function test(a, b) { + var parser = newParser(REQUEST); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'POST'); + assert.equal(info.url || parser.url, '/it'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + }); + + var expected_body = '123123456123456789123456789ABC123456789ABCDEF'; + + parser.onBody = function(buf, start, len) { + var chunk = '' + buf.slice(start, start + len); + assert.equal(expected_body.indexOf(chunk), 0); + expected_body = expected_body.slice(chunk.length); + }; + + parser.execute(a, 0, a.length); + parser.execute(b, 0, b.length); + + assert.equal(expected_body, ''); + } + + for (var i = 1; i < request.length - 1; ++i) { + var a = request.slice(0, i); + var b = request.slice(i); + test(a, b); + } +})(); + + +// +// Byte by byte test. +// +(function() { + var request = Buffer( + 'POST /it HTTP/1.1' + CRLF + + 'Content-Type: text/plain' + CRLF + + 'Transfer-Encoding: chunked' + CRLF + + CRLF + + '3' + CRLF + + '123' + CRLF + + '6' + CRLF + + '123456' + CRLF + + '9' + CRLF + + '123456789' + CRLF + + 'C' + CRLF + + '123456789ABC' + CRLF + + 'F' + CRLF + + '123456789ABCDEF' + CRLF + + '0' + CRLF + ); + + var parser = newParser(REQUEST); + + parser.onHeadersComplete = mustCall(function(info) { + assert.equal(info.method, 'POST'); + assert.equal(info.url || parser.url, '/it'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + assert.deepEqual(info.headers || parser.headers, + ['Content-Type', 'text/plain', + 'Transfer-Encoding','chunked']); + }); + + var expected_body = '123123456123456789123456789ABC123456789ABCDEF'; + + parser.onBody = function(buf, start, len) { + var chunk = '' + buf.slice(start, start + len); + assert.equal(expected_body.indexOf(chunk), 0); + expected_body = expected_body.slice(chunk.length); + }; + + for (var i = 0; i < request.length; ++i) { + parser.execute(request, i, 1); + } + + assert.equal(expected_body, ''); +})(); + + +// +// +// +(function() { + var req1 = Buffer( + 'PUT /this HTTP/1.1' + CRLF + + 'Content-Type: text/plain' + CRLF + + 'Transfer-Encoding: chunked' + CRLF + + CRLF + + '4' + CRLF + + 'ping' + CRLF + + '0' + CRLF + ); + + var req2 = Buffer( + 'POST /that HTTP/1.0' + CRLF + + 'Content-Type: text/plain' + CRLF + + 'Content-Length: 4' + CRLF + + CRLF + + 'pong' + ); + + function onHeadersComplete1(info) { + assert.equal(info.method, 'PUT'); + assert.equal(info.url, '/this'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 1); + assert.deepEqual(info.headers, + ['Content-Type', 'text/plain', + 'Transfer-Encoding', 'chunked']); + }; -parser.onURL = function(b, off, len) { - throw new Error('hello world'); -}; + function onHeadersComplete2(info) { + assert.equal(info.method, 'POST'); + assert.equal(info.url, '/that'); + assert.equal(info.versionMajor, 1); + assert.equal(info.versionMinor, 0); + assert.deepEqual(info.headers, + ['Content-Type', 'text/plain', + 'Content-Length', '4']); + }; -assert.throws(function() { - parser.execute(buffer, 0, request.length); -}, Error, 'hello world'); + var parser = newParser(REQUEST); + parser.onHeadersComplete = onHeadersComplete1; + parser.onBody = expectBody('ping'); + parser.execute(req1, 0, req1.length); + parser.reinitialize(REQUEST); + parser.onBody = expectBody('pong'); + parser.onHeadersComplete = onHeadersComplete2; + parser.execute(req2, 0, req2.length); +})(); \ No newline at end of file