tipi 0.36 → 0.39

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.
@@ -6,40 +6,40 @@ module DigitalFabric
6
6
  class RequestAdapter
7
7
  def initialize(agent, msg)
8
8
  @agent = agent
9
- @id = msg['id']
9
+ @id = msg[Protocol::Attribute::ID]
10
10
  end
11
11
 
12
12
  def protocol
13
13
  'df'
14
14
  end
15
15
 
16
- def get_body_chunk
16
+ def get_body_chunk(request)
17
17
  @agent.get_http_request_body(@id, 1)
18
18
  end
19
19
 
20
- def consume_request
20
+ def consume_request(request)
21
21
  @agent.get_http_request_body(@id, nil)
22
22
  end
23
23
 
24
- def respond(body, headers)
24
+ def respond(request, body, headers)
25
25
  @agent.send_df_message(
26
26
  Protocol.http_response(@id, body, headers, true)
27
27
  )
28
28
  end
29
29
 
30
- def send_headers(headers, opts = {})
30
+ def send_headers(request, headers, opts = {})
31
31
  @agent.send_df_message(
32
32
  Protocol.http_response(@id, nil, headers, false)
33
33
  )
34
34
  end
35
35
 
36
- def send_chunk(body, done: )
36
+ def send_chunk(request, body, done: )
37
37
  @agent.send_df_message(
38
38
  Protocol.http_response(@id, body, nil, done)
39
39
  )
40
40
  end
41
41
 
42
- def finish
42
+ def finish(request)
43
43
  @agent.send_df_message(
44
44
  Protocol.http_response(@id, nil, nil, true)
45
45
  )
@@ -99,10 +99,12 @@ module DigitalFabric
99
99
  end
100
100
 
101
101
  agent.http_request(req)
102
- rescue IOError, SystemCallError
102
+ rescue IOError, SystemCallError, HTTP2::Error::StreamClosed
103
103
  @counters[:errors] += 1
104
104
  rescue => e
105
105
  @counters[:errors] += 1
106
+ puts '*' * 40
107
+ p req
106
108
  p e
107
109
  puts e.backtrace.join("\n")
