rubysl-net-http 1.0.1 → 2.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b10637970739bb5cce790889102a544e0d5fb9aa
4
- data.tar.gz: 6bda2384dd0f44c7ca814cb58773efe6ee3c20a4
3
+ metadata.gz: 7f80bf01091463123fc8aed6388722df45ac1db6
4
+ data.tar.gz: a8562d9334cf2870ea2917e1c160e191515009ab
5
5
  SHA512:
6
- metadata.gz: 3d49ef1c1024ce082f99b9d18cb4cfebd102ef94986c617ecdd7ca94cb76c1389447912ba6f50f1010aec2bcfec37891210a800453aba90466ea8d3010b51139
7
- data.tar.gz: 695a352d5fa8651a9fbe6db528f71b8f94a22b995dd450f0da7dc55f50ca220f9a667b8c70f23150c842a0c278ffb90601d50813ca61ede77a9037d0464806fc
6
+ metadata.gz: a1f154967c6fa30137b0a0d46bda20cc16cff19e6712302450710fe47ba04e3ad0c416ea1857533792756068d66e54626c499cc02c55ce2e175859d9fcc6bbf3
7
+ data.tar.gz: 5f3b0e77bdd8b61cd92caa1324ae71cf7816adc337cef9f4e304d60c2a38a5d016795b9ab0ecf0bccdf696685b4f68ee12ea78644663440edbe33f72c087ca3b
@@ -1,8 +1,9 @@
1
1
  language: ruby
2
2
  before_install:
3
+ - rvm use $RVM --install --binary --fuzzy
3
4
  - gem update --system
4
5
  - gem --version
5
6
  - gem install rubysl-bundler
7
+ env:
8
+ - RVM=rbx-nightly-d21 RUBYLIB=lib
6
9
  script: bundle exec mspec spec
