tipi 0.33 → 0.37.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/Gemfile.lock +10 -4
  4. data/LICENSE +1 -1
  5. data/TODO.md +11 -47
  6. data/df/agent.rb +63 -0
  7. data/df/etc_benchmark.rb +15 -0
  8. data/df/multi_agent_supervisor.rb +87 -0
  9. data/df/multi_client.rb +84 -0
  10. data/df/routing_benchmark.rb +60 -0
  11. data/df/sample_agent.rb +89 -0
  12. data/df/server.rb +54 -0
  13. data/df/sse_page.html +29 -0
  14. data/df/stress.rb +24 -0
  15. data/df/ws_page.html +38 -0
  16. data/e +0 -0
  17. data/examples/http_request_ws_server.rb +35 -0
  18. data/examples/http_server.rb +6 -6
  19. data/examples/http_server_form.rb +23 -0
  20. data/examples/http_unix_socket_server.rb +17 -0
  21. data/examples/http_ws_server.rb +10 -12
  22. data/examples/routing_server.rb +34 -0
  23. data/examples/ws_page.html +1 -2
  24. data/lib/tipi.rb +5 -1
  25. data/lib/tipi/digital_fabric.rb +7 -0
  26. data/lib/tipi/digital_fabric/agent.rb +225 -0
  27. data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
  28. data/lib/tipi/digital_fabric/executive.rb +100 -0
  29. data/lib/tipi/digital_fabric/executive/index.html +69 -0
  30. data/lib/tipi/digital_fabric/protocol.rb +90 -0
  31. data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
  32. data/lib/tipi/digital_fabric/service.rb +230 -0
  33. data/lib/tipi/http1_adapter.rb +50 -14
  34. data/lib/tipi/http2_adapter.rb +4 -2
  35. data/lib/tipi/http2_stream.rb +20 -8
  36. data/lib/tipi/rack_adapter.rb +1 -1
  37. data/lib/tipi/version.rb +1 -1
  38. data/lib/tipi/websocket.rb +33 -29
  39. data/test/helper.rb +1 -2
  40. data/test/test_http_server.rb +10 -12
  41. data/test/test_request.rb +108 -0
  42. data/tipi.gemspec +7 -3
  43. metadata +57 -6
  44. data/lib/tipi/request.rb +0 -118
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'http/parser'
4
- require_relative './request'
5
4
  require_relative './http2_adapter'
5
+ require 'qeweney/request'
6
6
 
7
7
  module Tipi
8
8
  # HTTP1 protocol implementation
9
9
  class HTTP1Adapter
10
+ attr_reader :conn
11
+
10
12
  # Initializes a protocol adapter instance
11
13
  def initialize(conn, opts)
12
14
  @conn = conn
13
15
  @opts = opts
16
+ @first = true
14
17
  @parser = ::HTTP::Parser.new(self)
15
18
  end
16
19
 
@@ -28,6 +31,10 @@ module Tipi
28
31
  def handle_incoming_data(data, &block)
29
32
  @parser << data
30
33
  while (request = @requests_head)
34
+ if @first
35
+ request.headers[':first'] = true
36
+ @first = nil
37
+ end
31
38
  return true if upgrade_connection(request.headers, &block)
32
39
 
33
40
  @requests_head = request.__next__
@@ -77,9 +84,23 @@ module Tipi
77
84
  end
78
85
 
79
86
  def on_headers_complete(headers)
87
+ headers = normalize_headers(headers)
80
88
  headers[':path'] = @parser.request_url
81
- headers[':method'] = @parser.http_method
82
- queue_request(Request.new(headers, self))
89
+ headers[':method'] = @parser.http_method.downcase
90
+ queue_request(Qeweney::Request.new(headers, self))
91
+ end
92
+
93
+ def normalize_headers(headers)
94
+ headers.each_with_object({}) do |(k, v), h|
95
+ k = k.downcase
96
+ hk = h[k]
97
+ if hk
98
+ hk = h[k] = [hk] unless hk.is_a?(Array)
99
+ v.is_a?(Array) ? hk.concat(v) : hk << v
100
+ else
101
+ h[k] = v
102
+ end
103
+ end
83
104
  end
84
105
 
85
106
  def queue_request(request)
@@ -110,6 +131,14 @@ module Tipi
110
131
  # protocols, notably WebSocket, can be specified by passing a hash to the
111
132
  # :upgrade option when starting a server:
112
133
  #
