hatetepe 0.0.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/.travis.yml +3 -0
- data/README.md +21 -5
- data/Rakefile +3 -10
- data/bin/hatetepe +4 -0
- data/hatetepe.gemspec +10 -3
- data/lib/hatetepe.rb +5 -0
- data/lib/hatetepe/app.rb +44 -0
- data/lib/hatetepe/body.rb +79 -0
- data/lib/hatetepe/builder.rb +19 -3
- data/lib/hatetepe/cli.rb +50 -0
- data/lib/hatetepe/client.rb +95 -0
- data/lib/hatetepe/events.rb +35 -0
- data/lib/hatetepe/message.rb +13 -0
- data/lib/hatetepe/parser.rb +41 -71
- data/lib/hatetepe/prefork.rb +11 -0
- data/lib/hatetepe/proxy.rb +58 -0
- data/lib/hatetepe/request.rb +31 -0
- data/lib/hatetepe/response.rb +20 -0
- data/lib/hatetepe/server.rb +98 -0
- data/lib/hatetepe/thread_pool.rb +4 -0
- data/lib/hatetepe/version.rb +1 -1
- data/lib/rack/handler/hatetepe.rb +33 -0
- data/spec/integration/cli/start_spec.rb +169 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/unit/app_spec.rb +108 -0
- data/spec/unit/body_spec.rb +198 -0
- data/spec/unit/client_spec.rb +270 -0
- data/spec/unit/events_spec.rb +96 -0
- data/spec/unit/parser_spec.rb +215 -0
- data/spec/unit/rack_handler_spec.rb +70 -0
- data/spec/unit/server_spec.rb +255 -0
- metadata +141 -56
- data/example.rb +0 -29
- data/test/builder_test.rb +0 -7
- data/test/parser_test.rb +0 -7
- data/test/test_helper.rb +0 -7
data/lib/hatetepe/parser.rb
CHANGED
@@ -1,68 +1,51 @@
|
|
1
1
|
require "http/parser"
|
2
2
|
|
3
|
+
require "hatetepe/events"
|
4
|
+
require "hatetepe/request"
|
5
|
+
require "hatetepe/response"
|
6
|
+
|
3
7
|
module Hatetepe
|
4
8
|
class ParserError < StandardError; end
|
5
9
|
|
6
10
|
class Parser
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
message[:status] = args[0]
|
17
|
-
message[:http_version] = args[1]
|
18
|
-
end
|
19
|
-
p.on_header do |name, value|
|
20
|
-
(message[:headers] ||= {})[name] = value
|
21
|
-
end
|
22
|
-
p.on_body_chunk do |chunk|
|
23
|
-
(message[:body] ||= "") << chunk
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
if block
|
28
|
-
block.arity == 0 ? parser.instance_eval(&block) : block.call(parser)
|
29
|
-
end
|
30
|
-
|
31
|
-
Array(data).each {|chunk| parser << chunk }
|
32
|
-
message
|
33
|
-
end
|
11
|
+
include Events
|
12
|
+
|
13
|
+
event :reset
|
14
|
+
event :request, :response
|
15
|
+
event :headers, :body
|
16
|
+
event :trailing_header, :trailing_headers_complete
|
17
|
+
event :complete
|
18
|
+
|
19
|
+
attr_reader :message
|
34
20
|
|
35
21
|
def initialize(&block)
|
36
|
-
@
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
22
|
+
@parser = HTTP::Parser.new.tap {|p|
|
23
|
+
p.on_headers_complete = proc {
|
24
|
+
version = p.http_version.join(".")
|
25
|
+
if p.http_method
|
26
|
+
@message = Request.new(p.http_method, p.request_url, version)
|
27
|
+
event! :request, message
|
28
|
+
else
|
29
|
+
@message = Response.new(p.status_code, version)
|
30
|
+
event! :response, message
|
45
31
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
32
|
+
|
33
|
+
message.headers = p.headers
|
34
|
+
event! :headers, message.headers
|
35
|
+
|
36
|
+
event! :body, message.body
|
37
|
+
nil
|
38
|
+
}
|
51
39
|
|
52
|
-
|
53
|
-
|
54
|
-
|
40
|
+
p.on_body = proc {|chunk|
|
41
|
+
message.body << chunk unless message.body.closed_write?
|
42
|
+
}
|
55
43
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
@parser.on_message_complete = proc do
|
64
|
-
on_complete.each {|f| f.call }
|
65
|
-
end
|
44
|
+
p.on_message_complete = proc {
|
45
|
+
message.body.close_write unless message.body.closed_write?
|
46
|
+
event! :complete
|
47
|
+
}
|
48
|
+
}
|
66
49
|
|
67
50
|
reset
|
68
51
|
|
@@ -73,27 +56,14 @@ module Hatetepe
|
|
73
56
|
|
74
57
|
def reset
|
75
58
|
@parser.reset!
|
76
|
-
|
77
|
-
|
78
|
-
[:request, :response, :header, :headers_complete,
|
79
|
-
:body_chunk, :complete, :error].each do |hook|
|
80
|
-
define_method :"on_#{hook}" do |&block|
|
81
|
-
store = instance_variable_get(:"@on_#{hook}")
|
82
|
-
return store unless block
|
83
|
-
store << block
|
84
|
-
end
|
59
|
+
event! :reset
|
60
|
+
@message = nil
|
85
61
|
end
|
86
62
|
|
87
63
|
def <<(data)
|
88
64
|
@parser << data
|
89
|
-
rescue HTTP::Parser::Error =>
|
90
|
-
|
91
|
-
error.set_backtrace(original_error.backtrace)
|
92
|
-
unless on_error.empty?
|
93
|
-
on_error.each {|e| e.call(error) }
|
94
|
-
else
|
95
|
-
raise(error)
|
96
|
-
end
|
65
|
+
rescue HTTP::Parser::Error => e
|
66
|
+
raise Hatetepe::ParserError, e.message, e.backtrace
|
97
67
|
end
|
98
68
|
end
|
99
69
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
require "hatetepe/client"
|
5
|
+
|
6
|
+
module Hatetepe
|
7
|
+
class Proxy
|
8
|
+
attr_reader :app, :env
|
9
|
+
|
10
|
+
def initialize(app)
|
11
|
+
@app = app
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
@env = env
|
16
|
+
env["proxy.start"] = method(:start)
|
17
|
+
|
18
|
+
app.call env
|
19
|
+
end
|
20
|
+
|
21
|
+
def start(target)
|
22
|
+
uri = build_uri(target)
|
23
|
+
|
24
|
+
env.delete "proxy.start"
|
25
|
+
env["proxy.callback"] ||= method(:callback)
|
26
|
+
|
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
|
44
|
+
|
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
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(env, target)
|
55
|
+
response = Client.request(env["rity.request"])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "hatetepe/message"
|
2
|
+
|
3
|
+
module Hatetepe
|
4
|
+
class Request < Message
|
5
|
+
include EM::Deferrable
|
6
|
+
|
7
|
+
attr_accessor :verb, :uri, :response
|
8
|
+
|
9
|
+
def initialize(verb, uri, http_version = "1.1")
|
10
|
+
@verb, @uri = verb, uri
|
11
|
+
super http_version
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
{
|
16
|
+
"rack.version" => [1, 0],
|
17
|
+
"hatetepe.request" => self,
|
18
|
+
"rack.input" => body,
|
19
|
+
"REQUEST_METHOD" => verb.dup,
|
20
|
+
"REQUEST_URI" => uri.dup
|
21
|
+
}.tap {|h|
|
22
|
+
headers.each {|key, value|
|
23
|
+
h["HTTP_#{key.upcase.gsub(/[^A-Z_]/, "_")}"] = value
|
24
|
+
}
|
25
|
+
|
26
|
+
h["REQUEST_PATH"], qm, h["QUERY_STRING"] = uri.partition("?")
|
27
|
+
h["PATH_INFO"], h["SCRIPT_NAME"] = h["REQUEST_PATH"].dup, ""
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "hatetepe/message"
|
2
|
+
|
3
|
+
module Hatetepe
|
4
|
+
class Response < Message
|
5
|
+
attr_accessor :status
|
6
|
+
|
7
|
+
def initialize(status, http_version = "1.1")
|
8
|
+
@status = status
|
9
|
+
super http_version
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_a
|
13
|
+
[status, headers, body]
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](i)
|
17
|
+
to_a[i]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "em-synchrony"
|
3
|
+
require "rack"
|
4
|
+
|
5
|
+
require "hatetepe/app"
|
6
|
+
require "hatetepe/builder"
|
7
|
+
require "hatetepe/parser"
|
8
|
+
require "hatetepe/request"
|
9
|
+
|
10
|
+
module Hatetepe
|
11
|
+
class Server < EM::Connection
|
12
|
+
def self.start(config)
|
13
|
+
server = EM.start_server(config[:host], config[:port], self, config)
|
14
|
+
#Prefork.run server if config[:prefork]
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :app, :config, :errors
|
18
|
+
attr_reader :requests, :parser, :builder
|
19
|
+
|
20
|
+
def initialize(config)
|
21
|
+
@config = config
|
22
|
+
@errors = config[:errors] || $stderr
|
23
|
+
|
24
|
+
@app = Rack::Builder.new.tap {|b|
|
25
|
+
b.use Hatetepe::App
|
26
|
+
#b.use Hatetepe::Proxy
|
27
|
+
b.run config[:app]
|
28
|
+
}
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def post_init
|
34
|
+
@requests = []
|
35
|
+
@parser, @builder = Parser.new, Builder.new
|
36
|
+
|
37
|
+
parser.on_request << requests.method(:<<)
|
38
|
+
parser.on_headers << method(:process)
|
39
|
+
|
40
|
+
builder.on_write << method(:send_data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def receive_data(data)
|
44
|
+
parser << data
|
45
|
+
rescue ParserError
|
46
|
+
close_connection
|
47
|
+
rescue Exception => ex
|
48
|
+
close_connection_after_writing
|
49
|
+
backtrace = ex.backtrace.map {|line| "\t#{line}" }.join("\n")
|
50
|
+
errors << "#{ex.class}: #{ex.message}\n#{backtrace}\n"
|
51
|
+
errors.flush
|
52
|
+
end
|
53
|
+
|
54
|
+
def process(*)
|
55
|
+
previous, request = requests.values_at(-2, -1)
|
56
|
+
|
57
|
+
env = request.to_hash.tap {|e|
|
58
|
+
e["hatetepe.connection"] = self
|
59
|
+
e["rack.url_scheme"] = "http"
|
60
|
+
e["rack.input"].source = self
|
61
|
+
e["rack.errors"] = errors
|
62
|
+
|
63
|
+
e["rack.multithread"] = false
|
64
|
+
e["rack.multiprocess"] = false
|
65
|
+
e["rack.run_once"] = false
|
66
|
+
|
67
|
+
e["SERVER_NAME"] = config[:host].dup
|
68
|
+
e["SERVER_PORT"] = String(config[:port])
|
69
|
+
|
70
|
+
host = e["HTTP_HOST"] || config[:host].dup
|
71
|
+
host += ":#{config[:port]}" unless host.include? ":"
|
72
|
+
e["HTTP_HOST"] = host
|
73
|
+
|
74
|
+
e["stream.start"] = proc {|response|
|
75
|
+
e.delete "stream.start"
|
76
|
+
EM::Synchrony.sync previous if previous
|
77
|
+
response[1]["Server"] = "hatetepe/#{VERSION}"
|
78
|
+
builder.response response[0..1]
|
79
|
+
}
|
80
|
+
|
81
|
+
e["stream.send"] = builder.method(:body)
|
82
|
+
|
83
|
+
e["stream.close"] = proc {
|
84
|
+
e.delete "stream.send"
|
85
|
+
e.delete "stream.close"
|
86
|
+
|
87
|
+
builder.complete
|
88
|
+
requests.delete request
|
89
|
+
request.succeed
|
90
|
+
|
91
|
+
close_connection_after_writing if requests.empty?
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
Fiber.new { app.call env }.resume
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/hatetepe/version.rb
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "hatetepe/server"
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Handler
|
6
|
+
class Hatetepe
|
7
|
+
def self.run(app, options = {})
|
8
|
+
options = {
|
9
|
+
:host => options[:Host] || "0.0.0.0",
|
10
|
+
:port => options[:Port] || 8080,
|
11
|
+
:app => app
|
12
|
+
}
|
13
|
+
|
14
|
+
Signal.trap("INT") { EM.stop }
|
15
|
+
Signal.trap("TERM") { EM.stop }
|
16
|
+
|
17
|
+
EM.run {
|
18
|
+
EM.epoll
|
19
|
+
|
20
|
+
server = ::Hatetepe::Server.start options
|
21
|
+
yield server if block_given?
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.valid_options
|
26
|
+
{
|
27
|
+
"Host=HOST" => "Hostname to listen on (default: 0.0.0.0 / all interfaces)",
|
28
|
+
"Port=PORT" => "Port to listen on (default: 8080)",
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "hatetepe/cli"
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
describe "start command" do
|
6
|
+
def hook_event_loop(&block)
|
7
|
+
EM.spec_hooks << block
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_stop_timer(timeout)
|
11
|
+
hook_event_loop do
|
12
|
+
EM.add_timer(timeout) { EM.stop }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
before do
|
17
|
+
$stderr = StringIO.new ""
|
18
|
+
|
19
|
+
FakeFS.activate!
|
20
|
+
File.open("config.ru", "w") do |f|
|
21
|
+
f.write %q{run proc {|e| [200, {"Content-Type" => "text/plain"}, [e["REQUEST_URI"]]] }}
|
22
|
+
end
|
23
|
+
File.open("config2.ru", "w") do |f|
|
24
|
+
f.write %q{run proc {|e| [200, {"Content-Type" => "text/plain"}, ["config2.ru loaded"]] }}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
after do
|
29
|
+
$stderr = STDERR
|
30
|
+
|
31
|
+
FakeFS.deactivate!
|
32
|
+
FakeFS::FileSystem.clear
|
33
|
+
end
|
34
|
+
|
35
|
+
it "starts an instance of Rity" do
|
36
|
+
add_stop_timer 0.05
|
37
|
+
hook_event_loop do
|
38
|
+
Socket.tcp("127.0.0.1", 3000) {|*| }
|
39
|
+
end
|
40
|
+
Hatetepe::CLI.start %w{}
|
41
|
+
|
42
|
+
$stderr.string.should include("127.0.0.1:3000")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "answers HTTP requests" do
|
46
|
+
add_stop_timer 0.02
|
47
|
+
hook_event_loop do
|
48
|
+
request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
|
49
|
+
response = EM::Synchrony.sync(request)
|
50
|
+
|
51
|
+
response.response_header.status.should == 200
|
52
|
+
response.response_header["CONTENT_TYPE"].should == "text/plain"
|
53
|
+
response.response.should == "/"
|
54
|
+
end
|
55
|
+
Hatetepe::CLI.start %w{}
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "--port option" do
|
59
|
+
it "changes the listen port" do
|
60
|
+
add_stop_timer 0.05
|
61
|
+
hook_event_loop do
|
62
|
+
Socket.tcp("127.0.0.1", 3001) {|*| }
|
63
|
+
end
|
64
|
+
Hatetepe::CLI.start %w{--port=3001}
|
65
|
+
|
66
|
+
$stderr.string.should include(":3001")
|
67
|
+
end
|
68
|
+
|
69
|
+
it "has an alias: -p" do
|
70
|
+
add_stop_timer 0.05
|
71
|
+
hook_event_loop do
|
72
|
+
Socket.tcp("127.0.0.1", 3002) {|*| }
|
73
|
+
end
|
74
|
+
Hatetepe::CLI.start %w{-p 3002}
|
75
|
+
|
76
|
+
$stderr.string.should include(":3002")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "--bind option" do
|
81
|
+
it "changes the listen interface" do
|
82
|
+
add_stop_timer 0.05
|
83
|
+
hook_event_loop do
|
84
|
+
Socket.tcp("127.0.0.2", 3000) {|*| }
|
85
|
+
end
|
86
|
+
Hatetepe::CLI.start %w{--bind=127.0.0.2}
|
87
|
+
|
88
|
+
$stderr.string.should include("127.0.0.2:")
|
89
|
+
end
|
90
|
+
|
91
|
+
it "has an alias: -b" do
|
92
|
+
add_stop_timer 0.05
|
93
|
+
hook_event_loop do
|
94
|
+
Socket.tcp("127.0.0.3", 3000) {|*| }
|
95
|
+
end
|
96
|
+
Hatetepe::CLI.start %w{-b 127.0.0.3}
|
97
|
+
|
98
|
+
$stderr.string.should include("127.0.0.3:")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "--rackup option" do
|
103
|
+
it "changes the rackup file that'll be loaded" do
|
104
|
+
add_stop_timer 0.05
|
105
|
+
hook_event_loop do
|
106
|
+
request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
|
107
|
+
response = EM::Synchrony.sync(request)
|
108
|
+
response.response.should include("config2.ru")
|
109
|
+
end
|
110
|
+
Hatetepe::CLI.start %w{--rackup=config2.ru}
|
111
|
+
end
|
112
|
+
|
113
|
+
it "has an alias: -r" do
|
114
|
+
add_stop_timer 0.05
|
115
|
+
hook_event_loop do
|
116
|
+
request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
|
117
|
+
response = EM::Synchrony.sync(request)
|
118
|
+
response.response.should include("config2.ru")
|
119
|
+
end
|
120
|
+
Hatetepe::CLI.start %w{-r config2.ru}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "--quiet option" do
|
125
|
+
it "discards all output" do
|
126
|
+
pending
|
127
|
+
|
128
|
+
add_stop_timer 0.05
|
129
|
+
Hatetepe::CLI.start %w{--quiet}
|
130
|
+
|
131
|
+
$stderr.string.should be_empty
|
132
|
+
end
|
133
|
+
|
134
|
+
it "has an alias: -q" do
|
135
|
+
pending
|
136
|
+
|
137
|
+
add_stop_timer 0.05
|
138
|
+
Hatetepe::CLI.start %w{-q}
|
139
|
+
|
140
|
+
$stderr.string.should be_empty
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "--verbose option" do
|
145
|
+
it "prints debugging data" do
|
146
|
+
pending
|
147
|
+
|
148
|
+
add_stop_timer 0.05
|
149
|
+
hook_event_loop do
|
150
|
+
request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
|
151
|
+
end
|
152
|
+
Hatetepe::CLI.start %w{--verbose}
|
153
|
+
|
154
|
+
$stderr.string.split("\n").size.should > 10
|
155
|
+
end
|
156
|
+
|
157
|
+
it "has an alias: -V" do
|
158
|
+
pending
|
159
|
+
|
160
|
+
add_stop_timer 0.05
|
161
|
+
hook_event_loop do
|
162
|
+
request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
|
163
|
+
end
|
164
|
+
Hatetepe::CLI.start %w{-V}
|
165
|
+
|
166
|
+
$stderr.string.split("\n").size.should > 10
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|