h1p 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +6 -15
- data/README.md +220 -3
- data/benchmarks/bm_http1_parser.rb +23 -10
- data/examples/callable.rb +10 -0
- data/examples/http_server.rb +28 -30
- data/ext/h1p/h1p.c +11 -1
- data/h1p.gemspec +2 -3
- data/lib/h1p.rb +1 -1
- data/lib/h1p/version.rb +1 -1
- data/test/helper.rb +0 -1
- data/test/test_h1p.rb +10 -10
- metadata +8 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 58f7bb5105298c97edf87c91b8038ccc50ac6732417b981331a39cf56711da9b
|
4
|
+
data.tar.gz: d5745229262d866ed3564eede8b52706b14cb791652886be9f0a3ec2bc44eaf4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d64c4623aa5c1cffd019222f6b75e3ec07da72997a5118490e07437ee0b0449371da96d89f4755182703fc71bc0974f415e6c9eecffa1f173c3bcb59f894d376
|
7
|
+
data.tar.gz: 9df79da840b37ecacb1df7818a661a33560970b5c07e4b82250e29466b868c6087848b527a8123a1373ead55ec1a37a4626e1a702c04037a4515e66ac6220476
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,33 +1,24 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
h1p (0.
|
4
|
+
h1p (0.2)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
|
10
|
-
|
11
|
-
minitest (5.11.3)
|
12
|
-
minitest-reporters (1.4.3)
|
13
|
-
ansi
|
14
|
-
builder
|
15
|
-
minitest (>= 5.0)
|
16
|
-
ruby-progressbar
|
17
|
-
rake (12.3.3)
|
9
|
+
minitest (5.14.4)
|
10
|
+
rake (13.0.6)
|
18
11
|
rake-compiler (1.1.1)
|
19
12
|
rake
|
20
|
-
ruby-progressbar (1.11.0)
|
21
13
|
|
22
14
|
PLATFORMS
|
23
15
|
ruby
|
24
16
|
|
25
17
|
DEPENDENCIES
|
26
18
|
h1p!
|
27
|
-
minitest (~> 5.
|
28
|
-
|
29
|
-
rake (~> 12.3.3)
|
19
|
+
minitest (~> 5.14.4)
|
20
|
+
rake (~> 13.0.6)
|
30
21
|
rake-compiler (= 1.1.1)
|
31
22
|
|
32
23
|
BUNDLED WITH
|
33
|
-
2.
|
24
|
+
2.2.26
|
data/README.md
CHANGED
@@ -1,5 +1,222 @@
|
|
1
|
-
#
|
1
|
+
# H1P - a blocking HTTP/1 parser for Ruby
|
2
2
|
|
3
|
-
h1p
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/h1p.svg)](http://rubygems.org/gems/h1p)
|
4
|
+
[![Modulation Test](https://github.com/digital-fabric/h1p/workflows/Tests/badge.svg)](https://github.com/digital-fabric/h1p/actions?query=workflow%3ATests)
|
5
|
+
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/digital-fabric/h1p/blob/master/LICENSE)
|
4
6
|
|
5
|
-
HTTP
|
7
|
+
H1P is a blocking/synchronous HTTP/1 parser for Ruby with a simple and intuitive
|
8
|
+
API. Its design lends itself to writing HTTP servers in a sequential style. As
|
9
|
+
such, it might prove useful in conjunction with the new fiber scheduler
|
10
|
+
introduced in Ruby 3.0, but is also useful with a normal thread-based server
|
11
|
+
(see
|
12
|
+
[example](https://github.com/digital-fabric/h1p/blob/main/examples/http_server.rb).)
|
13
|
+
The H1P was originally written as part of
|
14
|
+
[Tipi](https://github.com/digital-fabric/tipi), a web server running on top of
|
15
|
+
[Polyphony](https://github.com/digital-fabric/polyphony).
|
16
|
+
|
17
|
+
> H1P is still a very young project and as such should be used with caution. It
|
18
|
+
> has not undergone any significant conformance or security testing, and its API
|
19
|
+
> is not yet stable.
|
20
|
+
|
21
|
+
## Features
|
22
|
+
|
23
|
+
- Simple, blocking/synchronous API
|
24
|
+
- Zero dependencies
|
25
|
+
- Transport-agnostic
|
26
|
+
- Support for chunked encoding
|
27
|
+
- Support for both `LF` and `CRLF` line breaks
|
28
|
+
- Track total incoming traffic
|
29
|
+
|
30
|
+
## Installing
|
31
|
+
|
32
|
+
If you're using bundler just add it to your `Gemfile`:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
source 'https://rubygems.org'
|
36
|
+
|
37
|
+
gem 'h1p'
|
38
|
+
```
|
39
|
+
|
40
|
+
You can then run `bundle install` to install it. Otherwise, just run `gem install h1p`.
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
Start by creating an instance of H1P::Parser, passing a connection instance:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'h1p'
|
48
|
+
|
49
|
+
parser = H1P::Parser.new(conn)
|
50
|
+
```
|
51
|
+
|
52
|
+
To read the next request from the connection, call `#parse_headers`:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
loop do
|
56
|
+
headers = parser.parse_headers
|
57
|
+
break unless headers
|
58
|
+
|
59
|
+
handle_request(headers)
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
The `#parse_headers` method returns a single hash containing the different HTTP
|
64
|
+
headers. In case the client has closed the connection, `#parse_headers` will
|
65
|
+
return `nil` (see the guard clause above).
|
66
|
+
|
67
|
+
In addition to the header keys and values, the resulting hash also contains the
|
68
|
+
following "pseudo-headers":
|
69
|
+
|
70
|
+
- `:method`: the HTTP method (in upper case)
|
71
|
+
- `:path`: the request target
|
72
|
+
- `:protocol`: the protocol used (either `'http/1.0'` or `'http/1.1'`)
|
73
|
+
- `:rx`: the total bytes read by the parser
|
74
|
+
|
75
|
+
The header keys are always lower-cased. Consider the following HTTP request:
|
76
|
+
|
77
|
+
```
|
78
|
+
GET /foo HTTP/1.1
|
79
|
+
Host: example.com
|
80
|
+
User-Agent: curl/7.74.0
|
81
|
+
Accept: */*
|
82
|
+
|
83
|
+
```
|
84
|
+
|
85
|
+
The request will be parsed into the following Ruby hash:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
{
|
89
|
+
":method" => "get",
|
90
|
+
":path" => "/foo",
|
91
|
+
":protocol" => "http/1.1",
|
92
|
+
"host" => "example.com",
|
93
|
+
"user-agent" => "curl/7.74.0",
|
94
|
+
"accept" => "*/*",
|
95
|
+
":rx" => 78
|
96
|
+
}
|
97
|
+
```
|
98
|
+
|
99
|
+
Multiple headers with the same key will be coalesced into a single key-value
|
100
|
+
where the value is an array containing the corresponding values. For example,
|
101
|
+
multiple `Cookie` headers will appear in the hash as a single `"cookie"` entry,
|
102
|
+
e.g. `{ "cookie" => ['a=1', 'b=2'] }`
|
103
|
+
|
104
|
+
### Handling of invalid requests
|
105
|
+
|
106
|
+
When an invalid request is encountered, the parser will raise a `H1P::Error`
|
107
|
+
exception. An incoming request may be considered invalid if an invalid character
|
108
|
+
has been encountered at any point in parsing the request, or if any of the
|
109
|
+
tokens have an invalid length. You can consult the limits used by the parser
|
110
|
+
[here](https://github.com/digital-fabric/h1p/blob/main/ext/h1p/limits.rb).
|
111
|
+
|
112
|
+
### Reading the request body
|
113
|
+
|
114
|
+
To read the request body use `#read_body`:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
# read entire body
|
118
|
+
body = parser.read_body
|
119
|
+
```
|
120
|
+
|
121
|
+
The H1P parser knows how to read both request bodies with a specified
|
122
|
+
`Content-Length` and request bodies in chunked encoding. The method call will
|
123
|
+
return when the entire body has been read. If the body is incomplete or has
|
124
|
+
invalid formatting, the parser will raise a `H1P::Error` exception.
|
125
|
+
|
126
|
+
You can also read a single chunk of the body by calling `#read_body_chunk`:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
# read a body chunk
|
130
|
+
chunk = parser.read_body_chunk(false)
|
131
|
+
|
132
|
+
# read chunk only from buffer:
|
133
|
+
chunk = parser.read_body_chunk(true)
|
134
|
+
```
|
135
|
+
|
136
|
+
If no more chunks are availble, `#read_body_chunk` will return nil. To test
|
137
|
+
whether the request is complete, you can call `#complete?`:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
headers = parser.parse_headers
|
141
|
+
unless parser.complete?
|
142
|
+
body = parser.read_body
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
The `#read_body` and `#read_body_chunk` methods will return `nil` if no body is
|
147
|
+
expected (based on the received headers).
|
148
|
+
|
149
|
+
## Parsing from arbitrary transports
|
150
|
+
|
151
|
+
The H1P parser was built to read from any arbitrary transport or source, as long
|
152
|
+
as they conform to one of two alternative interfaces:
|
153
|
+
|
154
|
+
- An object implementing a `__parser_read_method__` method, which returns any of
|
155
|
+
the following values:
|
156
|
+
|
157
|
+
- `:stock_readpartial` - to be used for instances of `IO`, `Socket`,
|
158
|
+
`TCPSocket`, `SSLSocket` etc.
|
159
|
+
- `:backend_read` - for use in Polyphony-based servers.
|
160
|
+
- `:backend_recv` - for use in Polyphony-based servers.
|
161
|
+
- `:readpartial` - for use in Polyphony-based servers.
|
162
|
+
|
163
|
+
- An object implementing a `call` method, such as a `Proc` or any other. The
|
164
|
+
call is given a single argument signifying the maximum number of bytes to
|
165
|
+
read, and is expected to return either a string with the read data, or `nil`
|
166
|
+
if no more data is available. The callable can be passed as an argument or as
|
167
|
+
a block. Here's an example for parsing from a callable:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
data = ['GET ', '/foo', " HTTP/1.1\r\n", "\r\n"]
|
171
|
+
data = ['GET ', '/foo', " HTTP/1.1\r\n", "\r\n"]
|
172
|
+
parser = H1P::Parser.new { data.shift }
|
173
|
+
parser.parse_headers
|
174
|
+
#=> {":method"=>"get", ":path"=>"/foo", ":protocol"=>"http/1.1", ":rx"=>21}
|
175
|
+
```
|
176
|
+
|
177
|
+
## Design
|
178
|
+
|
179
|
+
The H1P parser design is based on the following principles:
|
180
|
+
|
181
|
+
- Implement a blocking API for use with a sequential programming style.
|
182
|
+
- Minimize copying of data between buffers.
|
183
|
+
- Parse each piece of data only once.
|
184
|
+
- Minimize object and buffer allocations.
|
185
|
+
- Minimize the API surface area.
|
186
|
+
|
187
|
+
One of the unique aspects of H1P is that instead of the server needing to feed
|
188
|
+
data to the parser, the parser itself reads data from its source whenever it
|
189
|
+
needs more of it. If no data is yet available, the parser blocks until more data
|
190
|
+
is received.
|
191
|
+
|
192
|
+
The different parts of the request are parsed one byte at a time, and once each
|
193
|
+
token is considered complete, it is copied from the buffer into a new string, to
|
194
|
+
be stored in the headers hash.
|
195
|
+
|
196
|
+
## Performance
|
197
|
+
|
198
|
+
The included benchmark (against
|
199
|
+
[http_parser.rb](https://github.com/tmm1/http_parser.rb), based on the *old*
|
200
|
+
[node.js HTTP parser](https://github.com/nodejs/http-parser)) shows the H1P
|
201
|
+
parser to be about 10-20% slower than http_parser.rb.
|
202
|
+
|
203
|
+
However, in a fiber-based environment such as
|
204
|
+
[Polyphony](https://github.com/digital-fabric/polyphony), H1P is slightly
|
205
|
+
faster, as the overhead of dealing with pipelined requests (which will cause
|
206
|
+
`http_parser.rb` to emit callbacks multiple times) significantly affects its
|
207
|
+
performance.
|
208
|
+
|
209
|
+
## Roadmap
|
210
|
+
|
211
|
+
Here are some of the features and enhancements planned for H1P:
|
212
|
+
|
213
|
+
- Add conformance and security tests
|
214
|
+
- Add ability to parse HTTP/1 responses (for implementing HTTP clients)
|
215
|
+
- Add ability to splice the request body into an arbitrary fd
|
216
|
+
(Polyphony-specific)
|
217
|
+
- Improve performance
|
218
|
+
|
219
|
+
## Contributing
|
220
|
+
|
221
|
+
Issues and pull requests will be gladly accepted. If you have found this gem
|
222
|
+
useful, please let me know.
|
@@ -1,8 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
HTTP_REQUEST = "GET /foo HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\nUser-Agent: foobar\r\n\r\n"
|
4
4
|
|
5
|
-
HTTP_REQUEST =
|
5
|
+
# HTTP_REQUEST =
|
6
|
+
# "GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n" +
|
7
|
+
# "Host: www.kittyhell.com\r\n" +
|
8
|
+
# "User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n" +
|
9
|
+
# "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
|
10
|
+
# "Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n" +
|
11
|
+
# "Accept-Encoding: gzip,deflate\r\n" +
|
12
|
+
# "Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n" +
|
13
|
+
# "Keep-Alive: 115\r\n" +
|
14
|
+
# "Connection: keep-alive\r\n" +
|
15
|
+
# "Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral\r\n\r\n"
|
6
16
|
|
7
17
|
def measure_time_and_allocs
|
8
18
|
4.times { GC.start }
|
@@ -31,10 +41,12 @@ def benchmark_other_http1_parser(iterations)
|
|
31
41
|
parser = Http::Parser.new
|
32
42
|
done = false
|
33
43
|
headers = nil
|
44
|
+
rx = 0
|
34
45
|
parser.on_headers_complete = proc do |h|
|
35
46
|
headers = h
|
36
47
|
headers[':method'] = parser.http_method
|
37
48
|
headers[':path'] = parser.request_url
|
49
|
+
headers[':rx'] = rx
|
38
50
|
end
|
39
51
|
parser.on_message_complete = proc { done = true }
|
40
52
|
|
@@ -42,8 +54,10 @@ def benchmark_other_http1_parser(iterations)
|
|
42
54
|
iterations.times do
|
43
55
|
o << HTTP_REQUEST
|
44
56
|
done = false
|
57
|
+
rx = 0
|
45
58
|
while !done
|
46
59
|
msg = i.readpartial(4096)
|
60
|
+
rx += msg.bytesize
|
47
61
|
parser << msg
|
48
62
|
end
|
49
63
|
end
|
@@ -52,11 +66,10 @@ def benchmark_other_http1_parser(iterations)
|
|
52
66
|
end
|
53
67
|
|
54
68
|
def benchmark_tipi_http1_parser(iterations)
|
55
|
-
STDOUT << "
|
56
|
-
require_relative '../lib/
|
69
|
+
STDOUT << "H1P parser: "
|
70
|
+
require_relative '../lib/h1p'
|
57
71
|
i, o = IO.pipe
|
58
|
-
|
59
|
-
parser = Tipi::HTTP1Parser.new(reader)
|
72
|
+
parser = H1P::Parser.new(i)
|
60
73
|
|
61
74
|
elapsed, allocated = measure_time_and_allocs do
|
62
75
|
iterations.times do
|
@@ -78,8 +91,8 @@ def fork_benchmark(method, iterations)
|
|
78
91
|
Process.wait(pid)
|
79
92
|
end
|
80
93
|
|
81
|
-
x =
|
82
|
-
|
83
|
-
|
94
|
+
x = 100000
|
95
|
+
fork_benchmark(:benchmark_other_http1_parser, x)
|
96
|
+
fork_benchmark(:benchmark_tipi_http1_parser, x)
|
84
97
|
|
85
|
-
benchmark_tipi_http1_parser(x)
|
98
|
+
# benchmark_tipi_http1_parser(x)
|
data/examples/http_server.rb
CHANGED
@@ -1,41 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
-
require '
|
5
|
-
|
6
|
-
opts = {
|
7
|
-
reuse_addr: true,
|
8
|
-
dont_linger: true
|
9
|
-
}
|
4
|
+
require 'h1p'
|
10
5
|
|
11
6
|
puts "pid: #{Process.pid}"
|
12
|
-
puts 'Listening on port
|
7
|
+
puts 'Listening on port 1234...'
|
13
8
|
|
14
|
-
|
15
|
-
# Thread.current.backend.idle_gc_period = 60
|
9
|
+
trap('SIGINT') { exit! }
|
16
10
|
|
17
|
-
|
11
|
+
def handle_client(conn)
|
12
|
+
Thread.new do
|
13
|
+
parser = H1P::Parser.new(conn)
|
14
|
+
loop do
|
15
|
+
headers = parser.parse_headers
|
16
|
+
break unless headers
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
req_body = parser.read_body
|
19
|
+
|
20
|
+
p headers: headers
|
21
|
+
p body: req_body
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
req.send_chunk("bar\n")
|
31
|
-
req.finish
|
32
|
-
elsif req.path == '/upload'
|
33
|
-
body = req.read
|
34
|
-
req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
|
35
|
-
else
|
36
|
-
req.respond("Hello world!\n")
|
23
|
+
resp = 'Hello, world!'
|
24
|
+
conn << "HTTP/1.1 200 OK\r\nContent-Length: #{resp.bytesize}\r\n\r\n#{resp}"
|
25
|
+
rescue H1P::Error => e
|
26
|
+
puts "Invalid request: #{e.message}"
|
27
|
+
ensure
|
28
|
+
conn.close
|
29
|
+
break
|
37
30
|
end
|
38
|
-
# p req.transfer_counts
|
39
31
|
end
|
40
|
-
|
41
|
-
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'socket'
|
35
|
+
server = TCPServer.new('0.0.0.0', 1234)
|
36
|
+
loop do
|
37
|
+
conn = server.accept
|
38
|
+
handle_client(conn)
|
39
|
+
end
|
data/ext/h1p/h1p.c
CHANGED
@@ -24,6 +24,7 @@ ID ID_parser_read_method;
|
|
24
24
|
ID ID_read;
|
25
25
|
ID ID_readpartial;
|
26
26
|
ID ID_to_i;
|
27
|
+
ID ID_upcase;
|
27
28
|
|
28
29
|
static VALUE mPolyphony = Qnil;
|
29
30
|
static VALUE cError;
|
@@ -145,6 +146,7 @@ VALUE Parser_initialize(VALUE self, VALUE io) {
|
|
145
146
|
////////////////////////////////////////////////////////////////////////////////
|
146
147
|
|
147
148
|
#define str_downcase(str) (rb_funcall((str), ID_downcase, 0))
|
149
|
+
#define str_upcase(str) (rb_funcall((str), ID_upcase, 0))
|
148
150
|
|
149
151
|
#define FILL_BUFFER_OR_GOTO_EOF(state) { if (!fill_buffer(state)) goto eof; }
|
150
152
|
|
@@ -155,6 +157,7 @@ VALUE Parser_initialize(VALUE self, VALUE io) {
|
|
155
157
|
#define BUFFER_PTR(state, pos) ((state)->ptr + pos)
|
156
158
|
#define BUFFER_STR(state, pos, len) (rb_obj_freeze(rb_utf8_str_new((state)->ptr + pos, len)))
|
157
159
|
#define BUFFER_STR_DOWNCASE(state, pos, len) (rb_obj_freeze(str_downcase(rb_utf8_str_new((state)->ptr + pos, len))))
|
160
|
+
#define BUFFER_STR_UPCASE(state, pos, len) (rb_obj_freeze(str_upcase(rb_utf8_str_new((state)->ptr + pos, len))))
|
158
161
|
|
159
162
|
#define INC_BUFFER_POS(state) { \
|
160
163
|
BUFFER_POS(state)++; \
|
@@ -209,6 +212,12 @@ VALUE Parser_initialize(VALUE self, VALUE io) {
|
|
209
212
|
RB_GC_GUARD(value); \
|
210
213
|
}
|
211
214
|
|
215
|
+
#define SET_HEADER_UPCASE_VALUE_FROM_BUFFER(state, headers, key, pos, len) { \
|
216
|
+
VALUE value = BUFFER_STR_UPCASE(state, pos, len); \
|
217
|
+
rb_hash_aset(headers, key, value); \
|
218
|
+
RB_GC_GUARD(value); \
|
219
|
+
}
|
220
|
+
|
212
221
|
#define CONSUME_CRLF(state) { \
|
213
222
|
INC_BUFFER_POS(state); \
|
214
223
|
if (BUFFER_CUR(state) != '\n') goto bad_request; \
|
@@ -335,7 +344,7 @@ static inline int parse_method(struct parser_state *state, VALUE headers) {
|
|
335
344
|
}
|
336
345
|
}
|
337
346
|
done:
|
338
|
-
|
347
|
+
SET_HEADER_UPCASE_VALUE_FROM_BUFFER(state, headers, STR_pseudo_method, pos, len);
|
339
348
|
return 1;
|
340
349
|
bad_request:
|
341
350
|
RAISE_BAD_REQUEST("Invalid method");
|
@@ -834,6 +843,7 @@ void Init_H1P() {
|
|
834
843
|
ID_read = rb_intern("read");
|
835
844
|
ID_readpartial = rb_intern("readpartial");
|
836
845
|
ID_to_i = rb_intern("to_i");
|
846
|
+
ID_upcase = rb_intern("upcase");
|
837
847
|
|
838
848
|
NUM_max_headers_read_length = INT2NUM(MAX_HEADERS_READ_LENGTH);
|
839
849
|
NUM_buffer_start = INT2NUM(0);
|
data/h1p.gemspec
CHANGED
@@ -19,7 +19,6 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.required_ruby_version = '>= 2.6'
|
20
20
|
|
21
21
|
s.add_development_dependency 'rake-compiler', '1.1.1'
|
22
|
-
s.add_development_dependency 'rake', '~>
|
23
|
-
s.add_development_dependency 'minitest', '~>5.
|
24
|
-
s.add_development_dependency 'minitest-reporters', '~>1.4.2'
|
22
|
+
s.add_development_dependency 'rake', '~>13.0.6'
|
23
|
+
s.add_development_dependency 'minitest', '~>5.14.4'
|
25
24
|
end
|
data/lib/h1p.rb
CHANGED
data/lib/h1p/version.rb
CHANGED
data/test/helper.rb
CHANGED
data/test/test_h1p.rb
CHANGED
@@ -22,7 +22,7 @@ class H1PTest < MiniTest::Test
|
|
22
22
|
|
23
23
|
assert_equal(
|
24
24
|
{
|
25
|
-
':method' => '
|
25
|
+
':method' => 'GET',
|
26
26
|
':path' => '/',
|
27
27
|
':protocol' => 'http/1.1',
|
28
28
|
':rx' => msg.bytesize
|
@@ -38,7 +38,7 @@ class H1PTest < MiniTest::Test
|
|
38
38
|
|
39
39
|
assert_equal(
|
40
40
|
{
|
41
|
-
':method' => '
|
41
|
+
':method' => 'GET',
|
42
42
|
':path' => '/',
|
43
43
|
':protocol' => 'http/1.1',
|
44
44
|
':rx' => msg.bytesize
|
@@ -57,17 +57,17 @@ class H1PTest < MiniTest::Test
|
|
57
57
|
def test_method_case
|
58
58
|
@o << "GET / HTTP/1.1\r\n\r\n"
|
59
59
|
headers = @parser.parse_headers
|
60
|
-
assert_equal '
|
60
|
+
assert_equal 'GET', headers[':method']
|
61
61
|
|
62
62
|
reset_parser
|
63
63
|
@o << "post / HTTP/1.1\r\n\r\n"
|
64
64
|
headers = @parser.parse_headers
|
65
|
-
assert_equal '
|
65
|
+
assert_equal 'POST', headers[':method']
|
66
66
|
|
67
67
|
reset_parser
|
68
68
|
@o << "PoST / HTTP/1.1\r\n\r\n"
|
69
69
|
headers = @parser.parse_headers
|
70
|
-
assert_equal '
|
70
|
+
assert_equal 'POST', headers[':method']
|
71
71
|
end
|
72
72
|
|
73
73
|
def test_bad_method
|
@@ -80,7 +80,7 @@ class H1PTest < MiniTest::Test
|
|
80
80
|
|
81
81
|
reset_parser
|
82
82
|
@o << "#{'a' * max_length} / HTTP/1.1\r\n\r\n"
|
83
|
-
assert_equal '
|
83
|
+
assert_equal 'A' * max_length, @parser.parse_headers[':method']
|
84
84
|
|
85
85
|
reset_parser
|
86
86
|
@o << "#{'a' * (max_length + 1)} / HTTP/1.1\r\n\r\n"
|
@@ -245,7 +245,7 @@ class H1PTest < MiniTest::Test
|
|
245
245
|
@o << msg
|
246
246
|
headers = @parser.parse_headers
|
247
247
|
assert_equal({
|
248
|
-
':method' => '
|
248
|
+
':method' => 'GET',
|
249
249
|
':path' => '/foo',
|
250
250
|
':protocol' => 'http/1.1',
|
251
251
|
'bar' => 'baz',
|
@@ -534,7 +534,7 @@ class H1PTest < MiniTest::Test
|
|
534
534
|
client << msg
|
535
535
|
reply = client.read
|
536
536
|
assert_equal({
|
537
|
-
':method' => '
|
537
|
+
':method' => 'GET',
|
538
538
|
':path' => '/foo',
|
539
539
|
':protocol' => 'http/1.1',
|
540
540
|
'cookie' => 'abc=def',
|
@@ -560,7 +560,7 @@ class H1PTest < MiniTest::Test
|
|
560
560
|
|
561
561
|
headers = parser.parse_headers
|
562
562
|
assert_equal({
|
563
|
-
':method' => '
|
563
|
+
':method' => 'GET',
|
564
564
|
':path' => '/foo',
|
565
565
|
':protocol' => 'http/1.1',
|
566
566
|
'host' => 'bar',
|
@@ -572,7 +572,7 @@ class H1PTest < MiniTest::Test
|
|
572
572
|
request = +"GET /bar HTTP/1.1\r\nHost: baz\r\n\r\n"
|
573
573
|
headers = parser.parse_headers
|
574
574
|
assert_equal({
|
575
|
-
':method' => '
|
575
|
+
':method' => 'GET',
|
576
576
|
':path' => '/bar',
|
577
577
|
':protocol' => 'http/1.1',
|
578
578
|
'host' => 'baz',
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: h1p
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-08-
|
11
|
+
date: 2021-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -30,42 +30,28 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 13.0.6
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 13.0.6
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: minitest
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 5.
|
47
|
+
version: 5.14.4
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 5.
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: minitest-reporters
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 1.4.2
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 1.4.2
|
54
|
+
version: 5.14.4
|
69
55
|
description:
|
70
56
|
email: sharon@noteflakes.com
|
71
57
|
executables: []
|
@@ -84,6 +70,7 @@ files:
|
|
84
70
|
- Rakefile
|
85
71
|
- TODO.md
|
86
72
|
- benchmarks/bm_http1_parser.rb
|
73
|
+
- examples/callable.rb
|
87
74
|
- examples/http_server.rb
|
88
75
|
- ext/h1p/extconf.rb
|
89
76
|
- ext/h1p/h1p.c
|
@@ -118,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
118
105
|
- !ruby/object:Gem::Version
|
119
106
|
version: '0'
|
120
107
|
requirements: []
|
121
|
-
rubygems_version: 3.1.
|
108
|
+
rubygems_version: 3.1.4
|
122
109
|
signing_key:
|
123
110
|
specification_version: 4
|
124
111
|
summary: H1P is a blocking HTTP/1 parser for Ruby
|