Class: Tipi::HTTP1Adapter

Inherits:
Object
  • Object
show all
Defined in:
lib/tipi/http1_adapter.rb

Overview

HTTP1 protocol implementation

Direct Known Subclasses

StockHTTP1Adapter

Constant Summary collapse

CRLF =

response API

"\r\n"
CRLF_ZERO_CRLF_CRLF =
"\r\n0\r\n\r\n"
CHUNK_LENGTH_PROC =
->(len) { "#{len.to_s(16)}\r\n" }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(conn, opts) ⇒ HTTP1Adapter

Initializes a protocol adapter instance



14
15
16
17
18
19
# File 'lib/tipi/http1_adapter.rb', line 14

def initialize(conn, opts)
  @conn = conn
  @opts = opts
  @first = true
  @parser = H1P::Parser.new(@conn, :server)
end

Instance Attribute Details

#connObject (readonly)

Returns the value of attribute conn.



11
12
13
# File 'lib/tipi/http1_adapter.rb', line 11

def conn
  @conn
end

Instance Method Details

#closeObject



259
260
261
262
# File 'lib/tipi/http1_adapter.rb', line 259

def close
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
  @conn.close
end

#complete?(request) ⇒ Boolean



87
88
89
# File 'lib/tipi/http1_adapter.rb', line 87

def complete?(request)
  @parser.complete?
end

#each(&block) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/tipi/http1_adapter.rb', line 21

def each(&block)
  while true
    headers = @parser.parse_headers
    break unless headers

    # handle_request returns true if connection is not persistent or was
    # upgraded
    break if handle_request(headers, &block)
  end
rescue H1P::Error, ArgumentError
  # an ArgumentError might be raised in the parser if an invalid input
  # string is given as the HTTP method (String#upcase will raise on invalid HTTP string)
  #
  # ignore
rescue SystemCallError, IOError
  # ignore
ensure
  finalize_client_loop
end

#finalize_client_loopObject



70
71
72
73
74
75
# File 'lib/tipi/http1_adapter.rb', line 70

def finalize_client_loop
  @parser = nil
  @splicing_pipe = nil
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
  @conn.close
end

#finish(request) ⇒ void

This method returns an undefined value.

Finishes the response to the current request. If no headers were sent, default headers are sent using #send_headers.



254
255
256
257
# File 'lib/tipi/http1_adapter.rb', line 254

def finish(request)
  request.tx_incr(5)
  @conn << "0\r\n\r\n"
end

#get_body(request) ⇒ Object



83
84
85
# File 'lib/tipi/http1_adapter.rb', line 83

def get_body(request)
  @parser.read_body
end

#get_body_chunk(request, buffered_only = false) ⇒ Object

Reads a body chunk for the current request. Transfers control to the parse loop, and resumes once the parse_loop has fired the on_body callback



79
80
81
# File 'lib/tipi/http1_adapter.rb', line 79

def get_body_chunk(request, buffered_only = false)
  @parser.read_body_chunk(buffered_only)
end

#handle_request(headers, &block) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/tipi/http1_adapter.rb', line 41

def handle_request(headers, &block)
  scheme = (proto = headers['x-forwarded-proto']) ?
            proto.downcase : scheme_from_connection
  headers[':scheme'] = scheme
  @protocol = headers[':protocol']
  if @first
    headers[':first'] = true
    @first = nil
  end

  return true if upgrade_connection(headers, &block)

  request = Qeweney::Request.new(headers, self)
  if !@parser.complete?
    request.buffer_body_chunk(@parser.read_body_chunk(true))
  end
  block.call(request)
  return !persistent_connection?(headers)
end

#http1_1?(request) ⇒ Boolean



211
212
213
# File 'lib/tipi/http1_adapter.rb', line 211

def http1_1?(request)
  request.headers[':protocol'] == 'http/1.1'
end

#http2_upgraded_headers(headers) ⇒ Hash

Returns headers for HTTP2 upgrade



145
146
147
148
149
150
# File 'lib/tipi/http1_adapter.rb', line 145

def http2_upgraded_headers(headers)
  headers.merge(
    ':scheme'    => 'http',
    ':authority' => headers['host']
  )
end

#persistent_connection?(headers) ⇒ Boolean



61
62
63
64
65
66
67
68
# File 'lib/tipi/http1_adapter.rb', line 61

def persistent_connection?(headers)
  if headers[':protocol'] == 'http/1.1'
    return headers['connection'] != 'close'
  else
    connection = headers['connection']
    return connection && connection != 'close'
  end
end

#protocolObject



