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/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,13 +1,29 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
The HTTP toolkit
|
2
|
+
================
|
3
3
|
|
4
|
-
|
5
|
-
messages as comfortable as possible.
|
4
|
+
Documentation is asking why you don't write it.
|
6
5
|
|
7
6
|
TODO
|
8
7
|
----
|
9
8
|
|
9
|
+
- Proxy
|
10
|
+
- Code reloading
|
11
|
+
- Client
|
12
|
+
- Keep-alive
|
13
|
+
- Preforking
|
14
|
+
- Native file sending/receiving
|
15
|
+
- MVM support via Thread Pool
|
16
|
+
- Support for SPDY
|
17
|
+
- Serving via filesystem or in-memory
|
18
|
+
- Foreman support
|
19
|
+
- Daemonizing and dropping privileges
|
20
|
+
- Trailing headers
|
21
|
+
- Propagating connection errors to the app
|
22
|
+
|
23
|
+
Things to check out
|
24
|
+
-------------------
|
25
|
+
|
10
26
|
- Fix http_parser.rb's parsing of chunked bodies
|
11
27
|
- Does http_parser.rb recognize trailing headers?
|
12
|
-
- Support for pausing and resuming parsing/building
|
13
28
|
- Encoding support (see https://github.com/tmm1/http_parser.rb/pull/1)
|
29
|
+
- Are there any good C libs for building HTTP messages?
|
data/Rakefile
CHANGED
@@ -1,11 +1,4 @@
|
|
1
|
-
|
2
|
-
Bundler.setup :default
|
1
|
+
task :default => :spec
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
require "rake/testtask"
|
7
|
-
Rake::TestTask.new :test do |t|
|
8
|
-
t.test_files = FileList["test/*_test.rb"]
|
9
|
-
end
|
10
|
-
|
11
|
-
Bundler::GemHelper.install_tasks
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
RSpec::Core::RakeTask.new :spec
|
data/bin/hatetepe
ADDED
data/hatetepe.gemspec
CHANGED
@@ -9,12 +9,19 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.authors = ["Lars Gierth"]
|
10
10
|
s.email = ["lars.gierth@gmail.com"]
|
11
11
|
s.homepage = "https://github.com/lgierth/hatetepe"
|
12
|
-
s.summary = %q{
|
13
|
-
s.description = %q{
|
12
|
+
s.summary = %q{The HTTP toolkit}
|
13
|
+
#s.description = %q{TODO: write description}
|
14
14
|
|
15
15
|
s.add_dependency "http_parser.rb"
|
16
|
+
s.add_dependency "eventmachine"
|
17
|
+
s.add_dependency "em-synchrony"
|
18
|
+
s.add_dependency "rack"
|
19
|
+
s.add_dependency "async-rack"
|
20
|
+
s.add_dependency "thor"
|
16
21
|
|
17
|
-
s.add_development_dependency "
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "fakefs"
|
24
|
+
s.add_development_dependency "em-http-request"
|
18
25
|
|
19
26
|
s.files = `git ls-files`.split("\n") - [".gitignore"]
|
20
27
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
data/lib/hatetepe.rb
CHANGED
data/lib/hatetepe/app.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require "async-rack"
|
2
|
+
require "rack"
|
3
|
+
|
4
|
+
Rack::STREAMING = "Rack::STREAMING"
|
5
|
+
|
6
|
+
module Hatetepe
|
7
|
+
ASYNC_RESPONSE = [-1, {}, []].freeze
|
8
|
+
|
9
|
+
ERROR_RESPONSE = [500, {"Content-Type" => "text/html"},
|
10
|
+
["Internal Server Error"]].freeze
|
11
|
+
|
12
|
+
class App
|
13
|
+
attr_reader :app
|
14
|
+
|
15
|
+
def initialize(app)
|
16
|
+
@app = app
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
env["async.callback"] = proc {|response|
|
21
|
+
postprocess env, response
|
22
|
+
}
|
23
|
+
|
24
|
+
response = ASYNC_RESPONSE
|
25
|
+
catch(:async) {
|
26
|
+
response = app.call(env) rescue ERROR_RESPONSE
|
27
|
+
}
|
28
|
+
postprocess env, response
|
29
|
+
end
|
30
|
+
|
31
|
+
def postprocess(env, response)
|
32
|
+
return if response[0] == ASYNC_RESPONSE[0]
|
33
|
+
|
34
|
+
env["stream.start"].call response[0..1]
|
35
|
+
return if response[2] == Rack::STREAMING
|
36
|
+
|
37
|
+
begin
|
38
|
+
response[2].each {|chunk| env["stream.send"].call chunk }
|
39
|
+
ensure
|
40
|
+
env["stream.close"].call
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "em-synchrony"
|
2
|
+
require "eventmachine"
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
module Hatetepe
|
6
|
+
class Body
|
7
|
+
include EM::Deferrable
|
8
|
+
|
9
|
+
attr_reader :io
|
10
|
+
attr_accessor :source
|
11
|
+
|
12
|
+
def initialize(string = "")
|
13
|
+
@receivers = []
|
14
|
+
@io = StringIO.new(string)
|
15
|
+
end
|
16
|
+
|
17
|
+
def sync
|
18
|
+
source.resume if source && source.paused?
|
19
|
+
EM::Synchrony.sync self
|
20
|
+
end
|
21
|
+
|
22
|
+
def length
|
23
|
+
# TODO maybe I want to #sync here
|
24
|
+
@io.length
|
25
|
+
end
|
26
|
+
|
27
|
+
def empty?
|
28
|
+
length == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def pos
|
32
|
+
@io.pos
|
33
|
+
end
|
34
|
+
|
35
|
+
def rewind
|
36
|
+
@io.rewind
|
37
|
+
end
|
38
|
+
|
39
|
+
def close_write
|
40
|
+
ret = @io.close_write
|
41
|
+
succeed
|
42
|
+
ret
|
43
|
+
end
|
44
|
+
|
45
|
+
def closed_write?
|
46
|
+
@io.closed_write?
|
47
|
+
end
|
48
|
+
|
49
|
+
def each(&block)
|
50
|
+
@receivers << block
|
51
|
+
block.call @io.string.dup unless @io.string.empty?
|
52
|
+
sync
|
53
|
+
end
|
54
|
+
|
55
|
+
def read(*args)
|
56
|
+
sync
|
57
|
+
rewind
|
58
|
+
@io.read *args
|
59
|
+
end
|
60
|
+
|
61
|
+
def gets
|
62
|
+
sync
|
63
|
+
rewind
|
64
|
+
@io.gets
|
65
|
+
end
|
66
|
+
|
67
|
+
def write(chunk)
|
68
|
+
ret = @io.write chunk
|
69
|
+
@receivers.each {|r| r.call chunk }
|
70
|
+
ret
|
71
|
+
end
|
72
|
+
|
73
|
+
def <<(chunk)
|
74
|
+
ret = @io << chunk
|
75
|
+
@receivers.each {|r| r.call chunk }
|
76
|
+
ret
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/hatetepe/builder.rb
CHANGED
@@ -29,7 +29,7 @@ module Hatetepe
|
|
29
29
|
|
30
30
|
def reset
|
31
31
|
@state = :ready
|
32
|
-
@chunked =
|
32
|
+
@chunked = nil
|
33
33
|
end
|
34
34
|
|
35
35
|
[:write, :complete, :error].each do |hook|
|
@@ -90,6 +90,12 @@ module Hatetepe
|
|
90
90
|
raw_header "#{name}: #{value}#{charset}"
|
91
91
|
end
|
92
92
|
|
93
|
+
def headers(hash)
|
94
|
+
# wrong number of arguments (1 for 2)
|
95
|
+
#hash.each_pair &method(:header)
|
96
|
+
hash.each_pair {|name, value| header name, value }
|
97
|
+
end
|
98
|
+
|
93
99
|
def raw_header(header)
|
94
100
|
if ready?
|
95
101
|
error "A request or response line is required before writing headers"
|
@@ -101,24 +107,33 @@ module Hatetepe
|
|
101
107
|
|
102
108
|
if header[0..13] == "Content-Length"
|
103
109
|
@chunked = false
|
110
|
+
elsif header[0..16] == "Transfer-Encoding"
|
111
|
+
@chunked = true
|
104
112
|
end
|
105
113
|
|
106
114
|
write "#{header}\r\n"
|
107
115
|
end
|
108
116
|
|
109
117
|
def body(chunk)
|
118
|
+
if Body === chunk
|
119
|
+
chunk.each &method(:body)
|
120
|
+
return
|
121
|
+
end
|
122
|
+
|
110
123
|
if ready?
|
111
124
|
error "A request or response line and headers are required before writing body"
|
112
125
|
elsif writing_trailing_headers?
|
113
126
|
error "Cannot write body after trailing headers"
|
114
127
|
elsif writing_headers?
|
115
|
-
|
128
|
+
if @chunked.nil?
|
129
|
+
header "Transfer-Encoding", "chunked"
|
130
|
+
end
|
116
131
|
write "\r\n"
|
117
132
|
@state = :writing_body
|
118
133
|
end
|
119
134
|
|
120
135
|
if chunked?
|
121
|
-
write "#{chunk.
|
136
|
+
write "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
122
137
|
else
|
123
138
|
write chunk
|
124
139
|
end
|
@@ -129,6 +144,7 @@ module Hatetepe
|
|
129
144
|
|
130
145
|
if writing_body? && chunked?
|
131
146
|
write "0\r\n"
|
147
|
+
write "\r\n"
|
132
148
|
elsif writing_headers? || writing_trailing_headers?
|
133
149
|
write "\r\n"
|
134
150
|
end
|
data/lib/hatetepe/cli.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
require "hatetepe"
|
4
|
+
|
5
|
+
module Hatetepe
|
6
|
+
class CLI < Thor
|
7
|
+
map "--version" => :version
|
8
|
+
map "-v" => :version
|
9
|
+
|
10
|
+
default_task :start
|
11
|
+
|
12
|
+
desc :version, "Print version information"
|
13
|
+
def version
|
14
|
+
say Rity::VERSION
|
15
|
+
end
|
16
|
+
|
17
|
+
desc :start, "Start an instance of Rity"
|
18
|
+
method_option :bind, :aliases => "-b", :type => :string,
|
19
|
+
:banner => "Bind to the specified TCP interface (default: 127.0.0.1)"
|
20
|
+
method_option :port, :aliases => "-p", :type => :numeric,
|
21
|
+
:banner => "Bind to the specified port (default: 3000)"
|
22
|
+
method_option :rackup, :aliases => "-r", :type => :string,
|
23
|
+
:banner => "Load specified rackup (.ru) file (default: config.ru)"
|
24
|
+
def start
|
25
|
+
rackup = options[:rackup] || "config.ru"
|
26
|
+
$stderr << "Booting from #{File.expand_path rackup}\n"
|
27
|
+
$stderr.flush
|
28
|
+
app = Rack::Builder.parse_file(rackup)[0]
|
29
|
+
|
30
|
+
EM.synchrony do
|
31
|
+
trap("INT") { EM.stop }
|
32
|
+
trap("TERM") { EM.stop }
|
33
|
+
|
34
|
+
EM.epoll
|
35
|
+
|
36
|
+
host = options[:bind] || "127.0.0.1"
|
37
|
+
port = options[:port] || 3000
|
38
|
+
|
39
|
+
$stderr << "Binding to #{host}:#{port}\n"
|
40
|
+
$stderr.flush
|
41
|
+
Server.start({
|
42
|
+
:app => app,
|
43
|
+
:errors => $stderr,
|
44
|
+
:host => host,
|
45
|
+
:port => port
|
46
|
+
})
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "em-synchrony"
|
2
|
+
require "eventmachine"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
require "hatetepe/body"
|
6
|
+
require "hatetepe/builder"
|
7
|
+
require "hatetepe/parser"
|
8
|
+
require "hatetepe/request"
|
9
|
+
require "hatetepe/response"
|
10
|
+
|
11
|
+
module Hatetepe
|
12
|
+
class Client < EM::Connection
|
13
|
+
def self.start(config)
|
14
|
+
EM.connect config[:host], config[:port], self, config
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.request(verb, uri, headers = {}, body = nil)
|
18
|
+
uri = URI.parse(uri)
|
19
|
+
client = start(:host => uri.host, :port => uri.port)
|
20
|
+
|
21
|
+
headers["User-Agent"] ||= "hatetepe/#{VERSION}"
|
22
|
+
|
23
|
+
EM::Synchrony.sync Request.new(verb, uri.request_uri).tap {|req|
|
24
|
+
req.headers = headers
|
25
|
+
req.body = body || Body.new.tap {|b| b.close_write }
|
26
|
+
client << req
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
[:get, :head].each {|verb|
|
32
|
+
define_method(verb) {|uri, headers = {}|
|
33
|
+
request verb.to_s.upcase, uri, headers
|
34
|
+
}
|
35
|
+
}
|
36
|
+
[:options, :post, :put, :delete, :trace, :connect].each {|verb|
|
37
|
+
define_method(verb) {|uri, headers = {}, body = nil|
|
38
|
+
request verb.to_s.upcase, uri, headers, body
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :config
|
44
|
+
attr_reader :requests, :parser, :builder
|
45
|
+
|
46
|
+
def initialize(config)
|
47
|
+
@config = config
|
48
|
+
@requests = []
|
49
|
+
@parser, @builder = Parser.new, Builder.new
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
def post_init
|
54
|
+
parser.on_response {|response|
|
55
|
+
requests.find {|req| !req.response }.response = response
|
56
|
+
}
|
57
|
+
|
58
|
+
parser.on_headers {
|
59
|
+
requests.reverse.find {|req| !!req.response }.tap {|req|
|
60
|
+
req.succeed req.response
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
#builder.on_write {|chunk|
|
65
|
+
# ap "-> #{chunk}"
|
66
|
+
#}
|
67
|
+
builder.on_write << method(:send_data)
|
68
|
+
end
|
69
|
+
|
70
|
+
def <<(request)
|
71
|
+
request.headers["Host"] = "#{config[:host]}:#{config[:port]}"
|
72
|
+
|
73
|
+
requests << request
|
74
|
+
Fiber.new {
|
75
|
+
builder.request request.verb, request.uri
|
76
|
+
builder.headers request.headers
|
77
|
+
builder.body request.body unless request.body.empty?
|
78
|
+
builder.complete
|
79
|
+
}.resume
|
80
|
+
end
|
81
|
+
|
82
|
+
def receive_data(data)
|
83
|
+
#ap "<- #{data}"
|
84
|
+
parser << data
|
85
|
+
end
|
86
|
+
|
87
|
+
def stop
|
88
|
+
close_connection_after_writing
|
89
|
+
end
|
90
|
+
|
91
|
+
def responses
|
92
|
+
requests.map(&:response).compact
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Hatetepe
|
2
|
+
module Events
|
3
|
+
def self.included(klass)
|
4
|
+
klass.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :state
|
8
|
+
|
9
|
+
def event(name, *args)
|
10
|
+
send(:"on_#{name}").each {|blk| blk.call *args }
|
11
|
+
end
|
12
|
+
|
13
|
+
def event!(name, *args)
|
14
|
+
@state = name
|
15
|
+
event(name, *args)
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def event(name, *more_names)
|
20
|
+
define_method(:"on_#{name}") {|&block|
|
21
|
+
ivar = :"@on_#{name}"
|
22
|
+
store = instance_variable_get(ivar)
|
23
|
+
store ||= instance_variable_set(ivar, [])
|
24
|
+
|
25
|
+
return store unless block
|
26
|
+
store << block
|
27
|
+
}
|
28
|
+
|
29
|
+
define_method(:"#{name}?") { state == name }
|
30
|
+
|
31
|
+
more_names.each {|n| event n }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|