134
+ # def ws_handler(conn)
135
+ # conn << 'hi'
136
+ # msg = conn.recv
137
+ # conn << "You said #{msg}"
138
+ # conn << 'bye'
139
+ # conn.close
140
+ # end
141
+ #
113
142
  # opts = {
114
143
  # upgrade: {
115
144
  # websocket: Tipi::Websocket.handler(&method(:ws_handler))
@@ -120,7 +149,7 @@ module Tipi
120
149
  # @param headers [Hash] request headers
121
150
  # @return [boolean] truthy if the connection has been upgraded
122
151
  def upgrade_connection(headers, &block)
123
- upgrade_protocol = headers['Upgrade']
152
+ upgrade_protocol = headers['upgrade']
124
153
  return nil unless upgrade_protocol
125
154
 
126
155
  upgrade_protocol = upgrade_protocol.downcase.to_sym
@@ -133,7 +162,7 @@ module Tipi
133
162
 
134
163
  def upgrade_with_handler(handler, headers)
135
164
  @parser = @requests_head = @requests_tail = nil
136
- handler.(@conn, headers)
165
+ handler.(self, headers)
137
166
  true
138
167
  end
139
168
 
@@ -149,9 +178,13 @@ module Tipi
149
178
  def http2_upgraded_headers(headers)
150
179
  headers.merge(
151
180
  ':scheme' => 'http',
152
- ':authority' => headers['Host']
181
+ ':authority' => headers['host']
153
182
  )
154
183
  end
184
+
185
+ def websocket_connection(req)
186
+ Tipi::Websocket.new(@conn, req.headers)
187
+ end
155
188
 
156
189
  # response API
157
190
 
@@ -164,15 +197,17 @@ module Tipi
164
197
  # @param headers
165
198
  def respond(body, headers)
166
199
  consume_request if @parsing
167
- data = format_headers(headers, body)
200
+ data = [format_headers(headers, body)]
168
201
  if body
169
202
  if @parser.http_minor == 0
170
203
  data << body
171
204
  else
172
- data << body.bytesize.to_s(16) << CRLF << body << CRLF_ZERO_CRLF_CRLF
205
+ # data << body.bytesize.to_s(16) << CRLF << body << CRLF_ZERO_CRLF_CRLF
206
+ data << "#{body.bytesize.to_s(16)}\r\n#{body}\r\n0\r\n\r\n"
173
207
  end
174
208
  end
175
- @conn.write(data.join)
209
+ # Polyphony.backend_sendv(@conn, data, 0)
210
+ @conn.write(*data)
176
211
  end
177
212
 
