tipi 0.38 → 0.42

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +5 -1
  3. data/.gitignore +5 -0
  4. data/CHANGELOG.md +34 -0
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +58 -16
  7. data/Rakefile +7 -3
  8. data/TODO.md +77 -1
  9. data/benchmarks/bm_http1_parser.rb +61 -0
  10. data/bin/benchmark +37 -0
  11. data/bin/h1pd +6 -0
  12. data/bin/tipi +3 -21
  13. data/df/sample_agent.rb +1 -1
  14. data/df/server.rb +16 -47
  15. data/df/server_utils.rb +178 -0
  16. data/examples/full_service.rb +13 -0
  17. data/examples/http1_parser.rb +55 -0
  18. data/examples/http_server.rb +15 -3
  19. data/examples/http_server_forked.rb +5 -1
  20. data/examples/http_server_routes.rb +29 -0
  21. data/examples/http_server_static.rb +26 -0
  22. data/examples/http_server_throttled.rb +3 -2
  23. data/examples/https_server.rb +6 -4
  24. data/examples/https_wss_server.rb +2 -1
  25. data/examples/rack_server.rb +5 -0
  26. data/examples/rack_server_https.rb +1 -1
  27. data/examples/rack_server_https_forked.rb +4 -3
  28. data/examples/routing_server.rb +5 -4
  29. data/examples/servername_cb.rb +37 -0
  30. data/examples/websocket_demo.rb +2 -8
  31. data/examples/ws_page.html +2 -2
  32. data/ext/tipi/extconf.rb +13 -0
  33. data/ext/tipi/http1_parser.c +823 -0
  34. data/ext/tipi/http1_parser.h +18 -0
  35. data/ext/tipi/tipi_ext.c +5 -0
  36. data/lib/tipi.rb +89 -1
  37. data/lib/tipi/acme.rb +308 -0
  38. data/lib/tipi/cli.rb +30 -0
  39. data/lib/tipi/digital_fabric/agent.rb +22 -17
  40. data/lib/tipi/digital_fabric/agent_proxy.rb +95 -40
  41. data/lib/tipi/digital_fabric/executive.rb +6 -2
  42. data/lib/tipi/digital_fabric/protocol.rb +87 -15
  43. data/lib/tipi/digital_fabric/request_adapter.rb +6 -10
  44. data/lib/tipi/digital_fabric/service.rb +77 -51
  45. data/lib/tipi/http1_adapter.rb +116 -117
  46. data/lib/tipi/http2_adapter.rb +56 -10
  47. data/lib/tipi/http2_stream.rb +106 -53
  48. data/lib/tipi/rack_adapter.rb +2 -53
  49. data/lib/tipi/response_extensions.rb +17 -0
  50. data/lib/tipi/version.rb +1 -1
  51. data/security/http1.rb +12 -0
  52. data/test/helper.rb +60 -11
  53. data/test/test_http1_parser.rb +586 -0
  54. data/test/test_http_server.rb +0 -27
  55. data/test/test_request.rb +1 -28
  56. data/tipi.gemspec +11 -5
  57. metadata +96 -22
  58. data/e +0 -0
@@ -3,20 +3,34 @@
3
3
  require 'http/2'
4
4
  require_relative './http2_stream'
5
5
 
6
+ # patch to fix bug in HTTP2::Stream
7
+ class HTTP2::Stream
8
+ def end_stream?(frame)
9
+ case frame[:type]
10
+ when :data, :headers, :continuation
11
+ frame[:flags]&.include?(:end_stream)
12
+ else false
13
+ end
14
+ end
15
+ end
16
+
6
17
  module Tipi
7
18
  # HTTP2 server adapter
8
19
  class HTTP2Adapter
9
- def self.upgrade_each(socket, opts, headers, &block)
10
- adapter = new(socket, opts, headers)
20
+ def self.upgrade_each(socket, opts, headers, body, &block)
21
+ adapter = new(socket, opts, headers, body)
11
22
  adapter.each(&block)
12
23
  end
13
24
 
14
- def initialize(conn, opts, upgrade_headers = nil)
25
+ def initialize(conn, opts, upgrade_headers = nil, upgrade_body = nil)
15
26
  @conn = conn
16
27
  @opts = opts
17
28
  @upgrade_headers = upgrade_headers
29
+ @upgrade_body = upgrade_body
18
30
  @first = true
19
-
31
+ @rx = (upgrade_headers && upgrade_headers[':rx']) || 0
32
+ @tx = (upgrade_headers && upgrade_headers[':tx']) || 0
33
+
20
34
  @interface = ::HTTP2::Server.new
