Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,23 @@
name: Testing on Ubuntu
on:
- push
- pull_request
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
ruby: [ '2.6', '2.7', '3.0' ]
os: [ 'ubuntu-latest' ]
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: unit testing
run: |
gem install bundler rake
bundle install --jobs 4 --retry 3
bundle exec rake
@@ -0,0 +1,23 @@
name: Testing on Windows
on:
- push
- pull_request
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
ruby: [ '2.6', '2.7', '3.0' ]
os: [ 'windows-latest' ]
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: unit testing
run: |
gem install bundler rake
bundle install --jobs 4 --retry 3
bundle exec rake
+12
View File
@@ -0,0 +1,12 @@
*.bundle
*.bundle
*.gem
*.jar
*.o
*.rbc
*.so
*.swp
Gemfile.lock
Makefile
tags
tmp
@@ -0,0 +1,6 @@
[submodule "http-parser"]
path = ext/ruby_http_parser/vendor/http-parser
url = https://github.com/nodejs/http-parser.git
[submodule "http-parser-java"]
path = ext/ruby_http_parser/vendor/http-parser-java
url = https://github.com/tmm1/http-parser.java
+2
View File
@@ -0,0 +1,2 @@
source 'https://rubygems.org'
gemspec
+20
View File
@@ -0,0 +1,20 @@
Copyright 2009,2010 Marc-André Cournoyer <macournoyer@gmail.com>
Copyright 2010,2011 Aman Gupta <aman@tmm1.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
+90
View File
@@ -0,0 +1,90 @@
# http_parser.rb
A simple callback-based HTTP request/response parser for writing http
servers, clients and proxies.
This gem is built on top of [joyent/http-parser](https://github.com/joyent/http-parser) and its java port [http-parser/http-parser.java](https://github.com/http-parser/http-parser.java).
## Supported Platforms
This gem aims to work on all major Ruby platforms, including:
- MRI 1.8, 1.9 and 2.0; should work on MRI 2.4+
- Rubinius
- JRuby
- win32
## Usage
```ruby
require "http/parser"
parser = Http::Parser.new
parser.on_headers_complete = proc do
p parser.http_version
p parser.http_method # for requests
p parser.request_url
p parser.status_code # for responses
p parser.headers
end
parser.on_body = proc do |chunk|
# One chunk of the body
p chunk
end
parser.on_message_complete = proc do |env|
# Headers and body is all parsed
puts "Done!"
end
```
# Feed raw data from the socket to the parser
`parser << raw_data`
## Advanced Usage
### Accept callbacks on an object
```ruby
module MyHttpConnection
def connection_completed
@parser = Http::Parser.new(self)
end
def receive_data(data)
@parser << data
end
def on_message_begin
@headers = nil
@body = ''
end
def on_headers_complete(headers)
@headers = headers
end
def on_body(chunk)
@body << chunk
end
def on_message_complete
p [@headers, @body]
end
end
```
### Stop parsing after headers
```ruby
parser = Http::Parser.new
parser.on_headers_complete = proc{ :stop }
offset = parser << request_data
body = request_data[offset..-1]
```
+8
View File
@@ -0,0 +1,8 @@
require 'bundler/gem_tasks'
# default task
task :compile => :submodules
task :default => [:compile, :spec]
# load tasks
Dir['tasks/*.rake'].sort.each { |f| load f }
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env ruby
$:.unshift File.dirname(__FILE__) + "/../lib"
require "rubygems"
require "http/parser"
require "benchmark/ips"
request = <<-REQUEST
GET / HTTP/1.1
Host: www.example.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.78 S
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
REQUEST
request.gsub!(/\n/m, "\r\n")
Benchmark.ips do |ips|
ips.report("instance") { Http::Parser.new }
ips.report("parsing") { Http::Parser.new << request }
end
@@ -0,0 +1,58 @@
$:.unshift File.dirname(__FILE__) + "/../lib"
require "rubygems"
require "thin_parser"
require "http_parser"
require "benchmark"
require "stringio"
data = "POST /postit HTTP/1.1\r\n" +
"Host: localhost:3000\r\n" +
"User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9\r\n" +
"Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\n" +
"Accept-Language: en-us,en;q=0.5\r\n" +
"Accept-Encoding: gzip,deflate\r\n" +
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
"Keep-Alive: 300\r\n" +
"Connection: keep-alive\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 37\r\n" +
"\r\n" +
"name=marc&email=macournoyer@gmail.com"
def thin(data)
env = {"rack.input" => StringIO.new}
Thin::HttpParser.new.execute(env, data, 0)
env
end
def http_parser(data)
body = StringIO.new
env = nil
parser = HTTP::RequestParser.new
parser.on_headers_complete = proc { |e| env = e }
parser.on_body = proc { |c| body << c }
parser << data
env["rack-input"] = body
env
end
# p thin(data)
# p http_parser(data)
TESTS = 30_000
Benchmark.bmbm do |results|
results.report("thin:") { TESTS.times { thin data } }
results.report("http-parser:") { TESTS.times { http_parser data } }
end
# On my MBP core duo 2.2Ghz
# Rehearsal ------------------------------------------------
# thin: 1.470000 0.000000 1.470000 ( 1.474737)
# http-parser: 1.270000 0.020000 1.290000 ( 1.292758)
# --------------------------------------- total: 2.760000sec
#
# user system total real
# thin: 1.150000 0.030000 1.180000 ( 1.173767)
# http-parser: 1.250000 0.010000 1.260000 ( 1.263796)
@@ -0,0 +1 @@
ryah_http_parser.*
@@ -0,0 +1,18 @@
import java.io.IOException;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.runtime.load.BasicLibraryService;
import org.ruby_http_parser.*;
public class RubyHttpParserService implements BasicLibraryService {
public boolean basicLoad(final Ruby runtime) throws IOException {
RubyModule mHTTP = runtime.defineModule("HTTP");
RubyClass cParser = mHTTP.defineClassUnder("Parser", runtime.getObject(), RubyHttpParser.ALLOCATOR);
cParser.defineAnnotatedMethods(RubyHttpParser.class);
cParser.defineClassUnder("Error", runtime.getClass("IOError"),runtime.getClass("IOError").getAllocator());
return true;
}
}
@@ -0,0 +1,18 @@
#ifndef ext_help_h
#define ext_help_h
#define RAISE_NOT_NULL(T) if(T == NULL) rb_raise(rb_eArgError, "NULL found for " # T " when shouldn't be.");
#define DATA_GET(from,type,name) Data_Get_Struct(from,type,name); RAISE_NOT_NULL(name);
#define REQUIRE_TYPE(V, T) if(TYPE(V) != T) rb_raise(rb_eTypeError, "Wrong argument type for " # V " required " # T);
/* for compatibility with Ruby 1.8.5, which doesn't declare RSTRING_PTR */
#ifndef RSTRING_PTR
#define RSTRING_PTR(s) (RSTRING(s)->ptr)
#endif
/* for compatibility with Ruby 1.8.5, which doesn't declare RSTRING_LEN */
#ifndef RSTRING_LEN
#define RSTRING_LEN(s) (RSTRING(s)->len)
#endif
#endif
@@ -0,0 +1,24 @@
require 'mkmf'
# check out code if it hasn't been already
if Dir[File.expand_path('../vendor/http-parser/*', __FILE__)].empty?
Dir.chdir(File.expand_path('../../../', __FILE__)) do
xsystem 'git submodule init'
xsystem 'git submodule update'
end
end
# mongrel and http-parser both define http_parser_(init|execute), so we
# rename functions in http-parser before using them.
vendor_dir = File.expand_path('../vendor/http-parser/', __FILE__)
src_dir = File.expand_path('../', __FILE__)
%w[ http_parser.c http_parser.h ].each do |file|
File.open(File.join(src_dir, "ryah_#{file}"), 'w'){ |f|
f.write File.read(File.join(vendor_dir, file)).gsub('http_parser', 'ryah_http_parser')
}
end
$CFLAGS << " -I\"#{src_dir}\""
dir_config("ruby_http_parser")
create_makefile("ruby_http_parser")
@@ -0,0 +1,502 @@
package org.ruby_http_parser;
import http_parser.HTTPException;
import http_parser.HTTPMethod;
import http_parser.HTTPParser;
import http_parser.lolevel.HTTPCallback;
import http_parser.lolevel.HTTPDataCallback;
import http_parser.lolevel.ParserSettings;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import org.jcodings.Encoding;
import org.jcodings.specific.UTF8Encoding;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyHash;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.RubySymbol;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
public class RubyHttpParser extends RubyObject {
@JRubyMethod(name = "strict?", module = true)
public static IRubyObject strict(IRubyObject recv) {
return recv.getRuntime().newBoolean(true);
}
public static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new RubyHttpParser(runtime, klass);
}
};
byte[] fetchBytes(ByteBuffer b, int pos, int len) {
byte[] by = new byte[len];
int saved = b.position();
b.position(pos);
b.get(by);
b.position(saved);
return by;
}
public class StopException extends RuntimeException {
}
private Ruby runtime;
private HTTPParser parser;
private ParserSettings settings;
private RubyClass eParserError;
private RubyHash headers;
private IRubyObject on_message_begin;
private IRubyObject on_headers_complete;
private IRubyObject on_body;
private IRubyObject on_message_complete;
private IRubyObject requestUrl;
private IRubyObject requestPath;
private IRubyObject queryString;
private IRubyObject fragment;
private IRubyObject header_value_type;
private IRubyObject upgradeData;
private IRubyObject callback_object;
private boolean completed;
private byte[] _current_header;
private byte[] _last_header;
private static final Encoding UTF8 = UTF8Encoding.INSTANCE;
private static final List<String> VALUE_TYPES = new ArrayList<String>(
Arrays.asList("mixed", "arrays", "strings")
);
public RubyHttpParser(final Ruby runtime, RubyClass clazz) {
super(runtime, clazz);
this.runtime = runtime;
this.eParserError = (RubyClass) runtime.getModule("HTTP").getClass("Parser").getConstant("Error");
this.on_message_begin = null;
this.on_headers_complete = null;
this.on_body = null;
this.on_message_complete = null;
this.callback_object = null;
this.completed = false;
this.header_value_type = runtime.getModule("HTTP").getClass("Parser")
.getInstanceVariable("@default_header_value_type");
initSettings();
init();
}
private void initSettings() {
this.settings = new ParserSettings();
this.settings.on_url = new HTTPDataCallback() {
public int cb(http_parser.lolevel.HTTPParser p, ByteBuffer buf, int pos, int len) {
byte[] data = fetchBytes(buf, pos, len);
if (runtime.is1_9() || runtime.is2_0()) {
((RubyString) requestUrl).cat(data, 0, data.length, UTF8);
} else {
((RubyString) requestUrl).cat(data);
}
return 0;
}
};
this.settings.on_header_field = new HTTPDataCallback() {
public int cb(http_parser.lolevel.HTTPParser p, ByteBuffer buf, int pos, int len) {
byte[] data = fetchBytes(buf, pos, len);
if (_current_header == null)
_current_header = data;
else {
byte[] tmp = new byte[_current_header.length + data.length];
System.arraycopy(_current_header, 0, tmp, 0, _current_header.length);
System.arraycopy(data, 0, tmp, _current_header.length, data.length);
_current_header = tmp;
}
return 0;
}
};
final RubySymbol arraysSym = runtime.newSymbol("arrays");
final RubySymbol mixedSym = runtime.newSymbol("mixed");
final RubySymbol stopSym = runtime.newSymbol("stop");
final RubySymbol resetSym = runtime.newSymbol("reset");
this.settings.on_header_value = new HTTPDataCallback() {
public int cb(http_parser.lolevel.HTTPParser p, ByteBuffer buf, int pos, int len) {
byte[] data = fetchBytes(buf, pos, len);
ThreadContext context = headers.getRuntime().getCurrentContext();
IRubyObject key, val;
int new_field = 0;
if (_current_header != null) {
new_field = 1;
_last_header = _current_header;
_current_header = null;
}
key = RubyString.newString(runtime, new ByteList(_last_header, UTF8, false));
val = headers.op_aref(context, key);
if (new_field == 1) {
if (val.isNil()) {
if (header_value_type == arraysSym) {
headers.op_aset(context, key,
RubyArray.newArrayLight(runtime, RubyString.newStringLight(runtime, 10, UTF8)));
} else {
headers.op_aset(context, key, RubyString.newStringLight(runtime, 10, UTF8));
}
} else {
if (header_value_type == mixedSym) {
if (val instanceof RubyString) {
headers.op_aset(context, key,
RubyArray.newArrayLight(runtime, val, RubyString.newStringLight(runtime, 10, UTF8)));
} else {
((RubyArray) val).add(RubyString.newStringLight(runtime, 10, UTF8));
}
} else if (header_value_type == arraysSym) {
((RubyArray) val).add(RubyString.newStringLight(runtime, 10, UTF8));
} else {
if (runtime.is1_9() || runtime.is2_0()) {
((RubyString) val).cat(',', UTF8).cat(' ', UTF8);
} else {
((RubyString) val).cat(',').cat(' ');
}
}
}
val = headers.op_aref(context, key);
}
if (val instanceof RubyArray) {
val = ((RubyArray) val).entry(-1);
}
if (runtime.is1_9() || runtime.is2_0()) {
((RubyString) val).cat(data, 0, data.length, UTF8);
} else {
((RubyString) val).cat(data);
}
return 0;
}
};
this.settings.on_message_begin = new HTTPCallback() {
public int cb(http_parser.lolevel.HTTPParser p) {
headers = new RubyHash(runtime);
if (runtime.is1_9() || runtime.is2_0()) {
requestUrl = RubyString.newEmptyString(runtime, UTF8);
requestPath = RubyString.newEmptyString(runtime, UTF8);
queryString = RubyString.newEmptyString(runtime, UTF8);
fragment = RubyString.newEmptyString(runtime, UTF8);
upgradeData = RubyString.newEmptyString(runtime, UTF8);
} else {
requestUrl = RubyString.newEmptyString(runtime);
requestPath = RubyString.newEmptyString(runtime);
queryString = RubyString.newEmptyString(runtime);
fragment = RubyString.newEmptyString(runtime);
upgradeData = RubyString.newEmptyString(runtime);
}
IRubyObject ret = runtime.getNil();
if (callback_object != null) {
if (((RubyObject) callback_object).respondsTo("on_message_begin")) {
ThreadContext context = callback_object.getRuntime().getCurrentContext();
ret = callback_object.callMethod(context, "on_message_begin");
}
} else if (on_message_begin != null) {
ThreadContext context = on_message_begin.getRuntime().getCurrentContext();
ret = on_message_begin.callMethod(context, "call");
}
if (ret == stopSym) {
throw new StopException();
} else {
return 0;
}
}
};
this.settings.on_message_complete = new HTTPCallback() {
public int cb(http_parser.lolevel.HTTPParser p) {
IRubyObject ret = runtime.getNil();
completed = true;
if (callback_object != null) {
if (((RubyObject) callback_object).respondsTo("on_message_complete")) {
ThreadContext context = callback_object.getRuntime().getCurrentContext();
ret = callback_object.callMethod(context, "on_message_complete");
}
} else if (on_message_complete != null) {
ThreadContext context = on_message_complete.getRuntime().getCurrentContext();
ret = on_message_complete.callMethod(context, "call");
}
if (ret == stopSym) {
throw new StopException();
} else {
return 0;
}
}
};
this.settings.on_headers_complete = new HTTPCallback() {
public int cb(http_parser.lolevel.HTTPParser p) {
IRubyObject ret = runtime.getNil();
if (callback_object != null) {
if (((RubyObject) callback_object).respondsTo("on_headers_complete")) {
ThreadContext context = callback_object.getRuntime().getCurrentContext();
ret = callback_object.callMethod(context, "on_headers_complete", headers);
}
} else if (on_headers_complete != null) {
ThreadContext context = on_headers_complete.getRuntime().getCurrentContext();
ret = on_headers_complete.callMethod(context, "call", headers);
}
if (ret == stopSym) {
throw new StopException();
} else if (ret == resetSym) {
return 1;
} else {
return 0;
}
}
};
this.settings.on_body = new HTTPDataCallback() {
public int cb(http_parser.lolevel.HTTPParser p, ByteBuffer buf, int pos, int len) {
IRubyObject ret = runtime.getNil();
byte[] data = fetchBytes(buf, pos, len);
if (callback_object != null) {
if (((RubyObject) callback_object).respondsTo("on_body")) {
ThreadContext context = callback_object.getRuntime().getCurrentContext();
ret = callback_object.callMethod(context, "on_body",
RubyString.newString(runtime, new ByteList(data, UTF8, false)));
}
} else if (on_body != null) {
ThreadContext context = on_body.getRuntime().getCurrentContext();
ret = on_body.callMethod(context, "call", RubyString.newString(runtime, new ByteList(data, UTF8, false)));
}
if (ret == stopSym) {
throw new StopException();
} else {
return 0;
}
}
};
}
private void init() {
this.parser = new HTTPParser();
this.parser.HTTP_PARSER_STRICT = true;
this.headers = null;
this.requestUrl = runtime.getNil();
this.requestPath = runtime.getNil();
this.queryString = runtime.getNil();
this.fragment = runtime.getNil();
this.upgradeData = runtime.getNil();
}
@JRubyMethod(name = "initialize")
public IRubyObject initialize() {
return this;
}
@JRubyMethod(name = "initialize")
public IRubyObject initialize(IRubyObject arg) {
callback_object = arg;
return initialize();
}
@JRubyMethod(name = "initialize")
public IRubyObject initialize(IRubyObject arg, IRubyObject arg2) {
header_value_type = arg2;
return initialize(arg);
}
@JRubyMethod(name = "on_message_begin=")
public IRubyObject set_on_message_begin(IRubyObject cb) {
on_message_begin = cb;
return cb;
}
@JRubyMethod(name = "on_headers_complete=")
public IRubyObject set_on_headers_complete(IRubyObject cb) {
on_headers_complete = cb;
return cb;
}
@JRubyMethod(name = "on_body=")
public IRubyObject set_on_body(IRubyObject cb) {
on_body = cb;
return cb;
}
@JRubyMethod(name = "on_message_complete=")
public IRubyObject set_on_message_complete(IRubyObject cb) {
on_message_complete = cb;
return cb;
}
@JRubyMethod(name = "<<")
public IRubyObject execute(IRubyObject data) {
RubyString str = (RubyString) data;
ByteList byteList = str.getByteList();
ByteBuffer buf = ByteBuffer.wrap(byteList.getUnsafeBytes(), byteList.getBegin(), byteList.getRealSize());
boolean stopped = false;
try {
this.parser.execute(this.settings, buf);
} catch (HTTPException e) {
throw new RaiseException(runtime, eParserError, e.getMessage(), true);
} catch (StopException e) {
stopped = true;
}
if (parser.getUpgrade()) {
byte[] upData = fetchBytes(buf, buf.position(), buf.limit() - buf.position());
if (runtime.is1_9() || runtime.is2_0()) {
((RubyString) upgradeData).cat(upData, 0, upData.length, UTF8);
} else {
((RubyString) upgradeData).cat(upData);
}
} else if (buf.hasRemaining() && !completed) {
if (!stopped)
throw new RaiseException(runtime, eParserError, "Could not parse data entirely", true);
}
return RubyNumeric.int2fix(runtime, buf.position());
}
@JRubyMethod(name = "keep_alive?")
public IRubyObject shouldKeepAlive() {
return runtime.newBoolean(parser.shouldKeepAlive());
}
@JRubyMethod(name = "upgrade?")
public IRubyObject shouldUpgrade() {
return runtime.newBoolean(parser.getUpgrade());
}
@JRubyMethod(name = "http_major")
public IRubyObject httpMajor() {
if (parser.getMajor() == 0 && parser.getMinor() == 0)
return runtime.getNil();
else
return RubyNumeric.int2fix(runtime, parser.getMajor());
}
@JRubyMethod(name = "http_minor")
public IRubyObject httpMinor() {
if (parser.getMajor() == 0 && parser.getMinor() == 0)
return runtime.getNil();
else
return RubyNumeric.int2fix(runtime, parser.getMinor());
}
@JRubyMethod(name = "http_version")
public IRubyObject httpVersion() {
if (parser.getMajor() == 0 && parser.getMinor() == 0)
return runtime.getNil();
else
return runtime.newArray(httpMajor(), httpMinor());
}
@JRubyMethod(name = "http_method")
public IRubyObject httpMethod() {
HTTPMethod method = parser.getHTTPMethod();
if (method != null)
return runtime.newString(new String(method.bytes));
else
return runtime.getNil();
}
@JRubyMethod(name = "status_code")
public IRubyObject statusCode() {
int code = parser.getStatusCode();
if (code != 0)
return RubyNumeric.int2fix(runtime, code);
else
return runtime.getNil();
}
@JRubyMethod(name = "headers")
public IRubyObject getHeaders() {
return headers == null ? runtime.getNil() : headers;
}
@JRubyMethod(name = "request_url")
public IRubyObject getRequestUrl() {
return requestUrl == null ? runtime.getNil() : requestUrl;
}
@JRubyMethod(name = "request_path")
public IRubyObject getRequestPath() {
return requestPath == null ? runtime.getNil() : requestPath;
}
@JRubyMethod(name = "query_string")
public IRubyObject getQueryString() {
return queryString == null ? runtime.getNil() : queryString;
}
@JRubyMethod(name = "fragment")
public IRubyObject getFragment() {
return fragment == null ? runtime.getNil() : fragment;
}
@JRubyMethod(name = "header_value_type")
public IRubyObject getHeaderValueType() {
return header_value_type == null ? runtime.getNil() : header_value_type;
}
@JRubyMethod(name = "header_value_type=")
public IRubyObject set_header_value_type(IRubyObject val) {
String valString = val.toString();
if (!VALUE_TYPES.contains(valString)) {
throw runtime.newArgumentError("Invalid header value type");
}
header_value_type = val;
return val;
}
@JRubyMethod(name = "upgrade_data")
public IRubyObject upgradeData() {
return upgradeData == null ? runtime.getNil() : upgradeData;
}
@JRubyMethod(name = "reset!")
public IRubyObject reset() {
init();
return runtime.getTrue();
}
}
@@ -0,0 +1,561 @@
#include "ruby.h"
#include "ext_help.h"
#include "ryah_http_parser.h"
#define GET_WRAPPER(N, from) ParserWrapper *N = (ParserWrapper *)(from)->data;
#define HASH_CAT(h, k, ptr, len) \
do { \
VALUE __v = rb_hash_aref(h, k); \
if (__v != Qnil) { \
rb_str_cat(__v, ptr, len); \
} else { \
rb_hash_aset(h, k, rb_str_new(ptr, len)); \
} \
} while(0)
typedef struct ParserWrapper {
ryah_http_parser parser;
VALUE status;
VALUE request_url;
VALUE headers;
VALUE upgrade_data;
VALUE on_message_begin;
VALUE on_headers_complete;
VALUE on_body;
VALUE on_message_complete;
VALUE callback_object;
VALUE stopped;
VALUE completed;
VALUE header_value_type;
VALUE last_field_name;
VALUE curr_field_name;
enum ryah_http_parser_type type;
} ParserWrapper;
void ParserWrapper_init(ParserWrapper *wrapper) {
ryah_http_parser_init(&wrapper->parser, wrapper->type);
wrapper->parser.status_code = 0;
wrapper->parser.http_major = 0;
wrapper->parser.http_minor = 0;
wrapper->status = Qnil;
wrapper->request_url = Qnil;
wrapper->upgrade_data = Qnil;
wrapper->headers = Qnil;
wrapper->completed = Qfalse;
wrapper->last_field_name = Qnil;
wrapper->curr_field_name = Qnil;
}
void ParserWrapper_mark(void *data) {
if(data) {
ParserWrapper *wrapper = (ParserWrapper *) data;
rb_gc_mark_maybe(wrapper->status);
rb_gc_mark_maybe(wrapper->request_url);
rb_gc_mark_maybe(wrapper->upgrade_data);
rb_gc_mark_maybe(wrapper->headers);
rb_gc_mark_maybe(wrapper->on_message_begin);
rb_gc_mark_maybe(wrapper->on_headers_complete);
rb_gc_mark_maybe(wrapper->on_body);
rb_gc_mark_maybe(wrapper->on_message_complete);
rb_gc_mark_maybe(wrapper->callback_object);
rb_gc_mark_maybe(wrapper->last_field_name);
rb_gc_mark_maybe(wrapper->curr_field_name);
}
}
void ParserWrapper_free(void *data) {
if(data) {
free(data);
}
}
static VALUE cParser;
static VALUE cRequestParser;
static VALUE cResponseParser;
static VALUE eParserError;
static ID Icall;
static ID Ion_message_begin;
static ID Ion_headers_complete;
static ID Ion_body;
static ID Ion_message_complete;
static VALUE Sstop;
static VALUE Sreset;
static VALUE Sarrays;
static VALUE Sstrings;
static VALUE Smixed;
/** Callbacks **/
int on_message_begin(ryah_http_parser *parser) {
GET_WRAPPER(wrapper, parser);
wrapper->status = rb_str_new2("");
wrapper->request_url = rb_str_new2("");
wrapper->headers = rb_hash_new();
wrapper->upgrade_data = rb_str_new2("");
VALUE ret = Qnil;
if (wrapper->callback_object != Qnil && rb_respond_to(wrapper->callback_object, Ion_message_begin)) {
ret = rb_funcall(wrapper->callback_object, Ion_message_begin, 0);
} else if (wrapper->on_message_begin != Qnil) {
ret = rb_funcall(wrapper->on_message_begin, Icall, 0);
}
if (ret == Sstop) {
wrapper->stopped = Qtrue;
return -1;
} else {
return 0;
}
}
int on_status(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
if (at && length) {
if (wrapper->status == Qnil) {
wrapper->status = rb_str_new(at, length);
} else {
rb_str_cat(wrapper->status, at, length);
}
}
return 0;
}
int on_url(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
if (at && length) {
if (wrapper->request_url == Qnil) {
wrapper->request_url = rb_str_new(at, length);
} else {
rb_str_cat(wrapper->request_url, at, length);
}
}
return 0;
}
int on_header_field(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
if (wrapper->curr_field_name == Qnil) {
wrapper->last_field_name = Qnil;
wrapper->curr_field_name = rb_str_new(at, length);
} else {
rb_str_cat(wrapper->curr_field_name, at, length);
}
return 0;
}
int on_header_value(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
int new_field = 0;
VALUE current_value;
if (wrapper->last_field_name == Qnil) {
new_field = 1;
wrapper->last_field_name = wrapper->curr_field_name;
wrapper->curr_field_name = Qnil;
}
current_value = rb_hash_aref(wrapper->headers, wrapper->last_field_name);
if (new_field == 1) {
if (current_value == Qnil) {
if (wrapper->header_value_type == Sarrays) {
rb_hash_aset(wrapper->headers, wrapper->last_field_name, rb_ary_new3(1, rb_str_new2("")));
} else {
rb_hash_aset(wrapper->headers, wrapper->last_field_name, rb_str_new2(""));
}
} else {
if (wrapper->header_value_type == Smixed) {
if (TYPE(current_value) == T_STRING) {
rb_hash_aset(wrapper->headers, wrapper->last_field_name, rb_ary_new3(2, current_value, rb_str_new2("")));
} else {
rb_ary_push(current_value, rb_str_new2(""));
}
} else if (wrapper->header_value_type == Sarrays) {
rb_ary_push(current_value, rb_str_new2(""));
} else {
rb_str_cat(current_value, ", ", 2);
}
}
current_value = rb_hash_aref(wrapper->headers, wrapper->last_field_name);
}
if (TYPE(current_value) == T_ARRAY) {
current_value = rb_ary_entry(current_value, -1);
}
rb_str_cat(current_value, at, length);
return 0;
}
int on_headers_complete(ryah_http_parser *parser) {
GET_WRAPPER(wrapper, parser);
VALUE ret = Qnil;
if (wrapper->callback_object != Qnil && rb_respond_to(wrapper->callback_object, Ion_headers_complete)) {
ret = rb_funcall(wrapper->callback_object, Ion_headers_complete, 1, wrapper->headers);
} else if (wrapper->on_headers_complete != Qnil) {
ret = rb_funcall(wrapper->on_headers_complete, Icall, 1, wrapper->headers);
}
if (ret == Sstop) {
wrapper->stopped = Qtrue;
return -1;
} else if (ret == Sreset){
return 1;
} else {
return 0;
}
}
int on_body(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
VALUE ret = Qnil;
if (wrapper->callback_object != Qnil && rb_respond_to(wrapper->callback_object, Ion_body)) {
ret = rb_funcall(wrapper->callback_object, Ion_body, 1, rb_str_new(at, length));
} else if (wrapper->on_body != Qnil) {
ret = rb_funcall(wrapper->on_body, Icall, 1, rb_str_new(at, length));
}
if (ret == Sstop) {
wrapper->stopped = Qtrue;
return -1;
} else {
return 0;
}
}
int on_message_complete(ryah_http_parser *parser) {
GET_WRAPPER(wrapper, parser);
VALUE ret = Qnil;
wrapper->completed = Qtrue;
if (wrapper->callback_object != Qnil && rb_respond_to(wrapper->callback_object, Ion_message_complete)) {
ret = rb_funcall(wrapper->callback_object, Ion_message_complete, 0);
} else if (wrapper->on_message_complete != Qnil) {
ret = rb_funcall(wrapper->on_message_complete, Icall, 0);
}
if (ret == Sstop) {
wrapper->stopped = Qtrue;
return -1;
} else {
return 0;
}
}
static ryah_http_parser_settings settings = {
.on_message_begin = on_message_begin,
.on_status = on_status,
.on_url = on_url,
.on_header_field = on_header_field,
.on_header_value = on_header_value,
.on_headers_complete = on_headers_complete,
.on_body = on_body,
.on_message_complete = on_message_complete
};
VALUE Parser_alloc_by_type(VALUE klass, enum ryah_http_parser_type type) {
ParserWrapper *wrapper = ALLOC_N(ParserWrapper, 1);
wrapper->type = type;
wrapper->parser.data = wrapper;
wrapper->on_message_begin = Qnil;
wrapper->on_headers_complete = Qnil;
wrapper->on_body = Qnil;
wrapper->on_message_complete = Qnil;
wrapper->callback_object = Qnil;
ParserWrapper_init(wrapper);
return Data_Wrap_Struct(klass, ParserWrapper_mark, ParserWrapper_free, wrapper);
}
VALUE Parser_alloc(VALUE klass) {
return Parser_alloc_by_type(klass, HTTP_BOTH);
}
VALUE RequestParser_alloc(VALUE klass) {
return Parser_alloc_by_type(klass, HTTP_REQUEST);
}
VALUE ResponseParser_alloc(VALUE klass) {
return Parser_alloc_by_type(klass, HTTP_RESPONSE);
}
VALUE Parser_strict_p(VALUE klass) {
return HTTP_PARSER_STRICT == 1 ? Qtrue : Qfalse;
}
VALUE Parser_initialize(int argc, VALUE *argv, VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
VALUE default_header_value_type = Qnil;
if (argc > 0 && RB_TYPE_P(argv[argc-1], T_HASH)) {
ID keyword_ids[1];
keyword_ids[0] = rb_intern("default_header_value_type");
rb_get_kwargs(argv[argc-1], keyword_ids, 0, 1, &default_header_value_type);
if (default_header_value_type == Qundef) {
default_header_value_type = Qnil;
}
--argc;
}
if (argc == 1) {
wrapper->callback_object = argv[0];
}
if (argc == 2) {
wrapper->callback_object = argv[0];
default_header_value_type = argv[1];
}
if (default_header_value_type == Qnil) {
wrapper->header_value_type = rb_iv_get(CLASS_OF(self), "@default_header_value_type");
} else {
wrapper->header_value_type = default_header_value_type;
}
return self;
}
VALUE Parser_execute(VALUE self, VALUE data) {
ParserWrapper *wrapper = NULL;
Check_Type(data, T_STRING);
char *ptr = RSTRING_PTR(data);
long len = RSTRING_LEN(data);
DATA_GET(self, ParserWrapper, wrapper);
wrapper->stopped = Qfalse;
size_t nparsed = ryah_http_parser_execute(&wrapper->parser, &settings, ptr, len);
if (wrapper->parser.upgrade) {
if (RTEST(wrapper->stopped) && !RTEST(wrapper->completed))
nparsed += 1;
if (nparsed < len)
rb_str_cat(wrapper->upgrade_data, ptr + nparsed, len - nparsed);
} else if (nparsed != (size_t)len) {
if (!RTEST(wrapper->stopped) && !RTEST(wrapper->completed))
rb_raise(eParserError, "Could not parse data entirely (%zu != %zu)", nparsed, len);
else
nparsed += 1; // error states fail on the current character
}
return INT2FIX(nparsed);
}
VALUE Parser_set_on_message_begin(VALUE self, VALUE callback) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->on_message_begin = callback;
return callback;
}
VALUE Parser_set_on_headers_complete(VALUE self, VALUE callback) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->on_headers_complete = callback;
return callback;
}
VALUE Parser_set_on_body(VALUE self, VALUE callback) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->on_body = callback;
return callback;
}
VALUE Parser_set_on_message_complete(VALUE self, VALUE callback) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->on_message_complete = callback;
return callback;
}
VALUE Parser_keep_alive_p(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
return http_should_keep_alive(&wrapper->parser) == 1 ? Qtrue : Qfalse;
}
VALUE Parser_upgrade_p(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
return wrapper->parser.upgrade ? Qtrue : Qfalse;
}
VALUE Parser_http_version(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.http_major == 0 && wrapper->parser.http_minor == 0)
return Qnil;
else
return rb_ary_new3(2, INT2FIX(wrapper->parser.http_major), INT2FIX(wrapper->parser.http_minor));
}
VALUE Parser_http_major(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.http_major == 0 && wrapper->parser.http_minor == 0)
return Qnil;
else
return INT2FIX(wrapper->parser.http_major);
}
VALUE Parser_http_minor(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.http_major == 0 && wrapper->parser.http_minor == 0)
return Qnil;
else
return INT2FIX(wrapper->parser.http_minor);
}
VALUE Parser_http_method(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.type == HTTP_REQUEST)
return rb_str_new2(http_method_str(wrapper->parser.method));
else
return Qnil;
}
VALUE Parser_status_code(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.status_code)
return INT2FIX(wrapper->parser.status_code);
else
return Qnil;
}
#define DEFINE_GETTER(name) \
VALUE Parser_##name(VALUE self) { \
ParserWrapper *wrapper = NULL; \
DATA_GET(self, ParserWrapper, wrapper); \
return wrapper->name; \
}
DEFINE_GETTER(status);
DEFINE_GETTER(request_url);
DEFINE_GETTER(headers);
DEFINE_GETTER(upgrade_data);
DEFINE_GETTER(header_value_type);
VALUE Parser_set_header_value_type(VALUE self, VALUE val) {
if (val != Sarrays && val != Sstrings && val != Smixed) {
rb_raise(rb_eArgError, "Invalid header value type");
}
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->header_value_type = val;
return wrapper->header_value_type;
}
VALUE Parser_reset(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
ParserWrapper_init(wrapper);
return Qtrue;
}
void Init_ruby_http_parser() {
#ifdef HAVE_RB_EXT_RACTOR_SAFE
rb_ext_ractor_safe(true);
#endif
VALUE mHTTP = rb_define_module("HTTP");
cParser = rb_define_class_under(mHTTP, "Parser", rb_cObject);
cRequestParser = rb_define_class_under(mHTTP, "RequestParser", cParser);
cResponseParser = rb_define_class_under(mHTTP, "ResponseParser", cParser);
eParserError = rb_define_class_under(cParser, "Error", rb_eIOError);
Icall = rb_intern("call");
Ion_message_begin = rb_intern("on_message_begin");
Ion_headers_complete = rb_intern("on_headers_complete");
Ion_body = rb_intern("on_body");
Ion_message_complete = rb_intern("on_message_complete");
Sstop = ID2SYM(rb_intern("stop"));
Sreset = ID2SYM(rb_intern("reset"));
Sarrays = ID2SYM(rb_intern("arrays"));
Sstrings = ID2SYM(rb_intern("strings"));
Smixed = ID2SYM(rb_intern("mixed"));
rb_define_alloc_func(cParser, Parser_alloc);
rb_define_alloc_func(cRequestParser, RequestParser_alloc);
rb_define_alloc_func(cResponseParser, ResponseParser_alloc);
rb_define_singleton_method(cParser, "strict?", Parser_strict_p, 0);
rb_define_method(cParser, "initialize", Parser_initialize, -1);
rb_define_method(cParser, "on_message_begin=", Parser_set_on_message_begin, 1);
rb_define_method(cParser, "on_headers_complete=", Parser_set_on_headers_complete, 1);
rb_define_method(cParser, "on_body=", Parser_set_on_body, 1);
rb_define_method(cParser, "on_message_complete=", Parser_set_on_message_complete, 1);
rb_define_method(cParser, "<<", Parser_execute, 1);
rb_define_method(cParser, "keep_alive?", Parser_keep_alive_p, 0);
rb_define_method(cParser, "upgrade?", Parser_upgrade_p, 0);
rb_define_method(cParser, "http_version", Parser_http_version, 0);
rb_define_method(cParser, "http_major", Parser_http_major, 0);
rb_define_method(cParser, "http_minor", Parser_http_minor, 0);
rb_define_method(cParser, "http_method", Parser_http_method, 0);
rb_define_method(cParser, "status_code", Parser_status_code, 0);
rb_define_method(cParser, "status", Parser_status, 0);
rb_define_method(cParser, "request_url", Parser_request_url, 0);
rb_define_method(cParser, "headers", Parser_headers, 0);
rb_define_method(cParser, "upgrade_data", Parser_upgrade_data, 0);
rb_define_method(cParser, "header_value_type", Parser_header_value_type, 0);
rb_define_method(cParser, "header_value_type=", Parser_set_header_value_type, 1);
rb_define_method(cParser, "reset!", Parser_reset, 0);
}
@@ -0,0 +1,32 @@
Gem::Specification.new do |s|
s.name = "http_parser.rb"
s.version = "0.8.0"
s.summary = "Simple callback-based HTTP request/response parser"
s.description = "Ruby bindings to https://github.com/joyent/http-parser and https://github.com/http-parser/http-parser.java"
s.authors = ["Marc-Andre Cournoyer", "Aman Gupta"]
s.email = ["macournoyer@gmail.com", "aman@tmm1.net"]
s.license = 'MIT'
s.homepage = "https://github.com/tmm1/http_parser.rb"
s.files = `git ls-files`.split("\n") + Dir['ext/ruby_http_parser/vendor/**/*']
s.require_paths = ["lib"]
s.extensions = ["ext/ruby_http_parser/extconf.rb"]
s.add_development_dependency 'rake-compiler', '~> 1.0'
s.add_development_dependency 'rspec', '~> 3'
s.add_development_dependency 'json', '~> 2.1'
s.add_development_dependency 'benchmark_suite', '~> 1.0'
s.add_development_dependency 'ffi', '~> 1.9'
if RUBY_PLATFORM =~ /java/
s.add_development_dependency 'jruby-openssl'
else
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.4.0')
s.add_development_dependency 'yajl-ruby', '~> 1.3'
else
s.add_development_dependency 'yajl-ruby', '= 1.2.1'
end
end
end
@@ -0,0 +1 @@
require 'http_parser'
@@ -0,0 +1,21 @@
$:.unshift File.expand_path('../', __FILE__)
require 'ruby_http_parser'
Http = HTTP
module HTTP
class Parser
class << self
attr_reader :default_header_value_type
def default_header_value_type=(val)
if (val != :mixed && val != :strings && val != :arrays)
raise ArgumentError, "Invalid header value type"
end
@default_header_value_type = val
end
end
end
end
HTTP::Parser.default_header_value_type = :mixed
@@ -0,0 +1,428 @@
if defined?(Encoding)
Encoding.default_external = "UTF-8"
end
require "spec_helper"
require "json"
describe HTTP::Parser do
before do
@parser = HTTP::Parser.new
@headers = nil
@body = ""
@started = false
@done = false
@parser.on_message_begin = proc{ @started = true }
@parser.on_headers_complete = proc { |e| @headers = e }
@parser.on_body = proc { |chunk| @body << chunk }
@parser.on_message_complete = proc{ @done = true }
end
it "should have initial state" do
expect(@parser.headers).to be_nil
expect(@parser.http_version).to be_nil
expect(@parser.http_method).to be_nil
expect(@parser.status_code).to be_nil
expect(@parser.request_url).to be_nil
expect(@parser.header_value_type).to eq(:mixed)
end
it "should be able to run in non-main ractors" do
skip unless Kernel.const_defined?(:Ractor)
default_header_value_type = HTTP::Parser.default_header_value_type
r = Ractor.new(default_header_value_type) { |type|
parser = HTTP::Parser.new(default_header_value_type: type)
done = false
parser.on_message_complete = proc {
done = true
}
parser <<
"GET /ractor HTTP/1.1\r\n" +
"Content-Length: 5\r\n" +
"\r\n" +
"World"
done
}
expect(r.take).to be true
end
it "should allow us to set the header value type" do
[:mixed, :arrays, :strings].each do |type|
@parser.header_value_type = type
expect(@parser.header_value_type).to eq(type)
parser_tmp = HTTP::Parser.new(nil, type)
expect(parser_tmp.header_value_type).to eq(type)
parser_tmp2 = HTTP::Parser.new(default_header_value_type: type)
expect(parser_tmp2.header_value_type).to eq(type)
end
end
it "should allow us to set the default header value type" do
[:mixed, :arrays, :strings].each do |type|
HTTP::Parser.default_header_value_type = type
parser = HTTP::Parser.new
expect(parser.header_value_type).to eq(type)
end
end
it "should throw an Argument Error if header value type is invalid" do
expect{ @parser.header_value_type = 'bob' }.to raise_error(ArgumentError)
end
it "should throw an Argument Error if default header value type is invalid" do
expect{ HTTP::Parser.default_header_value_type = 'bob' }.to raise_error(ArgumentError)
end
it "should implement basic api" do
@parser <<
"GET /test?ok=1 HTTP/1.1\r\n" +
"User-Agent: curl/7.18.0\r\n" +
"Host: 0.0.0.0:5000\r\n" +
"Accept: */*\r\n" +
"Content-Length: 5\r\n" +
"\r\n" +
"World"
expect(@started).to be true
expect(@done).to be true
expect(@parser.http_major).to eq(1)
expect(@parser.http_minor).to eq(1)
expect(@parser.http_version).to eq([1,1])
expect(@parser.http_method).to eq('GET')
expect(@parser.status_code).to be_nil
expect(@parser.request_url).to eq('/test?ok=1')
expect(@parser.headers).to eq(@headers)
expect(@parser.headers['User-Agent']).to eq('curl/7.18.0')
expect(@parser.headers['Host']).to eq('0.0.0.0:5000')
expect(@body).to eq("World")
end
it "should raise errors on invalid data" do
expect{ @parser << "BLAH" }.to raise_error(HTTP::Parser::Error)
end
it "should abort parser via header complete callback with a body" do
@parser.on_headers_complete = proc { |e| @headers = e; :stop }
data =
"GET / HTTP/1.0\r\n" +
"Content-Length: 5\r\n" +
"\r\n" +
"World"
bytes = @parser << data
expect(bytes).to eq(37)
expect(data[bytes..-1]).to eq('World')
expect(@headers).to eq({'Content-Length' => '5'})
expect(@body).to be_empty
expect(@done).to be false
end
it "should abort parser via header complete callback without a body" do
@parser.on_headers_complete = proc { |e| @headers = e; :stop }
data =
"GET / HTTP/1.0\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
bytes = @parser << data
expect(bytes).to eq(37)
expect(data[bytes..-1]).to eq('')
expect(@headers).to eq({'Content-Length' => '0'})
expect(@body).to be_empty
expect(@done).to be false
end
it "should abort parser via message complete callback with a body" do
@parser.on_message_complete = proc { :stop }
data =
"CONNECT www.example.com:443 HTTP/1.0\r\n" +
"Connection: keep-alive\r\n" +
"\r\n" +
"World"
bytes = @parser << data
expect(bytes).to eq(64)
expect(data[bytes..-1]).to eq('World')
expect(@headers).to eq({'Connection' => 'keep-alive'})
expect(@parser.upgrade_data).to eq('World')
expect(@body).to be_empty
expect(@done).to be false
end
it "should abort parser via message complete callback without a body" do
@parser.on_message_complete = proc { :stop }
data =
"CONNECT www.example.com:443 HTTP/1.0\r\n" +
"Connection: keep-alive\r\n" +
"\r\n"
bytes = @parser << data
expect(bytes).to eq(64)
expect(data[bytes..-1]).to eq('')
expect(@headers).to eq({'Connection' => 'keep-alive'})
expect(@parser.upgrade_data).to eq('')
expect(@body).to be_empty
expect(@done).to be false
end
it "should reset to initial state" do
@parser << "GET / HTTP/1.0\r\n\r\n"
expect(@parser.http_method).to eq('GET')
expect(@parser.http_version).to eq([1,0])
expect(@parser.request_url).to eq('/')
expect(@parser.reset!).to be true
expect(@parser.http_version).to be_nil
expect(@parser.http_method).to be_nil
expect(@parser.status_code).to be_nil
expect(@parser.request_url).to be_nil
end
it "should optionally reset parser state on no-body responses" do
expect(@parser.reset!).to be true
@head, @complete = 0, 0
@parser.on_headers_complete = proc {|h| @head += 1; :reset }
@parser.on_message_complete = proc { @complete += 1 }
@parser.on_body = proc {|b| fail }
head_response = "HTTP/1.1 200 OK\r\nContent-Length:10\r\n\r\n"
@parser << head_response
expect(@head).to eq(1)
expect(@complete).to eq(1)
@parser << head_response
expect(@head).to eq(2)
expect(@complete).to eq(2)
end
it "should retain callbacks after reset" do
expect(@parser.reset!).to be true
@parser << "GET / HTTP/1.0\r\n\r\n"
expect(@started).to be true
expect(@headers).to eq({})
expect(@done).to be true
end
it "should parse headers incrementally" do
request =
"GET / HTTP/1.0\r\n" +
"Header1: value 1\r\n" +
"Header2: value 2\r\n" +
"\r\n"
while chunk = request.slice!(0,2) and !chunk.empty?
@parser << chunk
end
expect(@parser.headers).to eq({
'Header1' => 'value 1',
'Header2' => 'value 2'
})
end
it "should handle multiple headers using strings" do
@parser.header_value_type = :strings
@parser <<
"GET / HTTP/1.0\r\n" +
"Set-Cookie: PREF=ID=a7d2c98; expires=Fri, 05-Apr-2013 05:00:45 GMT; path=/; domain=.bob.com\r\n" +
"Set-Cookie: NID=46jSHxPM; path=/; domain=.bob.com; HttpOnly\r\n" +
"\r\n"
expect(@parser.headers["Set-Cookie"]).to eq("PREF=ID=a7d2c98; expires=Fri, 05-Apr-2013 05:00:45 GMT; path=/; domain=.bob.com, NID=46jSHxPM; path=/; domain=.bob.com; HttpOnly")
end
it "should handle multiple headers using strings" do
@parser.header_value_type = :arrays
@parser <<
"GET / HTTP/1.0\r\n" +
"Set-Cookie: PREF=ID=a7d2c98; expires=Fri, 05-Apr-2013 05:00:45 GMT; path=/; domain=.bob.com\r\n" +
"Set-Cookie: NID=46jSHxPM; path=/; domain=.bob.com; HttpOnly\r\n" +
"\r\n"
expect(@parser.headers["Set-Cookie"]).to eq([
"PREF=ID=a7d2c98; expires=Fri, 05-Apr-2013 05:00:45 GMT; path=/; domain=.bob.com",
"NID=46jSHxPM; path=/; domain=.bob.com; HttpOnly"
])
end
it "should handle multiple headers using mixed" do
@parser.header_value_type = :mixed
@parser <<
"GET / HTTP/1.0\r\n" +
"Set-Cookie: PREF=ID=a7d2c98; expires=Fri, 05-Apr-2013 05:00:45 GMT; path=/; domain=.bob.com\r\n" +
"Set-Cookie: NID=46jSHxPM; path=/; domain=.bob.com; HttpOnly\r\n" +
"\r\n"
expect(@parser.headers["Set-Cookie"]).to eq([
"PREF=ID=a7d2c98; expires=Fri, 05-Apr-2013 05:00:45 GMT; path=/; domain=.bob.com",
"NID=46jSHxPM; path=/; domain=.bob.com; HttpOnly"
])
end
it "should handle a single cookie using mixed" do
@parser.header_value_type = :mixed
@parser <<
"GET / HTTP/1.0\r\n" +
"Set-Cookie: PREF=ID=a7d2c98; expires=Fri, 05-Apr-2013 05:00:45 GMT; path=/; domain=.bob.com\r\n" +
"\r\n"
expect(@parser.headers["Set-Cookie"]).to eq("PREF=ID=a7d2c98; expires=Fri, 05-Apr-2013 05:00:45 GMT; path=/; domain=.bob.com")
end
it "should support alternative api" do
callbacks = double('callbacks')
allow(callbacks).to receive(:on_message_begin){ @started = true }
allow(callbacks).to receive(:on_headers_complete){ |e| @headers = e }
allow(callbacks).to receive(:on_body){ |chunk| @body << chunk }
allow(callbacks).to receive(:on_message_complete){ @done = true }
@parser = HTTP::Parser.new(callbacks)
@parser << "GET / HTTP/1.0\r\n\r\n"
expect(@started).to be true
expect(@headers).to eq({})
expect(@body).to eq('')
expect(@done).to be true
end
it "should ignore extra content beyond specified length" do
@parser <<
"GET / HTTP/1.0\r\n" +
"Content-Length: 5\r\n" +
"\r\n" +
"hello" +
" \n"
expect(@body).to eq('hello')
expect(@done).to be true
end
it 'sets upgrade_data if available' do
@parser <<
"GET /demo HTTP/1.1\r\n" +
"Connection: Upgrade\r\n" +
"Upgrade: WebSocket\r\n\r\n" +
"third key data"
expect(@parser.upgrade?).to be true
expect(@parser.upgrade_data).to eq('third key data')
end
it 'sets upgrade_data to blank if un-available' do
@parser <<
"GET /demo HTTP/1.1\r\n" +
"Connection: Upgrade\r\n" +
"Upgrade: WebSocket\r\n\r\n"
expect(@parser.upgrade?).to be true
expect(@parser.upgrade_data).to eq('')
end
it 'should stop parsing headers when instructed' do
request = "GET /websocket HTTP/1.1\r\n" +
"host: localhost\r\n" +
"connection: Upgrade\r\n" +
"upgrade: websocket\r\n" +
"sec-websocket-key: SD6/hpYbKjQ6Sown7pBbWQ==\r\n" +
"sec-websocket-version: 13\r\n" +
"\r\n"
@parser.on_headers_complete = proc { |e| :stop }
offset = (@parser << request)
expect(@parser.upgrade?).to be true
expect(@parser.upgrade_data).to eq('')
expect(offset).to eq(request.length)
end
it "should execute on_body on requests with no content-length" do
expect(@parser.reset!).to be true
@head, @complete, @body = 0, 0, 0
@parser.on_headers_complete = proc {|h| @head += 1 }
@parser.on_message_complete = proc { @complete += 1 }
@parser.on_body = proc {|b| @body += 1 }
head_response = "HTTP/1.1 200 OK\r\n\r\nstuff"
@parser << head_response
@parser << ''
expect(@head).to eq(1)
expect(@complete).to eq(1)
expect(@body).to eq(1)
end
%w[ request response ].each do |type|
JSON.parse(File.read(File.expand_path("../support/#{type}s.json", __FILE__))).each do |test|
test['headers'] ||= {}
next if !defined?(JRUBY_VERSION) and HTTP::Parser.strict? != test['strict']
it "should parse #{type}: #{test['name']}" do
@parser << test['raw']
expect(@parser.http_method).to eq(test['method'])
expect(@parser.keep_alive?).to eq(test['should_keep_alive'])
if test.has_key?('upgrade') and test['upgrade'] != 0
expect(@parser.upgrade?).to be true
expect(@parser.upgrade_data).to eq(test['upgrade'])
end
expect(@parser.send("http_major")).to eq(test["http_major"])
expect(@parser.send("http_minor")).to eq(test["http_minor"])
if test['type'] == 'HTTP_REQUEST'
if defined?(JRUBY_VERSION)
expect(@parser.send("request_url")).to eq(test["request_url"])
else
# It's created by rb_str_new(), so that encoding is Encoding::ASCII_8BIT a.k.a Encoding::BINARY
expect(@parser.send("request_url")).to eq(test["request_url"].force_encoding(Encoding::ASCII_8BIT))
end
else
expect(@parser.send("status_code")).to eq(test["status_code"])
expect(@parser.send("status")).to eq(test["status"].force_encoding(Encoding::ASCII_8BIT)) if !defined?(JRUBY_VERSION)
end
expect(@headers.size).to eq(test['num_headers'])
expect(@headers).to eq(test['headers'])
expect(@body).to eq(test['body'])
expect(@body.size).to eq(test['body_size']) if test['body_size']
end
end
end
end
@@ -0,0 +1 @@
require "http_parser"
@@ -0,0 +1,612 @@
[
{
"name": "curl get",
"type": "HTTP_REQUEST",
"raw": "GET /test HTTP/1.1\r\nUser-Agent: curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1\r\nHost: 0.0.0.0=5000\r\nAccept: */*\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/test",
"request_url": "/test",
"num_headers": 3,
"headers": {
"User-Agent": "curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1",
"Host": "0.0.0.0=5000",
"Accept": "*/*"
},
"body": "",
"strict": true
},
{
"name": "firefox get",
"type": "HTTP_REQUEST",
"raw": "GET /favicon.ico HTTP/1.1\r\nHost: 0.0.0.0=5000\r\nUser-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-us,en;q=0.5\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nConnection: keep-alive\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/favicon.ico",
"request_url": "/favicon.ico",
"num_headers": 8,
"headers": {
"Host": "0.0.0.0=5000",
"User-Agent": "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-us,en;q=0.5",
"Accept-Encoding": "gzip,deflate",
"Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
"Keep-Alive": "300",
"Connection": "keep-alive"
},
"body": "",
"strict": true
},
{
"name": "dumbfuck",
"type": "HTTP_REQUEST",
"raw": "GET /dumbfuck HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/dumbfuck",
"request_url": "/dumbfuck",
"num_headers": 1,
"headers": {
"aaaaaaaaaaaaa": "++++++++++"
},
"body": "",
"strict": true
},
{
"name": "fragment in url",
"type": "HTTP_REQUEST",
"raw": "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "page=1",
"fragment": "posts-17408",
"request_path": "/forums/1/topics/2375",
"request_url": "/forums/1/topics/2375?page=1#posts-17408",
"num_headers": 0,
"body": "",
"strict": true
},
{
"name": "get no headers no body",
"type": "HTTP_REQUEST",
"raw": "GET /get_no_headers_no_body/world HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/get_no_headers_no_body/world",
"request_url": "/get_no_headers_no_body/world",
"num_headers": 0,
"body": "",
"strict": true
},
{
"name": "get one header no body",
"type": "HTTP_REQUEST",
"raw": "GET /get_one_header_no_body HTTP/1.1\r\nAccept: */*\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/get_one_header_no_body",
"request_url": "/get_one_header_no_body",
"num_headers": 1,
"headers": {
"Accept": "*/*"
},
"body": "",
"strict": true
},
{
"name": "get funky content length body hello",
"type": "HTTP_REQUEST",
"raw": "GET /get_funky_content_length_body_hello HTTP/1.0\r\nconTENT-Length: 5\r\n\r\nHELLO",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/get_funky_content_length_body_hello",
"request_url": "/get_funky_content_length_body_hello",
"num_headers": 1,
"headers": {
"conTENT-Length": "5"
},
"body": "HELLO",
"strict": true
},
{
"name": "post identity body world",
"type": "HTTP_REQUEST",
"raw": "POST /post_identity_body_world?q=search#hey HTTP/1.1\r\nAccept: */*\r\nTransfer-Encoding: identity\r\nContent-Length: 5\r\n\r\nWorld",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "POST",
"query_string": "q=search",
"fragment": "hey",
"request_path": "/post_identity_body_world",
"request_url": "/post_identity_body_world?q=search#hey",
"num_headers": 3,
"headers": {
"Accept": "*/*",
"Transfer-Encoding": "identity",
"Content-Length": "5"
},
"body": "World",
"strict": true
},
{
"name": "post - chunked body: all your base are belong to us",
"type": "HTTP_REQUEST",
"raw": "POST /post_chunked_all_your_base HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n1e\r\nall your base are belong to us\r\n0\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "POST",
"query_string": "",
"fragment": "",
"request_path": "/post_chunked_all_your_base",
"request_url": "/post_chunked_all_your_base",
"num_headers": 1,
"headers": {
"Transfer-Encoding": "chunked"
},
"body": "all your base are belong to us",
"strict": true
},
{
"name": "two chunks ; triple zero ending",
"type": "HTTP_REQUEST",
"raw": "POST /two_chunks_mult_zero_end HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n000\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "POST",
"query_string": "",
"fragment": "",
"request_path": "/two_chunks_mult_zero_end",
"request_url": "/two_chunks_mult_zero_end",
"num_headers": 1,
"headers": {
"Transfer-Encoding": "chunked"
},
"body": "hello world",
"strict": true
},
{
"name": "chunked with trailing headers. blech.",
"type": "HTTP_REQUEST",
"raw": "POST /chunked_w_trailing_headers HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\nVary: *\r\nContent-Type: text/plain\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "POST",
"query_string": "",
"fragment": "",
"request_path": "/chunked_w_trailing_headers",
"request_url": "/chunked_w_trailing_headers",
"num_headers": 3,
"headers": {
"Transfer-Encoding": "chunked",
"Vary": "*",
"Content-Type": "text/plain"
},
"body": "hello world",
"strict": true
},
{
"name": "with bullshit after the length",
"type": "HTTP_REQUEST",
"raw": "POST /chunked_w_bullshit_after_length HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5; ihatew3;whatthefuck=aretheseparametersfor\r\nhello\r\n6; blahblah; blah\r\n world\r\n0\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "POST",
"query_string": "",
"fragment": "",
"request_path": "/chunked_w_bullshit_after_length",
"request_url": "/chunked_w_bullshit_after_length",
"num_headers": 1,
"headers": {
"Transfer-Encoding": "chunked"
},
"body": "hello world",
"strict": true
},
{
"name": "with quotes",
"type": "HTTP_REQUEST",
"raw": "GET /with_\"stupid\"_quotes?foo=\"bar\" HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "foo=\"bar\"",
"fragment": "",
"request_path": "/with_\"stupid\"_quotes",
"request_url": "/with_\"stupid\"_quotes?foo=\"bar\"",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "apachebench get",
"type": "HTTP_REQUEST",
"raw": "GET /test HTTP/1.0\r\nHost: 0.0.0.0:5000\r\nUser-Agent: ApacheBench/2.3\r\nAccept: */*\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/test",
"request_url": "/test",
"num_headers": 3,
"headers": {
"Host": "0.0.0.0:5000",
"User-Agent": "ApacheBench/2.3",
"Accept": "*/*"
},
"body": "",
"strict": true
},
{
"name": "query url with question mark",
"type": "HTTP_REQUEST",
"raw": "GET /test.cgi?foo=bar?baz HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "foo=bar?baz",
"fragment": "",
"request_path": "/test.cgi",
"request_url": "/test.cgi?foo=bar?baz",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "newline prefix get",
"type": "HTTP_REQUEST",
"raw": "\r\nGET /test HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/test",
"request_url": "/test",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "upgrade request",
"type": "HTTP_REQUEST",
"raw": "GET /demo HTTP/1.1\r\nHost: example.com\r\nConnection: Upgrade\r\nSec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\nSec-WebSocket-Protocol: sample\r\nUpgrade: WebSocket\r\nSec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\nOrigin: http://example.com\r\n\r\nHot diggity dogg",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/demo",
"request_url": "/demo",
"num_headers": 7,
"upgrade": "Hot diggity dogg",
"headers": {
"Host": "example.com",
"Connection": "Upgrade",
"Sec-WebSocket-Key2": "12998 5 Y3 1 .P00",
"Sec-WebSocket-Protocol": "sample",
"Upgrade": "WebSocket",
"Sec-WebSocket-Key1": "4 @1 46546xW%0l 1 5",
"Origin": "http://example.com"
},
"body": "",
"strict": true
},
{
"name": "connect request",
"type": "HTTP_REQUEST",
"raw": "CONNECT 0-home0.netscape.com:443 HTTP/1.0\r\nUser-agent: Mozilla/1.1N\r\nProxy-authorization: basic aGVsbG86d29ybGQ=\r\n\r\nsome data\r\nand yet even more data",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"method": "CONNECT",
"query_string": "",
"fragment": "",
"request_path": "",
"request_url": "0-home0.netscape.com:443",
"num_headers": 2,
"upgrade": "some data\r\nand yet even more data",
"headers": {
"User-agent": "Mozilla/1.1N",
"Proxy-authorization": "basic aGVsbG86d29ybGQ="
},
"body": "",
"strict": true
},
{
"name": "report request",
"type": "HTTP_REQUEST",
"raw": "REPORT /test HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "REPORT",
"query_string": "",
"fragment": "",
"request_path": "/test",
"request_url": "/test",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "request with no http version",
"type": "HTTP_REQUEST",
"raw": "GET /\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 0,
"http_minor": 9,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/",
"request_url": "/",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "m-search request",
"type": "HTTP_REQUEST",
"raw": "M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: \"ssdp:discover\"\r\nST: \"ssdp:all\"\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "M-SEARCH",
"query_string": "",
"fragment": "",
"request_path": "*",
"request_url": "*",
"num_headers": 3,
"headers": {
"HOST": "239.255.255.250:1900",
"MAN": "\"ssdp:discover\"",
"ST": "\"ssdp:all\""
},
"body": "",
"strict": true
},
{
"name": "line folding in header value",
"type": "HTTP_REQUEST",
"raw": "GET / HTTP/1.1\r\nLine1: abc\r\n\tdef\r\n ghi\r\n\t\tjkl\r\n mno \r\n\t \tqrs\r\nLine2: \t line2\t\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "/",
"request_url": "/",
"num_headers": 2,
"headers": {
"Line1": "abc\tdef ghi\t\tjkl mno \t \tqrs",
"Line2": "line2\t"
},
"body": "",
"strict": true
},
{
"name": "host terminated by a query string",
"type": "HTTP_REQUEST",
"raw": "GET http://hypnotoad.org?hail=all HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "hail=all",
"fragment": "",
"request_path": "",
"request_url": "http://hypnotoad.org?hail=all",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "host:port terminated by a query string",
"type": "HTTP_REQUEST",
"raw": "GET http://hypnotoad.org:1234?hail=all HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "hail=all",
"fragment": "",
"request_path": "",
"request_url": "http://hypnotoad.org:1234?hail=all",
"port": 1234,
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "host:port terminated by a space",
"type": "HTTP_REQUEST",
"raw": "GET http://hypnotoad.org:1234 HTTP/1.1\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "",
"fragment": "",
"request_path": "",
"request_url": "http://hypnotoad.org:1234",
"port": 1234,
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "PATCH request",
"type": "HTTP_REQUEST",
"raw": "PATCH /file.txt HTTP/1.1\r\nHost: www.example.com\r\nContent-Type: application/example\r\nIf-Match: \"e0023aa4e\"\r\nContent-Length: 10\r\n\r\ncccccccccc",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "PATCH",
"query_string": "",
"fragment": "",
"request_path": "/file.txt",
"request_url": "/file.txt",
"num_headers": 4,
"headers": {
"Host": "www.example.com",
"Content-Type": "application/example",
"If-Match": "\"e0023aa4e\"",
"Content-Length": "10"
},
"body": "cccccccccc",
"strict": true
},
{
"name": "connect caps request",
"type": "HTTP_REQUEST",
"raw": "CONNECT HOME0.NETSCAPE.COM:443 HTTP/1.0\r\nUser-agent: Mozilla/1.1N\r\nProxy-authorization: basic aGVsbG86d29ybGQ=\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"method": "CONNECT",
"query_string": "",
"fragment": "",
"request_path": "",
"request_url": "HOME0.NETSCAPE.COM:443",
"num_headers": 2,
"upgrade": "",
"headers": {
"User-agent": "Mozilla/1.1N",
"Proxy-authorization": "basic aGVsbG86d29ybGQ="
},
"body": "",
"strict": true
},
{
"name": "utf-8 path request",
"type": "HTTP_REQUEST",
"strict": false,
"raw": "GET /δ¶/δt/pope?q=1#narf HTTP/1.1\r\nHost: github.com\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"method": "GET",
"query_string": "q=1",
"fragment": "narf",
"request_path": "/δ¶/δt/pope",
"request_url": "/δ¶/δt/pope?q=1#narf",
"num_headers": 1,
"headers": {
"Host": "github.com"
},
"body": ""
},
{
"name": "hostname underscore",
"type": "HTTP_REQUEST",
"strict": false,
"raw": "CONNECT home_0.netscape.com:443 HTTP/1.0\r\nUser-agent: Mozilla/1.1N\r\nProxy-authorization: basic aGVsbG86d29ybGQ=\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"method": "CONNECT",
"query_string": "",
"fragment": "",
"request_path": "",
"request_url": "home_0.netscape.com:443",
"num_headers": 2,
"upgrade": "",
"headers": {
"User-agent": "Mozilla/1.1N",
"Proxy-authorization": "basic aGVsbG86d29ybGQ="
},
"body": ""
}
]
@@ -0,0 +1,395 @@
[
{
"name": "google 301",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.google.com/\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Sun, 26 Apr 2009 11:11:49 GMT\r\nExpires: Tue, 26 May 2009 11:11:49 GMT\r\nX-$PrototypeBI-Version: 1.6.0.3\r\nCache-Control: public, max-age=2592000\r\nServer: gws\r\nContent-Length: 219 \r\n\r\n<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF=\"http://www.google.com/\">here</A>.\r\n</BODY></HTML>\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 301,
"status": "Moved Permanently",
"num_headers": 8,
"headers": {
"Location": "http://www.google.com/",
"Content-Type": "text/html; charset=UTF-8",
"Date": "Sun, 26 Apr 2009 11:11:49 GMT",
"Expires": "Tue, 26 May 2009 11:11:49 GMT",
"X-$PrototypeBI-Version": "1.6.0.3",
"Cache-Control": "public, max-age=2592000",
"Server": "gws",
"Content-Length": "219 "
},
"body": "<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF=\"http://www.google.com/\">here</A>.\r\n</BODY></HTML>\r\n",
"strict": true
},
{
"name": "no content-length response",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nDate: Tue, 04 Aug 2009 07:59:32 GMT\r\nServer: Apache\r\nX-Powered-By: Servlet/2.5 JSP/2.1\r\nContent-Type: text/xml; charset=utf-8\r\nConnection: close\r\n\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <SOAP-ENV:Body>\n <SOAP-ENV:Fault>\n <faultcode>SOAP-ENV:Client</faultcode>\n <faultstring>Client Error</faultstring>\n </SOAP-ENV:Fault>\n </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 5,
"headers": {
"Date": "Tue, 04 Aug 2009 07:59:32 GMT",
"Server": "Apache",
"X-Powered-By": "Servlet/2.5 JSP/2.1",
"Content-Type": "text/xml; charset=utf-8",
"Connection": "close"
},
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <SOAP-ENV:Body>\n <SOAP-ENV:Fault>\n <faultcode>SOAP-ENV:Client</faultcode>\n <faultstring>Client Error</faultstring>\n </SOAP-ENV:Fault>\n </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>",
"strict": true
},
{
"name": "404 no headers no body",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 404 Not Found\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 404,
"status": "Not Found",
"num_headers": 0,
"headers": {
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "301 no response phrase",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 301\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 301,
"status": "",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "200 trailing space on chunked body",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n25 \r\nThis is the data in the first chunk\r\n\r\n1C\r\nand this is the second one\r\n\r\n0 \r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 2,
"headers": {
"Content-Type": "text/plain",
"Transfer-Encoding": "chunked"
},
"body_size": 65,
"body": "This is the data in the first chunk\r\nand this is the second one\r\n",
"strict": true
},
{
"name": "no carriage ret",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\nConnection: close\n\nthese headers are from http://news.ycombinator.com/",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 2,
"headers": {
"Content-Type": "text/html; charset=utf-8",
"Connection": "close"
},
"body": "these headers are from http://news.ycombinator.com/",
"strict": true
},
{
"name": "proxy connection",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: 11\r\nProxy-Connection: close\r\nDate: Thu, 31 Dec 2009 20:55:48 +0000\r\n\r\nhello world",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 4,
"headers": {
"Content-Type": "text/html; charset=UTF-8",
"Content-Length": "11",
"Proxy-Connection": "close",
"Date": "Thu, 31 Dec 2009 20:55:48 +0000"
},
"body": "hello world",
"strict": true
},
{
"name": "underscore header key",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nServer: DCLK-AdSvr\r\nContent-Type: text/xml\r\nContent-Length: 0\r\nDCLK_imp: v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 4,
"headers": {
"Server": "DCLK-AdSvr",
"Content-Type": "text/xml",
"Content-Length": "0",
"DCLK_imp": "v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o"
},
"body": "",
"strict": true
},
{
"name": "bonjourmadame.fr",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.0 301 Moved Permanently\r\nDate: Thu, 03 Jun 2010 09:56:32 GMT\r\nServer: Apache/2.2.3 (Red Hat)\r\nCache-Control: public\r\nPragma: \r\nLocation: http://www.bonjourmadame.fr/\r\nVary: Accept-Encoding\r\nContent-Length: 0\r\nContent-Type: text/html; charset=UTF-8\r\nConnection: keep-alive\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"status_code": 301,
"status": "Moved Permanently",
"num_headers": 9,
"headers": {
"Date": "Thu, 03 Jun 2010 09:56:32 GMT",
"Server": "Apache/2.2.3 (Red Hat)",
"Cache-Control": "public",
"Pragma": "",
"Location": "http://www.bonjourmadame.fr/",
"Vary": "Accept-Encoding",
"Content-Length": "0",
"Content-Type": "text/html; charset=UTF-8",
"Connection": "keep-alive"
},
"body": "",
"strict": true
},
{
"name": "field underscore",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nDate: Tue, 28 Sep 2010 01:14:13 GMT\r\nServer: Apache\r\nCache-Control: no-cache, must-revalidate\r\nExpires: Mon, 26 Jul 1997 05:00:00 GMT\r\n.et-Cookie: PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com\r\nVary: Accept-Encoding\r\n_eep-Alive: timeout=45\r\n_onnection: Keep-Alive\r\nTransfer-Encoding: chunked\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n0\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 11,
"headers": {
"Date": "Tue, 28 Sep 2010 01:14:13 GMT",
"Server": "Apache",
"Cache-Control": "no-cache, must-revalidate",
"Expires": "Mon, 26 Jul 1997 05:00:00 GMT",
".et-Cookie": "PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com",
"Vary": "Accept-Encoding",
"_eep-Alive": "timeout=45",
"_onnection": "Keep-Alive",
"Transfer-Encoding": "chunked",
"Content-Type": "text/html",
"Connection": "close"
},
"body": "",
"strict": true
},
{
"name": "non-ASCII in status line",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 500 Oriëntatieprobleem\r\nDate: Fri, 5 Nov 2010 23:07:12 GMT+2\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 500,
"status": "Oriëntatieprobleem",
"num_headers": 3,
"headers": {
"Date": "Fri, 5 Nov 2010 23:07:12 GMT+2",
"Content-Length": "0",
"Connection": "close"
},
"body": "",
"strict": true
},
{
"name": "http version 0.9",
"type": "HTTP_RESPONSE",
"raw": "HTTP/0.9 200 OK\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 0,
"http_minor": 9,
"status_code": 200,
"status": "OK",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "neither content-length nor transfer-encoding response",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello world",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 1,
"headers": {
"Content-Type": "text/plain"
},
"body": "hello world",
"strict": true
},
{
"name": "HTTP/1.0 with keep-alive and EOF-terminated 200 status",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.0 200 OK\r\nConnection: keep-alive\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 0,
"status_code": 200,
"status": "OK",
"num_headers": 1,
"headers": {
"Connection": "keep-alive"
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.0 with keep-alive and a 204 status",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.0 204 No content\r\nConnection: keep-alive\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"status_code": 204,
"status": "No content",
"num_headers": 1,
"headers": {
"Connection": "keep-alive"
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.1 with an EOF-terminated 200 status",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 0,
"headers": {
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.1 with a 204 status",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 204 No content\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 204,
"status": "No content",
"num_headers": 0,
"headers": {
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.1 with a 204 status and keep-alive disabled",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 204 No content\r\nConnection: close\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 204,
"status": "No content",
"num_headers": 1,
"headers": {
"Connection": "close"
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.1 with chunked endocing and a 200 response",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 1,
"headers": {
"Transfer-Encoding": "chunked"
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "field space",
"type": "HTTP_RESPONSE",
"strict": false,
"raw": "HTTP/1.1 200 OK\r\nServer: Microsoft-IIS/6.0\r\nX-Powered-By: ASP.NET\r\nen-US Content-Type: text/xml\r\nContent-Type: text/xml\r\nContent-Length: 16\r\nDate: Fri, 23 Jul 2010 18:45:38 GMT\r\nConnection: keep-alive\r\n\r\n<xml>hello</xml>",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 7,
"headers": {
"Server": "Microsoft-IIS/6.0",
"X-Powered-By": "ASP.NET",
"en-US Content-Type": "text/xml",
"Content-Type": "text/xml",
"Content-Length": "16",
"Date": "Fri, 23 Jul 2010 18:45:38 GMT",
"Connection": "keep-alive"
},
"body": "<xml>hello</xml>"
}
]
@@ -0,0 +1,42 @@
require 'rubygems/package_task'
require 'rake/extensiontask'
require 'rake/javaextensiontask'
def gemspec
@clean_gemspec ||= eval(File.read(File.expand_path('../../http_parser.rb.gemspec', __FILE__)))
end
Gem::PackageTask.new(gemspec) do |pkg|
end
if RUBY_PLATFORM =~ /java/
Rake::JavaExtensionTask.new("ruby_http_parser", gemspec) do |ext|
ext.classpath = File.expand_path('../../ext/ruby_http_parser/vendor/http-parser-java/ext/primitives.jar', __FILE__)
end
else
Rake::ExtensionTask.new("ruby_http_parser", gemspec) do |ext|
unless RUBY_PLATFORM =~ /mswin|mingw/
ext.cross_compile = true
ext.cross_platform = ['x86-mingw32', 'x86-mswin32-60']
# inject 1.8/1.9 pure-ruby entry point
ext.cross_compiling do |spec|
spec.files += ['lib/ruby_http_parser.rb']
end
end
end
end
file 'lib/ruby_http_parser.rb' do |t|
File.open(t.name, 'wb') do |f|
f.write <<-eoruby
RUBY_VERSION =~ /(\\d+.\\d+)/
require "\#{$1}/ruby_http_parser"
eoruby
end
at_exit{ FileUtils.rm t.name if File.exists?(t.name) }
end
if Rake::Task.task_defined?(:cross)
task :cross => 'lib/ruby_http_parser.rb'
end
@@ -0,0 +1,71 @@
desc "Generate test fixtures"
task :fixtures => :submodules do
require 'yajl'
data = File.read File.expand_path('../../ext/ruby_http_parser/vendor/http-parser/test.c', __FILE__)
%w[ requests responses ].each do |type|
# find test definitions in between requests/responses[]= and .name=NULL
tmp = data[/#{type}\[\]\s*=(.+?),\s*\{\s*\.name=\s*NULL/m, 1]
# replace first { with a [ (parsing an array of test cases)
tmp.sub!('{','[')
# replace booleans
tmp.gsub!('TRUE', 'true')
tmp.gsub!('FALSE', 'false')
# mark strict mode tests
tmp.gsub!(%r|#if\s+!HTTP_PARSER_STRICT(.+?)#endif\s*/\*\s*!HTTP_PARSER_STRICT.+\n|m){
$1.gsub(/^(.+,\.type= .+)$/, "\\1\n, .strict= false")
}
# remove macros and comments
tmp.gsub!(/^#(if|elif|endif|define).+$/,'')
tmp.gsub!(/\/\*(.+?)\*\/$/,'')
# HTTP_* enums become strings
tmp.gsub!(/(= )(HTTP_\w+)/){
"#{$1}#{$2.sub('MSEARCH','M-SEARCH').dump}"
}
# join multiline strings for body and raw data
tmp.gsub!(/((body|raw)\s*=)(.+?)(\n\s+[\},])/m){
before, after = $1, $4
raw = $3.split("\n").map{ |l| l.strip[1..-2] }.join('')
"#{before} \"#{raw}\" #{after}"
}
# make headers an array of array tuples
tmp.gsub!(/(\.headers\s*=)(.+?)(\s*,\.)/m){
before, after = $1, $3
raw = $2.gsub('{', '[').gsub('}', ']')
"#{before} #{raw} #{after}"
}
# .name= becomes "name":
tmp.gsub!(/^(.{2,5})\.(\w+)\s*=/){
"#{$1}#{$2.dump}: "
}
# evaluate addition expressions
tmp.gsub!(/(body_size\":\s*)(\d+)\+(\d+)/){
"#{$1}#{$2.to_i+$3.to_i}"
}
# end result array
tmp << ']'
# normalize data
results = Yajl.load(tmp, :symbolize_keys => true)
results.map{ |res|
res[:headers] and res[:headers] = Hash[*res[:headers].flatten]
res[:method] and res[:method].gsub!(/^HTTP_/, '')
res[:strict] = true unless res.has_key?(:strict)
}
# write to a file
File.open("spec/support/#{type}.json", 'w'){ |f|
f.write Yajl.dump(results, :pretty => true)
}
end
end
@@ -0,0 +1,5 @@
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new do |t|
t.rspec_opts = %w(--format documentation --color)
end
@@ -0,0 +1,7 @@
desc "Fetch upstream submodules"
task :submodules do
if Dir['ext/ruby_http_parser/vendor/http-parser/*'].empty?
sh 'git submodule init'
sh 'git submodule update'
end
end