tipi 0.41 → 0.46

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +3 -1
  4. data/.gitignore +3 -1
  5. data/CHANGELOG.md +34 -0
  6. data/Gemfile +7 -1
  7. data/Gemfile.lock +53 -33
  8. data/README.md +184 -8
  9. data/Rakefile +1 -7
  10. data/benchmarks/bm_http1_parser.rb +85 -0
  11. data/bin/benchmark +37 -0
  12. data/bin/h1pd +6 -0
  13. data/bin/tipi +3 -21
  14. data/bm.png +0 -0
  15. data/df/agent.rb +1 -1
  16. data/df/sample_agent.rb +2 -2
  17. data/df/server.rb +3 -1
  18. data/df/server_utils.rb +48 -46
  19. data/examples/full_service.rb +13 -0
  20. data/examples/hello.rb +5 -0
  21. data/examples/hello.ru +3 -3
  22. data/examples/http1_parser.rb +10 -8
  23. data/examples/http_server.js +1 -1
  24. data/examples/http_server.rb +4 -1
  25. data/examples/http_server_graceful.rb +1 -1
  26. data/examples/https_server.rb +41 -15
  27. data/examples/rack_server_forked.rb +26 -0
  28. data/examples/rack_server_https_forked.rb +1 -1
  29. data/examples/servername_cb.rb +37 -0
  30. data/examples/websocket_demo.rb +1 -1
  31. data/lib/tipi/acme.rb +320 -0
  32. data/lib/tipi/cli.rb +93 -0
  33. data/lib/tipi/config_dsl.rb +13 -13
  34. data/lib/tipi/configuration.rb +2 -2
  35. data/lib/tipi/controller/bare_polyphony.rb +0 -0
  36. data/lib/tipi/controller/bare_stock.rb +10 -0
  37. data/lib/tipi/controller/extensions.rb +37 -0
  38. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  39. data/lib/tipi/controller/web_polyphony.rb +353 -0
  40. data/lib/tipi/controller/web_stock.rb +635 -0
  41. data/lib/tipi/controller.rb +12 -0
  42. data/lib/tipi/digital_fabric/agent.rb +5 -5
  43. data/lib/tipi/digital_fabric/agent_proxy.rb +15 -8
  44. data/lib/tipi/digital_fabric/executive.rb +7 -3
  45. data/lib/tipi/digital_fabric/protocol.rb +3 -3
  46. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  47. data/lib/tipi/digital_fabric/service.rb +17 -18
  48. data/lib/tipi/handler.rb +2 -2
  49. data/lib/tipi/http1_adapter.rb +85 -124
  50. data/lib/tipi/http2_adapter.rb +29 -16
  51. data/lib/tipi/http2_stream.rb +52 -57
  52. data/lib/tipi/rack_adapter.rb +2 -2
  53. data/lib/tipi/response_extensions.rb +1 -1
  54. data/lib/tipi/supervisor.rb +75 -0
  55. data/lib/tipi/version.rb +1 -1
  56. data/lib/tipi/websocket.rb +3 -3
  57. data/lib/tipi.rb +9 -7
  58. data/test/coverage.rb +2 -2
  59. data/test/helper.rb +60 -12
  60. data/test/test_http_server.rb +14 -41
  61. data/test/test_request.rb +2 -29
  62. data/tipi.gemspec +10 -10
  63. metadata +80 -54
  64. data/examples/automatic_certificate.rb +0 -193
  65. data/ext/tipi/extconf.rb +0 -12
  66. data/ext/tipi/http1_parser.c +0 -534
  67. data/ext/tipi/http1_parser.h +0 -18
  68. data/ext/tipi/tipi_ext.c +0 -5
  69. data/lib/tipi/http1_adapter_new.rb +0 -293
