tipi 0.30

Sign up to get free protection for your applications and to get access to all the features.
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: []