h1p 0.5 → 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: 53aadd6f6c4ae112ff844b68e8325fe334d51dd0a09bb11cc54d017190e97677
4
- data.tar.gz: e9f6d504813d74c050a46b4e973e9fbc227dd790c02395a0073b62f43a7393da
3
+ metadata.gz: 90f7937fd6bcefdd1627c208bf73054fb3fb03a855666c7247f04aac4398d280
4
+ data.tar.gz: ab6a866b843beaf92137b991048db9af006f4cb5e510e211dc6ba6652bbc84a2
5
5
  SHA512:
6
- metadata.gz: 3ac98c2d7e702f8cf5f9052e78e9abe486ee3ba0814c4c16fe222acdccfb5338ac9d5c9b4a5d1da2178fca65472693c94a4997cc86de05db70f13c90f1bc767e
7
- data.tar.gz: 73ce78e3435eb40f3e6d46ee8eef803284e8da99f32d610dbd723c3661548a74ed454ae70e1ec02dcfbdb70aa346059fb7713eab19de3e6e6e65a4ca859926c7
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,10 @@
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
+
1
8
  ## 0.5 2022-03-19
2
9
 
3
10
  - Implement `Parser#splice_body_to` (#3)
data/Gemfile.lock CHANGED
@@ -1,14 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- h1p (0.5)
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
 
@@ -29,6 +28,8 @@ The H1P was originally written as part of
29
28
  - Support for **splicing** request/response bodies (when used with
30
29
  [Polyphony](https://github.com/digital-fabric/polyphony))
31
30
  - Track total incoming traffic
31
+ - Write HTTP requests and responses to any IO instance, with support for chunked
32
+ transfer encoding.
32
33
 
33
34
  ## Installing
34
35
 
@@ -221,7 +222,54 @@ as they conform to one of two alternative interfaces:
221
222
  #=> {":method"=>"get", ":path"=>"/foo", ":protocol"=>"http/1.1", ":rx"=>21}
222
223
  ```
223
224
 
224
- ## 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
225
273
 
226
274
  The H1P parser design is based on the following principles:
227
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
@@ -27,11 +26,15 @@ ID ID_read_method;
27
26
  ID ID_read;
28
27
  ID ID_readpartial;
29
28
  ID ID_to_i;
29
+ ID ID_to_s;
30
30
  ID ID_upcase;
31
+ ID ID_write;
31
32
  ID ID_write_method;
32
33
 
33
34
  static VALUE cError;
34
35
 
36
+ VALUE eArgumentError;
37
+
35
38
  VALUE NUM_max_headers_read_length;
36
39
  VALUE NUM_buffer_start;
37
40
  VALUE NUM_buffer_end;
@@ -39,13 +42,20 @@ VALUE NUM_buffer_end;
39
42
  VALUE STR_pseudo_method;
40
43
  VALUE STR_pseudo_path;
41
44
  VALUE STR_pseudo_protocol;
45
+ VALUE STR_pseudo_protocol_default;
42
46
  VALUE STR_pseudo_rx;
43
47
  VALUE STR_pseudo_status;
48
+ VALUE STR_pseudo_status_default;
44
49
  VALUE STR_pseudo_status_message;
45
50
 
46
51
  VALUE STR_chunked;
47
52
  VALUE STR_content_length;
53
+ VALUE STR_content_length_capitalized;
48
54
  VALUE STR_transfer_encoding;
55
+ VALUE STR_transfer_encoding_capitalized;
56
+
57
+ VALUE STR_CRLF;
58
+ VALUE STR_EMPTY_CHUNK;
49
59
 
50
60
  VALUE SYM_backend_read;
51
61
  VALUE SYM_backend_recv;
@@ -125,7 +135,7 @@ static VALUE Parser_allocate(VALUE klass) {
125
135
  #define GetParser(obj, parser) \
126
136
  TypedData_Get_Struct((obj), Parser_t, &Parser_type, (parser))
127
137
 
128
- static inline VALUE Polyphony() {
138
+ static inline VALUE Polyphony(void) {
129
139
  static VALUE mPolyphony = Qnil;
130
140
  if (mPolyphony == Qnil) {
131
141
  mPolyphony = rb_const_get(rb_cObject, rb_intern("Polyphony"));
@@ -166,6 +176,12 @@ enum parser_mode parse_parser_mode(VALUE mode) {
166
176
  rb_raise(rb_eRuntimeError, "Invalid parser mode specified");
167
177
  }
168
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
+ */
169
185
  VALUE Parser_initialize(VALUE self, VALUE io, VALUE mode) {
170
186
  Parser_t *parser;
171
187
  GetParser(self, parser);
@@ -706,6 +722,19 @@ eof:
706
722
  return 0;
707
723
  }
708
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
+ */
709
738
  VALUE Parser_parse_headers(VALUE self) {
710
739
  Parser_t *parser;
711
740
  GetParser(self, parser);
@@ -1016,7 +1045,6 @@ void splice_body_with_content_length(Parser_t *parser, VALUE dest, enum write_me
1016
1045
  parser->current_request_rx += spliced;
1017
1046
  parser->body_left -= spliced;
1018
1047
  }
1019
- done:
1020
1048
  rb_hash_aset(parser->headers, STR_pseudo_rx, INT2FIX(parser->current_request_rx));
1021
1049
  return;
1022
1050
  eof:
@@ -1056,14 +1084,29 @@ static inline VALUE read_body(VALUE self, int read_entire_body, int buffered_onl
1056
1084
  return read_body_with_content_length(parser, read_entire_body, buffered_only);
1057
1085
  }
1058
1086
 
1087
+ /* call-seq: parser.read_body -> body
1088
+ *
1089
+ * Reads an HTTP request/response body from the associated IO instance.
1090
+ */
1059
1091
  VALUE Parser_read_body(VALUE self) {
1060
1092
  return read_body(self, 1, 0);
1061
1093
  }
1062
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
+ */
1063
1101
  VALUE Parser_read_body_chunk(VALUE self, VALUE buffered_only) {
1064
1102
  return read_body(self, 0, buffered_only == Qtrue);
1065
1103
  }
1066
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
+ */
1067
1110
  VALUE Parser_splice_body_to(VALUE self, VALUE dest) {
1068
1111
  Parser_t *parser;
1069
1112
  GetParser(self, parser);
@@ -1080,6 +1123,11 @@ VALUE Parser_splice_body_to(VALUE self, VALUE dest) {
1080
1123
  return self;
1081
1124
  }
1082
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
+ */
1083
1131
  VALUE Parser_complete_p(VALUE self) {
1084
1132
  Parser_t *parser;
1085
1133
  GetParser(self, parser);
@@ -1090,7 +1138,212 @@ VALUE Parser_complete_p(VALUE self) {
1090
1138
  return parser->request_completed ? Qtrue : Qfalse;
1091
1139
  }
1092
1140
 
1093
- 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) {
1094
1347
  VALUE mH1P;
1095
1348
  VALUE cParser;
1096
1349
 
@@ -1102,7 +1355,6 @@ void Init_H1P() {
1102
1355
  cError = rb_define_class_under(mH1P, "Error", rb_eRuntimeError);
1103
1356
  rb_gc_register_mark_object(cError);
1104
1357
 
1105
- // backend methods
1106
1358
  rb_define_method(cParser, "initialize", Parser_initialize, 2);
1107
1359
  rb_define_method(cParser, "parse_headers", Parser_parse_headers, 0);
1108
1360
  rb_define_method(cParser, "read_body", Parser_read_body, 0);
@@ -1110,6 +1362,10 @@ void Init_H1P() {
1110
1362
  rb_define_method(cParser, "splice_body_to", Parser_splice_body_to, 1);
1111
1363
  rb_define_method(cParser, "complete?", Parser_complete_p, 0);
1112
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
+
1113
1369
  ID_arity = rb_intern("arity");
1114
1370
  ID_backend_read = rb_intern("backend_read");
1115
1371
  ID_backend_recv = rb_intern("backend_recv");
@@ -1124,23 +1380,32 @@ void Init_H1P() {
1124
1380
  ID_read = rb_intern("read");
1125
1381
  ID_readpartial = rb_intern("readpartial");
1126
1382
  ID_to_i = rb_intern("to_i");
1383
+ ID_to_s = rb_intern("to_s");
1127
1384
  ID_upcase = rb_intern("upcase");
1385
+ ID_write = rb_intern("write");
1128
1386
  ID_write_method = rb_intern("__write_method__");
1129
1387
 
1130
1388
  NUM_max_headers_read_length = INT2FIX(MAX_HEADERS_READ_LENGTH);
1131
1389
  NUM_buffer_start = INT2FIX(0);
1132
1390
  NUM_buffer_end = INT2FIX(-1);
1133
1391
 
1134
- GLOBAL_STR(STR_pseudo_method, ":method");
1135
- GLOBAL_STR(STR_pseudo_path, ":path");
1136
- GLOBAL_STR(STR_pseudo_protocol, ":protocol");
1137
- GLOBAL_STR(STR_pseudo_rx, ":rx");
1138
- GLOBAL_STR(STR_pseudo_status, ":status");
1139
- GLOBAL_STR(STR_pseudo_status_message, ":status_message");
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");
1140
1406
 
1141
- GLOBAL_STR(STR_chunked, "chunked");
1142
- GLOBAL_STR(STR_content_length, "content-length");
1143
- GLOBAL_STR(STR_transfer_encoding, "transfer-encoding");
1407
+ GLOBAL_STR(STR_CRLF, "\r\n");
1408
+ GLOBAL_STR(STR_EMPTY_CHUNK, "0\r\n\r\n");
1144
1409
 
1145
1410
  SYM_backend_read = ID2SYM(ID_backend_read);
1146
1411
  SYM_backend_recv = ID2SYM(ID_backend_recv);
@@ -1153,8 +1418,10 @@ void Init_H1P() {
1153
1418
  SYM_server = ID2SYM(rb_intern("server"));
1154
1419
 
1155
1420
  rb_global_variable(&mH1P);
1421
+
1422
+ eArgumentError = rb_const_get(rb_cObject, rb_intern("ArgumentError"));
1156
1423
  }
1157
1424
 
1158
- void Init_h1p_ext() {
1425
+ void Init_h1p_ext(void) {
1159
1426
  Init_H1P();
1160
1427
  }
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.5'
4
+ VERSION = '0.6'
5
5
  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
@@ -6,7 +6,7 @@ require 'socket'
6
6
  require_relative '../ext/h1p/limits'
7
7
  require 'securerandom'
8
8
 
9
- class H1PRequestTest < MiniTest::Test
9
+ class H1PServerTest < MiniTest::Test
10
10
  Error = H1P::Error
11
11
 
12
12
  def setup
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.5'
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-03-22 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