21
35
  @connection_fiber = Fiber.current
22
36
  @interface.on(:frame, &method(:send_frame))
@@ -24,7 +38,12 @@ module Tipi
24
38
  end
25
39
 
26
40
  def send_frame(data)
41
+ if @transfer_count_request
42
+ @transfer_count_request.tx_incr(data.bytesize)
43
+ end
27
44
  @conn << data
45
+ rescue Polyphony::BaseException
46
+ raise
28
47
  rescue Exception => e
29
48
  @connection_fiber.transfer e
30
49
  end
@@ -38,9 +57,9 @@ module Tipi
38
57
 
39
58
  def upgrade
40
59
  @conn << UPGRADE_MESSAGE
60
+ @tx += UPGRADE_MESSAGE.bytesize
41
61
  settings = @upgrade_headers['http2-settings']
42
- Fiber.current.schedule(nil)
43
- @interface.upgrade(settings, @upgrade_headers, '')
62
+ @interface.upgrade(settings, @upgrade_headers, @upgrade_body || '')
44
63
  ensure
45
64
  @upgrade_headers = nil
46
65
  end
@@ -49,16 +68,31 @@ module Tipi
49
68
  def each(&block)
50
69
  @interface.on(:stream) { |stream| start_stream(stream, &block) }
51
70
  upgrade if @upgrade_headers
52
-
53
- @conn.recv_loop(&@interface.method(:<<))
54
- rescue SystemCallError, IOError
71
+
72
+ @conn.recv_loop do |data|
73
+ @rx += data.bytesize
74
+ @interface << data
75
+ end
76
+ rescue SystemCallError, IOError, HTTP2::Error::Error
55
77
  # ignore
56
78
  ensure
57
79
  finalize_client_loop
58
80
  end
81
+
82
+ def get_rx_count
83
+ count = @rx
84
+ @rx = 0
85
+ count
86
+ end
87
+
88
+ def get_tx_count
89
+ count = @tx
90
+ @tx = 0
91
+ count
92
+ end
59
93
 
60
94
  def start_stream(stream, &block)
61
- stream = HTTP2StreamHandler.new(stream, @conn, @first, &block)
95
+ stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)
62
96
  @first = nil if @first
63
97
  @streams[stream] = true
64
98
  end
@@ -66,11 +100,23 @@ module Tipi
66
100
  def finalize_client_loop
67
101
  @interface = nil
68
102
  @streams.each_key(&:stop)
103
+ @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
69
104
  @conn.close
70
105
  end
71
106
 
72
107
  def close
108
+ @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
73
109
  @conn.close
74
110
  end
111
+
112
+ def set_request_for_transfer_count(request)
113
+ @transfer_count_request = request
114
+ end
115
+
116
+ def unset_request_for_transfer_count(request)
117
+ return unless @transfer_count_request == request
118
+
119
+ @transfer_count_request = nil
120
+ end
75
121
  end
76
122
  end
@@ -9,13 +9,14 @@ module Tipi
9
9
  attr_accessor :__next__
10
10
  attr_reader :conn
11
11
 
12
- def initialize(stream, conn, first, &block)
12
+ def initialize(adapter, stream, conn, first, &block)
13
+ @adapter = adapter
13
14
  @stream = stream
14
15
  @conn = conn
15
16
  @first = first
16
17
  @connection_fiber = Fiber.current
17
- @stream_fiber = spin { |req| handle_request(req, &block) }
18
-
18
+ @stream_fiber = spin { run(&block) }
19
+
19
20
  # Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
20
21
  # The request handler is run on a separate fiber for each stream, allowing
21
22
  # concurrent handling of incoming requests on the same HTTP/2 connection.
@@ -32,112 +33,164 @@ module Tipi
32
33
  stream.on(:half_close, &method(:on_half_close))
33
34
  end
34
35
 
35
- def handle_request(request, &block)
36
+ def run(&block)
37
+ request = receive
36
38
  error = nil
37
39
  block.(request)
38
40
  @connection_fiber.schedule
39
- rescue Polyphony::MoveOn
40
- # ignore
41
+ rescue Polyphony::BaseException
42
+ raise
41
43
  rescue Exception => e
42
44
  error = e
43
45
  ensure
44
- @done = true
45
46
  @connection_fiber.schedule error
46
47
  end
47
48
 
48
49
  def on_headers(headers)
49
50
  @request = Qeweney::Request.new(headers.to_h, self)
51
+ @request.rx_incr(@adapter.get_rx_count)
52
+ @request.tx_incr(@adapter.get_tx_count)
50
53
  if @first
51
54
  @request.headers[':first'] = true
