hatetepe 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|