hatetepe 0.2.4 → 0.3.0

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