108
110
  req.respond(e.inspect, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
@@ -115,7 +117,7 @@ module DigitalFabric
115
117
  req.headers['x-request-id'] = SecureRandom.uuid
116
118
  conn = req.adapter.conn
117
119
  req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
118
- req.headers['x-forwarded-proto'] = conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
120
+ req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
119
121
  end
120
122
 
121
123
  def upgrade_request(req)
@@ -172,7 +174,7 @@ module DigitalFabric
172
174
  def find_agent(req)
173
175
  compile_agent_routes if @routing_changed
174
176
 
175
- host = req.headers['host'] || INVALID_HOST
177
+ host = req.headers[':authority'] || req.headers['host'] || INVALID_HOST
176
178
  path = req.headers[':path']
177
179
 
178
180
  route = @route_keys.find do |route|
@@ -180,11 +182,11 @@ module DigitalFabric
180
182
  end
181
183
  return @routes[route] if route
182
184
 
183
- # search for a known route for an agent that recently unmounted
184
- route, wait_list = @waiting_lists.find do |route, _|
185
- (host == route[:host]) || (path =~ route[:path_regexp])
186
- end
187
- return wait_for_agent(wait_list) if route
185
+ # # search for a known route for an agent that recently unmounted
186
+ # route, wait_list = @waiting_lists.find do |route, _|
187
+ # (host == route[:host]) || (path =~ route[:path_regexp])
188
+ # end
189
+ # return wait_for_agent(wait_list) if route
188
190
 
189
191
  nil
190
192
  end
@@ -29,8 +29,10 @@ module Tipi
29
29
 
30
30
  # return [Boolean] true if client loop should stop
31
31
  def handle_incoming_data(data, &block)
32
+ rx = data.bytesize
32
33
  @parser << data
33
34
  while (request = @requests_head)
35
+ request.headers[':rx'] = rx
34
36
  if @first
35
37
  request.headers[':first'] = true
36
38
  @first = nil
@@ -53,10 +55,11 @@ module Tipi
53
55
 
54
56
  # Reads a body chunk for the current request. Transfers control to the parse
55
57
  # loop, and resumes once the parse_loop has fired the on_body callback
56
- def get_body_chunk
58
+ def get_body_chunk(request)
57
59
  @waiting_for_body_chunk = true
58
60
  @next_chunk = nil
59
61
  while !@requests_tail.complete? && (data = @conn.readpartial(8192))
62
+ request.rx_incr(data.bytesize)
60
63
  @parser << data
61
64
  return @next_chunk if @next_chunk
62
65
 
@@ -70,9 +73,10 @@ module Tipi
70
73
  # Waits for the current request to complete. Transfers control to the parse
71
74
  # loop, and resumes once the parse_loop has fired the on_message_complete
72
75
  # callback
73
- def consume_request
76
+ def consume_request(request)
74
77
  request = @requests_head
75
78
  @conn.recv_loop do |data|
79
+ request.rx_incr(data.bytesize)
76
80
  @parser << data
77
81
  return if request.complete?
78
82
  end
@@ -87,6 +91,9 @@ module Tipi
87
91
  headers = normalize_headers(headers)
88
92
  headers[':path'] = @parser.request_url
89
93
  headers[':method'] = @parser.http_method.downcase
94
+ scheme = (proto = headers['x-forwarded-proto']) ?
95
+ proto.downcase : scheme_from_connection
96
+ headers[':scheme'] = scheme
90
97
  queue_request(Qeweney::Request.new(headers, self))
91
98
  end
92
99
 
@@ -131,6 +138,14 @@ module Tipi
131
138
  # protocols, notably WebSocket, can be specified by passing a hash to the
132
139
  # :upgrade option when starting a server:
133
140
  #
141
+ # def ws_handler(conn)
142
+ # conn << 'hi'
143
+ # msg = conn.recv
144
+ # conn << "You said #{msg}"
145
+ # conn << 'bye'
146
+ # conn.close
147
+ # end
148
+ #
134
149
  # opts = {
135
150
  # upgrade: {
136
151
  # websocket: Tipi::Websocket.handler(&method(:ws_handler))
@@ -154,7 +169,7 @@ module Tipi
154
169
 
155
170
  def upgrade_with_handler(handler, headers)
156
171
  @parser = @requests_head = @requests_tail = nil
157
- handler.(@conn, headers)
172
+ handler.(self, headers)
158
173
  true
159
174
  end
160
175
 
@@ -173,6 +188,14 @@ module Tipi
173
188
  ':authority' => headers['host']
174
189
  )
175
190
  end
191
+
192
+ def websocket_connection(request)
193
+ Tipi::Websocket.new(@conn, request.headers)
194
+ end
195
+
196
+ def scheme_from_connection
197
+ @conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
198
+ end
176
199
 
177
200
  # response API
178
201
 
@@ -181,53 +204,55 @@ module Tipi
181
204
 
182
205
  # Sends response including headers and body. Waits for the request to complete
183
206
  # if not yet completed. The body is sent using chunked transfer encoding.
207
+ # @param request [Qeweney::Request] HTTP request
184
208
  # @param body [String] response body
185
209
  # @param headers
186
- def respond(body, headers)
187
- consume_request if @parsing
188
- data = format_headers(headers, body)
210
+ def respond(request, body, headers)
211
+ consume_request(request) if @parsing
212
+ formatted_headers = format_headers(headers, body, false)
213
+ request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
189
214
  if body
190
- if @parser.http_minor == 0
191
- data << body
192
- else
193
- data << body.bytesize.to_s(16) << CRLF << body << CRLF_ZERO_CRLF_CRLF
194
- end
215
+ @conn.write(formatted_headers, body)
216
+ else
217
+ @conn.write(formatted_headers)
195
218
  end
196
- @conn.write(data.join)
197
219
  end
198
220
 
199
- DEFAULT_HEADERS_OPTS = {
200
- empty_response: false,
201
- consume_request: true
202
- }.freeze
203
-
204
221
  # Sends response headers. If empty_response is truthy, the response status
