Class: TP2::Connection

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

Overview

Implements an HTTP/1.1 connection received by the TP2 server. This implementation rejects incoming HTTP/0.9 or HTTP/1.0 requests. The response body is sent exclusively using chunked transfer encoding. Request bodies are accepted using either fixed length (Content-Length header) or chunked transfer encoding.

Constant Summary collapse

SEND_FLAGS =
UM::MSG_NOSIGNAL | UM::MSG_WAITALL
EMPTY_CHUNK =
"0\r\n\r\n"
EMPTY_CHUNK_LEN =
EMPTY_CHUNK.bytesize

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(server, machine, fd, env, &app) ⇒ Connection

Returns a new instance of Connection.



16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/tp2/connection.rb', line 16

def initialize(server, machine, fd, env, &app)
  @server = server
  @machine = machine
  @fd = fd
  @env = env
  @logger = env[:logger]
  @stream = UM::Stream.new(machine, fd)
  @app = app

  @done = nil
  @response_headers = nil
end

Instance Attribute Details

#fdObject (readonly)

Returns the value of attribute fd.



14
15
16
# File 'lib/tp2/connection.rb', line 14

def fd
  @fd
end

#loggerObject (readonly)

Returns the value of attribute logger.



14
15
16
# File 'lib/tp2/connection.rb', line 14

def logger
  @logger
end

#response_headersObject (readonly)

Returns the value of attribute response_headers.



14
15
16
# File 'lib/tp2/connection.rb', line 14

def response_headers
  @response_headers
end

Instance Method Details

#closeObject



258
259
260
# File 'lib/tp2/connection.rb', line 258

def close
  @machine.close_async(@fd)
end

#complete?(req) ⇒ Boolean

Returns:

  • (Boolean)


129
130
131
# File 'lib/tp2/connection.rb', line 129

def complete?(req)
  req.headers[':body-done-reading']
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.



217
218
219
220
221
222
223
224
# File 'lib/tp2/connection.rb', line 217

def finish(request)
  request.tx_incr(EMPTY_CHUNK_LEN)
  @machine.send(@fd, EMPTY_CHUNK, EMPTY_CHUNK_LEN, SEND_FLAGS)
  return if @done

  @logger&.info(request, request, response_headers: @response_headers)
  @done = true
end

#get_body(req) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/tp2/connection.rb', line 95

def get_body(req)
  headers = req.headers
  content_length = headers['content-length']
  return read(content_length.to_i) if content_length

  chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
  return get_body_chunked_encoding(headers) if chunked_encoding

  # if content-length is not specified, we read to EOF, up to max 1MB size
  read(1 << 20, nil, false)
end

#get_body_chunk(req, _buffered_only = false) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/tp2/connection.rb', line 107

def get_body_chunk(req, _buffered_only = false)
  headers = req.headers
  content_length = headers['content-length']
  if content_length
    return nil if headers[':body-done-reading']

    chunk = read(content_length.to_i)
    headers[':body-done-reading'] = true
    return chunk
  end

  chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
  return read_chunk(headers, nil) if chunked_encoding

  return nil if headers[':body-done-reading']

  # if content-length is not specified, we read to EOF, up to max 1MB size
  chunk = read(1 << 20, nil, false)
  headers[':body-done-reading'] = true
  chunk
end

#handle_error(request, err) ⇒ void

This method returns an undefined value.

Handles an error encountered while serving a request by logging the error and optionally sending an error response with the relevant HTTP status code. For I/O errors, no response is sent.

Parameters:



71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/tp2/connection.rb', line 71

