httpkit 0.6.0.pre.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|