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