def handle_error(request, err)
  case err
  when SystemCallError
    log_error(err, 'I/O error')
  when ProtocolError
    log_error(err, err.message)
    respond(request, err.message, ':status' => err.http_status)
  else
    log_error(err, 'Internal error')
    return if !request || @done

    respond(request, 'Internal server error', ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
  end
end

#log_error(err, message) ⇒ void

This method returns an undefined value.

Logs the given err and given message.

Parameters:

  • err (Exception)

    error

  • message (String)

    error message



91
92
93
# File 'lib/tp2/connection.rb', line 91

def log_error(err, message)
  @logger&.error(message: "#{message}, closing connection", error: err)
end

#monotonic_clockObject



262
263
264
# File 'lib/tp2/connection.rb', line 262

def monotonic_clock
  ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
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


161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/tp2/connection.rb', line 161

def respond(request, body, headers)
  headers = @response_headers.merge(headers) if @response_headers

  formatted_headers = format_headers(headers, body)
  @response_headers = headers
  request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
  if body
    buf = "#{formatted_headers}#{body.bytesize.to_s(16)}\r\n#{body}\r\n#{EMPTY_CHUNK}"
    @machine.send(@fd, buf, buf.bytesize, SEND_FLAGS)
  else
    @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
  end
  @logger&.info(request: request, response_headers: headers) if request
  @done = true
end

#respond_with_static_file(req, path, env, cache_headers) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/tp2/connection.rb', line 226

def respond_with_static_file(req, path, env, cache_headers)
  fd = @machine.open(path, UM::O_RDONLY)
  env ||= {}
  if env[:headers]
    env[:headers].merge!(cache_headers)
  else
    env[:headers] = cache_headers
  end

  maxlen = env[:max_len] || 65_536
  buf = String.new(capacity: maxlen)
  headers_sent = nil
  loop do
    res = @machine.read(fd, buf, maxlen, 0)
    if res < maxlen && !headers_sent
      return respond(req, buf, env[:headers])
    elsif res == 0
      return finish(req)
    end

    if !headers_sent
      send_headers(req, env[:headers])
      headers_sent = true
    end
    done = res < maxlen
    send_chunk(req, buf, done: done)
    return if done
  end
ensure
  @machine.close_async(fd) if fd
end

#runObject



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/tp2/connection.rb', line 29

def run
  loop do
    @done = nil
    @response_headers = nil
    persist = serve_request
    break if !persist
  end
rescue UM::Terminate
  # server is terminated, do nothing
rescue StandardError => e
  @logger&.error(
    message:  'Uncaught error while running connection',
    error:    e
  )
ensure
  @machine.close_async(@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



200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/tp2/connection.rb', line 200

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

  request.tx_incr(data.bytesize)
  @machine.send(@fd, data, data.bytesize, SEND_FLAGS)
  return if @done || !done

  @logger&.info(request: request, response_headers: @response_headers)
  @done = true
end

#send_headers(request, headers, empty_response: false) ⇒ 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



183
184
185
186
187
188
# File 'lib/tp2/connection.rb', line 183

def send_headers(request, headers, empty_response: false)
  formatted_headers = format_headers(headers, !empty_response)
  request.tx_incr(formatted_headers.bytesize)
  @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
  @response_headers = headers
end

#serve_requestObject

Processes an incoming request by parsing the headers, creating a request object and handing it off to the app handler. Returns true if the connection should be persisted.



50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/tp2/connection.rb', line 50

def serve_request
  headers = parse_headers
  return false if !headers

  request = Qeweney::Request.new(headers, self)

  request.start_stamp = monotonic_clock
  @app.call(request)
  persist_connection?(headers)
rescue StandardError => e
  handle_error(request, e)
  false
end


145
146
147
148
149
150
151
152
# File 'lib/tp2/connection.rb', line 145

def set_cookie(*cookies)
  existing_cookies = @response_headers && @response_headers['Set-Cookie']
  if existing_cookies
    @response_headers['Set-Cookie'] = existing_cookies + cookies
  else
    set_response_headers('Set-Cookie' => cookies)
  end
end

#set_response_headers(headers) ⇒ void

This method returns an undefined value.

Sets response headers before sending any response. This method is used to add headers such as Set-Cookie or cache control headers to a response before actually responding, specifically in middleware hooks.

Parameters:

  • headers (Hash)

    response headers



141
142
143
# File 'lib/tp2/connection.rb', line 141

def set_response_headers(headers)
  @response_headers ? @response_headers.merge!(headers) : @response_headers = headers
end