http: improve http parser bindings
Speeds up HTTP benchmarks by 10% on average.
This commit is contained in:
parent
c83dda89a4
commit
84d0b1bcc5
100
lib/http.js
100
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');
|
||||
var parser = new HTTPParser(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._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;
|
||||
};
|
||||
|
||||
// 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) {
|
||||
if (parser.field && (parser.value != undefined)) {
|
||||
parser.incoming._addHeaderLine(parser.field, parser.value);
|
||||
parser.field = null;
|
||||
parser.value = null;
|
||||
var headers = info.headers;
|
||||
var url = info.url;
|
||||
|
||||
if (!headers) {
|
||||
headers = parser._headers;
|
||||
parser._headers = [];
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -51,10 +51,7 @@ namespace node {
|
||||
|
||||
using namespace v8;
|
||||
|
||||
static Persistent<String> on_message_begin_sym;
|
||||
static Persistent<String> on_url_sym;
|
||||
static Persistent<String> on_header_field_sym;
|
||||
static Persistent<String> on_header_value_sym;
|
||||
static Persistent<String> on_headers_sym;
|
||||
static Persistent<String> on_headers_complete_sym;
|
||||
static Persistent<String> on_body_sym;
|
||||
static Persistent<String> on_message_complete_sym;
|
||||
@ -92,6 +89,8 @@ static Persistent<String> version_major_sym;
|
||||
static Persistent<String> version_minor_sym;
|
||||
static Persistent<String> should_keep_alive_sym;
|
||||
static Persistent<String> upgrade_sym;
|
||||
static Persistent<String> headers_sym;
|
||||
static Persistent<String> 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<Parser*>(p->data); \
|
||||
Local<Value> cb_value = parser->handle_->Get(name##_sym); \
|
||||
if (!cb_value->IsFunction()) return 0; \
|
||||
Local<Function> cb = Local<Function>::Cast(cb_value); \
|
||||
Local<Value> 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<Parser*>(p->data); \
|
||||
assert(current_buffer); \
|
||||
Local<Value> cb_value = parser->handle_->Get(name##_sym); \
|
||||
if (!cb_value->IsFunction()) return 0; \
|
||||
Local<Function> cb = Local<Function>::Cast(cb_value); \
|
||||
Local<Value> argv[3] = { *current_buffer \
|
||||
, Integer::New(at - current_buffer_data) \
|
||||
, Integer::New(length) \
|
||||
}; \
|
||||
Local<Value> 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<String>
|
||||
@ -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<String> 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;
|
||||
}
|
||||
|
||||
static int on_headers_complete(http_parser *p) {
|
||||
Parser *parser = static_cast<Parser*>(p->data);
|
||||
|
||||
Local<Value> cb_value = parser->handle_->Get(on_headers_complete_sym);
|
||||
if (!cb_value->IsFunction()) return 0;
|
||||
Local<Function> cb = Local<Function>::Cast(cb_value);
|
||||
HTTP_DATA_CB(on_url) {
|
||||
url_.Update(at, length);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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<Value> cb = handle_->Get(on_headers_complete_sym);
|
||||
|
||||
if (!cb->IsFunction())
|
||||
return 0;
|
||||
|
||||
Local<Object> 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<Value> argv[1] = { message_info };
|
||||
|
||||
Local<Value> head_response = cb->Call(parser->handle_, 1, argv);
|
||||
Local<Value> head_response =
|
||||
Local<Function>::Cast(cb)->Call(handle_, 1, argv);
|
||||
|
||||
if (head_response.IsEmpty()) {
|
||||
parser->got_exception_ = true;
|
||||
got_exception_ = true;
|
||||
return -1;
|
||||
} else {
|
||||
return head_response->IsTrue() ? 1 : 0;
|
||||
}
|
||||
|
||||
return head_response->IsTrue() ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
HTTP_DATA_CB(on_body) {
|
||||
HandleScope scope;
|
||||
|
||||
Local<Value> cb = handle_->Get(on_body_sym);
|
||||
if (!cb->IsFunction())
|
||||
return 0;
|
||||
|
||||
Handle<Value> argv[3] = {
|
||||
*current_buffer,
|
||||
Integer::New(at - current_buffer_data),
|
||||
Integer::New(length)
|
||||
};
|
||||
|
||||
Local<Value> r = Local<Function>::Cast(cb)->Call(handle_, 3, argv);
|
||||
|
||||
if (r.IsEmpty()) {
|
||||
got_exception_ = true;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
HTTP_CB(on_message_complete) {
|
||||
HandleScope scope;
|
||||
|
||||
if (num_fields_ != -1)
|
||||
Flush(); // Flush trailing HTTP headers.
|
||||
|
||||
Local<Value> cb = handle_->Get(on_message_complete_sym);
|
||||
|
||||
if (!cb->IsFunction())
|
||||
return 0;
|
||||
|
||||
Local<Value> r = Local<Function>::Cast(cb)->Call(handle_, 0, NULL);
|
||||
|
||||
if (r.IsEmpty()) {
|
||||
got_exception_ = true;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static Handle<Value> New(const Arguments& args) {
|
||||
HandleScope scope;
|
||||
|
||||
String::Utf8Value type(args[0]->ToString());
|
||||
http_parser_type type =
|
||||
static_cast<http_parser_type>(args[0]->Int32Value());
|
||||
|
||||
Parser *parser;
|
||||
|
||||
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 (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<Value> Execute(const Arguments& args) {
|
||||
HandleScope scope;
|
||||
|
||||
Parser *parser = ObjectWrap::Unwrap<Parser>(args.This());
|
||||
Parser* parser = ObjectWrap::Unwrap<Parser>(args.This());
|
||||
|
||||
assert(!current_buffer);
|
||||
assert(!current_buffer_data);
|
||||
@ -321,10 +462,11 @@ class Parser : public ObjectWrap {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static Handle<Value> Finish(const Arguments& args) {
|
||||
HandleScope scope;
|
||||
|
||||
Parser *parser = ObjectWrap::Unwrap<Parser>(args.This());
|
||||
Parser* parser = ObjectWrap::Unwrap<Parser>(args.This());
|
||||
|
||||
assert(!current_buffer);
|
||||
parser->got_exception_ = false;
|
||||
@ -343,33 +485,83 @@ class Parser : public ObjectWrap {
|
||||
return Undefined();
|
||||
}
|
||||
|
||||
|
||||
static Handle<Value> Reinitialize(const Arguments& args) {
|
||||
HandleScope scope;
|
||||
Parser *parser = ObjectWrap::Unwrap<Parser>(args.This());
|
||||
|
||||
String::Utf8Value type(args[0]->ToString());
|
||||
http_parser_type type =
|
||||
static_cast<http_parser_type>(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<Parser>(args.This());
|
||||
parser->Init(type);
|
||||
|
||||
return Undefined();
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
private:
|
||||
|
||||
void Init (enum http_parser_type type) {
|
||||
http_parser_init(&parser_, type);
|
||||
parser_.data = this;
|
||||
Local<Array> CreateHeaders() {
|
||||
// num_values_ is either -1 or the entry # of the last header
|
||||
// so num_values_ == 0 means there's a single header
|
||||
Local<Array> 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;
|
||||
}
|
||||
|
||||
bool got_exception_;
|
||||
|
||||
// spill headers and request path to JS land
|
||||
void Flush() {
|
||||
HandleScope scope;
|
||||
|
||||
Local<Value> cb = handle_->Get(on_headers_sym);
|
||||
|
||||
if (!cb->IsFunction())
|
||||
return;
|
||||
|
||||
Handle<Value> argv[2] = {
|
||||
CreateHeaders(),
|
||||
url_.ToString()
|
||||
};
|
||||
|
||||
Local<Value> r = Local<Function>::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);
|
||||
url_.Reset();
|
||||
num_fields_ = -1;
|
||||
num_values_ = -1;
|
||||
have_flushed_ = false;
|
||||
got_exception_ = false;
|
||||
}
|
||||
|
||||
|
||||
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<Object> 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<Object> 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;
|
||||
|
@ -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);
|
||||
|
||||
var Buffer = require('buffer').Buffer;
|
||||
var buffer = new Buffer(1024);
|
||||
parser.headers = [];
|
||||
parser.url = '';
|
||||
|
||||
var request = 'GET /hello HTTP/1.1\r\n\r\n';
|
||||
parser.onHeaders = function(headers, url) {
|
||||
parser.headers = parser.headers.concat(headers);
|
||||
parser.url += url;
|
||||
};
|
||||
|
||||
buffer.write(request, 0, 'ascii');
|
||||
parser.onHeadersComplete = function(info) {
|
||||
};
|
||||
|
||||
var callbacks = 0;
|
||||
parser.onBody = function(b, start, len) {
|
||||
assert.ok(false, 'Function should not be called.');
|
||||
};
|
||||
|
||||
parser.onMessageBegin = function() {
|
||||
console.log('message begin');
|
||||
callbacks++;
|
||||
};
|
||||
parser.onMessageComplete = function() {
|
||||
};
|
||||
|
||||
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++;
|
||||
};
|
||||
return parser;
|
||||
}
|
||||
|
||||
parser.onURL = function(b, off, len) {
|
||||
//throw new Error('hello world');
|
||||
callbacks++;
|
||||
};
|
||||
|
||||
parser.execute(buffer, 0, request.length);
|
||||
assert.equal(3, callbacks);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Check that if we throw an error in the callbacks that error will be
|
||||
// thrown from parser.execute()
|
||||
// Simple request test.
|
||||
//
|
||||
(function() {
|
||||
var request = Buffer(
|
||||
'GET /hello HTTP/1.1' + CRLF +
|
||||
CRLF
|
||||
);
|
||||
|
||||
parser.onURL = function(b, off, len) {
|
||||
throw new Error('hello world');
|
||||
};
|
||||
var parser = newParser(REQUEST);
|
||||
|
||||
assert.throws(function() {
|
||||
parser.execute(buffer, 0, request.length);
|
||||
}, Error, 'hello world');
|
||||
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');
|
||||
})();
|
||||
|
||||
|
||||
//
|
||||
// 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);
|
||||
})();
|
||||
|
||||
|
||||
//
|
||||
// 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 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);
|
||||
// expect to see trailing headers now
|
||||
parser.onHeaders = mustCall(onHeaders);
|
||||
});
|
||||
|
||||
parser.onBody = mustCall(function(buf, start, len) {
|
||||
var body = '' + buf.slice(start, start + len);
|
||||
assert.equal(body, 'ping');
|
||||
seen_body = true;
|
||||
});
|
||||
|
||||
parser.execute(request, 0, request.length);
|
||||
})();
|
||||
|
||||
|
||||
//
|
||||
// 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']);
|
||||
};
|
||||
|
||||
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']);
|
||||
};
|
||||
|
||||
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);
|
||||
})();
|
Loading…
x
Reference in New Issue
Block a user