h1p 0.5 → 0.6.1

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: 53aadd6f6c4ae112ff844b68e8325fe334d51dd0a09bb11cc54d017190e97677
4
- data.tar.gz: e9f6d504813d74c050a46b4e973e9fbc227dd790c02395a0073b62f43a7393da
3
+ metadata.gz: 96197142deeb30d27fe9b3fc9de21a07ed1de8f0a1d3db9b8d967310056bdd12
4
+ data.tar.gz: aa71893e95f7a7e8ba8de8123987445262edd6dbcad7efba12337091a2421d14
5
5
  SHA512:
6
- metadata.gz: 3ac98c2d7e702f8cf5f9052e78e9abe486ee3ba0814c4c16fe222acdccfb5338ac9d5c9b4a5d1da2178fca65472693c94a4997cc86de05db70f13c90f1bc767e
7
- data.tar.gz: 73ce78e3435eb40f3e6d46ee8eef803284e8da99f32d610dbd723c3661548a74ed454ae70e1ec02dcfbdb70aa346059fb7713eab19de3e6e6e65a4ca859926c7
6
+ metadata.gz: 9380e218a2433f61a88676fb807ecb742391dff2c6d7e4937d24ff1385839ed897eaf81eb3d8d1f916bcf53f30cd2fd9c31f0c78fcd6c0e35d3753dc1d3a1252
7
+ data.tar.gz: 2ce69a5e542390018f779e9ba0a73263b362b3a6d69838494da73d18cb6f84a3262ed98fd086bb098ffa253f264db4d3e1d40e5ae8f9757ca1928c36ce9f02ce
@@ -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.1 2023-05-28
2
+
3
+ - Fix sending response with frozen headers hash
4
+
5
+ ## 0.6 2023-01-05
6
+
7
+ - Add documentation
8
+ - Implement `H1P.send_body_chunk`
9
+ - Implement `H1P.send_chunked_response`
10
+ - Implement `H1P.send_response`
11
+
1
12
  ## 0.5 2022-03-19
2
13
 
