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
@@ -36,7 +36,7 @@ module DigitalFabric
36
36
  process_incoming_messages(false)
37
37
  rescue GracefulShutdown
38
38
  puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
39
- process_incoming_messages(true)
39
+ move_on_after(15) { process_incoming_messages(true) }
40
40
  ensure
41
41
  # keep_alive_timer&.stop
42
42
  unmount
@@ -56,11 +56,11 @@ module DigitalFabric
56
56
  def unmount
57
57
  return unless @mounted
58
58
 
59
- @service.unmount(self)
59
+ @service.unmount(self)
60
60
  @mounted = nil
61
61
  end
62
62
 
63
- def shutdown
63
+ def send_shutdown
64
64
  send_df_message(Protocol.shutdown)
65
65
  @fiber.raise GracefulShutdown.new
66
66
  end
@@ -144,13 +144,20 @@ module DigitalFabric
144
144
  t0 = Time.now
145
145
  t1 = nil
146
146
  with_request do |id|
147
- send_df_message(Protocol.http_request(id, req))
147
+ msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
148
+ send_df_message(msg)
148
149
  while (message = receive)
150
+ kind = message[Protocol::Attribute::KIND]
149
151
  unless t1
150
152
  t1 = Time.now
151
- @service.record_latency_measurement(t1 - t0, req)
153
+ if kind == Protocol::HTTP_RESPONSE
154
+ headers = message[Protocol::Attribute::HttpResponse::HEADERS]
155
+ status = (headers && headers[':status']) || 200
156
+ if status < Qeweney::Status::BAD_REQUEST
157
+ @service.record_latency_measurement(t1 - t0, req)
158
+ end
159
+ end
152
160
  end
153
- kind = message[Protocol::Attribute::KIND]
154
161
  attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
155
162
  return if http_request_message(id, req, kind, attributes)
156
163
  end
@@ -240,7 +247,7 @@ module DigitalFabric
240
247
  else
241
248
  req.send_headers(headers) if headers && !req.headers_sent?
242
249
  req.send_chunk(body, done: complete) if body or complete
243
-
250
+
244
251
  if complete && transfer_count_key
245
252
  rx, tx = req.transfer_counts
246
253
  send_transfer_count(transfer_count_key, rx, tx)
@@ -284,7 +291,7 @@ module DigitalFabric
284
291
  response = receive
285
292
  case response[0]
286
293
  when Protocol::WS_RESPONSE
287
- headers = response[2] || {}
294
+ headers = response[2] || {}
288
295
  status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
289
296
  if status != Qeweney::Status::SWITCHING_PROTOCOLS
290
297
  req.respond(nil, headers)
@@ -16,7 +16,7 @@ module DigitalFabric
16
16
  @service.mount(route, self)
17
17
  @current_request_count = 0
18
18
  # @updater = spin_loop(:executive_updater, interval: 10) { update_service_stats }
19
- # update_service_stats
19
+ update_service_stats
20
20
  end
21
21
 
22
22
  def current_request_count
@@ -33,9 +33,13 @@ module DigitalFabric
33
33
  req.respond(message.to_json, { 'Content-Type' => 'text.json' })
34
34
  when '/stream/stats'
35
35
  stream_stats(req)
36
+ when '/upload'
37
+ req.respond("body: #{req.read.inspect}")
36
38
  else
37
39
  req.respond('Invalid path', { ':status' => Qeweney::Status::NOT_FOUND })
38
40
  end
41
+ rescue => e
42
+ puts "Error: #{e.inspect}"
39
43
  ensure
40
44
  @current_request_count -= 1
41
45
  end
@@ -43,7 +47,7 @@ module DigitalFabric
43
47
  def stream_stats(req)
44
48
  req.send_headers({ 'Content-Type' => 'text/event-stream' })
45
49
 
46
- @service.timer.every(10) do
50
+ every(10) do
47
51
  message = last_service_stats
48
52
  req.send_chunk(format_sse_event(message.to_json))
49
53
  end
@@ -79,7 +83,7 @@ module DigitalFabric
79
83
  raise 'Invalid output from top (cpu)'
80
84
  end
81
85
  cpu_utilization = 100 - Regexp.last_match(1).to_i
82
-
86
+
83
87
  unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP
84
88
  raise 'Invalid output from top (mem)'
85
89
  end
@@ -102,8 +102,8 @@ module DigitalFabric
102
102
  DF_UPGRADE_RESPONSE
103
103
  end
104
104
 
