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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a27a3323482c8a0cec845e60962516b755338ccc54d0ba76bf6716bbb449a8ff
4
- data.tar.gz: 22613ade68e261e3fca56cafcab410b1bed4587efe8c1da4ef52d1d335bc7efd
3
+ metadata.gz: 90f7937fd6bcefdd1627c208bf73054fb3fb03a855666c7247f04aac4398d280
4
+ data.tar.gz: ab6a866b843beaf92137b991048db9af006f4cb5e510e211dc6ba6652bbc84a2
5
5
  SHA512:
6
- metadata.gz: 36cb9020745437627ad3dca5f83de6f81f8ad6e04075909db192a509b9f022960c5a7768c1f1ab8fdb807ae0d7b54e6febbc76581e68475c5f27057ff5b7aeb5
7
- data.tar.gz: 20943cfead5e0236e60cb3a0d70057c69a91bbde45973f0f4ff87f38a1866523c1a02132254eebc13b8c5e2c4f75cd8e5791246257a792f63391cff88a6e1011
6
+ metadata.gz: 101e6e13aeed0cf2250dbe962c0c9c9edc5bf69e6ff2a59ee0d02018992c9b76dcaa4be3ddb44bbcb2272e355278a0b717c51050a2786cc95c00fbdb23bc40fa
7
+ data.tar.gz: 2d3130c067d51f173873d6181b76999dd897c241dcafc5fa3f69ec0837404b2fd7e292cadc2fbf9e27003e3dfd933dc394799abf03f12187d6c80e4f2dab83d6
@@ -8,17 +8,18 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest]
11
- ruby: [2.6, 2.7, 3.0]
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@v1
19
- - uses: actions/setup-ruby@v1
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)
4
+ h1p (0.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- minitest (5.14.4)
9
+ minitest (5.17.0)
10
10
  rake (13.0.6)
11
- rake-compiler (1.1.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.14.4)
19
+ minitest (~> 5.17.0)
20
20
  rake (~> 13.0.6)
21
- rake-compiler (= 1.1.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 - a blocking HTTP/1 parser for Ruby
1
+ # H1P - HTTP/1 tools 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)
@@ -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
- > 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.
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
- ## Design
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
- method_readpartial, // receiver.readpartial(len, buf, pos, raise_on_eof: false) (Polyphony-specific)
56
- method_backend_read, // Polyphony.backend_read (Polyphony-specific)
57
- method_backend_recv, // Polyphony.backend_recv (Polyphony-specific)
58
- method_call, // receiver.call(len) (Universal)
59
- method_stock_readpartial // receiver.readpartial(len)
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 void get_polyphony() {
119
- if (mPolyphony != Qnil) return;
120
-
121
- mPolyphony = rb_const_get(rb_cObject, rb_intern("Polyphony"));
122
- rb_gc_register_mark_object(mPolyphony);
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 method_stock_readpartial;
129
-
130
- get_polyphony();
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 method_readpartial;
154
+ return RM_READPARTIAL;
135
155
  }
136
156
  else if (rb_respond_to(io, ID_call)) {
137
- return method_call;
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, INT2NUM(value)); \
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 method_backend_read:
298
- return rb_funcall(mPolyphony, ID_backend_read, 5, parser->io, buf, maxlen, Qfalse, buf_pos);
299
- case method_backend_recv:
300
- return rb_funcall(mPolyphony, ID_backend_recv, 4, parser->io, buf, maxlen, buf_pos);
301
- case method_readpartial:
302
- return rb_funcall(parser-> io, ID_readpartial, 4, maxlen, buf, buf_pos, Qfalse);
303
- case method_call:
304
- return io_call(parser ->io, maxlen, buf, buf_pos);
305
- case method_stock_readpartial:
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, INT2NUM(read_bytes));
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, INT2NUM(maxlen), Qnil, NUM_buffer_start);
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, INT2NUM(parser->current_request_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, INT2NUM(maxlen), Qnil, NUM_buffer_start);
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, INT2NUM(parser->current_request_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
- return read_body_with_content_length(parser, read_entire_body, buffered_only);
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
- void Init_H1P() {
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 = rb_intern("__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.6'
19
+ s.required_ruby_version = '>= 2.7'
20
20
 
21
- s.add_development_dependency 'rake-compiler', '1.1.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.14.4'
23
+ s.add_development_dependency 'minitest', '~>5.17.0'
24
24
  end
data/lib/h1p/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module H1P
4
- VERSION = '0.4'
4
+ VERSION = '0.6'
5
5
  end
data/lib/h1p.rb CHANGED
@@ -2,28 +2,34 @@
2
2
 
3
3
  require_relative './h1p_ext'
4
4
 
5
- unless Object.const_defined?('Polyphony')
6
- class IO
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
- require 'socket'
13
+ require 'socket'
13
14
 
14
- class Socket
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
- class TCPSocket
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
- class UNIXSocket
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
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/setup'
4
-
5
4
  require 'fileutils'
6
-
7
5
  require 'minitest/autorun'
8
6
 
9
7
  module Minitest::Assertions
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
@@ -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 H1PRequestTest < MiniTest::Test
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'
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: 2022-02-28 00:00:00.000000000 Z
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.1.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.1.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.14.4
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.14.4
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.6'
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.6
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