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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/test.yml +3 -1
- data/.gitignore +3 -1
- data/CHANGELOG.md +34 -0
- data/Gemfile +7 -1
- data/Gemfile.lock +53 -33
- data/README.md +184 -8
- data/Rakefile +1 -7
- data/benchmarks/bm_http1_parser.rb +85 -0
- data/bin/benchmark +37 -0
- data/bin/h1pd +6 -0
- data/bin/tipi +3 -21
- data/bm.png +0 -0
- data/df/agent.rb +1 -1
- data/df/sample_agent.rb +2 -2
- data/df/server.rb +3 -1
- data/df/server_utils.rb +48 -46
- data/examples/full_service.rb +13 -0
- data/examples/hello.rb +5 -0
- data/examples/hello.ru +3 -3
- data/examples/http1_parser.rb +10 -8
- data/examples/http_server.js +1 -1
- data/examples/http_server.rb +4 -1
- data/examples/http_server_graceful.rb +1 -1
- data/examples/https_server.rb +41 -15
- data/examples/rack_server_forked.rb +26 -0
- data/examples/rack_server_https_forked.rb +1 -1
- data/examples/servername_cb.rb +37 -0
- data/examples/websocket_demo.rb +1 -1
- data/lib/tipi/acme.rb +320 -0
- data/lib/tipi/cli.rb +93 -0
- data/lib/tipi/config_dsl.rb +13 -13
- data/lib/tipi/configuration.rb +2 -2
- data/lib/tipi/controller/bare_polyphony.rb +0 -0
- data/lib/tipi/controller/bare_stock.rb +10 -0
- data/lib/tipi/controller/extensions.rb +37 -0
- data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
- data/lib/tipi/controller/web_polyphony.rb +353 -0
- data/lib/tipi/controller/web_stock.rb +635 -0
- data/lib/tipi/controller.rb +12 -0
- data/lib/tipi/digital_fabric/agent.rb +5 -5
- data/lib/tipi/digital_fabric/agent_proxy.rb +15 -8
- data/lib/tipi/digital_fabric/executive.rb +7 -3
- data/lib/tipi/digital_fabric/protocol.rb +3 -3
- data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
- data/lib/tipi/digital_fabric/service.rb +17 -18
- data/lib/tipi/handler.rb +2 -2
- data/lib/tipi/http1_adapter.rb +85 -124
- data/lib/tipi/http2_adapter.rb +29 -16
- data/lib/tipi/http2_stream.rb +52 -57
- data/lib/tipi/rack_adapter.rb +2 -2
- data/lib/tipi/response_extensions.rb +1 -1
- data/lib/tipi/supervisor.rb +75 -0
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +3 -3
- data/lib/tipi.rb +9 -7
- data/test/coverage.rb +2 -2
- data/test/helper.rb +60 -12
- data/test/test_http_server.rb +14 -41
- data/test/test_request.rb +2 -29
- data/tipi.gemspec +10 -10
- metadata +80 -54
- data/examples/automatic_certificate.rb +0 -193
- data/ext/tipi/extconf.rb +0 -12
- data/ext/tipi/http1_parser.c +0 -534
- data/ext/tipi/http1_parser.h +0 -18
- data/ext/tipi/tipi_ext.c +0 -5
- 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
|