tipi 0.30

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +27 -0
  3. data/.gitignore +56 -0
  4. data/CHANGELOG.md +33 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +50 -0
  7. data/LICENSE +21 -0
  8. data/README.md +23 -0
  9. data/Rakefile +12 -0
  10. data/TODO.md +66 -0
  11. data/bin/tipi +12 -0
  12. data/docs/README.md +62 -0
  13. data/docs/summary.md +60 -0
  14. data/examples/cuba.ru +23 -0
  15. data/examples/hanami-api.ru +23 -0
  16. data/examples/http_server.js +24 -0
  17. data/examples/http_server.rb +21 -0
  18. data/examples/http_server_forked.rb +29 -0
  19. data/examples/http_server_graceful.rb +27 -0
  20. data/examples/http_server_simple.rb +11 -0
  21. data/examples/http_server_throttled.rb +15 -0
  22. data/examples/http_server_timeout.rb +35 -0
  23. data/examples/http_ws_server.rb +37 -0
  24. data/examples/https_server.rb +24 -0
  25. data/examples/https_server_forked.rb +32 -0
  26. data/examples/https_wss_server.rb +39 -0
  27. data/examples/rack_server.rb +12 -0
  28. data/examples/rack_server_https.rb +19 -0
  29. data/examples/rack_server_https_forked.rb +27 -0
  30. data/examples/websocket_secure_server.rb +27 -0
  31. data/examples/websocket_server.rb +24 -0
  32. data/examples/ws_page.html +34 -0
  33. data/examples/wss_page.html +34 -0
  34. data/lib/tipi.rb +54 -0
  35. data/lib/tipi/http1_adapter.rb +268 -0
  36. data/lib/tipi/http2_adapter.rb +74 -0
  37. data/lib/tipi/http2_stream.rb +134 -0
  38. data/lib/tipi/rack_adapter.rb +67 -0
  39. data/lib/tipi/request.rb +118 -0
  40. data/lib/tipi/version.rb +5 -0
  41. data/lib/tipi/websocket.rb +61 -0
  42. data/test/coverage.rb +45 -0
  43. data/test/eg.rb +27 -0
  44. data/test/helper.rb +51 -0
  45. data/test/run.rb +5 -0
  46. data/test/test_http_server.rb +321 -0
  47. data/tipi.gemspec +34 -0
  48. metadata +241 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'fileutils'
