h1p 0.5 → 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: 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