Class: Polyphony::HTTP::Server::HTTP1Adapter

Inherits:
Object
  • Object
show all
Defined in:
lib/polyphony/http/server/http1.rb

Overview

HTTP1 protocol implementation

Constant Summary collapse

DEFAULT_HEADERS_OPTS =
{
  empty_response:  false,
  consume_request: true
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(conn, opts) ⇒ HTTP1Adapter

Initializes a protocol adapter instance



13
14
15
16
17
# File 'lib/polyphony/http/server/http1.rb', line 13

def initialize(conn, opts)
  @conn = conn
  @opts = opts
  @parser = ::HTTP::Parser.new(self)
end

Instance Method Details

#closeObject



219
220
221
# File 'lib/polyphony/http/server/http1.rb', line 219

def close
  @conn.close
end

#consume_requestObject

Waits for the current request to complete. Transfers control to the parse loop, and resumes once the parse_loop has fired the on_message_complete callback



72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/polyphony/http/server/http1.rb', line 72

def consume_request
  request = @requests_head
  loop do
    data = @conn.readpartial(8192)
    @parser << data
    return if request.complete?

    snooze
  rescue EOFError
    break
  end
end

#each(&block) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/polyphony/http/server/http1.rb', line 19

def each(&block)
  loop do
    data = @conn.readpartial(8192)
    return if handle_incoming_data(data, &block)
  rescue EOFError
    break
  end
rescue SystemCallError, IOError
  # ignore
ensure
  finalize_client_loop
end

#finalize_client_loopObject



46
47
48
49
50
51
# File 'lib/polyphony/http/server/http1.rb', line 46

def finalize_client_loop
  # release references to various objects
  @requests_head = @requests_tail = nil
  @parser = nil
  @conn.close
end

#finishvoid

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.



215
216
217
# File 'lib/polyphony/http/server/http1.rb', line 215

def finish
  @conn << "0\r\n\r\n"
end

#get_body_chunkObject

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



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/polyphony/http/server/http1.rb', line 55

def get_body_chunk
  @waiting_for_body_chunk = true
  @next_chunk = nil
  while !@requests_tail.complete? && (data = @conn.readpartial(8192))
    @parser << data
    return @next_chunk if @next_chunk

    snooze
  end
  nil
ensure
  @waiting_for_body_chunk = nil
end

#handle_incoming_data(data, &block) ⇒ Object

return [Boolean] true if client loop should stop



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/polyphony/http/server/http1.rb', line 33

def handle_incoming_data(data, &block)
  @parser << data
  snooze
  while (request = @requests_head)
    return true if upgrade_connection(request.headers, &block)

    @requests_head = request.__next__
    block.call(request)
    return true unless request.keep_alive?
  end
  nil
end

#http2_upgraded_headers(headers) ⇒ Hash

Returns headers for HTTP2 upgrade

Parameters:

  • headers (Hash)

    request headers

Returns:

  • (Hash)

    headers for HTTP2 upgrade



160
161
162
163
164
165
# File 'lib/polyphony/http/server/http1.rb', line 160

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

#on_body(chunk) ⇒ Object



105
106
107
108
109
110
111
112
# File 'lib/polyphony/http/server/http1.rb', line 105

def on_body(chunk)
  if @waiting_for_body_chunk
    @next_chunk = chunk
    @waiting_for_body_chunk = nil
  else
    @requests_tail.buffer_body_chunk(chunk)
  end
end

#on_headers_complete(headers) ⇒ Object



90
91
92
93
94
# File 'lib/polyphony/http/server/http1.rb', line 90

def on_headers_complete(headers)
  headers[':path'] = @parser.request_url
  headers[':method'] = @parser.http_method
  queue_request(Request.new(headers, self))
end

#on_message_completeObject



114
115
116
117
# File 'lib/polyphony/http/server/http1.rb', line 114

def on_message_complete
  @waiting_for_body_chunk = nil
  @requests_tail.complete!(@parser.keep_alive?)
end

#protocolObject



85
86
87
88
# File 'lib/polyphony/http/server/http1.rb', line 85

def protocol
  version = @parser.http_version
  "HTTP #{version.join('.')}"
end

#queue_request(request) ⇒ Object



96
97
98
99
100
101
102
103
# File 'lib/polyphony/http/server/http1.rb', line 96

def queue_request(request)
  if @requests_head
    @requests_tail.__next__ = request
    @requests_tail = request
  else
    @requests_head = @requests_tail = request
  end
end

#respond(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:

  • body (String)

    response body

  • headers


173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/polyphony/http/server/http1.rb', line 173

def respond(body, headers)
  consume_request if @parsing
  data = format_headers(headers, body)
  if body
    data << if @parser.http_minor == 0
              body
            else
              "#{body.bytesize.to_s(16)}\r\n#{body}\r\n0\r\n\r\n"
            end
  end
  @conn << data
end

#send_chunk(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:

  • chunk (String)

    response body chunk

  • done (boolean) (defaults to: false)

    whether the response is completed



206
207
208
209
210
# File 'lib/polyphony/http/server/http1.rb', line 206

def send_chunk(chunk, done: false)
  data = +"#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
  data << "0\r\n\r\n" if done
  @conn << data
end

#send_headers(headers, opts = DEFAULT_HEADERS_OPTS) ⇒ 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:

  • headers (Hash)

    response headers

  • empty_response (boolean)

    whether a response body will be sent



196
197
198
# File 'lib/polyphony/http/server/http1.rb', line 196

def send_headers(headers, opts = DEFAULT_HEADERS_OPTS)
  @conn << format_headers(headers, !opts[:empty_response])
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:

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

Parameters:

  • headers (Hash)

    request headers

Returns:

  • (boolean)

    truthy if the connection has been upgraded



133
134
135
136
137
138
139
140
141
142
143
# File 'lib/polyphony/http/server/http1.rb', line 133

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



151
152
153
154
155
# File 'lib/polyphony/http/server/http1.rb', line 151

def upgrade_to_http2(headers, &block)
  @parser = @requests_head = @requests_tail = nil
  HTTP2Adapter.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
  true
end

#upgrade_with_handler(handler, headers) ⇒ Object



145
146
147
148
149
# File 'lib/polyphony/http/server/http1.rb', line 145

def upgrade_with_handler(handler, headers)
  @parser = @requests_head = @requests_tail = nil
  handler.(@conn, headers)
  true
end