From 42ee16978e81a0f1fba0768e163b5e9584178fa3 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Sun, 24 Jan 2010 11:21:45 -0800 Subject: [PATCH] Implement new http-parser binding using Buffer --- src/node.cc | 2 + src/node_http_parser.cc | 324 +++++++++++++++++++++++++++++++ src/node_http_parser.h | 12 ++ test/mjsunit/test-http-parser.js | 60 ++++++ wscript | 1 + 5 files changed, 399 insertions(+) create mode 100644 src/node_http_parser.cc create mode 100644 src/node_http_parser.h create mode 100644 test/mjsunit/test-http-parser.js diff --git a/src/node.cc b/src/node.cc index ea7a8c0f88..6c4fd51fdd 100644 --- a/src/node.cc +++ b/src/node.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -996,6 +997,7 @@ static Local Load(int argc, char *argv[]) { SignalHandler::Initialize(process); // signal_handler.cc InitNet2(process); // net2.cc + InitHttpParser(process); // http_parser.cc Stdio::Initialize(process); // stdio.cc ChildProcess::Initialize(process); // child_process.cc diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc new file mode 100644 index 0000000000..f90eb3c2c7 --- /dev/null +++ b/src/node_http_parser.cc @@ -0,0 +1,324 @@ +#include + +#include +#include +#include + +#include + +#include /* strcasecmp() */ + +// This is a binding to http_parser (http://github.com/ry/http-parser) +// The goal is to decouple sockets from parsing for more javascript-level +// agility. A Buffer is read from a socket and passed to parser.execute(). +// The parser then issues callbacks with slices of the data +// parser.onMessageBegin +// parser.onPath +// parser.onBody +// ... +// No copying is performed when slicing the buffer, only small reference +// allocations. + +namespace node { + +using namespace v8; + +static Persistent on_message_begin_sym; +static Persistent on_path_sym; +static Persistent on_query_string_sym; +static Persistent on_url_sym; +static Persistent on_fragment_sym; +static Persistent on_header_field_sym; +static Persistent on_header_value_sym; +static Persistent on_headers_complete_sym; +static Persistent on_body_sym; +static Persistent on_message_complete_sym; + +static Persistent delete_sym; +static Persistent get_sym; +static Persistent head_sym; +static Persistent post_sym; +static Persistent put_sym; +static Persistent connect_sym; +static Persistent options_sym; +static Persistent trace_sym; +static Persistent copy_sym; +static Persistent lock_sym; +static Persistent mkcol_sym; +static Persistent move_sym; +static Persistent propfind_sym; +static Persistent proppatch_sym; +static Persistent unlock_sym; +static Persistent unknown_method_sym; + +static Persistent method_sym; +static Persistent status_code_sym; +static Persistent http_version_sym; +static Persistent version_major_sym; +static Persistent version_minor_sym; +static Persistent should_keep_alive_sym; + +// Callback prototype for http_cb +#define DEFINE_HTTP_CB(name) \ + static int name(http_parser *p) { \ + Parser *parser = static_cast(p->data); \ + \ + HandleScope scope; \ + \ + 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); \ + return ret.IsEmpty() ? -1 : 0; \ + } + +// 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); \ + \ + HandleScope scope; \ + \ + assert(parser->buffer_); \ + char * base = buffer_p(parser->buffer_, 0); \ + \ + Local cb_value = parser->handle_->Get(name##_sym); \ + if (!cb_value->IsFunction()) return 0; \ + Local cb = Local::Cast(cb_value); \ + \ + Local off = Integer::New(at - base); \ + Local len = Integer::New(length); \ + Local argv[2] = { off, len }; \ + \ + Local ret = cb->Call(parser->handle_, 2, argv); \ + return ret.IsEmpty() ? -1 : 0; \ + } + + +static inline Persistent +method_to_str(enum http_method m) { + switch (m) { + case HTTP_DELETE: return delete_sym; + case HTTP_GET: return get_sym; + case HTTP_HEAD: return head_sym; + case HTTP_POST: return post_sym; + case HTTP_PUT: return put_sym; + case HTTP_CONNECT: return connect_sym; + case HTTP_OPTIONS: return options_sym; + case HTTP_TRACE: return trace_sym; + case HTTP_COPY: return copy_sym; + case HTTP_LOCK: return lock_sym; + case HTTP_MKCOL: return mkcol_sym; + case HTTP_MOVE: return move_sym; + case HTTP_PROPFIND: return propfind_sym; + case HTTP_PROPPATCH: return proppatch_sym; + case HTTP_UNLOCK: return unlock_sym; + default: return unknown_method_sym; + } +} + + +class Parser : public ObjectWrap { + public: + Parser(enum http_parser_type type) : ObjectWrap() { + buffer_ = NULL; + + http_parser_init(&parser_, type); + + parser_.on_message_begin = on_message_begin; + parser_.on_path = on_path; + parser_.on_query_string = on_query_string; + parser_.on_url = on_url; + parser_.on_fragment = on_fragment; + parser_.on_header_field = on_header_field; + parser_.on_header_value = on_header_value; + parser_.on_headers_complete = on_headers_complete; + parser_.on_body = on_body; + parser_.on_message_complete = on_message_complete; + + parser_.data = this; + } + + DEFINE_HTTP_CB(on_message_begin) + DEFINE_HTTP_CB(on_message_complete) + + DEFINE_HTTP_DATA_CB(on_path) + DEFINE_HTTP_DATA_CB(on_url) + DEFINE_HTTP_DATA_CB(on_fragment) + DEFINE_HTTP_DATA_CB(on_query_string) + DEFINE_HTTP_DATA_CB(on_header_field) + DEFINE_HTTP_DATA_CB(on_header_value) + DEFINE_HTTP_DATA_CB(on_body) + + static int on_headers_complete(http_parser *p) { + Parser *parser = static_cast(p->data); + + HandleScope scope; + + Local cb_value = parser->handle_->Get(on_headers_complete_sym); + if (!cb_value->IsFunction()) return 0; + Local cb = Local::Cast(cb_value); + + + Local message_info = Object::New(); + + // METHOD + if (p->type == HTTP_REQUEST) { + message_info->Set(method_sym, method_to_str(p->method)); + } + + // STATUS + if (p->type == HTTP_RESPONSE) { + message_info->Set(status_code_sym, Integer::New(p->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(should_keep_alive_sym, + http_should_keep_alive(p) ? True() : False()); + + Local argv[1] = { message_info }; + + Local ret = cb->Call(parser->handle_, 1, argv); + return ret.IsEmpty() ? -1 : 0; + } + + static Handle New(const Arguments& args) { + HandleScope scope; + + String::Utf8Value type(args[0]->ToString()); + + 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'"))); + } + + 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()); + + if (parser->buffer_) { + return ThrowException(Exception::TypeError( + String::New("Already parsing a buffer"))); + } + + if (!IsBuffer(args[0])) { + return ThrowException(Exception::TypeError( + String::New("Argument should be a buffer"))); + } + + struct buffer * buffer = BufferUnwrap(args[0]); + + size_t off = args[1]->Int32Value(); + if (buffer_p(buffer, off) == NULL) { + return ThrowException(Exception::Error( + String::New("Offset is out of bounds"))); + } + + size_t len = args[2]->Int32Value(); + if (buffer_remaining(buffer, off) < len) { + return ThrowException(Exception::Error( + String::New("Length is extends beyond buffer"))); + } + + TryCatch try_catch; + + // Assign 'buffer_' while we parse. The callbacks will access that varible. + parser->buffer_ = buffer; + + size_t nparsed = + http_parser_execute(&(parser->parser_), buffer_p(buffer, off), len); + + // Unassign the 'buffer_' variable + assert(parser->buffer_); + parser->buffer_ = NULL; + + // If there was an exception in one of the callbacks + if (try_catch.HasCaught()) return try_catch.ReThrow(); + + Local nparsed_obj = Integer::New(nparsed); + // If there was a parse error in one of the callbacks + // TODO What if there is an error on EOF? + if (nparsed != len) { + Local e = Exception::Error(String::New("Parse Error")); + Local obj = e->ToObject(); + obj->Set(String::NewSymbol("bytesParsed"), nparsed_obj); + return ThrowException(e); + } + + return scope.Close(nparsed_obj); + } + + + private: + + http_parser parser_; + struct buffer * buffer_; // The buffer currently being parsed. +}; + + +void InitHttpParser(Handle target) { + HandleScope scope; + + Local t = FunctionTemplate::New(Parser::New); + t->InstanceTemplate()->SetInternalFieldCount(1); + //t->SetClassName(String::NewSymbol("HTTPParser")); + + NODE_SET_PROTOTYPE_METHOD(t, "execute", Parser::Execute); + + target->Set(String::NewSymbol("HTTPParser"), t->GetFunction()); + + on_message_begin_sym = NODE_PSYMBOL("onMessageBegin"); + on_path_sym = NODE_PSYMBOL("onPath"); + on_query_string_sym = NODE_PSYMBOL("onQueryString"); + on_url_sym = NODE_PSYMBOL("onURL"); + on_fragment_sym = NODE_PSYMBOL("onFragment"); + on_header_field_sym = NODE_PSYMBOL("onHeaderField"); + on_header_value_sym = NODE_PSYMBOL("onHeaderValue"); + on_headers_complete_sym = NODE_PSYMBOL("onHeadersComplete"); + on_body_sym = NODE_PSYMBOL("onBody"); + on_message_complete_sym = NODE_PSYMBOL("onMessageComplete"); + + delete_sym = NODE_PSYMBOL("DELETE"); + get_sym = NODE_PSYMBOL("GET"); + head_sym = NODE_PSYMBOL("HEAD"); + post_sym = NODE_PSYMBOL("POST"); + put_sym = NODE_PSYMBOL("PUT"); + connect_sym = NODE_PSYMBOL("CONNECT"); + options_sym = NODE_PSYMBOL("OPTIONS"); + trace_sym = NODE_PSYMBOL("TRACE"); + copy_sym = NODE_PSYMBOL("COPY"); + lock_sym = NODE_PSYMBOL("LOCK"); + mkcol_sym = NODE_PSYMBOL("MKCOL"); + move_sym = NODE_PSYMBOL("MOVE"); + propfind_sym = NODE_PSYMBOL("PROPFIND"); + proppatch_sym = NODE_PSYMBOL("PROPPATCH"); + unlock_sym = NODE_PSYMBOL("UNLOCK"); + unknown_method_sym = NODE_PSYMBOL("UNKNOWN_METHOD"); + + method_sym = NODE_PSYMBOL("method"); + status_code_sym = NODE_PSYMBOL("statusCode"); + http_version_sym = NODE_PSYMBOL("httpVersion"); + version_major_sym = NODE_PSYMBOL("versionMajor"); + version_minor_sym = NODE_PSYMBOL("versionMinor"); + should_keep_alive_sym = NODE_PSYMBOL("shouldKeepAlive"); +} + +} // namespace node + diff --git a/src/node_http_parser.h b/src/node_http_parser.h new file mode 100644 index 0000000000..78ba175eed --- /dev/null +++ b/src/node_http_parser.h @@ -0,0 +1,12 @@ +#ifndef NODE_HTTP_PARSER +#define NODE_HTTP_PARSER + +#include + +namespace node { + +void InitHttpParser(v8::Handle target); + +} + +#endif // NODE_HTTP_PARSER diff --git a/test/mjsunit/test-http-parser.js b/test/mjsunit/test-http-parser.js new file mode 100644 index 0000000000..fa6ac6e592 --- /dev/null +++ b/test/mjsunit/test-http-parser.js @@ -0,0 +1,60 @@ +process.mixin(require("./common")); + +// 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 parser = new process.HTTPParser("request"); + +var buffer = new process.Buffer(1024); + +var request = "GET /hello HTTP/1.1\r\n\r\n"; + +buffer.asciiWrite(request, 0, request.length); + +var callbacks = 0; + +parser.onMessageBegin = function () { + puts("message begin"); + callbacks++; +}; + +parser.onHeadersComplete = function (info) { + puts("headers complete: " + JSON.stringify(info)); + assert.equal('GET', info.method); + assert.equal(1, info.versionMajor); + assert.equal(1, info.versionMinor); + callbacks++; +}; + +parser.onURL = function (off, len) { + //throw new Error("hello world"); + callbacks++; +}; + +parser.onPath = function (off, length) { + puts("path [" + off + ", " + length + "]"); + var path = buffer.asciiSlice(off, off+length); + puts("path = '" + path + "'"); + assert.equal('/hello', path); + callbacks++; +}; + +parser.execute(buffer, 0, request.length); +assert.equal(4, callbacks); + +// +// Check that if we throw an error in the callbacks that error will be +// thrown from parser.execute() +// + +parser.onURL = function (off, len) { + throw new Error("hello world"); +}; + +assert.throws(function () { + parser.execute(buffer, 0, request.length); +}, Error, "hello world"); + diff --git a/wscript b/wscript index 54c3f21380..2431ecb1c3 100644 --- a/wscript +++ b/wscript @@ -346,6 +346,7 @@ def build(bld): src/node.cc src/node_buffer.cc src/node_net2.cc + src/node_http_parser.cc src/node_io_watcher.cc src/node_child_process.cc src/node_constants.cc