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/.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
|