httpkit 0.6.0.pre.3

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