Implement new http-parser binding using Buffer
authorRyan Dahl <ry@tinyclouds.org>
Sun, 24 Jan 2010 19:21:45 +0000 (11:21 -0800)
committerRyan Dahl <ry@tinyclouds.org>
Sun, 24 Jan 2010 19:21:45 +0000 (11:21 -0800)
src/node.cc
src/node_http_parser.cc [new file with mode: 0644]
src/node_http_parser.h [new file with mode: 0644]
test/mjsunit/test-http-parser.js [new file with mode: 0644]
wscript

index ea7a8c0..6c4fd51 100644 (file)
@@ -19,6 +19,7 @@
 #include <node_file.h>
 #include <node_idle_watcher.h>
 #include <node_http.h>
+#include <node_http_parser.h>
 #include <node_signal_handler.h>
 #include <node_stat.h>
 #include <node_timer.h>
@@ -996,6 +997,7 @@ static Local<Object> 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 (file)
index 0000000..f90eb3c
--- /dev/null
@@ -0,0 +1,324 @@
+#include <node_http_parser.h>
+
+#include <v8.h>
+#include <node.h>
+#include <node_buffer.h>
+
+#include <http_parser.h>
+
+#include <strings.h>  /* 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<String> on_message_begin_sym;
+static Persistent<String> on_path_sym;
+static Persistent<String> on_query_string_sym;
+static Persistent<String> on_url_sym;
+static Persistent<String> on_fragment_sym;
+static Persistent<String> on_header_field_sym;
+static Persistent<String> on_header_value_sym;
+static Persistent<String> on_headers_complete_sym;
+static Persistent<String> on_body_sym;
+static Persistent<String> on_message_complete_sym;
+
+static Persistent<String> delete_sym;
+static Persistent<String> get_sym;
+static Persistent<String> head_sym;
+static Persistent<String> post_sym;
+static Persistent<String> put_sym;
+static Persistent<String> connect_sym;
+static Persistent<String> options_sym;
+static Persistent<String> trace_sym;
+static Persistent<String> copy_sym;
+static Persistent<String> lock_sym;
+static Persistent<String> mkcol_sym;
+static Persistent<String> move_sym;
+static Persistent<String> propfind_sym;
+static Persistent<String> proppatch_sym;
+static Persistent<String> unlock_sym;
+static Persistent<String> unknown_method_sym;
+
+static Persistent<String> method_sym;
+static Persistent<String> status_code_sym;
+static Persistent<String> http_version_sym;
+static Persistent<String> version_major_sym;
+static Persistent<String> version_minor_sym;
+static Persistent<String> should_keep_alive_sym;
+
+// Callback prototype for http_cb
+#define DEFINE_HTTP_CB(name)                                            \
+  static int name(http_parser *p) {                                     \
+    Parser *parser = static_cast<Parser*>(p->data);                     \
+                                                                        \
+    HandleScope scope;                                                  \
+                                                                        \
+    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);              \
+    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<Parser*>(p->data);                     \
+                                                                        \
+    HandleScope scope;                                                  \
+                                                                        \
+    assert(parser->buffer_);                                            \
+    char * base = buffer_p(parser->buffer_, 0);                         \
+                                                                        \
+    Local<Value> cb_value = parser->handle_->Get(name##_sym);           \
+    if (!cb_value->IsFunction()) return 0;                              \
+    Local<Function> cb = Local<Function>::Cast(cb_value);               \
+                                                                        \
+    Local<Integer> off = Integer::New(at - base);                       \
+    Local<Integer> len = Integer::New(length);                          \
+    Local<Value> argv[2] = { off, len };                                \
+                                                                        \
+    Local<Value> ret = cb->Call(parser->handle_, 2, argv);              \
+    return ret.IsEmpty() ? -1 : 0;                                      \
+  }
+
+
+static inline Persistent<String>
+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<Parser*>(p->data);
+
+    HandleScope scope;
+
+    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);
+
+
+    Local<Object> 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<Value> argv[1] = { message_info };
+
+    Local<Value> ret = cb->Call(parser->handle_, 1, argv);
+    return ret.IsEmpty() ? -1 : 0;
+  }
+
+  static Handle<Value> 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<Value> Execute(const Arguments& args) {
+    HandleScope scope;
+
+    Parser *parser = ObjectWrap::Unwrap<Parser>(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<Integer> 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<Value> e = Exception::Error(String::New("Parse Error"));
+      Local<Object> 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<Object> target) {
+  HandleScope scope;
+
+  Local<FunctionTemplate> 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 (file)
index 0000000..78ba175
--- /dev/null
@@ -0,0 +1,12 @@
+#ifndef NODE_HTTP_PARSER
+#define NODE_HTTP_PARSER
+
+#include <v8.h>
+
+namespace node {
+
+void InitHttpParser(v8::Handle<v8::Object> 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 (file)
index 0000000..fa6ac6e
--- /dev/null
@@ -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 54c3f21..2431ecb 100644 (file)
--- 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