@@ -1,293 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'tipi_ext'
4
- require_relative './http2_adapter'
5
- require 'qeweney/request'
6
-
7
- module Tipi
8
- # HTTP1 protocol implementation
9
- class HTTP1AdapterNew
10
- attr_reader :conn
11
-
12
- # Initializes a protocol adapter instance
13
- def initialize(conn, opts)
14
- @conn = conn
15
- @opts = opts
16
- @first = true
17
- @parser = Tipi::HTTP1Parser.new(@conn)
18
- end
19
-
20
- def each(&block)
21
- while true
22
- headers = @parser.parse_headers
23
- break unless headers
24
-
25
- # handle_request should return false if connection is persistent
26
- # break if handle_request(headers, &block)
27
- handle_request(headers, &block)
28
- end
29
- rescue Tipi::HTTP1Parser::Error
30
- # ignore
31
- rescue SystemCallError, IOError
32
- # ignore
33
- ensure
34
- finalize_client_loop
35
- end
36
-
37
- def handle_request(headers, &block)
38
- scheme = (proto = headers['x-forwarded-proto']) ?
39
- proto.downcase : scheme_from_connection
40
- headers[':scheme'] = scheme
41
- @protocol = headers[':protocol']
42
- if @first
43
- headers[':first'] = true
44
- @first = nil
45
- end
46
-
47
- request = Qeweney::Request.new(headers, self)
48
- return true if upgrade_connection(request.headers, &block)
49
-
50
- block.call(request)
51
- return !request.keep_alive?
52
- end
53
-
54
- def finalize_client_loop
55
- @parser = nil
56
- @splicing_pipe = nil
57
- @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
58
- @conn.close
59
- end
60
-
61
- # Reads a body chunk for the current request. Transfers control to the parse
62
- # loop, and resumes once the parse_loop has fired the on_body callback
63
- def get_body_chunk(request)
64
- raise NotImplementedError
65
- end
66
-
67
- # Waits for the current request to complete. Transfers control to the parse
68
- # loop, and resumes once the parse_loop has fired the on_message_complete
69
- # callback
70
- def consume_request(request)
71
- raise NotImplementedError
72
- end
73
-
74
- def protocol
75
- @protocol
76
- end
77
-
78
- # Upgrades the connection to a different protocol, if the 'Upgrade' header is
79
- # given. By default the only supported upgrade protocol is HTTP2. Additional
80
- # protocols, notably WebSocket, can be specified by passing a hash to the
81
- # :upgrade option when starting a server:
82
- #
83
- # def ws_handler(conn)
84
- # conn << 'hi'
85
- # msg = conn.recv
86
- # conn << "You said #{msg}"
87
- # conn << 'bye'
88
- # conn.close
89
- # end
90
- #
91
- # opts = {
92
- # upgrade: {
93
- # websocket: Tipi::Websocket.handler(&method(:ws_handler))
94
- # }
95
- # }
96
- # Tipi.serve('0.0.0.0', 1234, opts) { |req| ... }
97
- #
98
- # @param headers [Hash] request headers
99
- # @return [boolean] truthy if the connection has been upgraded
100
- def upgrade_connection(headers, &block)
101
- upgrade_protocol = headers['upgrade']
102
- return nil unless upgrade_protocol
103
-
104
- upgrade_protocol = upgrade_protocol.downcase.to_sym
105
- upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]
106
- return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler
107
- return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c
108
-
109
- nil
110
- end
111
-
112
- def upgrade_with_handler(handler, headers)
113
- @parser = @requests_head = @requests_tail = nil
114
- handler.(self, headers)
115
- true
116
- end
117
-
118
- def upgrade_to_http2(headers, &block)
119
- @parser = @requests_head = @requests_tail = nil
120
- HTTP2Adapter.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
121
- true
122
- end
123
-
124
- # Returns headers for HTTP2 upgrade
125
- # @param headers [Hash] request headers
126
- # @return [Hash] headers for HTTP2 upgrade
127
- def http2_upgraded_headers(headers)
128
- headers.merge(
129
- ':scheme' => 'http',
130
- ':authority' => headers['host']
131
- )
132
- end
133
-
134
- def websocket_connection(request)
135
- Tipi::Websocket.new(@conn, request.headers)
136
- end
137
-
138
- def scheme_from_connection
139
- @conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
140
- end
141
-
142
- # response API
143
-
144
- CRLF = "\r\n"
145
- CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
146
-
147
- # Sends response including headers and body. Waits for the request to complete
148
- # if not yet completed. The body is sent using chunked transfer encoding.
149
- # @param request [Qeweney::Request] HTTP request
150
- # @param body [String] response body
151
- # @param headers
152
- def respond(request, body, headers)
153
- consume_request(request) if @parsing
154
- formatted_headers = format_headers(headers, body, false)
155
- request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
156
- if body
157
- @conn.write(formatted_headers, body)
158
- else
159
- @conn.write(formatted_headers)
160
- end
161
- end
162
-
163
- def respond_from_io(request, io, headers, chunk_size = 2**14)
164
- consume_request(request) if @parsing
165
-
166
- formatted_headers = format_headers(headers, true, true)
167
- request.tx_incr(formatted_headers.bytesize)
168
-
169
- # assume chunked encoding
170
- Thread.current.backend.splice_chunks(
171
- io,
172
- @conn,
173
- formatted_headers,
174
- "0\r\n\r\n",
175
- ->(len) { "#{len.to_s(16)}\r\n" },
176
- "\r\n",
177
- chunk_size
178
- )
179
- end
180
-
181
- # Sends response headers. If empty_response is truthy, the response status
182
- # code will default to 204, otherwise to 200.
183
- # @param request [Qeweney::Request] HTTP request
184
- # @param headers [Hash] response headers
185
- # @param empty_response [boolean] whether a response body will be sent
186
- # @param chunked [boolean] whether to use chunked transfer encoding
187
- # @return [void]
188
- def send_headers(request, headers, empty_response: false, chunked: true)
189
- formatted_headers = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
190
- request.tx_incr(formatted_headers.bytesize)
191
- @conn.write(formatted_headers)
192
- end
193
-
194
- # Sends a response body chunk. If no headers were sent, default headers are
195
- # sent using #send_headers. if the done option is true(thy), an empty chunk
196
- # will be sent to signal response completion to the client.
197
- # @param request [Qeweney::Request] HTTP request
198
- # @param chunk [String] response body chunk
199
- # @param done [boolean] whether the response is completed
200
- # @return [void]
201
- def send_chunk(request, chunk, done: false)
202
- data = +''
203
- data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
204
- data << "0\r\n\r\n" if done
205
- return if data.empty?
206
-
207
- request.tx_incr(data.bytesize)
208
- @conn.write(data)
209
- end
210
-
211
- def send_chunk_from_io(request, io, r, w, chunk_size)
212
- len = w.splice(io, chunk_size)
213
- if len > 0
214
- Thread.current.backend.chain(
215
- [:write, @conn, "#{len.to_s(16)}\r\n"],
216
- [:splice, r, @conn, len],
217
- [:write, @conn, "\r\n"]
218
- )
219
- else
220
- @conn.write("0\r\n\r\n")
221
- end
222
- len
223
- end
224
-
225
- # Finishes the response to the current request. If no headers were sent,
226
- # default headers are sent using #send_headers.
227
- # @return [void]
228
- def finish(request)
229
- request.tx_incr(5)
230
- @conn << "0\r\n\r\n"
231
- end
232
-
233
- def close
234
- @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
235
- @conn.close
236
- end
237
-
238
- private
239
-
240
- INTERNAL_HEADER_REGEXP = /^:/.freeze
241
-
242
- # Formats response headers into an array. If empty_response is true(thy),
243
- # the response status code will default to 204, otherwise to 200.
244
- # @param headers [Hash] response headers
245
- # @param body [boolean] whether a response body will be sent
246
- # @param chunked [boolean] whether to use chunked transfer encoding
247
- # @return [String] formatted response headers
248
- def format_headers(headers, body, chunked)
249
- status = headers[':status']
250
- status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
251
- lines = format_status_line(body, status, chunked)
252
- headers.each do |k, v|
253
- next if k =~ INTERNAL_HEADER_REGEXP
254
-
255
- collect_header_lines(lines, k, v)
256
- end
257
- lines << CRLF
258
- lines
259
- end
260
-
261
- def format_status_line(body, status, chunked)
262
- if !body
263
- empty_status_line(status)
264
- else
265
- with_body_status_line(status, body, chunked)
266
- end
267
- end
268
-
269
- def empty_status_line(status)
270
- if status == 204
271
- +"HTTP/1.1 #{status}\r\n"
272
- else
273
- +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
274
- end
275
- end
276
-
277
- def with_body_status_line(status, body, chunked)
278
- if chunked
279
- +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
280
- else
281
- +"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
282
- end
283
- end
284
-
285
- def collect_header_lines(lines, key, value)
286
- if value.is_a?(Array)
287
- value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
288
- else
289
- lines << "#{key}: #{value}\r\n"
290
- end
291
- end
292
- end
293
- end