hatetepe 0.2.4 → 0.3.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/.yardopts +1 -0
- data/Gemfile +3 -0
- data/README.md +179 -9
- data/lib/hatetepe/app.rb +31 -0
- data/lib/hatetepe/body.rb +109 -23
- data/lib/hatetepe/builder.rb +1 -3
- data/lib/hatetepe/client.rb +1 -0
- data/lib/hatetepe/parser.rb +1 -1
- data/lib/hatetepe/proxy.rb +33 -37
- data/lib/hatetepe/server.rb +1 -1
- data/lib/hatetepe/version.rb +1 -1
- data/spec/unit/body_spec.rb +21 -15
- data/spec/unit/parser_spec.rb +2 -1
- data/spec/unit/proxy_spec.rb +154 -0
- data/spec/unit/server_spec.rb +1 -0
- metadata +22 -23
- data/lib/hatetepe/prefork.rb +0 -11
- data/lib/hatetepe/status.rb +0 -42
- data/lib/hatetepe/thread_pool.rb +0 -4
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
- LICENSE
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,17 +1,190 @@
|
|
1
|
-
The HTTP
|
1
|
+
The HTTP Toolkit
|
2
2
|
================
|
3
3
|
|
4
|
-
|
4
|
+
Hatetepe is a framework for building HTTP servers, clients and proxies using the
|
5
|
+
Ruby programming language. It makes use of EventMachine and uses a Fiber for
|
6
|
+
each request/response cycle to ensure maximum efficiency. It has some great
|
7
|
+
features that make it a good choice for building HTTP APIs.
|
5
8
|
|
6
|
-
|
7
|
-
|
9
|
+
Install it via `gem install hatetepe` or add `gem "hatetepe"` to your Gemfile.
|
10
|
+
|
11
|
+
Hatetepe only implements core HTTP functionality. If you need stuff like
|
12
|
+
automatic JSON or form-data encoding, have a look at
|
13
|
+
[Faraday](https://github.com/technoweenie/faraday), there's also an
|
14
|
+
[Hatetepe adapter](https://github.com/lgierth/faraday/tree/hatetepe-support)
|
15
|
+
for it being worked on.
|
16
|
+
|
17
|
+
[](http://travis-ci.org/lgierth/hatetepe)
|
18
|
+
|
19
|
+
|
20
|
+
Getting Started (Server)
|
21
|
+
------------------------
|
22
|
+
|
23
|
+
Using Hatetepe as your HTTP server is easy. Simply use the CLI that ships with
|
24
|
+
the gem:
|
25
|
+
|
26
|
+
$ hatetepe
|
27
|
+
Booting from /home/lars/workspace/hatetepe/config.ru
|
28
|
+
Binding to 127.0.0.1:3000
|
29
|
+
|
30
|
+
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 `hatetepe help` command.
|
32
|
+
|
33
|
+
|
34
|
+
Getting Started (Client)
|
35
|
+
------------------------
|
36
|
+
|
37
|
+
The `Hatetepe::Client` class can be used to make requests to an HTTP server.
|
38
|
+
|
39
|
+
client = Hatetepe::Client.start(:host => "example.org", :port => 80)
|
40
|
+
request = Hatetepe::Request.new("POST", "/search", {}, :q => "herp derp")
|
41
|
+
client << request
|
42
|
+
request.callback do |response|
|
43
|
+
puts "Results:"
|
44
|
+
puts response.body.read
|
45
|
+
end
|
46
|
+
request.errback do |response|
|
47
|
+
puts "Error Code: #{response.status}"
|
48
|
+
end
|
49
|
+
|
50
|
+
`Request` and `Response` objects are mostly the same, they offer:
|
51
|
+
|
52
|
+
- `#verb` (only `Request`)
|
53
|
+
- `#uri` (only `Request`)
|
54
|
+
- `#status` (only `Response`)
|
55
|
+
- `#http_version`
|
56
|
+
- `#headers`
|
57
|
+
- `#body`
|
58
|
+
|
59
|
+
`Request` also has `#to_hash` which will turn the object into something your
|
60
|
+
app can respond to.
|
61
|
+
|
62
|
+
|
63
|
+
Async Responses
|
64
|
+
---------------
|
65
|
+
|
66
|
+
Like Thin and Goliath, Hatetepe provides `env["async.callback"]` for responding
|
67
|
+
in an asynchronous fashion. Don't forget to synchronously indicate an
|
68
|
+
asynchronous response by responding with a status of `-1`.
|
69
|
+
|
70
|
+
def call(env)
|
71
|
+
EM.add_timer(5) do
|
72
|
+
env["async.callback"].call [200, {"Content-Type" => "text/html"}, ["Hello!"]]
|
73
|
+
end
|
74
|
+
[-1]
|
75
|
+
end
|
76
|
+
|
77
|
+
The reactor won't block while waiting for the timer to kick in, it will
|
78
|
+
instead process other requests meanwhile.
|
79
|
+
|
80
|
+
|
81
|
+
Proxying
|
82
|
+
--------
|
83
|
+
|
84
|
+
You can easily proxy a request to another HTTP server. The response will be
|
85
|
+
proxied back to the original client automatically. Remember to return an
|
86
|
+
async response.
|
87
|
+
|
88
|
+
def call(env)
|
89
|
+
env["proxy.start"].call "http://intra.example.org/derp"
|
90
|
+
[-1]
|
91
|
+
end
|
92
|
+
|
93
|
+
This will internally just call `env["proxy.callback"]` (which defaults to
|
94
|
+
`env["async.callback"]`). So if you want to send the response yourself, just
|
95
|
+
override `env["proxy.callback"]`.
|
96
|
+
|
97
|
+
If you want to reuse proxy connections (e.g. when doing Connection Pooling),
|
98
|
+
simply create a `Client` instance and pass it to `env["proxy.start"]`.
|
99
|
+
|
100
|
+
env["proxy.start"].call "http://intra.example.org/derp", pool.acquire
|
101
|
+
|
102
|
+
The reactor won't block while waiting for the proxy endpoint's response,
|
103
|
+
it will instead process other requests meanwhile.
|
104
|
+
|
105
|
+
|
106
|
+
Response Streaming
|
107
|
+
------------------
|
108
|
+
|
109
|
+
Streaming a response is easy. Just make your Rack app return a `-1` status code
|
110
|
+
and use the `stream.start`, `stream.send` and `stream.close` helpers.
|
111
|
+
|
112
|
+
def call(env)
|
113
|
+
EM.add_timer 0.5 do
|
114
|
+
env["stream.start"].call [200, {"Content-Type" => "text/plain"}]
|
115
|
+
end
|
116
|
+
|
117
|
+
1.upto 3 do |i|
|
118
|
+
EM.add_timer i do
|
119
|
+
env["stream.send"].call "I feel alive!\n"
|
120
|
+
env["stream.close"].call if i == 3
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
[-1]
|
125
|
+
end
|
126
|
+
|
127
|
+
There's no limit on how long you can stream, keep in mind though that you might
|
128
|
+
hit timeouts. You can occasionally send LFs or something similar to prevent this
|
129
|
+
from happening.
|
130
|
+
|
131
|
+
|
132
|
+
Sending and Receiving BLOBs
|
133
|
+
---------------------------
|
134
|
+
|
135
|
+
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 as
|
137
|
+
soon as all headers have arrived. It can then do stuff while it's still
|
138
|
+
receiving body data. You might for example want to track upload progress.
|
139
|
+
|
140
|
+
received = nil
|
141
|
+
total = nil
|
142
|
+
|
143
|
+
post "/upload" do
|
144
|
+
total = request.headers["Content-Length"].to_i
|
145
|
+
request.env["rack.input"].each do |chunk|
|
146
|
+
received += chunk.bytesize
|
147
|
+
end
|
148
|
+
request.env["rack.input"].rewind
|
149
|
+
end
|
150
|
+
|
151
|
+
get "/progress" do
|
152
|
+
json [received, total]
|
153
|
+
end
|
154
|
+
|
155
|
+
`Hatetepe::Body#each` will block until the response has been received completely
|
156
|
+
and yield each time a new chunk arrives. Calls to `#read`, `#gets` and `#length`
|
157
|
+
will block until everything arrived and then return their normal return value
|
158
|
+
as expected. `Body` includes `EM::Deferrable`, meaning you can attach
|
159
|
+
callbacks to it. `#close_write` will succeed it - this is important if you
|
160
|
+
want to make a request with a streaming body.
|
161
|
+
|
162
|
+
|
163
|
+
Contributing
|
164
|
+
------------
|
165
|
+
|
166
|
+
1. Fork at [github.com/lgierth/hatetepe](https://github.com/lgierth/hatetepe)
|
167
|
+
2. Create a new branch
|
168
|
+
3. Commit, commit, commit!
|
169
|
+
4. Open a Pull Request
|
170
|
+
|
171
|
+
You can also open an issue for discussion first, if you like.
|
172
|
+
|
173
|
+
|
174
|
+
License
|
175
|
+
-------
|
176
|
+
|
177
|
+
Hatetepe is subject to an MIT-style license (see LICENSE file).
|
178
|
+
|
179
|
+
|
180
|
+
To Do and Ideas
|
181
|
+
---------------
|
8
182
|
|
9
183
|
- Proxy
|
10
184
|
- Code reloading
|
11
|
-
- Client
|
12
185
|
- Keep-alive
|
13
|
-
- Preforking
|
14
186
|
- Native file sending/receiving
|
187
|
+
- Preforking
|
15
188
|
- MVM support via Thread Pool
|
16
189
|
- Support for SPDY
|
17
190
|
- Serving via filesystem or in-memory
|
@@ -20,9 +193,6 @@ TODO
|
|
20
193
|
- Trailing headers
|
21
194
|
- Propagating connection errors to the app
|
22
195
|
|
23
|
-
Things to check out
|
24
|
-
-------------------
|
25
|
-
|
26
196
|
- Fix http_parser.rb's parsing of chunked bodies
|
27
197
|
- Does http_parser.rb recognize trailing headers?
|
28
198
|
- Encoding support (see https://github.com/tmm1/http_parser.rb/pull/1)
|
data/lib/hatetepe/app.rb
CHANGED
@@ -9,13 +9,29 @@ module Hatetepe
|
|
9
9
|
ERROR_RESPONSE = [500, {"Content-Type" => "text/html"},
|
10
10
|
["Internal Server Error"]].freeze
|
11
11
|
|
12
|
+
# Interface between Rack-compatible applications and Hatetepe's server.
|
13
|
+
# Provides support for both synchronous and asynchronous responses.
|
12
14
|
class App
|
13
15
|
attr_reader :app
|
14
16
|
|
17
|
+
# Initializes a new App object.
|
18
|
+
#
|
19
|
+
# @param [#call] app
|
20
|
+
# The Rack app
|
21
|
+
#
|
15
22
|
def initialize(app)
|
16
23
|
@app = app
|
17
24
|
end
|
18
25
|
|
26
|
+
# Processes the request.
|
27
|
+
#
|
28
|
+
# Will call #postprocess with the Rack app's response. Catches :async
|
29
|
+
# as an additional indicator for an asynchronous response. Uses a standard
|
30
|
+
# 500 response if the Rack app raises an error.
|
31
|
+
#
|
32
|
+
# @param [Hash] env
|
33
|
+
# The Rack environment
|
34
|
+
#
|
19
35
|
def call(env)
|
20
36
|
env["async.callback"] = proc {|response|
|
21
37
|
postprocess env, response
|
@@ -28,6 +44,21 @@ module Hatetepe
|
|
28
44
|
postprocess env, response
|
29
45
|
end
|
30
46
|
|
47
|
+
# Sends the response.
|
48
|
+
#
|
49
|
+
# Does nothing if response status is indicating an asynchronous response.
|
50
|
+
# This is the case if the response Array's first element equals -1.
|
51
|
+
# Otherwise it will start sending the response (status and headers).
|
52
|
+
#
|
53
|
+
# If the body indicates streaming it will return after sending the status
|
54
|
+
# and headers. This happens if the body equals Rack::STREAMING. Otherwise
|
55
|
+
# it sends each body chunk and then closes the response stream.
|
56
|
+
#
|
57
|
+
# @param [Hash] env
|
58
|
+
# The Rack environment
|
59
|
+
# @param [Array] response
|
60
|
+
# An array of 1..3 length containing the status, headers, body
|
61
|
+
#
|
31
62
|
def postprocess(env, response)
|
32
63
|
return if response[0] == ASYNC_RESPONSE[0]
|
33
64
|
|
data/lib/hatetepe/body.rb
CHANGED
@@ -3,77 +3,163 @@ require "eventmachine"
|
|
3
3
|
require "stringio"
|
4
4
|
|
5
5
|
module Hatetepe
|
6
|
+
# Thin wrapper around StringIO for asynchronous body processing.
|
6
7
|
class Body
|
7
8
|
include EM::Deferrable
|
8
9
|
|
10
|
+
# The wrapped StringIO.
|
9
11
|
attr_reader :io
|
12
|
+
|
13
|
+
# The origin Client or Server connection.
|
10
14
|
attr_accessor :source
|
11
15
|
|
12
|
-
|
16
|
+
# Create a new Body instance.
|
17
|
+
#
|
18
|
+
# @param [String] data
|
19
|
+
# Initial content of the StringIO object.
|
20
|
+
def initialize(data = "")
|
13
21
|
@receivers = []
|
14
|
-
@io = StringIO.new(
|
22
|
+
@io = StringIO.new(data)
|
15
23
|
end
|
16
24
|
|
25
|
+
# Blocks until the Body is write-closed.
|
26
|
+
#
|
27
|
+
# Use this if you want to wait until _all_ of the body has arrived before
|
28
|
+
# continuing. It will resume the originating connection if it's paused.
|
29
|
+
#
|
30
|
+
# @return [undefined]
|
17
31
|
def sync
|
18
32
|
source.resume if source && source.paused?
|
19
33
|
EM::Synchrony.sync self
|
20
34
|
end
|
21
35
|
|
36
|
+
# Forwards to StringIO#length.
|
37
|
+
#
|
38
|
+
# Blocks until the Body is write-closed. Returns the current length of the
|
39
|
+
# underlying StringIO's content.
|
40
|
+
#
|
41
|
+
# @return [Fixnum]
|
42
|
+
# The StringIO's length.
|
22
43
|
def length
|
23
|
-
|
24
|
-
|
44
|
+
sync
|
45
|
+
io.length
|
25
46
|
end
|
26
47
|
|
48
|
+
# Returns true if the underlying StringIO is empty, false otherwise.
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
# True if empty, false otherwise.
|
27
52
|
def empty?
|
28
53
|
length == 0
|
29
54
|
end
|
30
55
|
|
56
|
+
# Forwards to StringIO#pos.
|
57
|
+
#
|
58
|
+
# Returns the underlying StringIO's current pointer position.
|
59
|
+
#
|
60
|
+
# @return [Fixnum]
|
61
|
+
# The current pointer position.
|
31
62
|
def pos
|
32
|
-
|
63
|
+
io.pos
|
33
64
|
end
|
34
65
|
|
66
|
+
# Forwards to StringIO#rewind.
|
67
|
+
#
|
68
|
+
# Moves the underlying StringIO's pointer back to the beginnung.
|
69
|
+
#
|
70
|
+
# @return [undefined]
|
35
71
|
def rewind
|
36
|
-
|
72
|
+
io.rewind
|
37
73
|
end
|
38
74
|
|
75
|
+
# Forwards to StringIO#close_write.
|
76
|
+
#
|
77
|
+
# Write-closes the body and succeeds, thus releasing all blocking method
|
78
|
+
# calls like #length, #each, #read and #get.
|
79
|
+
#
|
80
|
+
# @return [undefined]
|
39
81
|
def close_write
|
40
|
-
|
82
|
+
io.close_write
|
41
83
|
succeed
|
42
|
-
ret
|
43
84
|
end
|
44
85
|
|
86
|
+
# Forwards to StringIO#closed_write?.
|
87
|
+
#
|
88
|
+
# Returns true if the body is write-closed, false otherwise.
|
89
|
+
#
|
90
|
+
# @return [Boolean]
|
91
|
+
# True if the body is write-closed, false otherwise.
|
45
92
|
def closed_write?
|
46
|
-
|
93
|
+
io.closed_write?
|
47
94
|
end
|
48
95
|
|
96
|
+
# Yields incoming body data.
|
97
|
+
#
|
98
|
+
# Immediately yields all data that has already arrived. Blocks until the
|
99
|
+
# Body is write-closed and yields for each call to #write until then.
|
100
|
+
#
|
101
|
+
# @yield [String] Block to execute for each incoming data chunk
|
102
|
+
#
|
103
|
+
# @return [undefined]
|
49
104
|
def each(&block)
|
50
105
|
@receivers << block
|
51
|
-
block.call
|
106
|
+
block.call io.string.dup unless io.string.empty?
|
52
107
|
sync
|
53
108
|
end
|
54
109
|
|
110
|
+
# Forwards to StringIO#read.
|
111
|
+
#
|
112
|
+
# From the Rack Spec: If given, +length+ must be a non-negative Integer
|
113
|
+
# (>= 0) or +nil+, and +buffer+ must be a String and may not be nil. If
|
114
|
+
# +length+ is given and not nil, then this method reads at most +length+
|
115
|
+
# bytes from the input stream. If +length+ is not given or nil, then this
|
116
|
+
# method reads all data until EOF. When EOF is reached, this method returns
|
117
|
+
# nil if +length+ is given and not nil, or "" if +length+ is not given or
|
118
|
+
# is nil. If +buffer+ is given, then the read data will be placed into
|
119
|
+
# +buffer+ instead of a newly created String object.
|
120
|
+
#
|
121
|
+
# @param [Fixnum] length (optional)
|
122
|
+
# How many bytes to read.
|
123
|
+
# @param [String] buffer (optional)
|
124
|
+
# Buffer for read data.
|
125
|
+
#
|
126
|
+
# @return [nil]
|
127
|
+
# +nil+ if EOF has been reached.
|
128
|
+
# @return [String]
|
129
|
+
# All data or at most +length+ bytes of data if +length+ is given.
|
55
130
|
def read(*args)
|
56
131
|
sync
|
57
|
-
|
132
|
+
io.read *args
|
58
133
|
end
|
59
134
|
|
135
|
+
# Forwards to StringIO#gets.
|
136
|
+
#
|
137
|
+
# Reads one line from the IO. Returns the line or +nil+ if EOF has been
|
138
|
+
# reached.
|
139
|
+
#
|
140
|
+
# @return [String]
|
141
|
+
# One line.
|
142
|
+
# @return [nil]
|
143
|
+
# If has been reached.
|
60
144
|
def gets
|
61
145
|
sync
|
62
|
-
|
63
|
-
end
|
64
|
-
|
65
|
-
def write(chunk)
|
66
|
-
ret = @io.write chunk
|
67
|
-
Fiber.new {
|
68
|
-
@receivers.each {|r| r.call chunk }
|
69
|
-
}.resume
|
70
|
-
ret
|
146
|
+
io.gets
|
71
147
|
end
|
72
148
|
|
73
|
-
|
74
|
-
|
149
|
+
# Forwards to StringIO#write.
|
150
|
+
#
|
151
|
+
# Appends the given String to the underlying StringIO annd returns the
|
152
|
+
# number of bytes written.
|
153
|
+
#
|
154
|
+
# @param [String] data
|
155
|
+
# The data to append
|
156
|
+
#
|
157
|
+
# @return [Fixnum]
|
158
|
+
# The number of bytes written.
|
159
|
+
def write(data)
|
160
|
+
ret = io.write data
|
75
161
|
Fiber.new {
|
76
|
-
@receivers.each {|r| r.call
|
162
|
+
@receivers.each {|r| r.call data }
|
77
163
|
}.resume
|
78
164
|
ret
|
79
165
|
end
|
data/lib/hatetepe/builder.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "hatetepe/status"
|
2
|
-
|
3
1
|
module Hatetepe
|
4
2
|
class BuilderError < StandardError; end
|
5
3
|
|
@@ -82,7 +80,7 @@ module Hatetepe
|
|
82
80
|
|
83
81
|
def response_line(code, version = "1.1")
|
84
82
|
complete unless ready?
|
85
|
-
unless status =
|
83
|
+
unless status = Rack::Utils::HTTP_STATUS_CODES[code]
|
86
84
|
error "Unknown status code: #{code}"
|
87
85
|
end
|
88
86
|
|
data/lib/hatetepe/client.rb
CHANGED
data/lib/hatetepe/parser.rb
CHANGED
data/lib/hatetepe/proxy.rb
CHANGED
@@ -1,58 +1,54 @@
|
|
1
|
-
require "eventmachine"
|
2
|
-
require "uri"
|
3
|
-
|
4
1
|
require "hatetepe/client"
|
2
|
+
require "hatetepe/request"
|
3
|
+
require "uri"
|
5
4
|
|
6
5
|
module Hatetepe
|
7
6
|
class Proxy
|
8
|
-
attr_reader :app
|
7
|
+
attr_reader :app
|
9
8
|
|
10
9
|
def initialize(app)
|
11
10
|
@app = app
|
12
11
|
end
|
13
12
|
|
14
13
|
def call(env)
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
env["proxy.start"] = proc do |target, client = nil|
|
15
|
+
start env, target, client
|
16
|
+
end
|
18
17
|
app.call env
|
19
18
|
end
|
20
19
|
|
21
|
-
def start(target)
|
22
|
-
|
23
|
-
|
20
|
+
def start(env, target, client)
|
21
|
+
target = URI.parse(target)
|
24
22
|
env.delete "proxy.start"
|
25
|
-
env["proxy.callback"] ||= method(:callback)
|
26
23
|
|
27
|
-
|
28
|
-
env["proxy.callback"].call @response, env
|
29
|
-
end
|
30
|
-
|
31
|
-
def callback(response, env)
|
32
|
-
response
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
module Hatetepe
|
38
|
-
class OldProxy
|
39
|
-
attr_reader :env, :target
|
40
|
-
|
41
|
-
def initialize(env, target)
|
42
|
-
client = EM.connect target.host, target.port, Client
|
43
|
-
client.request env["rity.request"].verb, env["rity.request"].uri
|
24
|
+
env["proxy.callback"] ||= env["async.callback"]
|
44
25
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
env["
|
51
|
-
|
26
|
+
cl = client || Client.start(:host => target.host, :port => target.port)
|
27
|
+
build_request(env, target).tap do |req|
|
28
|
+
cl << req
|
29
|
+
EM::Synchrony.sync req
|
30
|
+
cl.stop unless client
|
31
|
+
env["proxy.callback"].call req.response
|
32
|
+
end
|
52
33
|
end
|
53
34
|
|
54
|
-
|
55
|
-
|
35
|
+
# TODO only use +env+ to build the request
|
36
|
+
def build_request(env, target)
|
37
|
+
unless base = env["hatetepe.request"]
|
38
|
+
raise ArgumentError, "Proxying requires env[hatetepe.request] to be set"
|
39
|
+
end
|
40
|
+
|
41
|
+
uri = target.path + base.uri
|
42
|
+
host = "#{target.host}:#{target.port}"
|
43
|
+
headers = base.headers.merge({
|
44
|
+
"X-Forwarded-For" => env["REMOTE_ADDR"],
|
45
|
+
"Host" => [base.headers["Host"], host].compact.join(", ")
|
46
|
+
})
|
47
|
+
|
48
|
+
Request.new(base.verb, uri, base.http_version).tap do |req|
|
49
|
+
req.headers = headers
|
50
|
+
req.body = base.body
|
51
|
+
end
|
56
52
|
end
|
57
53
|
end
|
58
54
|
end
|
data/lib/hatetepe/server.rb
CHANGED
data/lib/hatetepe/version.rb
CHANGED
data/spec/unit/body_spec.rb
CHANGED
@@ -17,6 +17,8 @@ describe Hatetepe::Body do
|
|
17
17
|
context "#initialize(string)" do
|
18
18
|
let(:body) { Hatetepe::Body.new "herp derp" }
|
19
19
|
|
20
|
+
before { body.close_write }
|
21
|
+
|
20
22
|
it "writes the passed string" do
|
21
23
|
body.length.should equal(9)
|
22
24
|
body.io.read.should == "herp derp"
|
@@ -44,9 +46,24 @@ describe Hatetepe::Body do
|
|
44
46
|
let(:length) { stub "length" }
|
45
47
|
|
46
48
|
it "forwards to io#length" do
|
49
|
+
body.stub :sync
|
47
50
|
body.io.stub :length => length
|
51
|
+
|
48
52
|
body.length.should equal(length)
|
49
53
|
end
|
54
|
+
|
55
|
+
it "waits for the body to succeed" do
|
56
|
+
succeeded = false
|
57
|
+
Fiber.new {
|
58
|
+
body.length
|
59
|
+
succeeded = true
|
60
|
+
}.resume
|
61
|
+
|
62
|
+
succeeded.should be_false
|
63
|
+
|
64
|
+
body.close_write
|
65
|
+
succeeded.should be_true
|
66
|
+
end
|
50
67
|
end
|
51
68
|
|
52
69
|
context "#empty?" do
|
@@ -81,10 +98,8 @@ describe Hatetepe::Body do
|
|
81
98
|
|
82
99
|
context "#close_write" do
|
83
100
|
it "forwards to io#close_write" do
|
84
|
-
|
85
|
-
body.
|
86
|
-
|
87
|
-
body.close_write.should equal(ret)
|
101
|
+
body.io.should_receive :close_write
|
102
|
+
body.close_write
|
88
103
|
end
|
89
104
|
|
90
105
|
it "succeeds the body" do
|
@@ -107,7 +122,7 @@ describe Hatetepe::Body do
|
|
107
122
|
chunks = ["111", "222"]
|
108
123
|
received, succeeded = [], false
|
109
124
|
|
110
|
-
body
|
125
|
+
body.write chunks[0]
|
111
126
|
Fiber.new {
|
112
127
|
body.each {|chunk| received << chunk }
|
113
128
|
succeeded = true
|
@@ -115,7 +130,7 @@ describe Hatetepe::Body do
|
|
115
130
|
received.should == chunks.values_at(0)
|
116
131
|
succeeded.should be_false
|
117
132
|
|
118
|
-
body
|
133
|
+
body.write chunks[1]
|
119
134
|
received.should == chunks
|
120
135
|
succeeded.should be_false
|
121
136
|
|
@@ -172,13 +187,4 @@ describe Hatetepe::Body do
|
|
172
187
|
body.write(arg).should equal(ret)
|
173
188
|
end
|
174
189
|
end
|
175
|
-
|
176
|
-
context "#<<(chunk)" do
|
177
|
-
it "forwards to io#<<" do
|
178
|
-
arg, ret = stub("arg"), stub("ret")
|
179
|
-
body.io.should_receive(:<<).with(arg) { ret }
|
180
|
-
|
181
|
-
body.<<(arg).should equal(ret)
|
182
|
-
end
|
183
|
-
end
|
184
190
|
end
|
data/spec/unit/parser_spec.rb
CHANGED
@@ -165,7 +165,8 @@ describe Hatetepe::Parser do
|
|
165
165
|
block.should_receive(:call) {|body|
|
166
166
|
body.should equal(parser.message.body)
|
167
167
|
|
168
|
-
body
|
168
|
+
# we'd have to #close_write to get body#length
|
169
|
+
body.io.length.should == 0
|
169
170
|
}
|
170
171
|
|
171
172
|
parser.on_body &block
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "hatetepe/proxy"
|
3
|
+
|
4
|
+
describe Hatetepe::Proxy do
|
5
|
+
let(:app) { stub "app" }
|
6
|
+
|
7
|
+
describe "#initialize(app)" do
|
8
|
+
it "sets the app" do
|
9
|
+
Hatetepe::Proxy.new(app).app.should equal(app)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:proxy) { Hatetepe::Proxy.new app }
|
14
|
+
let(:target) { stub "target" }
|
15
|
+
let(:env) { {} }
|
16
|
+
let(:client) { stub "client", :<< => nil }
|
17
|
+
|
18
|
+
describe "#call(env)" do
|
19
|
+
it "sets env[proxy.start]" do
|
20
|
+
app.stub :call do |env|
|
21
|
+
env["proxy.start"].should respond_to(:call)
|
22
|
+
end
|
23
|
+
proxy.call env
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:response) { stub "response" }
|
27
|
+
|
28
|
+
it "calls the app" do
|
29
|
+
app.should_receive(:call).with(env) { response }
|
30
|
+
proxy.call(env).should equal(response)
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "env[proxy.start]" do
|
34
|
+
it "forwards to #start" do
|
35
|
+
proxy.should_receive(:start).with(env, target, client)
|
36
|
+
app.stub :call do |env|
|
37
|
+
env["proxy.start"].call target, client
|
38
|
+
end
|
39
|
+
proxy.call env
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#start(env, target, client)" do
|
45
|
+
let(:request) { stub "request" }
|
46
|
+
let(:response) { stub "response" }
|
47
|
+
let(:callback) { stub "async.callback", :call => nil }
|
48
|
+
|
49
|
+
let(:host) { stub "host" }
|
50
|
+
let(:port) { stub "port" }
|
51
|
+
let(:target) { stub "target", :host => host, :port => port }
|
52
|
+
|
53
|
+
before do
|
54
|
+
URI.stub :parse => target
|
55
|
+
proxy.stub :build_request => request
|
56
|
+
|
57
|
+
request.stub :dup => request, :response => response
|
58
|
+
request.extend EM::Deferrable
|
59
|
+
env["async.callback"] = callback
|
60
|
+
end
|
61
|
+
|
62
|
+
it "deletes env[proxy.start] from the env hash" do
|
63
|
+
env.should_receive(:delete).with "proxy.start"
|
64
|
+
Fiber.new { proxy.start env, target, client }.resume
|
65
|
+
end
|
66
|
+
|
67
|
+
it "defaults env[proxy.callback] to env[async.callback]" do
|
68
|
+
Fiber.new { proxy.start env, target, client }.resume
|
69
|
+
env["proxy.callback"].should equal(env["async.callback"])
|
70
|
+
end
|
71
|
+
|
72
|
+
let(:new_client) { stub "new client" }
|
73
|
+
|
74
|
+
it "starts a client if none was passed" do
|
75
|
+
Hatetepe::Client.stub :start do |config|
|
76
|
+
config[:host].should equal(host)
|
77
|
+
config[:port].should equal(port)
|
78
|
+
new_client
|
79
|
+
end
|
80
|
+
new_client.should_receive(:<<).with request
|
81
|
+
Fiber.new { proxy.start env, target, nil }.resume
|
82
|
+
end
|
83
|
+
|
84
|
+
it "doesn't stop a client that was passed" do
|
85
|
+
client.should_not_receive :stop
|
86
|
+
Fiber.new { proxy.start env, target, client }.resume
|
87
|
+
request.succeed
|
88
|
+
end
|
89
|
+
|
90
|
+
it "passes the request to the client" do
|
91
|
+
proxy.should_receive :build_request do |e, t|
|
92
|
+
env.should equal(e)
|
93
|
+
target.should equal(t)
|
94
|
+
request
|
95
|
+
end
|
96
|
+
client.should_receive(:<<).with request
|
97
|
+
Fiber.new { proxy.start env, target, client }.resume
|
98
|
+
end
|
99
|
+
|
100
|
+
it "passes the response to env[async.callback]" do
|
101
|
+
callback.should_receive(:call).with response
|
102
|
+
Fiber.new { proxy.start env, target, client }.resume
|
103
|
+
request.succeed
|
104
|
+
end
|
105
|
+
|
106
|
+
it "waits for the request to succeed" do
|
107
|
+
succeeded = false
|
108
|
+
callback.stub(:call) {|response| succeeded = true }
|
109
|
+
|
110
|
+
Fiber.new { proxy.start env, target, client }.resume
|
111
|
+
succeeded.should be_false
|
112
|
+
|
113
|
+
request.succeed
|
114
|
+
succeeded.should be_true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#build_request(env, target)" do
|
119
|
+
let(:target) { URI.parse "http://localhost:3000/bar" }
|
120
|
+
let(:base_request) { Hatetepe::Request.new "GET", "/foo" }
|
121
|
+
|
122
|
+
before do
|
123
|
+
env["hatetepe.request"] = base_request
|
124
|
+
env["REMOTE_ADDR"] = "123.234.123.234"
|
125
|
+
end
|
126
|
+
|
127
|
+
it "fails if env[hatetepe.request] isn't set" do
|
128
|
+
env.delete "hatetepe.request"
|
129
|
+
proc { proxy.build_request env, target }.should raise_error(ArgumentError)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "combines the original URI with the target URI" do
|
133
|
+
proxy.build_request(env, target).uri.should == "/bar/foo"
|
134
|
+
end
|
135
|
+
|
136
|
+
it "sets X-Forwarded-For header" do
|
137
|
+
xff = proxy.build_request(env, target).headers["X-Forwarded-For"]
|
138
|
+
env["REMOTE_ADDR"].should == xff
|
139
|
+
end
|
140
|
+
|
141
|
+
it "adds the target to Host header" do
|
142
|
+
host = "localhost:3000"
|
143
|
+
proxy.build_request(env, target).headers["Host"].should == host
|
144
|
+
|
145
|
+
base_request.headers["Host"] = host
|
146
|
+
host = "localhost:3000, localhost:3000"
|
147
|
+
proxy.build_request(env, target).headers["Host"].should == host
|
148
|
+
end
|
149
|
+
|
150
|
+
it "builds a new request" do
|
151
|
+
proxy.build_request(env, target).should_not equal(base_request)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/spec/unit/server_spec.rb
CHANGED
@@ -45,6 +45,7 @@ describe Hatetepe::Server do
|
|
45
45
|
it "builds the app" do
|
46
46
|
Rack::Builder.stub :new => builder
|
47
47
|
builder.should_receive(:use).with Hatetepe::App
|
48
|
+
builder.should_receive(:use).with Hatetepe::Proxy
|
48
49
|
builder.should_receive(:run).with app
|
49
50
|
|
50
51
|
server.send :initialize, config
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hatetepe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-
|
12
|
+
date: 2011-11-01 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: http_parser.rb
|
16
|
-
requirement: &
|
16
|
+
requirement: &85707400 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 0.5.3
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *85707400
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: eventmachine
|
27
|
-
requirement: &
|
27
|
+
requirement: &85706930 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *85706930
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: em-synchrony
|
38
|
-
requirement: &
|
38
|
+
requirement: &85706430 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '1.0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *85706430
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rack
|
49
|
-
requirement: &
|
49
|
+
requirement: &85705840 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *85705840
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: async-rack
|
60
|
-
requirement: &
|
60
|
+
requirement: &85705450 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :runtime
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *85705450
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: thor
|
71
|
-
requirement: &
|
71
|
+
requirement: &85705070 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :runtime
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *85705070
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: rspec
|
82
|
-
requirement: &
|
82
|
+
requirement: &85704360 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *85704360
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: fakefs
|
93
|
-
requirement: &
|
93
|
+
requirement: &85703690 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - ! '>='
|
@@ -98,10 +98,10 @@ dependencies:
|
|
98
98
|
version: '0'
|
99
99
|
type: :development
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *85703690
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: em-http-request
|
104
|
-
requirement: &
|
104
|
+
requirement: &85703190 !ruby/object:Gem::Requirement
|
105
105
|
none: false
|
106
106
|
requirements:
|
107
107
|
- - ~>
|
@@ -109,7 +109,7 @@ dependencies:
|
|
109
109
|
version: '1.0'
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
|
-
version_requirements: *
|
112
|
+
version_requirements: *85703190
|
113
113
|
description:
|
114
114
|
email:
|
115
115
|
- lars.gierth@gmail.com
|
@@ -120,6 +120,7 @@ extra_rdoc_files: []
|
|
120
120
|
files:
|
121
121
|
- .rspec
|
122
122
|
- .travis.yml
|
123
|
+
- .yardopts
|
123
124
|
- Gemfile
|
124
125
|
- LICENSE
|
125
126
|
- README.md
|
@@ -135,13 +136,10 @@ files:
|
|
135
136
|
- lib/hatetepe/events.rb
|
136
137
|
- lib/hatetepe/message.rb
|
137
138
|
- lib/hatetepe/parser.rb
|
138
|
-
- lib/hatetepe/prefork.rb
|
139
139
|
- lib/hatetepe/proxy.rb
|
140
140
|
- lib/hatetepe/request.rb
|
141
141
|
- lib/hatetepe/response.rb
|
142
142
|
- lib/hatetepe/server.rb
|
143
|
-
- lib/hatetepe/status.rb
|
144
|
-
- lib/hatetepe/thread_pool.rb
|
145
143
|
- lib/hatetepe/version.rb
|
146
144
|
- lib/rack/handler/hatetepe.rb
|
147
145
|
- spec/integration/cli/start_spec.rb
|
@@ -152,6 +150,7 @@ files:
|
|
152
150
|
- spec/unit/client_spec.rb
|
153
151
|
- spec/unit/events_spec.rb
|
154
152
|
- spec/unit/parser_spec.rb
|
153
|
+
- spec/unit/proxy_spec.rb
|
155
154
|
- spec/unit/rack_handler_spec.rb
|
156
155
|
- spec/unit/server_spec.rb
|
157
156
|
homepage: https://github.com/lgierth/hatetepe
|
data/lib/hatetepe/prefork.rb
DELETED
data/lib/hatetepe/status.rb
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
module Hatetepe
|
2
|
-
# @author Mongrel
|
3
|
-
STATUS_CODES = {
|
4
|
-
100 => "Continue",
|
5
|
-
101 => "Switching Protocols",
|
6
|
-
200 => "OK",
|
7
|
-
201 => "Created",
|
8
|
-
202 => "Accepted",
|
9
|
-
203 => "Non-Authoritative Information",
|
10
|
-
204 => "No Content",
|
11
|
-
205 => "Reset Content",
|
12
|
-
206 => "Partial Content",
|
13
|
-
300 => "Multiple Choices",
|
14
|
-
301 => "Moved Permanently",
|
15
|
-
302 => "Moved Temporarily",
|
16
|
-
303 => "See Other",
|
17
|
-
304 => "Not Modified",
|
18
|
-
305 => "Use Proxy",
|
19
|
-
400 => "Bad Request",
|
20
|
-
401 => "Unauthorized",
|
21
|
-
402 => "Payment Required",
|
22
|
-
403 => "Forbidden",
|
23
|
-
404 => "Not Found",
|
24
|
-
405 => "Method Not Allowed",
|
25
|
-
406 => "Not Acceptable",
|
26
|
-
407 => "Proxy Authentication Required",
|
27
|
-
408 => "Request Time-out",
|
28
|
-
409 => "Conflict",
|
29
|
-
410 => "Gone",
|
30
|
-
411 => "Length Required",
|
31
|
-
412 => "Precondition Failed",
|
32
|
-
413 => "Request Entity Too Large",
|
33
|
-
414 => "Request-URI Too Large",
|
34
|
-
415 => "Unsupported Media Type",
|
35
|
-
500 => "Internal Server Error",
|
36
|
-
501 => "Not Implemented",
|
37
|
-
502 => "Bad Gateway",
|
38
|
-
503 => "Service Unavailable",
|
39
|
-
504 => "Gateway Time-out",
|
40
|
-
505 => "HTTP Version Not Supported"
|
41
|
-
}
|
42
|
-
end
|
data/lib/hatetepe/thread_pool.rb
DELETED