rbkb-http 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.2.0 / 2009-10-06
2
+ * split off rbkb/http from rbkb core.
3
+ * added MultiPartFormParams support and TextPlainFormParams
4
+ * added request body parameter interfaces to rbkb/http
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2009 Eric Monti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,18 @@
1
+ = rbkb-http
2
+
3
+ HTTP protocol addons for the Ruby BlackBag (rbkb-http)
4
+
5
+ * http://github.com/emonti/rbkb-http
6
+
7
+ == DESCRIPTION
8
+
9
+ This library various includes HTTP protocol tools and libraries based on and
10
+ complementary to the Ruby BlackBag library.
11
+
12
+ == REQUIREMENTS
13
+
14
+ * ruby blackbag (rbkb) - http://emonti.github.com/rbkb
15
+
16
+ == Copyright
17
+
18
+ Copyright (c) 2009 Eric Monti. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,59 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "rbkb-http"
9
+ gem.summary = %Q{HTTP protocol add-ons for Ruby BlackBag}
10
+ gem.description = %Q{HTTP libraries and tools based on and complementary to Ruby BlackBag}
11
+ gem.email = "emonti@matasano.com"
12
+ gem.homepage = "http://github.com/emonti/rbkb-http"
13
+ gem.authors = ["Eric Monti"]
14
+ gem.add_development_dependency "rspec"
15
+ gem.add_dependency "rbkb"
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/test_*.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ #require 'spec/rake/spectask'
30
+ #Spec::Rake::SpecTask.new(:spec) do |spec|
31
+ # spec.libs << 'lib' << 'spec'
32
+ # spec.spec_files = FileList['spec/**/*_spec.rb']
33
+ #end
34
+ #
35
+ #Spec::Rake::SpecTask.new(:rcov) do |spec|
36
+ # spec.libs << 'lib' << 'spec'
37
+ # spec.pattern = 'spec/**/*_spec.rb'
38
+ # spec.rcov = true
39
+ #end
40
+
41
+ #task :spec => :check_dependencies
42
+
43
+ #task :default => :spec
44
+ task :default => :test
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ if File.exist?('VERSION')
49
+ version = File.read('VERSION')
50
+ else
51
+ version = ""
52
+ end
53
+
54
+ rdoc.rdoc_dir = 'rdoc'
55
+ rdoc.title = "rbkb-http #{version}"
56
+ rdoc.rdoc_files.include('README*')
57
+ rdoc.rdoc_files.include('lib/**/*.rb')
58
+ end
59
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,179 @@
1
+ module Rbkb::Http
2
+
3
+ # A base class containing some common features for Request and Response
4
+ # objects.
5
+ #
6
+ # Don't use this class directly, it's intended for being overridden
7
+ # from its derived classes or mixins.
8
+ class Base
9
+ include CommonInterface
10
+
11
+ def self.parse(*args)
12
+ new(*args)
13
+ end
14
+
15
+ # Initializes a new Base object
16
+ def initialize(*args)
17
+ _common_init(*args)
18
+ end
19
+
20
+ # This method parses just HTTP message body. Expects body to be split
21
+ # from the headers before-hand.
22
+ def capture_body(bstr)
23
+ self.body ||= default_body_obj
24
+ @body.capture(bstr)
25
+ end
26
+
27
+ # XXX stub
28
+ def first_entity
29
+ @first_entity
30
+ end
31
+
32
+ # XXX stub
33
+ def first_entity=(f)
34
+ @first_entity=(f)
35
+ end
36
+
37
+ # This method parses only HTTP response headers. Expects headers to be
38
+ # split from the body before-hand.
39
+ def capture_headers(hstr)
40
+ self.headers ||= default_headers_obj
41
+
42
+ if @body and not @body.capture_complete?
43
+ return
44
+ elsif @headers.capture_complete?
45
+ self.first_entity, @headers = default_headers_obj.capture_full_headers(hstr)
46
+ else
47
+ @headers.capture(hstr)
48
+ end
49
+ end
50
+
51
+ # This method returns the content length from Headers. This is
52
+ # mostly useful if you are using a BoundBody object for the body.
53
+ #
54
+ # Returns nil if no "Content-Length" is not found.
55
+ #
56
+ # The opts parameter :ignore_content_length affects this method and
57
+ # will cause it always to return nil. This is useful, for example,
58
+ # for the responses to the HTTP HEAD request method, which return
59
+ # a Content-Length without actual content.
60
+ #
61
+ def content_length(hdrs=@headers)
62
+ raise "headers is nil?" if not hdrs
63
+ if( (not @opts[:ignore_content_length]) and
64
+ hdrs.get_header_value("Content-Length").to_s =~ /^(\d+)$/ )
65
+
66
+ $1.to_i
67
+ end
68
+ end
69
+
70
+ def content_type(hdrs=@headers)
71
+ raise "headers is nil?" if not hdrs
72
+ if ctype=hdrs.get_header_value("Content-Type")
73
+ ctype.split(/\s*;\s*/).first
74
+ end
75
+ end
76
+
77
+ def attach_new_header(hdr_obj=nil)
78
+ self.headers = hdr_obj
79
+ return hdr_obj
80
+ end
81
+
82
+ def attach_new_body(body_obj=nil)
83
+ self.body = body_obj
84
+ return body_obj
85
+ end
86
+
87
+ # XXX doc override!
88
+ def default_headers_obj(*args)
89
+ Header.new(*args)
90
+ end
91
+
92
+ # XXX doc override!
93
+ def default_body_obj(*args)
94
+ Body.new(*args)
95
+ end
96
+
97
+ # This method will non-destructively reset the capture state on this
98
+ # object and all child entities. Note, however, If child entities are not
99
+ # defined, it may instantiate new ones.
100
+ # See also: capture_complete?, reset_capture!
101
+ def reset_capture
102
+ if @headers
103
+ @headers.reset_capture if not @headers.capture_complete?
104
+ else
105
+ attach_new_header()
106
+ end
107
+
108
+ if @body
109
+ @body.reset_capture if not @body.capture_complete?
110
+ else
111
+ attach_new_body()
112
+ end
113
+ @capture_state = nil
114
+ self
115
+ end
116
+
117
+ # This method will destructively reset the capture state on this object.
118
+ # It does so by initializing fresh child entities and discarding the old
119
+ # ones. See also: capture_complete?, reset_capture
120
+ def reset_capture!
121
+ attach_new_header()
122
+ attach_new_body()
123
+ @capture_state = nil
124
+ self
125
+ end
126
+
127
+ # Indicates whether this object is ready to capture fresh data, or is
128
+ # waiting for additional data or a reset from a previous incomplete or
129
+ # otherwise broken capture. See also: reset_capture, reset_capture!
130
+ def capture_complete?
131
+ if( (@headers and not @headers.capture_complete?) or
132
+ (@body and not @body.capture_complete?) )
133
+ return false
134
+ else
135
+ true
136
+ end
137
+ end
138
+
139
+ attr_reader :body, :headers
140
+
141
+ # This accessor will attempt to always do the "right thing" while
142
+ # setting this object's body entity.
143
+ #
144
+ # See also: default_body_obj
145
+ def body=(b)
146
+ if @body
147
+ @body.data = b
148
+ elsif b.kind_of? Body
149
+ @body = b.dup
150
+ @body.opts = b.opts
151
+ else
152
+ @body = default_body_obj(b)
153
+ end
154
+ @body.base = self
155
+ return @body
156
+ end
157
+
158
+ # This accessor will attempt to always do the "right thing" while
159
+ # setting this object's headers entity.
160
+ #
161
+ # See also: default_headers_obj
162
+ def headers=(h)
163
+ if @headers
164
+ @headers.data = h
165
+ elsif h.kind_of? Headers
166
+ @headers = h.dup
167
+ @headers.opts = h.opts
168
+ else
169
+ @headers = default_headers_obj(h)
170
+ end
171
+ @headers.base = self
172
+ return @body
173
+ end
174
+
175
+ end
176
+
177
+
178
+ end
179
+
@@ -0,0 +1,220 @@
1
+ require 'stringio'
2
+
3
+ module Rbkb::Http
4
+ class Body < String
5
+ include CommonInterface
6
+
7
+ def self.parse(str)
8
+ new().capture(str)
9
+ end
10
+
11
+ attr_reader :expect_length
12
+
13
+ def initialize(str=nil, opts=nil)
14
+ self.opts = opts
15
+ if Body === str
16
+ self.replace(str)
17
+ @opts = str.opts.merge(@opts)
18
+ elsif String === str
19
+ super(str)
20
+ else
21
+ super()
22
+ end
23
+
24
+ yield(self) if block_given?
25
+ end
26
+
27
+ # The capture method is used when parsing HTTP requests/responses.
28
+ # This can and probably should be overridden in derived classes.
29
+ def capture(str)
30
+ yield(str) if block_given?
31
+ self.data=(str)
32
+ end
33
+
34
+ # The to_raw method is used when writing HTTP requests/responses.
35
+ # This can and probably should be overridden in derived classes.
36
+ def to_raw
37
+ (block_given?) ? yield(self.data) : self.data
38
+ end
39
+
40
+ attr_reader :base
41
+
42
+ def base=(b)
43
+ if b.nil? or b.is_a? Base
44
+ @base = b
45
+ else
46
+ raise "base must be a Response or Request object or nil"
47
+ end
48
+ end
49
+
50
+ def data
51
+ self
52
+ end
53
+
54
+ # Sets internal raw string data without any HTTP decoration.
55
+ def data=(str)
56
+ self.replace(str.to_s)
57
+ end
58
+
59
+ # Returns the content length from the HTTP base object if
60
+ # there is one and content-length is available.
61
+ def get_content_length
62
+ @base.content_length if @base
63
+ end
64
+ alias content_length get_content_length
65
+
66
+ def get_content_type
67
+ @base.content_type if @base
68
+ end
69
+ alias content_type get_content_type
70
+
71
+ # This method will non-destructively reset the capture state on this object.
72
+ # It is non-destructive in that it will not affect existing captured data
73
+ # if present.
74
+ def reset_capture
75
+ @expect_length = nil
76
+ @base.reset_capture() if @base and @base.capture_complete?
77
+ end
78
+
79
+ # This method will destructively reset the capture state on this object.
80
+ # This method is destructive in that it will clear any previously captured
81
+ # data.
82
+ def reset_capture!
83
+ reset_capture()
84
+ self.data=""
85
+ end
86
+
87
+ def capture_complete?
88
+ not @expect_length
89
+ end
90
+ end
91
+
92
+
93
+ # BoundBody is designed for handling an HTTP body when using the usual
94
+ # "Content-Length: NNN" HTTP header.
95
+ class BoundBody < Body
96
+
97
+ # This method may throw :expect_length with one of the following values
98
+ # to indicate certain content-length conditions:
99
+ #
100
+ # > 0 : Got incomplete data in this capture. The object expects
101
+ # capture to be called again with more body data.
102
+ #
103
+ # < 0 : Got more data than expected, the caller should truncate and
104
+ # handle the extra data in some way. Note: Calling capture again
105
+ # on this instance will start a fresh body capture.
106
+ #
107
+ # Caller can also detect the above conditions by checking the expect_length
108
+ # attribute but should still be prepared handle the throw().
109
+ #
110
+ # 0/nil: Got exactly what was expected. Caller can proceed with fresh
111
+ # captures on this or other Body objects.
112
+ #
113
+ # See also reset_capture and reset_capture!
114
+ def capture(str)
115
+ raise "arg 0 must be a string" unless String === str
116
+
117
+ # Start fresh unless we're expecting more data
118
+ self.data="" unless @expect_length and @expect_length > 0
119
+
120
+ if not clen=get_content_length()
121
+ raise "content-length is unknown. aborting capture"
122
+ else
123
+ @expect_length = clen - (self.size + str.size)
124
+ self << str[0, clen - self.size]
125
+ if @expect_length > 0
126
+ throw(:expect_length, @expect_length)
127
+ elsif @expect_length < 0
128
+ throw(:expect_length, @expect_length)
129
+ else
130
+ reset_capture()
131
+ end
132
+ end
133
+ return self
134
+ end
135
+
136
+ def to_raw(*args)
137
+ if @base
138
+ @base.headers.set_header("Content-Length", self.size)
139
+ end
140
+ super(*args)
141
+ end
142
+ end
143
+
144
+
145
+ # ChunkedBody is designed for handling an HTTP body when using a
146
+ # "Transfer-Encoding: chunked" HTTP header.
147
+ class ChunkedBody < Body
148
+ DEFAULT_CHUNK_SIZE = 2048
149
+
150
+ # Throws :expect_length with 'true' when given incomplete data and expects
151
+ # to be called again with more body data to parse.
152
+ #
153
+ # The caller can also detect this condition by checking the expect_length
154
+ # attribute but must still handle the throw().
155
+ #
156
+ # See also reset_capture and reset_capture!
157
+ def capture(str)
158
+ # chunked encoding is gross...
159
+ if @expect_length
160
+ sio = StringIO.new(@last_chunk.to_s + str)
161
+ else
162
+ sio = StringIO.new(str)
163
+ self.data=""
164
+ end
165
+ @last_chunk = nil
166
+
167
+ @expect_length = true
168
+ while not sio.eof?
169
+ unless m=/^([a-fA-F0-9]+)\s*(;[[:print:]\s]*)?\r?\n$/.match(line=sio.readline)
170
+ raise "invalid chunk at #{line.chomp.inspect}"
171
+ end
172
+ if (chunksz = m[1].hex) == 0
173
+ @expect_length = false
174
+ # XXX ignore Trailer headers
175
+ break
176
+ end
177
+
178
+ if ( (not sio.eof?) and
179
+ (chunk=sio.read(chunksz)) and
180
+ chunk.size == chunksz and
181
+ (not sio.eof?) and (extra = sio.readline) and
182
+ (not sio.eof?) and (extra << sio.readline)
183
+ )
184
+ if extra =~ /^\r?\n\r?\n$/
185
+ yield(chunk) if block_given?
186
+ self << chunk
187
+ else
188
+ raise "expected CRLF"
189
+ end
190
+ else
191
+ @last_chunk = line + chunk.to_s + extra.to_s
192
+ break
193
+ end
194
+ end
195
+ throw(:expect_length, @expect_length) if @expect_length
196
+ return self
197
+ end
198
+
199
+
200
+ def to_raw(csz=nil)
201
+ csz ||= (@opts[:output_chunk_size] || DEFAULT_CHUNK_SIZE)
202
+ unless csz.kind_of? Integer and csz > 0
203
+ raise "chunk size must be an integer >= 1"
204
+ end
205
+
206
+ out=[]
207
+ i=0
208
+ while i <= self.size
209
+ chunk = self[i, csz]
210
+ out << "#{chunk.size.to_s(16)}\r\n#{chunk}\r\n\r\n"
211
+ yield(self, out.last) if block_given?
212
+ i+=csz
213
+ end
214
+ out << "0\r\n"
215
+ yield(self, out.last) if block_given?
216
+ return out.join
217
+ end
218
+ end
219
+ end
220
+
@@ -0,0 +1,74 @@
1
+ module Rbkb::Http
2
+ DEFAULT_HTTP_VERSION = "HTTP/1.1"
3
+
4
+ module CommonInterface
5
+ # This provides a common method for use in 'initialize' to slurp in
6
+ # opts parameters and optionally capture a raw blob. This method also
7
+ # accepts a block to which it yields 'self'
8
+ def _common_init(raw=nil, opts=nil)
9
+ self.opts = opts
10
+ yield self if block_given?
11
+ capture(raw) if raw
12
+ return self
13
+ end
14
+
15
+ # Implements a common interface for an opts hash which is stored internally
16
+ # as the class variable @opts.
17
+ #
18
+ # The opts hash is designed to contain various named values for
19
+ # configuration, etc. The values and names are determined entirely
20
+ # by the class that uses it.
21
+ def opts
22
+ @opts
23
+ end
24
+
25
+ # Implements a common interface for setting a new opts hash containing
26
+ # various named values for configuration, etc. This also performs a
27
+ # minimal sanity check to ensure the object is a Hash.
28
+ def opts=(o=nil)
29
+ raise "opts must be a hash" unless (o ||= {}).is_a? Hash
30
+ @opts = o
31
+ end
32
+ end
33
+
34
+
35
+ # A generic cheat for an Array of named value pairs to pretend to
36
+ # be like Hash when using [] and []=
37
+ class NamedValueArray < Array
38
+
39
+ # Act like a hash with named values. Return the named value if a string
40
+ # or Symbol is supplied as the index argument.
41
+ #
42
+ # Note, this doesn't do any magic with String / Symbol conversion.
43
+ def [](*args)
44
+ if args.size == 1 and (String === args[0] or Symbol === args[0])
45
+ if h=find {|x| x[0] == args[0]}
46
+ return h[1]
47
+ end
48
+ else
49
+ super(*args)
50
+ end
51
+ end
52
+
53
+ # Act like a hash with named values. Set the named value if a String
54
+ # or Symbol is supplied as the index argument.
55
+ #
56
+ # Note, this doesn't do any magic with String / Symbol conversion.
57
+ def []=(*args)
58
+ if args.size > 1 and (String === args[0] or Symbol === args[0])
59
+ if h=find {|x| x[0] == args[0]}
60
+ h[1] = args[1]
61
+ else
62
+ self << args[0,2]
63
+ end
64
+ else
65
+ super(*args)
66
+ end
67
+ end
68
+
69
+ def delete_key(key)
70
+ delete_if {|x| x[0] == key }
71
+ end
72
+ end
73
+
74
+ end