105
- def http_request(id, req)
106
- [ HTTP_REQUEST, id, req.headers, req.next_chunk, req.complete? ]
105
+ def http_request(id, headers, buffered_chunk, complete)
106
+ [ HTTP_REQUEST, id, headers, buffered_chunk, complete ]
107
107
  end
108
108
 
109
109
  def http_response(id, body, headers, complete, transfer_count_key = nil)
@@ -141,7 +141,7 @@ module DigitalFabric
141
141
  def ws_data(id, data)
142
142
  [ WS_DATA, id, data ]
143
143
  end
144
-
144
+
145
145
  def ws_close(id)
146
146
  [ WS_CLOSE, id ]
147
147
  end
@@ -17,10 +17,6 @@ module DigitalFabric
17
17
  @agent.get_http_request_body(@id, 1)
18
18
  end
19
19
 
20
- def consume_request(request)
21
- @agent.get_http_request_body(@id, nil)
22
- end
23
-
24
20
  def respond(request, body, headers)
25
21
  @agent.send_df_message(
26
22
  Protocol.http_response(@id, body, headers, true)
@@ -25,7 +25,7 @@ module DigitalFabric
25
25
  @http_latency_max = 0
26
26
  @last_counters = @counters.merge(stamp: Time.now.to_f - 1)
27
27
  @fiber = Fiber.current
28
- @timer = Polyphony::Timer.new('service_timer', resolution: 5)
28
+ # @timer = Polyphony::Timer.new('service_timer', resolution: 5)
29
29
  end
30
30
 
31
31
  def calculate_stats
@@ -50,6 +50,8 @@ module DigitalFabric
50
50
  switch_rate = backend_stats[:switch_count] / elapsed
51
51
  poll_rate = backend_stats[:poll_count] / elapsed
52
52
 
53
+ object_space_stats = ObjectSpace.count_objects
54
+
53
55
  {
54
56
  service: {
55
57
  agent_count: @agents.size,
@@ -73,6 +75,8 @@ module DigitalFabric
73
75
  process: {
74
76
  cpu_usage: cpu,
75
77
  rss: rss.to_f / 1024,
78
+ objects_total: object_space_stats[:TOTAL],
79
+ objects_free: object_space_stats[:FREE]
76
80
  }
77
81
  }
78
82
  end
@@ -81,10 +85,12 @@ module DigitalFabric
81
85
  s = `ps -p #{pid} -o %cpu,rss`
82
86
  cpu, rss = s.lines[1].chomp.strip.split(' ')
83
87
  [cpu.to_f, rss.to_i]
88
+ rescue Polyphony::BaseException
89
+ raise
84
90
  rescue Exception
85
91
  [nil, nil]
86
92
  end
87
-
93
+
88
94
  def get_stats
89
95
  calculate_stats
90
96
  end
@@ -117,14 +123,14 @@ module DigitalFabric
117
123
 
118
124
  puts format('slow request (%.1f): %p', latency, req.headers)
119
125
  end
120
-
126
+
121
127
  def http_request(req, allow_df_upgrade = false)
122
128
  @current_request_count += 1
123
129
  @counters[:http_requests] += 1
124
130
  @counters[:connections] += 1 if req.headers[':first']
125
131
 
126
132
  return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol
127
-
133
+
128
134
  inject_request_headers(req)
129
135
  agent = find_agent(req)
130
136
  unless agent
@@ -153,7 +159,7 @@ module DigitalFabric
153
159
  req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
154
160
  req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
155
161
  end
156
-
162
+
157
163
  def upgrade_request(req, allow_df_upgrade)
158
164
  case (protocol = req.upgrade_protocol)
159
165
  when 'df'
@@ -172,7 +178,7 @@ module DigitalFabric
172
178
  agent.http_upgrade(req, protocol)
173
179
  end
174
180
  end
175
-
181
+
176
182
  def df_upgrade(req)
177
183
  # we don't want to count connected agents
178
184
  @current_request_count -= 1
@@ -185,7 +191,7 @@ module DigitalFabric
185
191
  ensure
186
192
  @current_request_count += 1
187
193
  end
188
-
194
+
189
195
  def mount(route, agent)
190
196
  if route[:path]
191
197
  route[:path_regexp] = path_regexp(route[:path])
@@ -194,7 +200,7 @@ module DigitalFabric
194
200
  @agents[agent] = route
195
201
  @routing_changed = true
196
202
  end
197
-
203
+
198
204
  def unmount(agent)
199
205
  route = @agents[agent]
200
206
  return unless route
@@ -205,7 +211,7 @@ module DigitalFabric
205
211
  end
206
212
 
207
213
  INVALID_HOST = 'INVALID_HOST'
208
-
214
+
209
215
  def find_agent(req)
210
216
  compile_agent_routes if @routing_changed
211
217
 
@@ -231,13 +237,6 @@ module DigitalFabric
231
237
  @route_keys = @routes.keys
232
238
  end
233
239
 
234
- def wait_for_agent(wait_list)
235
- wait_list << Fiber.current
236
- @timer.move_on_after(10) { suspend }
237
- ensure
238
- wait_list.delete(self)
239
- end
240
-
241
240
  def path_regexp(path)
242
241
  /^#{path}/
243
242
  end
@@ -245,8 +244,8 @@ module DigitalFabric
245
244
  def graceful_shutdown
246
245
  @shutdown = true
247
246
  @agents.keys.each do |agent|
248
- if agent.respond_to?(:shutdown)
249
- agent.shutdown
247
+ if agent.respond_to?(:send_shutdown)
248
+ agent.send_shutdown
250
249
  else
251
250
  @agents.delete(agent)
252
251
  end
data/lib/tipi/handler.rb CHANGED
@@ -20,14 +20,14 @@ module Tipi
20
20
  ensure
21
21
  socket.close
22
22
  end
23
-
23
+
24
24
  ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
25
25
  H2_PROTOCOL = 'h2'
26
26
 
27
27
  def protocol_adapter(socket, opts)
28
28
  use_http2 = socket.respond_to?(:alpn_protocol) &&
29
29
  socket.alpn_protocol == H2_PROTOCOL
30
-
30
+
31
31
  klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
32
32
  klass.new(socket, opts)
33
33
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'http/parser'
4
- require_relative './http2_adapter'
3
+ require 'h1p'
5
4
  require 'qeweney/request'
6
5
 
6
+ require_relative './http2_adapter'
7
+
7
8
  module Tipi
8
9
  # HTTP1 protocol implementation
9
10
  class HTTP1Adapter
@@ -14,127 +15,83 @@ module Tipi
14
15
  @conn = conn
15
16
  @opts = opts
16
17
  @first = true
17
- @parser = ::HTTP::Parser.new(self)
18
+ @parser = H1P::Parser.new(@conn)
18
19
  end
19
-
20
+
20
21
  def each(&block)
21
- @conn.recv_loop do |data|
22
- return if handle_incoming_data(data, &block)
22
+ while true
23
+ headers = @parser.parse_headers
24
+ break unless headers
25
+
26
+ # handle_request returns true if connection is not persistent or was
27
+ # upgraded
28
+ break if handle_request(headers, &block)
23
29
  end
30
+ rescue H1P::Error, ArgumentError
31
+ # an ArgumentError might be raised in the parser if an invalid input
32
+ # string is given as the HTTP method (String#upcase will raise on invalid HTTP string)
33
+ #
34
+ # ignore
24
35
  rescue SystemCallError, IOError
25
36
  # ignore
26
37
  ensure
27
38
  finalize_client_loop
28
39
  end
29
-
30
- # return [Boolean] true if client loop should stop
31
- def handle_incoming_data(data, &block)
32
- rx = data.bytesize
33
- @parser << data
34
- while (request = @requests_head)
35
- request.headers[':rx'] = rx
36
- if @first
37
- request.headers[':first'] = true
38
- @first = nil
39
- end
40
- return true if upgrade_connection(request.headers, &block)
41
-
42
- @requests_head = request.__next__
43
- block.call(request)
44
- return true unless request.keep_alive?
40
+
41
+ def handle_request(headers, &block)
42
+ scheme = (proto = headers['x-forwarded-proto']) ?
43
+ proto.downcase : scheme_from_connection
44
+ headers[':scheme'] = scheme
45
+ @protocol = headers[':protocol']
46
+ if @first
47
+ headers[':first'] = true
48
+ @first = nil
45
49
  end
46
- nil
50
+
51
+ return true if upgrade_connection(headers, &block)
52
+
53
+ request = Qeweney::Request.new(headers, self)
54
+ if !@parser.complete?
55
+ request.buffer_body_chunk(@parser.read_body_chunk(true))
56
+ end
57
+ block.call(request)
58
+ return !persistent_connection?(headers)
47
59
  end
48
-
60
+
61
+ def persistent_connection?(headers)
62
+ if headers[':protocol'] == 'http/1.1'
63
+ return headers['connection'] != 'close'
64
+ else
65
+ connection = headers['connection']
66
+ return connection && connection != 'close'
67
+ end
68
+ end
69
+
49
70
  def finalize_client_loop
50
- # release references to various objects
51
- @requests_head = @requests_tail = nil
52
71
  @parser = nil
53
72
  @splicing_pipe = nil
54
73
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
55
74
  @conn.close
56
75
  end
57
-
76
+
58
77
  # Reads a body chunk for the current request. Transfers control to the parse
59
78
  # loop, and resumes once the parse_loop has fired the on_body callback
60
- def get_body_chunk(request)
61
- @waiting_for_body_chunk = true
62
- @next_chunk = nil
63
- while !@requests_tail.complete? && (data = @conn.readpartial(8192))
64
- request.rx_incr(data.bytesize)
65
- @parser << data
66
- return @next_chunk if @next_chunk
67
-
68
- snooze
69
- end
70
- nil
71
- ensure
72
- @waiting_for_body_chunk = nil
73
- end
74
-
75
- # Waits for the current request to complete. Transfers control to the parse
76
- # loop, and resumes once the parse_loop has fired the on_message_complete
77
- # callback
78
- def consume_request(request)
79
- request = @requests_head
80
- @conn.recv_loop do |data|
81
- request.rx_incr(data.bytesize)
82
- @parser << data
83
- return if request.complete?
84
- end
85
- end
86
-
87
- def protocol
88
- version = @parser.http_version
89
- "HTTP #{version.join('.')}"
90
- end
91
-
92
- def on_headers_complete(headers)
93
- headers = normalize_headers(headers)
94
- headers[':path'] = @parser.request_url
95
- headers[':method'] = @parser.http_method.downcase
96
- scheme = (proto = headers['x-forwarded-proto']) ?
97
- proto.downcase : scheme_from_connection
98
- headers[':scheme'] = scheme
99
- queue_request(Qeweney::Request.new(headers, self))
79
+ def get_body_chunk(request, buffered_only = false)
80
+ @parser.read_body_chunk(buffered_only)
100
81
  end
101
82
 
102
- def normalize_headers(headers)
103
- headers.each_with_object({}) do |(k, v), h|
104
- k = k.downcase
105
- hk = h[k]
106
- if hk
107
- hk = h[k] = [hk] unless hk.is_a?(Array)
108
- v.is_a?(Array) ? hk.concat(v) : hk << v
109
- else
110
- h[k] = v
111
- end
112
- end
113
- end
114
-
115
- def queue_request(request)
116
- if @requests_head
117
- @requests_tail.__next__ = request
118
- @requests_tail = request
119
- else
120
- @requests_head = @requests_tail = request
121
- end
83
+ def get_body(request)
84
+ @parser.read_body
122
85
  end
123
-
124
- def on_body(chunk)
125
- if @waiting_for_body_chunk
126
- @next_chunk = chunk
127
- @waiting_for_body_chunk = nil
128
- else
129
- @requests_tail.buffer_body_chunk(chunk)
130
- end
86
+
87
+ def complete?(request)
88
+ @parser.complete?
131
89
  end
132
-
133
- def on_message_complete
134
- @waiting_for_body_chunk = nil
135
- @requests_tail.complete!(@parser.keep_alive?)
90
+
91
+ def protocol
92
+ @protocol
136
93
  end
137
-
94
+
138
95
  # Upgrades the connection to a different protocol, if the 'Upgrade' header is
139
96
  # given. By default the only supported upgrade protocol is HTTP2. Additional
140
97
  # protocols, notably WebSocket, can be specified by passing a hash to the
@@ -160,27 +117,28 @@ module Tipi
160
117
  def upgrade_connection(headers, &block)
161
118
  upgrade_protocol = headers['upgrade']
162
119
  return nil unless upgrade_protocol
163
-
120
+
164
121
  upgrade_protocol = upgrade_protocol.downcase.to_sym
165
122
  upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]
166
123
  return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler
167
124
  return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c
168
-
125
+
169
126
  nil
170
127
  end
171
-
128
+
172
129
  def upgrade_with_handler(handler, headers)
173
- @parser = @requests_head = @requests_tail = nil
130
+ @parser = nil
174
131
  handler.(self, headers)
175
132
  true
176
133
  end
177
-
134
+
178
135
  def upgrade_to_http2(headers, &block)
179
- @parser = @requests_head = @requests_tail = nil
180
- HTTP2Adapter.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
136
+ headers = http2_upgraded_headers(headers)
137
+ body = @parser.read_body
138
+ HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)
181
139
  true
