hatetepe 0.3.1 → 0.4.0
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/README.md +21 -16
- data/hatetepe.gemspec +1 -2
- data/lib/hatetepe/body.rb +5 -3
- data/lib/hatetepe/builder.rb +6 -3
- data/lib/hatetepe/cli.rb +27 -5
- data/lib/hatetepe/client.rb +157 -82
- data/lib/hatetepe/client/keep_alive.rb +58 -0
- data/lib/hatetepe/client/pipeline.rb +19 -0
- data/lib/hatetepe/connection.rb +42 -0
- data/lib/hatetepe/deferred_status_fix.rb +11 -0
- data/lib/hatetepe/message.rb +4 -4
- data/lib/hatetepe/parser.rb +3 -4
- data/lib/hatetepe/request.rb +19 -6
- data/lib/hatetepe/response.rb +11 -3
- data/lib/hatetepe/server.rb +115 -85
- data/lib/hatetepe/{app.rb → server/app.rb} +7 -2
- data/lib/hatetepe/server/keep_alive.rb +61 -0
- data/lib/hatetepe/server/pipeline.rb +24 -0
- data/lib/hatetepe/{proxy.rb → server/proxy.rb} +5 -11
- data/lib/hatetepe/version.rb +1 -1
- data/lib/rack/handler/hatetepe.rb +1 -4
- data/spec/integration/cli/start_spec.rb +75 -123
- data/spec/integration/client/keep_alive_spec.rb +74 -0
- data/spec/integration/server/keep_alive_spec.rb +99 -0
- data/spec/spec_helper.rb +41 -16
- data/spec/unit/app_spec.rb +16 -5
- data/spec/unit/builder_spec.rb +4 -4
- data/spec/unit/client/pipeline_spec.rb +40 -0
- data/spec/unit/client_spec.rb +355 -199
- data/spec/unit/connection_spec.rb +64 -0
- data/spec/unit/parser_spec.rb +3 -2
- data/spec/unit/proxy_spec.rb +9 -18
- data/spec/unit/rack_handler_spec.rb +2 -12
- data/spec/unit/server_spec.rb +154 -60
- metadata +31 -36
- data/.rspec +0 -1
- data/.travis.yml +0 -3
- data/.yardopts +0 -1
- data/lib/hatetepe/pipeline.rb +0 -27
data/README.md
CHANGED
@@ -24,11 +24,13 @@ Using Hatetepe as your HTTP server is easy. Simply use the CLI that ships with
|
|
24
24
|
the gem:
|
25
25
|
|
26
26
|
$ hatetepe
|
27
|
+
We're in development
|
27
28
|
Booting from /home/lars/workspace/hatetepe/config.ru
|
28
29
|
Binding to 127.0.0.1:3000
|
29
30
|
|
30
31
|
You can configure the network port and interface as well as the Rackup (.ru)
|
31
|
-
file to be used. More help is available via the
|
32
|
+
file to be used and the RACK_ENV to run in. More help is available via the
|
33
|
+
`hatetepe help` command.
|
32
34
|
|
33
35
|
|
34
36
|
Getting Started (Client)
|
@@ -37,7 +39,7 @@ Getting Started (Client)
|
|
37
39
|
The `Hatetepe::Client` class can be used to make requests to an HTTP server.
|
38
40
|
|
39
41
|
client = Hatetepe::Client.start(:host => "example.org", :port => 80)
|
40
|
-
request = Hatetepe::Request.new(
|
42
|
+
request = Hatetepe::Request.new(:post, "/search", {}, :q => "herp derp")
|
41
43
|
client << request
|
42
44
|
request.callback do |response|
|
43
45
|
puts "Results:"
|
@@ -56,7 +58,7 @@ The `Hatetepe::Client` class can be used to make requests to an HTTP server.
|
|
56
58
|
- `#headers`
|
57
59
|
- `#body`
|
58
60
|
|
59
|
-
`Request` also has `#
|
61
|
+
`Request` also has `#to_h` which will turn the object into something your
|
60
62
|
app can respond to.
|
61
63
|
|
62
64
|
|
@@ -133,8 +135,8 @@ Sending and Receiving BLOBs
|
|
133
135
|
---------------------------
|
134
136
|
|
135
137
|
Hatetepe provides a thin wrapper around StringIO that makes it easier to handle
|
136
|
-
streaming of request and response bodies. That means your app will be `#call`ed
|
137
|
-
soon as all headers have arrived. It can then do stuff while it's still
|
138
|
+
streaming of request and response bodies. That means your app will be `#call`ed
|
139
|
+
as soon as all headers have arrived. It can then do stuff while it's still
|
138
140
|
receiving body data. You might for example want to track upload progress.
|
139
141
|
|
140
142
|
received = nil
|
@@ -177,13 +179,21 @@ License
|
|
177
179
|
Hatetepe is subject to an MIT-style license (see LICENSE file).
|
178
180
|
|
179
181
|
|
180
|
-
|
181
|
-
|
182
|
+
Roadmap
|
183
|
+
-------
|
184
|
+
|
185
|
+
- 0.5.0
|
186
|
+
- Direct IO via EM.enable_proxy
|
187
|
+
- Encoding support (ref. [github.com/tmm1/http_parser.rb#1](https://github.com/tmm1/http_parser.rb/pull/1))
|
188
|
+
- Optimize for performance
|
189
|
+
- Propagate connection errors to the app
|
182
190
|
|
183
|
-
|
191
|
+
|
192
|
+
Ideas
|
193
|
+
-----
|
194
|
+
|
195
|
+
- Support for rubygems-test
|
184
196
|
- Code reloading
|
185
|
-
- Keep-alive
|
186
|
-
- Native file sending/receiving
|
187
197
|
- Preforking
|
188
198
|
- MVM support via Thread Pool
|
189
199
|
- Support for SPDY
|
@@ -191,9 +201,4 @@ To Do and Ideas
|
|
191
201
|
- Foreman support
|
192
202
|
- Daemonizing and dropping privileges
|
193
203
|
- Trailing headers
|
194
|
-
-
|
195
|
-
|
196
|
-
- Fix http_parser.rb's parsing of chunked bodies
|
197
|
-
- Does http_parser.rb recognize trailing headers?
|
198
|
-
- Encoding support (see https://github.com/tmm1/http_parser.rb/pull/1)
|
199
|
-
- Are there any good C libs for building HTTP messages?
|
204
|
+
- REPL for Server and Client
|
data/hatetepe.gemspec
CHANGED
@@ -21,9 +21,8 @@ Gem::Specification.new do |s|
|
|
21
21
|
|
22
22
|
s.add_development_dependency "rspec"
|
23
23
|
s.add_development_dependency "fakefs"
|
24
|
-
s.add_development_dependency "em-http-request", "~> 1.0"
|
25
24
|
|
26
|
-
s.files = `git ls-files`.split("\n") - [".gitignore"]
|
25
|
+
s.files = `git ls-files`.split("\n") - [".gitignore", ".rspec", ".travis.yml", ".yardopts"]
|
27
26
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
28
27
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
29
28
|
s.require_paths = ["lib"]
|
data/lib/hatetepe/body.rb
CHANGED
@@ -2,6 +2,8 @@ require "em-synchrony"
|
|
2
2
|
require "eventmachine"
|
3
3
|
require "stringio"
|
4
4
|
|
5
|
+
require "hatetepe/deferred_status_fix"
|
6
|
+
|
5
7
|
module Hatetepe
|
6
8
|
# Thin wrapper around StringIO for asynchronous body processing.
|
7
9
|
class Body
|
@@ -158,9 +160,9 @@ module Hatetepe
|
|
158
160
|
# The number of bytes written.
|
159
161
|
def write(data)
|
160
162
|
ret = io.write data
|
161
|
-
|
162
|
-
|
163
|
-
end
|
163
|
+
@receivers.each do |r|
|
164
|
+
Fiber.new { r.call data }.resume
|
165
|
+
end
|
164
166
|
ret
|
165
167
|
end
|
166
168
|
end
|
data/lib/hatetepe/builder.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "rack/utils"
|
2
|
+
|
1
3
|
module Hatetepe
|
2
4
|
class BuilderError < StandardError; end
|
3
5
|
|
@@ -59,7 +61,7 @@ module Hatetepe
|
|
59
61
|
end
|
60
62
|
|
61
63
|
def request(req)
|
62
|
-
request_line req[0], req[1]
|
64
|
+
request_line req[0], req[1], (req[4] || "1.1")
|
63
65
|
headers req[2]
|
64
66
|
body req[3] if req[3]
|
65
67
|
complete
|
@@ -72,7 +74,7 @@ module Hatetepe
|
|
72
74
|
end
|
73
75
|
|
74
76
|
def response(res)
|
75
|
-
response_line res[0]
|
77
|
+
response_line res[0], (res[3] || "1.1")
|
76
78
|
headers res[1]
|
77
79
|
body res[2] if res[2]
|
78
80
|
complete
|
@@ -89,7 +91,8 @@ module Hatetepe
|
|
89
91
|
end
|
90
92
|
|
91
93
|
def header(name, value)
|
92
|
-
|
94
|
+
value = String(value)
|
95
|
+
raw_header "#{name}: #{value}" unless value.empty?
|
93
96
|
end
|
94
97
|
|
95
98
|
def headers(hash)
|
data/lib/hatetepe/cli.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
require "thor"
|
2
2
|
|
3
|
-
require "hatetepe"
|
4
|
-
|
5
3
|
module Hatetepe
|
6
4
|
class CLI < Thor
|
7
5
|
map "--version" => :version
|
@@ -11,20 +9,32 @@ module Hatetepe
|
|
11
9
|
|
12
10
|
desc :version, "Print version information"
|
13
11
|
def version
|
14
|
-
|
12
|
+
require "hatetepe/version"
|
13
|
+
say Hatetepe::VERSION
|
15
14
|
end
|
16
15
|
|
17
|
-
desc
|
16
|
+
desc "[start]", "Start a server"
|
18
17
|
method_option :bind, :aliases => "-b", :type => :string,
|
19
18
|
:banner => "Bind to the specified TCP interface (default: 127.0.0.1)"
|
20
19
|
method_option :port, :aliases => "-p", :type => :numeric,
|
21
20
|
:banner => "Bind to the specified port (default: 3000)"
|
22
21
|
method_option :rackup, :aliases => "-r", :type => :string,
|
23
22
|
:banner => "Load specified rackup (.ru) file (default: config.ru)"
|
23
|
+
method_option :env, :aliases => "-e", :type => :string,
|
24
|
+
:banner => "Boot the app in the specified environment (default: development)"
|
25
|
+
method_option :timeout, :aliases => "-t", :type => :numeric,
|
26
|
+
:banner => "Time out connections after the specified admount of seconds (default: 1)"
|
24
27
|
def start
|
28
|
+
require "hatetepe/server"
|
29
|
+
|
30
|
+
ENV["RACK_ENV"] = expand_env(options[:env]) || ENV["RACK_ENV"] || "development"
|
31
|
+
$stderr << "We're in #{ENV["RACK_ENV"]}\n"
|
32
|
+
$stderr.flush
|
33
|
+
|
25
34
|
rackup = File.expand_path(options[:rackup] || "config.ru")
|
26
35
|
$stderr << "Booting from #{rackup}\n"
|
27
36
|
$stderr.flush
|
37
|
+
|
28
38
|
app = Rack::Builder.parse_file(rackup)[0]
|
29
39
|
|
30
40
|
EM.epoll
|
@@ -41,9 +51,21 @@ module Hatetepe
|
|
41
51
|
:app => app,
|
42
52
|
:errors => $stderr,
|
43
53
|
:host => host,
|
44
|
-
:port => port
|
54
|
+
:port => port,
|
55
|
+
:timeout => (options[:timeout] || 1)
|
45
56
|
})
|
46
57
|
end
|
47
58
|
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def expand_env(env)
|
63
|
+
env &&= env.dup.downcase
|
64
|
+
case env
|
65
|
+
when /^dev(el(op)?)?$/ then "development"
|
66
|
+
when /^test(ing)?$/ then "testing"
|
67
|
+
else env
|
68
|
+
end
|
69
|
+
end
|
48
70
|
end
|
49
71
|
end
|
data/lib/hatetepe/client.rb
CHANGED
@@ -1,111 +1,186 @@
|
|
1
1
|
require "em-synchrony"
|
2
2
|
require "eventmachine"
|
3
|
+
require "rack"
|
3
4
|
require "uri"
|
4
5
|
|
5
|
-
require "hatetepe/body"
|
6
6
|
require "hatetepe/builder"
|
7
|
+
require "hatetepe/connection"
|
8
|
+
require "hatetepe/deferred_status_fix"
|
7
9
|
require "hatetepe/parser"
|
8
10
|
require "hatetepe/request"
|
9
|
-
require "hatetepe/response"
|
10
11
|
require "hatetepe/version"
|
11
12
|
|
12
13
|
module Hatetepe
|
13
|
-
class Client <
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
class Client < Hatetepe::Connection; end
|
15
|
+
end
|
16
|
+
|
17
|
+
require "hatetepe/client/keep_alive"
|
18
|
+
require "hatetepe/client/pipeline"
|
19
|
+
|
20
|
+
class Hatetepe::Client
|
21
|
+
attr_reader :app, :config
|
22
|
+
attr_reader :parser, :builder
|
23
|
+
attr_reader :requests, :pending_transmission, :pending_response
|
24
|
+
|
25
|
+
def initialize(config)
|
26
|
+
@config = config
|
27
|
+
@parser, @builder = Hatetepe::Parser.new, Hatetepe::Builder.new
|
17
28
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
29
|
+
@requests = []
|
30
|
+
@pending_transmission, @pending_response = {}, {}
|
31
|
+
|
32
|
+
@app = Rack::Builder.new.tap do |b|
|
33
|
+
b.use KeepAlive
|
34
|
+
b.use Pipeline
|
35
|
+
b.run method(:send_request)
|
36
|
+
end.to_app
|
37
|
+
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def post_init
|
42
|
+
parser.on_response << method(:receive_response)
|
43
|
+
# XXX check if the connection is still present
|
44
|
+
builder.on_write << method(:send_data)
|
45
|
+
#builder.on_write {|data| p "client >> #{data}" }
|
46
|
+
|
47
|
+
self.processing_enabled = true
|
48
|
+
end
|
49
|
+
|
50
|
+
def receive_data(data)
|
51
|
+
#p "client << #{data}"
|
52
|
+
parser << data
|
53
|
+
rescue => e
|
54
|
+
close_connection
|
55
|
+
raise e
|
56
|
+
end
|
57
|
+
|
58
|
+
def send_request(request)
|
59
|
+
id = request.object_id
|
60
|
+
|
61
|
+
request.headers.delete "X-Hatetepe-Single"
|
62
|
+
builder.request request.to_a
|
63
|
+
pending_transmission[id].succeed
|
64
|
+
|
65
|
+
pending_response[id] = EM::DefaultDeferrable.new
|
66
|
+
EM::Synchrony.sync pending_response[id]
|
67
|
+
ensure
|
68
|
+
pending_response.delete id
|
69
|
+
end
|
70
|
+
|
71
|
+
def receive_response(response)
|
72
|
+
requests.find {|req| !req.response }.tap do |req|
|
73
|
+
req.response = response
|
74
|
+
pending_response[req.object_id].succeed response
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def <<(request)
|
79
|
+
request.connection = self
|
80
|
+
unless processing_enabled?
|
81
|
+
request.fail
|
82
|
+
return
|
30
83
|
end
|
31
84
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
85
|
+
requests << request
|
86
|
+
|
87
|
+
Fiber.new do
|
88
|
+
begin
|
89
|
+
pending_transmission[request.object_id] = EM::DefaultDeferrable.new
|
90
|
+
|
91
|
+
app.call(request).tap do |response|
|
92
|
+
request.response = response
|
93
|
+
# XXX check for response.nil?
|
94
|
+
status = (response && response.success?) ? :succeed : :fail
|
95
|
+
requests.delete(request).send status, response
|
41
96
|
end
|
97
|
+
ensure
|
98
|
+
pending_transmission.delete request.object_id
|
42
99
|
end
|
100
|
+
end.resume
|
101
|
+
end
|
102
|
+
|
103
|
+
def request(verb, uri, headers = {}, body = nil, http_version = "1.1")
|
104
|
+
headers["Host"] ||= "#{config[:host]}:#{config[:port]}"
|
105
|
+
headers["User-Agent"] ||= "hatetepe/#{Hatetepe::VERSION}"
|
106
|
+
|
107
|
+
body = wrap_body(body)
|
108
|
+
if headers["Content-Type"] == "application/x-www-form-urlencoded"
|
109
|
+
enum = Enumerator.new(body)
|
110
|
+
headers["Content-Length"] = enum.inject(0) {|a, e| a + e.length }
|
43
111
|
end
|
44
112
|
|
45
|
-
|
46
|
-
|
113
|
+
request = Hatetepe::Request.new(verb, uri, headers, body, http_version)
|
114
|
+
self << request
|
115
|
+
self.processing_enabled = false
|
116
|
+
EM::Synchrony.sync request
|
47
117
|
|
48
|
-
|
49
|
-
@config = config
|
50
|
-
@requests = []
|
51
|
-
@parser, @builder = Parser.new, Builder.new
|
52
|
-
super
|
53
|
-
end
|
118
|
+
request.response.body.close_write if request.verb == "HEAD"
|
54
119
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
req.response.body.source = self
|
63
|
-
req.succeed req.response
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
#builder.on_write {|chunk|
|
68
|
-
# ap "-> #{chunk}"
|
69
|
-
#}
|
70
|
-
builder.on_write << method(:send_data)
|
120
|
+
request.response
|
121
|
+
end
|
122
|
+
|
123
|
+
def stop
|
124
|
+
unless requests.empty?
|
125
|
+
last_response = EM::Synchrony.sync(requests.last)
|
126
|
+
EM::Synchrony.sync last_response.body if last_response.body
|
71
127
|
end
|
128
|
+
close_connection
|
129
|
+
end
|
130
|
+
|
131
|
+
def unbind
|
132
|
+
super
|
72
133
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if request.headers["Content-Type"] == "application/x-www-form-urlencoded"
|
81
|
-
if request.body.respond_to? :read
|
82
|
-
request.headers["Content-Length"] = request.body.read.bytesize
|
83
|
-
else
|
84
|
-
request.headers["Content-Length"] = request.body.length
|
85
|
-
end
|
134
|
+
EM.next_tick do
|
135
|
+
requests.each do |req|
|
136
|
+
# fail state triggers
|
137
|
+
req.object_id.tap do |id|
|
138
|
+
pending_transmission[id].fail if pending_transmission[id]
|
139
|
+
pending_response[id].fail if pending_response[id]
|
86
140
|
end
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
if Body === b || b.respond_to?(:each)
|
91
|
-
builder.body b
|
92
|
-
elsif b.respond_to? :read
|
93
|
-
builder.body [b.read]
|
94
|
-
else
|
95
|
-
builder.body [b]
|
141
|
+
# fail reponse body if the response has already been started
|
142
|
+
if req.response
|
143
|
+
req.response.body.tap {|b| b.close_write unless b.closed_write? }
|
96
144
|
end
|
97
|
-
|
98
|
-
|
99
|
-
|
145
|
+
# XXX FiberError: dead fiber called because req already succeeded
|
146
|
+
# or failed, see github.com/eventmachine/eventmachine/issues/287
|
147
|
+
req.fail req.response
|
148
|
+
end
|
100
149
|
end
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
150
|
+
end
|
151
|
+
|
152
|
+
def wrap_body(body)
|
153
|
+
if body.respond_to? :each
|
154
|
+
body
|
155
|
+
elsif body.respond_to? :read
|
156
|
+
[body.read]
|
157
|
+
elsif body
|
158
|
+
[body]
|
159
|
+
else
|
160
|
+
[]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class << self
|
165
|
+
def start(config)
|
166
|
+
EM.connect config[:host], config[:port], self, config
|
105
167
|
end
|
106
168
|
|
107
|
-
def
|
108
|
-
|
169
|
+
def request(verb, uri, headers = {}, body = nil)
|
170
|
+
uri = URI(uri)
|
171
|
+
client = start(:host => uri.host, :port => uri.port)
|
172
|
+
|
173
|
+
headers["X-Hatetepe-Single"] = true
|
174
|
+
client.request(verb, uri.request_uri, headers, body).tap do |*|
|
175
|
+
client.stop
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
[self, self.singleton_class].each do |cls|
|
181
|
+
[:get, :head, :post, :put, :delete,
|
182
|
+
:options, :trace, :connect].each do |verb|
|
183
|
+
cls.send(:define_method, verb) {|uri, *args| request verb, uri, *args }
|
109
184
|
end
|
110
185
|
end
|
111
186
|
end
|