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"
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



248
249
250
251
# File 'lib/tipi/http1_adapter.rb', line 248

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

#complete?(request) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#each(&block) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 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 SystemCallError, IOError, H1P::Error
  # connection or parser error, ignore
ensure
  finalize_client_loop
end

#finalize_client_loopObject



65
66
67
68
69
70
# File 'lib/tipi/http1_adapter.rb', line 65

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.



243
244
245
246
# File 'lib/tipi/http1_adapter.rb', line 243

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

#get_body(request) ⇒ Object



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

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



74
75
76
# File 'lib/tipi/http1_adapter.rb', line 74

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

#handle_request(headers, &block) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/tipi/http1_adapter.rb', line 36

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

Returns:

  • (Boolean)


200
201
202
# File 'lib/tipi/http1_adapter.rb', line 200

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

#http2_upgraded_headers(headers) ⇒ Hash

Returns headers for HTTP2 upgrade

Parameters:

  • headers (Hash)

    request headers

Returns:

  • (Hash)

    headers for HTTP2 upgrade



140
141
142
143
144
145
# File 'lib/tipi/http1_adapter.rb', line 140

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

#persistent_connection?(headers) ⇒ Boolean

Returns:

  • (Boolean)


56
57
58
59
60
61
62
63
# File 'lib/tipi/http1_adapter.rb', line 56

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



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

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.

Parameters:

  • request (Qeweney::Request)

    HTTP request

  • body (String)

    response body

  • headers


164
165
166
167
# File 'lib/tipi/http1_adapter.rb', line 164

def respond(request, body, headers)
  written = H1P.send_response(@conn, headers, body)
  request.tx_incr(written)
end

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



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/tipi/http1_adapter.rb', line 171

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



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

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.

Parameters:

  • request (Qeweney::Request)

    HTTP request

  • chunk (String)

    response body chunk

  • done (boolean) (defaults to: false)

    whether the response is completed



211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/tipi/http1_adapter.rb', line 211

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



226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/tipi/http1_adapter.rb', line 226

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.

Parameters:

  • request (Qeweney::Request)

    HTTP request

  • headers (Hash)

    response headers

  • empty_response (boolean) (defaults to: false)

    whether a response body will be sent

  • chunked (boolean) (defaults to: true)

    whether to use chunked transfer encoding



194
195
196
197
198
# File 'lib/tipi/http1_adapter.rb', line 194

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| ... }

Parameters:

  • headers (Hash)

    request headers

Returns:

  • (boolean)

    truthy if the connection has been upgraded



112
113
114
115
116
117
118
119
120
121
122
# File 'lib/tipi/http1_adapter.rb', line 112

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



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

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



124
125
126
127
128
# File 'lib/tipi/http1_adapter.rb', line 124

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

#websocket_connection(request) ⇒ Object



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

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