182
140
  end
183
-
141
+
184
142
  # Returns headers for HTTP2 upgrade
185
143
  # @param headers [Hash] request headers
186
144
  # @return [Hash] headers for HTTP2 upgrade
@@ -198,10 +156,10 @@ module Tipi
198
156
  def scheme_from_connection
199
157
  @conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
200
158
  end
201
-
159
+
202
160
  # response API
203
161
 
204
- CRLF = "\r\n"
162
+ CRLF = "\r\n"
205
163
  CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
206
164
 
207
165
  # Sends response including headers and body. Waits for the request to complete
@@ -210,7 +168,6 @@ module Tipi
210
168
  # @param body [String] response body
211
169
  # @param headers
212
170
  def respond(request, body, headers)
213
- consume_request(request) if @parsing
214
171
  formatted_headers = format_headers(headers, body, false)
215
172
  request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
216
173
  if body
@@ -220,19 +177,19 @@ module Tipi
220
177
  end
221
178
  end
222
179
 
223
- def respond_from_io(request, io, headers, chunk_size = 2**14)
224
- consume_request(request) if @parsing
180
+ CHUNK_LENGTH_PROC = ->(len) { "#{len.to_s(16)}\r\n" }
225
181
 
182
+ def respond_from_io(request, io, headers, chunk_size = 2**14)
226
183
  formatted_headers = format_headers(headers, true, true)
