tipi 0.38 → 0.42

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