h1p 0.4 → 0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -3
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +5 -5
- data/README.md +85 -5
- data/ext/h1p/h1p.c +470 -57
- data/ext/h1p/h1p.h +5 -0
- data/h1p.gemspec +3 -3
- data/lib/h1p/version.rb +1 -1
- data/lib/h1p.rb +12 -6
- data/test/helper.rb +0 -2
- data/test/test_h1p.rb +114 -0
- data/test/test_h1p_server.rb +56 -1
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90f7937fd6bcefdd1627c208bf73054fb3fb03a855666c7247f04aac4398d280
|
4
|
+
data.tar.gz: ab6a866b843beaf92137b991048db9af006f4cb5e510e211dc6ba6652bbc84a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 101e6e13aeed0cf2250dbe962c0c9c9edc5bf69e6ff2a59ee0d02018992c9b76dcaa4be3ddb44bbcb2272e355278a0b717c51050a2786cc95c00fbdb23bc40fa
|
7
|
+
data.tar.gz: 2d3130c067d51f173873d6181b76999dd897c241dcafc5fa3f69ec0837404b2fd7e292cadc2fbf9e27003e3dfd933dc394799abf03f12187d6c80e4f2dab83d6
|
data/.github/workflows/test.yml
CHANGED
@@ -8,17 +8,18 @@ jobs:
|
|
8
8
|
fail-fast: false
|
9
9
|
matrix:
|
10
10
|
os: [ubuntu-latest]
|
11
|
-
ruby: [2.
|
11
|
+
ruby: [2.7, 3.0, 3.1, 3.2]
|
12
12
|
|
13
13
|
name: >-
|
14
14
|
${{matrix.os}}, ${{matrix.ruby}}
|
15
15
|
|
16
16
|
runs-on: ${{matrix.os}}
|
17
17
|
steps:
|
18
|
-
- uses: actions/checkout@
|
19
|
-
- uses:
|
18
|
+
- uses: actions/checkout@v3
|
19
|
+
- uses: ruby/setup-ruby@v1
|
20
20
|
with:
|
21
21
|
ruby-version: ${{matrix.ruby}}
|
22
|
+
bundler-cache: true
|
22
23
|
- name: Install dependencies
|
23
24
|
run: |
|
24
25
|
gem install bundler
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## 0.6 2023-01-05
|
2
|
+
|
3
|
+
- Add documentation
|
4
|
+
- Implement `H1P.send_body_chunk`
|
5
|
+
- Implement `H1P.send_chunked_response`
|
6
|
+
- Implement `H1P.send_response`
|
7
|
+
|
8
|
+
## 0.5 2022-03-19
|
9
|
+
|
10
|
+
- Implement `Parser#splice_body_to` (#3)
|
11
|
+
|
1
12
|
## 0.4 2022-02-28
|
2
13
|
|
3
14
|
- Rename `__parser_read_method__` to `__read_method__`
|
data/Gemfile.lock
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
h1p (0.
|
4
|
+
h1p (0.6)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
minitest (5.
|
9
|
+
minitest (5.17.0)
|
10
10
|
rake (13.0.6)
|
11
|
-
rake-compiler (1.
|
11
|
+
rake-compiler (1.2.1)
|
12
12
|
rake
|
13
13
|
|
14
14
|
PLATFORMS
|
@@ -16,9 +16,9 @@ PLATFORMS
|
|
16
16
|
|
17
17
|
DEPENDENCIES
|
18
18
|
h1p!
|
19
|
-
minitest (~> 5.
|
19
|
+
minitest (~> 5.17.0)
|
20
20
|
rake (~> 13.0.6)
|
21
|
-
rake-compiler (= 1.
|
21
|
+
rake-compiler (= 1.2.1)
|
22
22
|
|
23
23
|
BUNDLED WITH
|
24
24
|
2.2.26
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# H1P -
|
1
|
+
# H1P - HTTP/1 tools for Ruby
|
2
2
|
|
3
3
|
[](http://rubygems.org/gems/h1p)
|
4
4
|
[](https://github.com/digital-fabric/h1p/actions?query=workflow%3ATests)
|
@@ -14,9 +14,8 @@ The H1P was originally written as part of
|
|
14
14
|
[Tipi](https://github.com/digital-fabric/tipi), a web server running on top of
|
15
15
|
[Polyphony](https://github.com/digital-fabric/polyphony).
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
> is not yet stable.
|
17
|
+
In addition to parsing, H1P offers APIs for formatting and writing HTTP/1
|
18
|
+
requests and responses.
|
20
19
|
|
21
20
|
## Features
|
22
21
|
|
@@ -26,7 +25,11 @@ The H1P was originally written as part of
|
|
26
25
|
- Parses both HTTP request and HTTP response
|
27
26
|
- Support for chunked encoding
|
28
27
|
- Support for both `LF` and `CRLF` line breaks
|
28
|
+
- Support for **splicing** request/response bodies (when used with
|
29
|
+
[Polyphony](https://github.com/digital-fabric/polyphony))
|
29
30
|
- Track total incoming traffic
|
31
|
+
- Write HTTP requests and responses to any IO instance, with support for chunked
|
32
|
+
transfer encoding.
|
30
33
|
|
31
34
|
## Installing
|
32
35
|
|
@@ -161,6 +164,36 @@ end
|
|
161
164
|
The `#read_body` and `#read_body_chunk` methods will return `nil` if no body is
|
162
165
|
expected (based on the received headers).
|
163
166
|
|
167
|
+
## Splicing request/response bodies
|
168
|
+
|
169
|
+
> Splicing of request/response bodies is available only on Linux, and works only
|
170
|
+
> with [Polyphony](https://github.com/digital-fabric/polyphony).
|
171
|
+
|
172
|
+
H1P also lets you [splice](https://man7.org/linux/man-pages/man2/splice.2.html)
|
173
|
+
request or response bodies directly to a pipe. This is particularly useful for
|
174
|
+
uploading or downloading large files, as the data does not need to be loaded
|
175
|
+
into Ruby strings. In fact, the data will stay almost entirely in kernel
|
176
|
+
buffers, which means any data copying is reduced to the absolute minimum.
|
177
|
+
|
178
|
+
The following example sends a request, then splices the response body to a file:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
require 'polyphony'
|
182
|
+
require 'h1p'
|
183
|
+
|
184
|
+
socket = TCPSocket.new('example.com', 80)
|
185
|
+
socket << "GET /bigfile HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
186
|
+
|
187
|
+
parser = H1P::Parser.new(socket, :client)
|
188
|
+
headers = parser.parse_headers
|
189
|
+
|
190
|
+
pipe = Polyphony.pipe
|
191
|
+
File.open('bigfile', 'w+') do |f|
|
192
|
+
spin { parser.splice_body_to(pipe) }
|
193
|
+
f.splice_from(pipe)
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
164
197
|
## Parsing from arbitrary transports
|
165
198
|
|
166
199
|
The H1P parser was built to read from any arbitrary transport or source, as long
|
@@ -189,7 +222,54 @@ as they conform to one of two alternative interfaces:
|
|
189
222
|
#=> {":method"=>"get", ":path"=>"/foo", ":protocol"=>"http/1.1", ":rx"=>21}
|
190
223
|
```
|
191
224
|
|
192
|
-
##
|
225
|
+
## Writing HTTP requests and responses
|
226
|
+
|
227
|
+
H1P implements optimized methods for writing HTTP requests and responses to
|
228
|
+
arbitrary IO instances. To write a response with or without a body, use
|
229
|
+
`H1P.send_response(io, headers, body = nil)`:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
H1P.send_response(socket, { 'Some-Header' => 'header value'}, 'foobar')
|
233
|
+
#=> "HTTP/1.1 200 OK\r\nSome-Header: header value\r\n\r\nfoobar"
|
234
|
+
|
235
|
+
# The :protocol pseudo header sets the protocol in the status line:
|
236
|
+
H1P.send_response(socket, { ':protocol' => 'HTTP/0.9' })
|
237
|
+
#=> "HTTP/0.9 200 OK\r\n\r\n"
|
238
|
+
|
239
|
+
# The :status pseudo header sets the response status:
|
240
|
+
H1P.send_response(socket, { ':status' => '418 I\'m a teapot' })
|
241
|
+
#=> "HTTP/1.1 418 I'm a teapot\r\n\r\n"
|
242
|
+
```
|
243
|
+
|
244
|
+
To send responses using chunked transfer encoding use
|
245
|
+
`H1P.send_chunked_response(io, header, body = nil)`:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
H1P.send_chunked_response(socket, {}, "foobar")
|
249
|
+
#=> "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
|
250
|
+
```
|
251
|
+
|
252
|
+
You can also call `H1P.send_chunked_response` with a block that provides the
|
253
|
+
next chunk to send. The last chunk is signalled by returning `nil` from the
|
254
|
+
block:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
IO.open('/path/to/file') do |f|
|
258
|
+
H1P.send_chunked_response(socket, {}) { f.read(CHUNK_SIZE) }
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
To send individual chunks use `H1P.send_body_chunk`:
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
H1P.send_body_chunk(socket, 'foo')
|
266
|
+
#=> "3\r\nfoo\r\n"
|
267
|
+
|
268
|
+
H1P.send_body_chunk(socket, nil)
|
269
|
+
#=> "0\r\n\r\n"
|
270
|
+
```
|
271
|
+
|
272
|
+
## Parser Design
|
193
273
|
|
194
274
|
The H1P parser design is based on the following principles:
|
195
275
|
|
data/ext/h1p/h1p.c
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
#include "ruby.h"
|
2
1
|
#include "h1p.h"
|
3
2
|
|
4
3
|
// Security-related limits are defined in limits.rb and injected as
|
@@ -16,6 +15,9 @@
|
|
16
15
|
ID ID_arity;
|
17
16
|
ID ID_backend_read;
|
18
17
|
ID ID_backend_recv;
|
18
|
+
ID ID_backend_send;
|
19
|
+
ID ID_backend_splice;
|
20
|
+
ID ID_backend_write;
|
19
21
|
ID ID_call;
|
20
22
|
ID ID_downcase;
|
21
23
|
ID ID_eof_p;
|
@@ -24,11 +26,15 @@ ID ID_read_method;
|
|
24
26
|
ID ID_read;
|
25
27
|
ID ID_readpartial;
|
26
28
|
ID ID_to_i;
|
29
|
+
ID ID_to_s;
|
27
30
|
ID ID_upcase;
|
31
|
+
ID ID_write;
|
32
|
+
ID ID_write_method;
|
28
33
|
|
29
|
-
static VALUE mPolyphony = Qnil;
|
30
34
|
static VALUE cError;
|
31
35
|
|
36
|
+
VALUE eArgumentError;
|
37
|
+
|
32
38
|
VALUE NUM_max_headers_read_length;
|
33
39
|
VALUE NUM_buffer_start;
|
34
40
|
VALUE NUM_buffer_end;
|
@@ -36,27 +42,41 @@ VALUE NUM_buffer_end;
|
|
36
42
|
VALUE STR_pseudo_method;
|
37
43
|
VALUE STR_pseudo_path;
|
38
44
|
VALUE STR_pseudo_protocol;
|
45
|
+
VALUE STR_pseudo_protocol_default;
|
39
46
|
VALUE STR_pseudo_rx;
|
40
47
|
VALUE STR_pseudo_status;
|
48
|
+
VALUE STR_pseudo_status_default;
|
41
49
|
VALUE STR_pseudo_status_message;
|
42
50
|
|
43
51
|
VALUE STR_chunked;
|
44
52
|
VALUE STR_content_length;
|
53
|
+
VALUE STR_content_length_capitalized;
|
45
54
|
VALUE STR_transfer_encoding;
|
55
|
+
VALUE STR_transfer_encoding_capitalized;
|
56
|
+
|
57
|
+
VALUE STR_CRLF;
|
58
|
+
VALUE STR_EMPTY_CHUNK;
|
46
59
|
|
47
60
|
VALUE SYM_backend_read;
|
48
61
|
VALUE SYM_backend_recv;
|
62
|
+
VALUE SYM_backend_send;
|
63
|
+
VALUE SYM_backend_write;
|
49
64
|
VALUE SYM_stock_readpartial;
|
50
65
|
|
51
66
|
VALUE SYM_client;
|
52
67
|
VALUE SYM_server;
|
53
68
|
|
54
69
|
enum read_method {
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
70
|
+
RM_READPARTIAL, // receiver.readpartial(len, buf, pos, raise_on_eof: false) (Polyphony-specific)
|
71
|
+
RM_BACKEND_READ, // Polyphony.backend_read (Polyphony-specific)
|
72
|
+
RM_BACKEND_RECV, // Polyphony.backend_recv (Polyphony-specific)
|
73
|
+
RM_CALL, // receiver.call(len) (Universal)
|
74
|
+
RM_STOCK_READPARTIAL // receiver.readpartial(len)
|
75
|
+
};
|
76
|
+
|
77
|
+
enum write_method {
|
78
|
+
WM_BACKEND_WRITE,
|
79
|
+
WM_BACKEND_SEND
|
60
80
|
};
|
61
81
|
|
62
82
|
enum parser_mode {
|
@@ -115,31 +135,40 @@ static VALUE Parser_allocate(VALUE klass) {
|
|
115
135
|
#define GetParser(obj, parser) \
|
116
136
|
TypedData_Get_Struct((obj), Parser_t, &Parser_type, (parser))
|
117
137
|
|
118
|
-
static inline
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
138
|
+
static inline VALUE Polyphony(void) {
|
139
|
+
static VALUE mPolyphony = Qnil;
|
140
|
+
if (mPolyphony == Qnil) {
|
141
|
+
mPolyphony = rb_const_get(rb_cObject, rb_intern("Polyphony"));
|
142
|
+
rb_gc_register_mark_object(mPolyphony);
|
143
|
+
}
|
144
|
+
return mPolyphony;
|
123
145
|
}
|
124
146
|
|
125
|
-
enum read_method detect_read_method(VALUE io) {
|
147
|
+
static enum read_method detect_read_method(VALUE io) {
|
126
148
|
if (rb_respond_to(io, ID_read_method)) {
|
127
149
|
VALUE method = rb_funcall(io, ID_read_method, 0);
|
128
|
-
if (method == SYM_stock_readpartial) return
|
129
|
-
|
130
|
-
|
131
|
-
if (method == SYM_backend_read) return method_backend_read;
|
132
|
-
if (method == SYM_backend_recv) return method_backend_recv;
|
150
|
+
if (method == SYM_stock_readpartial) return RM_STOCK_READPARTIAL;
|
151
|
+
if (method == SYM_backend_read) return RM_BACKEND_READ;
|
152
|
+
if (method == SYM_backend_recv) return RM_BACKEND_RECV;
|
133
153
|
|
134
|
-
return
|
154
|
+
return RM_READPARTIAL;
|
135
155
|
}
|
136
156
|
else if (rb_respond_to(io, ID_call)) {
|
137
|
-
return
|
157
|
+
return RM_CALL;
|
138
158
|
}
|
139
159
|
else
|
140
160
|
rb_raise(rb_eRuntimeError, "Provided reader should be a callable or respond to #__read_method__");
|
141
161
|
}
|
142
162
|
|
163
|
+
static enum write_method detect_write_method(VALUE io) {
|
164
|
+
if (rb_respond_to(io, ID_write_method)) {
|
165
|
+
VALUE method = rb_funcall(io, ID_write_method, 0);
|
166
|
+
if (method == SYM_backend_write) return WM_BACKEND_WRITE;
|
167
|
+
if (method == SYM_backend_send) return WM_BACKEND_SEND;
|
168
|
+
}
|
169
|
+
rb_raise(rb_eRuntimeError, "Provided io should respond to #__write_method__");
|
170
|
+
}
|
171
|
+
|
143
172
|
enum parser_mode parse_parser_mode(VALUE mode) {
|
144
173
|
if (mode == SYM_server) return mode_server;
|
145
174
|
if (mode == SYM_client) return mode_client;
|
@@ -147,6 +176,12 @@ enum parser_mode parse_parser_mode(VALUE mode) {
|
|
147
176
|
rb_raise(rb_eRuntimeError, "Invalid parser mode specified");
|
148
177
|
}
|
149
178
|
|
179
|
+
/* call-seq:
|
180
|
+
* parser.initialize(io, mode)
|
181
|
+
*
|
182
|
+
* Initializes a new parser with the given IO instance and mode. Mode is either
|
183
|
+
* `:server` or `:client`.
|
184
|
+
*/
|
150
185
|
VALUE Parser_initialize(VALUE self, VALUE io, VALUE mode) {
|
151
186
|
Parser_t *parser;
|
152
187
|
GetParser(self, parser);
|
@@ -246,7 +281,7 @@ VALUE Parser_initialize(VALUE self, VALUE io, VALUE mode) {
|
|
246
281
|
}
|
247
282
|
|
248
283
|
#define SET_HEADER_VALUE_INT(parser, key, value) { \
|
249
|
-
rb_hash_aset(parser->headers, key,
|
284
|
+
rb_hash_aset(parser->headers, key, INT2FIX(value)); \
|
250
285
|
}
|
251
286
|
|
252
287
|
#define CONSUME_CRLF(parser) { \
|
@@ -294,21 +329,37 @@ static inline VALUE io_stock_readpartial(VALUE io, VALUE maxlen, VALUE buf, VALU
|
|
294
329
|
|
295
330
|
static inline VALUE parser_io_read(Parser_t *parser, VALUE maxlen, VALUE buf, VALUE buf_pos) {
|
296
331
|
switch (parser->read_method) {
|
297
|
-
case
|
298
|
-
return rb_funcall(
|
299
|
-
case
|
300
|
-
return rb_funcall(
|
301
|
-
case
|
302
|
-
return rb_funcall(parser->
|
303
|
-
case
|
304
|
-
return io_call(parser
|
305
|
-
case
|
332
|
+
case RM_BACKEND_READ:
|
333
|
+
return rb_funcall(Polyphony(), ID_backend_read, 5, parser->io, buf, maxlen, Qfalse, buf_pos);
|
334
|
+
case RM_BACKEND_RECV:
|
335
|
+
return rb_funcall(Polyphony(), ID_backend_recv, 4, parser->io, buf, maxlen, buf_pos);
|
336
|
+
case RM_READPARTIAL:
|
337
|
+
return rb_funcall(parser->io, ID_readpartial, 4, maxlen, buf, buf_pos, Qfalse);
|
338
|
+
case RM_CALL:
|
339
|
+
return io_call(parser->io, maxlen, buf, buf_pos);
|
340
|
+
case RM_STOCK_READPARTIAL:
|
306
341
|
return io_stock_readpartial(parser->io, maxlen, buf, buf_pos);
|
307
342
|
default:
|
308
343
|
return Qnil;
|
309
344
|
}
|
310
345
|
}
|
311
346
|
|
347
|
+
static inline VALUE parser_io_write(VALUE io, VALUE buf, enum write_method method) {
|
348
|
+
switch (method) {
|
349
|
+
case WM_BACKEND_WRITE:
|
350
|
+
return rb_funcall(Polyphony(), ID_backend_write, 2, io, buf);
|
351
|
+
case WM_BACKEND_SEND:
|
352
|
+
return rb_funcall(Polyphony(), ID_backend_send, 3, io, buf, INT2FIX(0));
|
353
|
+
default:
|
354
|
+
return Qnil;
|
355
|
+
}
|
356
|
+
}
|
357
|
+
|
358
|
+
static inline int parser_io_splice(VALUE src, VALUE dest, int len) {
|
359
|
+
VALUE ret = rb_funcall(Polyphony(), ID_backend_splice, 3, src, dest, INT2FIX(len));
|
360
|
+
return FIX2INT(ret);
|
361
|
+
}
|
362
|
+
|
312
363
|
static inline int fill_buffer(Parser_t *parser) {
|
313
364
|
VALUE ret = parser_io_read(parser, NUM_max_headers_read_length, parser->buffer, NUM_buffer_end);
|
314
365
|
if (ret == Qnil) return 0;
|
@@ -671,6 +722,19 @@ eof:
|
|
671
722
|
return 0;
|
672
723
|
}
|
673
724
|
|
725
|
+
/* call-seq: parser.parse_headers -> headers
|
726
|
+
*
|
727
|
+
* Parses headers from the associated IO instance, returning a hash mapping
|
728
|
+
* header keys to their respective values. Header keys are downcased and dashes
|
729
|
+
* are converted to underscores. The returned headers will also include the
|
730
|
+
* following pseudo-headers:
|
731
|
+
*
|
732
|
+
* - `':protocol'` - the protocol as specified in the query line / status line
|
733
|
+
* - `':path'` - the query path (for HTTP requests)
|
734
|
+
* - `':method'` - the HTTP method (for HTTP requests)
|
735
|
+
* - `':status'` - the HTTP status (for HTTP responses)
|
736
|
+
* - `':rx'` - the total number of bytes read by the parser
|
737
|
+
*/
|
674
738
|
VALUE Parser_parse_headers(VALUE self) {
|
675
739
|
Parser_t *parser;
|
676
740
|
GetParser(self, parser);
|
@@ -705,7 +769,7 @@ done:
|
|
705
769
|
|
706
770
|
parser->current_request_rx += read_bytes;
|
707
771
|
if (parser->headers != Qnil)
|
708
|
-
rb_hash_aset(parser->headers, STR_pseudo_rx,
|
772
|
+
rb_hash_aset(parser->headers, STR_pseudo_rx, INT2FIX(read_bytes));
|
709
773
|
return parser->headers;
|
710
774
|
}
|
711
775
|
|
@@ -754,7 +818,7 @@ VALUE read_body_with_content_length(Parser_t *parser, int read_entire_body, int
|
|
754
818
|
|
755
819
|
while (parser->body_left) {
|
756
820
|
int maxlen = parser->body_left <= MAX_BODY_READ_LENGTH ? parser->body_left : MAX_BODY_READ_LENGTH;
|
757
|
-
VALUE tmp_buf = parser_io_read(parser,
|
821
|
+
VALUE tmp_buf = parser_io_read(parser, INT2FIX(maxlen), Qnil, NUM_buffer_start);
|
758
822
|
if (tmp_buf == Qnil) goto eof;
|
759
823
|
if (body != Qnil)
|
760
824
|
rb_str_append(body, tmp_buf);
|
@@ -768,7 +832,7 @@ VALUE read_body_with_content_length(Parser_t *parser, int read_entire_body, int
|
|
768
832
|
if (!read_entire_body) goto done;
|
769
833
|
}
|
770
834
|
done:
|
771
|
-
rb_hash_aset(parser->headers, STR_pseudo_rx,
|
835
|
+
rb_hash_aset(parser->headers, STR_pseudo_rx, INT2FIX(parser->current_request_rx));
|
772
836
|
RB_GC_GUARD(body);
|
773
837
|
return body;
|
774
838
|
eof:
|
@@ -836,7 +900,7 @@ int read_body_chunk_with_chunked_encoding(Parser_t *parser, VALUE *body, int chu
|
|
836
900
|
while (left) {
|
837
901
|
int maxlen = left <= MAX_BODY_READ_LENGTH ? left : MAX_BODY_READ_LENGTH;
|
838
902
|
|
839
|
-
VALUE tmp_buf = parser_io_read(parser,
|
903
|
+
VALUE tmp_buf = parser_io_read(parser, INT2FIX(maxlen), Qnil, NUM_buffer_start);
|
840
904
|
if (tmp_buf == Qnil) goto eof;
|
841
905
|
if (*body != Qnil)
|
842
906
|
rb_str_append(*body, tmp_buf);
|
@@ -852,6 +916,33 @@ eof:
|
|
852
916
|
return 0;
|
853
917
|
}
|
854
918
|
|
919
|
+
int splice_body_chunk_with_chunked_encoding(Parser_t *parser, VALUE dest, int chunk_size, enum write_method method) {
|
920
|
+
int len = RSTRING_LEN(parser->buffer);
|
921
|
+
int pos = BUFFER_POS(parser);
|
922
|
+
int left = chunk_size;
|
923
|
+
|
924
|
+
if (pos < len) {
|
925
|
+
int available = len - pos;
|
926
|
+
if (available > left) available = left;
|
927
|
+
VALUE buf = rb_str_new(RSTRING_PTR(parser->buffer) + pos, available);
|
928
|
+
BUFFER_POS(parser) += available;
|
929
|
+
parser->current_request_rx += available;
|
930
|
+
parser_io_write(dest, buf, method);
|
931
|
+
RB_GC_GUARD(buf);
|
932
|
+
left -= available;
|
933
|
+
}
|
934
|
+
|
935
|
+
while (left) {
|
936
|
+
int spliced = parser_io_splice(parser->io, dest, left);
|
937
|
+
if (!spliced) goto eof;
|
938
|
+
parser->current_request_rx += spliced;
|
939
|
+
left -= spliced;
|
940
|
+
}
|
941
|
+
return 1;
|
942
|
+
eof:
|
943
|
+
return 0;
|
944
|
+
}
|
945
|
+
|
855
946
|
static inline int parse_chunk_postfix(Parser_t *parser) {
|
856
947
|
int initial_pos = BUFFER_POS(parser);
|
857
948
|
if (initial_pos == BUFFER_LEN(parser)) FILL_BUFFER_OR_GOTO_EOF(parser);
|
@@ -897,11 +988,69 @@ bad_request:
|
|
897
988
|
eof:
|
898
989
|
RAISE_BAD_REQUEST("Incomplete request body");
|
899
990
|
done:
|
900
|
-
rb_hash_aset(parser->headers, STR_pseudo_rx,
|
991
|
+
rb_hash_aset(parser->headers, STR_pseudo_rx, INT2FIX(parser->current_request_rx));
|
901
992
|
RB_GC_GUARD(body);
|
902
993
|
return body;
|
903
994
|
}
|
904
995
|
|
996
|
+
void splice_body_with_chunked_encoding(Parser_t *parser, VALUE dest, enum write_method method) {
|
997
|
+
buffer_trim(parser);
|
998
|
+
INIT_PARSER_STATE(parser);
|
999
|
+
|
1000
|
+
while (1) {
|
1001
|
+
int chunk_size = 0;
|
1002
|
+
if (BUFFER_POS(parser) == BUFFER_LEN(parser)) FILL_BUFFER_OR_GOTO_EOF(parser);
|
1003
|
+
if (!parse_chunk_size(parser, &chunk_size)) goto bad_request;
|
1004
|
+
|
1005
|
+
if (chunk_size) {
|
1006
|
+
if (!splice_body_chunk_with_chunked_encoding(parser, dest, chunk_size, method))
|
1007
|
+
goto bad_request;
|
1008
|
+
}
|
1009
|
+
else
|
1010
|
+
parser->request_completed = 1;
|
1011
|
+
|
1012
|
+
// read post-chunk delimiter ("\r\n")
|
1013
|
+
if (!parse_chunk_postfix(parser)) goto bad_request;
|
1014
|
+
if (!chunk_size) goto done;
|
1015
|
+
}
|
1016
|
+
bad_request:
|
1017
|
+
RAISE_BAD_REQUEST("Malformed request body");
|
1018
|
+
eof:
|
1019
|
+
RAISE_BAD_REQUEST("Incomplete request body");
|
1020
|
+
done:
|
1021
|
+
rb_hash_aset(parser->headers, STR_pseudo_rx, INT2FIX(parser->current_request_rx));
|
1022
|
+
}
|
1023
|
+
|
1024
|
+
void splice_body_with_content_length(Parser_t *parser, VALUE dest, enum write_method method) {
|
1025
|
+
if (parser->body_left <= 0) return;
|
1026
|
+
|
1027
|
+
int len = RSTRING_LEN(parser->buffer);
|
1028
|
+
int pos = BUFFER_POS(parser);
|
1029
|
+
|
1030
|
+
if (pos < len) {
|
1031
|
+
int available = len - pos;
|
1032
|
+
if (available > parser->body_left) available = parser->body_left;
|
1033
|
+
VALUE buf = rb_str_new(RSTRING_PTR(parser->buffer) + pos, available);
|
1034
|
+
BUFFER_POS(parser) += available;
|
1035
|
+
parser_io_write(dest, buf, method);
|
1036
|
+
RB_GC_GUARD(buf);
|
1037
|
+
parser->current_request_rx += available;
|
1038
|
+
parser->body_left -= available;
|
1039
|
+
if (!parser->body_left) parser->request_completed = 1;
|
1040
|
+
}
|
1041
|
+
|
1042
|
+
while (parser->body_left) {
|
1043
|
+
int spliced = parser_io_splice(parser->io, dest, parser->body_left);
|
1044
|
+
if (!spliced) goto eof;
|
1045
|
+
parser->current_request_rx += spliced;
|
1046
|
+
parser->body_left -= spliced;
|
1047
|
+
}
|
1048
|
+
rb_hash_aset(parser->headers, STR_pseudo_rx, INT2FIX(parser->current_request_rx));
|
1049
|
+
return;
|
1050
|
+
eof:
|
1051
|
+
RAISE_BAD_REQUEST("Incomplete body");
|
1052
|
+
}
|
1053
|
+
|
905
1054
|
static inline void detect_body_read_mode(Parser_t *parser) {
|
906
1055
|
VALUE content_length = rb_hash_aref(parser->headers, STR_content_length);
|
907
1056
|
if (content_length != Qnil) {
|
@@ -931,17 +1080,54 @@ static inline VALUE read_body(VALUE self, int read_entire_body, int buffered_onl
|
|
931
1080
|
|
932
1081
|
if (parser->body_read_mode == BODY_READ_MODE_CHUNKED)
|
933
1082
|
return read_body_with_chunked_encoding(parser, read_entire_body, buffered_only);
|
934
|
-
|
1083
|
+
else
|
1084
|
+
return read_body_with_content_length(parser, read_entire_body, buffered_only);
|
935
1085
|
}
|
936
1086
|
|
1087
|
+
/* call-seq: parser.read_body -> body
|
1088
|
+
*
|
1089
|
+
* Reads an HTTP request/response body from the associated IO instance.
|
1090
|
+
*/
|
937
1091
|
VALUE Parser_read_body(VALUE self) {
|
938
1092
|
return read_body(self, 1, 0);
|
939
1093
|
}
|
940
1094
|
|
1095
|
+
/* call-seq: parser.read_body_chunk(buffered_only) -> chunk
|
1096
|
+
*
|
1097
|
+
* Reads a single body chunk (useful for chunked transfer encoding). If
|
1098
|
+
* `buffered_only` is true, will only read from the underlying buffer, without
|
1099
|
+
* reading from the associated IO instance.
|
1100
|
+
*/
|
941
1101
|
VALUE Parser_read_body_chunk(VALUE self, VALUE buffered_only) {
|
942
1102
|
return read_body(self, 0, buffered_only == Qtrue);
|
943
1103
|
}
|
944
1104
|
|
1105
|
+
/* call-seq: parser.splice_body_to(dest)
|
1106
|
+
*
|
1107
|
+
* Splices the HTTP request/response body from the associated IO instance to
|
1108
|
+
* `dest`.
|
1109
|
+
*/
|
1110
|
+
VALUE Parser_splice_body_to(VALUE self, VALUE dest) {
|
1111
|
+
Parser_t *parser;
|
1112
|
+
GetParser(self, parser);
|
1113
|
+
enum write_method method = detect_write_method(dest);
|
1114
|
+
|
1115
|
+
if (parser->body_read_mode == BODY_READ_MODE_UNKNOWN)
|
1116
|
+
detect_body_read_mode(parser);
|
1117
|
+
|
1118
|
+
if (parser->body_read_mode == BODY_READ_MODE_CHUNKED)
|
1119
|
+
splice_body_with_chunked_encoding(parser, dest, method);
|
1120
|
+
else
|
1121
|
+
splice_body_with_content_length(parser, dest, method);
|
1122
|
+
|
1123
|
+
return self;
|
1124
|
+
}
|
1125
|
+
|
1126
|
+
/* call-seq: parser.complete?
|
1127
|
+
*
|
1128
|
+
* Returns true if a complete HTTP request/response has been read from the
|
1129
|
+
* associated IO instance.
|
1130
|
+
*/
|
945
1131
|
VALUE Parser_complete_p(VALUE self) {
|
946
1132
|
Parser_t *parser;
|
947
1133
|
GetParser(self, parser);
|
@@ -952,7 +1138,212 @@ VALUE Parser_complete_p(VALUE self) {
|
|
952
1138
|
return parser->request_completed ? Qtrue : Qfalse;
|
953
1139
|
}
|
954
1140
|
|
955
|
-
|
1141
|
+
typedef struct send_response_ctx {
|
1142
|
+
VALUE io;
|
1143
|
+
VALUE buffer;
|
1144
|
+
char *buffer_ptr;
|
1145
|
+
unsigned int buffer_len;
|
1146
|
+
unsigned int total_written;
|
1147
|
+
} send_response_ctx;
|
1148
|
+
|
1149
|
+
#define MAX_RESPONSE_BUFFER_SIZE 65536
|
1150
|
+
|
1151
|
+
void send_response_flush_buffer(send_response_ctx *ctx) {
|
1152
|
+
if (!ctx->buffer_len) return;
|
1153
|
+
|
1154
|
+
rb_str_set_len(ctx->buffer, ctx->buffer_len);
|
1155
|
+
VALUE written = rb_funcall(ctx->io, ID_write, 1, ctx->buffer);
|
1156
|
+
ctx->total_written += NUM2INT(written);
|
1157
|
+
|
1158
|
+
rb_str_set_len(ctx->buffer, 0);
|
1159
|
+
ctx->buffer_len = 0;
|
1160
|
+
}
|
1161
|
+
|
1162
|
+
void send_response_write_status_line(send_response_ctx *ctx, VALUE protocol, VALUE status) {
|
1163
|
+
char *ptr = ctx->buffer_ptr;
|
1164
|
+
|
1165
|
+
unsigned int partlen = RSTRING_LEN(protocol);
|
1166
|
+
memcpy(ptr, RSTRING_PTR(protocol), partlen);
|
1167
|
+
ctx->buffer_len += partlen;
|
1168
|
+
ptr[ctx->buffer_len] = ' ';
|
1169
|
+
ctx->buffer_len++;
|
1170
|
+
|
1171
|
+
ptr += ctx->buffer_len;
|
1172
|
+
partlen = RSTRING_LEN(status);
|
1173
|
+
memcpy(ptr, RSTRING_PTR(status), partlen);
|
1174
|
+
ptr[partlen] = '\r';
|
1175
|
+
ptr[partlen + 1] = '\n';
|
1176
|
+
ctx->buffer_len += partlen + 2;
|
1177
|
+
}
|
1178
|
+
|
1179
|
+
int send_response_write_header(VALUE key, VALUE val, VALUE arg) {
|
1180
|
+
if (TYPE(key) != T_STRING) key = rb_funcall(key, ID_to_s, 0);
|
1181
|
+
char *keyptr = RSTRING_PTR(key);
|
1182
|
+
if (RSTRING_LEN(key) < 1 || keyptr[0] == ':') return 0;
|
1183
|
+
|
1184
|
+
if (TYPE(val) != T_STRING) val = rb_funcall(val, ID_to_s, 0);
|
1185
|
+
unsigned int keylen = RSTRING_LEN(key);
|
1186
|
+
char *valptr = RSTRING_PTR(val);
|
1187
|
+
unsigned int vallen = RSTRING_LEN(val);
|
1188
|
+
send_response_ctx *ctx = (send_response_ctx *)arg;
|
1189
|
+
|
1190
|
+
if (ctx->buffer_len + keylen + vallen > (MAX_RESPONSE_BUFFER_SIZE - 8))
|
1191
|
+
send_response_flush_buffer(ctx);
|
1192
|
+
|
1193
|
+
char *ptr = ctx->buffer_ptr + ctx->buffer_len;
|
1194
|
+
memcpy(ptr, keyptr, keylen);
|
1195
|
+
ptr[keylen] = ':';
|
1196
|
+
ptr[keylen + 1] = ' ';
|
1197
|
+
ptr += keylen + 2;
|
1198
|
+
|
1199
|
+
memcpy(ptr, valptr, vallen);
|
1200
|
+
ptr[vallen] = '\r';
|
1201
|
+
ptr[vallen + 1] = '\n';
|
1202
|
+
ctx->buffer_len += keylen + vallen + 4;
|
1203
|
+
|
1204
|
+
RB_GC_GUARD(key);
|
1205
|
+
RB_GC_GUARD(val);
|
1206
|
+
|
1207
|
+
return 0; // ST_CONTINUE
|
1208
|
+
}
|
1209
|
+
|
1210
|
+
/* call-seq: H1P.send_response(io, headers, body = nil) -> total_written
|
1211
|
+
*
|
1212
|
+
* Sends an HTTP response with the given headers and body.
|
1213
|
+
*/
|
1214
|
+
VALUE H1P_send_response(int argc,VALUE *argv, VALUE self) {
|
1215
|
+
if (argc < 2)
|
1216
|
+
rb_raise(eArgumentError, "(wrong number of arguments (expected 2 or more))");
|
1217
|
+
|
1218
|
+
VALUE io = argv[0];
|
1219
|
+
VALUE headers = argv[1];
|
1220
|
+
VALUE body = argc >= 3 ? argv[2] : Qnil;
|
1221
|
+
VALUE buffer = rb_str_new_literal("");
|
1222
|
+
rb_str_modify_expand(buffer, MAX_RESPONSE_BUFFER_SIZE);
|
1223
|
+
send_response_ctx ctx = {io, buffer, RSTRING_PTR(buffer), 0, 0};
|
1224
|
+
|
1225
|
+
char *bodyptr = 0;
|
1226
|
+
unsigned int bodylen = 0;
|
1227
|
+
|
1228
|
+
VALUE protocol = rb_hash_aref(headers, STR_pseudo_protocol);
|
1229
|
+
if (protocol == Qnil) protocol = STR_pseudo_protocol_default;
|
1230
|
+
VALUE status = rb_hash_aref(headers, STR_pseudo_status);
|
1231
|
+
if (status == Qnil) status = STR_pseudo_status_default;
|
1232
|
+
send_response_write_status_line(&ctx, protocol, status);
|
1233
|
+
|
1234
|
+
if (body != Qnil) {
|
1235
|
+
if (TYPE(body) != T_STRING) body = rb_funcall(body, ID_to_s, 0);
|
1236
|
+
|
1237
|
+
bodyptr = RSTRING_PTR(body);
|
1238
|
+
bodylen = RSTRING_LEN(body);
|
1239
|
+
rb_hash_aset(headers, STR_content_length_capitalized, INT2FIX(bodylen));
|
1240
|
+
}
|
1241
|
+
|
1242
|
+
rb_hash_foreach(headers, send_response_write_header, (VALUE)&ctx);
|
1243
|
+
|
1244
|
+
char *endptr = ctx.buffer_ptr + ctx.buffer_len;
|
1245
|
+
endptr[0] = '\r';
|
1246
|
+
endptr[1] = '\n';
|
1247
|
+
ctx.buffer_len += 2;
|
1248
|
+
|
1249
|
+
if (body != Qnil) {
|
1250
|
+
while (bodylen > 0) {
|
1251
|
+
unsigned int chunklen = bodylen;
|
1252
|
+
if (chunklen > MAX_RESPONSE_BUFFER_SIZE) chunklen = MAX_RESPONSE_BUFFER_SIZE;
|
1253
|
+
|
1254
|
+
if (ctx.buffer_len + chunklen > MAX_RESPONSE_BUFFER_SIZE)
|
1255
|
+
send_response_flush_buffer(&ctx);
|
1256
|
+
|
1257
|
+
memcpy(ctx.buffer_ptr + ctx.buffer_len, bodyptr, chunklen);
|
1258
|
+
ctx.buffer_len += chunklen;
|
1259
|
+
bodyptr += chunklen;
|
1260
|
+
bodylen -= chunklen;
|
1261
|
+
}
|
1262
|
+
RB_GC_GUARD(body);
|
1263
|
+
}
|
1264
|
+
|
1265
|
+
send_response_flush_buffer(&ctx);
|
1266
|
+
|
1267
|
+
RB_GC_GUARD(buffer);
|
1268
|
+
|
1269
|
+
return INT2FIX(ctx.total_written);
|
1270
|
+
}
|
1271
|
+
|
1272
|
+
/* call-seq: H1P.send_body_chunk(io, chunk) -> total_written
|
1273
|
+
*
|
1274
|
+
* Sends a body chunk using chunked transfer encoding.
|
1275
|
+
*/
|
1276
|
+
VALUE H1P_send_body_chunk(VALUE self, VALUE io, VALUE chunk) {
|
1277
|
+
if (chunk != Qnil) {
|
1278
|
+
if (TYPE(chunk) != T_STRING) chunk = rb_funcall(chunk, ID_to_s, 0);
|
1279
|
+
|
1280
|
+
VALUE len_string = rb_str_new_literal("");
|
1281
|
+
rb_str_modify_expand(len_string, 16);
|
1282
|
+
int len_string_len = sprintf(RSTRING_PTR(len_string), "%lx\r\n", RSTRING_LEN(chunk));
|
1283
|
+
rb_str_set_len(len_string,len_string_len);
|
1284
|
+
|
1285
|
+
VALUE total_written = rb_funcall(io, ID_write, 3, len_string, chunk, STR_CRLF);
|
1286
|
+
|
1287
|
+
RB_GC_GUARD(len_string);
|
1288
|
+
RB_GC_GUARD(chunk);
|
1289
|
+
return total_written;
|
1290
|
+
}
|
1291
|
+
else {
|
1292
|
+
return rb_funcall(io, ID_write, 1, STR_EMPTY_CHUNK);
|
1293
|
+
}
|
1294
|
+
}
|
1295
|
+
|
1296
|
+
/* call-seq: H1P.send_chunked_response(io, headers, body = nil) -> total_written
|
1297
|
+
*
|
1298
|
+
* Sends an HTTP response with the given headers and body using chunked transfer
|
1299
|
+
* encoding.
|
1300
|
+
*/
|
1301
|
+
VALUE H1P_send_chunked_response(VALUE self, VALUE io, VALUE headers) {
|
1302
|
+
VALUE buffer = rb_str_new_literal("");
|
1303
|
+
rb_str_modify_expand(buffer, MAX_RESPONSE_BUFFER_SIZE);
|
1304
|
+
send_response_ctx ctx = {io, buffer, RSTRING_PTR(buffer), 0, 0};
|
1305
|
+
|
1306
|
+
VALUE protocol = rb_hash_aref(headers, STR_pseudo_protocol);
|
1307
|
+
if (protocol == Qnil) protocol = STR_pseudo_protocol_default;
|
1308
|
+
VALUE status = rb_hash_aref(headers, STR_pseudo_status);
|
1309
|
+
if (status == Qnil) status = STR_pseudo_status_default;
|
1310
|
+
send_response_write_status_line(&ctx, protocol, status);
|
1311
|
+
|
1312
|
+
rb_hash_aset(headers, STR_transfer_encoding_capitalized, STR_chunked);
|
1313
|
+
rb_hash_foreach(headers, send_response_write_header, (VALUE)&ctx);
|
1314
|
+
|
1315
|
+
ctx.buffer_ptr[ctx.buffer_len] = '\r';
|
1316
|
+
ctx.buffer_ptr[ctx.buffer_len + 1] = '\n';
|
1317
|
+
ctx.buffer_len += 2;
|
1318
|
+
send_response_flush_buffer(&ctx);
|
1319
|
+
|
1320
|
+
VALUE len_string = rb_str_new_literal("");
|
1321
|
+
rb_str_modify_expand(len_string, 16);
|
1322
|
+
while (1) {
|
1323
|
+
VALUE chunk = rb_yield(Qnil);
|
1324
|
+
if (chunk == Qnil) {
|
1325
|
+
VALUE written = rb_funcall(io, ID_write, 1, STR_EMPTY_CHUNK);
|
1326
|
+
ctx.total_written += NUM2INT(written);
|
1327
|
+
break;
|
1328
|
+
}
|
1329
|
+
else {
|
1330
|
+
if (TYPE(chunk) != T_STRING) chunk = rb_funcall(chunk, ID_to_s, 0);
|
1331
|
+
|
1332
|
+
int len_string_len = sprintf(RSTRING_PTR(len_string), "%lx\r\n", RSTRING_LEN(chunk));
|
1333
|
+
rb_str_set_len(len_string,len_string_len);
|
1334
|
+
VALUE written = rb_funcall(io, ID_write, 3, len_string, chunk, STR_CRLF);
|
1335
|
+
ctx.total_written += NUM2INT(written);
|
1336
|
+
}
|
1337
|
+
RB_GC_GUARD(chunk);
|
1338
|
+
}
|
1339
|
+
|
1340
|
+
RB_GC_GUARD(len_string);
|
1341
|
+
RB_GC_GUARD(buffer);
|
1342
|
+
|
1343
|
+
return INT2FIX(ctx.total_written);
|
1344
|
+
}
|
1345
|
+
|
1346
|
+
void Init_H1P(void) {
|
956
1347
|
VALUE mH1P;
|
957
1348
|
VALUE cParser;
|
958
1349
|
|
@@ -964,51 +1355,73 @@ void Init_H1P() {
|
|
964
1355
|
cError = rb_define_class_under(mH1P, "Error", rb_eRuntimeError);
|
965
1356
|
rb_gc_register_mark_object(cError);
|
966
1357
|
|
967
|
-
// backend methods
|
968
1358
|
rb_define_method(cParser, "initialize", Parser_initialize, 2);
|
969
1359
|
rb_define_method(cParser, "parse_headers", Parser_parse_headers, 0);
|
970
1360
|
rb_define_method(cParser, "read_body", Parser_read_body, 0);
|
971
1361
|
rb_define_method(cParser, "read_body_chunk", Parser_read_body_chunk, 1);
|
1362
|
+
rb_define_method(cParser, "splice_body_to", Parser_splice_body_to, 1);
|
972
1363
|
rb_define_method(cParser, "complete?", Parser_complete_p, 0);
|
973
1364
|
|
1365
|
+
rb_define_singleton_method(mH1P, "send_response", H1P_send_response, -1);
|
1366
|
+
rb_define_singleton_method(mH1P, "send_body_chunk", H1P_send_body_chunk, 2);
|
1367
|
+
rb_define_singleton_method(mH1P, "send_chunked_response", H1P_send_chunked_response, 2);
|
1368
|
+
|
974
1369
|
ID_arity = rb_intern("arity");
|
975
1370
|
ID_backend_read = rb_intern("backend_read");
|
976
1371
|
ID_backend_recv = rb_intern("backend_recv");
|
1372
|
+
ID_backend_send = rb_intern("backend_send");
|
1373
|
+
ID_backend_splice = rb_intern("backend_splice");
|
1374
|
+
ID_backend_write = rb_intern("backend_write");
|
977
1375
|
ID_call = rb_intern("call");
|
978
1376
|
ID_downcase = rb_intern("downcase");
|
979
1377
|
ID_eof_p = rb_intern("eof?");
|
980
1378
|
ID_eq = rb_intern("==");
|
981
|
-
ID_read_method
|
1379
|
+
ID_read_method = rb_intern("__read_method__");
|
982
1380
|
ID_read = rb_intern("read");
|
983
1381
|
ID_readpartial = rb_intern("readpartial");
|
984
1382
|
ID_to_i = rb_intern("to_i");
|
1383
|
+
ID_to_s = rb_intern("to_s");
|
985
1384
|
ID_upcase = rb_intern("upcase");
|
1385
|
+
ID_write = rb_intern("write");
|
1386
|
+
ID_write_method = rb_intern("__write_method__");
|
1387
|
+
|
1388
|
+
NUM_max_headers_read_length = INT2FIX(MAX_HEADERS_READ_LENGTH);
|
1389
|
+
NUM_buffer_start = INT2FIX(0);
|
1390
|
+
NUM_buffer_end = INT2FIX(-1);
|
1391
|
+
|
1392
|
+
GLOBAL_STR(STR_pseudo_method, ":method");
|
1393
|
+
GLOBAL_STR(STR_pseudo_path, ":path");
|
1394
|
+
GLOBAL_STR(STR_pseudo_protocol, ":protocol");
|
1395
|
+
GLOBAL_STR(STR_pseudo_protocol_default, "HTTP/1.1");
|
1396
|
+
GLOBAL_STR(STR_pseudo_rx, ":rx");
|
1397
|
+
GLOBAL_STR(STR_pseudo_status, ":status");
|
1398
|
+
GLOBAL_STR(STR_pseudo_status_default, "200 OK");
|
1399
|
+
GLOBAL_STR(STR_pseudo_status_message, ":status_message");
|
1400
|
+
|
1401
|
+
GLOBAL_STR(STR_chunked, "chunked");
|
1402
|
+
GLOBAL_STR(STR_content_length, "content-length");
|
1403
|
+
GLOBAL_STR(STR_content_length_capitalized, "Content-Length");
|
1404
|
+
GLOBAL_STR(STR_transfer_encoding, "transfer-encoding");
|
1405
|
+
GLOBAL_STR(STR_transfer_encoding_capitalized, "Transfer-Encoding");
|
1406
|
+
|
1407
|
+
GLOBAL_STR(STR_CRLF, "\r\n");
|
1408
|
+
GLOBAL_STR(STR_EMPTY_CHUNK, "0\r\n\r\n");
|
1409
|
+
|
1410
|
+
SYM_backend_read = ID2SYM(ID_backend_read);
|
1411
|
+
SYM_backend_recv = ID2SYM(ID_backend_recv);
|
1412
|
+
SYM_backend_send = ID2SYM(ID_backend_send);
|
1413
|
+
SYM_backend_write = ID2SYM(ID_backend_write);
|
986
1414
|
|
987
|
-
NUM_max_headers_read_length = INT2NUM(MAX_HEADERS_READ_LENGTH);
|
988
|
-
NUM_buffer_start = INT2NUM(0);
|
989
|
-
NUM_buffer_end = INT2NUM(-1);
|
990
|
-
|
991
|
-
GLOBAL_STR(STR_pseudo_method, ":method");
|
992
|
-
GLOBAL_STR(STR_pseudo_path, ":path");
|
993
|
-
GLOBAL_STR(STR_pseudo_protocol, ":protocol");
|
994
|
-
GLOBAL_STR(STR_pseudo_rx, ":rx");
|
995
|
-
GLOBAL_STR(STR_pseudo_status, ":status");
|
996
|
-
GLOBAL_STR(STR_pseudo_status_message, ":status_message");
|
997
|
-
|
998
|
-
GLOBAL_STR(STR_chunked, "chunked");
|
999
|
-
GLOBAL_STR(STR_content_length, "content-length");
|
1000
|
-
GLOBAL_STR(STR_transfer_encoding, "transfer-encoding");
|
1001
|
-
|
1002
|
-
SYM_backend_read = ID2SYM(ID_backend_read);
|
1003
|
-
SYM_backend_recv = ID2SYM(ID_backend_recv);
|
1004
1415
|
SYM_stock_readpartial = ID2SYM(rb_intern("stock_readpartial"));
|
1005
1416
|
|
1006
1417
|
SYM_client = ID2SYM(rb_intern("client"));
|
1007
1418
|
SYM_server = ID2SYM(rb_intern("server"));
|
1008
1419
|
|
1009
1420
|
rb_global_variable(&mH1P);
|
1421
|
+
|
1422
|
+
eArgumentError = rb_const_get(rb_cObject, rb_intern("ArgumentError"));
|
1010
1423
|
}
|
1011
1424
|
|
1012
|
-
void Init_h1p_ext() {
|
1425
|
+
void Init_h1p_ext(void) {
|
1013
1426
|
Init_H1P();
|
1014
1427
|
}
|
data/ext/h1p/h1p.h
CHANGED
@@ -14,5 +14,10 @@
|
|
14
14
|
for (unsigned long i = 0; i < size; i++) printf("%s\n", strings[i]); \
|
15
15
|
free(strings); \
|
16
16
|
}
|
17
|
+
#define PRINT_BUFFER(prefix, ptr, len) { \
|
18
|
+
printf("%s buffer (%d): ", prefix, (int)len); \
|
19
|
+
for (int i = 0; i < len; i++) printf("%02X ", ptr[i]); \
|
20
|
+
printf("\n"); \
|
21
|
+
}
|
17
22
|
|
18
23
|
#endif /* H1P_H */
|
data/h1p.gemspec
CHANGED
@@ -16,9 +16,9 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.extra_rdoc_files = ["README.md"]
|
17
17
|
s.extensions = ["ext/h1p/extconf.rb"]
|
18
18
|
s.require_paths = ["lib"]
|
19
|
-
s.required_ruby_version = '>= 2.
|
19
|
+
s.required_ruby_version = '>= 2.7'
|
20
20
|
|
21
|
-
s.add_development_dependency 'rake-compiler', '1.
|
21
|
+
s.add_development_dependency 'rake-compiler', '1.2.1'
|
22
22
|
s.add_development_dependency 'rake', '~>13.0.6'
|
23
|
-
s.add_development_dependency 'minitest', '~>5.
|
23
|
+
s.add_development_dependency 'minitest', '~>5.17.0'
|
24
24
|
end
|
data/lib/h1p/version.rb
CHANGED
data/lib/h1p.rb
CHANGED
@@ -2,28 +2,34 @@
|
|
2
2
|
|
3
3
|
require_relative './h1p_ext'
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
class ::IO
|
6
|
+
if !method_defined?(:__read_method__)
|
7
7
|
def __read_method__
|
8
8
|
:stock_readpartial
|
9
9
|
end
|
10
10
|
end
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
+
require 'socket'
|
13
14
|
|
14
|
-
|
15
|
+
class Socket
|
16
|
+
if !method_defined?(:__read_method__)
|
15
17
|
def __read_method__
|
16
18
|
:stock_readpartial
|
17
19
|
end
|
18
20
|
end
|
21
|
+
end
|
19
22
|
|
20
|
-
|
23
|
+
class TCPSocket
|
24
|
+
if !method_defined?(:__read_method__)
|
21
25
|
def __read_method__
|
22
26
|
:stock_readpartial
|
23
27
|
end
|
24
28
|
end
|
29
|
+
end
|
25
30
|
|
26
|
-
|
31
|
+
class UNIXSocket
|
32
|
+
if !method_defined?(:__read_method__)
|
27
33
|
def __read_method__
|
28
34
|
:stock_readpartial
|
29
35
|
end
|
data/test/helper.rb
CHANGED
data/test/test_h1p.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
require 'h1p'
|
5
|
+
|
6
|
+
class SendResponseTest < MiniTest::Test
|
7
|
+
def test_send_response_status_line
|
8
|
+
i, o = IO.pipe
|
9
|
+
H1P.send_response(o, { ':status' => '418 I\'m a teapot' })
|
10
|
+
o.close
|
11
|
+
response = i.read
|
12
|
+
assert_equal "HTTP/1.1 418 I'm a teapot\r\n\r\n", response
|
13
|
+
|
14
|
+
i, o = IO.pipe
|
15
|
+
count = H1P.send_response(o, { ':protocol' => 'HTTP/1.0' })
|
16
|
+
o.close
|
17
|
+
response = i.read
|
18
|
+
assert_equal "HTTP/1.0 200 OK\r\n\r\n", response
|
19
|
+
assert_equal "HTTP/1.0 200 OK\r\n\r\n".bytesize, count
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_send_response_string_headers
|
23
|
+
i, o = IO.pipe
|
24
|
+
H1P.send_response(o, { 'Foo' => 'Bar', 'Content-Length' => '123' })
|
25
|
+
o.close
|
26
|
+
response = i.read
|
27
|
+
assert_equal "HTTP/1.1 200 OK\r\nFoo: Bar\r\nContent-Length: 123\r\n\r\n", response
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_send_response_non_string_headers
|
31
|
+
i, o = IO.pipe
|
32
|
+
H1P.send_response(o, { :Foo => 'Bar', 'Content-Length' => 123 })
|
33
|
+
o.close
|
34
|
+
response = i.read
|
35
|
+
assert_equal "HTTP/1.1 200 OK\r\nFoo: Bar\r\nContent-Length: 123\r\n\r\n", response
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_send_response_with_body
|
39
|
+
i, o = IO.pipe
|
40
|
+
H1P.send_response(o, {}, "foobar")
|
41
|
+
o.close
|
42
|
+
response = i.read
|
43
|
+
assert_equal "HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nfoobar", response
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_send_response_with_big_body
|
47
|
+
i, o = IO.pipe
|
48
|
+
body = "abcdefg" * 10000
|
49
|
+
Thread.new { H1P.send_response(o, {}, body); o.close }
|
50
|
+
|
51
|
+
response = i.read
|
52
|
+
assert_equal "HTTP/1.1 200 OK\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}", response
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_send_response_with_big_body
|
56
|
+
i, o = IO.pipe
|
57
|
+
body = "abcdefg" * 10000
|
58
|
+
Thread.new { H1P.send_response(o, {}, body); o.close }
|
59
|
+
|
60
|
+
response = i.read
|
61
|
+
assert_equal "HTTP/1.1 200 OK\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}", response
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class SendBodyChunkTest < MiniTest::Test
|
66
|
+
def test_send_body_chunk
|
67
|
+
i, o = IO.pipe
|
68
|
+
len1 = H1P.send_body_chunk(o, 'foo')
|
69
|
+
assert_equal 8, len1
|
70
|
+
len2 = H1P.send_body_chunk(o, :barbazbarbaz)
|
71
|
+
assert_equal 17, len2
|
72
|
+
len3 = H1P.send_body_chunk(o, 1234)
|
73
|
+
assert_equal 9, len3
|
74
|
+
len4 = H1P.send_body_chunk(o, nil)
|
75
|
+
assert_equal 5, len4
|
76
|
+
o.close
|
77
|
+
response = i.read
|
78
|
+
assert_equal "3\r\nfoo\r\nc\r\nbarbazbarbaz\r\n4\r\n1234\r\n0\r\n\r\n", response
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_send_body_chunk_big
|
82
|
+
i, o = IO.pipe
|
83
|
+
|
84
|
+
chunk = 'foobar1' * 20000
|
85
|
+
len = nil
|
86
|
+
|
87
|
+
Thread.new do
|
88
|
+
len = H1P.send_body_chunk(o, chunk)
|
89
|
+
o.close
|
90
|
+
end
|
91
|
+
|
92
|
+
response = i.read
|
93
|
+
assert_equal "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n", response
|
94
|
+
assert_equal chunk.bytesize + chunk.bytesize.to_s(16).bytesize + 4, len
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class SendChunkedResponseTest < MiniTest::Test
|
99
|
+
def test_send_chunked_response
|
100
|
+
isrc, osrc = IO.pipe
|
101
|
+
osrc << 'foobarbaz'
|
102
|
+
osrc.close
|
103
|
+
|
104
|
+
i, o = IO.pipe
|
105
|
+
len = H1P.send_chunked_response(o, { 'Foo' => 'bar' }) do
|
106
|
+
isrc.read(3)
|
107
|
+
end
|
108
|
+
o.close
|
109
|
+
|
110
|
+
response = i.read
|
111
|
+
assert_equal "HTTP/1.1 200 OK\r\nFoo: bar\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n3\r\nbar\r\n3\r\nbaz\r\n0\r\n\r\n", response
|
112
|
+
assert_equal len, response.bytesize
|
113
|
+
end
|
114
|
+
end
|
data/test/test_h1p_server.rb
CHANGED
@@ -4,8 +4,9 @@ require_relative 'helper'
|
|
4
4
|
require 'h1p'
|
5
5
|
require 'socket'
|
6
6
|
require_relative '../ext/h1p/limits'
|
7
|
+
require 'securerandom'
|
7
8
|
|
8
|
-
class
|
9
|
+
class H1PServerTest < MiniTest::Test
|
9
10
|
Error = H1P::Error
|
10
11
|
|
11
12
|
def setup
|
@@ -446,6 +447,60 @@ class H1PRequestTest < MiniTest::Test
|
|
446
447
|
assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
|
447
448
|
end
|
448
449
|
|
450
|
+
PolyphonyMockup = Object.new
|
451
|
+
def PolyphonyMockup.backend_write(io, buf)
|
452
|
+
io << buf
|
453
|
+
end
|
454
|
+
def PolyphonyMockup.backend_splice(src, dest, len)
|
455
|
+
buf = src.read(len)
|
456
|
+
len = dest.write(buf)
|
457
|
+
len
|
458
|
+
end
|
459
|
+
Object::Polyphony = PolyphonyMockup
|
460
|
+
|
461
|
+
def test_splice_body_to_chunked_encoding
|
462
|
+
req_body = SecureRandom.alphanumeric(60000)
|
463
|
+
req_headers = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
464
|
+
r, w = IO.pipe
|
465
|
+
|
466
|
+
Thread.new do
|
467
|
+
@o << req_headers
|
468
|
+
@o << "#{req_body.bytesize.to_s(16)}\r\n"
|
469
|
+
@o << req_body
|
470
|
+
@o << "\r\n0\r\n\r\n"
|
471
|
+
@o.close
|
472
|
+
end
|
473
|
+
def w.__write_method__; :backend_write; end
|
474
|
+
|
475
|
+
headers = @parser.parse_headers
|
476
|
+
@parser.splice_body_to(w)
|
477
|
+
w.close
|
478
|
+
assert_equal req_body, r.read
|
479
|
+
|
480
|
+
chunk_header_size = "#{req_body.bytesize.to_s(16)}\r\n".bytesize + "\r\n0\r\n\r\n".bytesize
|
481
|
+
assert_equal req_headers.bytesize + req_body.bytesize + chunk_header_size, headers[':rx']
|
482
|
+
end
|
483
|
+
|
484
|
+
def test_splice_body_to_content_length
|
485
|
+
req_body = SecureRandom.alphanumeric(60000)
|
486
|
+
req_headers = "POST / HTTP/1.1\r\nContent-Length: #{req_body.bytesize}\r\n\r\n"
|
487
|
+
r, w = IO.pipe
|
488
|
+
|
489
|
+
Thread.new do
|
490
|
+
@o << req_headers
|
491
|
+
@o << req_body
|
492
|
+
@o.close
|
493
|
+
end
|
494
|
+
def w.__write_method__; :backend_write; end
|
495
|
+
|
496
|
+
headers = @parser.parse_headers
|
497
|
+
@parser.splice_body_to(w)
|
498
|
+
w.close
|
499
|
+
assert_equal req_body, r.read
|
500
|
+
|
501
|
+
assert_equal req_headers.bytesize + req_body.bytesize, headers[':rx']
|
502
|
+
end
|
503
|
+
|
449
504
|
def test_complete?
|
450
505
|
@o << "GET / HTTP/1.1\r\n\r\n"
|
451
506
|
headers = @parser.parse_headers
|
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.6'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.
|
19
|
+
version: 1.2.1
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 1.
|
26
|
+
version: 1.2.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 5.
|
47
|
+
version: 5.17.0
|
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.
|
54
|
+
version: 5.17.0
|
55
55
|
description:
|
56
56
|
email: sharon@noteflakes.com
|
57
57
|
executables: []
|
@@ -82,6 +82,7 @@ files:
|
|
82
82
|
- lib/h1p/version.rb
|
83
83
|
- test/helper.rb
|
84
84
|
- test/run.rb
|
85
|
+
- test/test_h1p.rb
|
85
86
|
- test/test_h1p_client.rb
|
86
87
|
- test/test_h1p_server.rb
|
87
88
|
homepage: http://github.com/digital-fabric/h1p
|
@@ -101,14 +102,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
102
|
requirements:
|
102
103
|
- - ">="
|
103
104
|
- !ruby/object:Gem::Version
|
104
|
-
version: '2.
|
105
|
+
version: '2.7'
|
105
106
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
107
|
requirements:
|
107
108
|
- - ">="
|
108
109
|
- !ruby/object:Gem::Version
|
109
110
|
version: '0'
|
110
111
|
requirements: []
|
111
|
-
rubygems_version: 3.1
|
112
|
+
rubygems_version: 3.4.1
|
112
113
|
signing_key:
|
113
114
|
specification_version: 4
|
114
115
|
summary: H1P is a blocking HTTP/1 parser for Ruby
|