picohttp 0.3.1 → 0.4.0

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: 664158c1afacf094df70190d2b86e9a132e8ad4c5326bf57d26e8ba31050caab
4
- data.tar.gz: 4db750f953e50493e2dbbc87e26619aa4078d125ad2f45ca567562efaac37176
3
+ metadata.gz: 32830c406cc611f0808d7b360b22294b03a7a3a76cec684175e6bd058c2d2d6e
4
+ data.tar.gz: f929ecc68df200572fd38506cf00db14cc8b80baaa5ad01a1d51a5b5f11f2805
5
5
  SHA512:
6
- metadata.gz: 6379e766625fcd567fd47aaa7387514d8e7447b85488876e7dea3365fda1cda2c314e744885c44ab425e8947a8695178a35eb8b907701c3a7b85c357f82245d1
7
- data.tar.gz: 9505e4c27f13c9b0253c4b910013157fdf945700d7e0f0b34e8d4c1ee0450b223508d6e060187d93c380017be153703eef9cd9cf77ec40686075246b7f6dd3ef
6
+ metadata.gz: f0cc7ecee0bcbb834c2a44ac85c8a33e66278e87efee338fa91c9d392b07337be3d1c7feb2f2b98b015f2bbae574022136702a98b266474b68c1263373520940
7
+ data.tar.gz: 2bbdee0a6887a7fb0d068f80d61e934edb3fd703e67e75f4dbf0fbb271b9f3ffa03a55f3b0995f28e5316daaf37a31f0eb16bc84d88b8149429fb7927d6781df
data/README.md CHANGED
@@ -34,6 +34,49 @@ env = Picohttp.parse_request_env(request)
34
34
 
35
35
  Returns `nil` for incomplete requests, raises `Picohttp::ParseError` for invalid ones.
36
36
 
37
+ ### Environment fields
38
+
39
+ The produced hash uses CGI/Rack-style keys:
40
+
41
+ | Key | Value | Set when |
42
+ | ----------------- | --------------------------------------------------------------------------------------------------- | -------- |
43
+ | `REQUEST_METHOD` | The request method, e.g. `"GET"`, `"POST"`. | always |
44
+ | `SCRIPT_NAME` | Always `""`. | always |
45
+ | `PATH_INFO` | The request path with any query string removed, e.g. `"/api/users"`. | always |
46
+ | `QUERY_STRING` | The part of the target after `?`, or `""` when there is none. | always |
47
+ | `REQUEST_URI` | The full request target, e.g. `"/api/users?limit=10"`. | always |
48
+ | `SERVER_PROTOCOL` | `"HTTP/1.0"` or `"HTTP/1.1"`. | always |
49
+ | `SERVER_NAME` | The host portion of the `Host` header, with brackets kept for IPv6 literals (`"[::1]"`). | `Host` present |
50
+ | `SERVER_PORT` | The port from the `Host` header, e.g. `"8080"`. | `Host` has a port |
51
+ | `CONTENT_TYPE` | The `Content-Type` header (note: no `HTTP_` prefix). | header present |
52
+ | `CONTENT_LENGTH` | The `Content-Length` header (note: no `HTTP_` prefix). | header present |
53
+
54
+ Every other request header becomes an `HTTP_`-prefixed key: the name is
55
+ uppercased and `-` becomes `_` (e.g. `User-Agent` → `HTTP_USER_AGENT`). Repeated
56
+ headers are joined with `, `. A repeated `Host` header raises
57
+ `Picohttp::ParseError` (RFC 7230 §5.4).
58
+
59
+ ### Starting from a template
60
+
61
+ Servers usually add the same constant keys to every env. `parse_request_env_with_template` starts from a copy of a template hash instead, which is faster than merging the constants in afterwards:
62
+
63
+ ```ruby
64
+ RACK_ENV_CONST = {
65
+ "rack.url_scheme" => "http",
66
+ "SERVER_SOFTWARE" => "MyServer/1.0",
67
+ }.freeze
68
+
69
+ env = Picohttp.parse_request_env_with_template(request, RACK_ENV_CONST)
70
+ # => {
71
+ # "rack.url_scheme" => "http",
72
+ # "SERVER_SOFTWARE" => "MyServer/1.0",
73
+ # "REQUEST_METHOD" => "GET",
74
+ # ...
75
+ # }
76
+ ```
77
+
78
+ The template is never modified and may be frozen. If a key is in both the template and the request, the request wins, but this isn't recommended.
79
+
37
80
  ## Development
38
81
 
39
82
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -132,8 +132,8 @@ build_hash_with_combined_duplicates(VALUE *header_values, int count)
132
132
  return env;
133
133
  }
134
134
 
