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.
Files changed (59) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +15 -0
  4. data/.yardopts +2 -0
  5. data/Gemfile +19 -0
  6. data/Gemfile.devtools +67 -0
  7. data/Procfile +1 -0
  8. data/README.md +66 -0
  9. data/Rakefile +5 -0
  10. data/UNLICENSE +24 -0
  11. data/config/devtools.yml +2 -0
  12. data/config/flay.yml +3 -0
  13. data/config/flog.yml +2 -0
  14. data/config/mutant.yml +3 -0
  15. data/config/reek.yml +114 -0
  16. data/config/rubocop.yml +56 -0
  17. data/config/yardstick.yml +2 -0
  18. data/examples/echo_server.rb +36 -0
  19. data/examples/getting_started.rb +34 -0
  20. data/httpkit.gemspec +27 -0
  21. data/lib/httpkit/body.rb +67 -0
  22. data/lib/httpkit/client/keep_alive.rb +10 -0
  23. data/lib/httpkit/client/timeouts.rb +14 -0
  24. data/lib/httpkit/client.rb +94 -0
  25. data/lib/httpkit/connection/eventmachine.rb +72 -0
  26. data/lib/httpkit/connection/status.rb +28 -0
  27. data/lib/httpkit/promise.rb +19 -0
  28. data/lib/httpkit/request.rb +19 -0
  29. data/lib/httpkit/response.rb +110 -0
  30. data/lib/httpkit/serializer/encoding.rb +43 -0
  31. data/lib/httpkit/serializer.rb +75 -0
  32. data/lib/httpkit/server/keep_alive.rb +58 -0
  33. data/lib/httpkit/server/timeouts.rb +13 -0
  34. data/lib/httpkit/server.rb +62 -0
  35. data/lib/httpkit/support/handler_manager.rb +25 -0
  36. data/lib/httpkit/support/message.rb +66 -0
  37. data/lib/httpkit/version.rb +5 -0
  38. data/lib/httpkit.rb +49 -0
  39. data/spec/integration/error_handling_spec.rb +76 -0
  40. data/spec/integration/keep_alive_spec.rb +101 -0
  41. data/spec/integration/smoke_spec.rb +21 -0
  42. data/spec/integration/streaming_spec.rb +57 -0
  43. data/spec/integration/timeouts_spec.rb +82 -0
  44. data/spec/shared/integration/server_client_pair.rb +29 -0
  45. data/spec/spec_helper.rb +45 -0
  46. data/spec/support/handler.rb +48 -0
  47. data/spec/support/helper.rb +70 -0
  48. data/spec/unit/client_spec.rb +230 -0
  49. data/spec/unit/connection/eventmachine_spec.rb +211 -0
  50. data/spec/unit/connection/status_spec.rb +83 -0
  51. data/spec/unit/httpkit_spec.rb +41 -0
  52. data/spec/unit/promise_spec.rb +56 -0
  53. data/spec/unit/request_spec.rb +35 -0
  54. data/spec/unit/response_spec.rb +108 -0
  55. data/spec/unit/server/keep_alive_spec.rb +69 -0
  56. data/spec/unit/server_spec.rb +128 -0
  57. data/spec/unit/support/handler_manager_spec.rb +21 -0
  58. data/spec/unit/support/message_spec.rb +115 -0
  59. 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
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module HTTPkit
4
+ VERSION = '0.6.0.pre.3'
5
+ end