hatetepe 0.2.4 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build status](https://secure.travis-ci.org/lgierth/hatetepe.png?branch=master)](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