tipi 0.41 → 0.46

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