httpkit 0.6.0.pre.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/.travis.yml +15 -0
- data/.yardopts +2 -0
- data/Gemfile +19 -0
- data/Gemfile.devtools +67 -0
- data/Procfile +1 -0
- data/README.md +66 -0
- data/Rakefile +5 -0
- data/UNLICENSE +24 -0
- data/config/devtools.yml +2 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +114 -0
- data/config/rubocop.yml +56 -0
- data/config/yardstick.yml +2 -0
- data/examples/echo_server.rb +36 -0
- data/examples/getting_started.rb +34 -0
- data/httpkit.gemspec +27 -0
- data/lib/httpkit/body.rb +67 -0
- data/lib/httpkit/client/keep_alive.rb +10 -0
- data/lib/httpkit/client/timeouts.rb +14 -0
- data/lib/httpkit/client.rb +94 -0
- data/lib/httpkit/connection/eventmachine.rb +72 -0
- data/lib/httpkit/connection/status.rb +28 -0
- data/lib/httpkit/promise.rb +19 -0
- data/lib/httpkit/request.rb +19 -0
- data/lib/httpkit/response.rb +110 -0
- data/lib/httpkit/serializer/encoding.rb +43 -0
- data/lib/httpkit/serializer.rb +75 -0
- data/lib/httpkit/server/keep_alive.rb +58 -0
- data/lib/httpkit/server/timeouts.rb +13 -0
- data/lib/httpkit/server.rb +62 -0
- data/lib/httpkit/support/handler_manager.rb +25 -0
- data/lib/httpkit/support/message.rb +66 -0
- data/lib/httpkit/version.rb +5 -0
- data/lib/httpkit.rb +49 -0
- data/spec/integration/error_handling_spec.rb +76 -0
- data/spec/integration/keep_alive_spec.rb +101 -0
- data/spec/integration/smoke_spec.rb +21 -0
- data/spec/integration/streaming_spec.rb +57 -0
- data/spec/integration/timeouts_spec.rb +82 -0
- data/spec/shared/integration/server_client_pair.rb +29 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/support/handler.rb +48 -0
- data/spec/support/helper.rb +70 -0
- data/spec/unit/client_spec.rb +230 -0
- data/spec/unit/connection/eventmachine_spec.rb +211 -0
- data/spec/unit/connection/status_spec.rb +83 -0
- data/spec/unit/httpkit_spec.rb +41 -0
- data/spec/unit/promise_spec.rb +56 -0
- data/spec/unit/request_spec.rb +35 -0
- data/spec/unit/response_spec.rb +108 -0
- data/spec/unit/server/keep_alive_spec.rb +69 -0
- data/spec/unit/server_spec.rb +128 -0
- data/spec/unit/support/handler_manager_spec.rb +21 -0
- data/spec/unit/support/message_spec.rb +115 -0
- metadata +190 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
class Client
|
5
|
+
USER_AGENT = 'User-Agent'.freeze
|
6
|
+
USER_AGENT_VALUE = "httpkit/#{HTTPkit::VERSION}".freeze
|
7
|
+
HOST = 'Host'.freeze
|
8
|
+
HOST_VALUE = '%s:%d'.freeze
|
9
|
+
|
10
|
+
include Support::HandlerManager::Setup
|
11
|
+
include Connection::Status
|
12
|
+
|
13
|
+
def self.start(config)
|
14
|
+
Connection::EventMachine.start_client(config, self)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :config
|
18
|
+
|
19
|
+
def initialize(config, connection)
|
20
|
+
@config = config
|
21
|
+
@requests = []
|
22
|
+
|
23
|
+
setup_connection(connection)
|
24
|
+
setup_handlers
|
25
|
+
end
|
26
|
+
|
27
|
+
def request(*args)
|
28
|
+
request = Request.new(*args)
|
29
|
+
perform(request).sync
|
30
|
+
end
|
31
|
+
|
32
|
+
def perform(request)
|
33
|
+
served = Promise.new
|
34
|
+
|
35
|
+
if closed?
|
36
|
+
served.reject(@connection.closed.reason)
|
37
|
+
else
|
38
|
+
@requests << [request, served]
|
39
|
+
perform!(request)
|
40
|
+
end
|
41
|
+
|
42
|
+
served
|
43
|
+
end
|
44
|
+
|
45
|
+
def receive(response)
|
46
|
+
request, served = find_request
|
47
|
+
|
48
|
+
if request
|
49
|
+
served.fulfill(response)
|
50
|
+
@handlers.notify(:receive, request, response)
|
51
|
+
response.closed { finish(request) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def finish(request)
|
56
|
+
@requests.delete_if { |(req)| req == request }
|
57
|
+
@handlers.notify(:finish, request)
|
58
|
+
end
|
59
|
+
|
60
|
+
def teardown(reason)
|
61
|
+
@requests.each do |_, served|
|
62
|
+
served.reject(reason)
|
63
|
+
if (response = served.value)
|
64
|
+
response.reject_closed(reason)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def add_extra_headers(request)
|
72
|
+
host = sprintf(HOST_VALUE, config[:address], config[:port])
|
73
|
+
request.add_extra_headers(USER_AGENT => USER_AGENT_VALUE, HOST => host)
|
74
|
+
end
|
75
|
+
|
76
|
+
def perform!(request)
|
77
|
+
Fiber.new do
|
78
|
+
add_extra_headers(request)
|
79
|
+
@handlers.notify(:perform, request)
|
80
|
+
@connection.serialize(request)
|
81
|
+
end.resume
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_request
|
85
|
+
@requests.detect { |_, served| served.pending? }
|
86
|
+
end
|
87
|
+
|
88
|
+
def setup_connection(connection)
|
89
|
+
@connection = connection
|
90
|
+
@connection.on_message = method(:receive)
|
91
|
+
@connection.closed.then(method(:teardown), method(:teardown))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
module Connection
|
5
|
+
class EventMachine < EM::Connection
|
6
|
+
def self.start_client(config, client_class)
|
7
|
+
connection = EM.connect(config[:address], config[:port], self)
|
8
|
+
client_class.new(config, connection)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.start_server(config, server_class)
|
12
|
+
EM.start_server(config[:address], config[:port], self,
|
13
|
+
proc { |conn| server_class.new(config, conn) })
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :closed
|
17
|
+
attr_writer :on_message
|
18
|
+
|
19
|
+
def initialize(callback = proc {})
|
20
|
+
@closed = Promise.new
|
21
|
+
@parser = HTTP::Parser.new(self)
|
22
|
+
callback.call(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def serialize(message)
|
26
|
+
serializer = Serializer.new(message, method(:send_data))
|
27
|
+
serializer.setup_body
|
28
|
+
serializer.serialize
|
29
|
+
end
|
30
|
+
|
31
|
+
def receive_data(data)
|
32
|
+
# p [__id__, :receive, data]
|
33
|
+
# p data
|
34
|
+
@parser << data
|
35
|
+
rescue => ex
|
36
|
+
close(ex)
|
37
|
+
end
|
38
|
+
|
39
|
+
# def send_data(data)
|
40
|
+
# # p [__id__, :send, data]
|
41
|
+
# p data
|
42
|
+
# super
|
43
|
+
# end
|
44
|
+
|
45
|
+
def close(reason = nil)
|
46
|
+
closed.reject(reason) if reason
|
47
|
+
close_connection_after_writing
|
48
|
+
end
|
49
|
+
|
50
|
+
def unbind(reason = nil)
|
51
|
+
if reason
|
52
|
+
closed.reject(reason)
|
53
|
+
else
|
54
|
+
closed.fulfill
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_headers_complete(_)
|
59
|
+
@message = Support::Message.build(@parser)
|
60
|
+
@on_message.call(@message)
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_body(chunk)
|
64
|
+
@message.body.closed.progress(chunk)
|
65
|
+
end
|
66
|
+
|
67
|
+
def on_message_complete
|
68
|
+
@message.close
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
module Connection
|
5
|
+
module Status
|
6
|
+
def close(reason = nil)
|
7
|
+
@connection.close(reason)
|
8
|
+
end
|
9
|
+
|
10
|
+
def closed?
|
11
|
+
!@connection.closed.pending?
|
12
|
+
end
|
13
|
+
|
14
|
+
def error?
|
15
|
+
@connection.closed.rejected?
|
16
|
+
end
|
17
|
+
|
18
|
+
def network_fault?
|
19
|
+
[Errno::ENOTCONN, Errno::ENETUNREACH]
|
20
|
+
.include?(@connection.closed.reason)
|
21
|
+
end
|
22
|
+
|
23
|
+
def timeout?
|
24
|
+
@connection.closed.reason == Errno::ETIMEDOUT
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
class Promise < ::Promise
|
5
|
+
def wait
|
6
|
+
fiber = Fiber.current
|
7
|
+
resume = proc { fiber.resume }
|
8
|
+
self.then(resume, resume)
|
9
|
+
|
10
|
+
Fiber.yield
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def defer
|
16
|
+
EM.next_tick { yield } if EM.reactor_running?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
Request = Struct.new(:http_method, :uri, :headers, :body, :http_version)
|
5
|
+
|
6
|
+
class Request
|
7
|
+
private :http_method=, :uri=, :headers=, :body=
|
8
|
+
|
9
|
+
include Support::Message
|
10
|
+
|
11
|
+
# TODO: URI.join is really slow
|
12
|
+
def initialize(http_method, uri, headers = {}, body = '')
|
13
|
+
super(http_method,
|
14
|
+
# URI('http://' + uri)
|
15
|
+
URI.join('http:///', uri).request_uri,
|
16
|
+
headers, Body.build(body), 1.1)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
Response = Struct.new(:status, :headers, :body, :http_version)
|
5
|
+
|
6
|
+
class Response
|
7
|
+
private :status=, :headers=, :body=
|
8
|
+
|
9
|
+
include Support::Message
|
10
|
+
|
11
|
+
def initialize(status, headers = {}, body = '')
|
12
|
+
super(status, headers, Body.build(body), 1.1)
|
13
|
+
end
|
14
|
+
|
15
|
+
def status_name
|
16
|
+
STATUS_NAMES[status.to_i] || UNKNOWN_STATUS
|
17
|
+
end
|
18
|
+
|
19
|
+
def status_class
|
20
|
+
status / 100
|
21
|
+
end
|
22
|
+
|
23
|
+
def informational?
|
24
|
+
status_class == 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def successful?
|
28
|
+
status_class == 2
|
29
|
+
end
|
30
|
+
|
31
|
+
def redirection?
|
32
|
+
status_class == 3
|
33
|
+
end
|
34
|
+
|
35
|
+
def client_error?
|
36
|
+
status_class == 4
|
37
|
+
end
|
38
|
+
|
39
|
+
def server_error?
|
40
|
+
status_class == 5
|
41
|
+
end
|
42
|
+
|
43
|
+
UNKNOWN_STATUS = 'Unknown Status'.freeze
|
44
|
+
|
45
|
+
STATUS_NAMES =
|
46
|
+
{ 100 => 'Continue'.freeze,
|
47
|
+
101 => 'Switching Protocols'.freeze,
|
48
|
+
102 => 'Processing'.freeze,
|
49
|
+
200 => 'OK'.freeze,
|
50
|
+
201 => 'Created'.freeze,
|
51
|
+
202 => 'Accepted'.freeze,
|
52
|
+
203 => 'Non-Authoritative Information'.freeze,
|
53
|
+
204 => 'No Content'.freeze,
|
54
|
+
205 => 'Reset Content'.freeze,
|
55
|
+
206 => 'Partial Content'.freeze,
|
56
|
+
207 => 'Multi-Status'.freeze,
|
57
|
+
208 => 'Already Reported'.freeze,
|
58
|
+
226 => 'IM Used'.freeze,
|
59
|
+
300 => 'Multiple Choices'.freeze,
|
60
|
+
301 => 'Moved Permanently'.freeze,
|
61
|
+
302 => 'Found'.freeze,
|
62
|
+
303 => 'See Other'.freeze,
|
63
|
+
304 => 'Not Modified'.freeze,
|
64
|
+
305 => 'Use Proxy'.freeze,
|
65
|
+
306 => 'Reserved'.freeze,
|
66
|
+
307 => 'Temporary Redirect'.freeze,
|
67
|
+
308 => 'Permanent Redirect'.freeze,
|
68
|
+
400 => 'Bad Request'.freeze,
|
69
|
+
401 => 'Unauthorized'.freeze,
|
70
|
+
402 => 'Payment Required'.freeze,
|
71
|
+
403 => 'Forbidden'.freeze,
|
72
|
+
404 => 'Not Found'.freeze,
|
73
|
+
405 => 'Method Not Allowed'.freeze,
|
74
|
+
406 => 'Not Acceptable'.freeze,
|
75
|
+
407 => 'Proxy Authentication Required'.freeze,
|
76
|
+
408 => 'Request Timeout'.freeze,
|
77
|
+
409 => 'Conflict'.freeze,
|
78
|
+
410 => 'Gone'.freeze,
|
79
|
+
411 => 'Length Required'.freeze,
|
80
|
+
412 => 'Precondition Failed'.freeze,
|
81
|
+
413 => 'Request Entity Too Large'.freeze,
|
82
|
+
414 => 'Request-URI Too Long'.freeze,
|
83
|
+
415 => 'Unsupported Media Type'.freeze,
|
84
|
+
416 => 'Requested Range Not Satisfiable'.freeze,
|
85
|
+
417 => 'Expectation Failed'.freeze,
|
86
|
+
422 => 'Unprocessable Entity'.freeze,
|
87
|
+
423 => 'Locked'.freeze,
|
88
|
+
424 => 'Failed Dependency'.freeze,
|
89
|
+
425 =>
|
90
|
+
'Reserved for WebDAV advanced collections expired proposal'.freeze,
|
91
|
+
426 => 'Upgrade Required'.freeze,
|
92
|
+
427 => 'Unassigned'.freeze,
|
93
|
+
428 => 'Precondition Required'.freeze,
|
94
|
+
429 => 'Too Many Requests'.freeze,
|
95
|
+
430 => 'Unassigned'.freeze,
|
96
|
+
431 => 'Request Header Fields Too Large'.freeze,
|
97
|
+
500 => 'Internal Server Error'.freeze,
|
98
|
+
501 => 'Not Implemented'.freeze,
|
99
|
+
502 => 'Bad Gateway'.freeze,
|
100
|
+
503 => 'Service Unavailable'.freeze,
|
101
|
+
504 => 'Gateway Timeout'.freeze,
|
102
|
+
505 => 'HTTP Version Not Supported'.freeze,
|
103
|
+
506 => 'Variant Also Negotiates (Experimental)'.freeze,
|
104
|
+
507 => 'Insufficient Storage'.freeze,
|
105
|
+
508 => 'Loop Detected'.freeze,
|
106
|
+
509 => 'Unassigned'.freeze,
|
107
|
+
510 => 'Not Extended'.freeze,
|
108
|
+
511 => 'Network Authentication Required'.freeze }
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
class Serializer
|
5
|
+
module Encoding
|
6
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
7
|
+
CHUNKED = 'chunked'.freeze
|
8
|
+
CRLF = "\r\n".freeze
|
9
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
10
|
+
|
11
|
+
def setup_chunked_encoding
|
12
|
+
headers.delete(CONTENT_LENGTH)
|
13
|
+
headers[TRANSFER_ENCODING] = CHUNKED
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_chunked_streaming
|
17
|
+
body.closed.on_progress { |chunk| write_chunk(chunk) }
|
18
|
+
body.closed.then { |_| write_chunk!('') }
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup_identity_encoding
|
22
|
+
headers.delete(TRANSFER_ENCODING)
|
23
|
+
headers[CONTENT_LENGTH] = body.to_s.bytesize
|
24
|
+
end
|
25
|
+
|
26
|
+
def write_body
|
27
|
+
if headers.key?(TRANSFER_ENCODING)
|
28
|
+
write_chunk(body.string)
|
29
|
+
else
|
30
|
+
write(body.to_s)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_chunk(chunk)
|
35
|
+
write_chunk!(chunk) unless chunk.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def write_chunk!(chunk)
|
39
|
+
write(chunk.bytesize.to_s(16), CRLF, chunk, CRLF)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module HTTPkit
|
6
|
+
class Serializer
|
7
|
+
REQUEST_LINE = "%s %s HTTP/%.1f\r\n".freeze
|
8
|
+
RESPONSE_LINE = "HTTP/%.1f %d %s\r\n".freeze
|
9
|
+
HEADER = "%s: %s\r\n".freeze
|
10
|
+
CRLF = "\r\n".freeze
|
11
|
+
|
12
|
+
include Encoding
|
13
|
+
|
14
|
+
extend Forwardable
|
15
|
+
def_delegator :@message, :headers
|
16
|
+
def_delegator :@message, :body
|
17
|
+
|
18
|
+
def initialize(message, writer)
|
19
|
+
@message = message
|
20
|
+
@writer = writer
|
21
|
+
end
|
22
|
+
|
23
|
+
def serialize
|
24
|
+
write(banana_line)
|
25
|
+
write(header_block)
|
26
|
+
|
27
|
+
write_body
|
28
|
+
end
|
29
|
+
|
30
|
+
def setup_body
|
31
|
+
if body.closed.pending?
|
32
|
+
setup_chunked_encoding
|
33
|
+
setup_chunked_streaming
|
34
|
+
else
|
35
|
+
setup_identity_encoding
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def write(*data)
|
42
|
+
@writer.call(data.join(''))
|
43
|
+
end
|
44
|
+
|
45
|
+
def banana_line
|
46
|
+
if Request === @message
|
47
|
+
request_line
|
48
|
+
else
|
49
|
+
response_line
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def request_line
|
54
|
+
sprintf(REQUEST_LINE,
|
55
|
+
@message.http_method.upcase, @message.uri, @message.http_version)
|
56
|
+
end
|
57
|
+
|
58
|
+
def response_line
|
59
|
+
sprintf(RESPONSE_LINE,
|
60
|
+
@message.http_version, @message.status, @message.status_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def header_block
|
64
|
+
headers.reduce('') { |a, e| a << header_line(*e) } + CRLF
|
65
|
+
end
|
66
|
+
|
67
|
+
def header_line(key, value)
|
68
|
+
if value.to_s.empty?
|
69
|
+
''
|
70
|
+
else
|
71
|
+
sprintf(HEADER, key, value)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
class Server::KeepAlive
|
5
|
+
CONNECTION = 'Connection'.freeze
|
6
|
+
CLOSE = 'close'.freeze
|
7
|
+
KEEP_ALIVE = 'keep-alive'.freeze
|
8
|
+
|
9
|
+
def setup(_, server, _)
|
10
|
+
@server = server
|
11
|
+
@requests, @previous = {}, nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def serve(request, served)
|
15
|
+
@requests[request] = [served, @previous]
|
16
|
+
@previous = request
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond(request, response)
|
20
|
+
synchronize_responses(request)
|
21
|
+
|
22
|
+
if close_connection?(request)
|
23
|
+
response.headers[CONNECTION] = CLOSE
|
24
|
+
# XXX: possible race condition with other locations waiting for this
|
25
|
+
response.closed { @server.close }
|
26
|
+
else
|
27
|
+
response.headers[CONNECTION] = KEEP_ALIVE
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def finish(request)
|
32
|
+
@requests.delete(request)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def synchronize_responses(request)
|
38
|
+
_, previous = @requests[request]
|
39
|
+
served, _ = @requests[previous]
|
40
|
+
|
41
|
+
served.sync.closed! if served
|
42
|
+
end
|
43
|
+
|
44
|
+
def connection_header(request)
|
45
|
+
response = @requests[request][0].value
|
46
|
+
request.headers[CONNECTION] || response.headers[CONNECTION]
|
47
|
+
end
|
48
|
+
|
49
|
+
def close_connection?(request)
|
50
|
+
header = connection_header(request).to_s.downcase
|
51
|
+
header == CLOSE || http10_close?(request.http_version, header)
|
52
|
+
end
|
53
|
+
|
54
|
+
def http10_close?(version, header)
|
55
|
+
version < 1.1 && header != KEEP_ALIVE
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
# @see EM.heartbeat_interval
|
5
|
+
class Server::Timeouts
|
6
|
+
def setup(config, _, connection)
|
7
|
+
@config = config
|
8
|
+
@connection = connection
|
9
|
+
|
10
|
+
@connection.comm_inactivity_timeout = @config[:timeout] ||= 2.0
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
class Server
|
5
|
+
SERVER = 'Server'.freeze
|
6
|
+
SERVER_VALUE = "httpkit/#{HTTPkit::VERSION}".freeze
|
7
|
+
DATE = 'Date'.freeze
|
8
|
+
|
9
|
+
def self.start(config)
|
10
|
+
Connection::EventMachine.start_server(config, self)
|
11
|
+
end
|
12
|
+
|
13
|
+
include Support::HandlerManager::Setup
|
14
|
+
include Connection::Status
|
15
|
+
|
16
|
+
attr_reader :config
|
17
|
+
|
18
|
+
def initialize(config, connection)
|
19
|
+
@config = config
|
20
|
+
|
21
|
+
setup_connection(connection)
|
22
|
+
setup_handlers
|
23
|
+
end
|
24
|
+
|
25
|
+
def serve(request)
|
26
|
+
served = Promise.new
|
27
|
+
served.then { |response| respond(request, response) }
|
28
|
+
|
29
|
+
Fiber.new { @handlers.notify(:serve, request, served) }.resume
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond(request, response)
|
33
|
+
fiber = Fiber.new do
|
34
|
+
add_extra_headers(response)
|
35
|
+
respond!(request, response)
|
36
|
+
response.closed { finish(request) }
|
37
|
+
end
|
38
|
+
fiber.resume
|
39
|
+
end
|
40
|
+
|
41
|
+
def finish(request)
|
42
|
+
@handlers.notify(:finish, request)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def add_extra_headers(response)
|
48
|
+
date = Time.now.httpdate
|
49
|
+
response.add_extra_headers(SERVER => SERVER_VALUE, DATE => date)
|
50
|
+
end
|
51
|
+
|
52
|
+
def respond!(request, response)
|
53
|
+
@handlers.notify(:respond, request, response)
|
54
|
+
@connection.serialize(response)
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_connection(connection)
|
58
|
+
@connection = connection
|
59
|
+
@connection.on_message = method(:serve)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
module Support
|
5
|
+
class HandlerManager
|
6
|
+
def initialize(handlers)
|
7
|
+
@handlers = handlers
|
8
|
+
end
|
9
|
+
|
10
|
+
def notify(message, *args)
|
11
|
+
@handlers.each do |handler|
|
12
|
+
handler.public_send(message, *args) if handler.respond_to?(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# XXX not mutation covered
|
17
|
+
module Setup
|
18
|
+
def setup_handlers
|
19
|
+
@handlers = Support::HandlerManager.new(@config[:handlers] || [])
|
20
|
+
@handlers.notify(:setup, @config, self, @connection)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module HTTPkit
|
4
|
+
module Support
|
5
|
+
module Message
|
6
|
+
def closed?
|
7
|
+
!body.closed.pending?
|
8
|
+
end
|
9
|
+
|
10
|
+
def closed!
|
11
|
+
body.closed.sync
|
12
|
+
end
|
13
|
+
|
14
|
+
def closed(&block)
|
15
|
+
body.closed.then(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def close
|
19
|
+
body.closed.fulfill
|
20
|
+
end
|
21
|
+
|
22
|
+
def reject_closed(reason)
|
23
|
+
body.closed.reject(reason)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_extra_headers(extra)
|
27
|
+
extra.each { |k, v| headers.key?(k) || headers[k] = v }
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.build(parser)
|
31
|
+
if parser.http_method
|
32
|
+
build_request(parser)
|
33
|
+
else
|
34
|
+
build_response(parser)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def self.build_request(parser)
|
41
|
+
request = Request.new(http_method_from(parser),
|
42
|
+
parser.request_url,
|
43
|
+
parser.headers,
|
44
|
+
Body.new)
|
45
|
+
request.http_version = http_version_from(parser)
|
46
|
+
request
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.build_response(parser)
|
50
|
+
response = Response.new(parser.status_code,
|
51
|
+
parser.headers,
|
52
|
+
Body.new)
|
53
|
+
response.http_version = http_version_from(parser)
|
54
|
+
response
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.http_method_from(parser)
|
58
|
+
parser.http_method.downcase.to_sym
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.http_version_from(parser)
|
62
|
+
parser.http_version.join('.').to_f
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|