rbkb-http 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/.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