178
213
  DEFAULT_HEADERS_OPTS = {
@@ -187,7 +222,7 @@ module Tipi
187
222
  # @return [void]
188
223
  def send_headers(headers, opts = DEFAULT_HEADERS_OPTS)
189
224
  data = format_headers(headers, true)
190
- @conn.write(data.join)
225
+ @conn.write(data)
191
226
  end
192
227
 
193
228
  # Sends a response body chunk. If no headers were sent, default headers are
@@ -198,9 +233,9 @@ module Tipi
198
233
  # @return [void]
199
234
  def send_chunk(chunk, done: false)
200
235
  data = []
201
- data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
236
+ data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
202
237
  data << "0\r\n\r\n" if done
203
- @conn.write(data.join)
238
+ @conn.write(data.join) unless data.empty?
204
239
  end
205
240
 
206
241
  # Finishes the response to the current request. If no headers were sent,
@@ -222,8 +257,9 @@ module Tipi
222
257
  # @param empty_response [boolean] whether a response body will be sent
223
258
  # @return [String] formatted response headers
224
259
  def format_headers(headers, body)
225
- status = headers[':status'] || (body ? 200 : 204)
226
- lines = [format_status_line(body, status)]
260
+ status = headers[':status']
261
+ status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
262
+ lines = format_status_line(body, status)
227
263
  headers.each do |k, v|
228
264
  next if k =~ /^:/
229
265
 
@@ -15,6 +15,7 @@ module Tipi
15
15
  @conn = conn
16
16
  @opts = opts
17
17
  @upgrade_headers = upgrade_headers
18
+ @first = true
18
19
 
19
20
  @interface = ::HTTP2::Server.new
20
21
  @connection_fiber = Fiber.current
@@ -37,7 +38,7 @@ module Tipi
37
38
 
38
39
  def upgrade
39
40
  @conn << UPGRADE_MESSAGE
40
- settings = @upgrade_headers['HTTP2-Settings']
41
+ settings = @upgrade_headers['http2-settings']
41
42
  Fiber.current.schedule(nil)
42
43
  @interface.upgrade(settings, @upgrade_headers, '')
43
44
  ensure
@@ -57,7 +58,8 @@ module Tipi
57
58
  end
58
59
 
59
60
  def start_stream(stream, &block)
60
- stream = HTTP2StreamHandler.new(stream, &block)
61
+ stream = HTTP2StreamHandler.new(stream, @conn, @first, &block)
62
+ @first = nil if @first
61
63
  @streams[stream] = true
62
64
  end
63
65
 
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'http/2'
4
- require_relative './request'
4
+ require 'qeweney/request'
5
5
 
6
6
  module Tipi
7
7
  # Manages an HTTP 2 stream
8
8
  class HTTP2StreamHandler
9
9
  attr_accessor :__next__
10
+ attr_reader :conn
10
11
 
11
- def initialize(stream, &block)
12
+ def initialize(stream, conn, first, &block)
12
13
  @stream = stream
14
+ @conn = conn
15
+ @first = first
13
16
  @connection_fiber = Fiber.current
14
17
  @stream_fiber = spin { |req| handle_request(req, &block) }
15
18
 
@@ -43,7 +46,11 @@ module Tipi
43
46
  end
44
47
 
45
48
  def on_headers(headers)
46
- @request = Request.new(headers.to_h, self)
49
+ @request = Qeweney::Request.new(headers.to_h, self)
50
+ if @first
51
+ @request.headers[':first'] = true
52
+ @first = false
53
+ end
47
54
  @stream_fiber.schedule @request
48
55
  end
49
56
 
@@ -55,7 +62,7 @@ module Tipi
55
62
  @request.buffer_body_chunk(data)
56
63
  end
57
64
  end
58
-
65
+
59
66
  def on_half_close
60
67
  if @waiting_for_body_chunk
61
68
  @waiting_for_body_chunk = nil
@@ -96,7 +103,7 @@ module Tipi
96
103
 
97
104
  # response API
98
105
  def respond(chunk, headers)
99
- headers[':status'] ||= '200'
106
+ headers[':status'] ||= Qeweney::Status::OK
100
107
  @stream.headers(headers, end_stream: false)
101
108
  @stream.data(chunk, end_stream: true)
102
109
  @headers_sent = true
@@ -105,21 +112,26 @@ module Tipi
105
112
  def send_headers(headers, empty_response = false)
106
113
  return if @headers_sent
107
114
 
108
- headers[':status'] ||= (empty_response ? 204 : 200).to_s
115
+ headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
109
116
  @stream.headers(headers, end_stream: false)
110
117
  @headers_sent = true
111
118
  end
112
119
 
113
120
  def send_chunk(chunk, done: false)
114
121
  send_headers({}, false) unless @headers_sent
115
- @stream.data(chunk, end_stream: done)
122
+
123
+ if chunk
124
+ @stream.data(chunk, end_stream: done)
125
+ elsif done
126
+ @stream.close
127
+ end
116
128
  end
117
129
 
118
130
  def finish
119
131
  if @headers_sent
120
132
  @stream.close
121
133
  else
122
- headers[':status'] ||= '204'
134
+ headers[':status'] ||= Qeweney::Status::NO_CONTENT
123
135
  @stream.headers(headers, end_stream: true)
124
136
  end
125
137
  end
@@ -62,7 +62,7 @@ module Tipi
62
62
  when 'REQUEST_METHOD' then request.method
63
63
  when 'PATH_INFO' then request.path
64
64
  when 'QUERY_STRING' then request.query_string || ''
65
- when 'SERVER_NAME' then request.headers['Host']
65
+ when 'SERVER_NAME' then request.headers['host']
66
66
  when 'rack.input' then InputStream.new(request)
67
67
  when HTTP_HEADER_RE then request.headers[$1.downcase]
68
68
  else RACK_ENV[key]
data/lib/tipi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tipi
4
- VERSION = '0.33'
4
+ VERSION = '0.37.1'
5
5
  end
@@ -7,32 +7,17 @@ module Tipi
7
7
  # Websocket connection
8
8
  class Websocket
9
9
  def self.handler(&block)
10
- proc { |client, header|
11
- block.(new(client, header))
12
- }
13
- end
10
+ proc do |adapter, headers|
11
+ req = Qeweney::Request.new(headers, adapter)
12
+ websocket = req.upgrade_to_websocket
13
+ block.(websocket)
14
+ end
15
+ end
14
16
 
15
- def initialize(client, headers)
16
- @client = client
17
+ def initialize(conn, headers)
18
+ @conn = conn
17
19
  @headers = headers
18
- setup(headers)
19
- end
20
-
21
- S_WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
22
- UPGRADE_RESPONSE = <<~HTTP.gsub("\n", "\r\n")
23
- HTTP/1.1 101 Switching Protocols
24
- Upgrade: websocket
25
- Connection: Upgrade
26
- Sec-WebSocket-Accept: %<accept>s
27
-
28
- HTTP
29
-
30
- def setup(headers)
31
- key = headers['Sec-WebSocket-Key']
32
- @version = headers['Sec-WebSocket-Version'].to_i
33
- accept = Digest::SHA1.base64digest([key, S_WS_GUID].join)
34
- @client << format(UPGRADE_RESPONSE, accept: accept)
35
-
20
+ @version = headers['sec-websocket-version'].to_i
36
21
  @reader = ::WebSocket::Frame::Incoming::Server.new(version: @version)
37
22
  end
38
23
 
@@ -41,22 +26,41 @@ module Tipi
41
26
  return msg.to_s
42
27
  end
43
28
 
44
- @client.recv_loop do |data|
29
+ @conn.recv_loop do |data|
45
30
  @reader << data
46
31
  if (msg = @reader.next)
47
- break msg.to_s
32
+ return msg.to_s
48
33
  end
49
34
  end
50
-
35
+
51
36
  nil
52
37
  end
38
+
39
+ def recv_loop
40
+ if (msg = @reader.next)
41
+ yield msg.to_s
42
+ end
43
+
44
+ @conn.recv_loop do |data|
45
+ @reader << data
46
+ while (msg = @reader.next)
47
+ yield msg.to_s
48
+ end
49
+ end
50
+ end
53
51
 
52
+ OutgoingFrame = ::WebSocket::Frame::Outgoing::Server
53
+
54
54
  def send(data)
55
- frame = ::WebSocket::Frame::Outgoing::Server.new(
55
+ frame = OutgoingFrame.new(
56
56
  version: @version, data: data, type: :text
57
57
  )
58
- @client << frame.to_s
58
+ @conn << frame.to_s
59
59
  end
60
60
  alias_method :<<, :send
61
+
62
+ def close
63
+ @conn.close
64
+ end
61
65
  end
62
66
  end
data/test/helper.rb CHANGED
@@ -33,8 +33,7 @@ class MiniTest::Test
33
33
 
34
34
  def teardown
35
35
  # puts "* teardown #{self.name.inspect} Fiber.current: #{Fiber.current.inspect}"
36
- Fiber.current.terminate_all_children
37
- Fiber.current.await_all_children
36
+ Fiber.current.shutdown_all_children
38
37
  rescue => e
39
38
  puts e
40
39
  puts e.backtrace.join("\n")
@@ -23,13 +23,13 @@ class IO
23
23
 
24
24
  def self.mockup_connection(input, output, output2)
25
25
  eg(
26
- :read => ->(*args) { p [:read]; input.read(*args) },
27
- :read_loop => ->(*args, &block) { p [:read_loop, caller]; input.read_loop(*args, &block) },
28
- :recv_loop => ->(*args, &block) { p [:recv_loop]; input.read_loop(*args, &block) },
29
- :readpartial => ->(*args) { p [:readpartial]; input.readpartial(*args) },
30
- :recv => ->(*args) { p [:recv]; input.readpartial(*args) },
31
- :<< => ->(*args) { p [:<<, args]; output.write(*args) },
32
- :write => ->(*args) { p [:write, args]; output.write(*args) },
26
+ :read => ->(*args) { input.read(*args) },
27
+ :read_loop => ->(*args, &block) { input.read_loop(*args, &block) },
28
+ :recv_loop => ->(*args, &block) { input.read_loop(*args, &block) },
29
+ :readpartial => ->(*args) { input.readpartial(*args) },
30
+ :recv => ->(*args) { input.readpartial(*args) },
31
+ :<< => ->(*args) { output.write(*args) },
32
+ :write => ->(*args) { output.write(*args) },
33
33
  :close => -> { output.close },
34
34
  :eof? => -> { output2.closed? }
35
35
  )
@@ -91,7 +91,6 @@ class HTTP1ServerTest < MiniTest::Test
91
91
  end
92
92
 
93
93
  def test_that_server_maintains_connection_when_using_keep_alives
94
- puts 'test_that_server_maintains_connection_when_using_keep_alives'
95
94
  @server, connection = spin_server do |req|
96
95
  req.respond('Hi', {})
97
96
  end
@@ -123,7 +122,7 @@ class HTTP1ServerTest < MiniTest::Test
123
122
 
124
123
  def test_pipelining_client
125
124
  @server, connection = spin_server do |req|
126
- if req.headers['Foo'] == 'bar'
125
+ if req.headers['foo'] == 'bar'
127
126
  req.respond('Hello, foobar!', {})
128
127
  else
129
128
  req.respond('Hello, world!', {})
@@ -160,14 +159,12 @@ class HTTP1ServerTest < MiniTest::Test
160
159
  request = req
161
160
  req.send_headers
162
161
  req.each_chunk do |c|
163
- puts "chunk: #{c.inspect}"
164
162
  chunks << c
165
163
  req << c.upcase
166
164
  end
167
165
  req.finish
168
166
  end
169
167
 
170
- p connection
171
168
  connection << <<~HTTP.http_lines
172
169
  POST / HTTP/1.1
173
170
  Transfer-Encoding: chunked
@@ -213,7 +210,8 @@ class HTTP1ServerTest < MiniTest::Test
213
210
 
214
211
  opts = {
215
212
  upgrade: {
216
- echo: lambda do |conn, _headers|
213
+ echo: lambda do |adapter, _headers|
214
+ conn = adapter.conn
217
215
  conn << <<~HTTP.http_lines
218
216
  HTTP/1.1 101 Switching Protocols
219
217
  Upgrade: echo
@@ -0,0 +1,108 @@
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
+ :recv_loop => ->(*args, &block) { input.read_loop(*args, &block) },
29
+ :readpartial => ->(*args) { input.readpartial(*args) },
30
+ :recv => ->(*args) { input.readpartial(*args) },
31
+ :<< => ->(*args) { output.write(*args) },
32
+ :write => ->(*args) { output.write(*args) },
33
+ :close => -> { output.close },
34
+ :eof? => -> { output2.closed? }
35
+ )
36
+ end
37
+ end
38
+
39
+ class RequestHeadersTest < MiniTest::Test
40
+ def teardown
41
+ @server&.interrupt if @server&.alive?
42
+ snooze
43
+ super
44
+ end
45
+
46
+ def spin_server(opts = {}, &handler)
47
+ server_connection, client_connection = IO.server_client_mockup
48
+ coproc = spin do
49
+ Tipi.client_loop(server_connection, opts, &handler)
50
+ end
51
+ [coproc, client_connection, server_connection]
52
+ end
53
+
54
+ def test_request_headers
55
+ req = nil
56
+ @server, connection = spin_server do |r|
57
+ req = r
58
+ req.respond('Hello, world!')
59
+ end
60
+
61
+ connection << "GET /titi HTTP/1.1\r\nHost: blah.com\r\nFoo: bar\r\nhi: 1\r\nHi: 2\r\nhi: 3\r\n\r\n"
62
+
63
+ snooze
64
+
65
+ assert_kind_of Qeweney::Request, req
66
+ assert_equal 'blah.com', req.headers['host']
67
+ assert_equal 'bar', req.headers['foo']
68
+ assert_equal ['1', '3', '2'], req.headers['hi']
69
+ assert_equal 'get', req.headers[':method']
70
+ assert_equal '/titi', req.headers[':path']
71
+ end
72
+
73
+ def test_request_host
74
+ req = nil
75
+ @server, connection = spin_server do |r|
76
+ req = r
77
+ req.respond('Hello, world!')
78
+ end
79
+
80
+ connection << "GET /titi HTTP/1.1\nHost: blah.com\nFoo: bar\nhi: 1\nHi: 2\nhi: 3\n\n"
81
+ snooze
82
+ assert_equal 'blah.com', req.host
83
+ end
84
+
85
+ def test_request_connection
86
+ req = nil
87
+ @server, connection = spin_server do |r|
88
+ req = r
89
+ req.respond('Hello, world!')
90
+ end
91
+
92
+ connection << "GET /titi HTTP/1.1\nConnection: keep-alive\nFoo: bar\nhi: 1\nHi: 2\nhi: 3\n\n"
93
+ snooze
94
+ assert_equal 'keep-alive', req.connection
95
+ end
96
+
97
+ def test_request_upgrade_protocol
98
+ req = nil
99
+ @server, connection = spin_server do |r|
100
+ req = r
101
+ req.respond('Hello, world!')
102
+ end
103
+
104
+ connection << "GET /titi HTTP/1.1\nConnection: upgrade\nUpgrade: foobar\n\n"
105
+ snooze
106
+ assert_equal 'foobar', req.upgrade_protocol
107
+ end
108
+ end