205
222
  # code will default to 204, otherwise to 200.
223
+ # @param request [Qeweney::Request] HTTP request
206
224
  # @param headers [Hash] response headers
207
225
  # @param empty_response [boolean] whether a response body will be sent
226
+ # @param chunked [boolean] whether to use chunked transfer encoding
208
227
  # @return [void]
209
- def send_headers(headers, opts = DEFAULT_HEADERS_OPTS)
210
- data = format_headers(headers, true)
211
- @conn.write(data.join)
228
+ def send_headers(request, headers, empty_response: false, chunked: true)
229
+ formatted_headers = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
230
+ request.tx_incr(formatted_headers.bytesize)
231
+ @conn.write(formatted_headers)
212
232
  end
213
233
 
214
234
  # Sends a response body chunk. If no headers were sent, default headers are
215
235
  # sent using #send_headers. if the done option is true(thy), an empty chunk
216
236
  # will be sent to signal response completion to the client.
237
+ # @param request [Qeweney::Request] HTTP request
217
238
  # @param chunk [String] response body chunk
218
239
  # @param done [boolean] whether the response is completed
219
240
  # @return [void]
220
- def send_chunk(chunk, done: false)
221
- data = []
241
+ def send_chunk(request, chunk, done: false)
242
+ data = +''
222
243
  data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
223
244
  data << "0\r\n\r\n" if done
224
- @conn.write(data.join)
245
+ return if data.empty?
246
+
247
+ request.tx_incr(data.bytesize)
248
+ @conn.write(data)
225
249
  end
226
250
 
227
251
  # Finishes the response to the current request. If no headers were sent,
228
252
  # default headers are sent using #send_headers.
229
253
  # @return [void]
230
- def finish
254
+ def finish(request)
255
+ request.tx_incr(5)
231
256
  @conn << "0\r\n\r\n"
232
257
  end
233
258
 
@@ -237,17 +262,20 @@ module Tipi
237
262
 
238
263
  private
239
264
 
265
+ INTERNAL_HEADER_REGEXP = /^:/.freeze
266
+
240
267
  # Formats response headers into an array. If empty_response is true(thy),
241
268
  # the response status code will default to 204, otherwise to 200.
242
269
  # @param headers [Hash] response headers
243
- # @param empty_response [boolean] whether a response body will be sent
270
+ # @param body [boolean] whether a response body will be sent
271
+ # @param chunked [boolean] whether to use chunked transfer encoding
244
272
  # @return [String] formatted response headers
245
- def format_headers(headers, body)
273
+ def format_headers(headers, body, chunked)
246
274
  status = headers[':status']
247
275
  status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
248
- lines = [format_status_line(body, status)]
276
+ lines = format_status_line(body, status, chunked)
249
277
  headers.each do |k, v|
250
- next if k =~ /^:/
278
+ next if k =~ INTERNAL_HEADER_REGEXP
251
279
 
252
280
  collect_header_lines(lines, k, v)
253
281
  end
@@ -255,11 +283,11 @@ module Tipi
255
283
  lines
256
284
  end
257
285
 
258
- def format_status_line(body, status)
286
+ def format_status_line(body, status, chunked)
259
287
  if !body
260
288
  empty_status_line(status)
261
289
  else
262
- with_body_status_line(status, body)
290
+ with_body_status_line(status, body, chunked)
263
291
  end
264
292
  end
265
293
 
@@ -271,17 +299,17 @@ module Tipi
271
299
  end
272
300
  end
273
301
 
274
- def with_body_status_line(status, body)
275
- if @parser.http_minor == 0
276
- +"HTTP/1.0 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
277
- else
302
+ def with_body_status_line(status, body, chunked)
303
+ if chunked
278
304
  +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
305
+ else
306
+ +"HTTP/1.1 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
279
307
  end
280
308
  end
281
309
 
282
310
  def collect_header_lines(lines, key, value)
283
311
  if value.is_a?(Array)
284
- value.inject(lines) { |lines, item| data << "#{key}: #{item}\r\n" }
312
+ value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
285
313
  else
286
314
  lines << "#{key}: #{value}\r\n"
