h1p 0.4 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
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