kantan 0.0.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.
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require 'rack'
5
+ require 'concurrent'
6
+ require 'kantan/rack_handler'
7
+
8
+ class TestRackHandler < Minitest::Test
9
+ def setup
10
+ @executor = Concurrent::FixedThreadPool.new(5)
11
+ end
12
+
13
+ def teardown
14
+ @executor.shutdown
15
+ @executor.wait_for_termination(2)
16
+ end
17
+
18
+ def setup_pair(rack_app)
19
+ client_io, server_io = Socket.pair(:UNIX, :STREAM, 0)
20
+ client_io.sync = true
21
+ server_io.sync = true
22
+
23
+ handler = Kantan::RackHandler.new(rack_app,
24
+ executor: @executor,
25
+ server_name: "localhost",
26
+ server_port: 443,
27
+ scheme: "https")
28
+
29
+ client_handler = TestClientHandler.new
30
+
31
+ server_session = Kantan::Session.new(server_io, handler: handler)
32
+ client_session = Kantan::Session.new(client_io, handler: client_handler)
33
+
34
+ server_thread = Thread.new { server_session.receive }
35
+ client_session.connect
36
+
37
+ [client_session, client_handler, client_io, server_io, server_thread]
38
+ end
39
+
40
+ def collect_response(client_handler)
41
+ response_headers = nil
42
+ body = "".b
43
+ loop do
44
+ type, value = client_handler.queue.pop
45
+ case type
46
+ when :headers then response_headers = value
47
+ when :data then body << value
48
+ when :done then break
49
+ end
50
+ end
51
+ [response_headers, body]
52
+ end
53
+
54
+ def test_simple_get
55
+ app = ->(env) {
56
+ [200, { "content-type" => "text/plain" }, ["Hello from Rack!"]]
57
+ }
58
+
59
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(app)
60
+
61
+ client_session.request([
62
+ [":method", "GET"],
63
+ [":path", "/foo"],
64
+ [":scheme", "https"],
65
+ [":authority", "localhost"],
66
+ ])
67
+
68
+ headers, body = collect_response(client_handler)
69
+
70
+ assert_equal "200", headers.assoc(":status").last
71
+ assert_equal "text/plain", headers.assoc("content-type").last
72
+ assert_equal "Hello from Rack!", body
73
+ ensure
74
+ client_io&.close rescue nil
75
+ server_io&.close rescue nil
76
+ server_thread&.join(2)
77
+ end
78
+
79
+ def test_env_is_correct
80
+ captured_env = nil
81
+ app = ->(env) {
82
+ captured_env = env.dup
83
+ [200, { "content-type" => "text/plain" }, ["ok"]]
84
+ }
85
+
86
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(app)
87
+
88
+ client_session.request([
89
+ [":method", "POST"],
90
+ [":path", "/test?foo=bar"],
91
+ [":scheme", "https"],
92
+ [":authority", "example.com"],
93
+ ])
94
+
95
+ collect_response(client_handler)
96
+
97
+ assert_equal "POST", captured_env["REQUEST_METHOD"]
98
+ assert_equal "/test", captured_env["PATH_INFO"]
99
+ assert_equal "foo=bar", captured_env["QUERY_STRING"]
100
+ assert_equal "", captured_env["SCRIPT_NAME"]
101
+ assert_equal "localhost", captured_env["SERVER_NAME"]
102
+ assert_equal "443", captured_env["SERVER_PORT"]
103
+ assert_equal "HTTP/2", captured_env["SERVER_PROTOCOL"]
104
+ assert_equal "https", captured_env["rack.url_scheme"]
105
+ assert_instance_of StringIO, captured_env["rack.input"]
106
+ # :authority should map to HTTP_HOST
107
+ assert_equal "example.com", captured_env["HTTP_HOST"]
108
+ ensure
109
+ client_io&.close rescue nil
110
+ server_io&.close rescue nil
111
+ server_thread&.join(2)
112
+ end
113
+
114
+ def test_post_with_body
115
+ captured_body = nil
116
+ app = ->(env) {
117
+ captured_body = env["rack.input"].read
118
+ [200, { "content-type" => "text/plain" }, ["got it"]]
119
+ }
120
+
121
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(app)
122
+
123
+ client_session.request([
124
+ [":method", "POST"],
125
+ [":path", "/upload"],
126
+ [":scheme", "https"],
127
+ [":authority", "localhost"],
128
+ ["content-type", "application/json"],
129
+ ["content-length", "13"],
130
+ ], body: '{"key":"val"}')
131
+
132
+ headers, body = collect_response(client_handler)
133
+
134
+ assert_equal "200", headers.assoc(":status").last
135
+ assert_equal "got it", body
136
+ assert_equal '{"key":"val"}', captured_body
137
+ ensure
138
+ client_io&.close rescue nil
139
+ server_io&.close rescue nil
140
+ server_thread&.join(2)
141
+ end
142
+
143
+ def test_content_type_and_length_not_prefixed_with_http
144
+ captured_env = nil
145
+ app = ->(env) {
146
+ captured_env = env.dup
147
+ [200, { "content-type" => "text/plain" }, ["ok"]]
148
+ }
149
+
150
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(app)
151
+
152
+ client_session.request([
153
+ [":method", "POST"],
154
+ [":path", "/"],
155
+ [":scheme", "https"],
156
+ [":authority", "localhost"],
157
+ ["content-type", "application/json"],
158
+ ["content-length", "5"],
159
+ ], body: "hello")
160
+
161
+ collect_response(client_handler)
162
+
163
+ assert_equal "application/json", captured_env["CONTENT_TYPE"]
164
+ assert_equal "5", captured_env["CONTENT_LENGTH"]
165
+ refute captured_env.key?("HTTP_CONTENT_TYPE")
166
+ refute captured_env.key?("HTTP_CONTENT_LENGTH")
167
+ ensure
168
+ client_io&.close rescue nil
169
+ server_io&.close rescue nil
170
+ server_thread&.join(2)
171
+ end
172
+
173
+ def test_custom_headers_mapped_to_http_prefix
174
+ captured_env = nil
175
+ app = ->(env) {
176
+ captured_env = env.dup
177
+ [200, { "content-type" => "text/plain" }, ["ok"]]
178
+ }
179
+
180
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(app)
181
+
182
+ client_session.request([
183
+ [":method", "GET"],
184
+ [":path", "/"],
185
+ [":scheme", "https"],
186
+ [":authority", "localhost"],
187
+ ["x-request-id", "abc123"],
188
+ ["accept", "text/html"],
189
+ ])
190
+
191
+ collect_response(client_handler)
192
+
193
+ assert_equal "abc123", captured_env["HTTP_X_REQUEST_ID"]
194
+ assert_equal "text/html", captured_env["HTTP_ACCEPT"]
195
+ ensure
196
+ client_io&.close rescue nil
197
+ server_io&.close rescue nil
198
+ server_thread&.join(2)
199
+ end
200
+
201
+ def test_empty_body_response
202
+ app = ->(env) {
203
+ [204, {}, []]
204
+ }
205
+
206
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(app)
207
+
208
+ client_session.request([
209
+ [":method", "DELETE"],
210
+ [":path", "/resource/1"],
211
+ [":scheme", "https"],
212
+ [":authority", "localhost"],
213
+ ])
214
+
215
+ headers, body = collect_response(client_handler)
216
+
217
+ assert_equal "204", headers.assoc(":status").last
218
+ assert_equal "", body
219
+ ensure
220
+ client_io&.close rescue nil
221
+ server_io&.close rescue nil
222
+ server_thread&.join(2)
223
+ end
224
+
225
+ def test_query_string_absent
226
+ captured_env = nil
227
+ app = ->(env) {
228
+ captured_env = env.dup
229
+ [200, { "content-type" => "text/plain" }, ["ok"]]
230
+ }
231
+
232
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(app)
233
+
234
+ client_session.request([
235
+ [":method", "GET"],
236
+ [":path", "/no-query"],
237
+ [":scheme", "https"],
238
+ [":authority", "localhost"],
239
+ ])
240
+
241
+ collect_response(client_handler)
242
+
243
+ assert_equal "/no-query", captured_env["PATH_INFO"]
244
+ assert_equal "", captured_env["QUERY_STRING"]
245
+ ensure
246
+ client_io&.close rescue nil
247
+ server_io&.close rescue nil
248
+ server_thread&.join(2)
249
+ end
250
+
251
+ def test_send_file_via_to_path
252
+ require "tempfile"
253
+ tmpfile = Tempfile.new("htwo_test")
254
+ tmpfile.binmode
255
+ tmpfile.write("file body content")
256
+ tmpfile.flush
257
+
258
+ app = ->(env) {
259
+ [200, { "content-type" => "application/octet-stream" }, tmpfile]
260
+ }
261
+
262
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(app)
263
+
264
+ client_session.request([
265
+ [":method", "GET"],
266
+ [":path", "/download"],
267
+ [":scheme", "https"],
268
+ [":authority", "localhost"],
269
+ ])
270
+
271
+ headers, body = collect_response(client_handler)
272
+
273
+ assert_equal "200", headers.assoc(":status").last
274
+ assert_equal "file body content", body
275
+ assert tmpfile.closed?, "rack body should be closed"
276
+ ensure
277
+ tmpfile&.unlink
278
+ client_io&.close rescue nil
279
+ server_io&.close rescue nil
280
+ server_thread&.join(2)
281
+ end
282
+
283
+ def test_rack_lint_compliance
284
+ app = ->(env) {
285
+ [200, { "content-type" => "text/plain" }, ["lint ok"]]
286
+ }
287
+ linted_app = Rack::Lint.new(app)
288
+
289
+ client_session, client_handler, client_io, server_io, server_thread = setup_pair(linted_app)
290
+
291
+ client_session.request([
292
+ [":method", "GET"],
293
+ [":path", "/"],
294
+ [":scheme", "https"],
295
+ [":authority", "localhost"],
296
+ ])
297
+
298
+ headers, body = collect_response(client_handler)
299
+
300
+ assert_equal "200", headers.assoc(":status").last
301
+ assert_equal "lint ok", body
302
+ ensure
303
+ client_io&.close rescue nil
304
+ server_io&.close rescue nil
305
+ server_thread&.join(2)
306
+ end
307
+ end
data/test.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kantan"
4
+ require "socket"
5
+ require "logger"
6
+
7
+ class MyApp < Kantan::Handler
8
+ def on_request stream
9
+ stream.respond [[":status", "200"]], body: "hello"
10
+ end
11
+ end
12
+
13
+ server = TCPServer.new "127.0.0.1", 8888
14
+ port = server.addr[1]
15
+
16
+ logger = Logger.new $stderr
17
+ logger.debug "port #{port}"
18
+
19
+ while true
20
+ client_fd = server.sysaccept
21
+
22
+ logger.debug "new connection"
23
+ Ractor.new(client_fd) do |c_fd|
24
+ c = Socket.for_fd(c_fd)
25
+ c.autoclose = true
26
+ c.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
27
+ session = Kantan::Session.new(c, handler: MyApp.new)
28
+ session.receive
29
+ session.join
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kantan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Patterson
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: logger
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rack
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3'
68
+ - !ruby/object:Gem::Dependency
69
+ name: concurrent-ruby
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.3'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.3'
82
+ description: An HTTP/2.0 parser in Ruby
83
+ email: tenderlove@ruby-lang.org
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - ".gitmodules"
89
+ - CODE_OF_CONDUCT.md
90
+ - Gemfile
91
+ - LICENSE
92
+ - README.md
93
+ - Rakefile
94
+ - examples/client.rb
95
+ - examples/fetch_google.rb
96
+ - examples/server.rb
97
+ - examples/socket_abstraction.rb
98
+ - examples/tls.rb
99
+ - kantan.gemspec
100
+ - lib/kantan.rb
101
+ - lib/kantan/errors.rb
102
+ - lib/kantan/hpack.rb
103
+ - lib/kantan/huffman.rb
104
+ - lib/kantan/rack_handler.rb
105
+ - lib/kantan/stream.rb
106
+ - lib/kantan/version.rb
107
+ - profile.rb
108
+ - server.log
109
+ - test.rb
110
+ - test/h2spec_server.rb
111
+ - test/helper.rb
112
+ - test/ssl_server.rb
113
+ - test/test_hpack.rb
114
+ - test/test_http2.rb
115
+ - test/test_rack_handler.rb
116
+ homepage: https://github.com/tenderlove/kantan
117
+ licenses:
118
+ - Apache-2.0
119
+ metadata: {}
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 4.1.0.dev
135
+ specification_version: 4
136
+ summary: HTTP/2.0 parser written in Ruby
137
+ test_files:
138
+ - test/h2spec_server.rb
139
+ - test/helper.rb
140
+ - test/ssl_server.rb
141
+ - test/test_hpack.rb
142
+ - test/test_http2.rb
143
+ - test/test_rack_handler.rb