hatetepe 0.0.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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
|