227
184
  request.tx_incr(formatted_headers.bytesize)
228
-
185
+
229
186
  # assume chunked encoding
230
187
  Thread.current.backend.splice_chunks(
231
188
  io,
232
189
  @conn,
233
190
  formatted_headers,
234
191
  "0\r\n\r\n",
235
- ->(len) { "#{len.to_s(16)}\r\n" },
192
+ CHUNK_LENGTH_PROC,
236
193
  "\r\n",
237
194
  chunk_size
238
195
  )
@@ -246,11 +203,15 @@ module Tipi
246
203
  # @param chunked [boolean] whether to use chunked transfer encoding
247
204
  # @return [void]
248
205
  def send_headers(request, headers, empty_response: false, chunked: true)
249
- formatted_headers = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
206
+ formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
250
207
  request.tx_incr(formatted_headers.bytesize)
251
208
  @conn.write(formatted_headers)
252
209
  end
253
-
210
+
211
+ def http1_1?(request)
212
+ request.headers[':protocol'] == 'http/1.1'
213
+ end
214
+
254
215
  # Sends a response body chunk. If no headers were sent, default headers are
255
216
  # sent using #send_headers. if the done option is true(thy), an empty chunk
256
217
  # will be sent to signal response completion to the client.
@@ -267,7 +228,7 @@ module Tipi
267
228
  request.tx_incr(data.bytesize)
