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 +4 -4
- data/.github/workflows/test.yml +4 -3
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +5 -5
- data/README.md +53 -5
- data/ext/h1p/h1p.c +282 -15
- data/h1p.gemspec +3 -3
- data/lib/h1p/version.rb +1 -1
- data/test/helper.rb +0 -2
- data/test/test_h1p.rb +114 -0
- data/test/test_h1p_server.rb +1 -1
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90f7937fd6bcefdd1627c208bf73054fb3fb03a855666c7247f04aac4398d280
|
4
|
+
data.tar.gz: ab6a866b843beaf92137b991048db9af006f4cb5e510e211dc6ba6652bbc84a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 101e6e13aeed0cf2250dbe962c0c9c9edc5bf69e6ff2a59ee0d02018992c9b76dcaa4be3ddb44bbcb2272e355278a0b717c51050a2786cc95c00fbdb23bc40fa
|
7
|
+
data.tar.gz: 2d3130c067d51f173873d6181b76999dd897c241dcafc5fa3f69ec0837404b2fd7e292cadc2fbf9e27003e3dfd933dc394799abf03f12187d6c80e4f2dab83d6
|
data/.github/workflows/test.yml
CHANGED
@@ -8,17 +8,18 @@ jobs:
|
|
8
8
|
fail-fast: false
|
9
9
|
matrix:
|
10
10
|
os: [ubuntu-latest]
|
11
|
-
ruby: [2.
|
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@
|
19
|
-
- uses:
|
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
data/Gemfile.lock
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
h1p (0.
|
4
|
+
h1p (0.6)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
minitest (5.
|
9
|
+
minitest (5.17.0)
|
10
10
|
rake (13.0.6)
|
11
|
-
rake-compiler (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.
|
19
|
+
minitest (~> 5.17.0)
|
20
20
|
rake (~> 13.0.6)
|
21
|
-
rake-compiler (= 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 -
|
1
|
+
# H1P - HTTP/1 tools for Ruby
|
2
2
|
|
3
3
|
[](http://rubygems.org/gems/h1p)
|
4
4
|
[](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
|
-
|
18
|
-
|
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
|
-
##
|
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
|
-
|
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,
|
1135
|
-
GLOBAL_STR(STR_pseudo_path,
|
1136
|
-
GLOBAL_STR(STR_pseudo_protocol,
|
1137
|
-
GLOBAL_STR(
|
1138
|
-
GLOBAL_STR(
|
1139
|
-
GLOBAL_STR(
|
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(
|
1142
|
-
GLOBAL_STR(
|
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.
|
19
|
+
s.required_ruby_version = '>= 2.7'
|
20
20
|
|
21
|
-
s.add_development_dependency 'rake-compiler', '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.
|
23
|
+
s.add_development_dependency 'minitest', '~>5.17.0'
|
24
24
|
end
|
data/lib/h1p/version.rb
CHANGED
data/test/helper.rb
CHANGED
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
|
data/test/test_h1p_server.rb
CHANGED
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
|
+
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:
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|