6
+ require_relative './eg'
7
+
8
+ require_relative './coverage' if ENV['COVERAGE']
9
+
10
+ require 'minitest/autorun'
11
+ require 'minitest/reporters'
12
+
13
+ require 'polyphony'
14
+
15
+ ::Exception.__disable_sanitized_backtrace__ = true
16
+
17
+ Minitest::Reporters.use! [
18
+ Minitest::Reporters::SpecReporter.new
19
+ ]
20
+
21
+ class MiniTest::Test
22
+ def setup
23
+ # puts "* setup #{self.name}"
24
+ if Fiber.current.children.size > 0
25
+ puts "Children left: #{Fiber.current.children.inspect}"
26
+ exit!
27
+ end
28
+ Fiber.current.setup_main_fiber
29
+ Fiber.current.instance_variable_set(:@auto_watcher, nil)
30
+ Thread.current.agent = Polyphony::LibevAgent.new
31
+ sleep 0
32
+ end
33
+
34
+ def teardown
35
+ # puts "* teardown #{self.name.inspect} Fiber.current: #{Fiber.current.inspect}"
36
+ Fiber.current.terminate_all_children
37
+ Fiber.current.await_all_children
38
+ rescue => e
39
+ puts e
40
+ puts e.backtrace.join("\n")
41
+ exit!
42
+ end
43
+ end
44
+
45
+ module Kernel
46
+ def capture_exception
47
+ yield
48
+ rescue Exception => e
49
+ e
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{__dir__}/test_*.rb").each do |path|
4
+ require(path)
5
+ end
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'tipi'
5
+
6
+ class String
7
+ def http_lines
8
+ gsub "\n", "\r\n"
9
+ end
10
+ end
11
+
12
+ class IO
13
+ # Creates two mockup sockets for simulating server-client communication
14
+ def self.server_client_mockup
15
+ server_in, client_out = IO.pipe
16
+ client_in, server_out = IO.pipe
17
+
18
+ server_connection = mockup_connection(server_in, server_out, client_out)
19
+ client_connection = mockup_connection(client_in, client_out, server_out)
20
+
21
+ [server_connection, client_connection]
22
+ end
23
+
24
+ def self.mockup_connection(input, output, output2)
25
+ eg(
26
+ :read => ->(*args) { input.read(*args) },
27
+ :read_loop => ->(*args, &block) { input.read_loop(*args, &block) },
28
+ :readpartial => ->(*args) { input.readpartial(*args) },
29
+ :<< => ->(*args) { output.write(*args) },
30
+ :write => ->(*args) { output.write(*args) },
31
+ :close => -> { output.close },
32
+ :eof? => -> { output2.closed? }
33
+ )
34
+ end
35
+ end
36
+
37
+ class HTTP1ServerTest < MiniTest::Test
38
+ def teardown
39
+ @server&.interrupt if @server&.alive?
40
+ snooze
41
+ super
42
+ end
43
+
44
+ def spin_server(opts = {}, &handler)
45
+ server_connection, client_connection = IO.server_client_mockup
46
+ coproc = spin do
47
+ Tipi.client_loop(server_connection, opts, &handler)
48
+ end
49
+ [coproc, client_connection, server_connection]
50
+ end
51
+
52
+ def test_that_server_uses_content_length_in_http_1_0
53
+ @server, connection = spin_server do |req|
54
+ req.respond('Hello, world!', {})
55
+ end
56
+
57
+ # using HTTP 1.0, server should close connection after responding
58
+ connection << "GET / HTTP/1.0\r\n\r\n"
59
+
60
+ response = connection.readpartial(8192)
61
+ expected = <<~HTTP.chomp.http_lines
62
+ HTTP/1.0 200
63
+ Content-Length: 13
64
+
65
+ Hello, world!
66
+ HTTP
67
+ assert_equal(expected, response)
68
+ end
69
+
70
+ def test_that_server_uses_chunked_encoding_in_http_1_1
71
+ @server, connection = spin_server do |req|
72
+ req.respond('Hello, world!')
73
+ end
74
+
75
+ # using HTTP 1.0, server should close connection after responding
76
+ connection << "GET / HTTP/1.1\r\n\r\n"
77
+
78
+ response = connection.readpartial(8192)
79
+ expected = <<~HTTP.http_lines
80
+ HTTP/1.1 200
81
+ Transfer-Encoding: chunked
82
+
83
+ d
84
+ Hello, world!
85
+ 0
86
+
87
+ HTTP
88
+ assert_equal(expected, response)
89
+ end
90
+
91
+ def test_that_server_maintains_connection_when_using_keep_alives
92
+ puts 'test_that_server_maintains_connection_when_using_keep_alives'
93
+ @server, connection = spin_server do |req|
94
+ req.respond('Hi', {})
95
+ end
96
+
97
+ connection << "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
98
+ response = connection.readpartial(8192)
99
+ assert !connection.eof?
100
+ assert_equal("HTTP/1.0 200\r\nContent-Length: 2\r\n\r\nHi", response)
101
+
102
+ connection << "GET / HTTP/1.1\r\n\r\n"
103
+ response = connection.readpartial(8192)
104
+ assert !connection.eof?
105
+ expected = <<~HTTP.http_lines
106
+ HTTP/1.1 200
107
+ Transfer-Encoding: chunked
108
+
109
+ 2
110
+ Hi
111
+ 0
112
+
113
+ HTTP
114
+ assert_equal(expected, response)
115
+
116
+ connection << "GET / HTTP/1.0\r\n\r\n"
117
+ response = connection.readpartial(8192)
118
+ assert connection.eof?
119
+ assert_equal("HTTP/1.0 200\r\nContent-Length: 2\r\n\r\nHi", response)
120
+ end
121
+
122
+ def test_pipelining_client
123
+ @server, connection = spin_server do |req|
124
+ if req.headers['Foo'] == 'bar'
125
+ req.respond('Hello, foobar!', {})
126
+ else
127
+ req.respond('Hello, world!', {})
128
+ end
129
+ end
130
+
131
+ connection << "GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nFoo: bar\r\n\r\n"
132
+ 2.times { snooze }
133
+ response = connection.readpartial(8192)
134
+
135
+ expected = <<~HTTP.http_lines
136
+ HTTP/1.1 200
137
+ Transfer-Encoding: chunked
138
+
139
+ d
140
+ Hello, world!
141
+ 0
142
+
143
+ HTTP/1.1 200
144
+ Transfer-Encoding: chunked
145
+
146
+ e
147
+ Hello, foobar!
148
+ 0
149
+
150
+ HTTP
151
+ assert_equal(expected, response)
152
+ end
153
+
154
+ def test_body_chunks
155
+ chunks = []
156
+ request = nil
157
+ @server, connection = spin_server do |req|
158
+ request = req
159
+ req.send_headers
160
+ req.each_chunk do |c|
161
+ chunks << c
162
+ req << c.upcase
163
+ end
164
+ req.finish
165
+ end
166
+
167
+ connection << <<~HTTP.http_lines
168
+ POST / HTTP/1.1
169
+ Transfer-Encoding: chunked
170
+
171
+ 6
172
+ foobar
173
+ HTTP
174
+ 20.times { snooze }
175
+ assert request
176
+ assert_equal %w[foobar], chunks
177
+ assert !request.complete?
178
+
179
+ connection << "6\r\nbazbud\r\n"
180
+ 20.times { snooze }
181
+ assert_equal %w[foobar bazbud], chunks
182
+ assert !request.complete?
183
+
184
+ connection << "0\r\n\r\n"
185
+ 20.times { snooze }
186
+ assert_equal %w[foobar bazbud], chunks
187
+ assert request.complete?
188
+
189
+ 2.times { snooze }
190
+
191
+ response = connection.readpartial(8192)
192
+
193
+ expected = <<~HTTP.http_lines
194
+ HTTP/1.1 200
195
+ Transfer-Encoding: chunked
196
+
197
+ 6
198
+ FOOBAR
199
+ 6
200
+ BAZBUD
201
+ 0
202
+
203
+ HTTP
204
+ assert_equal(expected, response)
205
+ end
206
+
207
+ def test_upgrade
208
+ done = nil
209
+
210
+ opts = {
211
+ upgrade: {
212
+ echo: lambda do |conn, _headers|
213
+ p :echo1
214
+ conn << <<~HTTP.http_lines
215
+ HTTP/1.1 101 Switching Protocols
216
+ Upgrade: echo
217
+ Connection: Upgrade
218
+
219
+ HTTP
220
+
221
+ loop do
222
+ data = conn.readpartial(8192)
223
+ conn << data
224
+ snooze
225
+ rescue EOFError
226
+ break
227
+ end
228
+ done = true
229
+ end
230
+ }
231
+ }
232
+
233
+ @server, connection = spin_server(opts) do |req|
234
+ req.respond('Hi')
235
+ end
236
+
237
+ connection << "GET / HTTP/1.1\r\n\r\n"
238
+ response = connection.readpartial(8192)
239
+ assert !connection.eof?
240
+ expected = <<~HTTP.http_lines
241
+ HTTP/1.1 200
242
+ Transfer-Encoding: chunked
243
+
244
+ 2
245
+ Hi
246
+ 0
247
+
248
+ HTTP
249
+ assert_equal(expected, response)
250
+
251
+ connection << <<~HTTP.http_lines
252
+ GET / HTTP/1.1
253
+ Upgrade: echo
254
+ Connection: upgrade
255
+
256
+ HTTP
257
+
258
+ snooze
259
+ response = connection.readpartial(8192)
260
+ assert !connection.eof?
261
+ expected = <<~HTTP.http_lines
262
+ HTTP/1.1 101 Switching Protocols
263
+ Upgrade: echo
264
+ Connection: Upgrade
265
+
266
+ HTTP
267
+ assert_equal(expected, response)
268
+
269
+ assert !done
270
+
271
+ connection << 'foo'
272
+ assert_equal 'foo', connection.readpartial(8192)
273
+
274
+ connection << 'bar'
275
+ assert_equal 'bar', connection.readpartial(8192)
276
+
277
+ connection.close
278
+ assert !done
279
+
280
+ 10.times { snooze }
281
+ assert done
282
+ end
283
+
284
+ def test_big_download
285
+ chunk_size = 1000
286
+ chunk_count = 1000
287
+ chunk = '*' * chunk_size
288
+ @server, connection = spin_server do |req|
289
+ req.send_headers
290
+ chunk_count.times do |i|
291
+ req << chunk
292
+ snooze
293
+ end
294
+ req.finish
295
+ req.adapter.close
296
+ end
297
+
298
+ response = +''
299
+ count = 0
300
+
301
+ connection << "GET / HTTP/1.1\r\n\r\n"
302
+
303
+ while (data = connection.read(chunk_size))
304
+ response << data
305
+ count += 1
306
+ snooze
307
+ end
308
+
309
+ chunks = "#{chunk_size.to_s(16)}\n#{'*' * chunk_size}\n" * chunk_count
310
+ expected = <<~HTTP.http_lines
311
+ HTTP/1.1 200
312
+ Transfer-Encoding: chunked
313
+
314
+ #{chunks}0
315
+
316
+ HTTP
317
+
318
+ assert_equal expected, response
319
+ assert count >= chunk_count
320
+ end
321
+ end
@@ -0,0 +1,34 @@
1
+ require_relative './lib/tipi/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'tipi'
5
+ s.version = Tipi::VERSION
6
+ s.licenses = ['MIT']
7
+ s.summary = 'Tipi - the All-in-one Web Server for Ruby Apps'
8
+ s.author = 'Sharon Rosner'
9
+ s.email = 'ciconia@gmail.com'
10
+ s.files = `git ls-files`.split
11
+ s.homepage = 'http://github.com/digital-fabric/tipi'
12
+ s.metadata = {
13
+ "source_code_uri" => "https://github.com/digital-fabric/tipi"
14
+ }
15
+ s.rdoc_options = ["--title", "tipi", "--main", "README.md"]
16
+ s.extra_rdoc_files = ["README.md"]
17
+ s.require_paths = ["lib"]
18
+ s.required_ruby_version = '>= 2.6'
19
+
20
+ s.executables = ['tipi']
21
+
22
+ s.add_runtime_dependency 'polyphony', '~>0.43.5'
23
+
24
+ s.add_runtime_dependency 'http_parser.rb', '~>0.6.0'
25
+ s.add_runtime_dependency 'http-2', '~>0.10.0'
26
+ s.add_runtime_dependency 'rack', '>=2.0.8', '<2.3.0'
27
+ s.add_runtime_dependency 'websocket', '~>1.2.8'
28
+
29
+ s.add_development_dependency 'rake', '~>12.3.3'
30
+ s.add_development_dependency 'localhost', '~>1.1.4'
31
+ s.add_development_dependency 'minitest', '~>5.11.3'
32
+ s.add_development_dependency 'minitest-reporters', '~>1.4.2'
33
+ s.add_development_dependency 'simplecov', '~>0.17.1'
34
+ end
metadata ADDED
@@ -0,0 +1,241 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tipi
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.30'
5
+ platform: ruby
6
+ authors:
7
+ - Sharon Rosner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-07-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: polyphony
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.43.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.43.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: http_parser.rb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.6.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: http-2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.10.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.10.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.8
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: 2.3.0
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 2.0.8
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: 2.3.0
75
+ - !ruby/object:Gem::Dependency
76
+ name: websocket
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 1.2.8
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 1.2.8
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 12.3.3
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 12.3.3
103
+ - !ruby/object:Gem::Dependency
104
+ name: localhost
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 1.1.4
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 1.1.4
117
+ - !ruby/object:Gem::Dependency
118
+ name: minitest
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 5.11.3
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 5.11.3
131
+ - !ruby/object:Gem::Dependency
132
+ name: minitest-reporters
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 1.4.2
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: 1.4.2
145
+ - !ruby/object:Gem::Dependency
146
+ name: simplecov
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 0.17.1
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 0.17.1
159
+ description:
160
+ email: ciconia@gmail.com
161
+ executables:
162
+ - tipi
163
+ extensions: []
164
+ extra_rdoc_files:
165
+ - README.md
166
+ files:
167
+ - ".github/workflows/test.yml"
168
+ - ".gitignore"
169
+ - CHANGELOG.md
170
+ - Gemfile
171
+ - Gemfile.lock
172
+ - LICENSE
173
+ - README.md
174
+ - Rakefile
175
+ - TODO.md
176
+ - bin/tipi
177
+ - docs/README.md
178
+ - docs/summary.md
179
+ - examples/cuba.ru
180
+ - examples/hanami-api.ru
181
+ - examples/http_server.js
182
+ - examples/http_server.rb
183
+ - examples/http_server_forked.rb
184
+ - examples/http_server_graceful.rb
185
+ - examples/http_server_simple.rb
186
+ - examples/http_server_throttled.rb
187
+ - examples/http_server_timeout.rb
188
+ - examples/http_ws_server.rb
189
+ - examples/https_server.rb
190
+ - examples/https_server_forked.rb
191
+ - examples/https_wss_server.rb
192
+ - examples/rack_server.rb
193
+ - examples/rack_server_https.rb
194
+ - examples/rack_server_https_forked.rb
195
+ - examples/websocket_secure_server.rb
196
+ - examples/websocket_server.rb
197
+ - examples/ws_page.html
198
+ - examples/wss_page.html
199
+ - lib/tipi.rb
200
+ - lib/tipi/http1_adapter.rb
201
+ - lib/tipi/http2_adapter.rb
202
+ - lib/tipi/http2_stream.rb
203
+ - lib/tipi/rack_adapter.rb
204
+ - lib/tipi/request.rb
205
+ - lib/tipi/version.rb
206
+ - lib/tipi/websocket.rb
207
+ - test/coverage.rb
208
+ - test/eg.rb
209
+ - test/helper.rb
210
+ - test/run.rb
211
+ - test/test_http_server.rb
212
+ - tipi.gemspec
213
+ homepage: http://github.com/digital-fabric/tipi
214
+ licenses:
215
+ - MIT
216
+ metadata:
217
+ source_code_uri: https://github.com/digital-fabric/tipi
218
+ post_install_message:
219
+ rdoc_options:
220
+ - "--title"
221
+ - tipi
222
+ - "--main"
223
+ - README.md
224
+ require_paths:
225
+ - lib
226
+ required_ruby_version: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: '2.6'
231
+ required_rubygems_version: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ requirements: []
237
+ rubygems_version: 3.0.6
238
+ signing_key:
239
+ specification_version: 4
240
+ summary: Tipi - the All-in-one Web Server for Ruby Apps
241
+ test_files: []