h1p 0.2 → 0.3
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 -1
- data/Gemfile.lock +1 -1
- data/README.md +28 -14
- data/Rakefile +1 -1
- data/benchmarks/bm_http1_parser.rb +1 -1
- data/benchmarks/pipelined.rb +101 -0
- data/examples/callable.rb +1 -1
- data/examples/http_server.rb +2 -2
- data/ext/h1p/h1p.c +340 -196
- data/ext/h1p/limits.rb +7 -6
- data/lib/h1p/version.rb +1 -1
- data/test/run.rb +5 -0
- data/test/test_h1p_client.rb +532 -0
- data/test/{test_h1p.rb → test_h1p_server.rb} +36 -36
- metadata +11 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c4b5a2d00d0bb28e75e75a1bb5c5c0bf4ef060d837e33606408d92bcb70deca
|
4
|
+
data.tar.gz: 73332efbff1047e8f5e3a65b6a718a187b332b405f943f6d53fc9acdf6b55b7e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b1616a34cf1f43c1dece307cabccd882f4805824a54e58a70e476fc37db0738542f260cf22a88a8c8d7e8fb310cdc3a67d3f4e50a22285ecdd4a08b43fbb46c
|
7
|
+
data.tar.gz: ca10eb8b6c764f15c5f816068e84719cb7b242d6e5cd521fb941995cc2b8fca7c48abe5e0791ba0d06d7331a3601adbf860373a550a132a13fb073c6684c66f8
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# H1P - a blocking HTTP/1 parser for Ruby
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/h1p.svg)](http://rubygems.org/gems/h1p)
|
4
|
-
[![
|
4
|
+
[![H1P Test](https://github.com/digital-fabric/h1p/workflows/Tests/badge.svg)](https://github.com/digital-fabric/h1p/actions?query=workflow%3ATests)
|
5
5
|
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/digital-fabric/h1p/blob/master/LICENSE)
|
6
6
|
|
7
7
|
H1P is a blocking/synchronous HTTP/1 parser for Ruby with a simple and intuitive
|
@@ -23,6 +23,7 @@ The H1P was originally written as part of
|
|
23
23
|
- Simple, blocking/synchronous API
|
24
24
|
- Zero dependencies
|
25
25
|
- Transport-agnostic
|
26
|
+
- Parses both HTTP request and HTTP response
|
26
27
|
- Support for chunked encoding
|
27
28
|
- Support for both `LF` and `CRLF` line breaks
|
28
29
|
- Track total incoming traffic
|
@@ -41,15 +42,21 @@ You can then run `bundle install` to install it. Otherwise, just run `gem instal
|
|
41
42
|
|
42
43
|
## Usage
|
43
44
|
|
44
|
-
Start by creating an instance of H1P::Parser
|
45
|
+
Start by creating an instance of `H1P::Parser`, passing a connection instance and the parsing mode:
|
45
46
|
|
46
47
|
```ruby
|
47
48
|
require 'h1p'
|
48
49
|
|
49
|
-
parser = H1P::Parser.new(conn)
|
50
|
+
parser = H1P::Parser.new(conn, :server)
|
50
51
|
```
|
51
52
|
|
52
|
-
|
53
|
+
In order to parse HTTP responses, change the mode to `:client`:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
parser = H1P::Parser.new(conn, :client)
|
57
|
+
```
|
58
|
+
|
59
|
+
To read the next message from the connection, call `#parse_headers`:
|
53
60
|
|
54
61
|
```ruby
|
55
62
|
loop do
|
@@ -65,13 +72,21 @@ headers. In case the client has closed the connection, `#parse_headers` will
|
|
65
72
|
return `nil` (see the guard clause above).
|
66
73
|
|
67
74
|
In addition to the header keys and values, the resulting hash also contains the
|
68
|
-
following "pseudo-headers":
|
75
|
+
following "pseudo-headers" (in server mode):
|
69
76
|
|
70
77
|
- `:method`: the HTTP method (in upper case)
|
71
78
|
- `:path`: the request target
|
72
79
|
- `:protocol`: the protocol used (either `'http/1.0'` or `'http/1.1'`)
|
73
80
|
- `:rx`: the total bytes read by the parser
|
74
81
|
|
82
|
+
In client mode, the following pseudo-headers will be present:
|
83
|
+
|
84
|
+
- `:protocol`: the protocol used (either `'http/1.0'` or `'http/1.1'`)
|
85
|
+
- `:status': the HTTP status as an integer
|
86
|
+
- `:status_message`: the HTTP status message
|
87
|
+
- `:rx`: the total bytes read by the parser
|
88
|
+
|
89
|
+
|
75
90
|
The header keys are always lower-cased. Consider the following HTTP request:
|
76
91
|
|
77
92
|
```
|
@@ -101,24 +116,24 @@ where the value is an array containing the corresponding values. For example,
|
|
101
116
|
multiple `Cookie` headers will appear in the hash as a single `"cookie"` entry,
|
102
117
|
e.g. `{ "cookie" => ['a=1', 'b=2'] }`
|
103
118
|
|
104
|
-
### Handling of invalid
|
119
|
+
### Handling of invalid message
|
105
120
|
|
106
|
-
When an invalid
|
107
|
-
exception. An incoming
|
108
|
-
has been encountered at any point in parsing the
|
121
|
+
When an invalid message is encountered, the parser will raise a `H1P::Error`
|
122
|
+
exception. An incoming message may be considered invalid if an invalid character
|
123
|
+
has been encountered at any point in parsing the message, or if any of the
|
109
124
|
tokens have an invalid length. You can consult the limits used by the parser
|
110
125
|
[here](https://github.com/digital-fabric/h1p/blob/main/ext/h1p/limits.rb).
|
111
126
|
|
112
|
-
### Reading the
|
127
|
+
### Reading the message body
|
113
128
|
|
114
|
-
To read the
|
129
|
+
To read the message body use `#read_body`:
|
115
130
|
|
116
131
|
```ruby
|
117
132
|
# read entire body
|
118
133
|
body = parser.read_body
|
119
134
|
```
|
120
135
|
|
121
|
-
The H1P parser knows how to read both
|
136
|
+
The H1P parser knows how to read both message bodies with a specified
|
122
137
|
`Content-Length` and request bodies in chunked encoding. The method call will
|
123
138
|
return when the entire body has been read. If the body is incomplete or has
|
124
139
|
invalid formatting, the parser will raise a `H1P::Error` exception.
|
@@ -211,8 +226,7 @@ performance.
|
|
211
226
|
Here are some of the features and enhancements planned for H1P:
|
212
227
|
|
213
228
|
- Add conformance and security tests
|
214
|
-
- Add ability to
|
215
|
-
- Add ability to splice the request body into an arbitrary fd
|
229
|
+
- Add ability to splice the message body into an arbitrary fd
|
216
230
|
(Polyphony-specific)
|
217
231
|
- Improve performance
|
218
232
|
|
data/Rakefile
CHANGED
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
HTTP_REQUEST = "GET /foo HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\nUser-Agent: foobar\r\n\r\n" +
|
4
|
+
"GET /bar HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\nUser-Agent: foobar\r\n\r\n"
|
5
|
+
|
6
|
+
def measure_time_and_allocs
|
7
|
+
4.times { GC.start }
|
8
|
+
GC.disable
|
9
|
+
|
10
|
+
t0 = Time.now
|
11
|
+
a0 = object_count
|
12
|
+
yield
|
13
|
+
t1 = Time.now
|
14
|
+
a1 = object_count
|
15
|
+
[t1 - t0, a1 - a0]
|
16
|
+
ensure
|
17
|
+
GC.enable
|
18
|
+
end
|
19
|
+
|
20
|
+
def object_count
|
21
|
+
count = ObjectSpace.count_objects
|
22
|
+
count[:TOTAL] - count[:FREE]
|
23
|
+
end
|
24
|
+
|
25
|
+
def benchmark_other_http1_parser(iterations)
|
26
|
+
STDOUT << "http_parser.rb: "
|
27
|
+
require 'http_parser.rb'
|
28
|
+
|
29
|
+
i, o = IO.pipe
|
30
|
+
parser = Http::Parser.new
|
31
|
+
done = false
|
32
|
+
queue = nil
|
33
|
+
rx = 0
|
34
|
+
req_count = 0
|
35
|
+
parser.on_headers_complete = proc do |h|
|
36
|
+
h[':method'] = parser.http_method
|
37
|
+
h[':path'] = parser.request_url
|
38
|
+
h[':rx'] = rx
|
39
|
+
queue << h
|
40
|
+
end
|
41
|
+
parser.on_message_complete = proc { done = true }
|
42
|
+
|
43
|
+
writer = Thread.new do
|
44
|
+
iterations.times { o << HTTP_REQUEST }
|
45
|
+
o.close
|
46
|
+
end
|
47
|
+
|
48
|
+
elapsed, allocated = measure_time_and_allocs do
|
49
|
+
queue = []
|
50
|
+
done = false
|
51
|
+
rx = 0
|
52
|
+
loop do
|
53
|
+
data = i.readpartial(4096) rescue nil
|
54
|
+
break unless data
|
55
|
+
|
56
|
+
rx += data.bytesize
|
57
|
+
parser << data
|
58
|
+
while (req = queue.shift)
|
59
|
+
req_count += 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
puts(format('count: %d, elapsed: %f, allocated: %d (%f/req), rate: %f ips', req_count, elapsed, allocated, allocated.to_f / iterations, iterations / elapsed))
|
64
|
+
end
|
65
|
+
|
66
|
+
def benchmark_h1p_parser(iterations)
|
67
|
+
STDOUT << "H1P parser: "
|
68
|
+
require_relative '../lib/h1p'
|
69
|
+
i, o = IO.pipe
|
70
|
+
parser = H1P::Parser.new(i)
|
71
|
+
req_count = 0
|
72
|
+
|
73
|
+
writer = Thread.new do
|
74
|
+
iterations.times { o << HTTP_REQUEST }
|
75
|
+
o.close
|
76
|
+
end
|
77
|
+
|
78
|
+
elapsed, allocated = measure_time_and_allocs do
|
79
|
+
while (headers = parser.parse_headers)
|
80
|
+
req_count += 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
puts(format('count: %d, elapsed: %f, allocated: %d (%f/req), rate: %f ips', req_count, elapsed, allocated, allocated.to_f / iterations, iterations / elapsed))
|
84
|
+
end
|
85
|
+
|
86
|
+
def fork_benchmark(method, iterations)
|
87
|
+
pid = fork do
|
88
|
+
send(method, iterations)
|
89
|
+
rescue Exception => e
|
90
|
+
p e
|
91
|
+
p e.backtrace
|
92
|
+
exit!
|
93
|
+
end
|
94
|
+
Process.wait(pid)
|
95
|
+
end
|
96
|
+
|
97
|
+
x = 100000
|
98
|
+
fork_benchmark(:benchmark_other_http1_parser, x)
|
99
|
+
fork_benchmark(:benchmark_h1p_parser, x)
|
100
|
+
|
101
|
+
# benchmark_h1p_parser(x)
|
data/examples/callable.rb
CHANGED
data/examples/http_server.rb
CHANGED
@@ -10,13 +10,13 @@ trap('SIGINT') { exit! }
|
|
10
10
|
|
11
11
|
def handle_client(conn)
|
12
12
|
Thread.new do
|
13
|
-
parser = H1P::Parser.new(conn)
|
13
|
+
parser = H1P::Parser.new(conn, :server)
|
14
14
|
loop do
|
15
15
|
headers = parser.parse_headers
|
16
16
|
break unless headers
|
17
17
|
|
18
18
|
req_body = parser.read_body
|
19
|
-
|
19
|
+
|
20
20
|
p headers: headers
|
21
21
|
p body: req_body
|
22
22
|
|