268
229
  @conn.write(data)
269
230
  end
270
-
231
+
271
232
  def send_chunk_from_io(request, io, r, w, chunk_size)
272
233
  len = w.splice(io, chunk_size)
273
234
  if len > 0
@@ -289,12 +250,12 @@ module Tipi
289
250
  request.tx_incr(5)
290
251
  @conn << "0\r\n\r\n"
291
252
  end
292
-
253
+
293
254
  def close
294
255
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
295
256
  @conn.close
296
257
  end
297
-
258
+
298
259
  private
299
260
 
300
261
  INTERNAL_HEADER_REGEXP = /^:/.freeze
@@ -311,13 +272,13 @@ module Tipi
311
272
  lines = format_status_line(body, status, chunked)
312
273
  headers.each do |k, v|
313
274
  next if k =~ INTERNAL_HEADER_REGEXP
314
-
275
+
315
276
  collect_header_lines(lines, k, v)
316
277
  end
317
278
  lines << CRLF
318
279
  lines
319
280
  end
320
-
281
+
321
282
  def format_status_line(body, status, chunked)
322
283
  if !body
323
284
  empty_status_line(status)
@@ -325,7 +286,7 @@ module Tipi
325
286
  with_body_status_line(status, body, chunked)
326
287
  end
327
288
  end
328
-
289
+
329
290
  def empty_status_line(status)
330
291
  if status == 204
331
292
  +"HTTP/1.1 #{status}\r\n"
@@ -333,7 +294,7 @@ module Tipi
333
294
  +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
334
295
  end
335
296
  end
336
-
297
+
337
298
  def with_body_status_line(status, body, chunked)
338
299
  if chunked
339
300
  +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"