rubysl-net-http 1.0.1 → 2.0.4

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.
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
+