52
55
  @first = false
53
56
  end
54
- @stream_fiber.schedule @request
57
+ @stream_fiber << @request
55
58
  end
56
-
59
+
57
60
  def on_data(data)
58
- if @waiting_for_body_chunk
59
- @waiting_for_body_chunk = nil
60
- @stream_fiber.schedule data
61
- else
62
- @request.buffer_body_chunk(data)
63
- end
61
+ data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
62
+
63
+ (@buffered_chunks ||= []) << data
64
+ @get_body_chunk_fiber&.schedule
64
65
  end
65
66
 
66
67
  def on_half_close
67
- if @waiting_for_body_chunk
68
- @waiting_for_body_chunk = nil
69
- @stream_fiber.schedule
70
- elsif @waiting_for_half_close
71
- @waiting_for_half_close = nil
72
- @stream_fiber.schedule
73
- else
74
- @request.complete!
75
- end
68
+ @get_body_chunk_fiber&.schedule
69
+ @complete = true
76
70
  end
77
71
 
78
72
  def protocol
79
73
  'h2'
80
74
  end
81
-
82
- def get_body_chunk
83
- # called in the context of the stream fiber
84
- return nil if @request.complete?
85
-
86
- @waiting_for_body_chunk = true
87
- # the chunk (or an exception) will be returned once the stream fiber is
88
- # resumed
89
- suspend
75
+
76
+ def with_transfer_count(request)
77
+ @adapter.set_request_for_transfer_count(request)
78
+ yield
90
79
  ensure
91
- @waiting_for_body_chunk = nil
80
+ @adapter.unset_request_for_transfer_count(request)
92
81
  end
93
-
94
- # Wait for request to finish
95
- def consume_request
96
- return if @request.complete?
82
+
83
+ def get_body_chunk(request, buffered_only = false)
84
+ @buffered_chunks ||= []
85
+ return @buffered_chunks.shift unless @buffered_chunks.empty?
86
+ return nil if @complete
97
87
 
98
- @waiting_for_half_close = true
99
- suspend
100
- ensure
101
- @waiting_for_half_close = nil
88
+ begin
89
+ @get_body_chunk_fiber = Fiber.current
90
+ suspend
91
+ ensure
92
+ @get_body_chunk_fiber = nil
93
+ end
94
+ @buffered_chunks.shift
95
+ end
96
+
97
+ def get_body(request)
98
+ @buffered_chunks ||= []
99
+ return @buffered_chunks.join if @complete
100
+
101
+ while !@complete
102
+ begin
103
+ @get_body_chunk_fiber = Fiber.current
104
+ suspend
105
+ ensure
106
+ @get_body_chunk_fiber = nil
107
+ end
108
+ end
109
+ @buffered_chunks.join
110
+ end
111
+
112
+ def complete?(request)
113
+ @complete
102
114
  end
103
115
 
104
116
  # response API
105
- def respond(chunk, headers)
117
+ def respond(request, chunk, headers)
106
118
  headers[':status'] ||= Qeweney::Status::OK
107
- @stream.headers(headers, end_stream: false)
108
- @stream.data(chunk, end_stream: true)
109
- @headers_sent = true
119
+ headers[':status'] = headers[':status'].to_s
120
+ with_transfer_count(request) do
121
+ @stream.headers(transform_headers(headers))
122
+ @headers_sent = true
123
+ @stream.data(chunk || '')
124
+ end
125
+ rescue HTTP2::Error::StreamClosed
126
+ # ignore
127
+ end
128
+
129
+ def respond_from_io(request, io, headers, chunk_size = 2**16)
130
+ headers[':status'] ||= Qeweney::Status::OK
131
+ headers[':status'] = headers[':status'].to_s
132
+ with_transfer_count(request) do
133
+ @stream.headers(transform_headers(headers))
134
+ @headers_sent = true
135
+ while (chunk = io.read(chunk_size))
136
+ @stream.data(chunk)
137
+ end
138
+ end
139
+ rescue HTTP2::Error::StreamClosed
140
+ # ignore
141
+ end
142
+
143
+ def transform_headers(headers)
144
+ headers.each_with_object([]) do |(k, v), a|
145
+ if v.is_a?(Array)
146
+ v.each { |vv| a << [k, vv.to_s] }
147
+ else
148
+ a << [k, v.to_s]
149
+ end
150
+ end
110
151
  end
111
152
 
112
- def send_headers(headers, empty_response = false)
153
+ def send_headers(request, headers, empty_response: false)
113
154
  return if @headers_sent
114
155
 
115
156
  headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
