net-http 0.1.0

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