Class: TP2::HTTP1Connection

Inherits:
Object
  • Object
show all
Defined in:
lib/tp2/http.rb

Overview

Encapsulates a HTTP/1 connection

Constant Summary collapse

SEND_FLAGS =

rubocop:disable Metrics/ClassLength

UM::MSG_NOSIGNAL | UM::MSG_WAITALL
CRLF =

response API

"\r\n"
ZERO_CRLF_CRLF =
"0\r\n\r\n"
CRLF_ZERO_CRLF_CRLF =
"\r\n0\r\n\r\n"
INTERNAL_HEADER_REGEXP =
/^:/.freeze

Instance Method Summary collapse

Constructor Details

#initialize(machine, fd, bgid, &app) ⇒ HTTP1Connection

rubocop:disable Naming/MethodParameterName



11
12
13
14
15
16
17
# File 'lib/tp2/http.rb', line 11

def initialize(machine, fd, bgid, &app) # rubocop:disable Naming/MethodParameterName
  @machine = machine
  @fd = fd
  @bgid = bgid
  @parser = Http::Parser.new(self)
  @app = app
end

Instance Method Details

#collect_header_lines(lines, key, value) ⇒ Object



180
181
182
183
184
185
186
# File 'lib/tp2/http.rb', line 180

def collect_header_lines(lines, key, value)
  if value.is_a?(Array)
    value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
  else
    lines << "#{key}: #{value}\r\n"
  end
end

#empty_status_line(status) ⇒ Object



164
165
166
167
168
169
170
# File 'lib/tp2/http.rb', line 164

def empty_status_line(status)
  if status == 204
    +"HTTP/1.1 #{status}\r\n"
  else
    +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
  end
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.



126
127
128
129
# File 'lib/tp2/http.rb', line 126

def finish(_request)
  # request.tx_incr(5)
  @machine.send(@fd, ZERO_CRLF_CRLF, ZERO_CRLF_CRLF.bytesize, SEND_FLAGS)
end

#format_headers(headers, body, chunked) ⇒ String

Formats response headers into an array. If empty_response is true(thy), the response status code will default to 204, otherwise to 200.

Parameters:

  • headers (Hash)

    response headers

  • body (boolean)

    whether a response body will be sent

  • chunked (boolean)

    whether to use chunked transfer encoding

Returns:

  • (String)

    formatted response headers



143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/tp2/http.rb', line 143

def format_headers(headers, body, chunked)
  status = headers[':status']
  status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
  lines = format_status_line(body, status, chunked)
  headers.each do |k, v|
    next if k =~ INTERNAL_HEADER_REGEXP

    collect_header_lines(lines, k, v)
  end
  lines << CRLF
  lines
end

#format_status_line(body, status, chunked) ⇒ Object



156
157
158
159
160
161
162
# File 'lib/tp2/http.rb', line 156

def format_status_line(body, status, chunked)
  if !body
    empty_status_line(status)
  else
    with_body_status_line(status, body, chunked)
  end
end

#get_scheme_from_headers(headers) ⇒ Object



28
29
30
31
32
33
34
# File 'lib/tp2/http.rb', line 28

def get_scheme_from_headers(headers)
  if (proto = headers['x-forwarded-proto'])
    proto.downcase
  else
    'http'
  end
end

#http1_1?(request) ⇒ Boolean

Returns:

  • (Boolean)


131
132
133
# File 'lib/tp2/http.rb', line 131

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

#normalize_headers(headers) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/tp2/http.rb', line 36

def normalize_headers(headers)
  headers.each_with_object({}) do |(k, v), h|
    k = k.downcase
    hk = h[k]
    if hk
      hk = h[k] = [hk] unless hk.is_a?(Array)
      v.is_a?(Array) ? hk.concat(v) : hk << v
    else
      h[k] = v
    end
  end
end

#on_body(chunk) ⇒ Object



49
50
51
# File 'lib/tp2/http.rb', line 49

def on_body(chunk)
  @request.buffer_body_chunk(chunk)
end

#on_headers_complete(headers) ⇒ Object

rubocop:disable Metrics/MethodLength



19
20
21
22
23
24
25
26
# File 'lib/tp2/http.rb', line 19

def on_headers_complete(headers) # rubocop:disable Metrics/MethodLength
  headers = normalize_headers(headers)
  headers[':path'] = @parser.request_url
  headers[':method'] = @parser.http_method.downcase
  headers[':scheme'] = get_scheme_from_headers(headers)
  headers[':protocol'] = "http/#{@parser.http_major}.#{@parser.http_minor}"
  @request = Qeweney::Request.new(headers, self)
end

#on_message_completeObject



53
54
55
56
# File 'lib/tp2/http.rb', line 53

def on_message_complete
  @done = @request.headers[':protocol'] != 'http/1.1'
  @app.call(@request)
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


80
81
82
83
84
85
86
87
88
89
90
# File 'lib/tp2/http.rb', line 80

def respond(_request, body, headers)
  formatted_headers = format_headers(headers, body, false)
  # request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
  if body
    buf = formatted_headers + body
    @machine.send(@fd, buf, buf.bytesize, SEND_FLAGS)
    # handle_write(formatted_headers + body)
  else
    @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
  end
end

#runObject



58
59
60
61
62
63
64
65
66
67
# File 'lib/tp2/http.rb', line 58

def run
  @machine.recv_each(@fd, @bgid, 0) do |buf|
    @parser << buf
    break if @done
  end
rescue SystemCallError, HTTP::Parser::Error
  # ignore error, just close the port silently
ensure
  @machine.close(@fd)
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



113
114
115
116
117
118
119
120
121
# File 'lib/tp2/http.rb', line 113

def send_chunk(_request, chunk, done: false)
  data = +''
  data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
  data << "0\r\n\r\n" if done
  return if data.empty?

  # request.tx_incr(data.bytesize)
  @machine.send(@fd, data, data.bytesize, SEND_FLAGS)
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



99
100
101
102
103
104
# File 'lib/tp2/http.rb', line 99

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)
  #
  @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
end

#with_body_status_line(status, body, chunked) ⇒ Object



172
173
174
175
176
177
178
# File 'lib/tp2/http.rb', line 172

def with_body_status_line(status, body, chunked)
  if chunked
    +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
  else
    +"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
  end
end