tipi 0.33 → 0.37.1

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