287
315
  end
@@ -16,7 +16,9 @@ module Tipi
16
16
  @opts = opts
17
17
  @upgrade_headers = upgrade_headers
18
18
  @first = true
19
-
19
+ @rx = (upgrade_headers && upgrade_headers[':rx']) || 0
20
+ @tx = (upgrade_headers && upgrade_headers[':tx']) || 0
21
+
20
22
  @interface = ::HTTP2::Server.new
21
23
  @connection_fiber = Fiber.current
22
24
  @interface.on(:frame, &method(:send_frame))
@@ -24,6 +26,9 @@ module Tipi
24
26
  end
25
27
 
26
28
  def send_frame(data)
29
+ if @transfer_count_request
30
+ @transfer_count_request.tx_incr(data.bytesize)
31
+ end
27
32
  @conn << data
28
33
  rescue Exception => e
29
34
  @connection_fiber.transfer e
@@ -38,6 +43,7 @@ module Tipi
38
43
 
39
44
  def upgrade
40
45
  @conn << UPGRADE_MESSAGE
46
+ @tx += UPGRADE_MESSAGE.bytesize
41
47
  settings = @upgrade_headers['http2-settings']
42
48
  Fiber.current.schedule(nil)
43
49
  @interface.upgrade(settings, @upgrade_headers, '')
@@ -49,16 +55,31 @@ module Tipi
49
55
  def each(&block)
50
56
  @interface.on(:stream) { |stream| start_stream(stream, &block) }
51
57
  upgrade if @upgrade_headers
52
-
53
- @conn.recv_loop(&@interface.method(:<<))
58
+
59
+ @conn.recv_loop do |data|
60
+ @rx += data.bytesize
61
+ @interface << data
62
+ end
54
63
  rescue SystemCallError, IOError
55
64
  # ignore
56
65
  ensure
57
66
  finalize_client_loop
58
67
  end
68
+
69
+ def get_rx_count
70
+ count = @rx
71
+ @rx = 0
72
+ count
73
+ end
74
+
75
+ def get_tx_count
76
+ count = @tx
77
+ @tx = 0
78
+ count
79
+ end
59
80
 
60
81
  def start_stream(stream, &block)
61
- stream = HTTP2StreamHandler.new(stream, @conn, @first, &block)
82
+ stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)
62
83
  @first = nil if @first
63
84
  @streams[stream] = true
64
85
  end
@@ -72,5 +93,15 @@ module Tipi
72
93
  def close
73
94
  @conn.close
74
95
  end
96
+
97
+ def set_request_for_transfer_count(request)
98
+ @transfer_count_request = request
99
+ end
100
+
101
+ def unset_request_for_transfer_count(request)
102
+ return unless @transfer_count_request == request
103
+
104
+ @transfer_count_request = nil
105
+ end
75
106
  end
76
107
  end
@@ -9,13 +9,15 @@ 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
18
  @stream_fiber = spin { |req| handle_request(req, &block) }
18
-
19
+ Thread.current.fiber_unschedule(@stream_fiber)
20
+
19
21
  # Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
20
22
  # The request handler is run on a separate fiber for each stream, allowing
21
23
  # concurrent handling of incoming requests on the same HTTP/2 connection.
@@ -47,14 +49,17 @@ module Tipi
47
49
 
48
50
  def on_headers(headers)
49
51
  @request = Qeweney::Request.new(headers.to_h, self)
52
+ @request.rx_incr(@adapter.get_rx_count)
53
+ @request.tx_incr(@adapter.get_tx_count)
50
54
  if @first
51
55
  @request.headers[':first'] = true
52
56
  @first = false
53
57
  end
54
58
  @stream_fiber.schedule @request
55
59
  end
56
-
60
+
57
61
  def on_data(data)
62
+ data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
58
63
  if @waiting_for_body_chunk
59
64
  @waiting_for_body_chunk = nil
60
65
  @stream_fiber.schedule data
@@ -62,7 +67,7 @@ module Tipi
62
67
  @request.buffer_body_chunk(data)
63
68
  end
64
69
  end
65
-
70
+
66
71
  def on_half_close
