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 ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,3 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
data/README.md CHANGED
@@ -1,13 +1,29 @@
1
- Builds and parses HTTP messages
2
- ===============================
1
+ The HTTP toolkit
2
+ ================
3
3
 
4
- Hatetepe combines its own builder with http_parser.rb to make dealing with HTTP
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
- require "bundler"
2
- Bundler.setup :default
1
+ task :default => :spec
3
2
 
4
- task :default => :test
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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "hatetepe/cli"
4
+ Hatetepe::CLI.start
@@ -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{Builds and parses HTTP messages}
13
- s.description = %q{Hatetepe combines its own builder with http_parser.rb to make dealing with HTTP messages as comfortable as possible.}
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 "test-unit"
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")
@@ -1,3 +1,8 @@
1
1
  require "hatetepe/builder"
2
+ require "hatetepe/client"
2
3
  require "hatetepe/parser"
4
+ require "hatetepe/proxy"
5
+ require "hatetepe/request"
6
+ require "hatetepe/response"
7
+ require "hatetepe/server"
3
8
  require "hatetepe/version"
@@ -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
@@ -29,7 +29,7 @@ module Hatetepe
29
29
 
30
30
  def reset
31
31
  @state = :ready
32
- @chunked = true
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
- header "Transfer-Encoding", "chunked" if chunked?
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.length.to_s(16)}\r\n#{chunk}\r\n"
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
@@ -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