116
- @stream.headers(headers, end_stream: false)
157
+ with_transfer_count(request) do
158
+ @stream.headers(transform_headers(headers), end_stream: false)
159
+ end
117
160
  @headers_sent = true
161
+ rescue HTTP2::Error::StreamClosed
162
+ # ignore
118
163
  end
119
164
 
120
- def send_chunk(chunk, done: false)
165
+ def send_chunk(request, chunk, done: false)
121
166
  send_headers({}, false) unless @headers_sent
122
167
 
123
168
  if chunk
124
- @stream.data(chunk, end_stream: done)
169
+ with_transfer_count(request) do
170
+ @stream.data(chunk, end_stream: done)
171
+ end
125
172
  elsif done
126
173
  @stream.close
127
174
  end
175
+ rescue HTTP2::Error::StreamClosed
176
+ # ignore
128
177
  end
129
178
 
130
- def finish
179
+ def finish(request)
131
180
  if @headers_sent
132
181
  @stream.close
133
182
  else
134
183
  headers[':status'] ||= Qeweney::Status::NO_CONTENT
135
- @stream.headers(headers, end_stream: true)
184
+ with_transfer_count(request) do
185
+ @stream.headers(transform_headers(headers), end_stream: true)
186
+ end
136
187
  end
188
+ rescue HTTP2::Error::StreamClosed
189
+ # ignore
137
190
  end
138
191
 
139
192
  def stop
140
- return if @done
193
+ return if @complete
141
194
 
142
195
  @stream.close
143
196
  @stream_fiber.schedule(Polyphony::MoveOn.new)
@@ -3,25 +3,7 @@
3
3
  require 'rack'
4
4
 
5
5
  module Tipi
6
- module RackAdapter
7
- # Implements a rack input stream:
8
- # https://www.rubydoc.info/github/rack/rack/master/file/SPEC#label-The+Input+Stream
9
- class InputStream
10
- def initialize(request)
11
- @request = request
12
- end
13
-
14
- def gets; end
15
-
16
- def read(length = nil, outbuf = nil); end
17
-
18
- def each(&block)
19
- @request.each_chunk(&block)
20
- end
21
-
22
- def rewind; end
23
- end
24
-
6
+ module RackAdapter
25
7
  class << self
26
8
  def run(app)
27
9
  ->(req) { respond(req, app.(env(req))) }
@@ -32,41 +14,8 @@ module Tipi
32
14
  instance_eval(src, path, 1)
33
15
  end
34
16
 
35
- RACK_ENV = {
36
- 'SCRIPT_NAME' => '',
37
- 'rack.version' => Rack::VERSION,
38
- 'SERVER_PORT' => '80', # ?
39
- 'rack.url_scheme' => 'http', # ?
40
- 'rack.errors' => STDERR, # ?
41
- 'rack.multithread' => false,
42
- 'rack.run_once' => false,
43
- 'rack.hijack?' => false,
44
- 'rack.hijack' => nil,
45
- 'rack.hijack_io' => nil,
46
- 'rack.session' => nil,
47
- 'rack.logger' => nil,
48
- 'rack.multipart.buffer_size' => nil,
49
- 'rack.multipar.tempfile_factory' => nil
50
- }
51
-
52
17
  def env(request)
53
- Hash.new do |h, k|
54
- h[k] = env_value_from_request(request, k)
55
- end
56
- end
57
-
58
- HTTP_HEADER_RE = /^HTTP_(.+)$/.freeze
59
-
60
- def env_value_from_request(request, key)
61
- case key
62
- when 'REQUEST_METHOD' then request.method
63
- when 'PATH_INFO' then request.path
64
- when 'QUERY_STRING' then request.query_string || ''
65
- when 'SERVER_NAME' then request.headers['host']
66
- when 'rack.input' then InputStream.new(request)
67
- when HTTP_HEADER_RE then request.headers[$1.downcase]
68
- else RACK_ENV[key]
69
- end
18
+ Qeweney.rack_env_from_request(request)
70
19
  end
71
20
 
72
21
  def respond(request, (status_code, headers, body))
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'qeweney/request'
4
+
5
+ module Tipi
6
+ module ResponseExtensions
7
+ SPLICE_CHUNKS_SIZE_THRESHOLD = 2**20
8
+
9
+ def serve_io(io, opts)
10
+ if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
11
+ @adapter.respond_from_io(self, io, opts[:headers], opts[:chunk_size] || 2**14)
12
+ else
13
+ respond(io.read, opts[:headers] || {})
14
+ end
15
+ end
16
+ end
17
+ end
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.38'
4
+ VERSION = '0.42'
5
5
  end