91
92
93
# File 'lib/tipi/http1_adapter.rb', line 91

def protocol
  @protocol
end

#respond(request, body, headers) ⇒ Object

Sends response including headers and body. Waits for the request to complete if not yet completed. The body is sent using chunked transfer encoding.



170
171
172
173
174
175
176
177
178
# File 'lib/tipi/http1_adapter.rb', line 170

def respond(request, body, headers)
  formatted_headers = format_headers(headers, body, false)
  request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
  if body
    @conn.write(formatted_headers, body)
  else
    @conn.write(formatted_headers)
  end
end

#respond_from_io(request, io, headers, chunk_size = 2**14) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/tipi/http1_adapter.rb', line 182

def respond_from_io(request, io, headers, chunk_size = 2**14)
  formatted_headers = format_headers(headers, true, true)
  request.tx_incr(formatted_headers.bytesize)

  # assume chunked encoding
  Thread.current.backend.splice_chunks(
    io,
    @conn,
    formatted_headers,
    "0\r\n\r\n",
    CHUNK_LENGTH_PROC,
    "\r\n",
    chunk_size
  )
end

#scheme_from_connectionObject



156
157
158
# File 'lib/tipi/http1_adapter.rb', line 156

def scheme_from_connection
  @conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
end

#send_chunk(request, chunk, done: false) ⇒ void

This method returns an undefined value.

Sends a response body chunk. If no headers were sent, default headers are sent using #send_headers. if the done option is true(thy), an empty chunk will be sent to signal response completion to the client.



222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/tipi/http1_adapter.rb', line 222

def send_chunk(request, chunk, done: false)
  if done
    data = chunk ?
      "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n0\r\n\r\n" :
      "0\r\n\r\n"
  elsif chunk
    data = "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
  else
    return
  end

  request.tx_incr(data.bytesize)
  @conn.write(data)
end

#send_chunk_from_io(request, io, r, w, chunk_size) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/tipi/http1_adapter.rb', line 237

def send_chunk_from_io(request, io, r, w, chunk_size)
  len = w.splice(io, chunk_size)
  if len > 0
    Thread.current.backend.chain(
      [:write, @conn, "#{len.to_s(16)}\r\n"],
      [:splice, r, @conn, len],
      [:write, @conn, "\r\n"]
    )
  else
    @conn.write("0\r\n\r\n")
  end
  len
end

#send_headers(request, headers, empty_response: false, chunked: true) ⇒ void

This method returns an undefined value.

Sends response headers. If empty_response is truthy, the response status code will default to 204, otherwise to 200.



205
206
207
208
209
# File 'lib/tipi/http1_adapter.rb', line 205

def send_headers(request, headers, empty_response: false, chunked: true)
  formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
  request.tx_incr(formatted_headers.bytesize)
  @conn.write(formatted_headers)
end

#upgrade_connection(headers, &block) ⇒ boolean

Upgrades the connection to a different protocol, if the ‘Upgrade’ header is given. By default the only supported upgrade protocol is HTTP2. Additional protocols, notably WebSocket, can be specified by passing a hash to the :upgrade option when starting a server:

def ws_handler(conn)
  conn << 'hi'
  msg = conn.recv
  conn << "You said #{msg}"
  conn << 'bye'
  conn.close
end

opts = {
  upgrade: {
    websocket: Tipi::Websocket.handler(&method(:ws_handler))
  }
}
Tipi.serve('0.0.0.0', 1234, opts) { |req| ... }


117
118
119
120
121
122
123
124
125
126
127
# File 'lib/tipi/http1_adapter.rb', line 117

def upgrade_connection(headers, &block)
  upgrade_protocol = headers['upgrade']
  return nil unless upgrade_protocol

  upgrade_protocol = upgrade_protocol.downcase.to_sym
  upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]
  return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler
  return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c

  nil
end

#upgrade_to_http2(headers, &block) ⇒ Object



135
136
137
138
139
140
# File 'lib/tipi/http1_adapter.rb', line 135

def upgrade_to_http2(headers, &block)
  headers = http2_upgraded_headers(headers)
  body = @parser.read_body
  HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)
  true
end

#upgrade_with_handler(handler, headers) ⇒ Object



129
130
131
132
133
# File 'lib/tipi/http1_adapter.rb', line 129

def upgrade_with_handler(handler, headers)
  @parser = nil
  handler.(self, headers)
  true
end

#websocket_connection(request) ⇒ Object



152
153
154
# File 'lib/tipi/http1_adapter.rb', line 152

def websocket_connection(request)
  Tipi::Websocket.new(@conn, request.headers)
end