7
- rvm:
8
- - rbx-nightly-18mode
@@ -0,0 +1,25 @@
1
+ # for backward compatibility
2
+
3
+ # :enddoc:
4
+
5
+ class Net::HTTP
6
+ ProxyMod = ProxyDelta
7
+ end
8
+
9
+ module Net
10
+ HTTPSession = Net::HTTP
11
+ end
12
+
13
+ module Net::NetPrivate
14
+ HTTPRequest = ::Net::HTTPRequest
15
+ end
16
+
17
+ Net::HTTPInformationCode = Net::HTTPInformation
18
+ Net::HTTPSuccessCode = Net::HTTPSuccess
19
+ Net::HTTPRedirectionCode = Net::HTTPRedirection
20
+ Net::HTTPRetriableCode = Net::HTTPRedirection
21
+ Net::HTTPClientErrorCode = Net::HTTPClientError
22
+ Net::HTTPFatalErrorCode = Net::HTTPClientError
23
+ Net::HTTPServerErrorCode = Net::HTTPServerError
24
+ Net::HTTPResponceReceiver = Net::HTTPResponse
25
+
@@ -0,0 +1,25 @@
1
+ # Net::HTTP exception class.
2
+ # You cannot use Net::HTTPExceptions directly; instead, you must use
3
+ # its subclasses.
4
+ module Net::HTTPExceptions
5
+ def initialize(msg, res) #:nodoc:
6
+ super msg
7
+ @response = res
8
+ end
9
+ attr_reader :response
10
+ alias data response #:nodoc: obsolete
11
+ end
12
+ class Net::HTTPError < Net::ProtocolError
13
+ include Net::HTTPExceptions
14
+ end
15
+ class Net::HTTPRetriableError < Net::ProtoRetriableError
16
+ include Net::HTTPExceptions
17
+ end
18
+ class Net::HTTPServerException < Net::ProtoServerError
19
+ # We cannot use the name "HTTPServerError", it is the name of the response.
20
+ include Net::HTTPExceptions
21
+ end
22
+ class Net::HTTPFatalError < Net::ProtoFatalError
23
+ include Net::HTTPExceptions
24
+ end
25
+
@@ -0,0 +1,329 @@
1
+ # HTTPGenericRequest is the parent of the HTTPRequest class.
2
+ # Do not use this directly; use a subclass of HTTPRequest.
3
+ #
4
+ # Mixes in the HTTPHeader module to provide easier access to HTTP headers.
5
+ #
6
+ class Net::HTTPGenericRequest
7
+
8
+ include Net::HTTPHeader
9
+
10
+ def initialize(m, reqbody, resbody, uri_or_path, initheader = nil)
11
+ @method = m
12
+ @request_has_body = reqbody
13
+ @response_has_body = resbody
14
+
15
+ if URI === uri_or_path then
16
+ @uri = uri_or_path.dup
17
+ host = @uri.hostname
18
+ host += ":#{@uri.port}" if @uri.port != @uri.class::DEFAULT_PORT
19
+ path = uri_or_path.request_uri
20
+ else
21
+ @uri = nil
22
+ host = nil
23
+ path = uri_or_path
24
+ end
25
+
26
+ raise ArgumentError, "no HTTP request path given" unless path
27
+ raise ArgumentError, "HTTP request path is empty" if path.empty?
28
+ @path = path
29
+
30
+ @decode_content = false
31
+
32
+ if @response_has_body and Net::HTTP::HAVE_ZLIB then
33
+ if !initheader ||
34
+ !initheader.keys.any? { |k|
35
+ %w[accept-encoding range].include? k.downcase
36
+ } then
37
+ @decode_content = true
38
+ initheader = initheader ? initheader.dup : {}
39
+ initheader["accept-encoding"] =
40
+ "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
41
+ end
42
+ end
43
+
44
+ initialize_http_header initheader
45
+ self['Accept'] ||= '*/*'
46
+ self['User-Agent'] ||= 'Ruby'
47
+ self['Host'] ||= host
48
+ @body = nil
49
+ @body_stream = nil
50
+ @body_data = nil
51
+ end
52
+
53
+ attr_reader :method
54
+ attr_reader :path
55
+ attr_reader :uri
56
+
57
+ # Automatically set to false if the user sets the Accept-Encoding header.
58
+ # This indicates they wish to handle Content-encoding in responses
59
+ # themselves.
60
+ attr_reader :decode_content
61
+
62
+ def inspect
63
+ "\#<#{self.class} #{@method}>"
64
+ end
65
+
66
+ ##
67
+ # Don't automatically decode response content-encoding if the user indicates
68
+ # they want to handle it.
69
+
70
+ def []=(key, val) # :nodoc:
71
+ @decode_content = false if key.downcase == 'accept-encoding'
72
+
73
+ super key, val
74
+ end
75
+
76
+ def request_body_permitted?
77
+ @request_has_body
78
+ end
79
+
80
+ def response_body_permitted?
81
+ @response_has_body
82
+ end
83
+
84
+ def body_exist?
85
+ warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE
86
+ response_body_permitted?
87
+ end
88
+
89
+ attr_reader :body
90
+
91
+ def body=(str)
92
+ @body = str
93
+ @body_stream = nil
94
+ @body_data = nil
95
+ str
96
+ end
97
+
98
+ attr_reader :body_stream
99
+
100
+ def body_stream=(input)
101
+ @body = nil
102
+ @body_stream = input
103
+ @body_data = nil
104
+ input
105
+ end
106
+
107
+ def set_body_internal(str) #:nodoc: internal use only
108
+ raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
109
+ self.body = str if str
110
+ if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
111
+ self.body = ''
112
+ end
113
+ end
114
+
115
+ #
116
+ # write
117
+ #
118
+
119
+ def exec(sock, ver, path) #:nodoc: internal use only
120
+ if @uri
121
+ if @uri.port == @uri.default_port
122
+ # [Bug #7650] Amazon ECS API and GFE/1.3 disallow extra default port number
123
+ self['host'] = @uri.host
124
+ else
125
+ self['host'] = "#{@uri.host}:#{@uri.port}"
126
+ end
127
+ end
128
+
129
+ if @body
130
+ send_request_with_body sock, ver, path, @body
131
+ elsif @body_stream
132
+ send_request_with_body_stream sock, ver, path, @body_stream
133
+ elsif @body_data
134
+ send_request_with_body_data sock, ver, path, @body_data
135
+ else
136
+ write_header sock, ver, path
137
+ end
138
+ end
139
+
140
+ def update_uri(host, port, ssl) # :nodoc: internal use only
141
+ return unless @uri
142
+
143
+ @uri.host ||= host
144
+ @uri.port = port
145
+
146
+ scheme = ssl ? 'https' : 'http'
147
+
148
+ # convert the class of the URI
149
+ unless scheme == @uri.scheme then
150
+ new_uri = @uri.to_s.sub(/^https?/, scheme)
151
+ @uri = URI new_uri
152
+ end
153
+
154
+ @uri
155
+ end
156
+
157
+ private
158
+
159
+ class Chunker #:nodoc:
160
+ def initialize(sock)
161
+ @sock = sock
162
+ @prev = nil
163
+ end
164
+
165
+ def write(buf)
166
+ # avoid memcpy() of buf, buf can huge and eat memory bandwidth
167
+ @sock.write("#{buf.bytesize.to_s(16)}\r\n")
168
+ rv = @sock.write(buf)
169
+ @sock.write("\r\n")
170
+ rv
171
+ end
172
+
173
+ def finish
174
+ @sock.write("0\r\n\r\n")
175
+ end
176
+ end
177
+
178
+ def send_request_with_body(sock, ver, path, body)
179
+ self.content_length = body.bytesize
180
+ delete 'Transfer-Encoding'
181
+ supply_default_content_type
182
+ write_header sock, ver, path
183
+ wait_for_continue sock, ver if sock.continue_timeout
184
+ sock.write body
185
+ end
186
+
187
+ def send_request_with_body_stream(sock, ver, path, f)
188
+ unless content_length() or chunked?
189
+ raise ArgumentError,
190
+ "Content-Length not given and Transfer-Encoding is not `chunked'"
191
+ end
192
+ supply_default_content_type
193
+ write_header sock, ver, path
194
+ wait_for_continue sock, ver if sock.continue_timeout
195
+ if chunked?
196
+ chunker = Chunker.new(sock)
197
+ IO.copy_stream(f, chunker)
198
+ chunker.finish
199
+ else
200
+ # copy_stream can sendfile() to sock.io unless we use SSL.
201
+ # If sock.io is an SSLSocket, copy_stream will hit SSL_write()
202
+ IO.copy_stream(f, sock.io)
203
+ end
204
+ end
205
+
206
+ def send_request_with_body_data(sock, ver, path, params)
207
+ if /\Amultipart\/form-data\z/i !~ self.content_type
208
+ self.content_type = 'application/x-www-form-urlencoded'
209
+ return send_request_with_body(sock, ver, path, URI.encode_www_form(params))
210
+ end
211
+
212
+ opt = @form_option.dup
213
+ require 'securerandom' unless defined?(SecureRandom)
214
+ opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
215
+ self.set_content_type(self.content_type, boundary: opt[:boundary])
216
+ if chunked?
217
+ write_header sock, ver, path
218
+ encode_multipart_form_data(sock, params, opt)
219
+ else
220
+ require 'tempfile'
221
+ file = Tempfile.new('multipart')
222
+ file.binmode
223
+ encode_multipart_form_data(file, params, opt)
224
+ file.rewind
225
+ self.content_length = file.size
226
+ write_header sock, ver, path
227
+ IO.copy_stream(file, sock)
228
+ file.close(true)
229
+ end
230
+ end
231
+
232
+ def encode_multipart_form_data(out, params, opt)
233
+ charset = opt[:charset]
234
+ boundary = opt[:boundary]
235
+ require 'securerandom' unless defined?(SecureRandom)
236
+ boundary ||= SecureRandom.urlsafe_base64(40)
237
+ chunked_p = chunked?
238
+
239
+ buf = ''
240
+ params.each do |key, value, h={}|
241
+ key = quote_string(key, charset)
242
+ filename =
243
+ h.key?(:filename) ? h[:filename] :
244
+ value.respond_to?(:to_path) ? File.basename(value.to_path) :
245
+ nil
246
+
247
+ buf << "--#{boundary}\r\n"
248
+ if filename
249
+ filename = quote_string(filename, charset)
250
+ type = h[:content_type] || 'application/octet-stream'
251
+ buf << "Content-Disposition: form-data; " \
252
+ "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
253
+ "Content-Type: #{type}\r\n\r\n"
254
+ if !out.respond_to?(:write) || !value.respond_to?(:read)
255
+ # if +out+ is not an IO or +value+ is not an IO
256
+ buf << (value.respond_to?(:read) ? value.read : value)
257
+ elsif value.respond_to?(:size) && chunked_p
258
+ # if +out+ is an IO and +value+ is a File, use IO.copy_stream
259
+ flush_buffer(out, buf, chunked_p)
260
+ out << "%x\r\n" % value.size if chunked_p
261
+ IO.copy_stream(value, out)
262
+ out << "\r\n" if chunked_p
263
+ else
264
+ # +out+ is an IO, and +value+ is not a File but an IO
265
+ flush_buffer(out, buf, chunked_p)
266
+ 1 while flush_buffer(out, value.read(4096), chunked_p)
267
+ end
268
+ else
269
+ # non-file field:
270
+ # HTML5 says, "The parts of the generated multipart/form-data
271
+ # resource that correspond to non-file fields must not have a
272
+ # Content-Type header specified."
273
+ buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
274
+ buf << (value.respond_to?(:read) ? value.read : value)
275
+ end
276
+ buf << "\r\n"
277
+ end
278
+ buf << "--#{boundary}--\r\n"
279
+ flush_buffer(out, buf, chunked_p)
280
+ out << "0\r\n\r\n" if chunked_p
281
+ end
282
+
283
+ def quote_string(str, charset)
284
+ str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
285
+ str.gsub(/[\\"]/, '\\\\\&')
286
+ end
287
+
288
+ def flush_buffer(out, buf, chunked_p)
289
+ return unless buf
290
+ out << "%x\r\n"%buf.bytesize if chunked_p
291
+ out << buf
292
+ out << "\r\n" if chunked_p
293
+ buf.clear
294
+ end
295
+
296
+ def supply_default_content_type
297
+ return if content_type()
298
+ warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE
299
+ set_content_type 'application/x-www-form-urlencoded'
300
+ end
301
+
302
+ ##
303
+ # Waits up to the continue timeout for a response from the server provided
304
+ # we're speaking HTTP 1.1 and are expecting a 100-continue response.
305
+
306
+ def wait_for_continue(sock, ver)
307
+ if ver >= '1.1' and @header['expect'] and
308
+ @header['expect'].include?('100-continue')
309
+ if IO.select([sock.io], nil, nil, sock.continue_timeout)
310
+ res = Net::HTTPResponse.read_new(sock)
311
+ unless res.kind_of?(Net::HTTPContinue)
312
+ res.decode_content = @decode_content
313
+ throw :response, res
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ def write_header(sock, ver, path)
320
+ buf = "#{@method} #{path} HTTP/#{ver}\r\n"
321
+ each_capitalized do |k,v|
322
+ buf << "#{k}: #{v}\r\n"
323
+ end
324
+ buf << "\r\n"
325
+ sock.write buf
326
+ end
327
+
328
+ end
329
+
@@ -0,0 +1,452 @@
1
+ # The HTTPHeader module defines methods for reading and writing
2
+ # HTTP headers.
3
+ #
4
+ # It is used as a mixin by other classes, to provide hash-like
5
+ # access to HTTP header values. Unlike raw hash access, HTTPHeader
6
+ # provides access via case-insensitive keys. It also provides
7
+ # methods for accessing commonly-used HTTP header values in more
8
+ # convenient formats.
9
+ #
10
+ module Net::HTTPHeader
11
+
12
+ def initialize_http_header(initheader)
13
+ @header = {}
14
+ return unless initheader
15
+ initheader.each do |key, value|
16
+ warn "net/http: warning: duplicated HTTP header: #{key}" if key?(key) and $VERBOSE
17
+ @header[key.downcase] = [value.strip]
18
+ end
19
+ end
20
+
21
+ def size #:nodoc: obsolete
22
+ @header.size
23
+ end
24
+
25
+ alias length size #:nodoc: obsolete
26
+
27
+ # Returns the header field corresponding to the case-insensitive key.
28
+ # For example, a key of "Content-Type" might return "text/html"
29
+ def [](key)
30
+ a = @header[key.downcase] or return nil
31
+ a.join(', ')
32
+ end
33
+
34
+ # Sets the header field corresponding to the case-insensitive key.
35
+ def []=(key, val)
36
+ unless val
37
+ @header.delete key.downcase
38
+ return val
39
+ end
40
+ @header[key.downcase] = [val]
41
+ end
42
+
43
+ # [Ruby 1.8.3]
44
+ # Adds a value to a named header field, instead of replacing its value.
45
+ # Second argument +val+ must be a String.
46
+ # See also #[]=, #[] and #get_fields.
47
+ #
48
+ # request.add_field 'X-My-Header', 'a'
49
+ # p request['X-My-Header'] #=> "a"
50
+ # p request.get_fields('X-My-Header') #=> ["a"]
51
+ # request.add_field 'X-My-Header', 'b'
52
+ # p request['X-My-Header'] #=> "a, b"
53
+ # p request.get_fields('X-My-Header') #=> ["a", "b"]
54
+ # request.add_field 'X-My-Header', 'c'
55
+ # p request['X-My-Header'] #=> "a, b, c"
56
+ # p request.get_fields('X-My-Header') #=> ["a", "b", "c"]
57
+ #
58
+ def add_field(key, val)
59
+ if @header.key?(key.downcase)
60
+ @header[key.downcase].push val
61
+ else
62
+ @header[key.downcase] = [val]
63
+ end
64
+ end
65
+
66
+ # [Ruby 1.8.3]
67
+ # Returns an array of header field strings corresponding to the
68
+ # case-insensitive +key+. This method allows you to get duplicated
69
+ # header fields without any processing. See also #[].
70
+ #
71
+ # p response.get_fields('Set-Cookie')
72
+ # #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23",
73
+ # "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"]
74
+ # p response['Set-Cookie']
75
+ # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"
76
+ #
77
+ def get_fields(key)
78
+ return nil unless @header[key.downcase]
79
+ @header[key.downcase].dup
80
+ end
81
+
82
+ # Returns the header field corresponding to the case-insensitive key.
83
+ # Returns the default value +args+, or the result of the block, or
84
+ # raises an IndexError if there's no header field named +key+
85
+ # See Hash#fetch
86
+ def fetch(key, *args, &block) #:yield: +key+
87
+ a = @header.fetch(key.downcase, *args, &block)
88
+ a.kind_of?(Array) ? a.join(', ') : a
89
+ end
90
+
91
+ # Iterates through the header names and values, passing in the name
92
+ # and value to the code block supplied.
93
+ #
94
+ # Example:
95
+ #
96
+ # response.header.each_header {|key,value| puts "#{key} = #{value}" }
97
+ #
98
+ def each_header #:yield: +key+, +value+
99
+ block_given? or return enum_for(__method__)
100
+ @header.each do |k,va|
101
+ yield k, va.join(', ')
102
+ end
103
+ end
104
+
105
+ alias each each_header
106
+
107
+ # Iterates through the header names in the header, passing
108
+ # each header name to the code block.
109
+ def each_name(&block) #:yield: +key+
110
+ block_given? or return enum_for(__method__)
111
+ @header.each_key(&block)
112
+ end
113
+
114
+ alias each_key each_name
115
+
116
+ # Iterates through the header names in the header, passing
117
+ # capitalized header names to the code block.
118
+ #
119
+ # Note that header names are capitalized systematically;
120
+ # capitalization may not match that used by the remote HTTP
121
+ # server in its response.
122
+ def each_capitalized_name #:yield: +key+
123
+ block_given? or return enum_for(__method__)
124
+ @header.each_key do |k|
125
+ yield capitalize(k)
126
+ end
127
+ end
128
+
129
+ # Iterates through header values, passing each value to the
130
+ # code block.
131
+ def each_value #:yield: +value+
132
+ block_given? or return enum_for(__method__)
133
+ @header.each_value do |va|
134
+ yield va.join(', ')
135
+ end
136
+ end
137
+
138
+ # Removes a header field, specified by case-insensitive key.
139
+ def delete(key)
140
+ @header.delete(key.downcase)
141
+ end
142
+
143
+ # true if +key+ header exists.
144
+ def key?(key)
145
+ @header.key?(key.downcase)
146
+ end
147
+
148
+ # Returns a Hash consisting of header names and values.
149
+ # e.g.
150
+ # {"cache-control" => "private",
151
+ # "content-type" => "text/html",
152
+ # "date" => "Wed, 22 Jun 2005 22:11:50 GMT"}
153
+ def to_hash
154
+ @header.dup
155
+ end
156
+
157
+ # As for #each_header, except the keys are provided in capitalized form.
158
+ #
159
+ # Note that header names are capitalized systematically;
160
+ # capitalization may not match that used by the remote HTTP
161
+ # server in its response.
162
+ def each_capitalized
163
+ block_given? or return enum_for(__method__)
164
+ @header.each do |k,v|
165
+ yield capitalize(k), v.join(', ')
166
+ end
167
+ end
168
+
169
+ alias canonical_each each_capitalized
170
+
171
+ def capitalize(name)
172
+ name.split(/-/).map {|s| s.capitalize }.join('-')
173
+ end
174
+ private :capitalize
175
+
176
+ # Returns an Array of Range objects which represent the Range:
177
+ # HTTP header field, or +nil+ if there is no such header.
178
+ def range
179
+ return nil unless @header['range']
180
+
181
+ value = self['Range']
182
+ # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec )
183
+ # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] )
184
+ # corrected collected ABNF
185
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1
186
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C
187
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5
188
+ unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value
189
+ raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'"
190
+ end
191
+
192
+ byte_range_set = $1
193
+ result = byte_range_set.split(/,/).map {|spec|
194
+ m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or
195
+ raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'"
196
+ d1 = m[1].to_i
197
+ d2 = m[2].to_i
198
+ if m[1] and m[2]
199
+ if d1 > d2
200
+ raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'"
201
+ end
202
+ d1..d2
203
+ elsif m[1]
204
+ d1..-1
205
+ elsif m[2]
206
+ -d2..-1
207
+ else
208
+ raise Net::HTTPHeaderSyntaxError, 'range is not specified'
209
+ end
210
+ }
211
+ # if result.empty?
212
+ # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec
213
+ # but above regexp already denies it.
214
+ if result.size == 1 && result[0].begin == 0 && result[0].end == -1
215
+ raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length'
216
+ end
217
+ result
218
+ end
219
+
220
+ # Sets the HTTP Range: header.
221
+ # Accepts either a Range object as a single argument,
222
+ # or a beginning index and a length from that index.
223
+ # Example:
224
+ #
225
+ # req.range = (0..1023)
226
+ # req.set_range 0, 1023
227
+ #
228
+ def set_range(r, e = nil)
229
+ unless r
230
+ @header.delete 'range'
231
+ return r
232
+ end
233
+ r = (r...r+e) if e
234
+ case r
235
+ when Numeric
236
+ n = r.to_i
237
+ rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
238
+ when Range
239
+ first = r.first
240
+ last = r.end
241
+ last -= 1 if r.exclude_end?
242
+ if last == -1
243
+ rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
244
+ else
245
+ raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
246
+ raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
247
+ raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
248
+ rangestr = "#{first}-#{last}"
249
+ end
250
+ else
251
+ raise TypeError, 'Range/Integer is required'
252
+ end
253
+ @header['range'] = ["bytes=#{rangestr}"]
254
+ r
255
+ end
256
+
257
+ alias range= set_range
258
+
259
+ # Returns an Integer object which represents the HTTP Content-Length:
260
+ # header field, or +nil+ if that field was not provided.
261
+ def content_length
262
+ return nil unless key?('Content-Length')
263
+ len = self['Content-Length'].slice(/\d+/) or
264
+ raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
265
+ len.to_i
266
+ end
267
+
268
+ def content_length=(len)
269
+ unless len
270
+ @header.delete 'content-length'
271
+ return nil
272
+ end
273
+ @header['content-length'] = [len.to_i.to_s]
274
+ end
275
+
276
+ # Returns "true" if the "transfer-encoding" header is present and
277
+ # set to "chunked". This is an HTTP/1.1 feature, allowing the
278
+ # the content to be sent in "chunks" without at the outset
279
+ # stating the entire content length.
280
+ def chunked?
281
+ return false unless @header['transfer-encoding']
282
+ field = self['Transfer-Encoding']
283
+ (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
284
+ end
285
+
286
+ # Returns a Range object which represents the value of the Content-Range:
287
+ # header field.
288
+ # For a partial entity body, this indicates where this fragment
289
+ # fits inside the full entity body, as range of byte offsets.
290
+ def content_range
291
+ return nil unless @header['content-range']
292
+ m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or
293
+ raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
294
+ m[1].to_i .. m[2].to_i
295
+ end
296
+
297
+ # The length of the range represented in Content-Range: header.
298
+ def range_length
299
+ r = content_range() or return nil
300
+ r.end - r.begin + 1
301
+ end
302
+
303
+ # Returns a content type string such as "text/html".
304
+ # This method returns nil if Content-Type: header field does not exist.
305
+ def content_type
306
+ return nil unless main_type()
307
+ if sub_type()
308
+ then "#{main_type()}/#{sub_type()}"
309
+ else main_type()
310
+ end
311
+ end
312
+
313
+ # Returns a content type string such as "text".
314
+ # This method returns nil if Content-Type: header field does not exist.
315
+ def main_type
316
+ return nil unless @header['content-type']
317
+ self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
318
+ end
319
+
320
+ # Returns a content type string such as "html".
321
+ # This method returns nil if Content-Type: header field does not exist
322
+ # or sub-type is not given (e.g. "Content-Type: text").
323
+ def sub_type
324
+ return nil unless @header['content-type']
325
+ _, sub = *self['Content-Type'].split(';').first.to_s.split('/')
326
+ return nil unless sub
327
+ sub.strip
328
+ end
329
+
330
+ # Any parameters specified for the content type, returned as a Hash.
331
+ # For example, a header of Content-Type: text/html; charset=EUC-JP
332
+ # would result in type_params returning {'charset' => 'EUC-JP'}
333
+ def type_params
334
+ result = {}
335
+ list = self['Content-Type'].to_s.split(';')
336
+ list.shift
337
+ list.each do |param|
338
+ k, v = *param.split('=', 2)
339
+ result[k.strip] = v.strip
340
+ end
341
+ result
342
+ end
343
+
344
+ # Sets the content type in an HTTP header.
345
+ # The +type+ should be a full HTTP content type, e.g. "text/html".
346
+ # The +params+ are an optional Hash of parameters to add after the
347
+ # content type, e.g. {'charset' => 'iso-8859-1'}
348
+ def set_content_type(type, params = {})
349
+ @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
350
+ end
351
+
352
+ alias content_type= set_content_type
353
+
354
+ # Set header fields and a body from HTML form data.
355
+ # +params+ should be an Array of Arrays or
356
+ # a Hash containing HTML form data.
357
+ # Optional argument +sep+ means data record separator.
358
+ #
359
+ # Values are URL encoded as necessary and the content-type is set to
360
+ # application/x-www-form-urlencoded
361
+ #
362
+ # Example:
363
+ # http.form_data = {"q" => "ruby", "lang" => "en"}
364
+ # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"}
365
+ # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';')
366
+ #
367
+ def set_form_data(params, sep = '&')
368
+ query = URI.encode_www_form(params)
369
+ query.gsub!(/&/, sep) if sep != '&'
370
+ self.body = query
371
+ self.content_type = 'application/x-www-form-urlencoded'
372
+ end
373
+
374
+ alias form_data= set_form_data
375
+
376
+ # Set a HTML form data set.
377
+ # +params+ is the form data set; it is an Array of Arrays or a Hash
378
+ # +enctype is the type to encode the form data set.
379
+ # It is application/x-www-form-urlencoded or multipart/form-data.
380
+ # +formpot+ is an optional hash to specify the detail.
381
+ #
382
+ # boundary:: the boundary of the multipart message
383
+ # charset:: the charset of the message. All names and the values of
384
+ # non-file fields are encoded as the charset.
385
+ #
386
+ # Each item of params is an array and contains following items:
387
+ # +name+:: the name of the field
388
+ # +value+:: the value of the field, it should be a String or a File
389
+ # +opt+:: an optional hash to specify additional information
390
+ #
391
+ # Each item is a file field or a normal field.
392
+ # If +value+ is a File object or the +opt+ have a filename key,
393
+ # the item is treated as a file field.
394
+ #
395
+ # If Transfer-Encoding is set as chunked, this send the request in
396
+ # chunked encoding. Because chunked encoding is HTTP/1.1 feature,
397
+ # you must confirm the server to support HTTP/1.1 before sending it.
398
+ #
399
+ # Example:
400
+ # http.set_form([["q", "ruby"], ["lang", "en"]])
401
+ #
402
+ # See also RFC 2388, RFC 2616, HTML 4.01, and HTML5
403
+ #
404
+ def set_form(params, enctype='application/x-www-form-urlencoded', formopt={})
405
+ @body_data = params
406
+ @body = nil
407
+ @body_stream = nil
408
+ @form_option = formopt
409
+ case enctype
410
+ when /\Aapplication\/x-www-form-urlencoded\z/i,
411
+ /\Amultipart\/form-data\z/i
412
+ self.content_type = enctype
413
+ else
414
+ raise ArgumentError, "invalid enctype: #{enctype}"
415
+ end
416
+ end
417
+
418
+ # Set the Authorization: header for "Basic" authorization.
419
+ def basic_auth(account, password)
420
+ @header['authorization'] = [basic_encode(account, password)]
421
+ end
422
+
423
+ # Set Proxy-Authorization: header for "Basic" authorization.
424
+ def proxy_basic_auth(account, password)
425
+ @header['proxy-authorization'] = [basic_encode(account, password)]
426
+ end
427
+
428
+ def basic_encode(account, password)
429
+ 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n")
430
+ end
431
+ private :basic_encode
432
+
433
+ def connection_close?
434
+ tokens(@header['connection']).include?('close') or
435
+ tokens(@header['proxy-connection']).include?('close')
436
+ end
437
+
438
+ def connection_keep_alive?
439
+ tokens(@header['connection']).include?('keep-alive') or
440
+ tokens(@header['proxy-connection']).include?('keep-alive')
441
+ end
442
+
443
+ def tokens(vals)
444
+ return [] unless vals
445
+ vals.map {|v| v.split(',') }.flatten\
446
+ .reject {|str| str.strip.empty? }\
447
+ .map {|tok| tok.strip.downcase }
448
+ end
449
+ private :tokens
450
+
451
+ end
452
+