67
72
  if @waiting_for_body_chunk
68
73
  @waiting_for_body_chunk = nil
@@ -78,62 +83,100 @@ module Tipi
78
83
  def protocol
79
84
  'h2'
80
85
  end
86
+
87
+ def with_transfer_count(request)
88
+ @adapter.set_request_for_transfer_count(request)
89
+ yield
90
+ ensure
91
+ @adapter.unset_request_for_transfer_count(request)
92
+ end
81
93
 
82
- def get_body_chunk
94
+ def get_body_chunk(request)
83
95
  # called in the context of the stream fiber
84
96
  return nil if @request.complete?
85
97
 
86
- @waiting_for_body_chunk = true
87
- # the chunk (or an exception) will be returned once the stream fiber is
88
- # resumed
89
- suspend
98
+ with_transfer_count(request) do
99
+ @waiting_for_body_chunk = true
100
+ # the chunk (or an exception) will be returned once the stream fiber is
101
+ # resumed
102
+ suspend
103
+ end
90
104
  ensure
91
105
  @waiting_for_body_chunk = nil
92
106
  end
93
107
 
94
108
  # Wait for request to finish
95
- def consume_request
109
+ def consume_request(request)
96
110
  return if @request.complete?
97
111
 
98
- @waiting_for_half_close = true
99
- suspend
112
+ with_transfer_count(request) do
113
+ @waiting_for_half_close = true
114
+ suspend
115
+ end
100
116
  ensure
101
117
  @waiting_for_half_close = nil
102
118
  end
103
119
 
104
120
  # response API
105
- def respond(chunk, headers)
121
+ def respond(request, chunk, headers)
106
122
  headers[':status'] ||= Qeweney::Status::OK
107
- @stream.headers(headers, end_stream: false)
108
- @stream.data(chunk, end_stream: true)
123
+ headers[':status'] = headers[':status'].to_s
124
+ with_transfer_count(request) do
125
+ @stream.headers(transform_headers(headers))
126
+ @stream.data(chunk || '')
127
+ end
109
128
  @headers_sent = true
129
+ rescue HTTP2::Error::StreamClosed
130
+ # ignore
131
+ end
132
+
133
+ def transform_headers(headers)
134
+ headers.each_with_object([]) do |(k, v), a|
135
+ if v.is_a?(Array)
136
+ v.each { |vv| a << [k, vv.to_s] }
137
+ else
138
+ a << [k, v.to_s]
139
+ end
140
+ end
110
141
  end
111
142
 
112
- def send_headers(headers, empty_response = false)
143
+ def send_headers(request, headers, empty_response: false)
113
144
  return if @headers_sent
114
145
 
115
146
  headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
116
- @stream.headers(headers, end_stream: false)
147
+ with_transfer_count(request) do
148
+ @stream.headers(transform_headers(headers), end_stream: false)
149
+ end
117
150
  @headers_sent = true
151
+ rescue HTTP2::Error::StreamClosed
152
+ # ignore
118
153
  end
119
154
 
120
- def send_chunk(chunk, done: false)
155
+ def send_chunk(request, chunk, done: false)
121
156
  send_headers({}, false) unless @headers_sent
122
157
 
123
158
  if chunk
124
- @stream.data(chunk, end_stream: done)
159
+ with_transfer_count(request) do
160
+ @stream.data(chunk, end_stream: done)
161
+ end
125
162
  elsif done
126
163
  @stream.close
127
164
  end
165
+ rescue HTTP2::Error::StreamClosed
166
+ # ignore
128
167
  end
129
168
 
130
- def finish
169
+ def finish(request)
131
170
  if @headers_sent
132
171
  @stream.close
133
172
  else
134
173
  headers[':status'] ||= Qeweney::Status::NO_CONTENT
135
- @stream.headers(headers, end_stream: true)
174
+ with_transfer_count(request) do
175
+ @stream.headers(transform_headers(headers), end_stream: true)
176
+ end
136
177
  end
178
+ rescue HTTP2::Error::StreamClosed
179
+ # ignore
137
180
  end
138
181
 
139
182
  def stop