tp2 0.19.1 → 0.19.2

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: 11150b296eb03a7cc91e71c55a4f4738977aa423525e6c51f47467deec175f89
4
- data.tar.gz: f4fc0cb3407f322c1f3dc20f5b5c0ec4f6e68b32a32e5eb1079b5db0aeebe38d
3
+ metadata.gz: 3ecde63feddaaf920da1342c895b9ee40045dc56088d1b5668bd96cd98a26d84
4
+ data.tar.gz: bd79ac1a84bdfb208a1e5f19c72c9023ecf2a3c41b33977d8082c18fa8b40925
5
5
  SHA512:
6
- metadata.gz: 2b12e3db4d8d746b8914038824590ed6ff07dcdd9d72857d7c37e96558e78e483fd179fe333472f089a3b8f3134fd56ce1dd35a9a8d43ffda65a7723dc2bc1e4
7
- data.tar.gz: 72391f14fe4dfc227edff22e49441759cdf038f5676947a169dbe924543bb29e9439f5688aae0e6b04d5c49b21618d098b08337e63035140c614a2d85bfae718
6
+ metadata.gz: e6a0a68e0693206db0ad721c058e28365a105135d0fdb34b10c7ae059b82d4cb90701cc2c84546a66359533f28ba1e90f382293d27670894d2ab6aeaf30964d9
7
+ data.tar.gz: ae6baf0c99d571ffbce56db855c68ed571569747d695e74034b4a3cecf5a47e4ffdd0816c07d45eb4b19ac4b389e42eb976409ae39b801feea4aa3be2efd200a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 0.19.2 2025-11-03
2
+
3
+ - Update UringMachine
4
+
1
5
  # 0.19.1 2025-10-27
2
6
 
3
7
  - Update UringMachine
data/TODO.md CHANGED
@@ -1,23 +1,7 @@
1
1
  ## Immediate
2
2
 
3
- - [v] Remove support for HTTP/0.9, HTTP/1.0
4
- - [v] Reply with 505 HTTP version not supported:
5
-
6
- ```
7
- < GET / HTTP/0.9
8
-
9
- > HTTP/1.1 505
10
- > Connection: close
11
- ```
12
-
13
- - [v] Use chunked transfer encoding exclusively
14
- - [ ] Cache rendered headers
15
- - [ ] Look at sketch in ~/Desktop/docs/
16
-
17
- - [v] Add options for:
18
- - [v] Date header
19
- - [v] Server header
20
-
3
+ - [ ] Reimplement HTTP parsing, performance could be much improved. See
4
+ `examples/perf_parser.rb` and also: https://github.com/puma/puma/pull/3660
21
5
  - [ ] Change signal handling to use self-pipe
22
6
 
