diff --git a/.gitignore b/.gitignore index 823d4d9..5017be2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ ext/*.log ext/*.o ext/*.dll ext/*.bundle -ext/*.so \ No newline at end of file +ext/*.so +.idea \ No newline at end of file diff --git a/README b/README index 86bd426..3cc2b07 100644 --- a/README +++ b/README @@ -19,7 +19,12 @@ def post_init super - no_environment_strings + + # faster if you don't need evironment variables (CGI methods) + self.no_environment_strings + + # max length of the POST content + self.max_content_length = 10_000_000 end def process_http_request diff --git a/Rakefile b/Rakefile index 08be62c..89532c8 100644 --- a/Rakefile +++ b/Rakefile @@ -20,7 +20,8 @@ namespace :build do end CLEAN.include('ext/Makefile') CLEAN.include('ext/*.log') - + CLEAN.include('ext/*.o') + libfile = "ext/eventmachine_httpserver.#{Config::CONFIG['DLEXT']}" file libfile => ['ext/Makefile', *sources] do Dir.chdir 'ext' do diff --git a/eventmachine_httpserver.gemspec b/eventmachine_httpserver.gemspec index 3ed8dc4..ffe6e6f 100644 --- a/eventmachine_httpserver.gemspec +++ b/eventmachine_httpserver.gemspec @@ -2,11 +2,11 @@ Gem::Specification.new do |s| s.name = %q{eventmachine_httpserver} - s.version = "0.2.1" + s.version = "0.2.2" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Francis Cianfrocca"] - s.cert_chain = nil + s.cert_chain = [] s.date = %q{2007-03-16} s.description = %q{} s.email = %q{garbagecat10@gmail.com} @@ -17,11 +17,10 @@ Gem::Specification.new do |s| s.rdoc_options = ["--title", "EventMachine_HttpServer", "--main", "docs/README", "--line-numbers"] s.require_paths = ["lib"] s.required_ruby_version = Gem::Requirement.new("> 0.0.0") - s.rubygems_version = %q{1.3.7} + s.rubygems_version = %q{1.6.2} s.summary = %q{EventMachine HTTP Server} if s.respond_to? :specification_version then - current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION s.specification_version = 1 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then diff --git a/eventmachine_httpserver.gemspec.tmpl b/eventmachine_httpserver.gemspec.tmpl index 179f7e0..98919af 100644 --- a/eventmachine_httpserver.gemspec.tmpl +++ b/eventmachine_httpserver.gemspec.tmpl @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = %q{eventmachine_httpserver} - s.version = "0.2.1" + s.version = "0.2.2" s.specification_version = 1 if s.respond_to? :specification_version= diff --git a/ext/http.cpp b/ext/http.cpp index 241974b..009789b 100644 --- a/ext/http.cpp +++ b/ext/http.cpp @@ -75,6 +75,10 @@ HttpConnection_t::HttpConnection_t() // instead of buffering it here. To get the latter behavior, user code must call // dont_accumulate_post. bAccumulatePost = true; + + // By default this limit is initialized to 20 MiB, it could be changed at runtime + // by the user if needed + ContentLengthLimit = MaxContentLength; } @@ -140,6 +144,20 @@ void HttpConnection_t::ReceivePostData (const char *data, int len) cerr << "UNIMPLEMENTED ReceivePostData" << endl; } +/********************************* +HttpConnection_t::Get/SetMaxContentLength +*********************************/ + +int HttpConnection_t::GetMaxContentLength () +{ + return ContentLengthLimit; +} + +void HttpConnection_t::SetMaxContentLength (int len) +{ + ContentLengthLimit = len; +} + /***************************** HttpConnection_t::ConsumeData *****************************/ @@ -254,7 +272,7 @@ void HttpConnection_t::ConsumeData (const char *data, int length) } else { const char *nl = strpbrk (data, "\r\n"); - int len = nl ? (nl - data) : length; + int len = nl ? (int)(nl - data) : length; if ((size_t)(HeaderLinePos + len) >= sizeof(HeaderLine)) { // TODO, log this goto fail_connection; @@ -358,7 +376,7 @@ bool HttpConnection_t::_InterpretHeaderLine (const char *header) if (bContentLengthSeen) { // TODO, log this. There are some attacks that depend // on sending more than one content-length header. - _SendError (406); + _SendError (400, "Bad Request"); return false; } bContentLengthSeen = true; @@ -366,9 +384,9 @@ bool HttpConnection_t::_InterpretHeaderLine (const char *header) while (*s && ((*s==' ') || (*s=='\t'))) s++; ContentLength = atoi (s); - if (ContentLength > MaxContentLength) { + if (ContentLength > ContentLengthLimit) { // TODO, log this. - _SendError (406); + _SendError (413, "Request Entity Too Large"); return false; } } @@ -400,7 +418,7 @@ bool HttpConnection_t::_InterpretHeaderLine (const char *header) // Copy the incoming header into a block if ((HeaderBlockPos + strlen(header) + 1) < HeaderBlockSize) { - int len = strlen(header); + int len = (int)strlen(header); memcpy (HeaderBlock+HeaderBlockPos, header, len); HeaderBlockPos += len; HeaderBlock [HeaderBlockPos++] = 0; @@ -439,26 +457,37 @@ bool HttpConnection_t::_InterpretRequest (const char *header) const char *blank = strchr (header, ' '); if (!blank) { - _SendError (406); + _SendError (400, "Bad Request"); return false; } - if (!_DetectVerbAndSetEnvString (header, blank - header)) + if (!_DetectVerbAndSetEnvString (header, (int)(blank - header))) return false; blank++; + // according to http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1 + // HTTP 1.1 complient servers MUST also accept absolute URIs in the request line + // e.g. GET http://foo/bar + if (strncasecmp (blank, "http://", 7) == 0) { + blank += 7; // skip over scheme part + while (*blank && *blank != '/') { // skip to next slash + blank++; + } + } + if (*blank != '/') { - _SendError (406); + _SendError (400, "Bad Request"); return false; } const char *blank2 = strchr (blank, ' '); if (!blank2) { - _SendError (406); + _SendError (400, "Bad Request"); return false; } + if (strcasecmp (blank2 + 1, "HTTP/1.0") && strcasecmp (blank2 + 1, "HTTP/1.1")) { - _SendError (505); + _SendError (505, "HTTP Version Not Supported"); return false; } @@ -543,7 +572,8 @@ bool HttpConnection_t::_DetectVerbAndSetEnvString (const char *request, int verb "POST", "PUT", "DELETE", - "HEAD" + "HEAD", + "OPTIONS" }; int n_verbs = sizeof(verbs) / sizeof(const char*); @@ -573,13 +603,18 @@ HttpConnection_t::_SendError ****************************/ void HttpConnection_t::_SendError (int code) +{ + _SendError(code, "..."); +} + +void HttpConnection_t::_SendError (int code, const char *desc) { stringstream ss; - ss << "HTTP/1.1 " << code << " ...\r\n"; + ss << "HTTP/1.1 " << code << " " << desc << "\r\n"; ss << "Connection: close\r\n"; ss << "Content-type: text/plain\r\n"; ss << "\r\n"; ss << "Detected error: HTTP code " << code; - SendData (ss.str().c_str(), ss.str().length()); -} + SendData (ss.str().c_str(), (int)ss.str().length()); +} \ No newline at end of file diff --git a/ext/http.h b/ext/http.h index 48f48df..75f193d 100644 --- a/ext/http.h +++ b/ext/http.h @@ -58,6 +58,8 @@ class HttpConnection_t virtual void ReceivePostData(const char *data, int len); virtual void SetNoEnvironmentStrings() {bSetEnvironmentStrings = false;} virtual void SetDontAccumulatePost() {bAccumulatePost = false;} + virtual int GetMaxContentLength(); + virtual void SetMaxContentLength(int len); private: @@ -85,6 +87,7 @@ class HttpConnection_t int HeaderBlockPos; int ContentLength; + int ContentLengthLimit; int ContentPos; char *_Content; @@ -107,6 +110,7 @@ class HttpConnection_t bool _InterpretRequest (const char*); bool _DetectVerbAndSetEnvString (const char*, int); void _SendError (int); + void _SendError (int, const char*); }; #endif // __HttpPersonality__H_ diff --git a/ext/rubyhttp.cpp b/ext/rubyhttp.cpp index e35ee4d..e12628b 100644 --- a/ext/rubyhttp.cpp +++ b/ext/rubyhttp.cpp @@ -172,6 +172,30 @@ Statics VALUE Intern_http_conn; +/******************** +t_get_http_connection +********************/ + +static RubyHttpConnection_t *t_get_http_connection(VALUE self) +{ + RubyHttpConnection_t *hc; + VALUE ivar = rb_ivar_get (self, Intern_http_conn); + if (ivar != Qnil) + Data_Get_Struct (ivar, RubyHttpConnection_t, hc); + else + hc = NULL; + return hc; +} + +/******************** +t_delete_http_connection +********************/ + +void t_delete_http_connection(RubyHttpConnection_t *hc) +{ + delete hc; +} + /*********** t_post_init ***********/ @@ -182,7 +206,12 @@ static VALUE t_post_init (VALUE self) if (!hc) throw std::runtime_error ("no http-connection object"); - rb_ivar_set (self, Intern_http_conn, LONG2NUM ((long)hc)); + /* + HACK: http_connection should be given an actual type. No one should + be touching it from inside ruby, but still + */ + VALUE http_connection = Data_Wrap_Struct(CLASS_OF(self), 0, t_delete_http_connection, hc); + rb_ivar_set (self, Intern_http_conn, http_connection); return Qnil; } @@ -194,7 +223,7 @@ t_receive_data static VALUE t_receive_data (VALUE self, VALUE data) { int length = NUM2INT (rb_funcall (data, rb_intern ("length"), 0)); - RubyHttpConnection_t *hc = (RubyHttpConnection_t*)(NUM2LONG (rb_ivar_get (self, Intern_http_conn))); + RubyHttpConnection_t *hc = t_get_http_connection (self); if (hc) hc->ConsumeData (StringValuePtr (data), length); return Qnil; @@ -216,9 +245,6 @@ t_unbind static VALUE t_unbind (VALUE self) { - RubyHttpConnection_t *hc = (RubyHttpConnection_t*)(NUM2LONG (rb_ivar_get (self, Intern_http_conn))); - if (hc) - delete hc; return Qnil; } @@ -240,7 +266,7 @@ t_no_environment_strings static VALUE t_no_environment_strings (VALUE self) { - RubyHttpConnection_t *hc = (RubyHttpConnection_t*)(NUM2LONG (rb_ivar_get (self, Intern_http_conn))); + RubyHttpConnection_t *hc = t_get_http_connection (self); if (hc) hc->SetNoEnvironmentStrings(); return Qnil; @@ -252,12 +278,39 @@ t_dont_accumulate_post static VALUE t_dont_accumulate_post (VALUE self) { - RubyHttpConnection_t *hc = (RubyHttpConnection_t*)(NUM2LONG (rb_ivar_get (self, Intern_http_conn))); + RubyHttpConnection_t *hc = t_get_http_connection (self); if (hc) hc->SetDontAccumulatePost(); return Qnil; } +/********************** +t_get_max_content_length +**********************/ + +static VALUE t_get_max_content_length (VALUE self) +{ + RubyHttpConnection_t *hc = t_get_http_connection (self); + if (hc) + return INT2FIX (hc->GetMaxContentLength()); + + return Qnil; +} + +/********************** +t_set_max_content_length +**********************/ + +static VALUE t_set_max_content_length (VALUE self, VALUE data) +{ + RubyHttpConnection_t *hc = t_get_http_connection (self); + if (hc) { + hc->SetMaxContentLength(FIX2INT(data)); + return INT2FIX (hc->GetMaxContentLength()); + } + + return Qnil; +} /**************************** Init_eventmachine_httpserver @@ -276,4 +329,6 @@ extern "C" void Init_eventmachine_httpserver() rb_define_method (HttpServer, "process_http_request", (VALUE(*)(...))t_process_http_request, 0); rb_define_method (HttpServer, "no_environment_strings", (VALUE(*)(...))t_no_environment_strings, 0); rb_define_method (HttpServer, "dont_accumulate_post", (VALUE(*)(...))t_dont_accumulate_post, 0); + rb_define_method (HttpServer, "max_content_length", (VALUE(*)(...))t_get_max_content_length, 0); + rb_define_method (HttpServer, "max_content_length=", (VALUE(*)(...))t_set_max_content_length, 1); } diff --git a/lib/evma_httpserver/response.rb b/lib/evma_httpserver/response.rb index 61f7965..b8a5d76 100644 --- a/lib/evma_httpserver/response.rb +++ b/lib/evma_httpserver/response.rb @@ -49,7 +49,7 @@ module EventMachine # EventMachine::Connection. # class HttpResponse - attr_accessor :status, :content, :headers, :chunks, :multiparts + attr_accessor :status, :status_string, :content, :headers, :chunks, :multiparts def initialize @headers = {} @@ -98,7 +98,7 @@ def send_response send_headers send_body send_trailer - close_connection_after_writing unless (@keep_connection_open and (@status || 200) == 200) + close_connection_after_writing unless (@keep_connection_open and (@status || 200) < 500) end # Send the headers out in alpha-sorted order. This will degrade performance to some @@ -111,7 +111,7 @@ def send_headers fixup_headers ary = [] - ary << "HTTP/1.1 #{@status || 200} ...\r\n" + ary << "HTTP/1.1 #{@status || 200} #{@status_string || '...'}\r\n" ary += generate_header_lines(@headers) ary << "\r\n" @@ -286,6 +286,7 @@ def self.concoct_multipart_boundary def send_redirect location @status = 302 # TODO, make 301 available by parameter + @status_string = "Moved Temporarily" @headers["Location"] = location send_response end diff --git a/test/test_response.rb b/test/test_response.rb index 5e54c4d..9b8990c 100644 --- a/test/test_response.rb +++ b/test/test_response.rb @@ -70,6 +70,19 @@ def test_send_response assert_equal( true, a.closed_after_writing ) end + def test_send_response_with_status + a = EventMachine::HttpResponse.new + a.status = 200 + a.status_string = "OK-TEST" + a.send_response + assert_equal([ + "HTTP/1.1 200 OK-TEST\r\n", + "Content-length: 0\r\n", + "\r\n" + ].join, a.output_data) + assert_equal( true, a.closed_after_writing ) + end + def test_send_response_1 a = EventMachine::HttpResponse.new a.status = 200 @@ -103,6 +116,57 @@ def test_send_response_no_close assert( ! a.closed_after_writing ) end + def test_send_response_no_close_with_a_404_response + a = EventMachine::HttpResponse.new + a.status = 404 + a.content_type "text/plain" + a.content = "ABC" + a.keep_connection_open + a.send_response + assert_equal([ + "HTTP/1.1 404 ...\r\n", + "Content-length: 3\r\n", + "Content-type: text/plain\r\n", + "\r\n", + "ABC" + ].join, a.output_data) + assert( ! a.closed_after_writing ) + end + + def test_send_response_no_close_with_a_201_response + a = EventMachine::HttpResponse.new + a.status = 201 + a.content_type "text/plain" + a.content = "ABC" + a.keep_connection_open + a.send_response + assert_equal([ + "HTTP/1.1 201 ...\r\n", + "Content-length: 3\r\n", + "Content-type: text/plain\r\n", + "\r\n", + "ABC" + ].join, a.output_data) + assert( ! a.closed_after_writing ) + end + + def test_send_response_no_close_with_a_500_response + a = EventMachine::HttpResponse.new + a.status = 500 + a.content_type "text/plain" + a.content = "ABC" + a.keep_connection_open + a.send_response + assert_equal([ + "HTTP/1.1 500 ...\r\n", + "Content-length: 3\r\n", + "Content-type: text/plain\r\n", + "\r\n", + "ABC" + ].join, a.output_data) + assert( a.closed_after_writing ) + end + def test_send_response_multiple_times a = EventMachine::HttpResponse.new a.status = 200