hatetepe 0.0.1
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/Gemfile +6 -0
- data/README.md +13 -0
- data/Rakefile +11 -0
- data/example.rb +29 -0
- data/hatetepe.gemspec +23 -0
- data/lib/hatetepe.rb +3 -0
- data/lib/hatetepe/builder.rb +142 -0
- data/lib/hatetepe/parser.rb +96 -0
- data/lib/hatetepe/status.rb +42 -0
- data/lib/hatetepe/version.rb +3 -0
- data/test/builder_test.rb +7 -0
- data/test/parser_test.rb +7 -0
- data/test/test_helper.rb +7 -0
- metadata +94 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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)
|
data/Rakefile
ADDED
data/example.rb
ADDED
@@ -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
|
data/hatetepe.gemspec
ADDED
@@ -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
|
data/lib/hatetepe.rb
ADDED
@@ -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
|
data/test/parser_test.rb
ADDED
data/test/test_helper.rb
ADDED
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
|
+
|