135
- static VALUE
136
- picohttp_parse_request_env(VALUE self, VALUE str)
135
+ static int
136
+ parse_request_env_pairs(VALUE str, VALUE *header_values)
137
137
  {
138
138
  Check_Type(str, T_STRING);
139
139
 
@@ -150,12 +150,11 @@ picohttp_parse_request_env(VALUE self, VALUE str)
150
150
 
151
151
  if (result < 0) {
152
152
  if (result == -2) {
153
- return Qnil; // Incomplete request
153
+ return -1; // Incomplete request
154
154
  }
155
155
  rb_raise(rb_ePicohttpParseError, "Invalid HTTP request");
156
156
  }
157
157
 
158
- VALUE header_values[(MAX_HTTP_HEADERS + EXTRA_RACK_HEADERS) * 2];
159
158
  int idx = 0;
160
159
 
161
160
  // Standard CGI/Rack environment variables
@@ -193,6 +192,7 @@ picohttp_parse_request_env(VALUE self, VALUE str)
193
192
  header_values[idx++] = rb_str_empty;
194
193
 
195
194
  // Convert headers to HTTP_ prefixed environment variables
195
+ int host_seen = 0;
196
196
  for (size_t i = 0; i < num_headers; i++) {
197
197
  if (headers[i].name == NULL) {
198
198
  rb_raise(rb_ePicohttpParseError, "HTTP line folding not supported");
@@ -201,21 +201,37 @@ picohttp_parse_request_env(VALUE self, VALUE str)
201
201
  header_values[idx++] = header_name_to_env_key(headers[i].name, headers[i].name_len);
202
202
  header_values[idx++] = rb_str_new(headers[i].value, headers[i].value_len);
203
203
 
204
- // Extract SERVER_NAME/SERVER_PORT from Host header
205
204
  if (headers[i].name_len == 4 &&
206
205
  (headers[i].name[0] | 0x20) == 'h' &&
207
206
  (headers[i].name[1] | 0x20) == 'o' &&
208
207
  (headers[i].name[2] | 0x20) == 's' &&
209
208
  (headers[i].name[3] | 0x20) == 't') {
209
+ // More than one Host is a 400 (RFC 7230 5.4). Rejecting also keeps
210
+ // idx bounded: the SERVER_* extras are written at most once.
211
+ if (host_seen) {
212
+ rb_raise(rb_ePicohttpParseError, "Duplicate Host header");
213
+ }
214
+ host_seen = 1;
215
+
210
216
  const char *host = headers[i].value;
211
217
  size_t host_len = headers[i].value_len;
212
- const char *colon = memchr(host, ':', host_len);
218
+ const char *host_end = host + host_len;
219
+
220
+ // IPv6 authority is "[::1]" or "[::1]:port"; the colons inside the
221
+ // brackets aren't the port separator, so scan only after the ']'.
222
+ const char *colon;
223
+ if (host_len > 0 && host[0] == '[') {
224
+ const char *bracket = memchr(host, ']', host_len);
225
+ colon = bracket ? memchr(bracket, ':', host_end - bracket) : NULL;
226
+ } else {
227
+ colon = memchr(host, ':', host_len);
228
+ }
213
229
 
214
230
  if (colon) {
215
231
  header_values[idx++] = rb_str_server_name;
216
232
  header_values[idx++] = rb_str_new(host, colon - host);
217
233
  header_values[idx++] = rb_str_server_port;
218
- header_values[idx++] = rb_str_new(colon + 1, host_len - (colon - host) - 1);
234
+ header_values[idx++] = rb_str_new(colon + 1, host_end - colon - 1);
219
235
  } else {
220
236
  header_values[idx++] = rb_str_server_name;
221
237
  header_values[idx++] = rb_str_new(host, host_len);
@@ -223,8 +239,19 @@ picohttp_parse_request_env(VALUE self, VALUE str)
223
239
  }
224
240
  }
225
241
 
242
+ return idx;
243
+ }
244
+
245
+ static VALUE
246
+ picohttp_parse_request_env(VALUE self, VALUE str)
247
+ {
248
+ VALUE header_values[(MAX_HTTP_HEADERS + EXTRA_RACK_HEADERS) * 2];
249
+ int idx = parse_request_env_pairs(str, header_values);
250
+ if (idx < 0) return Qnil;
251
+
226
252
  #ifdef HAVE_RB_HASH_NEW_CAPA
227
- VALUE env = rb_hash_new_capa(idx / 2);
253
+ // Extra slack so servers can add their own env keys without a rehash
254
+ VALUE env = rb_hash_new_capa(idx / 2 + 8);
228
255
  #else
229
256
  VALUE env = rb_hash_new();
230
257
  #endif
@@ -239,6 +266,29 @@ picohttp_parse_request_env(VALUE self, VALUE str)
239
266
  return env;
240
267
  }
241
268
 
269
+ static VALUE
270
+ picohttp_parse_request_env_with_template(VALUE self, VALUE str, VALUE template)
271
+ {
272
+ Check_Type(template, T_HASH);
273
+
274
+ VALUE header_values[(MAX_HTTP_HEADERS + EXTRA_RACK_HEADERS) * 2];
275
+ int idx = parse_request_env_pairs(str, header_values);
276
+ if (idx < 0) return Qnil;
277
+
278
+ VALUE env = rb_hash_dup(template);
279
+ size_t expected_size = RHASH_SIZE(env) + idx / 2;
280
+
281
+ rb_hash_bulk_insert(idx, header_values, env);
282
+
283
+ // Handle duplicate headers per RFC 7230
284
+ if (RHASH_SIZE(env) != expected_size) {
285
+ env = rb_hash_dup(template);
286
+ rb_hash_update_by(env, build_hash_with_combined_duplicates(header_values, idx), NULL);
287
+ }
288
+
289
+ return env;
290
+ }
291
+
242
292
  static VALUE
243
293
  register_interned_string(const char *str)
244
294
  {
@@ -258,6 +308,7 @@ Init_picohttp(void)
258
308
  rb_ePicohttpParseError = rb_define_class_under(rb_mPicohttp, "ParseError", rb_eStandardError);
259
309
  rb_define_module_function(rb_mPicohttp, "parse_request", picohttp_parse_request, 1);
260
310
  rb_define_module_function(rb_mPicohttp, "parse_request_env", picohttp_parse_request_env, 1);
311
+ rb_define_module_function(rb_mPicohttp, "parse_request_env_with_template", picohttp_parse_request_env_with_template, 2);
261
312
 
262
313
  // Initialize interned string constants
263
314
  init_string_lookup();
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Picohttp
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,60 +1,61 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: picohttp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
- - John Hawthorn
7
+ - John Hawthorn
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: Wraps picohttpparser to parse HTTP request strings into Rack-compatible
13
- hashes
12
+ description: Wraps picohttpparser to parse HTTP request strings into Rack-compatible hashes
14
13
  email:
15
- - john@hawthorn.email
14
+ - john@hawthorn.email
16
15
  executables: []
17
16
  extensions:
18
- - ext/picohttp/extconf.rb
17
+ - ext/picohttp/extconf.rb
19
18
  extra_rdoc_files: []
20
19
  files:
21
- - CODE_OF_CONDUCT.md
22
- - LICENSE.txt
23
- - README.md
24
- - Rakefile
25
- - ext/picohttp/extconf.rb
26
- - ext/picohttp/picohttp.c
27
- - ext/picohttp/picohttp.h
28
- - ext/picohttp/picohttpparser.c
29
- - ext/picohttp/picohttpparser.h
30
- - ext/picohttp/string_lookup.inc
31
- - lib/picohttp.rb
32
- - lib/picohttp/version.rb
33
- - tool/generate_lookup.rb
34
- homepage: https://github.com/jhawthorn/picohttp
20
+ - CODE_OF_CONDUCT.md
21
+ - LICENSE.txt
22
+ - README.md
23
+ - Rakefile
24
+ - ext/picohttp/extconf.rb
25
+ - ext/picohttp/picohttp.c
26
+ - ext/picohttp/picohttp.h
27
+ - ext/picohttp/picohttpparser.c
28
+ - ext/picohttp/picohttpparser.h
29
+ - ext/picohttp/string_lookup.inc
30
+ - lib/picohttp.rb
31
+ - lib/picohttp/version.rb
32
+ - tool/generate_lookup.rb
33
+ homepage: "https://github.com/jhawthorn/picohttp"
35
34
  licenses:
36
- - MIT
35
+ - MIT
37
36
  metadata:
38
- homepage_uri: https://github.com/jhawthorn/picohttp
39
- source_code_uri: https://github.com/jhawthorn/picohttp
40
- changelog_uri: https://github.com/jhawthorn/picohttp/blob/main/CHANGELOG.md
41
- bug_tracker_uri: https://github.com/jhawthorn/picohttp/issues
42
- documentation_uri: https://github.com/jhawthorn/picohttp#readme
37
+ homepage_uri: "https://github.com/jhawthorn/picohttp"
38
+ source_code_uri: "https://github.com/jhawthorn/picohttp"
39
+ changelog_uri: "https://github.com/jhawthorn/picohttp/blob/main/CHANGELOG.md"
40
+ bug_tracker_uri: "https://github.com/jhawthorn/picohttp/issues"
41
+ documentation_uri: "https://github.com/jhawthorn/picohttp#readme"
43
42
  rdoc_options: []
44
43
  require_paths:
45
- - lib
44
+ - lib
46
45
  required_ruby_version: !ruby/object:Gem::Requirement
47
46
  requirements:
48
- - - ">="
49
- - !ruby/object:Gem::Version
50
- version: 3.2.0
47
+ -
48
+ - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 3.2.0
51
51
  required_rubygems_version: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - ">="
54
- - !ruby/object:Gem::Version
55
- version: '0'
53
+ -
54
+ - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
56
57
  requirements: []
57
- rubygems_version: 4.0.3
58
+ rubygems_version: 4.1.0.beta1
58
59
  specification_version: 4
59
60
  summary: Fast HTTP request parser using picohttpparser
60
61
  test_files: []