3
14
  - 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.1)
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,71 @@ 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
234
+ # Some-Header: header value
235
+ #
236
+ # foobar
237
+
238
+ # The :protocol pseudo header sets the protocol in the status line:
239
+ H1P.send_response(socket, { ':protocol' => 'HTTP/0.9' })
240
+ # HTTP/0.9 200 OK
241
+ #
242
+ #
243
+
244
+ # The :status pseudo header sets the response status:
245
+ H1P.send_response(socket, { ':status' => '418 I\'m a teapot' })
246
+ # HTTP/1.1 418 I'm a teapot
247
+ #
248
+ #
249
+ ```
250
+
251
+ To send responses using chunked transfer encoding use
252
+ `H1P.send_chunked_response(io, header, body = nil)`:
253
+
254
+ ```ruby
255
+ H1P.send_chunked_response(socket, {}, "foobar")
256
+ # HTTP/1.1 200 OK
257
+ # Transfer-Encoding: chunked
258
+ # 6
259
+ # foobar
260
+ # 0
261
+ #
262
+ #
263
+ ```
264
+
265
+ You can also call `H1P.send_chunked_response` with a block that provides the
266
+ next chunk to send. The last chunk is signalled by returning `nil` from the
267
+ block:
268
+
269
+ ```ruby
270
+ IO.open('/path/to/file') do |f|
271
+ H1P.send_chunked_response(socket, {}) { f.read(CHUNK_SIZE) }
272
+ end
273
+ ```
274
+
275
+ To send individual chunks use `H1P.send_body_chunk`:
276
+
277
+ ```ruby
278
+ H1P.send_body_chunk(socket, 'foo')
279
+ # 3
280
+ # foo
281
+ #
282
+
283
+ H1P.send_body_chunk(socket, nil)
284
+ # 0
285
+ #
286
+ #
287
+ ```
288
+
289
+ ## Parser Design
225
290
 
226
291
  The H1P parser design is based on the following principles:
227
292
 
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,213 @@ 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
+ send_response_write_header(STR_content_length_capitalized, INT2FIX(bodylen), (VALUE)&ctx);
1244
+
1245
+ char *endptr = ctx.buffer_ptr + ctx.buffer_len;
1246
+ endptr[0] = '\r';
1247
+ endptr[1] = '\n';
1248
+ ctx.buffer_len += 2;
1249
+
1250
+ if (body != Qnil) {
1251
+ while (bodylen > 0) {
1252
+ unsigned int chunklen = bodylen;
1253
+ if (chunklen > MAX_RESPONSE_BUFFER_SIZE) chunklen = MAX_RESPONSE_BUFFER_SIZE;
1254
+
1255
+ if (ctx.buffer_len + chunklen > MAX_RESPONSE_BUFFER_SIZE)
1256
+ send_response_flush_buffer(&ctx);
1257
+
1258
+ memcpy(ctx.buffer_ptr + ctx.buffer_len, bodyptr, chunklen);
1259
+ ctx.buffer_len += chunklen;
1260
+ bodyptr += chunklen;
1261
+ bodylen -= chunklen;
1262
+ }
1263
+ RB_GC_GUARD(body);
1264
+ }
1265
+
1266
+ send_response_flush_buffer(&ctx);
1267
+
1268
+ RB_GC_GUARD(buffer);
1269
+
1270
+ return INT2FIX(ctx.total_written);
1271
+ }
1272
+
1273
+ /* call-seq: H1P.send_body_chunk(io, chunk) -> total_written
1274
+ *
1275
+ * Sends a body chunk using chunked transfer encoding.
1276
+ */
1277
+ VALUE H1P_send_body_chunk(VALUE self, VALUE io, VALUE chunk) {
1278
+ if (chunk != Qnil) {
1279
+ if (TYPE(chunk) != T_STRING) chunk = rb_funcall(chunk, ID_to_s, 0);
1280
+
1281
+ VALUE len_string = rb_str_new_literal("");
1282
+ rb_str_modify_expand(len_string, 16);
1283
+ int len_string_len = sprintf(RSTRING_PTR(len_string), "%lx\r\n", RSTRING_LEN(chunk));
1284
+ rb_str_set_len(len_string,len_string_len);
1285
+
1286
+ VALUE total_written = rb_funcall(io, ID_write, 3, len_string, chunk, STR_CRLF);
1287
+
1288
+ RB_GC_GUARD(len_string);
1289
+ RB_GC_GUARD(chunk);
1290
+ return total_written;
1291
+ }
1292
+ else {
1293
+ return rb_funcall(io, ID_write, 1, STR_EMPTY_CHUNK);
1294
+ }
1295
+ }
1296
+
1297
+ /* call-seq: H1P.send_chunked_response(io, headers, body = nil) -> total_written
1298
+ *
1299
+ * Sends an HTTP response with the given headers and body using chunked transfer
1300
+ * encoding.
1301
+ */
1302
+ VALUE H1P_send_chunked_response(VALUE self, VALUE io, VALUE headers) {
1303
+ VALUE buffer = rb_str_new_literal("");
1304
+ rb_str_modify_expand(buffer, MAX_RESPONSE_BUFFER_SIZE);
1305
+ send_response_ctx ctx = {io, buffer, RSTRING_PTR(buffer), 0, 0};
1306
+
1307
+ VALUE protocol = rb_hash_aref(headers, STR_pseudo_protocol);
1308
+ if (protocol == Qnil) protocol = STR_pseudo_protocol_default;
1309
+ VALUE status = rb_hash_aref(headers, STR_pseudo_status);
1310
+ if (status == Qnil) status = STR_pseudo_status_default;
1311
+ send_response_write_status_line(&ctx, protocol, status);
1312
+
1313
+ rb_hash_foreach(headers, send_response_write_header, (VALUE)&ctx);
1314
+ send_response_write_header(STR_transfer_encoding_capitalized, STR_chunked, (VALUE)&ctx);
1315
+
1316
+ ctx.buffer_ptr[ctx.buffer_len] = '\r';
1317
+ ctx.buffer_ptr[ctx.buffer_len + 1] = '\n';
1318
+ ctx.buffer_len += 2;
1319
+ send_response_flush_buffer(&ctx);
1320
+
1321
+ VALUE len_string = rb_str_new_literal("");
1322
+ rb_str_modify_expand(len_string, 16);
1323
+ while (1) {
1324
+ VALUE chunk = rb_yield(Qnil);
1325
+ if (chunk == Qnil) {
1326
+ VALUE written = rb_funcall(io, ID_write, 1, STR_EMPTY_CHUNK);
1327
+ ctx.total_written += NUM2INT(written);
1328
+ break;
1329
+ }
1330
+ else {
1331
+ if (TYPE(chunk) != T_STRING) chunk = rb_funcall(chunk, ID_to_s, 0);
1332
+
1333
+ int len_string_len = sprintf(RSTRING_PTR(len_string), "%lx\r\n", RSTRING_LEN(chunk));
1334
+ rb_str_set_len(len_string,len_string_len);
1335
+ VALUE written = rb_funcall(io, ID_write, 3, len_string, chunk, STR_CRLF);
1336
+ ctx.total_written += NUM2INT(written);
1337
+ }
1338
+ RB_GC_GUARD(chunk);
1339
+ }
1340
+
1341
+ RB_GC_GUARD(len_string);
1342
+ RB_GC_GUARD(buffer);
1343
+
1344
+ return INT2FIX(ctx.total_written);
1345
+ }
1346
+
1347
+ void Init_H1P(void) {
1094
1348
  VALUE mH1P;
1095
1349
  VALUE cParser;
1096
1350
 
@@ -1102,7 +1356,6 @@ void Init_H1P() {
1102
1356
  cError = rb_define_class_under(mH1P, "Error", rb_eRuntimeError);
1103
1357
  rb_gc_register_mark_object(cError);
1104
1358
 
1105
- // backend methods
1106
1359
  rb_define_method(cParser, "initialize", Parser_initialize, 2);
1107
1360
  rb_define_method(cParser, "parse_headers", Parser_parse_headers, 0);
1108
1361
  rb_define_method(cParser, "read_body", Parser_read_body, 0);
@@ -1110,6 +1363,10 @@ void Init_H1P() {
1110
1363
  rb_define_method(cParser, "splice_body_to", Parser_splice_body_to, 1);
1111
1364
  rb_define_method(cParser, "complete?", Parser_complete_p, 0);
1112
1365
 
1366
+ rb_define_singleton_method(mH1P, "send_response", H1P_send_response, -1);
1367
+ rb_define_singleton_method(mH1P, "send_body_chunk", H1P_send_body_chunk, 2);
1368
+ rb_define_singleton_method(mH1P, "send_chunked_response", H1P_send_chunked_response, 2);
1369
+
1113
1370
  ID_arity = rb_intern("arity");
1114
1371
  ID_backend_read = rb_intern("backend_read");
1115
1372
  ID_backend_recv = rb_intern("backend_recv");
@@ -1124,23 +1381,32 @@ void Init_H1P() {
1124
1381
  ID_read = rb_intern("read");
1125
1382
  ID_readpartial = rb_intern("readpartial");
1126
1383
  ID_to_i = rb_intern("to_i");
1384
+ ID_to_s = rb_intern("to_s");
1127
1385
  ID_upcase = rb_intern("upcase");
1386
+ ID_write = rb_intern("write");
1128
1387
  ID_write_method = rb_intern("__write_method__");
1129
1388
 
1130
1389
  NUM_max_headers_read_length = INT2FIX(MAX_HEADERS_READ_LENGTH);
1131
1390
  NUM_buffer_start = INT2FIX(0);
1132
1391
  NUM_buffer_end = INT2FIX(-1);
1133
1392
 
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");
1393
+ GLOBAL_STR(STR_pseudo_method, ":method");
1394
+ GLOBAL_STR(STR_pseudo_path, ":path");
1395
+ GLOBAL_STR(STR_pseudo_protocol, ":protocol");
1396
+ GLOBAL_STR(STR_pseudo_protocol_default, "HTTP/1.1");
1397
+ GLOBAL_STR(STR_pseudo_rx, ":rx");
1398
+ GLOBAL_STR(STR_pseudo_status, ":status");
1399
+ GLOBAL_STR(STR_pseudo_status_default, "200 OK");
1400
+ GLOBAL_STR(STR_pseudo_status_message, ":status_message");
1401
+
1402
+ GLOBAL_STR(STR_chunked, "chunked");
1403
+ GLOBAL_STR(STR_content_length, "content-length");
1404
+ GLOBAL_STR(STR_content_length_capitalized, "Content-Length");
1405
+ GLOBAL_STR(STR_transfer_encoding, "transfer-encoding");
1406
+ GLOBAL_STR(STR_transfer_encoding_capitalized, "Transfer-Encoding");
1140
1407
 
1141
- GLOBAL_STR(STR_chunked, "chunked");
1142
- GLOBAL_STR(STR_content_length, "content-length");
1143
- GLOBAL_STR(STR_transfer_encoding, "transfer-encoding");
1408
+ GLOBAL_STR(STR_CRLF, "\r\n");
1409
+ GLOBAL_STR(STR_EMPTY_CHUNK, "0\r\n\r\n");
1144
1410
 
1145
1411
  SYM_backend_read = ID2SYM(ID_backend_read);
1146
1412
  SYM_backend_recv = ID2SYM(ID_backend_recv);
@@ -1153,8 +1419,10 @@ void Init_H1P() {
1153
1419
  SYM_server = ID2SYM(rb_intern("server"));
1154
1420
 
1155
1421
  rb_global_variable(&mH1P);
1422
+
1423
+ eArgumentError = rb_const_get(rb_cObject, rb_intern("ArgumentError"));
1156
1424
  }
1157
1425
 
1158
- void Init_h1p_ext() {
1426
+ void Init_h1p_ext(void) {
1159
1427
  Init_H1P();
1160
1428
  }
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.1'
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,140 @@
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\nContent-Length: 0\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\nContent-Length: 0\r\n\r\n", response
19
+ assert_equal "HTTP/1.0 200 OK\r\nContent-Length: 0\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', 'X-Blah' => '123' })
25
+ o.close
26
+ response = i.read
27
+ assert_equal "HTTP/1.1 200 OK\r\nFoo: Bar\r\nX-Blah: 123\r\nContent-Length: 0\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', 'X-Blah' => 123 })
33
+ o.close
34
+ response = i.read
35
+ assert_equal "HTTP/1.1 200 OK\r\nFoo: Bar\r\nX-Blah: 123\r\nContent-Length: 0\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_frozen_headers_hash
47
+ i, o = IO.pipe
48
+ h = {Foo: 'bar'}.freeze
49
+ H1P.send_response(o, h, 'foo')
50
+ o.close
51
+ response = i.read
52
+ assert_equal "HTTP/1.1 200 OK\r\nFoo: bar\r\nContent-Length: 3\r\n\r\nfoo", 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
+
64
+ def test_send_response_with_big_body
65
+ i, o = IO.pipe
66
+ body = "abcdefg" * 10000
67
+ Thread.new { H1P.send_response(o, {}, body); o.close }
68
+
69
+ response = i.read
70
+ assert_equal "HTTP/1.1 200 OK\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}", response
71
+ end
72
+ end
73
+
74
+ class SendBodyChunkTest < MiniTest::Test
75
+ def test_send_body_chunk
76
+ i, o = IO.pipe
77
+ len1 = H1P.send_body_chunk(o, 'foo')
78
+ assert_equal 8, len1
79
+ len2 = H1P.send_body_chunk(o, :barbazbarbaz)
80
+ assert_equal 17, len2
81
+ len3 = H1P.send_body_chunk(o, 1234)
82
+ assert_equal 9, len3
83
+ len4 = H1P.send_body_chunk(o, nil)
84
+ assert_equal 5, len4
85
+ o.close
86
+ response = i.read
87
+ assert_equal "3\r\nfoo\r\nc\r\nbarbazbarbaz\r\n4\r\n1234\r\n0\r\n\r\n", response
88
+ end
89
+
90
+ def test_send_body_chunk_big
91
+ i, o = IO.pipe
92
+
93
+ chunk = 'foobar1' * 20000
94
+ len = nil
95
+
96
+ Thread.new do
97
+ len = H1P.send_body_chunk(o, chunk)
98
+ o.close
99
+ end
100
+
101
+ response = i.read
102
+ assert_equal "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n", response
103
+ assert_equal chunk.bytesize + chunk.bytesize.to_s(16).bytesize + 4, len
104
+ end
105
+ end
106
+
107
+ class SendChunkedResponseTest < MiniTest::Test
108
+ def test_send_chunked_response
109
+ isrc, osrc = IO.pipe
110
+ osrc << 'foobarbaz'
111
+ osrc.close
112
+
113
+ i, o = IO.pipe
114
+ len = H1P.send_chunked_response(o, { 'Foo' => 'bar' }) do
115
+ isrc.read(3)
116
+ end
117
+ o.close
118
+
119
+ response = i.read
120
+ 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
121
+ assert_equal len, response.bytesize
122
+ end
123
+
124
+ def test_send_chunked_response_with_frozen_headers_hash
125
+ isrc, osrc = IO.pipe
126
+ osrc << 'foobarbaz'
127
+ osrc.close
128
+
129
+ i, o = IO.pipe
130
+ h = { 'Foo' => 'bar' }.freeze
131
+ len = H1P.send_chunked_response(o, h) do
132
+ isrc.read(3)
133
+ end
134
+ o.close
135
+
136
+ response = i.read
137
+ 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
138
+ assert_equal len, response.bytesize
139
+ end
140
+ 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.1
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-05-28 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