net-http 0.1.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.
@@ -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