hatetepe 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem "rake"
6
+ gem "awesome_print"
@@ -0,0 +1,13 @@
1
+ Builds and parses HTTP messages
2
+ ===============================
3
+
4
+ Hatetepe combines its own builder with http_parser.rb to make dealing with HTTP
5
+ messages as comfortable as possible.
6
+
7
+ TODO
8
+ ----
9
+
10
+ - Fix http_parser.rb's parsing of chunked bodies
11
+ - Does http_parser.rb recognize trailing headers?
12
+ - Support for pausing and resuming parsing/building
13
+ - Encoding support (see https://github.com/tmm1/http_parser.rb/pull/1)
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ Bundler.setup :default
3
+
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
@@ -0,0 +1,29 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ require "hatetepe"
5
+ require "awesome_print"
6
+
7
+ Hatetepe::Parser.parse do
8
+ [:request, :response, :header, :body_chunk, :complete, :error].each do |hook|
9
+ send :"on_#{hook}" do |*args|
10
+ puts "on_#{hook}: #{args.inspect}"
11
+ end
12
+ end
13
+
14
+ self << "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHallo Welt!"
15
+ end
16
+
17
+ Hatetepe::Builder.build do
18
+ on_write {|data| p data }
19
+ on_complete {|bytes_written| puts "Wrote #{bytes_written} bytes" }
20
+
21
+ response 200
22
+ header "Content-Type", "text/html", "utf-8"
23
+ raw_header "Content-Length: 25"
24
+ body "<p>Hallo Welt!</p>"
25
+
26
+ response 201
27
+ header "Location", "/new_entity"
28
+ complete
29
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "hatetepe/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "hatetepe"
7
+ s.version = Hatetepe::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Lars Gierth"]
10
+ s.email = ["lars.gierth@gmail.com"]
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.}
14
+
15
+ s.add_dependency "http_parser.rb"
16
+
17
+ s.add_development_dependency "test-unit"
18
+
19
+ s.files = `git ls-files`.split("\n") - [".gitignore"]
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ end
@@ -0,0 +1,3 @@
1
+ require "hatetepe/builder"
2
+ require "hatetepe/parser"
3
+ require "hatetepe/version"
@@ -0,0 +1,142 @@
1
+ require "hatetepe/status"
2
+
3
+ module Hatetepe
4
+ class BuilderError < StandardError; end
5
+
6
+ class Builder
7
+ def self.build(&block)
8
+ message = ""
9
+ builder = new do |b|
10
+ b.on_write {|data| message << data }
11
+ end
12
+
13
+ block.arity == 0 ? builder.instance_eval(&block) : block.call(builder)
14
+
15
+ builder.complete
16
+ return message.empty? ? nil : message
17
+ end
18
+
19
+ attr_reader :state, :bytes_written
20
+
21
+ def initialize(&block)
22
+ reset
23
+ @on_write, @on_complete, @on_error = [], [], []
24
+
25
+ if block
26
+ block.arity == 0 ? instance_eval(&block) : block.call(self)
27
+ end
28
+ end
29
+
30
+ def reset
31
+ @state = :ready
32
+ @chunked = true
33
+ @bytes_written = 0
34
+ end
35
+
36
+ [:write, :complete, :error].each do |hook|
37
+ define_method :"on_#{hook}" do |&block|
38
+ store = instance_variable_get(:"@on_#{hook}")
39
+ return store unless block
40
+ store << block
41
+ end
42
+ end
43
+
44
+ def ready?
45
+ state == :ready
46
+ end
47
+
48
+ def writing_headers?
49
+ state == :writing_headers
50
+ end
51
+
52
+ def writing_body?
53
+ state == :writing_body
54
+ end
55
+
56
+ def writing_trailing_headers?
57
+ state == :writing_trailing_headers
58
+ end
59
+
60
+ def chunked?
61
+ @chunked
62
+ end
63
+
64
+ def request(verb, uri, version = "1.1")
65
+ complete unless ready?
66
+ write "#{verb.upcase} #{uri} HTTP/#{version}\r\n"
67
+ @state = :writing_headers
68
+ end
69
+
70
+ def response(code, version = "1.1")
71
+ complete unless ready?
72
+ unless status = STATUS_CODES[code]
73
+ error "Unknown status code: #{code}"
74
+ end
75
+ write "HTTP/#{version} #{code} #{status}\r\n"
76
+ @state = :writing_headers
77
+ end
78
+
79
+ def header(name, value, charset = nil)
80
+ charset = charset ? "; charset=#{charset}" : ""
81
+ raw_header "#{name}: #{value}#{charset}"
82
+ end
83
+
84
+ def raw_header(header)
85
+ if ready?
86
+ error "A request or response line is required before writing headers"
87
+ elsif writing_body?
88
+ error "Trailing headers require chunked transfer encoding" unless chunked?
89
+ write "0\r\n"
90
+ @state = :writing_trailing_headers
91
+ end
92
+
93
+ if header[0..13] == "Content-Length"
94
+ @chunked = false
95
+ end
96
+
97
+ write "#{header}\r\n"
98
+ end
99
+
100
+ def body(chunk)
101
+ if ready?
102
+ error "A request or response line and headers are required before writing body"
103
+ elsif writing_trailing_headers?
104
+ error "Cannot write body after trailing headers"
105
+ elsif writing_headers?
106
+ write "\r\n"
107
+ @state = :writing_body
108
+ end
109
+
110
+ if chunked?
111
+ write "#{chunk.length.to_s(16)}\r\n#{chunk}\r\n"
112
+ else
113
+ write chunk
114
+ end
115
+ end
116
+
117
+ def complete
118
+ return if ready?
119
+
120
+ if writing_body? && chunked?
121
+ write "0\r\n\r\n"
122
+ elsif writing_headers? || writing_trailing_headers?
123
+ write "\r\n"
124
+ end
125
+
126
+ on_complete.each {|blk| blk.call(bytes_written) }
127
+ reset
128
+ end
129
+
130
+ def write(chunk)
131
+ @bytes_written += chunk.length
132
+ on_write.each {|blk| blk.call(chunk) }
133
+ end
134
+
135
+ def error(message)
136
+ exception = BuilderError.new(message)
137
+ exception.set_backtrace(caller[1..-1])
138
+ on_error.each {|blk| blk.call(exception) }
139
+ raise(exception)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,96 @@
1
+ require "http/parser"
2
+
3
+ module Hatetepe
4
+ class ParserError < StandardError; end
5
+
6
+ class Parser
7
+ def self.parse(data = [], &block)
8
+ message = {}
9
+ parser = new do |p|
10
+ p.on_request do |*args|
11
+ message[:http_method] = args[0]
12
+ message[:request_url] = args[1]
13
+ message[:http_version] = args[2]
14
+ end
15
+ p.on_response do |*args|
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
34
+
35
+ attr_reader :bytes_read
36
+
37
+ def initialize(&block)
38
+ @on_request, @on_response, @on_header = [], [], []
39
+ @on_body_chunk, @on_complete, @on_error = [], [], []
40
+ @parser = HTTP::Parser.new
41
+
42
+ @parser.on_headers_complete = proc do
43
+ if @parser.http_method
44
+ on_request.each do |r|
45
+ r.call(@parser.http_method, @parser.request_url, @parser.http_version.join("."))
46
+ end
47
+ else
48
+ on_response.each do |r|
49
+ r.call(@parser.status_code, @parser.http_version.join("."))
50
+ end
51
+ end
52
+
53
+ @parser.headers.each do |header|
54
+ on_header.each {|h| h.call(*header) }
55
+ end
56
+ end
57
+
58
+ @parser.on_body = proc do |chunk|
59
+ on_body_chunk.each {|b| b.call(chunk) }
60
+ end
61
+
62
+ @parser.on_message_complete = proc do
63
+ on_complete.each {|f| f.call(bytes_read) }
64
+ end
65
+
66
+ reset
67
+
68
+ if block
69
+ block.arity == 0 ? instance_eval(&block) : block.call(self)
70
+ end
71
+ end
72
+
73
+ def reset
74
+ @parser.reset!
75
+ @bytes_read = 0
76
+ end
77
+
78
+ [:request, :response, :header, :body_chunk, :complete, :error].each do |hook|
79
+ define_method :"on_#{hook}" do |&block|
80
+ store = instance_variable_get(:"@on_#{hook}")
81
+ return store unless block
82
+ store << block
83
+ end
84
+ end
85
+
86
+ def <<(data)
87
+ @bytes_read += data.length
88
+ @parser << data
89
+ rescue HTTP::Parser::Error => original_error
90
+ error = ParserError.new(original_error.message)
91
+ error.set_backtrace(original_error.backtrace)
92
+ on_error.each {|e| e.call(error) }
93
+ raise(error)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,42 @@
1
+ module Hatetepe
2
+ # @author Mongrel
3
+ STATUS_CODES = {
4
+ 100 => "Continue",
5
+ 101 => "Switching Protocols",
6
+ 200 => "OK",
7
+ 201 => "Created",
8
+ 202 => "Accepted",
9
+ 203 => "Non-Authoritative Information",
10
+ 204 => "No Content",
11
+ 205 => "Reset Content",
12
+ 206 => "Partial Content",
13
+ 300 => "Multiple Choices",
14
+ 301 => "Moved Permanently",
15
+ 302 => "Moved Temporarily",
16
+ 303 => "See Other",
17
+ 304 => "Not Modified",
18
+ 305 => "Use Proxy",
19
+ 400 => "Bad Request",
20
+ 401 => "Unauthorized",
21
+ 402 => "Payment Required",
22
+ 403 => "Forbidden",
23
+ 404 => "Not Found",
24
+ 405 => "Method Not Allowed",
25
+ 406 => "Not Acceptable",
26
+ 407 => "Proxy Authentication Required",
27
+ 408 => "Request Time-out",
28
+ 409 => "Conflict",
29
+ 410 => "Gone",
30
+ 411 => "Length Required",
31
+ 412 => "Precondition Failed",
32
+ 413 => "Request Entity Too Large",
33
+ 414 => "Request-URI Too Large",
34
+ 415 => "Unsupported Media Type",
35
+ 500 => "Internal Server Error",
36
+ 501 => "Not Implemented",
37
+ 502 => "Bad Gateway",
38
+ 503 => "Service Unavailable",
39
+ 504 => "Gateway Time-out",
40
+ 505 => "HTTP Version Not Supported"
41
+ }
42
+ end
@@ -0,0 +1,3 @@
1
+ module Hatetepe
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path("..", __FILE__) + "/test_helper"
2
+
3
+ class BuilderTest < Test::Unit::TestCase
4
+ def test_bar
5
+ assert true
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path("..", __FILE__) + "/test_helper"
2
+
3
+ class ParserTest < Test::Unit::TestCase
4
+ def test_foo
5
+ assert true
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "bundler"
2
+ Bundler.setup :default, :development
3
+
4
+ require "hatetepe"
5
+
6
+ require "test/unit"
7
+ require "awesome_print"
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hatetepe
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Lars Gierth
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-06-03 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: http_parser.rb
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: test-unit
28
+ requirement: &id002 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *id002
37
+ description: Hatetepe combines its own builder with http_parser.rb to make dealing with HTTP messages as comfortable as possible.
38
+ email:
39
+ - lars.gierth@gmail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - Gemfile
48
+ - README.md
49
+ - Rakefile
50
+ - example.rb
51
+ - hatetepe.gemspec
52
+ - lib/hatetepe.rb
53
+ - lib/hatetepe/builder.rb
54
+ - lib/hatetepe/parser.rb
55
+ - lib/hatetepe/status.rb
56
+ - lib/hatetepe/version.rb
57
+ - test/builder_test.rb
58
+ - test/parser_test.rb
59
+ - test/test_helper.rb
60
+ homepage: https://github.com/lgierth/hatetepe
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options: []
65
+
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 678111023
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ hash: 678111023
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ requirements: []
87
+
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.2
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Builds and parses HTTP messages
93
+ test_files: []
94
+