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.
@@ -0,0 +1 @@
1
+ - LICENSE
data/Gemfile CHANGED
@@ -4,3 +4,6 @@ gemspec
4
4
 
5
5
  gem "rake"
6
6
  gem "awesome_print"
7
+
8
+ gem "yard"
9
+ gem "rdiscount"
data/README.md CHANGED
@@ -1,17 +1,190 @@
1
- The HTTP toolkit
1
+ The HTTP Toolkit
2
2
  ================
3
3
 
4
- Documentation is asking why you don't write it.
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
- TODO
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)
@@ -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
 
@@ -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
- def initialize(string = "")
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(string)
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
- # TODO maybe I want to #sync here
24
- @io.length
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
- @io.pos
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
- @io.rewind
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
- ret = @io.close_write
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
- @io.closed_write?
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 @io.string.dup unless @io.string.empty?
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
- @io.read *args
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
- @io.gets
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
- def <<(chunk)
74
- ret = @io << chunk
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 chunk }
162
+ @receivers.each {|r| r.call data }
77
163
  }.resume
78
164
  ret
79
165
  end
@@ -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 = STATUS_CODES[code]
83
+ unless status = Rack::Utils::HTTP_STATUS_CODES[code]
86
84
  error "Unknown status code: #{code}"
87
85
  end
88
86
 
@@ -59,6 +59,7 @@ module Hatetepe
59
59
  parser.on_headers {
60
60
  requests.reverse.find {|req| !!req.response }.tap {|req|
61
61
  req.succeed req.response
62
+ # XXX do i want to treat HEAD individually?
62
63
  parser.complete if req.verb == :head
63
64
  }
64
65
  }
@@ -38,7 +38,7 @@ module Hatetepe
38
38
  }
39
39
 
40
40
  p.on_body = proc {|chunk|
41
- message.body << chunk unless message.body.closed_write?
41
+ message.body.write chunk unless message.body.closed_write?
42
42
  }
43
43
 
44
44
  p.on_message_complete = method(:complete)
@@ -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, :env
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
- @env = env
16
- env["proxy.start"] = method(:start)
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
- uri = build_uri(target)
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
- response = Client.request(verb, uri, headers)
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
- env["proxy.callback"] ||= proc {|response|
46
- env["proxy.start_reverse"].call response
47
- }
48
- env["proxy.start_reverse"] = proc {|response|
49
- env["stream.start"].call *response[0..1]
50
- env["stream.send_raw"].call client.requests
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
- def initialize(env, target)
55
- response = Client.request(env["rity.request"])
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
@@ -24,7 +24,7 @@ module Hatetepe
24
24
 
25
25
  @app = Rack::Builder.new.tap {|b|
26
26
  b.use Hatetepe::App
27
- #b.use Hatetepe::Proxy
27
+ b.use Hatetepe::Proxy
28
28
  b.run config[:app]
29
29
  }
30
30
 
@@ -1,3 +1,3 @@
1
1
  module Hatetepe
2
- VERSION = "0.2.4"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -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
- ret = stub("return")
85
- body.io.should_receive(:close_write) { ret }
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 << chunks[0]
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 << chunks[1]
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
@@ -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.should be_empty
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
@@ -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.2.4
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-10-24 00:00:00.000000000Z
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: &77539070 !ruby/object:Gem::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: *77539070
24
+ version_requirements: *85707400
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: eventmachine
27
- requirement: &77524530 !ruby/object:Gem::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: *77524530
35
+ version_requirements: *85706930
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: em-synchrony
38
- requirement: &77524030 !ruby/object:Gem::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: *77524030
46
+ version_requirements: *85706430
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rack
49
- requirement: &77523660 !ruby/object:Gem::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: *77523660
57
+ version_requirements: *85705840
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: async-rack
60
- requirement: &77523290 !ruby/object:Gem::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: *77523290
68
+ version_requirements: *85705450
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: thor
71
- requirement: &77522880 !ruby/object:Gem::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: *77522880
79
+ version_requirements: *85705070
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: rspec
82
- requirement: &77521160 !ruby/object:Gem::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: *77521160
90
+ version_requirements: *85704360
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: fakefs
93
- requirement: &77520680 !ruby/object:Gem::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: *77520680
101
+ version_requirements: *85703690
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: em-http-request
104
- requirement: &77519340 !ruby/object:Gem::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: *77519340
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
@@ -1,11 +0,0 @@
1
- module Hatetepe
2
- class Prefork
3
- def self.run(server)
4
- prefork = new(server)
5
- fork {
6
- prefork.serve
7
- }
8
- prefork.manage
9
- end
10
- end
11
- end
@@ -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
@@ -1,4 +0,0 @@
1
- module Hatetepe
2
- class ThreadPool
3
- end
4
- end