23
7
  ##
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ gem 'uringmachine'
7
+ gem 'benchmark'
8
+ gem 'benchmark-ips', '>= 2.14.0'
9
+ end
10
+
11
+ require 'uringmachine'
12
+ require 'benchmark/ips'
13
+
14
+ require 'strscan'
15
+
16
+ class TP2Parser
17
+ def initialize(machine, fd)
18
+ @machine = machine
19
+ @fd = fd
20
+ @stream = UM::Stream.new(machine, fd)
21
+ end
22
+
23
+ def run
24
+ parse_headers
25
+ end
26
+
27
+ RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+http\/([019\.]{1,3})/i
28
+ RE_HEADER_LINE = /^([a-z0-9-]+):\s+(.+)/i
29
+ MAX_REQUEST_LINE_LEN = 1 << 14 # 16KB
30
+ MAX_HEADER_LINE_LEN = 1 << 10 # 1KB
31
+ MAX_CHUNK_SIZE_LEN = 16
32
+
33
+ def parse_headers
34
+ buf = String.new(capacity: 4096)
35
+ headers = get_request_line(buf)
36
+ return nil if !headers
37
+
38
+ loop do
39
+ line = @stream.get_line(buf, MAX_HEADER_LINE_LEN)
40
+ break if line.nil? || line.empty?
41
+ m = line.match(RE_HEADER_LINE)
42
+ raise ProtocolError, "Invalid header: #{line[0..2047].inspect}" if !m
43
+ headers[m[1].downcase] = m[2]
44
+ end
45
+
46
+ headers
47
+ end
48
+
49
+ def get_request_line(buf)
50
+ line = @stream.get_line(buf, MAX_REQUEST_LINE_LEN)
51
+ return nil if !line
52
+
53
+ m = line.match(RE_REQUEST_LINE)
54
+ raise ProtocolError, 'Invalid request line' if !m
55
+
56
+ http_version = m[3]
57
+ raise UnsupportedHTTPVersionError, 'HTTP version not supported' if http_version != '1.1'
58
+
59
+ {
60
+ ':method' => m[1].downcase,
61
+ ':path' => m[2],
62
+ ':protocol' => 'http/1.1'
63
+ }
64
+ end
65
+ end
66
+
67
+ class PumaHttpParser
68
+ STEPS = [
69
+ :http_method,
70
+ :target,
71
+ :version,
72
+ :headers,
73
+ :body
74
+ ]
75
+
76
+ # Step delimeters
77
+ SPACE = /\s/
78
+ RETURN_OR_NEWLINE = /[\r\n]/
79
+
80
+ SSL = "Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?"
81
+
82
+ METHODS = %r{#{'GET|POST|HEAD|PUT|DELETE'}\b}
83
+ WS = /\s+/
84
+ def http_method(req)
85
+ req["REQUEST_METHOD"] = (@scanner.scan(METHODS) || raise_error!("Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?")).strip
86
+ @scanner.skip(WS)
87
+ @step += 1
88
+ end
89
+
90
+ # URL components
91
+ URI = /[^\s#]+/ # Match everything up until whitespace or a hash
92
+ SCHEME = %r{\Ahttps?://}
93
+ PARTS = /\A(?<path>[^?#]*)(\?(?<params>[^#]*))?/
94
+ NWS = /\S*/ # not white space
95
+ FRAGMENT_DELIMETER = /#/
96
+ UNPRINTABLE_CHARACTERS = %r{[^ -~]} # find a character that's not between " " (space) and "~" (tilde)
97
+ def target(req)
98
+ uri = @scanner.scan(URI)
99
+ req["REQUEST_URI"] = uri
100
+ raise_error!("HTTP element REQUEST_URI is longer than the (1024 * 12) allowed length (was #{uri.length})") if uri.length > 1024 * 12
101
+ unless uri.match? SCHEME
102
+ parts = PARTS
103
+ .match(uri)
104
+ .named_captures
105
+ path = parts["path"]
106
+ if path.match? UNPRINTABLE_CHARACTERS
107
+ raise_error!(SSL)
108
+ else
109
+ req["REQUEST_PATH"] = path
110
+ raise_error!("HTTP element REQUEST_PATH is longer than the (8192) allowed length (was #{path.length})") if path.length > 8192
111
+ end
112
+ params = parts["params"] || ""
113
+ if params.match? UNPRINTABLE_CHARACTERS
114
+ raise_error!(SSL)
115
+ else
116
+ req["QUERY_STRING"] = params
117
+ raise_error!("HTTP element QUERY_STRING is longer than the (1024 * 10) allowed length (was #{params.length})") if params.length > 1024 * 10
118
+ end
119
+ if @scanner.skip(FRAGMENT_DELIMETER)
120
+ req["FRAGMENT"] = @scanner.scan(NWS)
121
+ raise_error!("HTTP element FRAGMENT is longer than the 1024 allowed length (was #{req["FRAGMENT"].length})") if req["FRAGMENT"].length > 1024
122
+ end
123
+ end
124
+ @scanner.scan(WS)
125
+ @step += 1
126
+ end
127
+
128
+ HTTP_VERSION = %r{(HTTP/1\.[01])}
129
+ RETURN_NEWLINE = %r{\r\n}
130
+ NEWLINE = %r{\n}
131
+ def version(req)
132
+ req["SERVER_PROTOCOL"] = @scanner.scan(HTTP_VERSION) || raise_error!(SSL)
133
+ @scanner.skip(RETURN_NEWLINE) || raise_error!
134
+ @delimeter = RETURN_OR_NEWLINE
135
+ @step += 1
136
+ end
137
+
138
+ HEADER = %r{[^\r\n]+\r\n}
139
+ HEADER_FORMAT = /\A([^:]+):\s*([^\r\n]+)/
140
+ DIGITS = /\A\d+\z/
141
+ TRAILING_WS = /\s*$/
142
+ def headers(req)
143
+ while header = @scanner.scan(HEADER)
144
+ @headers_count += 1
145
+ raise_error! if @headers_count > 1024 # TODO Use a better number
146
+ @headers_total_length += header.length
147
+ raise_error!("HTTP element HEADER is longer than the (1024 * (80 + 32)) allowed length (was 114930)") if @headers_total_length > 1024 * (80 + 32) # TODO Need to figure out how to calculate 114930 better.
148
+ raise_error! unless HEADER_FORMAT.match(header)
149
+ key = $1
150
+ value = $2
151
+ raise_error!("HTTP element FIELD_NAME is longer than the 256 allowed length (was #{key.length})") if key.length > 256
152
+ raise_error!("HTTP element FIELD_VALUE is longer than the 80 * 1024 allowed length (was #{value.length})") if value.length > 80 * 1024
153
+ raise_error! if value.match?(NEWLINE)
154
+ key = key.upcase.tr("_-", ",_")
155
+ key = "HTTP_#{key}" unless ["CONTENT_LENGTH", "CONTENT_TYPE"].include?(key)
156
+ value = value.rstrip
157
+ if req.has_key?(key)
158
+ req[key] << ", #{value}"
159
+ else
160
+ req[key] = value
161
+ end
162
+ end
163
+ if @scanner.skip(RETURN_NEWLINE)
164
+ @step += 1
165
+ @finished = true
166
+ end
167
+ raise_error!(SSL) if @scanner.exist?(/[^\r]\n/) # catch bad headers that are missing a return.
168
+ raise_error!(SSL) if @scanner.exist?(/\r[^\n]/) # catch bad headers that are missing a newline.
169
+ end
170
+
171
+ def execute(req, data, start)
172
+ unless @scanner
173
+ reset
174
+ @scanner = StringScanner.new(data)
175
+ end
176
+
177
+ send(STEPS[@step], req) while !@finished && @scanner.exist?(@delimeter)
178
+
179
+ if @step == 0
180
+ raise_error!("Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?")
181
+ end
182
+
183
+ nread
184
+ end
185
+
186
+ def body
187
+ @body ||= @scanner.rest.tap { @scanner.terminate } # Using terminate feels hacky to me. Is there something better?
188
+ end
189
+
190
+ def nread
191
+ @scanner&.pos || 0
192
+ end
193
+
194
+ def reset
195
+ @step = 0
196
+ @finished = false
197
+ @delimeter = SPACE
198
+ @error = false
199
+ @scanner = nil
200
+ @body = nil
201
+ @headers_count = 0
202
+ @headers_total_length = 0
203
+ end
204
+
205
+ def finished?
206
+ @finished
207
+ end
208
+
209
+ def raise_error!(message = nil)
210
+ @error = true
211
+ raise HttpParserError.new(message)
212
+ end
213
+
214
+ def error?
215
+ @error
216
+ end
217
+ end
218
+
219
+
220
+
221
+
222
+ REQ = "GET /foo/bar HTTP/1.1\r\nHost: foobar.com\r\nAccept: blah blah\r\n\r\n"
223
+
224
+ @machine = UM.new
225
+
226
+ def run_tp2
227
+ r, w = UM.pipe
228
+ parser = TP2Parser.new(@machine, r)
229
+ @machine.write(w, REQ)
230
+ @machine.close_async(w)
231
+
232
+ parser.run
233
+ end
234
+
235
+ p run_tp2
236
+
237
+ def run_puma_pure_ruby
238
+ parser = PumaHttpParser.new
239
+ req = {}
240
+ r, w = UM.pipe
241
+ @machine.write(w, REQ)
242
+ @machine.close_async(w)
243
+ data = +''
244
+ @machine.read(r, data, 8192)
245
+ parser.execute(req, data, 0)
246
+ req
247
+ end
248
+
249
+ p run_puma_pure_ruby
250
+
251
+ Benchmark.ips do |x|
252
+ x.config(:time => 10, :warmup => 3)
253
+
254
+ x.report("tp2") { run_tp2 }
255
+ x.report("puma") { run_puma_pure_ruby }
256
+
257
+ x.compare!(order: :baseline)
258
+ end
data/lib/tp2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module TP2
2
- VERSION = '0.19.1'
2
+ VERSION = '0.19.2'
3
3
  end
data/tp2.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.required_ruby_version = '>= 3.4'
21
21
  s.executables = ['tp2']
22
22
 
23
- s.add_dependency 'uringmachine', '~> 0.19'
23
+ s.add_dependency 'uringmachine', '~> 0.19.1'
24
24
  s.add_dependency 'qeweney', '~> 0.23'
25
25
  s.add_dependency 'rack', '~> 3.1.15'
26
26
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tp2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.1
4
+ version: 0.19.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.19'
18
+ version: 0.19.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.19'
25
+ version: 0.19.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: qeweney
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -68,6 +68,7 @@ files:
68
68
  - TODO.md
69
69
  - bin/tp2
70
70
  - examples/app.rb
71
+ - examples/perf_parser.rb
71
72
  - examples/rack.ru
72
73
  - examples/simple.rb
73
74
  - lib/tp2.rb