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 +4 -4
- data/CHANGELOG.md +4 -0
- data/TODO.md +2 -18
- data/examples/perf_parser.rb +258 -0
- data/lib/tp2/version.rb +1 -1
- data/tp2.gemspec +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ecde63feddaaf920da1342c895b9ee40045dc56088d1b5668bd96cd98a26d84
|
|
4
|
+
data.tar.gz: bd79ac1a84bdfb208a1e5f19c72c9023ecf2a3c41b33977d8082c18fa8b40925
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6a0a68e0693206db0ad721c058e28365a105135d0fdb34b10c7ae059b82d4cb90701cc2c84546a66359533f28ba1e90f382293d27670894d2ab6aeaf30964d9
|
|
7
|
+
data.tar.gz: ae6baf0c99d571ffbce56db855c68ed571569747d695e74034b4a3cecf5a47e4ffdd0816c07d45eb4b19ac4b389e42eb976409ae39b801feea4aa3be2efd200a
|
data/CHANGELOG.md
CHANGED
data/TODO.md
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
## Immediate
|
|
2
2
|
|
|
3
|
-
- [
|
|
4
|
-
|
|
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
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.
|
|
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:
|
|
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:
|
|
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
|