diff options
Diffstat (limited to 'lib/net')
| -rw-r--r-- | lib/net/ftp.rb | 671 | ||||
| -rw-r--r-- | lib/net/http.rb | 3383 | ||||
| -rw-r--r-- | lib/net/http/exceptions.rb | 35 | ||||
| -rw-r--r-- | lib/net/http/generic_request.rb | 429 | ||||
| -rw-r--r-- | lib/net/http/header.rb | 985 | ||||
| -rw-r--r-- | lib/net/http/net-http.gemspec | 39 | ||||
| -rw-r--r-- | lib/net/http/proxy_delta.rb | 17 | ||||
| -rw-r--r-- | lib/net/http/request.rb | 88 | ||||
| -rw-r--r-- | lib/net/http/requests.rb | 444 | ||||
| -rw-r--r-- | lib/net/http/response.rb | 739 | ||||
| -rw-r--r-- | lib/net/http/responses.rb | 1242 | ||||
| -rw-r--r-- | lib/net/http/status.rb | 84 | ||||
| -rw-r--r-- | lib/net/https.rb | 23 | ||||
| -rw-r--r-- | lib/net/imap.rb | 1898 | ||||
| -rw-r--r-- | lib/net/net-protocol.gemspec | 33 | ||||
| -rw-r--r-- | lib/net/pop.rb | 450 | ||||
| -rw-r--r-- | lib/net/protocol.rb | 1042 | ||||
| -rw-r--r-- | lib/net/smtp.rb | 303 | ||||
| -rw-r--r-- | lib/net/telnet.rb | 779 |
19 files changed, 7020 insertions, 5664 deletions
diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb deleted file mode 100644 index 88361b9ddb..0000000000 --- a/lib/net/ftp.rb +++ /dev/null @@ -1,671 +0,0 @@ -=begin - -= net/ftp.rb - -written by Shugo Maeda <shugo@ruby-lang.org> - -This library is distributed under the terms of the Ruby license. -You can freely distribute/modify this library. - -=end - -require "socket" -require "monitor" - -module Net - - class FTPError < StandardError; end - class FTPReplyError < FTPError; end - class FTPTempError < FTPError; end - class FTPPermError < FTPError; end - class FTPProtoError < FTPError; end - - class FTP - include MonitorMixin - - FTP_PORT = 21 - CRLF = "\r\n" - - DEFAULT_BLOCKSIZE = 4096 - - attr_accessor :passive, :return_code, :debug_mode, :resume - attr_reader :welcome, :lastresp - - def FTP.open(host, user = nil, passwd = nil, acct = nil) - new(host, user, passwd, acct) - end - - def initialize(host = nil, user = nil, passwd = nil, acct = nil) - super() - @passive = false - @return_code = "\n" - @debug_mode = false - @resume = false - if host - connect(host) - if user - login(user, passwd, acct) - end - end - end - - def open_socket(host, port) - if defined? SOCKSsocket and ENV["SOCKS_SERVER"] - @passive = true - return SOCKSsocket.open(host, port) - else - return TCPsocket.open(host, port) - end - end - private :open_socket - - def connect(host, port = FTP_PORT) - if @debug_mode - print "connect: ", host, ", ", port, "\n" - end - synchronize do - @sock = open_socket(host, port) - voidresp - end - end - - def sanitize(s) - if s =~ /^PASS /i - return s[0, 5] + "*" * (s.length - 5) - else - return s - end - end - private :sanitize - - def putline(line) - if @debug_mode - print "put: ", sanitize(line), "\n" - end - line = line + CRLF - @sock.write(line) - end - private :putline - - def getline - line = @sock.readline # if get EOF, raise EOFError - if line[-2, 2] == CRLF - line = line[0 .. -3] - elsif line[-1] == ?\r or - line[-1] == ?\n - line = line[0 .. -2] - end - if @debug_mode - print "get: ", sanitize(line), "\n" - end - return line - end - private :getline - - def getmultiline - line = getline - buff = line - if line[3] == ?- - code = line[0, 3] - begin - line = getline - buff << "\n" << line - end until line[0, 3] == code and line[3] != ?- - end - return buff << "\n" - end - private :getmultiline - - def getresp - resp = getmultiline - @lastresp = resp[0, 3] - c = resp[0] - case c - when ?1, ?2, ?3 - return resp - when ?4 - raise FTPTempError, resp - when ?5 - raise FTPPermError, resp - else - raise FTPProtoError, resp - end - end - private :getresp - - def voidresp - resp = getresp - if resp[0] != ?2 - raise FTPReplyError, resp - end - end - private :voidresp - - def sendcmd(cmd) - synchronize do - putline(cmd) - return getresp - end - end - - def voidcmd(cmd) - synchronize do - putline(cmd) - voidresp - end - end - - def sendport(host, port) - af = (@sock.peeraddr)[0] - if af == "AF_INET" - hbytes = host.split(".") - pbytes = [port / 256, port % 256] - bytes = hbytes + pbytes - cmd = "PORT " + bytes.join(",") - elsif af == "AF_INET6" - cmd = "EPRT |2|" + host + "|" + sprintf("%d", port) + "|" - else - raise FTPProtoError, host - end - voidcmd(cmd) - end - private :sendport - - def makeport - sock = TCPserver.open(@sock.addr[3], 0) - port = sock.addr[1] - host = sock.addr[3] - resp = sendport(host, port) - return sock - end - private :makeport - - def makepasv - if @sock.peeraddr[0] == "AF_INET" - host, port = parse227(sendcmd("PASV")) - else - host, port = parse229(sendcmd("EPSV")) - # host, port = parse228(sendcmd("LPSV")) - end - return host, port - end - private :makepasv - - def transfercmd(cmd, rest_offset = nil) - if @passive - host, port = makepasv - conn = open_socket(host, port) - if @resume and rest_offset - resp = sendcmd("REST " + rest_offset.to_s) - if resp[0] != ?3 - raise FTPReplyError, resp - end - end - resp = sendcmd(cmd) - if resp[0] != ?1 - raise FTPReplyError, resp - end - else - sock = makeport - if @resume and rest_offset - resp = sendcmd("REST " + rest_offset.to_s) - if resp[0] != ?3 - raise FTPReplyError, resp - end - end - resp = sendcmd(cmd) - if resp[0] != ?1 - raise FTPReplyError, resp - end - conn = sock.accept - sock.close - end - return conn - end - private :transfercmd - - def getaddress - thishost = Socket.gethostname - if not thishost.index(".") - thishost = Socket.gethostbyname(thishost)[0] - end - if ENV.has_key?("LOGNAME") - realuser = ENV["LOGNAME"] - elsif ENV.has_key?("USER") - realuser = ENV["USER"] - else - realuser = "anonymous" - end - return realuser + "@" + thishost - end - private :getaddress - - def login(user = "anonymous", passwd = nil, acct = nil) - if user == "anonymous" and passwd == nil - passwd = getaddress - end - - resp = "" - synchronize do - resp = sendcmd('USER ' + user) - if resp[0] == ?3 - resp = sendcmd('PASS ' + passwd) - end - if resp[0] == ?3 - resp = sendcmd('ACCT ' + acct) - end - end - if resp[0] != ?2 - raise FTPReplyError, resp - end - @welcome = resp - end - - def retrbinary(cmd, blocksize, rest_offset = nil, callback = Proc.new) - synchronize do - voidcmd("TYPE I") - conn = transfercmd(cmd, rest_offset) - loop do - data = conn.read(blocksize) - break if data == nil - callback.call(data) - end - conn.close - voidresp - end - end - - def retrlines(cmd, callback = nil) - if block_given? - callback = Proc.new - elsif not callback.is_a?(Proc) - callback = Proc.new {|line| print line, "\n"} - end - synchronize do - voidcmd("TYPE A") - conn = transfercmd(cmd) - loop do - line = conn.gets - break if line == nil - if line[-2, 2] == CRLF - line = line[0 .. -3] - elsif line[-1] == ?\n - line = line[0 .. -2] - end - callback.call(line) - end - conn.close - voidresp - end - end - - def storbinary(cmd, file, blocksize, rest_offset = nil, callback = nil) - if block_given? - callback = Proc.new - end - use_callback = callback.is_a?(Proc) - synchronize do - voidcmd("TYPE I") - conn = transfercmd(cmd, rest_offset) - loop do - buf = file.read(blocksize) - break if buf == nil - conn.write(buf) - callback.call(buf) if use_callback - end - conn.close - voidresp - end - end - - def storlines(cmd, file, callback = nil) - if block_given? - callback = Proc.new - end - use_callback = callback.is_a?(Proc) - synchronize do - voidcmd("TYPE A") - conn = transfercmd(cmd) - loop do - buf = file.gets - break if buf == nil - if buf[-2, 2] != CRLF - buf = buf.chomp + CRLF - end - conn.write(buf) - callback.call(buf) if use_callback - end - conn.close - voidresp - end - end - - def getbinaryfile(remotefile, localfile, - blocksize = DEFAULT_BLOCKSIZE, callback = nil) - if block_given? - callback = Proc.new - end - use_callback = callback.is_a?(Proc) - if @resume - rest_offset = File.size?(localfile) - f = open(localfile, "a") - else - rest_offset = nil - f = open(localfile, "w") - end - begin - f.binmode - retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data| - f.write(data) - callback.call(data) if use_callback - end - ensure - f.close - end - end - - def gettextfile(remotefile, localfile, callback = nil) - if block_given? - callback = Proc.new - end - use_callback = callback.is_a?(Proc) - f = open(localfile, "w") - begin - retrlines("RETR " + remotefile) do |line| - line = line + @return_code - f.write(line) - callback.call(line) if use_callback - end - ensure - f.close - end - end - - def putbinaryfile(localfile, remotefile, - blocksize = DEFAULT_BLOCKSIZE, callback = nil) - if block_given? - callback = Proc.new - end - use_callback = callback.is_a?(Proc) - if @resume - rest_offset = size(remotefile) - else - rest_offset = nil - end - f = open(localfile) - begin - f.binmode - storbinary("STOR " + remotefile, f, blocksize, rest_offset) do |data| - callback.call(data) if use_callback - end - ensure - f.close - end - end - - def puttextfile(localfile, remotefile, callback = nil) - if block_given? - callback = Proc.new - end - use_callback = callback.is_a?(Proc) - f = open(localfile) - begin - storlines("STOR " + remotefile, f) do |line| - callback.call(line) if use_callback - end - ensure - f.close - end - end - - def acct(account) - cmd = "ACCT " + account - voidcmd(cmd) - end - - def nlst(dir = nil) - cmd = "NLST" - if dir - cmd = cmd + " " + dir - end - files = [] - retrlines(cmd) do |line| - files.push(line) - end - return files - end - - def list(*args, &block) - cmd = "LIST" - args.each do |arg| - cmd = cmd + " " + arg - end - if block - retrlines(cmd, &block) - else - lines = [] - retrlines(cmd) do |line| - lines << line - end - return lines - end - end - alias ls list - alias dir list - - def rename(fromname, toname) - resp = sendcmd("RNFR " + fromname) - if resp[0] != ?3 - raise FTPReplyError, resp - end - voidcmd("RNTO " + toname) - end - - def delete(filename) - resp = sendcmd("DELE " + filename) - if resp[0, 3] == "250" - return - elsif resp[0] == ?5 - raise FTPPermError, resp - else - raise FTPReplyError, resp - end - end - - def chdir(dirname) - if dirname == ".." - begin - voidcmd("CDUP") - return - rescue FTPPermError - if $![0, 3] != "500" - raise FTPPermError, $! - end - end - end - cmd = "CWD " + dirname - voidcmd(cmd) - end - - def size(filename) - voidcmd("TYPE I") - resp = sendcmd("SIZE " + filename) - if resp[0, 3] != "213" - raise FTPReplyError, resp - end - return resp[3..-1].strip.to_i - end - - MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ - - def mtime(filename, local = false) - str = mdtm(filename) - ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i} - return local ? Time.local(*ary) : Time.gm(*ary) - end - - def mkdir(dirname) - resp = sendcmd("MKD " + dirname) - return parse257(resp) - end - - def rmdir(dirname) - voidcmd("RMD " + dirname) - end - - def pwd - resp = sendcmd("PWD") - return parse257(resp) - end - alias getdir pwd - - def system - resp = sendcmd("SYST") - if resp[0, 3] != "215" - raise FTPReplyError, resp - end - return resp[4 .. -1] - end - - def abort - line = "ABOR" + CRLF - print "put: ABOR\n" if @debug_mode - @sock.send(line, Socket::MSG_OOB) - resp = getmultiline - unless ["426", "226", "225"].include?(resp[0, 3]) - raise FTPProtoError, resp - end - return resp - end - - def status - line = "STAT" + CRLF - print "put: STAT\n" if @debug_mode - @sock.send(line, Socket::MSG_OOB) - return getresp - end - - def mdtm(filename) - resp = sendcmd("MDTM " + filename) - if resp[0, 3] == "213" - return resp[3 .. -1].strip - end - end - - def help(arg = nil) - cmd = "HELP" - if arg - cmd = cmd + " " + arg - end - sendcmd(cmd) - end - - def quit - voidcmd("QUIT") - end - - def close - @sock.close if @sock and not @sock.closed? - end - - def closed? - @sock == nil or @sock.closed? - end - - def parse227(resp) - if resp[0, 3] != "227" - raise FTPReplyError, resp - end - left = resp.index("(") - right = resp.index(")") - if left == nil or right == nil - raise FTPProtoError, resp - end - numbers = resp[left + 1 .. right - 1].split(",") - if numbers.length != 6 - raise FTPProtoError, resp - end - host = numbers[0, 4].join(".") - port = (numbers[4].to_i << 8) + numbers[5].to_i - return host, port - end - private :parse227 - - def parse228(resp) - if resp[0, 3] != "228" - raise FTPReplyError, resp - end - left = resp.index("(") - right = resp.index(")") - if left == nil or right == nil - raise FTPProtoError, resp - end - numbers = resp[left + 1 .. right - 1].split(",") - if numbers[0] == "4" - if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2" - raise FTPProtoError, resp - end - host = numbers[2, 4].join(".") - port = (numbers[7].to_i << 8) + numbers[8].to_i - elsif numbers[0] == "6" - if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2" - raise FTPProtoError, resp - end - v6 = ["", "", "", "", "", "", "", ""] - for i in 0 .. 7 - v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i, - numbers[(i * 2) + 3].to_i) - end - host = v6[0, 8].join(":") - port = (numbers[19].to_i << 8) + numbers[20].to_i - end - return host, port - end - private :parse228 - - def parse229(resp) - if resp[0, 3] != "229" - raise FTPReplyError, resp - end - left = resp.index("(") - right = resp.index(")") - if left == nil or right == nil - raise FTPProtoError, resp - end - numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1]) - if numbers.length != 4 - raise FTPProtoError, resp - end - port = numbers[3].to_i - host = (@sock.peeraddr())[3] - return host, port - end - private :parse229 - - def parse257(resp) - if resp[0, 3] != "257" - raise FTPReplyError, resp - end - if resp[3, 2] != ' "' - return "" - end - dirname = "" - i = 5 - n = resp.length - while i < n - c = resp[i, 1] - i = i + 1 - if c == '"' - if i > n or resp[i, 1] != '"' - break - end - i = i + 1 - end - dirname = dirname + c - end - return dirname - end - private :parse257 - end - -end diff --git a/lib/net/http.rb b/lib/net/http.rb index 8925c498a2..98d6793aee 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1,1071 +1,2608 @@ -=begin - -= net/http.rb version 1.2.0 - - maintained by Minero Aoki <aamine@dp.u-netsurf.ne.jp> - This file is derived from "http-access.rb". - - This program is free software. - You can distribute/modify this program under - the terms of the Ruby Distribute License. - - Japanese version of this document is in "net" full package. - You can get it from RAA - (Ruby Application Archive: http://www.ruby-lang.org/en/raa.html). - - -= class HTTP - -== Class Methods - -: new( address = 'localhost', port = 80, proxy_addr = nil, proxy_port = nil ) - creates a new Net::HTTP object. - If proxy_addr is given, this method is equals to - Net::HTTP::Proxy(proxy_addr,proxy_port). - -: start( address = 'localhost', port = 80, proxy_addr = nil, proxy_port = nil ) -: start( address = 'localhost', port = 80, proxy_addr = nil, proxy_port = nil ) {|http| .... } - is equals to - Net::HTTP.new( address, port, proxy_addr, proxy_port ).start(&block) - -: Proxy( address, port ) - creates a HTTP proxy class. - Arguments are address/port of proxy host. - You can replace HTTP class by this proxy class. - - # example - proxy_http = HTTP::Proxy( 'proxy.foo.org', 8080 ) - : - proxy_http.start( 'www.ruby-lang.org' ) do |http| - # connecting proxy.foo.org:8080 - : - end - -: proxy_class? - If self is HTTP, false. - If self is a class which was created by HTTP::Proxy(), true. - -: port - HTTP default port (80). - - -== Methods - -: start -: start {|http| .... } - creates a new Net::HTTP object and starts HTTP session. - - When this method is called with block, gives HTTP object to block - and close HTTP session after block call finished. - -: proxy? - true if self is a HTTP proxy class - -: proxy_address - address of proxy host. If self is not a proxy, nil. - -: proxy_port - port number of proxy host. If self is not a proxy, nil. - -: get( path, header = nil, dest = '' ) -: get( path, header = nil ) {|str| .... } - gets data from "path" on connecting host. - "header" must be a Hash like { 'Accept' => '*/*', ... }. - Response body is written into "dest" by using "<<" method. - This method returns Net::HTTPResponse object. - - # example - response = http.get( '/index.html' ) - - If called with block, give a part String of entity body. - -: head( path, header = nil ) - gets only header from "path" on connecting host. - "header" is a Hash like { 'Accept' => '*/*', ... }. - This method returns a Net::HTTPResponse object. - You can http header from this object like: - - response['content-length'] #-> '2554' - response['content-type'] #-> 'text/html' - response['Content-Type'] #-> 'text/html' - response['CoNtEnT-tYpe'] #-> 'text/html' - -: post( path, data, header = nil, dest = '' ) -: post( path, data, header = nil ) {|str| .... } - posts "data" (must be String) to "path". - If the body exists, also gets entity body. - Response body is written into "dest" by using "<<" method. - "header" must be a Hash like { 'Accept' => '*/*', ... }. - This method returns Net::HTTPResponse object. - - If called with block, gives a part of entity body string. - -: new_get( path, header = nil ) {|req| .... } - creates a new GET request object and gives it to the block. - see also for Get class reference. - - # example - http.new_get( '/~foo/bar.html' ) do |req| - req['accept'] = 'text/html' - response = req.dispatch - p response['Content-Type'] - puts response.read_header - end - -: new_head( path, header = nil ) {|req| .... } - creates a new HEAD request object and gives it to the block. - see also Head class reference. - -: new_post( path, header = nil ) {|req| .... } - creates a new POST request object and gives it to the block. - see also Post class reference. - - -= class Get, Head, Post - -HTTP request class. This class wraps request header and entity path. -All "key" is case-insensitive. - -== Methods - -: self[ key ] - returns header field for "key". - -: dispatch [only Get, Head] - dispatches request. - This method returns HTTPResponse object. - -: dispatch( data = '' ) [only Post] -: dispatch {|adapter| .... } [only Post] - dispatches request. "data" is - -= class HTTPResponse - -HTTP response class. This class wraps response header and entity. -All "key" is case-insensitive. - -== Methods - -: body - the entity body. ("dest" argument for HTTP#get, post, put) - -: self[ key ] - returns header field for "key". - for HTTP, value is a string like 'text/plain'(for Content-Type), - '2045'(for Content-Length), 'bytes 0-1024/10024'(for Content-Range). - Multiple header had be joined by HTTP1.1 scheme. - -: self[ key ] = val - set field value for "key". - -: key?( key ) - true if key exists - -: each {|name,value| .... } - iterates for each field name and value pair - -: code - HTTP result code string. For example, '302' - -: message - HTTP result message. For example, 'Not Found' - -: read_body( dest = '' ) -: body( dest = '' ) - gets response body. - It is written into "dest" using "<<" method. - If this method is called twice or more, nothing will be done and - returns first "dest". - -: read_body {|str| .... } -: body {|str| .... } - gets response body with block. - - -= Swithing Net::HTTP versions - -You can use Net::HTTP 1.1 features by calling HTTP.old_implementation. -And calling Net::HTTP.new_implementation allows you to use 1.2 features -again. - - # example - HTTP.start {|http1| ...(http1 has 1.2 features)... } - - HTTP.version_1_1 - HTTP.start {|http2| ...(http2 has 1.1 features)... } - - HTTP.version_1_2 - HTTP.start {|http3| ...(http3 has 1.2 features)... } - -=end +# frozen_string_literal: true +# +# = net/http.rb +# +# Copyright (c) 1999-2007 Yukihiro Matsumoto +# Copyright (c) 1999-2007 Minero Aoki +# Copyright (c) 2001 GOTOU Yuuzou +# +# Written and maintained by Minero Aoki <aamine@loveruby.net>. +# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>. +# +# This file is derived from "http-access.rb". +# +# Documented by Minero Aoki; converted to RDoc by William Webber. +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms of ruby itself --- +# Ruby Distribution License or GNU General Public License. +# +# See Net::HTTP for an overview and examples. +# require 'net/protocol' +require 'uri' +require 'resolv' +autoload :OpenSSL, 'openssl' +module Net #:nodoc: -module Net - + # :stopdoc: class HTTPBadResponse < StandardError; end - - + class HTTPHeaderSyntaxError < StandardError; end + # :startdoc: + + # \Class \Net::HTTP provides a rich library that implements the client + # in a client-server model that uses the \HTTP request-response protocol. + # For information about \HTTP, see: + # + # - {Hypertext Transfer Protocol}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol]. + # - {Technical overview}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Technical_overview]. + # + # == About the Examples + # + # :include: doc/net-http/examples.rdoc + # + # == Strategies + # + # - If you will make only a few GET requests, + # consider using {OpenURI}[rdoc-ref:OpenURI]. + # - If you will make only a few requests of all kinds, + # consider using the various singleton convenience methods in this class. + # Each of the following methods automatically starts and finishes + # a {session}[rdoc-ref:Net::HTTP@Sessions] that sends a single request: + # + # # Return string response body. + # Net::HTTP.get(hostname, path) + # Net::HTTP.get(uri) + # + # # Write string response body to $stdout. + # Net::HTTP.get_print(hostname, path) + # Net::HTTP.get_print(uri) + # + # # Return response as Net::HTTPResponse object. + # Net::HTTP.get_response(hostname, path) + # Net::HTTP.get_response(uri) + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # Net::HTTP.post(uri, data) + # params = {title: 'foo', body: 'bar', userId: 1} + # Net::HTTP.post_form(uri, params) + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # Net::HTTP.put(uri, data) + # + # - If performance is important, consider using sessions, which lower request overhead. + # This {session}[rdoc-ref:Net::HTTP@Sessions] has multiple requests for + # {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods] + # and {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]: + # + # Net::HTTP.start(hostname) do |http| + # # Session started automatically before block execution. + # http.get(path) + # http.head(path) + # body = 'Some text' + # http.post(path, body) # Can also have a block. + # http.put(path, body) + # http.delete(path) + # http.options(path) + # http.trace(path) + # http.patch(path, body) # Can also have a block. + # http.copy(path) + # http.lock(path, body) + # http.mkcol(path, body) + # http.move(path) + # http.propfind(path, body) + # http.proppatch(path, body) + # http.unlock(path, body) + # # Session finished automatically at block exit. + # end + # + # The methods cited above are convenience methods that, via their few arguments, + # allow minimal control over the requests. + # For greater control, consider using {request objects}[rdoc-ref:Net::HTTPRequest]. + # + # == URIs + # + # On the internet, a URI + # ({Universal Resource Identifier}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]) + # is a string that identifies a particular resource. + # It consists of some or all of: scheme, hostname, path, query, and fragment; + # see {URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax]. + # + # A Ruby {URI::Generic}[rdoc-ref:URI::Generic] object + # represents an internet URI. + # It provides, among others, methods + # +scheme+, +hostname+, +path+, +query+, and +fragment+. + # + # === Schemes + # + # An internet \URI has + # a {scheme}[https://en.wikipedia.org/wiki/List_of_URI_schemes]. + # + # The two schemes supported in \Net::HTTP are <tt>'https'</tt> and <tt>'http'</tt>: + # + # uri.scheme # => "https" + # URI('http://example.com').scheme # => "http" + # + # === Hostnames + # + # A hostname identifies a server (host) to which requests may be sent: + # + # hostname = uri.hostname # => "jsonplaceholder.typicode.com" + # Net::HTTP.start(hostname) do |http| + # # Some HTTP stuff. + # end + # + # === Paths + # + # A host-specific path identifies a resource on the host: + # + # _uri = uri.dup + # _uri.path = '/todos/1' + # hostname = _uri.hostname + # path = _uri.path + # Net::HTTP.get(hostname, path) + # + # === Queries + # + # A host-specific query adds name/value pairs to the URI: + # + # _uri = uri.dup + # params = {userId: 1, completed: false} + # _uri.query = URI.encode_www_form(params) + # _uri # => #<URI::HTTPS https://jsonplaceholder.typicode.com?userId=1&completed=false> + # Net::HTTP.get(_uri) + # + # === Fragments + # + # A {URI fragment}[https://en.wikipedia.org/wiki/URI_fragment] has no effect + # in \Net::HTTP; + # the same data is returned, regardless of whether a fragment is included. + # + # == Request Headers + # + # Request headers may be used to pass additional information to the host, + # similar to arguments passed in a method call; + # each header is a name/value pair. + # + # Each of the \Net::HTTP methods that sends a request to the host + # has optional argument +headers+, + # where the headers are expressed as a hash of field-name/value pairs: + # + # headers = {Accept: 'application/json', Connection: 'Keep-Alive'} + # Net::HTTP.get(uri, headers) + # + # See lists of both standard request fields and common request fields at + # {Request Fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields]. + # A host may also accept other custom fields. + # + # == \HTTP Sessions + # + # A _session_ is a connection between a server (host) and a client that: + # + # - Is begun by instance method Net::HTTP#start. + # - May contain any number of requests. + # - Is ended by instance method Net::HTTP#finish. + # + # See example sessions at {Strategies}[rdoc-ref:Net::HTTP@Strategies]. + # + # === Session Using \Net::HTTP.start + # + # If you have many requests to make to a single host (and port), + # consider using singleton method Net::HTTP.start with a block; + # the method handles the session automatically by: + # + # - Calling #start before block execution. + # - Executing the block. + # - Calling #finish after block execution. + # + # In the block, you can use these instance methods, + # each of which that sends a single request: + # + # - {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods]: + # + # - #get, #request_get: GET. + # - #head, #request_head: HEAD. + # - #post, #request_post: POST. + # - #delete: DELETE. + # - #options: OPTIONS. + # - #trace: TRACE. + # - #patch: PATCH. + # + # - {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]: + # + # - #copy: COPY. + # - #lock: LOCK. + # - #mkcol: MKCOL. + # - #move: MOVE. + # - #propfind: PROPFIND. + # - #proppatch: PROPPATCH. + # - #unlock: UNLOCK. + # + # === Session Using \Net::HTTP.start and \Net::HTTP.finish + # + # You can manage a session manually using methods #start and #finish: + # + # http = Net::HTTP.new(hostname) + # http.start + # http.get('/todos/1') + # http.get('/todos/2') + # http.delete('/posts/1') + # http.finish # Needed to free resources. + # + # === Single-Request Session + # + # Certain convenience methods automatically handle a session by: + # + # - Creating an \HTTP object + # - Starting a session. + # - Sending a single request. + # - Finishing the session. + # - Destroying the object. + # + # Such methods that send GET requests: + # + # - ::get: Returns the string response body. + # - ::get_print: Writes the string response body to $stdout. + # - ::get_response: Returns a Net::HTTPResponse object. + # + # Such methods that send POST requests: + # + # - ::post: Posts data to the host. + # - ::post_form: Posts form data to the host. + # + # == \HTTP Requests and Responses + # + # Many of the methods above are convenience methods, + # each of which sends a request and returns a string + # without directly using \Net::HTTPRequest and \Net::HTTPResponse objects. + # + # You can, however, directly create a request object, send the request, + # and retrieve the response object; see: + # + # - Net::HTTPRequest. + # - Net::HTTPResponse. + # + # == Following Redirection + # + # Each returned response is an instance of a subclass of Net::HTTPResponse. + # See the {response class hierarchy}[rdoc-ref:Net::HTTPResponse@Response+Subclasses]. + # + # In particular, class Net::HTTPRedirection is the parent + # of all redirection classes. + # This allows you to craft a case statement to handle redirections properly: + # + # def fetch(uri, limit = 10) + # # You should choose a better exception. + # raise ArgumentError, 'Too many HTTP redirects' if limit == 0 + # + # res = Net::HTTP.get_response(URI(uri)) + # case res + # when Net::HTTPSuccess # Any success class. + # res + # when Net::HTTPRedirection # Any redirection class. + # location = res['Location'] + # warn "Redirected to #{location}" + # fetch(location, limit - 1) + # else # Any other class. + # res.value + # end + # end + # + # fetch(uri) + # + # == Basic Authentication + # + # Basic authentication is performed according to + # {RFC2617}[http://www.ietf.org/rfc/rfc2617.txt]: + # + # req = Net::HTTP::Get.new(uri) + # req.basic_auth('user', 'pass') + # res = Net::HTTP.start(hostname) do |http| + # http.request(req) + # end + # + # == Streaming Response Bodies + # + # By default \Net::HTTP reads an entire response into memory. If you are + # handling large files or wish to implement a progress bar you can instead + # stream the body directly to an IO. + # + # Net::HTTP.start(hostname) do |http| + # req = Net::HTTP::Get.new(uri) + # http.request(req) do |res| + # open('t.tmp', 'w') do |f| + # res.read_body do |chunk| + # f.write chunk + # end + # end + # end + # end + # + # == HTTPS + # + # HTTPS is enabled for an \HTTP connection by Net::HTTP#use_ssl=: + # + # Net::HTTP.start(hostname, :use_ssl => true) do |http| + # req = Net::HTTP::Get.new(uri) + # res = http.request(req) + # end + # + # Or if you simply want to make a GET request, you may pass in a URI + # object that has an \HTTPS URL. \Net::HTTP automatically turns on TLS + # verification if the URI object has a 'https' URI scheme: + # + # uri # => #<URI::HTTPS https://jsonplaceholder.typicode.com/> + # Net::HTTP.get(uri) + # + # == Proxy Server + # + # An \HTTP object can have + # a {proxy server}[https://en.wikipedia.org/wiki/Proxy_server]. + # + # You can create an \HTTP object with a proxy server + # using method Net::HTTP.new or method Net::HTTP.start. + # + # The proxy may be defined either by argument +p_addr+ + # or by environment variable <tt>'http_proxy'</tt>. + # + # === Proxy Using Argument +p_addr+ as a \String + # + # When argument +p_addr+ is a string hostname, + # the returned +http+ has the given host as its proxy: + # + # http = Net::HTTP.new(hostname, nil, 'proxy.example') + # http.proxy? # => true + # http.proxy_from_env? # => false + # http.proxy_address # => "proxy.example" + # # These use default values. + # http.proxy_port # => 80 + # http.proxy_user # => nil + # http.proxy_pass # => nil + # + # The port, username, and password for the proxy may also be given: + # + # http = Net::HTTP.new(hostname, nil, 'proxy.example', 8000, 'pname', 'ppass') + # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false> + # http.proxy? # => true + # http.proxy_from_env? # => false + # http.proxy_address # => "proxy.example" + # http.proxy_port # => 8000 + # http.proxy_user # => "pname" + # http.proxy_pass # => "ppass" + # + # === Proxy Using '<tt>ENV['http_proxy']</tt>' + # + # When environment variable <tt>'http_proxy'</tt> + # is set to a \URI string, + # the returned +http+ will have the server at that URI as its proxy; + # note that the \URI string must have a protocol + # such as <tt>'http'</tt> or <tt>'https'</tt>: + # + # ENV['http_proxy'] = 'http://example.com' + # http = Net::HTTP.new(hostname) + # http.proxy? # => true + # http.proxy_from_env? # => true + # http.proxy_address # => "example.com" + # # These use default values. + # http.proxy_port # => 80 + # http.proxy_user # => nil + # http.proxy_pass # => nil + # + # The \URI string may include proxy username, password, and port number: + # + # ENV['http_proxy'] = 'http://pname:ppass@example.com:8000' + # http = Net::HTTP.new(hostname) + # http.proxy? # => true + # http.proxy_from_env? # => true + # http.proxy_address # => "example.com" + # http.proxy_port # => 8000 + # http.proxy_user # => "pname" + # http.proxy_pass # => "ppass" + # + # === Filtering Proxies + # + # With method Net::HTTP.new (but not Net::HTTP.start), + # you can use argument +p_no_proxy+ to filter proxies: + # + # - Reject a certain address: + # + # http = Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example') + # http.proxy_address # => nil + # + # - Reject certain domains or subdomains: + # + # http = Net::HTTP.new('example.com', nil, 'my.proxy.example', 8000, 'pname', 'ppass', 'proxy.example') + # http.proxy_address # => nil + # + # - Reject certain addresses and port combinations: + # + # http = Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:1234') + # http.proxy_address # => "proxy.example" + # + # http = Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:8000') + # http.proxy_address # => nil + # + # - Reject a list of the types above delimited using a comma: + # + # http = Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000') + # http.proxy_address # => nil + # + # http = Net::HTTP.new('example.com', nil, 'my.proxy', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000') + # http.proxy_address # => nil + # + # == Compression and Decompression + # + # \Net::HTTP does not compress the body of a request before sending. + # + # By default, \Net::HTTP adds header <tt>'Accept-Encoding'</tt> + # to a new {request object}[rdoc-ref:Net::HTTPRequest]: + # + # Net::HTTP::Get.new(uri)['Accept-Encoding'] + # # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" + # + # This requests the server to zip-encode the response body if there is one; + # the server is not required to do so. + # + # \Net::HTTP does not automatically decompress a response body + # if the response has header <tt>'Content-Range'</tt>. + # + # Otherwise decompression (or not) depends on the value of header + # {Content-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-encoding-response-header]: + # + # - <tt>'deflate'</tt>, <tt>'gzip'</tt>, or <tt>'x-gzip'</tt>: + # decompresses the body and deletes the header. + # - <tt>'none'</tt> or <tt>'identity'</tt>: + # does not decompress the body, but deletes the header. + # - Any other value: + # leaves the body and header unchanged. + # + # == What's Here + # + # First, what's elsewhere. Class Net::HTTP: + # + # - Inherits from {class Object}[rdoc-ref:Object@What-27s+Here]. + # + # This is a categorized summary of methods and attributes. + # + # === \Net::HTTP Objects + # + # - {::new}[rdoc-ref:Net::HTTP.new]: + # Creates a new instance. + # - {#inspect}[rdoc-ref:Net::HTTP#inspect]: + # Returns a string representation of +self+. + # + # === Sessions + # + # - {::start}[rdoc-ref:Net::HTTP.start]: + # Begins a new session in a new \Net::HTTP object. + # - {#started?}[rdoc-ref:Net::HTTP#started?]: + # Returns whether in a session. + # - {#finish}[rdoc-ref:Net::HTTP#finish]: + # Ends an active session. + # - {#start}[rdoc-ref:Net::HTTP#start]: + # Begins a new session in an existing \Net::HTTP object (+self+). + # + # === Connections + # + # - {:continue_timeout}[rdoc-ref:Net::HTTP#continue_timeout]: + # Returns the continue timeout. + # - {#continue_timeout=}[rdoc-ref:Net::HTTP#continue_timeout=]: + # Sets the continue timeout seconds. + # - {:keep_alive_timeout}[rdoc-ref:Net::HTTP#keep_alive_timeout]: + # Returns the keep-alive timeout. + # - {:keep_alive_timeout=}[rdoc-ref:Net::HTTP#keep_alive_timeout=]: + # Sets the keep-alive timeout. + # - {:max_retries}[rdoc-ref:Net::HTTP#max_retries]: + # Returns the maximum retries. + # - {#max_retries=}[rdoc-ref:Net::HTTP#max_retries=]: + # Sets the maximum retries. + # - {:open_timeout}[rdoc-ref:Net::HTTP#open_timeout]: + # Returns the open timeout. + # - {:open_timeout=}[rdoc-ref:Net::HTTP#open_timeout=]: + # Sets the open timeout. + # - {:read_timeout}[rdoc-ref:Net::HTTP#read_timeout]: + # Returns the open timeout. + # - {:read_timeout=}[rdoc-ref:Net::HTTP#read_timeout=]: + # Sets the read timeout. + # - {:ssl_timeout}[rdoc-ref:Net::HTTP#ssl_timeout]: + # Returns the ssl timeout. + # - {:ssl_timeout=}[rdoc-ref:Net::HTTP#ssl_timeout=]: + # Sets the ssl timeout. + # - {:write_timeout}[rdoc-ref:Net::HTTP#write_timeout]: + # Returns the write timeout. + # - {write_timeout=}[rdoc-ref:Net::HTTP#write_timeout=]: + # Sets the write timeout. + # + # === Requests + # + # - {::get}[rdoc-ref:Net::HTTP.get]: + # Sends a GET request and returns the string response body. + # - {::get_print}[rdoc-ref:Net::HTTP.get_print]: + # Sends a GET request and write the string response body to $stdout. + # - {::get_response}[rdoc-ref:Net::HTTP.get_response]: + # Sends a GET request and returns a response object. + # - {::post_form}[rdoc-ref:Net::HTTP.post_form]: + # Sends a POST request with form data and returns a response object. + # - {::post}[rdoc-ref:Net::HTTP.post]: + # Sends a POST request with data and returns a response object. + # - {::put}[rdoc-ref:Net::HTTP.put]: + # Sends a PUT request with data and returns a response object. + # - {#copy}[rdoc-ref:Net::HTTP#copy]: + # Sends a COPY request and returns a response object. + # - {#delete}[rdoc-ref:Net::HTTP#delete]: + # Sends a DELETE request and returns a response object. + # - {#get}[rdoc-ref:Net::HTTP#get]: + # Sends a GET request and returns a response object. + # - {#head}[rdoc-ref:Net::HTTP#head]: + # Sends a HEAD request and returns a response object. + # - {#lock}[rdoc-ref:Net::HTTP#lock]: + # Sends a LOCK request and returns a response object. + # - {#mkcol}[rdoc-ref:Net::HTTP#mkcol]: + # Sends a MKCOL request and returns a response object. + # - {#move}[rdoc-ref:Net::HTTP#move]: + # Sends a MOVE request and returns a response object. + # - {#options}[rdoc-ref:Net::HTTP#options]: + # Sends a OPTIONS request and returns a response object. + # - {#patch}[rdoc-ref:Net::HTTP#patch]: + # Sends a PATCH request and returns a response object. + # - {#post}[rdoc-ref:Net::HTTP#post]: + # Sends a POST request and returns a response object. + # - {#propfind}[rdoc-ref:Net::HTTP#propfind]: + # Sends a PROPFIND request and returns a response object. + # - {#proppatch}[rdoc-ref:Net::HTTP#proppatch]: + # Sends a PROPPATCH request and returns a response object. + # - {#put}[rdoc-ref:Net::HTTP#put]: + # Sends a PUT request and returns a response object. + # - {#request}[rdoc-ref:Net::HTTP#request]: + # Sends a request and returns a response object. + # - {#request_get}[rdoc-ref:Net::HTTP#request_get]: + # Sends a GET request and forms a response object; + # if a block given, calls the block with the object, + # otherwise returns the object. + # - {#request_head}[rdoc-ref:Net::HTTP#request_head]: + # Sends a HEAD request and forms a response object; + # if a block given, calls the block with the object, + # otherwise returns the object. + # - {#request_post}[rdoc-ref:Net::HTTP#request_post]: + # Sends a POST request and forms a response object; + # if a block given, calls the block with the object, + # otherwise returns the object. + # - {#send_request}[rdoc-ref:Net::HTTP#send_request]: + # Sends a request and returns a response object. + # - {#trace}[rdoc-ref:Net::HTTP#trace]: + # Sends a TRACE request and returns a response object. + # - {#unlock}[rdoc-ref:Net::HTTP#unlock]: + # Sends an UNLOCK request and returns a response object. + # + # === Responses + # + # - {:close_on_empty_response}[rdoc-ref:Net::HTTP#close_on_empty_response]: + # Returns whether to close connection on empty response. + # - {:close_on_empty_response=}[rdoc-ref:Net::HTTP#close_on_empty_response=]: + # Sets whether to close connection on empty response. + # - {:ignore_eof}[rdoc-ref:Net::HTTP#ignore_eof]: + # Returns whether to ignore end-of-file when reading a response body + # with <tt>Content-Length</tt> headers. + # - {:ignore_eof=}[rdoc-ref:Net::HTTP#ignore_eof=]: + # Sets whether to ignore end-of-file when reading a response body + # with <tt>Content-Length</tt> headers. + # - {:response_body_encoding}[rdoc-ref:Net::HTTP#response_body_encoding]: + # Returns the encoding to use for the response body. + # - {#response_body_encoding=}[rdoc-ref:Net::HTTP#response_body_encoding=]: + # Sets the response body encoding. + # + # === Proxies + # + # - {:proxy_address}[rdoc-ref:Net::HTTP#proxy_address]: + # Returns the proxy address. + # - {:proxy_address=}[rdoc-ref:Net::HTTP#proxy_address=]: + # Sets the proxy address. + # - {::proxy_class?}[rdoc-ref:Net::HTTP.proxy_class?]: + # Returns whether +self+ is a proxy class. + # - {#proxy?}[rdoc-ref:Net::HTTP#proxy?]: + # Returns whether +self+ has a proxy. + # - {#proxy_address}[rdoc-ref:Net::HTTP#proxy_address]: + # Returns the proxy address. + # - {#proxy_from_env?}[rdoc-ref:Net::HTTP#proxy_from_env?]: + # Returns whether the proxy is taken from an environment variable. + # - {:proxy_from_env=}[rdoc-ref:Net::HTTP#proxy_from_env=]: + # Sets whether the proxy is to be taken from an environment variable. + # - {:proxy_pass}[rdoc-ref:Net::HTTP#proxy_pass]: + # Returns the proxy password. + # - {:proxy_pass=}[rdoc-ref:Net::HTTP#proxy_pass=]: + # Sets the proxy password. + # - {:proxy_port}[rdoc-ref:Net::HTTP#proxy_port]: + # Returns the proxy port. + # - {:proxy_port=}[rdoc-ref:Net::HTTP#proxy_port=]: + # Sets the proxy port. + # - {#proxy_user}[rdoc-ref:Net::HTTP#proxy_user]: + # Returns the proxy user name. + # - {:proxy_user=}[rdoc-ref:Net::HTTP#proxy_user=]: + # Sets the proxy user. + # + # === Security + # + # - {:ca_file}[rdoc-ref:Net::HTTP#ca_file]: + # Returns the path to a CA certification file. + # - {:ca_file=}[rdoc-ref:Net::HTTP#ca_file=]: + # Sets the path to a CA certification file. + # - {:ca_path}[rdoc-ref:Net::HTTP#ca_path]: + # Returns the path of to CA directory containing certification files. + # - {:ca_path=}[rdoc-ref:Net::HTTP#ca_path=]: + # Sets the path of to CA directory containing certification files. + # - {:cert}[rdoc-ref:Net::HTTP#cert]: + # Returns the OpenSSL::X509::Certificate object to be used for client certification. + # - {:cert=}[rdoc-ref:Net::HTTP#cert=]: + # Sets the OpenSSL::X509::Certificate object to be used for client certification. + # - {:cert_store}[rdoc-ref:Net::HTTP#cert_store]: + # Returns the X509::Store to be used for verifying peer certificate. + # - {:cert_store=}[rdoc-ref:Net::HTTP#cert_store=]: + # Sets the X509::Store to be used for verifying peer certificate. + # - {:ciphers}[rdoc-ref:Net::HTTP#ciphers]: + # Returns the available SSL ciphers. + # - {:ciphers=}[rdoc-ref:Net::HTTP#ciphers=]: + # Sets the available SSL ciphers. + # - {:extra_chain_cert}[rdoc-ref:Net::HTTP#extra_chain_cert]: + # Returns the extra X509 certificates to be added to the certificate chain. + # - {:extra_chain_cert=}[rdoc-ref:Net::HTTP#extra_chain_cert=]: + # Sets the extra X509 certificates to be added to the certificate chain. + # - {:key}[rdoc-ref:Net::HTTP#key]: + # Returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + # - {:key=}[rdoc-ref:Net::HTTP#key=]: + # Sets the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + # - {:max_version}[rdoc-ref:Net::HTTP#max_version]: + # Returns the maximum SSL version. + # - {:max_version=}[rdoc-ref:Net::HTTP#max_version=]: + # Sets the maximum SSL version. + # - {:min_version}[rdoc-ref:Net::HTTP#min_version]: + # Returns the minimum SSL version. + # - {:min_version=}[rdoc-ref:Net::HTTP#min_version=]: + # Sets the minimum SSL version. + # - {#peer_cert}[rdoc-ref:Net::HTTP#peer_cert]: + # Returns the X509 certificate chain for the session's socket peer. + # - {:ssl_version}[rdoc-ref:Net::HTTP#ssl_version]: + # Returns the SSL version. + # - {:ssl_version=}[rdoc-ref:Net::HTTP#ssl_version=]: + # Sets the SSL version. + # - {#use_ssl=}[rdoc-ref:Net::HTTP#use_ssl=]: + # Sets whether a new session is to use Transport Layer Security. + # - {#use_ssl?}[rdoc-ref:Net::HTTP#use_ssl?]: + # Returns whether +self+ uses SSL. + # - {:verify_callback}[rdoc-ref:Net::HTTP#verify_callback]: + # Returns the callback for the server certification verification. + # - {:verify_callback=}[rdoc-ref:Net::HTTP#verify_callback=]: + # Sets the callback for the server certification verification. + # - {:verify_depth}[rdoc-ref:Net::HTTP#verify_depth]: + # Returns the maximum depth for the certificate chain verification. + # - {:verify_depth=}[rdoc-ref:Net::HTTP#verify_depth=]: + # Sets the maximum depth for the certificate chain verification. + # - {:verify_hostname}[rdoc-ref:Net::HTTP#verify_hostname]: + # Returns the flags for server the certification verification at the beginning of the SSL/TLS session. + # - {:verify_hostname=}[rdoc-ref:Net::HTTP#verify_hostname=]: + # Sets he flags for server the certification verification at the beginning of the SSL/TLS session. + # - {:verify_mode}[rdoc-ref:Net::HTTP#verify_mode]: + # Returns the flags for server the certification verification at the beginning of the SSL/TLS session. + # - {:verify_mode=}[rdoc-ref:Net::HTTP#verify_mode=]: + # Sets the flags for server the certification verification at the beginning of the SSL/TLS session. + # + # === Addresses and Ports + # + # - {:address}[rdoc-ref:Net::HTTP#address]: + # Returns the string host name or host IP. + # - {::default_port}[rdoc-ref:Net::HTTP.default_port]: + # Returns integer 80, the default port to use for HTTP requests. + # - {::http_default_port}[rdoc-ref:Net::HTTP.http_default_port]: + # Returns integer 80, the default port to use for HTTP requests. + # - {::https_default_port}[rdoc-ref:Net::HTTP.https_default_port]: + # Returns integer 443, the default port to use for HTTPS requests. + # - {#ipaddr}[rdoc-ref:Net::HTTP#ipaddr]: + # Returns the IP address for the connection. + # - {#ipaddr=}[rdoc-ref:Net::HTTP#ipaddr=]: + # Sets the IP address for the connection. + # - {:local_host}[rdoc-ref:Net::HTTP#local_host]: + # Returns the string local host used to establish the connection. + # - {:local_host=}[rdoc-ref:Net::HTTP#local_host=]: + # Sets the string local host used to establish the connection. + # - {:local_port}[rdoc-ref:Net::HTTP#local_port]: + # Returns the integer local port used to establish the connection. + # - {:local_port=}[rdoc-ref:Net::HTTP#local_port=]: + # Sets the integer local port used to establish the connection. + # - {:port}[rdoc-ref:Net::HTTP#port]: + # Returns the integer port number. + # + # === \HTTP Version + # + # - {::version_1_2?}[rdoc-ref:Net::HTTP.version_1_2?] + # (aliased as {::version_1_2}[rdoc-ref:Net::HTTP.version_1_2]): + # Returns true; retained for compatibility. + # + # === Debugging + # + # - {#set_debug_output}[rdoc-ref:Net::HTTP#set_debug_output]: + # Sets the output stream for debugging. + # class HTTP < Protocol - protocol_param :port, '80' - + # :stopdoc: + VERSION = "0.9.1" HTTPVersion = '1.1' - - - def addr_port - address + (port == HTTP.port ? '' : ":#{port}") + begin + require 'zlib' + HAVE_ZLIB=true + rescue LoadError + HAVE_ZLIB=false end + # :startdoc: + # Returns +true+; retained for compatibility. + def HTTP.version_1_2 + true + end - ### - ### proxy - ### - - class << self - - def Proxy( p_addr, p_port = nil ) - ::Net::NetPrivate::HTTPProxy.create_proxy_class( - p_addr, p_port || self.port ) - end - - alias orig_new new - - def new( address = nil, port = nil, p_addr = nil, p_port = nil ) - c = p_addr ? self::Proxy(p_addr, p_port) : self - i = c.orig_new( address, port ) - setvar i - i - end - - def start( address = nil, port = nil, p_addr = nil, p_port = nil, &block ) - new( address, port, p_addr, p_port ).start( &block ) - end - - def proxy_class? - false - end - - def proxy_address - nil - end - - def proxy_port - nil - end - + # Returns +true+; retained for compatibility. + def HTTP.version_1_2? + true end - def proxy? + # Returns +false+; retained for compatibility. + def HTTP.version_1_1? #:nodoc: false end - def proxy_address - nil + class << HTTP + alias is_version_1_1? version_1_1? #:nodoc: + alias is_version_1_2? version_1_2? #:nodoc: end - def proxy_port + # :call-seq: + # Net::HTTP.get_print(hostname, path, port = 80) -> nil + # Net::HTTP:get_print(uri, headers = {}, port = uri.port) -> nil + # + # Like Net::HTTP.get, but writes the returned body to $stdout; + # returns +nil+. + def HTTP.get_print(uri_or_host, path_or_headers = nil, port = nil) + get_response(uri_or_host, path_or_headers, port) {|res| + res.read_body do |chunk| + $stdout.print chunk + end + } nil end - def edit_path( path ) - path + # :call-seq: + # Net::HTTP.get(hostname, path, port = 80) -> body + # Net::HTTP:get(uri, headers = {}, port = uri.port) -> body + # + # Sends a GET request and returns the \HTTP response body as a string. + # + # With string arguments +hostname+ and +path+: + # + # hostname = 'jsonplaceholder.typicode.com' + # path = '/todos/1' + # puts Net::HTTP.get(hostname, path) + # + # Output: + # + # { + # "userId": 1, + # "id": 1, + # "title": "delectus aut autem", + # "completed": false + # } + # + # With URI object +uri+ and optional hash argument +headers+: + # + # uri = URI('https://jsonplaceholder.typicode.com/todos/1') + # headers = {'Content-type' => 'application/json; charset=UTF-8'} + # Net::HTTP.get(uri, headers) + # + # Related: + # + # - Net::HTTP::Get: request class for \HTTP method +GET+. + # - Net::HTTP#get: convenience method for \HTTP method +GET+. + # + def HTTP.get(uri_or_host, path_or_headers = nil, port = nil) + get_response(uri_or_host, path_or_headers, port).body + end + + # :call-seq: + # Net::HTTP.get_response(hostname, path, port = 80) -> http_response + # Net::HTTP:get_response(uri, headers = {}, port = uri.port) -> http_response + # + # Like Net::HTTP.get, but returns a Net::HTTPResponse object + # instead of the body string. + def HTTP.get_response(uri_or_host, path_or_headers = nil, port = nil, &block) + if path_or_headers && !path_or_headers.is_a?(Hash) + host = uri_or_host + path = path_or_headers + new(host, port || HTTP.default_port).start {|http| + return http.request_get(path, &block) + } + else + uri = uri_or_host + headers = path_or_headers + start(uri.hostname, uri.port, + :use_ssl => uri.scheme == 'https') {|http| + return http.request_get(uri, headers, &block) + } + end end + # Posts data to a host; returns a Net::HTTPResponse object. + # + # Argument +url+ must be a URL; + # argument +data+ must be a string: + # + # _uri = uri.dup + # _uri.path = '/posts' + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # headers = {'content-type': 'application/json'} + # res = Net::HTTP.post(_uri, data, headers) # => #<Net::HTTPCreated 201 Created readbody=true> + # puts res.body + # + # Output: + # + # { + # "title": "foo", + # "body": "bar", + # "userId": 1, + # "id": 101 + # } + # + # Related: + # + # - Net::HTTP::Post: request class for \HTTP method +POST+. + # - Net::HTTP#post: convenience method for \HTTP method +POST+. + # + def HTTP.post(url, data, header = nil) + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.post(url, data, header) + } + end - ### - ### for compatibility - ### - - @@newimpl = true + # Posts data to a host; returns a Net::HTTPResponse object. + # + # Argument +url+ must be a URI; + # argument +data+ must be a hash: + # + # _uri = uri.dup + # _uri.path = '/posts' + # data = {title: 'foo', body: 'bar', userId: 1} + # res = Net::HTTP.post_form(_uri, data) # => #<Net::HTTPCreated 201 Created readbody=true> + # puts res.body + # + # Output: + # + # { + # "title": "foo", + # "body": "bar", + # "userId": "1", + # "id": 101 + # } + # + def HTTP.post_form(url, params) + req = Post.new(url) + req.form_data = params + req.basic_auth url.user, url.password if url.user + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.request(req) + } + end - class << self + # Sends a PUT request to the server; returns a Net::HTTPResponse object. + # + # Argument +url+ must be a URL; + # argument +data+ must be a string: + # + # _uri = uri.dup + # _uri.path = '/posts' + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # headers = {'content-type': 'application/json'} + # res = Net::HTTP.put(_uri, data, headers) # => #<Net::HTTPCreated 201 Created readbody=true> + # puts res.body + # + # Output: + # + # { + # "title": "foo", + # "body": "bar", + # "userId": 1, + # "id": 101 + # } + # + # Related: + # + # - Net::HTTP::Put: request class for \HTTP method +PUT+. + # - Net::HTTP#put: convenience method for \HTTP method +PUT+. + # + def HTTP.put(url, data, header = nil) + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.put(url, data, header) + } + end - def version_1_2 - @@newimpl = true + # + # \HTTP session management + # + + # Returns integer +80+, the default port to use for \HTTP requests: + # + # Net::HTTP.default_port # => 80 + # + def HTTP.default_port + http_default_port() + end + + # Returns integer +80+, the default port to use for \HTTP requests: + # + # Net::HTTP.http_default_port # => 80 + # + def HTTP.http_default_port + 80 + end + + # Returns integer +443+, the default port to use for HTTPS requests: + # + # Net::HTTP.https_default_port # => 443 + # + def HTTP.https_default_port + 443 + end + + def HTTP.socket_type #:nodoc: obsolete + BufferedIO + end + + # :call-seq: + # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) -> http + # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) {|http| ... } -> object + # + # Creates a new \Net::HTTP object, +http+, via \Net::HTTP.new: + # + # - For arguments +address+ and +port+, see Net::HTTP.new. + # - For proxy-defining arguments +p_addr+ through +p_pass+, + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + # - For argument +opts+, see below. + # + # With no block given: + # + # - Calls <tt>http.start</tt> with no block (see #start), + # which opens a TCP connection and \HTTP session. + # - Returns +http+. + # - The caller should call #finish to close the session: + # + # http = Net::HTTP.start(hostname) + # http.started? # => true + # http.finish + # http.started? # => false + # + # With a block given: + # + # - Calls <tt>http.start</tt> with the block (see #start), which: + # + # - Opens a TCP connection and \HTTP session. + # - Calls the block, + # which may make any number of requests to the host. + # - Closes the \HTTP session and TCP connection on block exit. + # - Returns the block's value +object+. + # + # - Returns +object+. + # + # Example: + # + # hostname = 'jsonplaceholder.typicode.com' + # Net::HTTP.start(hostname) do |http| + # puts http.get('/todos/1').body + # puts http.get('/todos/2').body + # end + # + # Output: + # + # { + # "userId": 1, + # "id": 1, + # "title": "delectus aut autem", + # "completed": false + # } + # { + # "userId": 1, + # "id": 2, + # "title": "quis ut nam facilis et officia qui", + # "completed": false + # } + # + # If the last argument given is a hash, it is the +opts+ hash, + # where each key is a method or accessor to be called, + # and its value is the value to be set. + # + # The keys may include: + # + # - #ca_file + # - #ca_path + # - #cert + # - #cert_store + # - #ciphers + # - #close_on_empty_response + # - +ipaddr+ (calls #ipaddr=) + # - #keep_alive_timeout + # - #key + # - #open_timeout + # - #read_timeout + # - #ssl_timeout + # - #ssl_version + # - +use_ssl+ (calls #use_ssl=) + # - #verify_callback + # - #verify_depth + # - #verify_mode + # - #write_timeout + # + # Note: If +port+ is +nil+ and <tt>opts[:use_ssl]</tt> is a truthy value, + # the value passed to +new+ is Net::HTTP.https_default_port, not +port+. + # + def HTTP.start(address, *arg, &block) # :yield: +http+ + arg.pop if opt = Hash.try_convert(arg[-1]) + port, p_addr, p_port, p_user, p_pass = *arg + p_addr = :ENV if arg.size < 2 + port = https_default_port if !port && opt && opt[:use_ssl] + http = new(address, port, p_addr, p_port, p_user, p_pass) + http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr] + + if opt + if opt[:use_ssl] + opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt) + end + http.methods.grep(/\A(\w+)=\z/) do |meth| + key = $1.to_sym + opt.key?(key) or next + http.__send__(meth, opt[key]) + end end - def version_1_1 - @@newimpl = false + http.start(&block) + end + + class << HTTP + alias newobj new # :nodoc: + end + + # Returns a new \Net::HTTP object +http+ + # (but does not open a TCP connection or \HTTP session). + # + # With only string argument +address+ given + # (and <tt>ENV['http_proxy']</tt> undefined or +nil+), + # the returned +http+: + # + # - Has the given address. + # - Has the default port number, Net::HTTP.default_port (80). + # - Has no proxy. + # + # Example: + # + # http = Net::HTTP.new(hostname) + # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false> + # http.address # => "jsonplaceholder.typicode.com" + # http.port # => 80 + # http.proxy? # => false + # + # With integer argument +port+ also given, + # the returned +http+ has the given port: + # + # http = Net::HTTP.new(hostname, 8000) + # # => #<Net::HTTP jsonplaceholder.typicode.com:8000 open=false> + # http.port # => 8000 + # + # For proxy-defining arguments +p_addr+ through +p_no_proxy+, + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + # + def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil, p_use_ssl = nil) + http = super address, port + + if proxy_class? then # from Net::HTTP::Proxy() + http.proxy_from_env = @proxy_from_env + http.proxy_address = @proxy_address + http.proxy_port = @proxy_port + http.proxy_user = @proxy_user + http.proxy_pass = @proxy_pass + http.proxy_use_ssl = @proxy_use_ssl + elsif p_addr == :ENV then + http.proxy_from_env = true + else + if p_addr && p_no_proxy && !URI::Generic.use_proxy?(address, address, port, p_no_proxy) + p_addr = nil + p_port = nil + end + http.proxy_address = p_addr + http.proxy_port = p_port || default_port + http.proxy_user = p_user + http.proxy_pass = p_pass + http.proxy_use_ssl = p_use_ssl end - private + http + end + + class << HTTP + # Allows to set the default configuration that will be used + # when creating a new connection. + # + # Example: + # + # Net::HTTP.default_configuration = { + # read_timeout: 1, + # write_timeout: 1 + # } + # http = Net::HTTP.new(hostname) + # http.open_timeout # => 60 + # http.read_timeout # => 1 + # http.write_timeout # => 1 + # + attr_accessor :default_configuration + end + + # Creates a new \Net::HTTP object for the specified server address, + # without opening the TCP connection or initializing the \HTTP session. + # The +address+ should be a DNS hostname or IP address. + def initialize(address, port = nil) # :nodoc: + defaults = { + keep_alive_timeout: 2, + close_on_empty_response: false, + open_timeout: 60, + read_timeout: 60, + write_timeout: 60, + continue_timeout: nil, + max_retries: 1, + debug_output: nil, + response_body_encoding: false, + ignore_eof: true + } + options = defaults.merge(self.class.default_configuration || {}) - def setvar( obj ) - f = @@newimpl - obj.instance_eval { @newimpl = f } + @address = address + @port = (port || HTTP.default_port) + @ipaddr = nil + @local_host = nil + @local_port = nil + @curr_http_version = HTTPVersion + @keep_alive_timeout = options[:keep_alive_timeout] + @last_communicated = nil + @close_on_empty_response = options[:close_on_empty_response] + @socket = nil + @started = false + @open_timeout = options[:open_timeout] + @read_timeout = options[:read_timeout] + @write_timeout = options[:write_timeout] + @continue_timeout = options[:continue_timeout] + @max_retries = options[:max_retries] + @debug_output = options[:debug_output] + @response_body_encoding = options[:response_body_encoding] + @ignore_eof = options[:ignore_eof] + @tcpsocket_supports_open_timeout = nil + + @proxy_from_env = false + @proxy_uri = nil + @proxy_address = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = nil + @proxy_use_ssl = nil + + @use_ssl = false + @ssl_context = nil + @ssl_session = nil + @sspi_enabled = false + SSL_IVNAMES.each do |ivname| + instance_variable_set ivname, nil end - end + # Returns a string representation of +self+: + # + # Net::HTTP.new(hostname).inspect + # # => "#<Net::HTTP jsonplaceholder.typicode.com:80 open=false>" + # + def inspect + "#<#{self.class} #{@address}:#{@port} open=#{started?}>" + end + + # *WARNING* This method opens a serious security hole. + # Never use this method in production code. + # + # Sets the output stream for debugging: + # + # http = Net::HTTP.new(hostname) + # File.open('t.tmp', 'w') do |file| + # http.set_debug_output(file) + # http.start + # http.get('/nosuch/1') + # http.finish + # end + # puts File.read('t.tmp') + # + # Output: + # + # opening connection to jsonplaceholder.typicode.com:80... + # opened + # <- "GET /nosuch/1 HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: jsonplaceholder.typicode.com\r\n\r\n" + # -> "HTTP/1.1 404 Not Found\r\n" + # -> "Date: Mon, 12 Dec 2022 21:14:11 GMT\r\n" + # -> "Content-Type: application/json; charset=utf-8\r\n" + # -> "Content-Length: 2\r\n" + # -> "Connection: keep-alive\r\n" + # -> "X-Powered-By: Express\r\n" + # -> "X-Ratelimit-Limit: 1000\r\n" + # -> "X-Ratelimit-Remaining: 999\r\n" + # -> "X-Ratelimit-Reset: 1670879660\r\n" + # -> "Vary: Origin, Accept-Encoding\r\n" + # -> "Access-Control-Allow-Credentials: true\r\n" + # -> "Cache-Control: max-age=43200\r\n" + # -> "Pragma: no-cache\r\n" + # -> "Expires: -1\r\n" + # -> "X-Content-Type-Options: nosniff\r\n" + # -> "Etag: W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"\r\n" + # -> "Via: 1.1 vegur\r\n" + # -> "CF-Cache-Status: MISS\r\n" + # -> "Server-Timing: cf-q-config;dur=1.3000000762986e-05\r\n" + # -> "Report-To: {\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=yOr40jo%2BwS1KHzhTlVpl54beJ5Wx2FcG4gGV0XVrh3X9OlR5q4drUn2dkt5DGO4GDcE%2BVXT7CNgJvGs%2BZleIyMu8CLieFiDIvOviOY3EhHg94m0ZNZgrEdpKD0S85S507l1vsEwEHkoTm%2Ff19SiO\"}],\"group\":\"cf-nel\",\"max_age\":604800}\r\n" + # -> "NEL: {\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}\r\n" + # -> "Server: cloudflare\r\n" + # -> "CF-RAY: 778977dc484ce591-DFW\r\n" + # -> "alt-svc: h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400\r\n" + # -> "\r\n" + # reading 2 bytes... + # -> "{}" + # read 2 bytes + # Conn keep-alive + # + def set_debug_output(output) + warn 'Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started? + @debug_output = output + end + + # Returns the string host name or host IP given as argument +address+ in ::new. + attr_reader :address + + # Returns the integer port number given as argument +port+ in ::new. + attr_reader :port + + # Sets or returns the string local host used to establish the connection; + # initially +nil+. + attr_accessor :local_host + + # Sets or returns the integer local port used to establish the connection; + # initially +nil+. + attr_accessor :local_port + + # Returns the encoding to use for the response body; + # see #response_body_encoding=. + attr_reader :response_body_encoding + + # Sets the encoding to be used for the response body; + # returns the encoding. + # + # The given +value+ may be: + # + # - An Encoding object. + # - The name of an encoding. + # - An alias for an encoding name. + # + # See {Encoding}[rdoc-ref:Encoding]. + # + # Examples: + # + # http = Net::HTTP.new(hostname) + # http.response_body_encoding = Encoding::US_ASCII # => #<Encoding:US-ASCII> + # http.response_body_encoding = 'US-ASCII' # => "US-ASCII" + # http.response_body_encoding = 'ASCII' # => "ASCII" + # + def response_body_encoding=(value) + value = Encoding.find(value) if value.is_a?(String) + @response_body_encoding = value + end + + # Sets whether to determine the proxy from environment variable + # '<tt>ENV['http_proxy']</tt>'; + # see {Proxy Using ENV['http_proxy']}[rdoc-ref:Net::HTTP@Proxy+Using+-27ENV-5B-27http_proxy-27-5D-27]. + attr_writer :proxy_from_env + + # Sets the proxy address; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + attr_writer :proxy_address + + # Sets the proxy port; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + attr_writer :proxy_port + + # Sets the proxy user; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + attr_writer :proxy_user + + # Sets the proxy password; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + attr_writer :proxy_pass + + # Sets whether the proxy uses SSL; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + attr_writer :proxy_use_ssl + + # Returns the IP address for the connection. + # + # If the session has not been started, + # returns the value set by #ipaddr=, + # or +nil+ if it has not been set: + # + # http = Net::HTTP.new(hostname) + # http.ipaddr # => nil + # http.ipaddr = '172.67.155.76' + # http.ipaddr # => "172.67.155.76" + # + # If the session has been started, + # returns the IP address from the socket: + # + # http = Net::HTTP.new(hostname) + # http.start + # http.ipaddr # => "172.67.155.76" + # http.finish + # + def ipaddr + started? ? @socket.io.peeraddr[3] : @ipaddr + end + + # Sets the IP address for the connection: + # + # http = Net::HTTP.new(hostname) + # http.ipaddr # => nil + # http.ipaddr = '172.67.155.76' + # http.ipaddr # => "172.67.155.76" + # + # The IP address may not be set if the session has been started. + def ipaddr=(addr) + raise IOError, "ipaddr value changed, but session already started" if started? + @ipaddr = addr + end + + # Sets or returns the numeric (\Integer or \Float) number of seconds + # to wait for a connection to open; + # initially 60. + # If the connection is not made in the given interval, + # an exception is raised. + attr_accessor :open_timeout + + # Returns the numeric (\Integer or \Float) number of seconds + # to wait for one block to be read (via one read(2) call); + # see #read_timeout=. + attr_reader :read_timeout + + # Returns the numeric (\Integer or \Float) number of seconds + # to wait for one block to be written (via one write(2) call); + # see #write_timeout=. + attr_reader :write_timeout + + # Sets the maximum number of times to retry an idempotent request in case of + # \Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET, + # Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError, + # Timeout::Error. + # The initial value is 1. + # + # Argument +retries+ must be a non-negative numeric value: + # + # http = Net::HTTP.new(hostname) + # http.max_retries = 2 # => 2 + # http.max_retries # => 2 + # + def max_retries=(retries) + retries = retries.to_int + if retries < 0 + raise ArgumentError, 'max_retries should be non-negative integer number' + end + @max_retries = retries + end + + # Returns the maximum number of times to retry an idempotent request; + # see #max_retries=. + attr_reader :max_retries + + # Sets the read timeout, in seconds, for +self+ to integer +sec+; + # the initial value is 60. + # + # Argument +sec+ must be a non-negative numeric value: + # + # http = Net::HTTP.new(hostname) + # http.read_timeout # => 60 + # http.get('/todos/1') # => #<Net::HTTPOK 200 OK readbody=true> + # http.read_timeout = 0 + # http.get('/todos/1') # Raises Net::ReadTimeout. + # + def read_timeout=(sec) + @socket.read_timeout = sec if @socket + @read_timeout = sec + end + + # Sets the write timeout, in seconds, for +self+ to integer +sec+; + # the initial value is 60. + # + # Argument +sec+ must be a non-negative numeric value: + # + # _uri = uri.dup + # _uri.path = '/posts' + # body = 'bar' * 200000 + # data = <<EOF + # {"title": "foo", "body": "#{body}", "userId": "1"} + # EOF + # headers = {'content-type': 'application/json'} + # http = Net::HTTP.new(hostname) + # http.write_timeout # => 60 + # http.post(_uri.path, data, headers) + # # => #<Net::HTTPCreated 201 Created readbody=true> + # http.write_timeout = 0 + # http.post(_uri.path, data, headers) # Raises Net::WriteTimeout. + # + def write_timeout=(sec) + @socket.write_timeout = sec if @socket + @write_timeout = sec + end + + # Returns the continue timeout value; + # see continue_timeout=. + attr_reader :continue_timeout + + # Sets the continue timeout value, + # which is the number of seconds to wait for an expected 100 Continue response. + # If the \HTTP object does not receive a response in this many seconds + # it sends the request body. + def continue_timeout=(sec) + @socket.continue_timeout = sec if @socket + @continue_timeout = sec + end + + # Sets or returns the numeric (\Integer or \Float) number of seconds + # to keep the connection open after a request is sent; + # initially 2. + # If a new request is made during the given interval, + # the still-open connection is used; + # otherwise the connection will have been closed + # and a new connection is opened. + attr_accessor :keep_alive_timeout + + # Sets or returns whether to ignore end-of-file when reading a response body + # with <tt>Content-Length</tt> headers; + # initially +true+. + attr_accessor :ignore_eof + + # Returns +true+ if the \HTTP session has been started: + # + # http = Net::HTTP.new(hostname) + # http.started? # => false + # http.start + # http.started? # => true + # http.finish # => nil + # http.started? # => false + # + # Net::HTTP.start(hostname) do |http| + # http.started? + # end # => true + # http.started? # => false + # + def started? + @started + end + + alias active? started? #:nodoc: obsolete + + # Sets or returns whether to close the connection when the response is empty; + # initially +false+. + attr_accessor :close_on_empty_response + + # Returns +true+ if +self+ uses SSL, +false+ otherwise. + # See Net::HTTP#use_ssl=. + def use_ssl? + @use_ssl + end + + # Sets whether a new session is to use + # {Transport Layer Security}[https://en.wikipedia.org/wiki/Transport_Layer_Security]: + # + # Raises IOError if attempting to change during a session. + # + # Raises OpenSSL::SSL::SSLError if the port is not an HTTPS port. + def use_ssl=(flag) + flag = flag ? true : false + if started? and @use_ssl != flag + raise IOError, "use_ssl value changed, but session already started" + end + @use_ssl = flag + end + + SSL_ATTRIBUTES = [ + :ca_file, + :ca_path, + :cert, + :cert_store, + :ciphers, + :extra_chain_cert, + :key, + :ssl_timeout, + :ssl_version, + :min_version, + :max_version, + :verify_callback, + :verify_depth, + :verify_mode, + :verify_hostname, + ].freeze # :nodoc: + + SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym }.freeze # :nodoc: + + # Sets or returns the path to a CA certification file in PEM format. + attr_accessor :ca_file + + # Sets or returns the path of to CA directory + # containing certification files in PEM format. + attr_accessor :ca_path + + # Sets or returns the OpenSSL::X509::Certificate object + # to be used for client certification. + attr_accessor :cert + + # Sets or returns the X509::Store to be used for verifying peer certificate. + attr_accessor :cert_store + + # Sets or returns the available SSL ciphers. + # See {OpenSSL::SSL::SSLContext#ciphers=}[OpenSSL::SSL::SSL::Context#ciphers=]. + attr_accessor :ciphers + + # Sets or returns the extra X509 certificates to be added to the certificate chain. + # See {OpenSSL::SSL::SSLContext#add_certificate}[OpenSSL::SSL::SSL::Context#add_certificate]. + attr_accessor :extra_chain_cert + + # Sets or returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + attr_accessor :key + + # Sets or returns the SSL timeout seconds. + attr_accessor :ssl_timeout + + # Sets or returns the SSL version. + # See {OpenSSL::SSL::SSLContext#ssl_version=}[OpenSSL::SSL::SSL::Context#ssl_version=]. + attr_accessor :ssl_version + + # Sets or returns the minimum SSL version. + # See {OpenSSL::SSL::SSLContext#min_version=}[OpenSSL::SSL::SSL::Context#min_version=]. + attr_accessor :min_version + + # Sets or returns the maximum SSL version. + # See {OpenSSL::SSL::SSLContext#max_version=}[OpenSSL::SSL::SSL::Context#max_version=]. + attr_accessor :max_version + + # Sets or returns the callback for the server certification verification. + attr_accessor :verify_callback + + # Sets or returns the maximum depth for the certificate chain verification. + attr_accessor :verify_depth + + # Sets or returns the flags for server the certification verification + # at the beginning of the SSL/TLS session. + # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable. + attr_accessor :verify_mode + + # Sets or returns whether to verify that the server certificate is valid + # for the hostname. + # See {OpenSSL::SSL::SSLContext#verify_hostname=}[OpenSSL::SSL::SSL::Context#verify_hostname=]. + attr_accessor :verify_hostname + + # Returns the X509 certificate chain (an array of strings) + # for the session's socket peer, + # or +nil+ if none. + def peer_cert + if not use_ssl? or not @socket + return nil + end + @socket.io.peer_cert + end + + # Starts an \HTTP session. + # + # Without a block, returns +self+: + # + # http = Net::HTTP.new(hostname) + # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false> + # http.start + # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=true> + # http.started? # => true + # http.finish + # + # With a block, calls the block with +self+, + # finishes the session when the block exits, + # and returns the block's value: + # + # http.start do |http| + # http + # end + # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false> + # http.started? # => false + # + def start # :yield: http + raise IOError, 'HTTP session already opened' if @started + if block_given? + begin + do_start + return yield(self) + ensure + do_finish + end + end + do_start + self + end - ### - ### http operations - ### + # Finishes the \HTTP session: + # + # http = Net::HTTP.new(hostname) + # http.start + # http.started? # => true + # http.finish # => nil + # http.started? # => false + # + # Raises IOError if not in a session. + def finish + raise IOError, 'HTTP session not yet started' unless started? + do_finish + end + + # :stopdoc: + def do_start + connect + @started = true + end + private :do_start + + def connect + if use_ssl? + # reference early to load OpenSSL before connecting, + # as OpenSSL may take time to load. + @ssl_context = OpenSSL::SSL::SSLContext.new + end - def self.defrequest( nm, hasdest, hasdata ) - name = nm.id2name.downcase - cname = nm.id2name - lineno = __LINE__ + 2 - src = <<S + if proxy? then + conn_addr = proxy_address + conn_port = proxy_port + else + conn_addr = conn_address + conn_port = port + end - def #{name}( path, #{hasdata ? 'data,' : ''} - u_header = nil #{hasdest ? ',dest = nil, &block' : ''} ) - resp = #{name}2( path, - #{hasdata ? 'data,' : ''} - u_header ) {|resp| - resp.read_body( #{hasdest ? 'dest, &block' : ''} ) - } - if @newimpl then - resp + debug "opening connection to #{conn_addr}:#{conn_port}..." + begin + s = timeouted_connect(conn_addr, conn_port) + rescue => e + if (defined?(IO::TimeoutError) && e.is_a?(IO::TimeoutError)) || e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions + e = Net::OpenTimeout.new(e) + end + raise e, "Failed to open TCP connection to " + + "#{conn_addr}:#{conn_port} (#{e.message})" + end + s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + debug "opened" + if use_ssl? + if proxy? + if @proxy_use_ssl + proxy_sock = OpenSSL::SSL::SSLSocket.new(s) + ssl_socket_connect(proxy_sock, @open_timeout) else - resp.value - #{hasdest ? 'return resp, resp.body' : 'resp'} + proxy_sock = s end + proxy_sock = BufferedIO.new(proxy_sock, read_timeout: @read_timeout, + write_timeout: @write_timeout, + continue_timeout: @continue_timeout, + debug_output: @debug_output) + buf = +"CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n" \ + "Host: #{@address}:#{@port}\r\n" + if proxy_user + credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0') + buf << "Proxy-Authorization: Basic #{credential}\r\n" + end + buf << "\r\n" + proxy_sock.write(buf) + HTTPResponse.read_new(proxy_sock).value + # assuming nothing left in buffers after successful CONNECT response end - def #{name}2( path, #{hasdata ? 'data,' : ''} - u_header = nil ) - new_#{name}( path, u_header ) do |req| - resp = req.dispatch#{hasdata ? '(data)' : ''} - yield resp if block_given? + ssl_parameters = Hash.new + iv_list = instance_variables + SSL_IVNAMES.each_with_index do |ivname, i| + if iv_list.include?(ivname) + value = instance_variable_get(ivname) + unless value.nil? + ssl_parameters[SSL_ATTRIBUTES[i]] = value + end end end - - def new_#{name}( path, u_header = nil, &block ) - common_oper ::Net::NetPrivate::#{cname}, path, u_header, &block + @ssl_context.set_params(ssl_parameters) + unless @ssl_context.session_cache_mode.nil? # a dummy method on JRuby + @ssl_context.session_cache_mode = + OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | + OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE + end + if @ssl_context.respond_to?(:session_new_cb) # not implemented under JRuby + @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess } end -S - # puts src - module_eval src, __FILE__, lineno - end - - - defrequest :Get, true, false - defrequest :Head, false, false - defrequest :Post, true, true - defrequest :Put, false, true - - - private - - - def initialize( addr = nil, port = nil ) - super - @command = ::Net::NetPrivate::Switch.new - @curr_http_version = HTTPVersion - end - - def connect( addr = @address, port = @port ) - @socket = type.socket_type.open( addr, port, @pipe ) - end - def disconnect - if @socket and not @socket.closed? then - @socket.close - end - @socket = nil - end + # Still do the post_connection_check below even if connecting + # to IP address + verify_hostname = @ssl_context.verify_hostname - def do_finish - end + # Server Name Indication (SNI) RFC 3546/6066 + case @address + when Resolv::IPv4::Regex, Resolv::IPv6::Regex + # don't set SNI, as IP addresses in SNI is not valid + # per RFC 6066, section 3. + # Avoid openssl warning + @ssl_context.verify_hostname = false + else + ssl_host_address = @address + end - def common_oper( reqc, path, u_header ) - req = nil + debug "starting SSL for #{conn_addr}:#{conn_port}..." + s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) + s.sync_close = true + s.hostname = ssl_host_address if s.respond_to?(:hostname=) && ssl_host_address - @command.on - if not @socket then - start - elsif @socket.closed? then - @socket.reopen + if @ssl_session and + Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout + s.session = @ssl_session + end + ssl_socket_connect(s, @open_timeout) + if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && verify_hostname + s.post_connection_check(@address) + end + debug "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}" end - - req = reqc.new( @curr_http_version, - @socket, inihead, - edit_path(path), u_header ) - yield req if block_given? - req.terminate - @curr_http_version = req.http_version - - unless keep_alive? req, req.response then - @socket.close + @socket = BufferedIO.new(s, read_timeout: @read_timeout, + write_timeout: @write_timeout, + continue_timeout: @continue_timeout, + debug_output: @debug_output) + @last_communicated = nil + on_connect + rescue => exception + if s + debug "Conn close because of connect error #{exception}" + s.close end - @command.off - - req.response + raise end + private :connect - def inihead - h = {} - h['Host'] = addr_port - h['Connection'] = 'Keep-Alive' - h['Accept'] = '*/*' - h + tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters + TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]] + tcp_socket_parameters.include?([:key, :open_timeout]) + else + # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize + # See discussion in https://github.com/ruby/net-http/pull/224 + Socket.method(:tcp).parameters.include?([:key, :open_timeout]) end + private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT - def keep_alive?( request, response ) - if response.key? 'connection' then - if /keep-alive/i === response['connection'] then - return true - end - elsif response.key? 'proxy-connection' then - if /keep-alive/i === response['proxy-connection'] then - return true - end - elsif request.key? 'Connection' then - if /keep-alive/i === request['Connection'] then - return true - end + def timeouted_connect(conn_addr, conn_port) + if TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port, open_timeout: @open_timeout) else - if @curr_http_version == '1.1' then - return true - end + Timeout.timeout(@open_timeout, Net::OpenTimeout) { + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) + } end - - false end + private :timeouted_connect - end - - HTTPSession = HTTP - - - - module NetPrivate - - class Switch - def initialize - @critical = false - end - - def critical? - @critical - end - - def on - @critical = true + def on_connect end + private :on_connect - def off - @critical = false + def do_finish + @started = false + @socket.close if @socket + @socket = nil end - end - - module HTTPProxy - - class << self - - def create_proxy_class( p_addr, p_port ) - klass = Class.new( HTTP ) - klass.module_eval { - include HTTPProxy + private :do_finish + + # + # proxy + # + + public + + # no proxy + @is_proxy_class = false + @proxy_from_env = false + @proxy_addr = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = nil + @proxy_use_ssl = nil + + # Creates an \HTTP proxy class which behaves like \Net::HTTP, but + # performs all access via the specified proxy. + # + # This class is obsolete. You may pass these same parameters directly to + # \Net::HTTP.new. See Net::HTTP.new for details of the arguments. + def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ssl = nil) #:nodoc: + return self unless p_addr + + Class.new(self) { + @is_proxy_class = true + + if p_addr == :ENV then + @proxy_from_env = true + @proxy_address = nil + @proxy_port = nil + else + @proxy_from_env = false @proxy_address = p_addr - @proxy_port = p_port - } - def klass.proxy_class? - true + @proxy_port = p_port || default_port end - def klass.proxy_address - @proxy_address - end + @proxy_user = p_user + @proxy_pass = p_pass + @proxy_use_ssl = p_use_ssl + } + end - def klass.proxy_port - @proxy_port - end + # :startdoc: - klass + class << HTTP + # Returns true if self is a class which was created by HTTP::Proxy. + def proxy_class? + defined?(@is_proxy_class) ? @is_proxy_class : false end - end - + # Returns the address of the proxy host, or +nil+ if none; + # see Net::HTTP@Proxy+Server. + attr_reader :proxy_address - def initialize( addr, port ) - super - @proxy_address = type.proxy_address - @proxy_port = type.proxy_port - end + # Returns the port number of the proxy host, or +nil+ if none; + # see Net::HTTP@Proxy+Server. + attr_reader :proxy_port - attr_reader :proxy_address, :proxy_port + # Returns the user name for accessing the proxy, or +nil+ if none; + # see Net::HTTP@Proxy+Server. + attr_reader :proxy_user - alias proxyaddr proxy_address - alias proxyport proxy_port + # Returns the password for accessing the proxy, or +nil+ if none; + # see Net::HTTP@Proxy+Server. + attr_reader :proxy_pass - def proxy? - true - end - - def connect( addr = nil, port = nil ) - super @proxy_address, @proxy_port + # Use SSL when talking to the proxy. If Net::HTTP does not use a proxy, nil. + attr_reader :proxy_use_ssl end - def edit_path( path ) - 'http://' + addr_port + path + # Returns +true+ if a proxy server is defined, +false+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + def proxy? + !!(@proxy_from_env ? proxy_uri : @proxy_address) end - - end - - end # net private - - class Code - - def http_mkchild( bodyexist = nil ) - c = mkchild(nil) - be = if bodyexist.nil? then @body_exist else bodyexist end - c.instance_eval { @body_exist = be } - c + # Returns +true+ if the proxy server is defined in the environment, + # +false+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + def proxy_from_env? + @proxy_from_env end - def body_exist? - @body_exist + # The proxy URI determined from the environment for this connection. + def proxy_uri # :nodoc: + return if @proxy_uri == false + @proxy_uri ||= URI::HTTP.new( + "http", nil, address, port, nil, nil, nil, nil, nil + ).find_proxy || false + @proxy_uri || nil end - - end - HTTPInformationCode = InformationCode.http_mkchild( false ) - HTTPSuccessCode = SuccessCode .http_mkchild( true ) - HTTPRedirectionCode = RetriableCode .http_mkchild( true ) - HTTPRetriableCode = HTTPRedirectionCode - HTTPClientErrorCode = FatalErrorCode .http_mkchild( true ) - HTTPFatalErrorCode = HTTPClientErrorCode - HTTPServerErrorCode = ServerErrorCode.http_mkchild( true ) - - - HTTPSwitchProtocol = HTTPInformationCode.http_mkchild - - HTTPOK = HTTPSuccessCode.http_mkchild - HTTPCreated = HTTPSuccessCode.http_mkchild - HTTPAccepted = HTTPSuccessCode.http_mkchild - HTTPNonAuthoritativeInformation = HTTPSuccessCode.http_mkchild - HTTPNoContent = HTTPSuccessCode.http_mkchild( false ) - HTTPResetContent = HTTPSuccessCode.http_mkchild( false ) - HTTPPartialContent = HTTPSuccessCode.http_mkchild - - HTTPMultipleChoice = HTTPRedirectionCode.http_mkchild - HTTPMovedPermanently = HTTPRedirectionCode.http_mkchild - HTTPMovedTemporarily = HTTPRedirectionCode.http_mkchild - HTTPNotModified = HTTPRedirectionCode.http_mkchild( false ) - HTTPUseProxy = HTTPRedirectionCode.http_mkchild( false ) - - HTTPBadRequest = HTTPClientErrorCode.http_mkchild - HTTPUnauthorized = HTTPClientErrorCode.http_mkchild - HTTPPaymentRequired = HTTPClientErrorCode.http_mkchild - HTTPForbidden = HTTPClientErrorCode.http_mkchild - HTTPNotFound = HTTPClientErrorCode.http_mkchild - HTTPMethodNotAllowed = HTTPClientErrorCode.http_mkchild - HTTPNotAcceptable = HTTPClientErrorCode.http_mkchild - HTTPProxyAuthenticationRequired = HTTPClientErrorCode.http_mkchild - HTTPRequestTimeOut = HTTPClientErrorCode.http_mkchild - HTTPConflict = HTTPClientErrorCode.http_mkchild - HTTPGone = HTTPClientErrorCode.http_mkchild - HTTPLengthRequired = HTTPClientErrorCode.http_mkchild - HTTPPreconditionFailed = HTTPClientErrorCode.http_mkchild - HTTPRequestEntityTooLarge = HTTPClientErrorCode.http_mkchild - HTTPRequestURITooLarge = HTTPClientErrorCode.http_mkchild - HTTPUnsupportedMediaType = HTTPClientErrorCode.http_mkchild - - HTTPNotImplemented = HTTPServerErrorCode.http_mkchild - HTTPBadGateway = HTTPServerErrorCode.http_mkchild - HTTPServiceUnavailable = HTTPServerErrorCode.http_mkchild - HTTPGatewayTimeOut = HTTPServerErrorCode.http_mkchild - HTTPVersionNotSupported = HTTPServerErrorCode.http_mkchild - - - - module NetPrivate - - - ### - ### request - ### - - class HTTPRequest - - def initialize( httpver, sock, inith, path, uhead ) - @http_version = httpver - @socket = sock - @path = path - @response = nil - - @u_header = inith - return unless uhead - tmp = {} - uhead.each do |k,v| - key = canonical(k) - if tmp.key? key then - $stderr.puts "WARNING: duplicated HTTP header: #{k}" if $VERBOSE - tmp[ key ] = v.strip - end + # Returns the address of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + def proxy_address + if @proxy_from_env then + proxy_uri&.hostname + else + @proxy_address end - @u_header.update tmp - end - - attr_reader :http_version - - attr_reader :path - attr_reader :response - - def inspect - "\#<#{type}>" - end - - def []( key ) - @u_header[ canonical key ] - end - - def []=( key, val ) - @u_header[ canonical key ] = val - end - - def key?( key ) - @u_header.key? canonical(key) end - def delete( key ) - @u_header.delete canonical(key) - end - - def each( &block ) - @u_header.each( &block ) - end - - def each_key( &block ) - @u_header.each_key( &block ) + # Returns the port number of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + def proxy_port + if @proxy_from_env then + proxy_uri&.port + else + @proxy_port + end end - def each_value( &block ) - @u_header.each_value( &block ) + # Returns the user name of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + def proxy_user + if @proxy_from_env + user = proxy_uri&.user + unescape(user) if user + else + @proxy_user + end end - - def terminate - @response.terminate + # Returns the password of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. + def proxy_pass + if @proxy_from_env + pass = proxy_uri&.password + unescape(pass) if pass + else + @proxy_pass + end end + alias proxyaddr proxy_address #:nodoc: obsolete + alias proxyport proxy_port #:nodoc: obsolete private + # :stopdoc: - def canonical( k ) - k.split('-').collect {|i| i.capitalize }.join('-') + def unescape(value) + require 'cgi/escape' + require 'cgi/util' unless defined?(CGI::EscapeExt) + CGI.unescape(value) end + # without proxy, obsolete - # write request & header - - def do_dispatch - if @response then - raise IOError, "#{type}\#dispatch called twice" - end - yield - @response = read_response + def conn_address # :nodoc: + @ipaddr || address() end - def request( req ) - @socket.writeline req - @u_header.each do |n,v| - @socket.writeline n + ': ' + v - end - @socket.writeline '' + def conn_port # :nodoc: + port() end - # read response & header - - def read_response - resp = rdresp0 - resp = rdresp0 while ContinueCode === resp - resp - end - - def rdresp0 - resp = get_resline - - while true do - line = @socket.readline - break if line.empty? - - m = /\A([^:]+):\s*/.match( line ) - unless m then - raise HTTPBadResponse, 'wrong header line format' - end - nm = m[1] - line = m.post_match - if resp.key? nm then - resp[nm] << ', ' << line + def edit_path(path) + if proxy? + if path.start_with?("ftp://") || use_ssl? + path else - resp[nm] = line + "http://#{addr_port}#{path}" end + else + path end - - resp - end - - def get_resline - str = @socket.readline - m = /\AHTTP\/(\d+\.\d+)?\s+(\d\d\d)\s*(.*)\z/i.match( str ) - unless m then - raise HTTPBadResponse, "wrong status line: #{str}" - end - @http_version = m[1] - status = m[2] - discrip = m[3] - - HTTPResponse.new( status, discrip, @socket, type::HAS_BODY ) end - - end - - class Get < HTTPRequest - - HAS_BODY = true - - def dispatch - do_dispatch { - request sprintf('GET %s HTTP/%s', @path, @http_version) + # :startdoc: + + # + # HTTP operations + # + + public + + # :call-seq: + # get(path, initheader = nil) {|res| ... } + # + # Sends a GET request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Get object + # created from string +path+ and initial headers hash +initheader+. + # + # With a block given, calls the block with the response body: + # + # http = Net::HTTP.new(hostname) + # http.get('/todos/1') do |res| + # p res + # end # => #<Net::HTTPOK 200 OK readbody=true> + # + # Output: + # + # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}" + # + # With no block given, simply returns the response object: + # + # http.get('/') # => #<Net::HTTPOK 200 OK readbody=true> + # + # Related: + # + # - Net::HTTP::Get: request class for \HTTP method GET. + # - Net::HTTP.get: sends GET request, returns response body. + # + def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + res = nil + + request(Get.new(path, initheader)) {|r| + r.read_body dest, &block + res = r } - end - - end - - class Head < HTTPRequest - - HAS_BODY = false - - def dispatch - do_dispatch { - request sprintf('HEAD %s HTTP/%s', @path, @http_version) - } - end - - end - - class HTTPRequestWithData < HTTPRequest - - def dispatch( str = nil ) - check_arg str, block_given? - - if block_given? then - ac = Accumulator.new - yield ac # must be yield, not block.call - data = ac.terminate - else - data = str + res + end + + # Sends a HEAD request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Head object + # created from string +path+ and initial headers hash +initheader+: + # + # res = http.head('/todos/1') # => #<Net::HTTPOK 200 OK readbody=true> + # res.body # => nil + # res.to_hash.take(3) + # # => + # [["date", ["Wed, 15 Feb 2023 15:25:42 GMT"]], + # ["content-type", ["application/json; charset=utf-8"]], + # ["connection", ["close"]]] + # + def head(path, initheader = nil) + request(Head.new(path, initheader)) + end + + # :call-seq: + # post(path, data, initheader = nil) {|res| ... } + # + # Sends a POST request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Post object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # With a block given, calls the block with the response body: + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.post('/todos', data) do |res| + # p res + # end # => #<Net::HTTPCreated 201 Created readbody=true> + # + # Output: + # + # "{\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\",\n \"id\": 201\n}" + # + # With no block given, simply returns the response object: + # + # http.post('/todos', data) # => #<Net::HTTPCreated 201 Created readbody=true> + # + # Related: + # + # - Net::HTTP::Post: request class for \HTTP method POST. + # - Net::HTTP.post: sends POST request, returns response body. + # + def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Post, &block) + end + + # :call-seq: + # patch(path, data, initheader = nil) {|res| ... } + # + # Sends a PATCH request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Patch object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # With a block given, calls the block with the response body: + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.patch('/todos/1', data) do |res| + # p res + # end # => #<Net::HTTPOK 200 OK readbody=true> + # + # Output: + # + # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false,\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\"\n}" + # + # With no block given, simply returns the response object: + # + # http.patch('/todos/1', data) # => #<Net::HTTPCreated 201 Created readbody=true> + # + def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Patch, &block) + end + + # Sends a PUT request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Put object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.put('/todos/1', data) # => #<Net::HTTPOK 200 OK readbody=true> + # + # Related: + # + # - Net::HTTP::Put: request class for \HTTP method PUT. + # - Net::HTTP.put: sends PUT request, returns response body. + # + def put(path, data, initheader = nil) + request(Put.new(path, initheader), data) + end + + # Sends a PROPPATCH request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Proppatch object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.proppatch('/todos/1', data) + # + def proppatch(path, body, initheader = nil) + request(Proppatch.new(path, initheader), body) + end + + # Sends a LOCK request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Lock object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.lock('/todos/1', data) + # + def lock(path, body, initheader = nil) + request(Lock.new(path, initheader), body) + end + + # Sends an UNLOCK request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Unlock object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.unlock('/todos/1', data) + # + def unlock(path, body, initheader = nil) + request(Unlock.new(path, initheader), body) + end + + # Sends an Options request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Options object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.options('/') + # + def options(path, initheader = nil) + request(Options.new(path, initheader)) + end + + # Sends a PROPFIND request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Propfind object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.propfind('/todos/1', data) + # + def propfind(path, body = nil, initheader = {'Depth' => '0'}) + request(Propfind.new(path, initheader), body) + end + + # Sends a DELETE request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Delete object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.delete('/todos/1') + # + def delete(path, initheader = {'Depth' => 'Infinity'}) + request(Delete.new(path, initheader)) + end + + # Sends a MOVE request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Move object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.move('/todos/1') + # + def move(path, initheader = nil) + request(Move.new(path, initheader)) + end + + # Sends a COPY request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Copy object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.copy('/todos/1') + # + def copy(path, initheader = nil) + request(Copy.new(path, initheader)) + end + + # Sends a MKCOL request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Mkcol object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http.mkcol('/todos/1', data) + # http = Net::HTTP.new(hostname) + # + def mkcol(path, body = nil, initheader = nil) + request(Mkcol.new(path, initheader), body) + end + + # Sends a TRACE request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Trace object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.trace('/todos/1') + # + def trace(path, initheader = nil) + request(Trace.new(path, initheader)) + end + + # Sends a GET request to the server; + # forms the response into a Net::HTTPResponse object. + # + # The request is based on the Net::HTTP::Get object + # created from string +path+ and initial headers hash +initheader+. + # + # With no block given, returns the response object: + # + # http = Net::HTTP.new(hostname) + # http.request_get('/todos') # => #<Net::HTTPOK 200 OK readbody=true> + # + # With a block given, calls the block with the response object + # and returns the response object: + # + # http.request_get('/todos') do |res| + # p res + # end # => #<Net::HTTPOK 200 OK readbody=true> + # + # Output: + # + # #<Net::HTTPOK 200 OK readbody=false> + # + def request_get(path, initheader = nil, &block) # :yield: +response+ + request(Get.new(path, initheader), &block) + end + + # Sends a HEAD request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Head object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.head('/todos/1') # => #<Net::HTTPOK 200 OK readbody=true> + # + def request_head(path, initheader = nil, &block) + request(Head.new(path, initheader), &block) + end + + # Sends a POST request to the server; + # forms the response into a Net::HTTPResponse object. + # + # The request is based on the Net::HTTP::Post object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # With no block given, returns the response object: + # + # http = Net::HTTP.new(hostname) + # http.post('/todos', 'xyzzy') + # # => #<Net::HTTPCreated 201 Created readbody=true> + # + # With a block given, calls the block with the response body + # and returns the response object: + # + # http.post('/todos', 'xyzzy') do |res| + # p res + # end # => #<Net::HTTPCreated 201 Created readbody=true> + # + # Output: + # + # "{\n \"xyzzy\": \"\",\n \"id\": 201\n}" + # + def request_post(path, data, initheader = nil, &block) # :yield: +response+ + request Post.new(path, initheader), data, &block + end + + # Sends a PUT request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Put object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.put('/todos/1', 'xyzzy') + # # => #<Net::HTTPOK 200 OK readbody=true> + # + def request_put(path, data, initheader = nil, &block) #:nodoc: + request Put.new(path, initheader), data, &block + end + + alias get2 request_get #:nodoc: obsolete + alias head2 request_head #:nodoc: obsolete + alias post2 request_post #:nodoc: obsolete + alias put2 request_put #:nodoc: obsolete + + # Sends an \HTTP request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTPRequest object + # created from string +path+, string +data+, and initial headers hash +header+. + # That object is an instance of the + # {subclass of Net::HTTPRequest}[rdoc-ref:Net::HTTPRequest@Request+Subclasses], + # that corresponds to the given uppercase string +name+, + # which must be + # an {HTTP request method}[https://en.wikipedia.org/wiki/HTTP#Request_methods] + # or a {WebDAV request method}[https://en.wikipedia.org/wiki/WebDAV#Implementation]. + # + # Examples: + # + # http = Net::HTTP.new(hostname) + # http.send_request('GET', '/todos/1') + # # => #<Net::HTTPOK 200 OK readbody=true> + # http.send_request('POST', '/todos', 'xyzzy') + # # => #<Net::HTTPCreated 201 Created readbody=true> + # + def send_request(name, path, data = nil, header = nil) + has_response_body = name != 'HEAD' + r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header) + request r, data + end + + # Sends the given request +req+ to the server; + # forms the response into a Net::HTTPResponse object. + # + # The given +req+ must be an instance of a + # {subclass of Net::HTTPRequest}[rdoc-ref:Net::HTTPRequest@Request+Subclasses]. + # Argument +body+ should be given only if needed for the request. + # + # With no block given, returns the response object: + # + # http = Net::HTTP.new(hostname) + # + # req = Net::HTTP::Get.new('/todos/1') + # http.request(req) + # # => #<Net::HTTPOK 200 OK readbody=true> + # + # req = Net::HTTP::Post.new('/todos') + # http.request(req, 'xyzzy') + # # => #<Net::HTTPCreated 201 Created readbody=true> + # + # With a block given, calls the block with the response and returns the response: + # + # req = Net::HTTP::Get.new('/todos/1') + # http.request(req) do |res| + # p res + # end # => #<Net::HTTPOK 200 OK readbody=true> + # + # Output: + # + # #<Net::HTTPOK 200 OK readbody=false> + # + def request(req, body = nil, &block) # :yield: +response+ + unless started? + start { + req['connection'] ||= 'close' + return request(req, body, &block) + } end - - do_dispatch { - @u_header['Content-Length'] = data.size.to_s - @u_header.delete 'Transfer-Encoding' - request sprintf('%s %s HTTP/%s', type::METHOD, @path, @http_version) - @socket.write data - } - end - - def check_arg( data, blkp ) - if data and blkp then - raise ArgumentError, 'both of data and block given' + if proxy_user() + req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? end - unless data or blkp then - raise ArgumentError, 'str or block required' + req.set_body_internal body + res = transport_request(req, &block) + if sspi_auth?(res) + sspi_auth(req) + res = transport_request(req, &block) end - end - - end - - class Post < HTTPRequestWithData - - HAS_BODY = true - - METHOD = 'POST' - - end - - class Put < HTTPRequestWithData - - HAS_BODY = true - - METHOD = 'PUT' - - end - - class Accumulator - - def initialize - @buf = '' - end - - def write( s ) - @buf.concat s - end - - alias << write - - def terminate - ret = @buf - @buf = nil - ret - end - - end - - - - ### - ### response - ### - - class HTTPResponse < Response - - HTTPCODE_CLASS_TO_OBJ = { - '1' => HTTPInformationCode, - '2' => HTTPSuccessCode, - '3' => HTTPRedirectionCode, - '4' => HTTPClientErrorCode, - '5' => HTTPServerErrorCode - } - - HTTPCODE_TO_OBJ = { - '100' => ContinueCode, - '101' => HTTPSwitchProtocol, - - '200' => HTTPOK, - '201' => HTTPCreated, - '202' => HTTPAccepted, - '203' => HTTPNonAuthoritativeInformation, - '204' => HTTPNoContent, - '205' => HTTPResetContent, - '206' => HTTPPartialContent, - - '300' => HTTPMultipleChoice, - '301' => HTTPMovedPermanently, - '302' => HTTPMovedTemporarily, - '303' => HTTPMovedPermanently, - '304' => HTTPNotModified, - '305' => HTTPUseProxy, - - '400' => HTTPBadRequest, - '401' => HTTPUnauthorized, - '402' => HTTPPaymentRequired, - '403' => HTTPForbidden, - '404' => HTTPNotFound, - '405' => HTTPMethodNotAllowed, - '406' => HTTPNotAcceptable, - '407' => HTTPProxyAuthenticationRequired, - '408' => HTTPRequestTimeOut, - '409' => HTTPConflict, - '410' => HTTPGone, - '411' => HTTPFatalErrorCode, - '412' => HTTPPreconditionFailed, - '413' => HTTPRequestEntityTooLarge, - '414' => HTTPRequestURITooLarge, - '415' => HTTPUnsupportedMediaType, - - '500' => HTTPFatalErrorCode, - '501' => HTTPNotImplemented, - '502' => HTTPBadGateway, - '503' => HTTPServiceUnavailable, - '504' => HTTPGatewayTimeOut, - '505' => HTTPVersionNotSupported - } - - - def initialize( status, msg, sock, be ) - code = HTTPCODE_TO_OBJ[status] || - HTTPCODE_CLASS_TO_OBJ[status[0,1]] || - UnknownCode - super code, status, msg - @socket = sock - @body_exist = be - - @header = {} - @body = nil - @read = false - end - - def inspect - "#<#{type} #{code}>" + res end - def []( key ) - @header[ key.downcase ] - end - - def []=( key, val ) - @header[ key.downcase ] = val - end - - def each( &block ) - @header.each( &block ) - end - - def each_key( &block ) - @header.each_key( &block ) - end - - def each_value( &block ) - @header.each_value( &block ) - end - - def delete( key ) - @header.delete key.downcase - end - - def key?( key ) - @header.key? key.downcase - end - - def to_hash - @header.dup - end + private - def value - unless SuccessCode === self then - error! self - end + # Executes a request which uses a representation + # and returns its body. + def send_entity(path, data, initheader, dest, type, &block) + res = nil + request(type.new(path, initheader), data) {|r| + r.read_body dest, &block + res = r + } + res end + # :stopdoc: - # header (for backward compatibility) + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc: - def read_header - self - end - - alias header read_header - alias response read_header + def transport_request(req) + count = 0 + begin + begin_transport req + res = catch(:response) { + begin + req.exec @socket, @curr_http_version, edit_path(req.path) + rescue Errno::EPIPE + # Failure when writing full request, but we can probably + # still read the received response. + end + begin + res = HTTPResponse.read_new(@socket) + res.decode_content = req.decode_content + res.body_encoding = @response_body_encoding + res.ignore_eof = @ignore_eof + end while res.kind_of?(HTTPInformation) - # body + res.uri = req.uri - def read_body( dest = nil, &block ) - if @read and (dest or block) then - raise IOError, "#{type}\#read_body called twice with argument" + res + } + res.reading_body(@socket, req.response_body_permitted?) { + if block_given? + count = max_retries # Don't restart in the middle of a download + yield res + end + } + rescue Net::OpenTimeout + raise + rescue Net::ReadTimeout, IOError, EOFError, + Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT, + # avoid a dependency on OpenSSL + defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError, + Timeout::Error => exception + if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method) + count += 1 + @socket.close if @socket + debug "Conn close because of error #{exception}, and retry" + retry + end + debug "Conn close because of error #{exception}" + @socket.close if @socket + raise end - unless @read then - to = procdest( dest, block ) - stream_check - - if @body_exist and code_type.body_exist? then - read_body_0 to - @body = to - else - @body = nil + end_transport req, res + res + rescue => exception + debug "Conn close because of error #{exception}" + @socket.close if @socket + raise exception + end + + def begin_transport(req) + if @socket.closed? + connect + elsif @last_communicated + if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC) + debug 'Conn close because of keep_alive_timeout' + @socket.close + connect + elsif @socket.io.to_io.wait_readable(0) && @socket.eof? + debug "Conn close because of EOF" + @socket.close + connect end - @read = true end - @body - end - - alias body read_body - alias entity read_body - + if not req.response_body_permitted? and @close_on_empty_response + req['connection'] ||= 'close' + end - # internal use only - def terminate - read_body + req.update_uri address, port, use_ssl? + req['host'] ||= addr_port() end - - private - - - def read_body_0( dest ) - if chunked? then - read_chunked dest + def end_transport(req, res) + @curr_http_version = res.http_version + @last_communicated = nil + if @socket.closed? + debug 'Conn socket closed' + elsif not res.body and @close_on_empty_response + debug 'Conn close' + @socket.close + elsif keep_alive?(req, res) + debug 'Conn keep-alive' + @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC) else - clen = content_length - if clen then - @socket.read clen, dest - else - clen = range_length - if clen then - @socket.read clen, dest - else - @socket.read_all dest - end - end + debug 'Conn close' + @socket.close end end - def read_chunked( dest ) - len = nil - total = 0 - - while true do - line = @socket.readline - m = /[0-9a-fA-F]+/.match( line ) - unless m then - raise HTTPBadResponse, "wrong chunk size line: #{line}" - end - len = m[0].hex - break if len == 0 - @socket.read( len, dest ); total += len - @socket.read 2 # \r\n - end - until @socket.readline.empty? do - ; + def keep_alive?(req, res) + return false if req.connection_close? + if @curr_http_version <= '1.0' + res.connection_keep_alive? + else # HTTP/1.1 or later + not res.connection_close? end end - def content_length - if @header.key? 'content-length' then - m = /\d+/.match( @header['content-length'] ) - unless m then - raise HTTPBadResponse, 'wrong Content-Length format' + def sspi_auth?(res) + return false unless @sspi_enabled + if res.kind_of?(HTTPProxyAuthenticationRequired) and + proxy? and res["Proxy-Authenticate"].include?("Negotiate") + begin + require 'win32/sspi' + true + rescue LoadError + false end - m[0].to_i else - nil + false end end - def chunked? - tmp = @header['transfer-encoding'] - tmp and /\bchunked\b/i === tmp - end - - def range_length - if @header.key? 'content-range' then - m = %r<bytes\s+(\d+)-(\d+)/\d+>.match( @header['content-range'] ) - unless m then - raise HTTPBadResponse, 'wrong Content-Range format' - end - l = m[2].to_i - u = m[1].to_i - if l > u then - nil - else - u - l - end - else - nil - end + def sspi_auth(req) + n = Win32::SSPI::NegotiateAuth.new + req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}" + # Some versions of ISA will close the connection if this isn't present. + req["Connection"] = "Keep-Alive" + req["Proxy-Connection"] = "Keep-Alive" + res = transport_request(req) + authphrase = res["Proxy-Authenticate"] or return res + req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}" + rescue => err + raise HTTPAuthenticationError.new('HTTP authentication failed', err) end - def stream_check - if @socket.closed? then - raise IOError, 'try to read body out of block' - end + # + # utils + # + + private + + def addr_port + addr = address + addr = "[#{addr}]" if addr.include?(":") + default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port + default_port == port ? addr : "#{addr}:#{port}" end - def procdest( dest, block ) - if dest and block then - raise ArgumentError, - 'both of arg and block are given for HTTP method' - end - if block then - ReadAdapter.new block - else - dest or '' - end + # Adds a message to debugging output + def debug(msg) + return unless @debug_output + @debug_output << msg + @debug_output << "\n" end + alias_method :D, :debug end + # for backward compatibility until Ruby 4.0 + # https://bugs.ruby-lang.org/issues/20900 + # https://github.com/bblimke/webmock/pull/1081 + HTTPSession = HTTP + deprecate_constant :HTTPSession +end + +require_relative 'http/exceptions' + +require_relative 'http/header' + +require_relative 'http/generic_request' +require_relative 'http/request' +require_relative 'http/requests' - end # module Net::NetPrivate +require_relative 'http/response' +require_relative 'http/responses' -end # module Net +require_relative 'http/proxy_delta' diff --git a/lib/net/http/exceptions.rb b/lib/net/http/exceptions.rb new file mode 100644 index 0000000000..4342cfc0ef --- /dev/null +++ b/lib/net/http/exceptions.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +module Net + # Net::HTTP exception class. + # You cannot use Net::HTTPExceptions directly; instead, you must use + # its subclasses. + module HTTPExceptions # :nodoc: + def initialize(msg, res) #:nodoc: + super msg + @response = res + end + attr_reader :response + alias data response #:nodoc: obsolete + end + + # :stopdoc: + class HTTPError < ProtocolError + include HTTPExceptions + end + + class HTTPRetriableError < ProtoRetriableError + include HTTPExceptions + end + + class HTTPClientException < ProtoServerError + include HTTPExceptions + end + + class HTTPFatalError < ProtoFatalError + include HTTPExceptions + end + + # We cannot use the name "HTTPServerError", it is the name of the response. + HTTPServerException = HTTPClientException # :nodoc: + deprecate_constant(:HTTPServerException) +end diff --git a/lib/net/http/generic_request.rb b/lib/net/http/generic_request.rb new file mode 100644 index 0000000000..5b01ea4abd --- /dev/null +++ b/lib/net/http/generic_request.rb @@ -0,0 +1,429 @@ +# frozen_string_literal: true +# +# \HTTPGenericRequest is the parent of the Net::HTTPRequest class. +# +# Do not use this directly; instead, use a subclass of Net::HTTPRequest. +# +# == About the Examples +# +# :include: doc/net-http/examples.rdoc +# +class Net::HTTPGenericRequest + + include Net::HTTPHeader + + def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) # :nodoc: + @method = m + @request_has_body = reqbody + @response_has_body = resbody + + if URI === uri_or_path then + raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path + hostname = uri_or_path.host + raise ArgumentError, "no host component for URI" unless (hostname && hostname.length > 0) + @uri = uri_or_path.dup + @path = uri_or_path.request_uri + raise ArgumentError, "no HTTP request path given" unless @path + else + @uri = nil + raise ArgumentError, "no HTTP request path given" unless uri_or_path + raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty? + @path = uri_or_path.dup + end + + @decode_content = false + + if Net::HTTP::HAVE_ZLIB then + if !initheader || + !initheader.keys.any? { |k| + %w[accept-encoding range].include? k.downcase + } then + @decode_content = true if @response_has_body + initheader = initheader ? initheader.dup : {} + initheader["accept-encoding"] = + "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" + end + end + + initialize_http_header initheader + self['Accept'] ||= '*/*' + self['User-Agent'] ||= 'Ruby' + self['Host'] ||= @uri.authority if @uri + @body = nil + @body_stream = nil + @body_data = nil + end + + # Returns the string method name for the request: + # + # Net::HTTP::Get.new(uri).method # => "GET" + # Net::HTTP::Post.new(uri).method # => "POST" + # + attr_reader :method + + # Returns the string path for the request: + # + # Net::HTTP::Get.new(uri).path # => "/" + # Net::HTTP::Post.new('example.com').path # => "example.com" + # + attr_reader :path + + # Returns the URI object for the request, or +nil+ if none: + # + # Net::HTTP::Get.new(uri).uri + # # => #<URI::HTTPS https://jsonplaceholder.typicode.com/> + # Net::HTTP::Get.new('example.com').uri # => nil + # + attr_reader :uri + + # Returns +false+ if the request's header <tt>'Accept-Encoding'</tt> + # has been set manually or deleted + # (indicating that the user intends to handle encoding in the response), + # +true+ otherwise: + # + # req = Net::HTTP::Get.new(uri) # => #<Net::HTTP::Get GET> + # req['Accept-Encoding'] # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" + # req.decode_content # => true + # req['Accept-Encoding'] = 'foo' + # req.decode_content # => false + # req.delete('Accept-Encoding') + # req.decode_content # => false + # + attr_reader :decode_content + + # Returns a string representation of the request: + # + # Net::HTTP::Post.new(uri).inspect # => "#<Net::HTTP::Post POST>" + # + def inspect + "\#<#{self.class} #{@method}>" + end + + # Returns a string representation of the request with the details for pp: + # + # require 'pp' + # post = Net::HTTP::Post.new(uri) + # post.inspect # => "#<Net::HTTP::Post POST>" + # post.pretty_inspect + # # => #<Net::HTTP::Post + # POST + # path="/" + # headers={"accept-encoding" => ["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], + # "accept" => ["*/*"], + # "user-agent" => ["Ruby"], + # "host" => ["www.ruby-lang.org"]}> + # + def pretty_print(q) + q.object_group(self) { + q.breakable + q.text @method + q.breakable + q.text "path="; q.pp @path + q.breakable + q.text "headers="; q.pp to_hash + } + end + + ## + # Don't automatically decode response content-encoding if the user indicates + # they want to handle it. + + def []=(key, val) # :nodoc: + @decode_content = false if key.downcase == 'accept-encoding' + + super key, val + end + + # Returns whether the request may have a body: + # + # Net::HTTP::Post.new(uri).request_body_permitted? # => true + # Net::HTTP::Get.new(uri).request_body_permitted? # => false + # + def request_body_permitted? + @request_has_body + end + + # Returns whether the response may have a body: + # + # Net::HTTP::Post.new(uri).response_body_permitted? # => true + # Net::HTTP::Head.new(uri).response_body_permitted? # => false + # + def response_body_permitted? + @response_has_body + end + + def body_exist? # :nodoc: + warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE + response_body_permitted? + end + + # Returns the string body for the request, or +nil+ if there is none: + # + # req = Net::HTTP::Post.new(uri) + # req.body # => nil + # req.body = '{"title": "foo","body": "bar","userId": 1}' + # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}" + # + attr_reader :body + + # Sets the body for the request: + # + # req = Net::HTTP::Post.new(uri) + # req.body # => nil + # req.body = '{"title": "foo","body": "bar","userId": 1}' + # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}" + # + def body=(str) + @body = str + @body_stream = nil + @body_data = nil + str + end + + # Returns the body stream object for the request, or +nil+ if there is none: + # + # req = Net::HTTP::Post.new(uri) # => #<Net::HTTP::Post POST> + # req.body_stream # => nil + # require 'stringio' + # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8> + # req.body_stream # => #<StringIO:0x0000027d1e5affa8> + # + attr_reader :body_stream + + # Sets the body stream for the request: + # + # req = Net::HTTP::Post.new(uri) # => #<Net::HTTP::Post POST> + # req.body_stream # => nil + # require 'stringio' + # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8> + # req.body_stream # => #<StringIO:0x0000027d1e5affa8> + # + def body_stream=(input) + @body = nil + @body_stream = input + @body_data = nil + input + end + + def set_body_internal(str) #:nodoc: internal use only + raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream) + self.body = str if str + if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted? + self.body = '' + end + end + + # + # write + # + + def exec(sock, ver, path) #:nodoc: internal use only + if @body + send_request_with_body sock, ver, path, @body + elsif @body_stream + send_request_with_body_stream sock, ver, path, @body_stream + elsif @body_data + send_request_with_body_data sock, ver, path, @body_data + else + write_header sock, ver, path + end + end + + def update_uri(addr, port, ssl) # :nodoc: internal use only + # reflect the connection and @path to @uri + return unless @uri + + if ssl + scheme = 'https' + klass = URI::HTTPS + else + scheme = 'http' + klass = URI::HTTP + end + + if host = self['host'] + host = URI.parse("//#{host}").host # Remove a port component from the existing Host header + elsif host = @uri.host + else + host = addr + end + # convert the class of the URI + if @uri.is_a?(klass) + @uri.host = host + @uri.port = port + else + @uri = klass.new( + scheme, @uri.userinfo, + host, port, nil, + @uri.path, nil, @uri.query, nil) + end + end + + private + + # :stopdoc: + + class Chunker #:nodoc: + def initialize(sock) + @sock = sock + @prev = nil + end + + def write(buf) + # avoid memcpy() of buf, buf can huge and eat memory bandwidth + rv = buf.bytesize + @sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n") + rv + end + + def finish + @sock.write("0\r\n\r\n") + end + end + + def send_request_with_body(sock, ver, path, body) + self.content_length = body.bytesize + delete 'Transfer-Encoding' + write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout + sock.write body + end + + def send_request_with_body_stream(sock, ver, path, f) + unless content_length() or chunked? + raise ArgumentError, + "Content-Length not given and Transfer-Encoding is not `chunked'" + end + write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout + if chunked? + chunker = Chunker.new(sock) + IO.copy_stream(f, chunker) + chunker.finish + else + IO.copy_stream(f, sock) + end + end + + def send_request_with_body_data(sock, ver, path, params) + if /\Amultipart\/form-data\z/i !~ self.content_type + self.content_type = 'application/x-www-form-urlencoded' + return send_request_with_body(sock, ver, path, URI.encode_www_form(params)) + end + + opt = @form_option.dup + require 'securerandom' unless defined?(SecureRandom) + opt[:boundary] ||= SecureRandom.urlsafe_base64(40) + self.set_content_type(self.content_type, boundary: opt[:boundary]) + if chunked? + write_header sock, ver, path + encode_multipart_form_data(sock, params, opt) + else + require 'tempfile' + file = Tempfile.new('multipart') + file.binmode + encode_multipart_form_data(file, params, opt) + file.rewind + self.content_length = file.size + write_header sock, ver, path + IO.copy_stream(file, sock) + file.close(true) + end + end + + def encode_multipart_form_data(out, params, opt) + charset = opt[:charset] + boundary = opt[:boundary] + require 'securerandom' unless defined?(SecureRandom) + boundary ||= SecureRandom.urlsafe_base64(40) + chunked_p = chunked? + + buf = +'' + params.each do |key, value, h={}| + key = quote_string(key, charset) + filename = + h.key?(:filename) ? h[:filename] : + value.respond_to?(:to_path) ? File.basename(value.to_path) : + nil + + buf << "--#{boundary}\r\n" + if filename + filename = quote_string(filename, charset) + type = h[:content_type] || 'application/octet-stream' + buf << "Content-Disposition: form-data; " \ + "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \ + "Content-Type: #{type}\r\n\r\n" + if !out.respond_to?(:write) || !value.respond_to?(:read) + # if +out+ is not an IO or +value+ is not an IO + buf << (value.respond_to?(:read) ? value.read : value) + elsif value.respond_to?(:size) && chunked_p + # if +out+ is an IO and +value+ is a File, use IO.copy_stream + flush_buffer(out, buf, chunked_p) + out << "%x\r\n" % value.size if chunked_p + IO.copy_stream(value, out) + out << "\r\n" if chunked_p + else + # +out+ is an IO, and +value+ is not a File but an IO + flush_buffer(out, buf, chunked_p) + 1 while flush_buffer(out, value.read(4096), chunked_p) + end + else + # non-file field: + # HTML5 says, "The parts of the generated multipart/form-data + # resource that correspond to non-file fields must not have a + # Content-Type header specified." + buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" + buf << (value.respond_to?(:read) ? value.read : value) + end + buf << "\r\n" + end + buf << "--#{boundary}--\r\n" + flush_buffer(out, buf, chunked_p) + out << "0\r\n\r\n" if chunked_p + end + + def quote_string(str, charset) + str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset + str.gsub(/[\\"]/, '\\\\\&') + end + + def flush_buffer(out, buf, chunked_p) + return unless buf + out << "%x\r\n"%buf.bytesize if chunked_p + out << buf + out << "\r\n" if chunked_p + buf.clear + end + + ## + # Waits up to the continue timeout for a response from the server provided + # we're speaking HTTP 1.1 and are expecting a 100-continue response. + + def wait_for_continue(sock, ver) + if ver >= '1.1' and @header['expect'] and + @header['expect'].include?('100-continue') + if sock.io.to_io.wait_readable(sock.continue_timeout) + res = Net::HTTPResponse.read_new(sock) + unless res.kind_of?(Net::HTTPContinue) + res.decode_content = @decode_content + throw :response, res + end + end + end + end + + def write_header(sock, ver, path) + reqline = "#{@method} #{path} HTTP/#{ver}" + if /[\r\n]/ =~ reqline + raise ArgumentError, "A Request-Line must not contain CR or LF" + end + buf = +'' + buf << reqline << "\r\n" + each_capitalized do |k,v| + buf << "#{k}: #{v}\r\n" + end + buf << "\r\n" + sock.write buf + end + +end diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb new file mode 100644 index 0000000000..5dcdcc7d74 --- /dev/null +++ b/lib/net/http/header.rb @@ -0,0 +1,985 @@ +# frozen_string_literal: true +# +# The \HTTPHeader module provides access to \HTTP headers. +# +# The module is included in: +# +# - Net::HTTPGenericRequest (and therefore Net::HTTPRequest). +# - Net::HTTPResponse. +# +# The headers are a hash-like collection of key/value pairs called _fields_. +# +# == Request and Response Fields +# +# Headers may be included in: +# +# - A Net::HTTPRequest object: +# the object's headers will be sent with the request. +# Any fields may be defined in the request; +# see {Setters}[rdoc-ref:Net::HTTPHeader@Setters]. +# - A Net::HTTPResponse object: +# the objects headers are usually those returned from the host. +# Fields may be retrieved from the object; +# see {Getters}[rdoc-ref:Net::HTTPHeader@Getters] +# and {Iterators}[rdoc-ref:Net::HTTPHeader@Iterators]. +# +# Exactly which fields should be sent or expected depends on the host; +# see: +# +# - {Request fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields]. +# - {Response fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields]. +# +# == About the Examples +# +# :include: doc/net-http/examples.rdoc +# +# == Fields +# +# A header field is a key/value pair. +# +# === Field Keys +# +# A field key may be: +# +# - A string: Key <tt>'Accept'</tt> is treated as if it were +# <tt>'Accept'.downcase</tt>; i.e., <tt>'accept'</tt>. +# - A symbol: Key <tt>:Accept</tt> is treated as if it were +# <tt>:Accept.to_s.downcase</tt>; i.e., <tt>'accept'</tt>. +# +# Examples: +# +# req = Net::HTTP::Get.new(uri) +# req[:accept] # => "*/*" +# req['Accept'] # => "*/*" +# req['ACCEPT'] # => "*/*" +# +# req['accept'] = 'text/html' +# req[:accept] = 'text/html' +# req['ACCEPT'] = 'text/html' +# +# === Field Values +# +# A field value may be returned as an array of strings or as a string: +# +# - These methods return field values as arrays: +# +# - #get_fields: Returns the array value for the given key, +# or +nil+ if it does not exist. +# - #to_hash: Returns a hash of all header fields: +# each key is a field name; its value is the array value for the field. +# +# - These methods return field values as string; +# the string value for a field is equivalent to +# <tt>self[key.downcase.to_s].join(', '))</tt>: +# +# - #[]: Returns the string value for the given key, +# or +nil+ if it does not exist. +# - #fetch: Like #[], but accepts a default value +# to be returned if the key does not exist. +# +# The field value may be set: +# +# - #[]=: Sets the value for the given key; +# the given value may be a string, a symbol, an array, or a hash. +# - #add_field: Adds a given value to a value for the given key +# (not overwriting the existing value). +# - #delete: Deletes the field for the given key. +# +# Example field values: +# +# - \String: +# +# req['Accept'] = 'text/html' # => "text/html" +# req['Accept'] # => "text/html" +# req.get_fields('Accept') # => ["text/html"] +# +# - \Symbol: +# +# req['Accept'] = :text # => :text +# req['Accept'] # => "text" +# req.get_fields('Accept') # => ["text"] +# +# - Simple array: +# +# req[:foo] = %w[bar baz bat] +# req[:foo] # => "bar, baz, bat" +# req.get_fields(:foo) # => ["bar", "baz", "bat"] +# +# - Simple hash: +# +# req[:foo] = {bar: 0, baz: 1, bat: 2} +# req[:foo] # => "bar, 0, baz, 1, bat, 2" +# req.get_fields(:foo) # => ["bar", "0", "baz", "1", "bat", "2"] +# +# - Nested: +# +# req[:foo] = [%w[bar baz], {bat: 0, bam: 1}] +# req[:foo] # => "bar, baz, bat, 0, bam, 1" +# req.get_fields(:foo) # => ["bar", "baz", "bat", "0", "bam", "1"] +# +# req[:foo] = {bar: %w[baz bat], bam: {bah: 0, bad: 1}} +# req[:foo] # => "bar, baz, bat, bam, bah, 0, bad, 1" +# req.get_fields(:foo) # => ["bar", "baz", "bat", "bam", "bah", "0", "bad", "1"] +# +# == Convenience Methods +# +# Various convenience methods retrieve values, set values, query values, +# set form values, or iterate over fields. +# +# === Setters +# +# \Method #[]= can set any field, but does little to validate the new value; +# some of the other setter methods provide some validation: +# +# - #[]=: Sets the string or array value for the given key. +# - #add_field: Creates or adds to the array value for the given key. +# - #basic_auth: Sets the string authorization header for <tt>'Authorization'</tt>. +# - #content_length=: Sets the integer length for field <tt>'Content-Length</tt>. +# - #content_type=: Sets the string value for field <tt>'Content-Type'</tt>. +# - #proxy_basic_auth: Sets the string authorization header for <tt>'Proxy-Authorization'</tt>. +# - #set_range: Sets the value for field <tt>'Range'</tt>. +# +# === Form Setters +# +# - #set_form: Sets an HTML form data set. +# - #set_form_data: Sets header fields and a body from HTML form data. +# +# === Getters +# +# \Method #[] can retrieve the value of any field that exists, +# but always as a string; +# some of the other getter methods return something different +# from the simple string value: +# +# - #[]: Returns the string field value for the given key. +# - #content_length: Returns the integer value of field <tt>'Content-Length'</tt>. +# - #content_range: Returns the Range value of field <tt>'Content-Range'</tt>. +# - #content_type: Returns the string value of field <tt>'Content-Type'</tt>. +# - #fetch: Returns the string field value for the given key. +# - #get_fields: Returns the array field value for the given +key+. +# - #main_type: Returns first part of the string value of field <tt>'Content-Type'</tt>. +# - #sub_type: Returns second part of the string value of field <tt>'Content-Type'</tt>. +# - #range: Returns an array of Range objects of field <tt>'Range'</tt>, or +nil+. +# - #range_length: Returns the integer length of the range given in field <tt>'Content-Range'</tt>. +# - #type_params: Returns the string parameters for <tt>'Content-Type'</tt>. +# +# === Queries +# +# - #chunked?: Returns whether field <tt>'Transfer-Encoding'</tt> is set to <tt>'chunked'</tt>. +# - #connection_close?: Returns whether field <tt>'Connection'</tt> is set to <tt>'close'</tt>. +# - #connection_keep_alive?: Returns whether field <tt>'Connection'</tt> is set to <tt>'keep-alive'</tt>. +# - #key?: Returns whether a given key exists. +# +# === Iterators +# +# - #each_capitalized: Passes each field capitalized-name/value pair to the block. +# - #each_capitalized_name: Passes each capitalized field name to the block. +# - #each_header: Passes each field name/value pair to the block. +# - #each_name: Passes each field name to the block. +# - #each_value: Passes each string field value to the block. +# +module Net::HTTPHeader + # The maximum length of HTTP header keys. + MAX_KEY_LENGTH = 1024 + # The maximum length of HTTP header values. + MAX_FIELD_LENGTH = 65536 + + def initialize_http_header(initheader) #:nodoc: + @header = {} + return unless initheader + initheader.each do |key, value| + warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE + if value.nil? + warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE + else + value = value.strip # raise error for invalid byte sequences + if key.to_s.bytesize > MAX_KEY_LENGTH + raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..." + end + if value.to_s.bytesize > MAX_FIELD_LENGTH + raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}" + end + if value.count("\r\n") > 0 + raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF" + end + @header[key.downcase.to_s] = [value] + end + end + end + + def size #:nodoc: obsolete + @header.size + end + + alias length size #:nodoc: obsolete + + # Returns the string field value for the case-insensitive field +key+, + # or +nil+ if there is no such key; + # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res['Connection'] # => "keep-alive" + # res['Nosuch'] # => nil + # + # Note that some field values may be retrieved via convenience methods; + # see {Getters}[rdoc-ref:Net::HTTPHeader@Getters]. + def [](key) + a = @header[key.downcase.to_s] or return nil + a.join(', ') + end + + # Sets the value for the case-insensitive +key+ to +val+, + # overwriting the previous value if the field exists; + # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: + # + # req = Net::HTTP::Get.new(uri) + # req['Accept'] # => "*/*" + # req['Accept'] = 'text/html' + # req['Accept'] # => "text/html" + # + # Note that some field values may be set via convenience methods; + # see {Setters}[rdoc-ref:Net::HTTPHeader@Setters]. + def []=(key, val) + unless val + @header.delete key.downcase.to_s + return val + end + set_field(key, val) + end + + # Adds value +val+ to the value array for field +key+ if the field exists; + # creates the field with the given +key+ and +val+ if it does not exist. + # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: + # + # req = Net::HTTP::Get.new(uri) + # req.add_field('Foo', 'bar') + # req['Foo'] # => "bar" + # req.add_field('Foo', 'baz') + # req['Foo'] # => "bar, baz" + # req.add_field('Foo', %w[baz bam]) + # req['Foo'] # => "bar, baz, baz, bam" + # req.get_fields('Foo') # => ["bar", "baz", "baz", "bam"] + # + def add_field(key, val) + stringified_downcased_key = key.downcase.to_s + if @header.key?(stringified_downcased_key) + append_field_value(@header[stringified_downcased_key], val) + else + set_field(key, val) + end + end + + # :stopdoc: + private def set_field(key, val) + case val + when Enumerable + ary = [] + append_field_value(ary, val) + @header[key.downcase.to_s] = ary + else + val = val.to_s # for compatibility use to_s instead of to_str + if val.b.count("\r\n") > 0 + raise ArgumentError, 'header field value cannot include CR/LF' + end + @header[key.downcase.to_s] = [val] + end + end + + private def append_field_value(ary, val) + case val + when Enumerable + val.each{|x| append_field_value(ary, x)} + else + val = val.to_s + if /[\r\n]/n.match?(val.b) + raise ArgumentError, 'header field value cannot include CR/LF' + end + ary.push val + end + end + # :startdoc: + + # Returns the array field value for the given +key+, + # or +nil+ if there is no such field; + # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res.get_fields('Connection') # => ["keep-alive"] + # res.get_fields('Nosuch') # => nil + # + def get_fields(key) + stringified_downcased_key = key.downcase.to_s + return nil unless @header[stringified_downcased_key] + @header[stringified_downcased_key].dup + end + + # call-seq: + # fetch(key, default_val = nil) {|key| ... } -> object + # fetch(key, default_val = nil) -> value or default_val + # + # With a block, returns the string value for +key+ if it exists; + # otherwise returns the value of the block; + # ignores the +default_val+; + # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # + # # Field exists; block not called. + # res.fetch('Connection') do |value| + # fail 'Cannot happen' + # end # => "keep-alive" + # + # # Field does not exist; block called. + # res.fetch('Nosuch') do |value| + # value.downcase + # end # => "nosuch" + # + # With no block, returns the string value for +key+ if it exists; + # otherwise, returns +default_val+ if it was given; + # otherwise raises an exception: + # + # res.fetch('Connection', 'Foo') # => "keep-alive" + # res.fetch('Nosuch', 'Foo') # => "Foo" + # res.fetch('Nosuch') # Raises KeyError. + # + def fetch(key, *args, &block) #:yield: +key+ + a = @header.fetch(key.downcase.to_s, *args, &block) + a.kind_of?(Array) ? a.join(', ') : a + end + + # Calls the block with each key/value pair: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res.each_header do |key, value| + # p [key, value] if key.start_with?('c') + # end + # + # Output: + # + # ["content-type", "application/json; charset=utf-8"] + # ["connection", "keep-alive"] + # ["cache-control", "max-age=43200"] + # ["cf-cache-status", "HIT"] + # ["cf-ray", "771d17e9bc542cf5-ORD"] + # + # Returns an enumerator if no block is given. + # + # Net::HTTPHeader#each is an alias for Net::HTTPHeader#each_header. + def each_header #:yield: +key+, +value+ + block_given? or return enum_for(__method__) { @header.size } + @header.each do |k,va| + yield k, va.join(', ') + end + end + + alias each each_header + + # Calls the block with each field key: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res.each_key do |key| + # p key if key.start_with?('c') + # end + # + # Output: + # + # "content-type" + # "connection" + # "cache-control" + # "cf-cache-status" + # "cf-ray" + # + # Returns an enumerator if no block is given. + # + # Net::HTTPHeader#each_name is an alias for Net::HTTPHeader#each_key. + def each_name(&block) #:yield: +key+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_key(&block) + end + + alias each_key each_name + + # Calls the block with each capitalized field name: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res.each_capitalized_name do |key| + # p key if key.start_with?('C') + # end + # + # Output: + # + # "Content-Type" + # "Connection" + # "Cache-Control" + # "Cf-Cache-Status" + # "Cf-Ray" + # + # The capitalization is system-dependent; + # see {Case Mapping}[rdoc-ref:case_mapping.rdoc]. + # + # Returns an enumerator if no block is given. + def each_capitalized_name #:yield: +key+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_key do |k| + yield capitalize(k) + end + end + + # Calls the block with each string field value: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res.each_value do |value| + # p value if value.start_with?('c') + # end + # + # Output: + # + # "chunked" + # "cf-q-config;dur=6.0000002122251e-06" + # "cloudflare" + # + # Returns an enumerator if no block is given. + def each_value #:yield: +value+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_value do |va| + yield va.join(', ') + end + end + + # Removes the header for the given case-insensitive +key+ + # (see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]); + # returns the deleted value, or +nil+ if no such field exists: + # + # req = Net::HTTP::Get.new(uri) + # req.delete('Accept') # => ["*/*"] + # req.delete('Nosuch') # => nil + # + def delete(key) + @header.delete(key.downcase.to_s) + end + + # Returns +true+ if the field for the case-insensitive +key+ exists, +false+ otherwise: + # + # req = Net::HTTP::Get.new(uri) + # req.key?('Accept') # => true + # req.key?('Nosuch') # => false + # + def key?(key) + @header.key?(key.downcase.to_s) + end + + # Returns a hash of the key/value pairs: + # + # req = Net::HTTP::Get.new(uri) + # req.to_hash + # # => + # {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], + # "accept"=>["*/*"], + # "user-agent"=>["Ruby"], + # "host"=>["jsonplaceholder.typicode.com"]} + # + def to_hash + @header.dup + end + + # Like #each_header, but the keys are returned in capitalized form. + # + # Net::HTTPHeader#canonical_each is an alias for Net::HTTPHeader#each_capitalized. + def each_capitalized + block_given? or return enum_for(__method__) { @header.size } + @header.each do |k,v| + yield capitalize(k), v.join(', ') + end + end + + alias canonical_each each_capitalized + + def capitalize(name) # :nodoc: + name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze) + end + private :capitalize + + # Returns an array of Range objects that represent + # the value of field <tt>'Range'</tt>, + # or +nil+ if there is no such field; + # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]: + # + # req = Net::HTTP::Get.new(uri) + # req['Range'] = 'bytes=0-99,200-299,400-499' + # req.range # => [0..99, 200..299, 400..499] + # req.delete('Range') + # req.range # # => nil + # + def range + return nil unless @header['range'] + + value = self['Range'] + # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec ) + # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] ) + # corrected collected ABNF + # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1 + # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C + # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5 + unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value + raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'" + end + + byte_range_set = $1 + result = byte_range_set.split(/,/).map {|spec| + m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or + raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'" + d1 = m[1].to_i + d2 = m[2].to_i + if m[1] and m[2] + if d1 > d2 + raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'" + end + d1..d2 + elsif m[1] + d1..-1 + elsif m[2] + -d2..-1 + else + raise Net::HTTPHeaderSyntaxError, 'range is not specified' + end + } + # if result.empty? + # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec + # but above regexp already denies it. + if result.size == 1 && result[0].begin == 0 && result[0].end == -1 + raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length' + end + result + end + + # call-seq: + # set_range(length) -> length + # set_range(offset, length) -> range + # set_range(begin..length) -> range + # + # Sets the value for field <tt>'Range'</tt>; + # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]: + # + # With argument +length+: + # + # req = Net::HTTP::Get.new(uri) + # req.set_range(100) # => 100 + # req['Range'] # => "bytes=0-99" + # + # With arguments +offset+ and +length+: + # + # req.set_range(100, 100) # => 100...200 + # req['Range'] # => "bytes=100-199" + # + # With argument +range+: + # + # req.set_range(100..199) # => 100..199 + # req['Range'] # => "bytes=100-199" + # + # Net::HTTPHeader#range= is an alias for Net::HTTPHeader#set_range. + def set_range(r, e = nil) + unless r + @header.delete 'range' + return r + end + r = (r...r+e) if e + case r + when Numeric + n = r.to_i + rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") + when Range + first = r.first + last = r.end + last -= 1 if r.exclude_end? + if last == -1 + rangestr = (first > 0 ? "#{first}-" : "-#{-first}") + else + raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 + raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 + raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last + rangestr = "#{first}-#{last}" + end + else + raise TypeError, 'Range/Integer is required' + end + @header['range'] = ["bytes=#{rangestr}"] + r + end + + alias range= set_range + + # Returns the value of field <tt>'Content-Length'</tt> as an integer, + # or +nil+ if there is no such field; + # see {Content-Length request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-request-header]: + # + # res = Net::HTTP.get_response(hostname, '/nosuch/1') + # res.content_length # => 2 + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res.content_length # => nil + # + def content_length + return nil unless key?('Content-Length') + len = self['Content-Length'].slice(/\d+/) or + raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format' + len.to_i + end + + # Sets the value of field <tt>'Content-Length'</tt> to the given numeric; + # see {Content-Length response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-response-header]: + # + # _uri = uri.dup + # hostname = _uri.hostname # => "jsonplaceholder.typicode.com" + # _uri.path = '/posts' # => "/posts" + # req = Net::HTTP::Post.new(_uri) # => #<Net::HTTP::Post POST> + # req.body = '{"title": "foo","body": "bar","userId": 1}' + # req.content_length = req.body.size # => 42 + # req.content_type = 'application/json' + # res = Net::HTTP.start(hostname) do |http| + # http.request(req) + # end # => #<Net::HTTPCreated 201 Created readbody=true> + # + def content_length=(len) + unless len + @header.delete 'content-length' + return nil + end + @header['content-length'] = [len.to_i.to_s] + end + + # Returns +true+ if field <tt>'Transfer-Encoding'</tt> + # exists and has value <tt>'chunked'</tt>, + # +false+ otherwise; + # see {Transfer-Encoding response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#transfer-encoding-response-header]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res['Transfer-Encoding'] # => "chunked" + # res.chunked? # => true + # + def chunked? + return false unless @header['transfer-encoding'] + field = self['Transfer-Encoding'] + (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false + end + + # Returns a Range object representing the value of field + # <tt>'Content-Range'</tt>, or +nil+ if no such field exists; + # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res['Content-Range'] # => nil + # res['Content-Range'] = 'bytes 0-499/1000' + # res['Content-Range'] # => "bytes 0-499/1000" + # res.content_range # => 0..499 + # + def content_range + return nil unless @header['content-range'] + m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or + raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format' + return unless m[1] == 'bytes' + m[2].to_i .. m[3].to_i + end + + # Returns the integer representing length of the value of field + # <tt>'Content-Range'</tt>, or +nil+ if no such field exists; + # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res['Content-Range'] # => nil + # res['Content-Range'] = 'bytes 0-499/1000' + # res.range_length # => 500 + # + def range_length + r = content_range() or return nil + r.end - r.begin + 1 + end + + # Returns the {media type}[https://en.wikipedia.org/wiki/Media_type] + # from the value of field <tt>'Content-Type'</tt>, + # or +nil+ if no such field exists; + # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res['content-type'] # => "application/json; charset=utf-8" + # res.content_type # => "application/json" + # + def content_type + main = main_type() + return nil unless main + + sub = sub_type() + if sub + "#{main}/#{sub}" + else + main + end + end + + # Returns the leading ('type') part of the + # {media type}[https://en.wikipedia.org/wiki/Media_type] + # from the value of field <tt>'Content-Type'</tt>, + # or +nil+ if no such field exists; + # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res['content-type'] # => "application/json; charset=utf-8" + # res.main_type # => "application" + # + def main_type + return nil unless @header['content-type'] + self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip + end + + # Returns the trailing ('subtype') part of the + # {media type}[https://en.wikipedia.org/wiki/Media_type] + # from the value of field <tt>'Content-Type'</tt>, + # or +nil+ if no such field exists; + # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res['content-type'] # => "application/json; charset=utf-8" + # res.sub_type # => "json" + # + def sub_type + return nil unless @header['content-type'] + _, sub = *self['Content-Type'].split(';').first.to_s.split('/') + return nil unless sub + sub.strip + end + + # Returns the trailing ('parameters') part of the value of field <tt>'Content-Type'</tt>, + # or +nil+ if no such field exists; + # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: + # + # res = Net::HTTP.get_response(hostname, '/todos/1') + # res['content-type'] # => "application/json; charset=utf-8" + # res.type_params # => {"charset"=>"utf-8"} + # + def type_params + result = {} + list = self['Content-Type'].to_s.split(';') + list.shift + list.each do |param| + k, v = *param.split('=', 2) + result[k.strip] = v.strip + end + result + end + + # Sets the value of field <tt>'Content-Type'</tt>; + # returns the new value; + # see {Content-Type request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-request-header]: + # + # req = Net::HTTP::Get.new(uri) + # req.set_content_type('application/json') # => ["application/json"] + # + # Net::HTTPHeader#content_type= is an alias for Net::HTTPHeader#set_content_type. + def set_content_type(type, params = {}) + @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] + end + + alias content_type= set_content_type + + # Sets the request body to a URL-encoded string derived from argument +params+, + # and sets request header field <tt>'Content-Type'</tt> + # to <tt>'application/x-www-form-urlencoded'</tt>. + # + # The resulting request is suitable for HTTP request +POST+ or +PUT+. + # + # Argument +params+ must be suitable for use as argument +enum+ to + # {URI.encode_www_form}[rdoc-ref:URI.encode_www_form]. + # + # With only argument +params+ given, + # sets the body to a URL-encoded string with the default separator <tt>'&'</tt>: + # + # req = Net::HTTP::Post.new('example.com') + # + # req.set_form_data(q: 'ruby', lang: 'en') + # req.body # => "q=ruby&lang=en" + # req['Content-Type'] # => "application/x-www-form-urlencoded" + # + # req.set_form_data([['q', 'ruby'], ['lang', 'en']]) + # req.body # => "q=ruby&lang=en" + # + # req.set_form_data(q: ['ruby', 'perl'], lang: 'en') + # req.body # => "q=ruby&q=perl&lang=en" + # + # req.set_form_data([['q', 'ruby'], ['q', 'perl'], ['lang', 'en']]) + # req.body # => "q=ruby&q=perl&lang=en" + # + # With string argument +sep+ also given, + # uses that string as the separator: + # + # req.set_form_data({q: 'ruby', lang: 'en'}, '|') + # req.body # => "q=ruby|lang=en" + # + # Net::HTTPHeader#form_data= is an alias for Net::HTTPHeader#set_form_data. + def set_form_data(params, sep = '&') + query = URI.encode_www_form(params) + query.gsub!(/&/, sep) if sep != '&' + self.body = query + self.content_type = 'application/x-www-form-urlencoded' + end + + alias form_data= set_form_data + + # Stores form data to be used in a +POST+ or +PUT+ request. + # + # The form data given in +params+ consists of zero or more fields; + # each field is: + # + # - A scalar value. + # - A name/value pair. + # - An IO stream opened for reading. + # + # Argument +params+ should be an + # {Enumerable}[rdoc-ref:Enumerable@Enumerable+in+Ruby+Classes] + # (method <tt>params.map</tt> will be called), + # and is often an array or hash. + # + # First, we set up a request: + # + # _uri = uri.dup + # _uri.path ='/posts' + # req = Net::HTTP::Post.new(_uri) + # + # <b>Argument +params+ As an Array</b> + # + # When +params+ is an array, + # each of its elements is a subarray that defines a field; + # the subarray may contain: + # + # - One string: + # + # req.set_form([['foo'], ['bar'], ['baz']]) + # + # - Two strings: + # + # req.set_form([%w[foo 0], %w[bar 1], %w[baz 2]]) + # + # - When argument +enctype+ (see below) is given as + # <tt>'multipart/form-data'</tt>: + # + # - A string name and an IO stream opened for reading: + # + # require 'stringio' + # req.set_form([['file', StringIO.new('Ruby is cool.')]]) + # + # - A string name, an IO stream opened for reading, + # and an options hash, which may contain these entries: + # + # - +:filename+: The name of the file to use. + # - +:content_type+: The content type of the uploaded file. + # + # Example: + # + # req.set_form([['file', file, {filename: "other-filename.foo"}]] + # + # The various forms may be mixed: + # + # req.set_form(['foo', %w[bar 1], ['file', file]]) + # + # <b>Argument +params+ As a Hash</b> + # + # When +params+ is a hash, + # each of its entries is a name/value pair that defines a field: + # + # - The name is a string. + # - The value may be: + # + # - +nil+. + # - Another string. + # - An IO stream opened for reading + # (only when argument +enctype+ -- see below -- is given as + # <tt>'multipart/form-data'</tt>). + # + # Examples: + # + # # Nil-valued fields. + # req.set_form({'foo' => nil, 'bar' => nil, 'baz' => nil}) + # + # # String-valued fields. + # req.set_form({'foo' => 0, 'bar' => 1, 'baz' => 2}) + # + # # IO-valued field. + # require 'stringio' + # req.set_form({'file' => StringIO.new('Ruby is cool.')}) + # + # # Mixture of fields. + # req.set_form({'foo' => nil, 'bar' => 1, 'file' => file}) + # + # Optional argument +enctype+ specifies the value to be given + # to field <tt>'Content-Type'</tt>, and must be one of: + # + # - <tt>'application/x-www-form-urlencoded'</tt> (the default). + # - <tt>'multipart/form-data'</tt>; + # see {RFC 7578}[https://www.rfc-editor.org/rfc/rfc7578]. + # + # Optional argument +formopt+ is a hash of options + # (applicable only when argument +enctype+ + # is <tt>'multipart/form-data'</tt>) + # that may include the following entries: + # + # - +:boundary+: The value is the boundary string for the multipart message. + # If not given, the boundary is a random string. + # See {Boundary}[https://www.rfc-editor.org/rfc/rfc7578#section-4.1]. + # - +:charset+: Value is the character set for the form submission. + # Field names and values of non-file fields should be encoded with this charset. + # + def set_form(params, enctype='application/x-www-form-urlencoded', formopt={}) + @body_data = params + @body = nil + @body_stream = nil + @form_option = formopt + case enctype + when /\Aapplication\/x-www-form-urlencoded\z/i, + /\Amultipart\/form-data\z/i + self.content_type = enctype + else + raise ArgumentError, "invalid enctype: #{enctype}" + end + end + + # Sets header <tt>'Authorization'</tt> using the given + # +account+ and +password+ strings: + # + # req.basic_auth('my_account', 'my_password') + # req['Authorization'] + # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA==" + # + def basic_auth(account, password) + @header['authorization'] = [basic_encode(account, password)] + end + + # Sets header <tt>'Proxy-Authorization'</tt> using the given + # +account+ and +password+ strings: + # + # req.proxy_basic_auth('my_account', 'my_password') + # req['Proxy-Authorization'] + # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA==" + # + def proxy_basic_auth(account, password) + @header['proxy-authorization'] = [basic_encode(account, password)] + end + + def basic_encode(account, password) # :nodoc: + 'Basic ' + ["#{account}:#{password}"].pack('m0') + end + private :basic_encode + + # Returns whether the HTTP session is to be closed. + def connection_close? + token = /(?:\A|,)\s*close\s*(?:\z|,)/i + @header['connection']&.grep(token) {return true} + @header['proxy-connection']&.grep(token) {return true} + false + end + + # Returns whether the HTTP session is to be kept alive. + def connection_keep_alive? + token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i + @header['connection']&.grep(token) {return true} + @header['proxy-connection']&.grep(token) {return true} + false + end + +end diff --git a/lib/net/http/net-http.gemspec b/lib/net/http/net-http.gemspec new file mode 100644 index 0000000000..80e94c7bb6 --- /dev/null +++ b/lib/net/http/net-http.gemspec @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +name = File.basename(__FILE__, ".gemspec") +version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir| + file = File.join(__dir__, dir, "#{name.tr('-', '/')}.rb") + begin + break File.foreach(file, mode: "rb") do |line| + /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 + end + rescue SystemCallError + next + end +end + +Gem::Specification.new do |spec| + spec.name = name + spec.version = version + spec.authors = ["NARUSE, Yui"] + spec.email = ["naruse@airemix.jp"] + + spec.summary = %q{HTTP client api for Ruby.} + spec.description = %q{HTTP client api for Ruby.} + spec.homepage = "https://github.com/ruby/net-http" + spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") + spec.licenses = ["Ruby", "BSD-2-Clause"] + + spec.metadata["changelog_uri"] = spec.homepage + "/releases" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + excludes = %W[/.git* /bin /test /*file /#{File.basename(__FILE__)}] + spec.files = IO.popen(%W[git -C #{__dir__} ls-files -z --] + excludes.map {|e| ":^#{e}"}, &:read).split("\x0") + spec.bindir = "exe" + spec.require_paths = ["lib"] + + spec.add_dependency "uri", ">= 0.11.1" +end diff --git a/lib/net/http/proxy_delta.rb b/lib/net/http/proxy_delta.rb new file mode 100644 index 0000000000..e7d30def64 --- /dev/null +++ b/lib/net/http/proxy_delta.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +module Net::HTTP::ProxyDelta #:nodoc: internal use only + private + + def conn_address + proxy_address() + end + + def conn_port + proxy_port() + end + + def edit_path(path) + use_ssl? ? path : "http://#{addr_port()}#{path}" + end +end + diff --git a/lib/net/http/request.rb b/lib/net/http/request.rb new file mode 100644 index 0000000000..4a138572e9 --- /dev/null +++ b/lib/net/http/request.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# This class is the base class for \Net::HTTP request classes. +# The class should not be used directly; +# instead you should use its subclasses, listed below. +# +# == Creating a Request +# +# An request object may be created with either a URI or a string hostname: +# +# require 'net/http' +# uri = URI('https://jsonplaceholder.typicode.com/') +# req = Net::HTTP::Get.new(uri) # => #<Net::HTTP::Get GET> +# req = Net::HTTP::Get.new(uri.hostname) # => #<Net::HTTP::Get GET> +# +# And with any of the subclasses: +# +# req = Net::HTTP::Head.new(uri) # => #<Net::HTTP::Head HEAD> +# req = Net::HTTP::Post.new(uri) # => #<Net::HTTP::Post POST> +# req = Net::HTTP::Put.new(uri) # => #<Net::HTTP::Put PUT> +# # ... +# +# The new instance is suitable for use as the argument to Net::HTTP#request. +# +# == Request Headers +# +# A new request object has these header fields by default: +# +# req.to_hash +# # => +# {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], +# "accept"=>["*/*"], +# "user-agent"=>["Ruby"], +# "host"=>["jsonplaceholder.typicode.com"]} +# +# See: +# +# - {Request header Accept-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Accept-Encoding] +# and {Compression and Decompression}[rdoc-ref:Net::HTTP@Compression+and+Decompression]. +# - {Request header Accept}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#accept-request-header]. +# - {Request header User-Agent}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#user-agent-request-header]. +# - {Request header Host}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#host-request-header]. +# +# You can add headers or override default headers: +# +# # res = Net::HTTP::Get.new(uri, {'foo' => '0', 'bar' => '1'}) +# +# This class (and therefore its subclasses) also includes (indirectly) +# module Net::HTTPHeader, which gives access to its +# {methods for setting headers}[rdoc-ref:Net::HTTPHeader@Setters]. +# +# == Request Subclasses +# +# Subclasses for HTTP requests: +# +# - Net::HTTP::Get +# - Net::HTTP::Head +# - Net::HTTP::Post +# - Net::HTTP::Put +# - Net::HTTP::Delete +# - Net::HTTP::Options +# - Net::HTTP::Trace +# - Net::HTTP::Patch +# +# Subclasses for WebDAV requests: +# +# - Net::HTTP::Propfind +# - Net::HTTP::Proppatch +# - Net::HTTP::Mkcol +# - Net::HTTP::Copy +# - Net::HTTP::Move +# - Net::HTTP::Lock +# - Net::HTTP::Unlock +# +class Net::HTTPRequest < Net::HTTPGenericRequest + # Creates an HTTP request object for +path+. + # + # +initheader+ are the default headers to use. Net::HTTP adds + # Accept-Encoding to enable compression of the response body unless + # Accept-Encoding or Range are supplied in +initheader+. + + def initialize(path, initheader = nil) + super self.class::METHOD, + self.class::REQUEST_HAS_BODY, + self.class::RESPONSE_HAS_BODY, + path, initheader + end +end diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb new file mode 100644 index 0000000000..939d413f91 --- /dev/null +++ b/lib/net/http/requests.rb @@ -0,0 +1,444 @@ +# frozen_string_literal: true + +# HTTP/1.1 methods --- RFC2616 + +# \Class for representing +# {HTTP method GET}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#GET_method]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Get.new(uri) # => #<Net::HTTP::Get GET> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: optional. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. +# +# Related: +# +# - Net::HTTP.get: sends +GET+ request, returns response body. +# - Net::HTTP#get: sends +GET+ request, returns response object. +# +class Net::HTTP::Get < Net::HTTPRequest + # :stopdoc: + METHOD = 'GET' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method HEAD}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#HEAD_method]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Head.new(uri) # => #<Net::HTTP::Head HEAD> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: optional. +# - Response body: no. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. +# +# Related: +# +# - Net::HTTP#head: sends +HEAD+ request, returns response object. +# +class Net::HTTP::Head < Net::HTTPRequest + # :stopdoc: + METHOD = 'HEAD' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = false +end + +# \Class for representing +# {HTTP method POST}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#POST_method]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts' +# req = Net::HTTP::Post.new(uri) # => #<Net::HTTP::Post POST> +# req.body = '{"title": "foo","body": "bar","userId": 1}' +# req.content_type = 'application/json' +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: yes. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. +# +# Related: +# +# - Net::HTTP.post: sends +POST+ request, returns response object. +# - Net::HTTP#post: sends +POST+ request, returns response object. +# +class Net::HTTP::Post < Net::HTTPRequest + # :stopdoc: + METHOD = 'POST' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method PUT}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PUT_method]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts' +# req = Net::HTTP::Put.new(uri) # => #<Net::HTTP::Put PUT> +# req.body = '{"title": "foo","body": "bar","userId": 1}' +# req.content_type = 'application/json' +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: yes. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Net::HTTP.put: sends +PUT+ request, returns response object. +# - Net::HTTP#put: sends +PUT+ request, returns response object. +# +class Net::HTTP::Put < Net::HTTPRequest + # :stopdoc: + METHOD = 'PUT' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method DELETE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#DELETE_method]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts/1' +# req = Net::HTTP::Delete.new(uri) # => #<Net::HTTP::Delete DELETE> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: optional. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Net::HTTP#delete: sends +DELETE+ request, returns response object. +# +class Net::HTTP::Delete < Net::HTTPRequest + # :stopdoc: + METHOD = 'DELETE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method OPTIONS}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#OPTIONS_method]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Options.new(uri) # => #<Net::HTTP::Options OPTIONS> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: optional. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Net::HTTP#options: sends +OPTIONS+ request, returns response object. +# +class Net::HTTP::Options < Net::HTTPRequest + # :stopdoc: + METHOD = 'OPTIONS' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method TRACE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#TRACE_method]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Trace.new(uri) # => #<Net::HTTP::Trace TRACE> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: no. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Net::HTTP#trace: sends +TRACE+ request, returns response object. +# +class Net::HTTP::Trace < Net::HTTPRequest + # :stopdoc: + METHOD = 'TRACE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method PATCH}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PATCH_method]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts' +# req = Net::HTTP::Patch.new(uri) # => #<Net::HTTP::Patch PATCH> +# req.body = '{"title": "foo","body": "bar","userId": 1}' +# req.content_type = 'application/json' +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: yes. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Net::HTTP#patch: sends +PATCH+ request, returns response object. +# +class Net::HTTP::Patch < Net::HTTPRequest + # :stopdoc: + METHOD = 'PATCH' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# +# WebDAV methods --- RFC2518 +# + +# \Class for representing +# {WebDAV method PROPFIND}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Propfind.new(uri) # => #<Net::HTTP::Propfind PROPFIND> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Net::HTTP#propfind: sends +PROPFIND+ request, returns response object. +# +class Net::HTTP::Propfind < Net::HTTPRequest + # :stopdoc: + METHOD = 'PROPFIND' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method PROPPATCH}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Proppatch.new(uri) # => #<Net::HTTP::Proppatch PROPPATCH> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Net::HTTP#proppatch: sends +PROPPATCH+ request, returns response object. +# +class Net::HTTP::Proppatch < Net::HTTPRequest + # :stopdoc: + METHOD = 'PROPPATCH' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method MKCOL}[http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Mkcol.new(uri) # => #<Net::HTTP::Mkcol MKCOL> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Net::HTTP#mkcol: sends +MKCOL+ request, returns response object. +# +class Net::HTTP::Mkcol < Net::HTTPRequest + # :stopdoc: + METHOD = 'MKCOL' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method COPY}[http://www.webdav.org/specs/rfc4918.html#METHOD_COPY]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Copy.new(uri) # => #<Net::HTTP::Copy COPY> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Net::HTTP#copy: sends +COPY+ request, returns response object. +# +class Net::HTTP::Copy < Net::HTTPRequest + # :stopdoc: + METHOD = 'COPY' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method MOVE}[http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Move.new(uri) # => #<Net::HTTP::Move MOVE> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Net::HTTP#move: sends +MOVE+ request, returns response object. +# +class Net::HTTP::Move < Net::HTTPRequest + # :stopdoc: + METHOD = 'MOVE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method LOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_LOCK]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Lock.new(uri) # => #<Net::HTTP::Lock LOCK> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Net::HTTP#lock: sends +LOCK+ request, returns response object. +# +class Net::HTTP::Lock < Net::HTTPRequest + # :stopdoc: + METHOD = 'LOCK' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method UNLOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_UNLOCK]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Net::HTTP::Unlock.new(uri) # => #<Net::HTTP::Unlock UNLOCK> +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Net::HTTP#unlock: sends +UNLOCK+ request, returns response object. +# +class Net::HTTP::Unlock < Net::HTTPRequest + # :stopdoc: + METHOD = 'UNLOCK' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb new file mode 100644 index 0000000000..8804a99c9e --- /dev/null +++ b/lib/net/http/response.rb @@ -0,0 +1,739 @@ +# frozen_string_literal: true + +# This class is the base class for \Net::HTTP response classes. +# +# == About the Examples +# +# :include: doc/net-http/examples.rdoc +# +# == Returned Responses +# +# \Method Net::HTTP.get_response returns +# an instance of one of the subclasses of \Net::HTTPResponse: +# +# Net::HTTP.get_response(uri) +# # => #<Net::HTTPOK 200 OK readbody=true> +# Net::HTTP.get_response(hostname, '/nosuch') +# # => #<Net::HTTPNotFound 404 Not Found readbody=true> +# +# As does method Net::HTTP#request: +# +# req = Net::HTTP::Get.new(uri) +# Net::HTTP.start(hostname) do |http| +# http.request(req) +# end # => #<Net::HTTPOK 200 OK readbody=true> +# +# \Class \Net::HTTPResponse includes module Net::HTTPHeader, +# which provides access to response header values via (among others): +# +# - \Hash-like method <tt>[]</tt>. +# - Specific reader methods, such as +content_type+. +# +# Examples: +# +# res = Net::HTTP.get_response(uri) # => #<Net::HTTPOK 200 OK readbody=true> +# res['Content-Type'] # => "text/html; charset=UTF-8" +# res.content_type # => "text/html" +# +# == Response Subclasses +# +# \Class \Net::HTTPResponse has a subclass for each +# {HTTP status code}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes]. +# You can look up the response class for a given code: +# +# Net::HTTPResponse::CODE_TO_OBJ['200'] # => Net::HTTPOK +# Net::HTTPResponse::CODE_TO_OBJ['400'] # => Net::HTTPBadRequest +# Net::HTTPResponse::CODE_TO_OBJ['404'] # => Net::HTTPNotFound +# +# And you can retrieve the status code for a response object: +# +# Net::HTTP.get_response(uri).code # => "200" +# Net::HTTP.get_response(hostname, '/nosuch').code # => "404" +# +# The response subclasses (indentation shows class hierarchy): +# +# - Net::HTTPUnknownResponse (for unhandled \HTTP extensions). +# +# - Net::HTTPInformation: +# +# - Net::HTTPContinue (100) +# - Net::HTTPSwitchProtocol (101) +# - Net::HTTPProcessing (102) +# - Net::HTTPEarlyHints (103) +# +# - Net::HTTPSuccess: +# +# - Net::HTTPOK (200) +# - Net::HTTPCreated (201) +# - Net::HTTPAccepted (202) +# - Net::HTTPNonAuthoritativeInformation (203) +# - Net::HTTPNoContent (204) +# - Net::HTTPResetContent (205) +# - Net::HTTPPartialContent (206) +# - Net::HTTPMultiStatus (207) +# - Net::HTTPAlreadyReported (208) +# - Net::HTTPIMUsed (226) +# +# - Net::HTTPRedirection: +# +# - Net::HTTPMultipleChoices (300) +# - Net::HTTPMovedPermanently (301) +# - Net::HTTPFound (302) +# - Net::HTTPSeeOther (303) +# - Net::HTTPNotModified (304) +# - Net::HTTPUseProxy (305) +# - Net::HTTPTemporaryRedirect (307) +# - Net::HTTPPermanentRedirect (308) +# +# - Net::HTTPClientError: +# +# - Net::HTTPBadRequest (400) +# - Net::HTTPUnauthorized (401) +# - Net::HTTPPaymentRequired (402) +# - Net::HTTPForbidden (403) +# - Net::HTTPNotFound (404) +# - Net::HTTPMethodNotAllowed (405) +# - Net::HTTPNotAcceptable (406) +# - Net::HTTPProxyAuthenticationRequired (407) +# - Net::HTTPRequestTimeOut (408) +# - Net::HTTPConflict (409) +# - Net::HTTPGone (410) +# - Net::HTTPLengthRequired (411) +# - Net::HTTPPreconditionFailed (412) +# - Net::HTTPRequestEntityTooLarge (413) +# - Net::HTTPRequestURITooLong (414) +# - Net::HTTPUnsupportedMediaType (415) +# - Net::HTTPRequestedRangeNotSatisfiable (416) +# - Net::HTTPExpectationFailed (417) +# - Net::HTTPMisdirectedRequest (421) +# - Net::HTTPUnprocessableEntity (422) +# - Net::HTTPLocked (423) +# - Net::HTTPFailedDependency (424) +# - Net::HTTPUpgradeRequired (426) +# - Net::HTTPPreconditionRequired (428) +# - Net::HTTPTooManyRequests (429) +# - Net::HTTPRequestHeaderFieldsTooLarge (431) +# - Net::HTTPUnavailableForLegalReasons (451) +# +# - Net::HTTPServerError: +# +# - Net::HTTPInternalServerError (500) +# - Net::HTTPNotImplemented (501) +# - Net::HTTPBadGateway (502) +# - Net::HTTPServiceUnavailable (503) +# - Net::HTTPGatewayTimeOut (504) +# - Net::HTTPVersionNotSupported (505) +# - Net::HTTPVariantAlsoNegotiates (506) +# - Net::HTTPInsufficientStorage (507) +# - Net::HTTPLoopDetected (508) +# - Net::HTTPNotExtended (510) +# - Net::HTTPNetworkAuthenticationRequired (511) +# +# There is also the Net::HTTPBadResponse exception which is raised when +# there is a protocol error. +# +class Net::HTTPResponse + class << self + # true if the response has a body. + def body_permitted? + self::HAS_BODY + end + + def exception_type # :nodoc: internal use only + self::EXCEPTION_TYPE + end + + def read_new(sock) #:nodoc: internal use only + httpv, code, msg = read_status_line(sock) + res = response_class(code).new(httpv, code, msg) + each_response_header(sock) do |k,v| + res.add_field k, v + end + res + end + + private + # :stopdoc: + + def read_status_line(sock) + str = sock.readline + m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or + raise Net::HTTPBadResponse, "wrong status line: #{str.dump}" + m.captures + end + + def response_class(code) + CODE_TO_OBJ[code] or + CODE_CLASS_TO_OBJ[code[0,1]] or + Net::HTTPUnknownResponse + end + + def each_response_header(sock) + key = value = nil + while true + line = sock.readuntil("\n", true).sub(/\s+\z/, '') + break if line.empty? + if line[0] == ?\s or line[0] == ?\t and value + value << ' ' unless value.empty? + value << line.strip + else + yield key, value if key + key, value = line.strip.split(/\s*:\s*/, 2) + raise Net::HTTPBadResponse, 'wrong header line format' if value.nil? + end + end + yield key, value if key + end + end + + # next is to fix bug in RDoc, where the private inside class << self + # spills out. + public + + include Net::HTTPHeader + + def initialize(httpv, code, msg) #:nodoc: internal use only + @http_version = httpv + @code = code + @message = msg + initialize_http_header nil + @body = nil + @read = false + @uri = nil + @decode_content = false + @body_encoding = false + @ignore_eof = true + end + + # The HTTP version supported by the server. + attr_reader :http_version + + # The HTTP result code string. For example, '302'. You can also + # determine the response type by examining which response subclass + # the response object is an instance of. + attr_reader :code + + # The HTTP result message sent by the server. For example, 'Not Found'. + attr_reader :message + alias msg message # :nodoc: obsolete + + # The URI used to fetch this response. The response URI is only available + # if a URI was used to create the request. + attr_reader :uri + + # Set to true automatically when the request did not contain an + # Accept-Encoding header from the user. + attr_accessor :decode_content + + # Returns the value set by body_encoding=, or +false+ if none; + # see #body_encoding=. + attr_reader :body_encoding + + # Sets the encoding that should be used when reading the body: + # + # - If the given value is an Encoding object, that encoding will be used. + # - Otherwise if the value is a string, the value of + # {Encoding#find(value)}[rdoc-ref:Encoding.find] + # will be used. + # - Otherwise an encoding will be deduced from the body itself. + # + # Examples: + # + # http = Net::HTTP.new(hostname) + # req = Net::HTTP::Get.new('/') + # + # http.request(req) do |res| + # p res.body.encoding # => #<Encoding:ASCII-8BIT> + # end + # + # http.request(req) do |res| + # res.body_encoding = "UTF-8" + # p res.body.encoding # => #<Encoding:UTF-8> + # end + # + def body_encoding=(value) + value = Encoding.find(value) if value.is_a?(String) + @body_encoding = value + end + + # Whether to ignore EOF when reading bodies with a specified Content-Length + # header. + attr_accessor :ignore_eof + + def inspect # :nodoc: + "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" + end + + # + # response <-> exception relationship + # + + def code_type #:nodoc: + self.class + end + + def error! #:nodoc: + message = @code + message = "#{message} #{@message.dump}" if @message + raise error_type().new(message, self) + end + + def error_type #:nodoc: + self.class::EXCEPTION_TYPE + end + + # Raises an HTTP error if the response is not 2xx (success). + def value + error! unless self.kind_of?(Net::HTTPSuccess) + end + + def uri= uri # :nodoc: + @uri = uri.dup if uri + end + + # + # header (for backward compatibility only; DO NOT USE) + # + + def response #:nodoc: + warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE + self + end + + def header #:nodoc: + warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE + self + end + + def read_header #:nodoc: + warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE + self + end + + # + # body + # + + def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only + @socket = sock + @body_exist = reqmethodallowbody && self.class.body_permitted? + begin + yield + self.body # ensure to read body + ensure + @socket = nil + end + end + + # Gets the entity body returned by the remote HTTP server. + # + # If a block is given, the body is passed to the block, and + # the body is provided in fragments, as it is read in from the socket. + # + # If +dest+ argument is given, response is read into that variable, + # with <code>dest#<<</code> method (it could be String or IO, or any + # other object responding to <code><<</code>). + # + # Calling this method a second or subsequent time for the same + # HTTPResponse object will return the value already read. + # + # http.request_get('/index.html') {|res| + # puts res.read_body + # } + # + # http.request_get('/index.html') {|res| + # p res.read_body.object_id # 538149362 + # p res.read_body.object_id # 538149362 + # } + # + # # using iterator + # http.request_get('/index.html') {|res| + # res.read_body do |segment| + # print segment + # end + # } + # + def read_body(dest = nil, &block) + if @read + raise IOError, "#{self.class}\#read_body called twice" if dest or block + return @body + end + to = procdest(dest, block) + stream_check + if @body_exist + read_body_0 to + @body = to + else + @body = nil + end + @read = true + return if @body.nil? + + case enc = @body_encoding + when Encoding, false, nil + # Encoding: force given encoding + # false/nil: do not force encoding + else + # other value: detect encoding from body + enc = detect_encoding(@body) + end + + @body.force_encoding(enc) if enc + + @body + end + + # Returns the string response body; + # note that repeated calls for the unmodified body return a cached string: + # + # path = '/todos/1' + # Net::HTTP.start(hostname) do |http| + # res = http.get(path) + # p res.body + # p http.head(path).body # No body. + # end + # + # Output: + # + # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}" + # nil + # + def body + read_body() + end + + # Sets the body of the response to the given value. + def body=(value) + @body = value + end + + alias entity body #:nodoc: obsolete + + private + + # :nodoc: + def detect_encoding(str, encoding=nil) + if encoding + elsif encoding = type_params['charset'] + elsif encoding = check_bom(str) + else + encoding = case content_type&.downcase + when %r{text/x(?:ht)?ml|application/(?:[^+]+\+)?xml} + /\A<xml[ \t\r\n]+ + version[ \t\r\n]*=[ \t\r\n]*(?:"[0-9.]+"|'[0-9.]*')[ \t\r\n]+ + encoding[ \t\r\n]*=[ \t\r\n]* + (?:"([A-Za-z][\-A-Za-z0-9._]*)"|'([A-Za-z][\-A-Za-z0-9._]*)')/x =~ str + encoding = $1 || $2 || Encoding::UTF_8 + when %r{text/html.*} + sniff_encoding(str) + end + end + return encoding + end + + # :nodoc: + def sniff_encoding(str, encoding=nil) + # the encoding sniffing algorithm + # http://www.w3.org/TR/html5/parsing.html#determining-the-character-encoding + if enc = scanning_meta(str) + enc + # 6. last visited page or something + # 7. frequency + elsif str.ascii_only? + Encoding::US_ASCII + elsif str.dup.force_encoding(Encoding::UTF_8).valid_encoding? + Encoding::UTF_8 + end + # 8. implementation-defined or user-specified + end + + # :nodoc: + def check_bom(str) + case str.byteslice(0, 2) + when "\xFE\xFF" + return Encoding::UTF_16BE + when "\xFF\xFE" + return Encoding::UTF_16LE + end + if "\xEF\xBB\xBF" == str.byteslice(0, 3) + return Encoding::UTF_8 + end + nil + end + + # :nodoc: + def scanning_meta(str) + require 'strscan' + ss = StringScanner.new(str) + if ss.scan_until(/<meta[\t\n\f\r ]*/) + attrs = {} # attribute_list + got_pragma = false + need_pragma = nil + charset = nil + + # step: Attributes + while attr = get_attribute(ss) + name, value = *attr + next if attrs[name] + attrs[name] = true + case name + when 'http-equiv' + got_pragma = true if value == 'content-type' + when 'content' + encoding = extracting_encodings_from_meta_elements(value) + unless charset + charset = encoding + end + need_pragma = true + when 'charset' + need_pragma = false + charset = value + end + end + + # step: Processing + return if need_pragma.nil? + return if need_pragma && !got_pragma + + charset = Encoding.find(charset) rescue nil + return unless charset + charset = Encoding::UTF_8 if charset == Encoding::UTF_16 + return charset # tentative + end + nil + end + + def get_attribute(ss) + ss.scan(/[\t\n\f\r \/]*/) + if ss.peek(1) == '>' + ss.getch + return nil + end + name = ss.scan(/[^=\t\n\f\r \/>]*/) + name.downcase! + raise if name.empty? + ss.skip(/[\t\n\f\r ]*/) + if ss.getch != '=' + value = '' + return [name, value] + end + ss.skip(/[\t\n\f\r ]*/) + case ss.peek(1) + when '"' + ss.getch + value = ss.scan(/[^"]+/) + value.downcase! + ss.getch + when "'" + ss.getch + value = ss.scan(/[^']+/) + value.downcase! + ss.getch + when '>' + value = '' + else + value = ss.scan(/[^\t\n\f\r >]+/) + value.downcase! + end + [name, value] + end + + def extracting_encodings_from_meta_elements(value) + # http://dev.w3.org/html5/spec/fetching-resources.html#algorithm-for-extracting-an-encoding-from-a-meta-element + if /charset[\t\n\f\r ]*=(?:"([^"]*)"|'([^']*)'|["']|\z|([^\t\n\f\r ;]+))/i =~ value + return $1 || $2 || $3 + end + return nil + end + + ## + # Checks for a supported Content-Encoding header and yields an Inflate + # wrapper for this response's socket when zlib is present. If the + # Content-Encoding is not supported or zlib is missing, the plain socket is + # yielded. + # + # If a Content-Range header is present, a plain socket is yielded as the + # bytes in the range may not be a complete deflate block. + + def inflater # :nodoc: + return yield @socket unless Net::HTTP::HAVE_ZLIB + return yield @socket unless @decode_content + return yield @socket if self['content-range'] + + v = self['content-encoding'] + case v&.downcase + when 'deflate', 'gzip', 'x-gzip' then + self.delete 'content-encoding' + + inflate_body_io = Inflater.new(@socket) + + begin + yield inflate_body_io + success = true + ensure + begin + inflate_body_io.finish + if self['content-length'] + self['content-length'] = inflate_body_io.bytes_inflated.to_s + end + rescue => err + # Ignore #finish's error if there is an exception from yield + raise err if success + end + end + when 'none', 'identity' then + self.delete 'content-encoding' + + yield @socket + else + yield @socket + end + end + + def read_body_0(dest) + inflater do |inflate_body_io| + if chunked? + read_chunked dest, inflate_body_io + return + end + + @socket = inflate_body_io + + clen = content_length() + if clen + @socket.read clen, dest, @ignore_eof + return + end + clen = range_length() + if clen + @socket.read clen, dest + return + end + @socket.read_all dest + end + end + + ## + # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF, + # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip + # encoded. + # + # See RFC 2616 section 3.6.1 for definitions + + def read_chunked(dest, chunk_data_io) # :nodoc: + total = 0 + while true + line = @socket.readline + hexlen = line.slice(/[0-9a-fA-F]+/) or + raise Net::HTTPBadResponse, "wrong chunk size line: #{line}" + len = hexlen.hex + break if len == 0 + begin + chunk_data_io.read len, dest + ensure + total += len + @socket.read 2 # \r\n + end + end + until @socket.readline.empty? + # none + end + end + + def stream_check + raise IOError, 'attempt to read body out of block' if @socket.nil? || @socket.closed? + end + + def procdest(dest, block) + raise ArgumentError, 'both arg and block given for HTTP method' if + dest and block + if block + Net::ReadAdapter.new(block) + else + dest || +'' + end + end + + ## + # Inflater is a wrapper around Net::BufferedIO that transparently inflates + # zlib and gzip streams. + + class Inflater # :nodoc: + + ## + # Creates a new Inflater wrapping +socket+ + + def initialize socket + @socket = socket + # zlib with automatic gzip detection + @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS) + end + + ## + # Finishes the inflate stream. + + def finish + return if @inflate.total_in == 0 + @inflate.finish + end + + ## + # The number of bytes inflated, used to update the Content-Length of + # the response. + + def bytes_inflated + @inflate.total_out + end + + ## + # Returns a Net::ReadAdapter that inflates each read chunk into +dest+. + # + # This allows a large response body to be inflated without storing the + # entire body in memory. + + def inflate_adapter(dest) + if dest.respond_to?(:set_encoding) + dest.set_encoding(Encoding::ASCII_8BIT) + elsif dest.respond_to?(:force_encoding) + dest.force_encoding(Encoding::ASCII_8BIT) + end + block = proc do |compressed_chunk| + @inflate.inflate(compressed_chunk) do |chunk| + compressed_chunk.clear + dest << chunk + end + end + + Net::ReadAdapter.new(block) + end + + ## + # Reads +clen+ bytes from the socket, inflates them, then writes them to + # +dest+. +ignore_eof+ is passed down to Net::BufferedIO#read + # + # Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes. + # At this time there is no way for a user of Net::HTTPResponse to read a + # specific number of bytes from the HTTP response body, so this internal + # API does not return the same number of bytes as were requested. + # + # See https://bugs.ruby-lang.org/issues/6492 for further discussion. + + def read clen, dest, ignore_eof = false + temp_dest = inflate_adapter(dest) + + @socket.read clen, temp_dest, ignore_eof + end + + ## + # Reads the rest of the socket, inflates it, then writes it to +dest+. + + def read_all dest + temp_dest = inflate_adapter(dest) + + @socket.read_all temp_dest + end + + end + +end + diff --git a/lib/net/http/responses.rb b/lib/net/http/responses.rb new file mode 100644 index 0000000000..941a6fed80 --- /dev/null +++ b/lib/net/http/responses.rb @@ -0,0 +1,1242 @@ +# frozen_string_literal: true +#-- +# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + +module Net + + # Unknown HTTP response + class HTTPUnknownResponse < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPError # + end + + # Parent class for informational (1xx) HTTP response classes. + # + # An informational response indicates that the request was received and understood. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.1xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#1xx_informational_response]. + # + class HTTPInformation < HTTPResponse + # :stopdoc: + HAS_BODY = false + EXCEPTION_TYPE = HTTPError # + end + + # Parent class for success (2xx) HTTP response classes. + # + # A success response indicates the action requested by the client + # was received, understood, and accepted. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.2xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_success]. + # + class HTTPSuccess < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPError # + end + + # Parent class for redirection (3xx) HTTP response classes. + # + # A redirection response indicates the client must take additional action + # to complete the request. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.3xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection]. + # + class HTTPRedirection < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPRetriableError # + end + + # Parent class for client error (4xx) HTTP response classes. + # + # A client error response indicates that the client may have caused an error. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.4xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors]. + # + class HTTPClientError < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPClientException # + end + + # Parent class for server error (5xx) HTTP response classes. + # + # A server error response indicates that the server failed to fulfill a request. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.5xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors]. + # + class HTTPServerError < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPFatalError # + end + + # Response class for +Continue+ responses (status code 100). + # + # A +Continue+ response indicates that the server has received the request headers. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-100-continue]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#100]. + # + class HTTPContinue < HTTPInformation + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Switching Protocol</tt> responses (status code 101). + # + # The <tt>Switching Protocol<tt> response indicates that the server has received + # a request to switch protocols, and has agreed to do so. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#101]. + # + class HTTPSwitchProtocol < HTTPInformation + # :stopdoc: + HAS_BODY = false + end + + # Response class for +Processing+ responses (status code 102). + # + # The +Processing+ response indicates that the server has received + # and is processing the request, but no response is available yet. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 2518}[https://www.rfc-editor.org/rfc/rfc2518#section-10.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#102]. + # + class HTTPProcessing < HTTPInformation + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Early Hints</tt> responses (status code 103). + # + # The <tt>Early Hints</tt> indicates that the server has received + # and is processing the request, and contains certain headers; + # the final response is not available yet. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103]. + # - {RFC 8297}[https://www.rfc-editor.org/rfc/rfc8297.html#section-2]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#103]. + # + class HTTPEarlyHints < HTTPInformation + # :stopdoc: + HAS_BODY = false + end + + # Response class for +OK+ responses (status code 200). + # + # The +OK+ response indicates that the server has received + # a request and has responded successfully. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-200-ok]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#200]. + # + class HTTPOK < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for +Created+ responses (status code 201). + # + # The +Created+ response indicates that the server has received + # and has fulfilled a request to create a new resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-201-created]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#201]. + # + class HTTPCreated < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for +Accepted+ responses (status code 202). + # + # The +Accepted+ response indicates that the server has received + # and is processing a request, but the processing has not yet been completed. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-202-accepted]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#202]. + # + class HTTPAccepted < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Non-Authoritative Information</tt> responses (status code 203). + # + # The <tt>Non-Authoritative Information</tt> response indicates that the server + # is a transforming proxy (such as a Web accelerator) + # that received a 200 OK response from its origin, + # and is returning a modified version of the origin's response. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/203]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-203-non-authoritative-infor]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#203]. + # + class HTTPNonAuthoritativeInformation < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>No Content</tt> responses (status code 204). + # + # The <tt>No Content</tt> response indicates that the server + # successfully processed the request, and is not returning any content. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-204-no-content]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#204]. + # + class HTTPNoContent < HTTPSuccess + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Reset Content</tt> responses (status code 205). + # + # The <tt>Reset Content</tt> response indicates that the server + # successfully processed the request, + # asks that the client reset its document view, and is not returning any content. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/205]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-205-reset-content]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#205]. + # + class HTTPResetContent < HTTPSuccess + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Partial Content</tt> responses (status code 206). + # + # The <tt>Partial Content</tt> response indicates that the server is delivering + # only part of the resource (byte serving) + # due to a Range header in the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-206-partial-content]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#206]. + # + class HTTPPartialContent < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Multi-Status (WebDAV)</tt> responses (status code 207). + # + # The <tt>Multi-Status (WebDAV)</tt> response indicates that the server + # has received the request, + # and that the message body can contain a number of separate response codes. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 4818}[https://www.rfc-editor.org/rfc/rfc4918#section-11.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#207]. + # + class HTTPMultiStatus < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Already Reported (WebDAV)</tt> responses (status code 208). + # + # The <tt>Already Reported (WebDAV)</tt> response indicates that the server + # has received the request, + # and that the members of a DAV binding have already been enumerated + # in a preceding part of the (multi-status) response, + # and are not being included again. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 5842}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#208]. + # + class HTTPAlreadyReported < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>IM Used</tt> responses (status code 226). + # + # The <tt>IM Used</tt> response indicates that the server has fulfilled a request + # for the resource, and the response is a representation of the result + # of one or more instance-manipulations applied to the current instance. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 3229}[https://www.rfc-editor.org/rfc/rfc3229.html#section-10.4.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#226]. + # + class HTTPIMUsed < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Multiple Choices</tt> responses (status code 300). + # + # The <tt>Multiple Choices</tt> response indicates that the server + # offers multiple options for the resource from which the client may choose. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/300]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-300-multiple-choices]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#300]. + # + class HTTPMultipleChoices < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + HTTPMultipleChoice = HTTPMultipleChoices + + # Response class for <tt>Moved Permanently</tt> responses (status code 301). + # + # The <tt>Moved Permanently</tt> response indicates that links or records + # returning this response should be updated to use the given URL. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-301-moved-permanently]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#301]. + # + class HTTPMovedPermanently < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Found</tt> responses (status code 302). + # + # The <tt>Found</tt> response indicates that the client + # should look at (browse to) another URL. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-302-found]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#302]. + # + class HTTPFound < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + HTTPMovedTemporarily = HTTPFound + + # Response class for <tt>See Other</tt> responses (status code 303). + # + # The response to the request can be found under another URI using the GET method. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-303-see-other]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#303]. + # + class HTTPSeeOther < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Not Modified</tt> responses (status code 304). + # + # Indicates that the resource has not been modified since the version + # specified by the request headers. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#304]. + # + class HTTPNotModified < HTTPRedirection + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Use Proxy</tt> responses (status code 305). + # + # The requested resource is available only through a proxy, + # whose address is provided in the response. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-305-use-proxy]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#305]. + # + class HTTPUseProxy < HTTPRedirection + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Temporary Redirect</tt> responses (status code 307). + # + # The request should be repeated with another URI; + # however, future requests should still use the original URI. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-307-temporary-redirect]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#307]. + # + class HTTPTemporaryRedirect < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Permanent Redirect</tt> responses (status code 308). + # + # This and all future requests should be directed to the given URI. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-308-permanent-redirect]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#308]. + # + class HTTPPermanentRedirect < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Bad Request</tt> responses (status code 400). + # + # The server cannot or will not process the request due to an apparent client error. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-400-bad-request]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#400]. + # + class HTTPBadRequest < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Unauthorized</tt> responses (status code 401). + # + # Authentication is required, but either was not provided or failed. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-401-unauthorized]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#401]. + # + class HTTPUnauthorized < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Payment Required</tt> responses (status code 402). + # + # Reserved for future use. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-402-payment-required]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#402]. + # + class HTTPPaymentRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Forbidden</tt> responses (status code 403). + # + # The request contained valid data and was understood by the server, + # but the server is refusing action. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-403-forbidden]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#403]. + # + class HTTPForbidden < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Not Found</tt> responses (status code 404). + # + # The requested resource could not be found but may be available in the future. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-404-not-found]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#404]. + # + class HTTPNotFound < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Method Not Allowed</tt> responses (status code 405). + # + # The request method is not supported for the requested resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-405-method-not-allowed]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#405]. + # + class HTTPMethodNotAllowed < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Not Acceptable</tt> responses (status code 406). + # + # The requested resource is capable of generating only content + # that not acceptable according to the Accept headers sent in the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-406-not-acceptable]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#406]. + # + class HTTPNotAcceptable < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Proxy Authentication Required</tt> responses (status code 407). + # + # The client must first authenticate itself with the proxy. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-407-proxy-authentication-re]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#407]. + # + class HTTPProxyAuthenticationRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Request Timeout</tt> responses (status code 408). + # + # The server timed out waiting for the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-408-request-timeout]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#408]. + # + class HTTPRequestTimeout < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + HTTPRequestTimeOut = HTTPRequestTimeout + + # Response class for <tt>Conflict</tt> responses (status code 409). + # + # The request could not be processed because of conflict in the current state of the resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-409-conflict]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#409]. + # + class HTTPConflict < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Gone</tt> responses (status code 410). + # + # The resource requested was previously in use but is no longer available + # and will not be available again. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-410-gone]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#410]. + # + class HTTPGone < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Length Required</tt> responses (status code 411). + # + # The request did not specify the length of its content, + # which is required by the requested resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-411-length-required]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#411]. + # + class HTTPLengthRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Precondition Failed</tt> responses (status code 412). + # + # The server does not meet one of the preconditions + # specified in the request headers. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-412-precondition-failed]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#412]. + # + class HTTPPreconditionFailed < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Payload Too Large</tt> responses (status code 413). + # + # The request is larger than the server is willing or able to process. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#413]. + # + class HTTPPayloadTooLarge < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + HTTPRequestEntityTooLarge = HTTPPayloadTooLarge + + # Response class for <tt>URI Too Long</tt> responses (status code 414). + # + # The URI provided was too long for the server to process. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-414-uri-too-long]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#414]. + # + class HTTPURITooLong < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + HTTPRequestURITooLong = HTTPURITooLong + HTTPRequestURITooLarge = HTTPRequestURITooLong + + # Response class for <tt>Unsupported Media Type</tt> responses (status code 415). + # + # The request entity has a media type which the server or resource does not support. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-415-unsupported-media-type]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#415]. + # + class HTTPUnsupportedMediaType < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Range Not Satisfiable</tt> responses (status code 416). + # + # The request entity has a media type which the server or resource does not support. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-416-range-not-satisfiable]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#416]. + # + class HTTPRangeNotSatisfiable < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + HTTPRequestedRangeNotSatisfiable = HTTPRangeNotSatisfiable + + # Response class for <tt>Expectation Failed</tt> responses (status code 417). + # + # The server cannot meet the requirements of the Expect request-header field. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-417-expectation-failed]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#417]. + # + class HTTPExpectationFailed < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # 418 I'm a teapot - RFC 2324; a joke RFC + # See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#418. + + # 420 Enhance Your Calm - Twitter + + # Response class for <tt>Misdirected Request</tt> responses (status code 421). + # + # The request was directed at a server that is not able to produce a response. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-421-misdirected-request]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#421]. + # + class HTTPMisdirectedRequest < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Unprocessable Entity</tt> responses (status code 422). + # + # The request was well-formed but had semantic errors. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-422-unprocessable-content]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#422]. + # + class HTTPUnprocessableEntity < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Locked (WebDAV)</tt> responses (status code 423). + # + # The requested resource is locked. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.3]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#423]. + # + class HTTPLocked < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Failed Dependency (WebDAV)</tt> responses (status code 424). + # + # The request failed because it depended on another request and that request failed. + # See {424 Failed Dependency (WebDAV)}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424]. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.4]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424]. + # + class HTTPFailedDependency < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # 425 Too Early + # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#425. + + # Response class for <tt>Upgrade Required</tt> responses (status code 426). + # + # The client should switch to the protocol given in the Upgrade header field. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-426-upgrade-required]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#426]. + # + class HTTPUpgradeRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Precondition Required</tt> responses (status code 428). + # + # The origin server requires the request to be conditional. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428]. + # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-3]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#428]. + # + class HTTPPreconditionRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Too Many Requests</tt> responses (status code 429). + # + # The user has sent too many requests in a given amount of time. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429]. + # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-4]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429]. + # + class HTTPTooManyRequests < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Request Header Fields Too Large</tt> responses (status code 431). + # + # An individual header field is too large, + # or all the header fields collectively, are too large. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431]. + # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-5]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#431]. + # + class HTTPRequestHeaderFieldsTooLarge < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Unavailable For Legal Reasons</tt> responses (status code 451). + # + # A server operator has received a legal demand to deny access to a resource or to a set of resources + # that includes the requested resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451]. + # - {RFC 7725}[https://www.rfc-editor.org/rfc/rfc7725.html#section-3]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#451]. + # + class HTTPUnavailableForLegalReasons < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + # 444 No Response - Nginx + # 449 Retry With - Microsoft + # 450 Blocked by Windows Parental Controls - Microsoft + # 499 Client Closed Request - Nginx + + # Response class for <tt>Internal Server Error</tt> responses (status code 500). + # + # An unexpected condition was encountered and no more specific message is suitable. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-500-internal-server-error]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#500]. + # + class HTTPInternalServerError < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Not Implemented</tt> responses (status code 501). + # + # The server either does not recognize the request method, + # or it lacks the ability to fulfil the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-501-not-implemented]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#501]. + # + class HTTPNotImplemented < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Bad Gateway</tt> responses (status code 502). + # + # The server was acting as a gateway or proxy + # and received an invalid response from the upstream server. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-502-bad-gateway]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#502]. + # + class HTTPBadGateway < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Service Unavailable</tt> responses (status code 503). + # + # The server cannot handle the request + # (because it is overloaded or down for maintenance). + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-503-service-unavailable]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#503]. + # + class HTTPServiceUnavailable < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Gateway Timeout</tt> responses (status code 504). + # + # The server was acting as a gateway or proxy + # and did not receive a timely response from the upstream server. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-504-gateway-timeout]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#504]. + # + class HTTPGatewayTimeout < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + HTTPGatewayTimeOut = HTTPGatewayTimeout + + # Response class for <tt>HTTP Version Not Supported</tt> responses (status code 505). + # + # The server does not support the HTTP version used in the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-505-http-version-not-suppor]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#505]. + # + class HTTPVersionNotSupported < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Variant Also Negotiates</tt> responses (status code 506). + # + # Transparent content negotiation for the request results in a circular reference. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506]. + # - {RFC 2295}[https://www.rfc-editor.org/rfc/rfc2295#section-8.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#506]. + # + class HTTPVariantAlsoNegotiates < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Insufficient Storage (WebDAV)</tt> responses (status code 507). + # + # The server is unable to store the representation needed to complete the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507]. + # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.5]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#507]. + # + class HTTPInsufficientStorage < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Loop Detected (WebDAV)</tt> responses (status code 508). + # + # The server detected an infinite loop while processing the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508]. + # - {RFC 5942}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.2]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#508]. + # + class HTTPLoopDetected < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + # 509 Bandwidth Limit Exceeded - Apache bw/limited extension + + # Response class for <tt>Not Extended</tt> responses (status code 510). + # + # Further extensions to the request are required for the server to fulfill it. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/510]. + # - {RFC 2774}[https://www.rfc-editor.org/rfc/rfc2774.html#section-7]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#510]. + # + class HTTPNotExtended < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Network Authentication Required</tt> responses (status code 511). + # + # The client needs to authenticate to gain network access. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511]. + # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-6]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#511]. + # + class HTTPNetworkAuthenticationRequired < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + +end + +class Net::HTTPResponse + # :stopdoc: + CODE_CLASS_TO_OBJ = { + '1' => Net::HTTPInformation, + '2' => Net::HTTPSuccess, + '3' => Net::HTTPRedirection, + '4' => Net::HTTPClientError, + '5' => Net::HTTPServerError + }.freeze + CODE_TO_OBJ = { + '100' => Net::HTTPContinue, + '101' => Net::HTTPSwitchProtocol, + '102' => Net::HTTPProcessing, + '103' => Net::HTTPEarlyHints, + + '200' => Net::HTTPOK, + '201' => Net::HTTPCreated, + '202' => Net::HTTPAccepted, + '203' => Net::HTTPNonAuthoritativeInformation, + '204' => Net::HTTPNoContent, + '205' => Net::HTTPResetContent, + '206' => Net::HTTPPartialContent, + '207' => Net::HTTPMultiStatus, + '208' => Net::HTTPAlreadyReported, + '226' => Net::HTTPIMUsed, + + '300' => Net::HTTPMultipleChoices, + '301' => Net::HTTPMovedPermanently, + '302' => Net::HTTPFound, + '303' => Net::HTTPSeeOther, + '304' => Net::HTTPNotModified, + '305' => Net::HTTPUseProxy, + '307' => Net::HTTPTemporaryRedirect, + '308' => Net::HTTPPermanentRedirect, + + '400' => Net::HTTPBadRequest, + '401' => Net::HTTPUnauthorized, + '402' => Net::HTTPPaymentRequired, + '403' => Net::HTTPForbidden, + '404' => Net::HTTPNotFound, + '405' => Net::HTTPMethodNotAllowed, + '406' => Net::HTTPNotAcceptable, + '407' => Net::HTTPProxyAuthenticationRequired, + '408' => Net::HTTPRequestTimeout, + '409' => Net::HTTPConflict, + '410' => Net::HTTPGone, + '411' => Net::HTTPLengthRequired, + '412' => Net::HTTPPreconditionFailed, + '413' => Net::HTTPPayloadTooLarge, + '414' => Net::HTTPURITooLong, + '415' => Net::HTTPUnsupportedMediaType, + '416' => Net::HTTPRangeNotSatisfiable, + '417' => Net::HTTPExpectationFailed, + '421' => Net::HTTPMisdirectedRequest, + '422' => Net::HTTPUnprocessableEntity, + '423' => Net::HTTPLocked, + '424' => Net::HTTPFailedDependency, + '426' => Net::HTTPUpgradeRequired, + '428' => Net::HTTPPreconditionRequired, + '429' => Net::HTTPTooManyRequests, + '431' => Net::HTTPRequestHeaderFieldsTooLarge, + '451' => Net::HTTPUnavailableForLegalReasons, + + '500' => Net::HTTPInternalServerError, + '501' => Net::HTTPNotImplemented, + '502' => Net::HTTPBadGateway, + '503' => Net::HTTPServiceUnavailable, + '504' => Net::HTTPGatewayTimeout, + '505' => Net::HTTPVersionNotSupported, + '506' => Net::HTTPVariantAlsoNegotiates, + '507' => Net::HTTPInsufficientStorage, + '508' => Net::HTTPLoopDetected, + '510' => Net::HTTPNotExtended, + '511' => Net::HTTPNetworkAuthenticationRequired, + }.freeze +end diff --git a/lib/net/http/status.rb b/lib/net/http/status.rb new file mode 100644 index 0000000000..e70b47d9fb --- /dev/null +++ b/lib/net/http/status.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require_relative '../http' + +if $0 == __FILE__ + require 'open-uri' + File.foreach(__FILE__) do |line| + puts line + break if line.start_with?('end') + end + puts + puts "Net::HTTP::STATUS_CODES = {" + url = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv" + URI(url).read.each_line do |line| + code, mes, = line.split(',') + next if ['(Unused)', 'Unassigned', 'Description'].include?(mes) + puts " #{code} => '#{mes}'," + end + puts "} # :nodoc:" +end + +Net::HTTP::STATUS_CODES = { + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 103 => 'Early Hints', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Content Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Content', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Too Early', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended (OBSOLETED)', + 511 => 'Network Authentication Required', +} # :nodoc: diff --git a/lib/net/https.rb b/lib/net/https.rb new file mode 100644 index 0000000000..0f23e1fb13 --- /dev/null +++ b/lib/net/https.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +=begin + += net/https -- SSL/TLS enhancement for Net::HTTP. + + This file has been merged with net/http. There is no longer any need to + require 'net/https' to use HTTPS. + + See Net::HTTP for details on how to make HTTPS connections. + +== Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU Yuuzou <gotoyuzo@notwork.org> + All rights reserved. + +== Licence + This program is licensed under the same licence as Ruby. + (See the file 'LICENCE'.) + +=end + +require_relative 'http' +require 'openssl' diff --git a/lib/net/imap.rb b/lib/net/imap.rb deleted file mode 100644 index 34d324ed12..0000000000 --- a/lib/net/imap.rb +++ /dev/null @@ -1,1898 +0,0 @@ -=begin - -= net/imap.rb - -Copyright (C) 2000 Shugo Maeda <shugo@ruby-lang.org> - -This library is distributed under the terms of the Ruby license. -You can freely distribute/modify this library. - -== class Net::IMAP - -Net::IMAP implements Internet Message Access Protocol (IMAP) clients. - -=== Super Class - -Object - -=== Class Methods - -: new(host, port = 143) - Creates a new Net::IMAP object and connects it to the specified - port on the named host. - -: debug - Returns the debug mode. - -: debug = val - Sets the debug mode. - -: add_authenticator(auth_type, authenticator) - Adds an authenticator for Net::IMAP#authenticate. - -=== Methods - -: greeting - Returns an initial greeting response from the server. - -: responses - Returns recorded untagged responses. - - ex). - imap.select("inbox") - p imap.responses["EXISTS"][-1] - #=> 2 - p imap.responses["UIDVALIDITY"][-1] - #=> 968263756 - -: disconnect - Disconnects from the server. - -: capability - Sends a CAPABILITY command, and returns a listing of - capabilities that the server supports. - -: noop - Sends a NOOP command to the server. It does nothing. - -: logout - Sends a LOGOUT command to inform the server that the client is - done with the connection. - -: authenticate(auth_type, arg...) - Sends an AUTEHNTICATE command to authenticate the client. - The auth_type parameter is a string that represents - the authentication mechanism to be used. Currently Net::IMAP - supports "LOGIN" and "CRAM-MD5" for the auth_type. - - ex). - imap.authenticate('LOGIN', user, password) - -: login(user, password) - Sends a LOGIN command to identify the client and carries - the plaintext password authenticating this user. - -: select(mailbox) - Sends a SELECT command to select a mailbox so that messages - in the mailbox can be accessed. - -: examine(mailbox) - Sends a EXAMINE command to select a mailbox so that messages - in the mailbox can be accessed. However, the selected mailbox - is identified as read-only. - -: create(mailbox) - Sends a CREATE command to create a new mailbox. - -: delete(mailbox) - Sends a DELETE command to remove the mailbox. - -: rename(mailbox, newname) - Sends a RENAME command to change the name of the mailbox to - the newname. - -: subscribe(mailbox) - Sends a SUBSCRIBE command to add the specified mailbox name to - the server's set of "active" or "subscribed" mailboxes. - -: unsubscribe(mailbox) - Sends a UNSUBSCRIBE command to remove the specified mailbox name - from the server's set of "active" or "subscribed" mailboxes. - -: list(refname, mailbox) - Sends a LIST command, and returns a subset of names from - the complete set of all names available to the client. - - ex). - imap.create("foo/bar") - imap.create("foo/baz") - p imap.list("", "foo/%") - #=> [#<Net::IMAP::MailboxList attr=[:NoSelect], delim="/", name="foo/">, #<Net::IMAP::MailboxList attr=[:NoInferiors, :Marked], delim="/", name="foo/bar">, #<Net::IMAP::MailboxList attr=[:NoInferiors], delim="/", name="foo/baz">] - -: lsub(refname, mailbox) - Sends a LSUB command, and returns a subset of names from the set - of names that the user has declared as being "active" or - "subscribed". - -: status(mailbox, attr) - Sends a STATUS command, and returns the status of the indicated - mailbox. - - ex). - p imap.status("inbox", ["MESSAGES", "RECENT"]) - #=> {"RECENT"=>0, "MESSAGES"=>44} - -: append(mailbox, message, flags = nil, date_time = nil) - Sends a APPEND command to append the message to the end of - the mailbox. - - ex). - imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now) - Subject: hello - From: shugo@ruby-lang.org - To: shugo@ruby-lang.org - - hello world - EOF - -: check - Sends a CHECK command to request a checkpoint of the currently - selected mailbox. - -: close - Sends a CLOSE command to close the currently selected mailbox. - The CLOSE command permanently removes from the mailbox all - messages that have the \Deleted flag set. - -: expunge - Sends a EXPUNGE command to permanently remove from the currently - selected mailbox all messages that have the \Deleted flag set. - -: search(keys, charset = nil) -: uid_search(keys, charset = nil) - Sends a SEARCH command to search the mailbox for messages that - match the given searching criteria, and returns message sequence - numbers (search) or unique identifiers (uid_search). - - ex). - p imap.search(["SUBJECT", "hello"]) - #=> [1, 6, 7, 8] - p imap.search('SUBJECT "hello"') - #=> [1, 6, 7, 8] - -: fetch(set, attr) -: uid_fetch(set, attr) - Sends a FETCH command to retrieve data associated with a message - in the mailbox. the set parameter is a number or an array of - numbers or a Range object. the number is a message sequence - number (fetch) or a unique identifier (uid_fetch). - - ex). - p imap.fetch(6..8, "UID") - #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>] - p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]") - #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>] - data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0] - p data.seqno - #=> 6 - p data.attr["RFC822.SIZE"] - #=> 611 - p data.attr["INTERNALDATE"] - #=> "12-Oct-2000 22:40:59 +0900" - p data.attr["UID"] - #=> 98 - -: store(set, attr, flags) -: uid_store(set, attr, flags) - Sends a STORE command to alter data associated with a message - in the mailbox. the set parameter is a number or an array of - numbers or a Range object. the number is a message sequence - number (store) or a unique identifier (uid_store). - - ex). - p imap.store(6..8, "+FLAGS", [:Deleted]) - #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>] - -: copy(set, mailbox) -: uid_copy(set, mailbox) - Sends a COPY command to copy the specified message(s) to the end - of the specified destination mailbox. the set parameter is - a number or an array of numbers or a Range object. the number is - a message sequence number (copy) or a unique identifier (uid_copy). - -: sort(sort_keys, search_keys, charset) -: uid_sort(sort_keys, search_keys, charset) - Sends a SORT command to sort messages in the mailbox. - - ex). - p imap.sort(["FROM"], ["ALL"], "US-ASCII") - #=> [1, 2, 3, 5, 6, 7, 8, 4, 9] - p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII") - #=> [6, 7, 8, 1] - -=end - -require "socket" -require "md5" - -module Net - class IMAP - attr_reader :greeting, :responses - - def self.debug - return @@debug - end - - def self.debug=(val) - return @@debug = val - end - - def self.add_authenticator(auth_type, authenticator) - @@authenticators[auth_type] = authenticator - end - - def disconnect - @sock.close - end - - def capability - send_command("CAPABILITY") - return @responses.delete("CAPABILITY")[-1] - end - - def noop - send_command("NOOP") - end - - def logout - send_command("LOGOUT") - end - - def authenticate(auth_type, *args) - auth_type = auth_type.upcase - unless @@authenticators.has_key?(auth_type) - raise ArgumentError, - format('unknown auth type - "%s"', auth_type) - end - authenticator = @@authenticators[auth_type].new(*args) - send_command("AUTHENTICATE", auth_type) do |resp| - if resp.instance_of?(ContinueRequest) - data = authenticator.process(resp.data.text.unpack("m")[0]) - send_data([data].pack("m").chomp) - end - end - end - - def login(user, password) - send_command("LOGIN", user, password) - end - - def select(mailbox) - @responses.clear - send_command("SELECT", mailbox) - end - - def examine(mailbox) - @responses.clear - send_command("EXAMINE", mailbox) - end - - def create(mailbox) - send_command("CREATE", mailbox) - end - - def delete(mailbox) - send_command("DELETE", mailbox) - end - - def rename(mailbox, newname) - send_command("RENAME", mailbox, newname) - end - - def subscribe(mailbox) - send_command("SUBSCRIBE", mailbox) - end - - def unsubscribe(mailbox) - send_command("UNSUBSCRIBE", mailbox) - end - - def list(refname, mailbox) - send_command("LIST", refname, mailbox) - return @responses.delete("LIST") - end - - def lsub(refname, mailbox) - send_command("LSUB", refname, mailbox) - return @responses.delete("LSUB") - end - - def status(mailbox, attr) - send_command("STATUS", mailbox, attr) - return @responses.delete("STATUS")[-1][1] - end - - def append(mailbox, message, flags = nil, date_time = nil) - args = [] - if flags - args.push(flags) - end - args.push(date_time) if date_time - args.push(Literal.new(message)) - send_command("APPEND", mailbox, *args) - end - - def check - send_command("CHECK") - end - - def close - send_command("CLOSE") - end - - def expunge - send_command("EXPUNGE") - return @responses.delete("EXPUNGE") - end - - def search(keys, charset = nil) - return search_internal("SEARCH", keys, charset) - end - - def uid_search(keys, charset = nil) - return search_internal("UID SEARCH", keys, charset) - end - - def fetch(set, attr) - return fetch_internal("FETCH", set, attr) - end - - def uid_fetch(set, attr) - return fetch_internal("UID FETCH", set, attr) - end - - def store(set, attr, flags) - return store_internal("STORE", set, attr, flags) - end - - def uid_store(set, attr, flags) - return store_internal("UID STORE", set, attr, flags) - end - - def copy(set, mailbox) - copy_internal("COPY", set, mailbox) - end - - def uid_copy(set, mailbox) - copy_internal("UID COPY", set, mailbox) - end - - def sort(sort_keys, search_keys, charset) - return sort_internal("SORT", sort_keys, search_keys, charset) - end - - def uid_sort(sort_keys, search_keys, charset) - return sort_internal("UID SORT", sort_keys, search_keys, charset) - end - - private - - CRLF = "\r\n" - PORT = 143 - - @@debug = false - @@authenticators = {} - - def initialize(host, port = PORT) - @host = host - @port = port - @tag_prefix = "RUBY" - @tagno = 0 - @parser = ResponseParser.new - @sock = TCPSocket.open(host, port) - @responses = Hash.new([].freeze) - @greeting = get_response - if /\ABYE\z/ni =~ @greeting.name - @sock.close - raise ByeResponseError, resp[0] - end - end - - def send_command(cmd, *args, &block) - tag = generate_tag - data = args.collect {|i| format_data(i)}.join(" ") - if data.length > 0 - put_line(tag + " " + cmd + " " + data) - else - put_line(tag + " " + cmd) - end - return get_all_responses(tag, cmd, &block) - end - - def generate_tag - @tagno += 1 - return format("%s%04d", @tag_prefix, @tagno) - end - - def send_data(*args) - data = args.collect {|i| format_data(i)}.join(" ") - put_line(data) - end - - def put_line(line) - line = line + CRLF - @sock.print(line) - if @@debug - $stderr.print(line.gsub(/^/n, "C: ")) - end - end - - def get_all_responses(tag, cmd, &block) - while resp = get_response - if @@debug - $stderr.printf("R: %s\n", resp.inspect) - end - case resp - when TaggedResponse - case resp.name - when /\A(?:NO)\z/ni - raise NoResponseError, resp.data.text - when /\A(?:BAD)\z/ni - raise BadResponseError, resp.data.text - else - return resp - end - when UntaggedResponse - if /\ABYE\z/ni =~ resp.name && - cmd != "LOGOUT" - raise ByeResponseError, resp.data.text - end - record_response(resp.name, resp.data) - if resp.data.instance_of?(ResponseText) && - (code = resp.data.code) - record_response(code.name, code.data) - end - end - block.call(resp) if block - end - end - - def get_response - buff = "" - while true - s = @sock.gets(CRLF) - break unless s - buff.concat(s) - if /\{(\d+)\}\r\n/n =~ s - s = @sock.read($1.to_i) - buff.concat(s) - else - break - end - end - return nil if buff.length == 0 - if @@debug - $stderr.print(buff.gsub(/^/n, "S: ")) - end - return @parser.parse(buff) - end - - def record_response(name, data) - unless @responses.has_key?(name) - @responses[name] = [] - end - @responses[name].push(data) - end - - def format_data(data) - case data - when nil - return "NIL" - when String - return format_string(data) - when Integer - return format_number(data) - when Array - return format_list(data) - when Time - return format_time(data) - when Symbol - return format_symbol(data) - else - return data.format_data - end - end - - def format_string(str) - case str - when "" - return '""' - when /[\x80-\xff\r\n]/n - # literal - return "{" + str.length.to_s + "}" + CRLF + str - when /[(){ \x00-\x1f\x7f%*"\\]/n - # quoted string - return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"' - else - # atom - return str - end - end - - def format_number(num) - if num < 0 || num >= 4294967296 - raise DataFormatError, num.to_s - end - return num.to_s - end - - def format_list(list) - contents = list.collect {|i| format_data(i)}.join(" ") - return "(" + contents + ")" - end - - DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) - - def format_time(time) - t = time.dup.gmtime - return format('"%2d-%3s-%4d %02d:%02d:%02d +0000"', - t.day, DATE_MONTH[t.month - 1], t.year, - t.hour, t.min, t.sec) - end - - def format_symbol(symbol) - return "\\" + symbol.to_s - end - - def search_internal(cmd, keys, charset) - if keys.instance_of?(String) - keys = [RawData.new(keys)] - else - normalize_searching_criteria(keys) - end - if charset - send_command(cmd, "CHARSET", charset, *keys) - else - send_command(cmd, *keys) - end - return @responses.delete("SEARCH")[-1] - end - - def fetch_internal(cmd, set, attr) - if attr.instance_of?(String) - attr = RawData.new(attr) - end - @responses.delete("FETCH") - send_command(cmd, MessageSet.new(set), attr) - return @responses.delete("FETCH") - end - - def store_internal(cmd, set, attr, flags) - if attr.instance_of?(String) - attr = RawData.new(attr) - end - @responses.delete("FETCH") - send_command(cmd, MessageSet.new(set), attr, flags) - return @responses.delete("FETCH") - end - - def copy_internal(cmd, set, mailbox) - send_command(cmd, MessageSet.new(set), mailbox) - end - - def sort_internal(cmd, sort_keys, search_keys, charset) - if search_keys.instance_of?(String) - search_keys = [RawData.new(search_keys)] - else - normalize_searching_criteria(search_keys) - end - normalize_searching_criteria(search_keys) - send_command(cmd, sort_keys, charset, *search_keys) - return @responses.delete("SORT")[-1] - end - - def normalize_searching_criteria(keys) - keys.collect! do |i| - case i - when -1, Range, Array - MessageSet.new(i) - else - i - end - end - end - - class RawData - def format_data - return @data - end - - private - - def initialize(data) - @data = data - end - end - - class Atom - def format_data - return @data - end - - private - - def initialize(data) - @data = data - end - end - - class QuotedString - def format_data - return '"' + @data.gsub(/["\\]/n, "\\\\\\&") + '"' - end - - private - - def initialize(data) - @data = data - end - end - - class Literal - def format_data - return "{" + @data.length.to_s + "}" + CRLF + @data - end - - private - - def initialize(data) - @data = data - end - end - - class MessageSet - def format_data - return format_internal(@data) - end - - private - - def initialize(data) - @data = data - end - - def format_internal(data) - case data - when "*" - return data - when Integer - ensure_nz_number(data) - if data == -1 - return "*" - else - return data.to_s - end - when Range - return format_internal(data.first) + - ":" + format_internal(data.last) - when Array - return data.collect {|i| format_internal(i)}.join(",") - else - raise DataFormatError, data.inspect - end - end - - def ensure_nz_number(num) - if num < -1 || num == 0 || num >= 4294967296 - raise DataFormatError, num.inspect - end - end - end - - ContinueRequest = Struct.new(:data, :raw_data) - UntaggedResponse = Struct.new(:name, :data, :raw_data) - TaggedResponse = Struct.new(:tag, :name, :data, :raw_data) - ResponseText = Struct.new(:code, :text) - ResponseCode = Struct.new(:name, :data) - MailboxList = Struct.new(:attr, :delim, :name) - StatusData = Struct.new(:mailbox, :attr) - FetchData = Struct.new(:seqno, :attr) - Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to, - :to, :cc, :bcc, :in_reply_to, :message_id) - Address = Struct.new(:name, :route, :mailbox, :host) - ContentDisposition = Struct.new(:dsp_type, :param) - - class BodyTypeBasic < Struct.new(:media_type, :media_subtype, - :param, :content_id, - :description, :encoding, :size, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - end - - class BodyTypeText < Struct.new(:media_type, :media_subtype, - :param, :content_id, - :description, :encoding, :size, - :lines, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - end - - class BodyTypeMessage < Struct.new(:media_type, :media_subtype, - :param, :content_id, - :description, :encoding, :size, - :envelope, :body, :lines, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - end - - class BodyTypeMultipart < Struct.new(:media_type, :media_subtype, - :parts, - :param, :disposition, :language, - :extension) - def multipart? - return true - end - end - - class ResponseParser - def parse(str) - @str = str - @pos = 0 - @lex_state = EXPR_BEG - @token = nil - return response - end - - private - - EXPR_BEG = :EXPR_BEG - EXPR_DATA = :EXPR_DATA - EXPR_TEXT = :EXPR_TEXT - EXPR_RTEXT = :EXPR_RTEXT - EXPR_CTEXT = :EXPR_CTEXT - - T_SPACE = :SPACE - T_NIL = :NIL - T_NUMBER = :NUMBER - T_ATOM = :ATOM - T_QUOTED = :QUOTED - T_LPAR = :LPAR - T_RPAR = :RPAR - T_BSLASH = :BSLASH - T_STAR = :STAR - T_LBRA = :LBRA - T_RBRA = :RBRA - T_LITERAL = :LITERAL - T_PLUS = :PLUS - T_PERCENT = :PERCENT - T_CRLF = :CRLF - T_EOF = :EOF - T_TEXT = :TEXT - - BEG_REGEXP = /\G(?:\ -(?# 1: SPACE )( )|\ -(?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\ -(?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\ -(?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\ -(?# 5: QUOTED )"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)"|\ -(?# 6: LPAR )(\()|\ -(?# 7: RPAR )(\))|\ -(?# 8: BSLASH )(\\)|\ -(?# 9: STAR )(\*)|\ -(?# 10: LBRA )(\[)|\ -(?# 11: RBRA )(\])|\ -(?# 12: LITERAL )\{(\d+)\}\r\n|\ -(?# 13: PLUS )(\+)|\ -(?# 14: PERCENT )(%)|\ -(?# 15: CRLF )(\r\n)|\ -(?# 16: EOF )(\z))/ni - - DATA_REGEXP = /\G(?:\ -(?# 1: SPACE )( )|\ -(?# 2: NIL )(NIL)|\ -(?# 3: NUMBER )(\d+)|\ -(?# 4: QUOTED )"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)"|\ -(?# 5: LITERAL )\{(\d+)\}\r\n|\ -(?# 6: LPAR )(\()|\ -(?# 7: RPAR )(\)))/ni - - TEXT_REGEXP = /\G(?:\ -(?# 1: TEXT )([^\x00\x80-\xff\r\n]*))/ni - - RTEXT_REGEXP = /\G(?:\ -(?# 1: LBRA )(\[)|\ -(?# 2: TEXT )([^\x00\x80-\xff\r\n]*))/ni - - CTEXT_REGEXP = /\G(?:\ -(?# 1: TEXT )([^\x00\x80-\xff\r\n\]]*))/ni - - Token = Struct.new(:symbol, :value) - - def response - token = lookahead - case token.symbol - when T_PLUS - result = continue_req - when T_STAR - result = response_untagged - else - result = response_tagged - end - match(T_CRLF) - match(T_EOF) - return result - end - - def continue_req - match(T_PLUS) - match(T_SPACE) - return ContinueRequest.new(resp_text, @str) - end - - def response_untagged - match(T_STAR) - match(T_SPACE) - token = lookahead - if token.symbol == T_NUMBER - return numeric_response - elsif token.symbol == T_ATOM - case token.value - when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni - return response_cond - when /\A(?:FLAGS)\z/ni - return flags_response - when /\A(?:LIST|LSUB)\z/ni - return list_response - when /\A(?:SEARCH|SORT)\z/ni - return search_response - when /\A(?:STATUS)\z/ni - return status_response - when /\A(?:CAPABILITY)\z/ni - return capability_response - else - return text_response - end - else - parse_error("unexpected token %s", token.symbol) - end - end - - def response_tagged - tag = atom - match(T_SPACE) - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return TaggedResponse.new(tag, name, resp_text, @str) - end - - def response_cond - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return UntaggedResponse.new(name, resp_text, @str) - end - - def numeric_response - n = number - match(T_SPACE) - token = match(T_ATOM) - name = token.value.upcase - case name - when "EXISTS", "RECENT", "EXPUNGE" - return UntaggedResponse.new(name, n, @str) - when "FETCH" - shift_token - match(T_SPACE) - data = FetchData.new(n, msg_att) - return UntaggedResponse.new(name, data, @str) - end - end - - def msg_att - match(T_LPAR) - attr = {} - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - break - when T_SPACE - shift_token - token = lookahead - end - case token.value - when /\A(?:ENVELOPE)\z/ni - name, val = envelope_data - when /\A(?:FLAGS)\z/ni - name, val = flags_data - when /\A(?:INTERNALDATE)\z/ni - name, val = internaldate_data - when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni - name, val = rfc822_text - when /\A(?:RFC822\.SIZE)\z/ni - name, val = rfc822_size - when /\A(?:BODY(?:STRUCTURE)?)\z/ni - name, val = body_data - when /\A(?:UID)\z/ni - name, val = uid_data - else - parse_error("unknown attribute `%s'", token.value) - end - attr[name] = val - end - return attr - end - - def envelope_data - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, envelope - end - - def envelope - @lex_state = EXPR_DATA - match(T_LPAR) - date = nstring - match(T_SPACE) - subject = nstring - match(T_SPACE) - from = address_list - match(T_SPACE) - sender = address_list - match(T_SPACE) - reply_to = address_list - match(T_SPACE) - to = address_list - match(T_SPACE) - cc = address_list - match(T_SPACE) - bcc = address_list - match(T_SPACE) - in_reply_to = nstring - match(T_SPACE) - message_id = nstring - match(T_RPAR) - @lex_state = EXPR_BEG - return Envelope.new(date, subject, from, sender, reply_to, - to, cc, bcc, in_reply_to, message_id) - end - - def flags_data - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, flag_list - end - - def internaldate_data - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - token = match(T_QUOTED) - return name, token.value - end - - def rfc822_text - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, nstring - end - - def rfc822_size - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, number - end - - def body_data - token = match(T_ATOM) - name = token.value.upcase - token = lookahead - if token.symbol == T_SPACE - shift_token - return name, body - end - name.concat(section) - token = lookahead - if token.symbol == T_ATOM - name.concat(token.value) - shift_token - end - match(T_SPACE) - data = nstring - return name, data - end - - def body - @lex_state = EXPR_DATA - match(T_LPAR) - token = lookahead - if token.symbol == T_LPAR - result = body_type_mpart - else - result = body_type_1part - end - match(T_RPAR) - @lex_state = EXPR_BEG - return result - end - - def body_type_1part - token = lookahead - case token.value - when /\A(?:TEXT)\z/ni - return body_type_text - when /\A(?:MESSAGE)\z/ni - return body_type_msg - else - return body_type_basic - end - end - - def body_type_basic - mtype, msubtype = media_type - match(T_SPACE) - param, content_id, desc, enc, size = body_fields - md5, disposition, language, extension = body_ext_1part - return BodyTypeBasic.new(mtype, msubtype, - param, content_id, - desc, enc, size, - md5, disposition, language, extension) - end - - def body_type_text - mtype, msubtype = media_type - match(T_SPACE) - param, content_id, desc, enc, size = body_fields - match(T_SPACE) - lines = number - md5, disposition, language, extension = body_ext_1part - return BodyTypeText.new(mtype, msubtype, - param, content_id, - desc, enc, size, - lines, - md5, disposition, language, extension) - end - - def body_type_msg - mtype, msubtype = media_type - match(T_SPACE) - param, content_id, desc, enc, size = body_fields - match(T_SPACE) - env = envelope - match(T_SPACE) - b = body - match(T_SPACE) - lines = number - md5, disposition, language, extension = body_ext_1part - return BodyTypeMessage.new(mtype, msubtype, - param, content_id, - desc, enc, size, - env, b, lines, - md5, disposition, language, extension) - end - - def body_type_mpart - parts = [] - while true - token = lookahead - if token.symbol == T_SPACE - shift_token - break - end - parts.push(body) - end - mtype = "MULTIPART" - msubtype = string.upcase - param, disposition, language, extension = body_ext_mpart - return BodyTypeMultipart.new(mtype, msubtype, parts, - param, disposition, language, - extension) - end - - def media_type - mtype = string.upcase - match(T_SPACE) - msubtype = string.upcase - return mtype, msubtype - end - - def body_fields - param = body_fld_param - match(T_SPACE) - content_id = nstring - match(T_SPACE) - desc = nstring - match(T_SPACE) - enc = string.upcase - match(T_SPACE) - size = number - return param, content_id, desc, enc, size - end - - def body_fld_param - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - match(T_LPAR) - param = {} - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - break - when T_SPACE - shift_token - end - name = string.upcase - match(T_SPACE) - val = string - param[name] = val - end - return param - end - - def body_ext_1part - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return nil - end - md5 = nstring - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return md5 - end - disposition = body_fld_dsp - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return md5, disposition - end - language = body_fld_lang - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return md5, disposition, language - end - - extension = body_extensions - return md5, disposition, language, extension - end - - def body_ext_mpart - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return nil - end - param = body_fld_param - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return param - end - disposition = body_fld_dsp - match(T_SPACE) - language = body_fld_lang - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return param, disposition, language - end - - extension = body_extensions - return param, disposition, language, extension - end - - def body_fld_dsp - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - match(T_LPAR) - dsp_type = string.upcase - match(T_SPACE) - param = body_fld_param - match(T_RPAR) - return ContentDisposition.new(dsp_type, param) - end - - def body_fld_lang - token = lookahead - if token.symbol == T_LPAR - shift_token - result = [] - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - return result - when T_SPACE - shift_token - end - result.push(string.upcase) - end - else - lang = nstring - if lang - return lang.upcase - else - return lang - end - end - end - - def body_extensions - result = [] - while true - token = lookahead - case token.symbol - when T_RPAR - return result - when T_SPACE - shift_token - end - result.push(body_extension) - end - end - - def body_extension - token = lookahead - case token.symbol - when T_LPAR - shift_token - result = body_extensions - match(T_RPAR) - return result - when T_NUMBER - return number - else - return nstring - end - end - - def section - str = "" - token = match(T_LBRA) - str.concat(token.value) - token = match(T_ATOM, T_NUMBER, T_RBRA) - if token.symbol == T_RBRA - str.concat(token.value) - return str - end - str.concat(token.value) - token = lookahead - if token.symbol == T_SPACE - shift_token - str.concat(token.value) - token = match(T_LPAR) - str.concat(token.value) - while true - token = lookahead - case token.symbol - when T_RPAR - str.concat(token.value) - shift_token - break - when T_SPACE - shift_token - str.concat(token.value) - end - str.concat(format_string(astring)) - end - end - token = match(T_RBRA) - str.concat(token.value) - return str - end - - def format_string(str) - case str - when "" - return '""' - when /[\x80-\xff\r\n]/n - # literal - return "{" + str.length.to_s + "}" + CRLF + str - when /[(){ \x00-\x1f\x7f%*"\\]/n - # quoted string - return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"' - else - # atom - return str - end - end - - def uid_data - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, number - end - - def text_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - @lex_state = EXPR_TEXT - token = match(T_TEXT) - @lex_state = EXPR_BEG - return UntaggedResponse.new(name, token.value) - end - - def flags_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return UntaggedResponse.new(name, flag_list, @str) - end - - def list_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return UntaggedResponse.new(name, mailbox_list, @str) - end - - def mailbox_list - attr = flag_list - match(T_SPACE) - token = match(T_QUOTED, T_NIL) - if token.symbol == T_NIL - delim = nil - else - delim = token.value - end - match(T_SPACE) - name = astring - return MailboxList.new(attr, delim, name) - end - - def search_response - token = match(T_ATOM) - name = token.value.upcase - token = lookahead - if token.symbol == T_SPACE - shift_token - data = [] - while true - token = lookahead - case token.symbol - when T_CRLF - break - when T_SPACE - shift_token - end - data.push(number) - end - else - data = [] - end - return UntaggedResponse.new(name, data, @str) - end - - def status_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - mailbox = astring - match(T_SPACE) - match(T_LPAR) - attr = {} - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - break - when T_SPACE - shift_token - end - token = match(T_ATOM) - key = token.value.upcase - match(T_SPACE) - val = number - attr[key] = val - end - data = StatusData.new(mailbox, attr) - return UntaggedResponse.new(name, data, @str) - end - - def capability_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - data = [] - while true - token = lookahead - case token.symbol - when T_CRLF - break - when T_SPACE - shift_token - end - data.push(atom.upcase) - end - return UntaggedResponse.new(name, data, @str) - end - - def resp_text - @lex_state = EXPR_RTEXT - token = lookahead - if token.symbol == T_LBRA - code = resp_text_code - else - code = nil - end - token = match(T_TEXT) - @lex_state = EXPR_BEG - return ResponseText.new(code, token.value) - end - - def resp_text_code - @lex_state = EXPR_BEG - match(T_LBRA) - token = match(T_ATOM) - name = token.value.upcase - case name - when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE)\z/n - result = ResponseCode.new(name, nil) - when /\A(?:PERMANENTFLAGS)\z/n - match(T_SPACE) - result = ResponseCode.new(name, flag_list) - when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n - match(T_SPACE) - result = ResponseCode.new(name, number) - end - match(T_RBRA) - @lex_state = EXPR_RTEXT - return result - end - - def address_list - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - else - result = [] - match(T_LPAR) - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - break - when T_SPACE - shift_token - end - result.push(address) - end - return result - end - end - - ADDRESS_REGEXP = /\G\ -(?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ -(?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ -(?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ -(?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\ -\)/ni - - def address - match(T_LPAR) - if @str.index(ADDRESS_REGEXP, @pos) - # address does not include literal. - @pos = $~.end(0) - name = $1 - route = $2 - mailbox = $3 - host = $4 - for s in [name, route, mailbox, host] - if s - s.gsub!(/\\(["\\])/n, "\\1") - end - end - else - name = nstring - match(T_SPACE) - route = nstring - match(T_SPACE) - mailbox = nstring - match(T_SPACE) - host = nstring - match(T_RPAR) - end - return Address.new(name, route, mailbox, host) - end - -# def flag_list -# result = [] -# match(T_LPAR) -# while true -# token = lookahead -# case token.symbol -# when T_RPAR -# shift_token -# break -# when T_SPACE -# shift_token -# end -# result.push(flag) -# end -# return result -# end - -# def flag -# token = lookahead -# if token.symbol == T_BSLASH -# shift_token -# token = lookahead -# if token.symbol == T_STAR -# shift_token -# return token.value.intern -# else -# return atom.intern -# end -# else -# return atom -# end -# end - - FLAG_REGEXP = /\ -(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\ -(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n - - def flag_list - if @str.index(/\(([^)]*)\)/ni, @pos) - @pos = $~.end(0) - return $1.scan(FLAG_REGEXP).collect { |flag, atom| - atom || flag.intern - } - else - parse_error("invalid flag list") - end - end - - def nstring - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - else - return string - end - end - - def astring - token = lookahead - if string_token?(token) - return string - else - return atom - end - end - - def string - token = match(T_QUOTED, T_LITERAL) - return token.value - end - - STRING_TOKENS = [T_QUOTED, T_LITERAL] - - def string_token?(token) - return STRING_TOKENS.include?(token.symbol) - end - - def atom - result = "" - while true - token = lookahead - if atom_token?(token) - result.concat(token.value) - shift_token - else - if result.empty? - parse_error("unexpected token %s", token.symbol) - else - return result - end - end - end - end - - ATOM_TOKENS = [ - T_ATOM, - T_NUMBER, - T_NIL, - T_LBRA, - T_RBRA, - T_PLUS - ] - - def atom_token?(token) - return ATOM_TOKENS.include?(token.symbol) - end - - def number - token = match(T_NUMBER) - return token.value.to_i - end - - def nil_atom - match(T_NIL) - return nil - end - - def match(*args) - token = lookahead - unless args.include?(token.symbol) - parse_error('unexpected token %s (expected %s)', - token.symbol.id2name, - args.collect {|i| i.id2name}.join(" or ")) - end - shift_token - return token - end - - def lookahead - unless @token - @token = next_token - end - return @token - end - - def shift_token - @token = nil - end - - def next_token - case @lex_state - when EXPR_BEG - if @str.index(BEG_REGEXP, @pos) - @pos = $~.end(0) - if $1 - return Token.new(T_SPACE, $+) - elsif $2 - return Token.new(T_NIL, $+) - elsif $3 - return Token.new(T_NUMBER, $+) - elsif $4 - return Token.new(T_ATOM, $+) - elsif $5 - return Token.new(T_QUOTED, - $+.gsub(/\\(["\\])/n, "\\1")) - elsif $6 - return Token.new(T_LPAR, $+) - elsif $7 - return Token.new(T_RPAR, $+) - elsif $8 - return Token.new(T_BSLASH, $+) - elsif $9 - return Token.new(T_STAR, $+) - elsif $10 - return Token.new(T_LBRA, $+) - elsif $11 - return Token.new(T_RBRA, $+) - elsif $12 - len = $+.to_i - val = @str[@pos, len] - @pos += len - return Token.new(T_LITERAL, val) - elsif $13 - return Token.new(T_PLUS, $+) - elsif $14 - return Token.new(T_PERCENT, $+) - elsif $15 - return Token.new(T_CRLF, $+) - elsif $16 - return Token.new(T_EOF, $+) - else - parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid") - end - else - @str.index(/\S*/n, @pos) - parse_error("unknown token - %s", $&.dump) - end - when EXPR_DATA - if @str.index(DATA_REGEXP, @pos) - @pos = $~.end(0) - if $1 - return Token.new(T_SPACE, $+) - elsif $2 - return Token.new(T_NIL, $+) - elsif $3 - return Token.new(T_NUMBER, $+) - elsif $4 - return Token.new(T_QUOTED, - $+.gsub(/\\(["\\])/n, "\\1")) - elsif $5 - len = $+.to_i - val = @str[@pos, len] - @pos += len - return Token.new(T_LITERAL, val) - elsif $6 - return Token.new(T_LPAR, $+) - elsif $7 - return Token.new(T_RPAR, $+) - else - parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid") - end - else - @str.index(/\S*/n, @pos) - parse_error("unknown token - %s", $&.dump) - end - when EXPR_TEXT - if @str.index(TEXT_REGEXP, @pos) - @pos = $~.end(0) - if $1 - return Token.new(T_TEXT, $+) - else - parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid") - end - else - @str.index(/\S*/n, @pos) - parse_error("unknown token - %s", $&.dump) - end - when EXPR_RTEXT - if @str.index(RTEXT_REGEXP, @pos) - @pos = $~.end(0) - if $1 - return Token.new(T_LBRA, $+) - elsif $2 - return Token.new(T_TEXT, $+) - else - parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid") - end - else - @str.index(/\S*/n, @pos) - parse_error("unknown token - %s", $&.dump) - end - when EXPR_CTEXT - if @str.index(CTEXT_REGEXP, @pos) - @pos = $~.end(0) - if $1 - return Token.new(T_TEXT, $+) - else - parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid") - end - else - @str.index(/\S*/n, @pos) #/ - parse_error("unknown token - %s", $&.dump) - end - else - parse_error("illegal @lex_state - %s", @lex_state.inspect) - end - end - - def parse_error(fmt, *args) - if IMAP.debug - $stderr.printf("@str: %s\n", @str.dump) - $stderr.printf("@pos: %d\n", @pos) - $stderr.printf("@lex_state: %s\n", @lex_state) - if @token.symbol - $stderr.printf("@token.symbol: %s\n", @token.symbol) - $stderr.printf("@token.value: %s\n", @token.value.inspect) - end - end - raise ResponseParseError, format(fmt, *args) - end - end - - class LoginAuthenticator - def process(data) - case @state - when STATE_USER - @state = STATE_PASSWORD - return @user - when STATE_PASSWORD - return @password - end - end - - private - - STATE_USER = :USER - STATE_PASSWORD = :PASSWORD - - def initialize(user, password) - @user = user - @password = password - @state = STATE_USER - end - end - add_authenticator "LOGIN", LoginAuthenticator - - class CramMD5Authenticator - def process(challenge) - digest = hmac_md5(challenge, @password) - return @user + " " + digest - end - - private - - def initialize(user, password) - @user = user - @password = password - end - - def hmac_md5(text, key) - if key.length > 64 - md5 = MD5.new(key) - key = md5.digest - end - - k_ipad = key + "\0" * (64 - key.length) - k_opad = key + "\0" * (64 - key.length) - for i in 0..63 - k_ipad[i] ^= 0x36 - k_opad[i] ^= 0x5c - end - - md5 = MD5.new - md5.update(k_ipad) - md5.update(text) - digest = md5.digest - - md5 = MD5.new - md5.update(k_opad) - md5.update(digest) - return md5.hexdigest - end - end - add_authenticator "CRAM-MD5", CramMD5Authenticator - - class Error < StandardError - end - - class DataFormatError < Error - end - - class ResponseParseError < Error - end - - class ResponseError < Error - end - - class NoResponseError < ResponseError - end - - class BadResponseError < ResponseError - end - - class ByeResponseError < ResponseError - end - end -end diff --git a/lib/net/net-protocol.gemspec b/lib/net/net-protocol.gemspec new file mode 100644 index 0000000000..2d911a966c --- /dev/null +++ b/lib/net/net-protocol.gemspec @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +name = File.basename(__FILE__, ".gemspec") +version = ["lib", Array.new(name.count("-"), "..").join("/")].find do |dir| + break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| + /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 + end rescue nil +end + +Gem::Specification.new do |spec| + spec.name = name + spec.version = version + spec.authors = ["Yukihiro Matsumoto"] + spec.email = ["matz@ruby-lang.org"] + + spec.summary = %q{The abstract interface for net-* client.} + spec.description = %q{The abstract interface for net-* client.} + spec.homepage = "https://github.com/ruby/net-protocol" + spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0") + spec.licenses = ["Ruby", "BSD-2-Clause"] + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + spec.metadata["changelog_uri"] = spec.homepage + "/releases" + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + excludes = %W[/.git* /bin /test /*file /#{File.basename(__FILE__)}] + spec.files = IO.popen(%W[git -C #{__dir__} ls-files -z --] + excludes.map {|e| ":^#{e}"}, &:read).split("\x0") + spec.require_paths = ["lib"] + + spec.add_dependency "timeout" +end diff --git a/lib/net/pop.rb b/lib/net/pop.rb deleted file mode 100644 index 29a48f375f..0000000000 --- a/lib/net/pop.rb +++ /dev/null @@ -1,450 +0,0 @@ -=begin - -= net/pop.rb version 1.2.0 - -written by Minero Aoki <aamine@dp.u-netsurf.ne.jp> - -This program is free software. -You can distribute/modify this program under -the terms of the Ruby Distribute License. - -Japanese version of this document is in "net" full package. -You can get it from RAA -(Ruby Application Archive: http://www.ruby-lang.org/en/raa.html). - - -== Net::POP3 - -=== Super Class - -Net::Protocol - -=== Class Methods - -: new( address = 'localhost', port = 110 ) - creates a new Net::POP3 object. - This method does not open TCP connection yet. - -: start( address = 'localhost', port = 110, account, password ) -: start( address = 'localhost', port = 110, account, password ) {|pop| .... } - equals to Net::POP3.new( address, port ).start( account, password ) - - # typical usage - Net::POP3.start( addr, port, acnt, pass ) do |pop| - pop.each_mail do |m| - any_file.write m.pop - m.delete - end - end - -: foreach( address = 'localhost', port = 110, account, password ) {|mail| .... } - starts protocol and iterate for each POPMail object. - This method equals to - - Net::POP3.start( address, port, account, password ) do |pop| - pop.each do |m| - yield m - end - end - - . - - # typical usage - Net::POP3.foreach( addr, nil, acnt, pass ) do |m| - m.pop file - m.delete - end - -: delete_all( address = 'localhost', port = 110, account, password ) -: delete_all( address = 'localhost', port = 110, account, password ) {|mail| .... } - starts POP3 session and delete all mails. - If block is given, iterates for each POPMail object before delete. - - # typical usage - Net::POP3.delete_all( addr, nil, acnt, pass ) do |m| - m.pop file - end - -=== Methods - -: start( account, password ) -: start( account, password ) {|pop| .... } - starts POP3 session. - - When called with block, gives a POP3 object to block and - closes the session after block call finish. - -: mails - an array of ((URL:#POPMail)). - This array is renewed when session started. - -: each_mail {|popmail| .... } -: each {|popmail| .... } - is equals to "pop3.mails.each" - -: delete_all -: delete_all {|popmail| .... } - deletes all mails. - If called with block, gives mails to the block before deleting. - - # example 1 - # pop and delete all mails - n = 1 - pop.delete_all do |m| - File.open("inbox/#{n}") {|f| f.write m.pop } - n += 1 - end - - # example 2 - # clear all mails on server - Net::POP3.start( addr, port, acc, pass ) do |pop| - pop.delete_all - end - -: reset - reset the session. All "deleted mark" are removed. - - -== Net::APOP - -This class defines no new methods. -Only difference from POP3 is using APOP authentification. - -=== Super Class - -Net::POP3 - - -== Net::POPMail - -A class of mail which exists on POP server. - -=== Super Class - -Object - - -=== Methods - -: pop( dest = '' ) - This method fetches a mail and write to 'dest' using '<<' method. - - # usage example - - mailarr = [] - POP3.start( 'localhost', 110 ) do |pop| - pop.each_mail do |popm| - mailarr.push popm.pop # all() returns 'dest' (this time, string) - # or, you can also - # popm.pop( $stdout ) # write mail to stdout - - # maybe you also want to delete mail after popping - popm.delete - end - end - -: pop {|str| .... } - If pop() is called with block, it gives the block part strings of a mail. - - # usage example - - POP3.start( 'localhost', 110 ) do |pop3| - pop3.each_mail do |m| - m.pop do |str| - # do anything - end - end - end - -: header - This method fetches only mail header. - -: top( lines ) - This method fetches mail header and 'lines' lines body. - -: delete -: delete! - This method deletes mail. - -: size - size of mail(bytes) - -: deleted? - true if mail was deleted - -=end - -require 'net/protocol' -require 'md5' - - -module Net - - class POP3 < Protocol - - protocol_param :port, '110' - protocol_param :command_type, '::Net::NetPrivate::POP3Command' - - protocol_param :mail_type, '::Net::POPMail' - - class << self - - def foreach( address = nil, port = nil, - account = nil, password = nil, &block ) - start( address, port, account, password ) do |pop| - pop.each_mail( &block ) - end - end - - def delete_all( address = nil, port = nil, - account = nil, password = nil, &block ) - start( address, port, account, password ) do |pop| - pop.delete_all( &block ) - end - end - - end - - - def initialize( addr = nil, port = nil ) - super - @mails = nil - end - - attr :mails - - def each_mail( &block ) - io_check - @mails.each( &block ) - end - - alias each each_mail - - def delete_all - @mails.each do |m| - yield m if block_given? - m.delete unless m.deleted? - end - end - - def reset - io_check - @command.rset - @mails.each do |m| - m.instance_eval { @deleted = false } - end - end - - - private - - def do_start( acnt, pwd ) - @command.auth( acnt, pwd ) - - @mails = [] - mtype = type.mail_type - @command.list.each_with_index do |size,idx| - if size then - @mails.push mtype.new( idx, size, @command ) - end - end - @mails.freeze - end - - def io_check - if not @socket or @socket.closed? then - raise IOError, 'pop session is not opened yet' - end - end - - end - - POP = POP3 - POPSession = POP3 - POP3Session = POP3 - - - class APOP < POP3 - protocol_param :command_type, 'Net::NetPrivate::APOPCommand' - end - - APOPSession = APOP - - - class POPMail - - def initialize( n, s, cmd ) - @num = n - @size = s - @command = cmd - - @deleted = false - end - - attr :size - - def inspect - "#<#{type} #{@num}#{@deleted ? ' deleted' : ''}>" - end - - def all( dest = '' ) - if block_given? then - dest = NetPrivate::ReadAdapter.new( Proc.new ) - end - @command.retr( @num, dest ) - end - alias pop all - alias mail all - - def top( lines, dest = '' ) - @command.top( @num, lines, dest ) - end - - def header( dest = '' ) - top 0, dest - end - - def delete - @command.dele( @num ) - @deleted = true - end - - alias delete! delete - - def deleted? - @deleted - end - - def uidl - @command.uidl @num - end - - end - - - - module NetPrivate - - - class POP3Command < Command - - def initialize( sock ) - super - critical { - check_reply SuccessCode - } - end - - def auth( acnt, pass ) - critical { - @socket.writeline 'USER ' + acnt - check_reply_auth - - @socket.writeline 'PASS ' + pass - check_reply_auth - } - end - - def list - arr = [] - critical { - getok 'LIST' - @socket.read_pendlist do |line| - num, siz = line.split( / +/o ) - arr[ num.to_i ] = siz.to_i - end - } - arr - end - - def rset - critical { - getok 'RSET' - } - end - - - def top( num, lines = 0, dest = '' ) - critical { - getok sprintf( 'TOP %d %d', num, lines ) - @socket.read_pendstr( dest ) - } - end - - def retr( num, dest = '', &block ) - critical { - getok sprintf( 'RETR %d', num ) - @socket.read_pendstr( dest, &block ) - } - end - - def dele( num ) - critical { - getok sprintf( 'DELE %d', num ) - } - end - - def uidl( num ) - critical { - getok( sprintf 'UIDL %d', num ).msg.split(' ')[1] - } - end - - def quit - critical { - getok 'QUIT' - } - end - - - private - - def check_reply_auth - begin - cod = check_reply( SuccessCode ) - rescue ProtocolError - raise ProtoAuthError, 'Fail to POP authentication' - end - - return cod - end - - def get_reply - str = @socket.readline - - if /\A\+/ === str then - return Response.new( SuccessCode, str[0,3], str[3, str.size - 3].strip ) - else - return Response.new( ErrorCode, str[0,4], str[4, str.size - 4].strip ) - end - end - - end - - - class APOPCommand < POP3Command - - def initialize( sock ) - rep = super( sock ) - - m = /<.+>/.match( rep.msg ) - unless m then - raise ProtoAuthError, "This is not APOP server: can't login" - end - @stamp = m[0] - end - - def auth( account, pass ) - critical { - @socket.writeline sprintf( 'APOP %s %s', - account, MD5.new(@stamp + pass).hexdigest ) - check_reply_auth - } - end - - end - - - end # module Net::NetPrivate - -end # module Net diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index e2bb1f47ca..8c81298c0e 100644 --- a/lib/net/protocol.rb +++ b/lib/net/protocol.rb @@ -1,229 +1,73 @@ -=begin - -= net/protocol.rb version 1.2.0 - -written by Minero Aoki <aamine@dp.u-netsurf.ne.jp> - -This program is free software. -You can distribute/modify this program under -the terms of the Ruby Distribute License. - -Japanese version of this document is in "net" full package. -You can get it from RAA -(Ruby Application Archive: http://www.ruby-lang.org/en/raa.html). - - -== Net::Protocol - -the abstract class for Internet protocol - -=== Super Class - -Object - -=== Class Methods - -: new( address = 'localhost', port = nil ) - This method Creates a new protocol object. - -: start( address = 'localhost', port = nil, *protoargs ) -: start( address = 'localhost', port = nil, *protoargs ) {|proto| .... } - This method creates a new Protocol object and opens a session. - equals to Net::Protocol.new( address, port ).start( *protoargs ) - -=== Methods - -: address - the address of connecting server (FQDN). - -: port - connecting port number - -: start( *args ) -: start( *args ) {|proto| .... } - This method starts protocol. If protocol was already started, - do nothing and returns false. - - '*args' are specified in subclasses. - - When is called with block, gives Protocol object to block and - close session when block finished. - -: finish - This method ends protocol. If you call this method before protocol starts, - it only return false without doing anything. - -: active? - true if session have been started - -=end +# frozen_string_literal: true +# +# = net/protocol.rb +# +#-- +# Copyright (c) 1999-2004 Yukihiro Matsumoto +# Copyright (c) 1999-2004 Minero Aoki +# +# written and maintained by Minero Aoki <aamine@loveruby.net> +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself, +# Ruby Distribute License or GNU General Public License. +# +# $Id$ +#++ +# +# WARNING: This file is going to remove. +# Do not rely on the implementation written in this file. +# require 'socket' +require 'timeout' +require 'io/wait' +module Net # :nodoc: -module Net - - class Protocol - - Version = '1.2.0' - + class Protocol #:nodoc: internal use only + VERSION = "0.2.2" - class << self - - def start( address = 'localhost', port = nil, *args ) - instance = new( address, port ) - - if block_given? then - instance.start( *args ) { yield instance } - else - instance.start( *args ) - instance + private + def Protocol.protocol_param(name, val) + module_eval(<<-End, __FILE__, __LINE__ + 1) + def #{name} + #{val} end - end - - private - - def protocol_param( name, val ) - module_eval %- - def self.#{name.id2name} - #{val} + End + end + + def ssl_socket_connect(s, timeout) + if timeout + while true + raise Net::OpenTimeout if timeout <= 0 + start = Process.clock_gettime Process::CLOCK_MONOTONIC + # to_io is required because SSLSocket doesn't have wait_readable yet + case s.connect_nonblock(exception: false) + when :wait_readable; s.to_io.wait_readable(timeout) + when :wait_writable; s.to_io.wait_writable(timeout) + else; break end - - - end - - end - - - # - # sub-class requirements - # - # protocol_param command_type - # protocol_param port - # - # private method do_start (optional) - # private method do_finish (optional) - # - - protocol_param :port, 'nil' - protocol_param :command_type, 'nil' - protocol_param :socket_type, '::Net::NetPrivate::Socket' - - - def initialize( addr = nil, port = nil ) - @address = addr || 'localhost' - @port = port || type.port - - @command = nil - @socket = nil - - @active = false - @pipe = nil - end - - attr_reader :address - attr_reader :port - - attr_reader :command - attr_reader :socket - - def inspect - "#<#{type} #{address}:#{port} open=#{active?}>" - end - - - def start( *args ) - return false if active? - - if block_given? then - begin - _start args - yield self - ensure - finish + timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start end else - _start args + s.connect end end - def _start( args ) - connect - do_start( *args ) - @active = true - end - private :_start - - def finish - return false unless active? - - do_finish unless @command.critical? - disconnect - @active = false - true - end - - def active? - @active - end - - def set_pipe( arg ) # un-documented - @pipe = arg - end - - - private - - - def do_start + tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters + TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]] + tcp_socket_parameters.include?([:key, :open_timeout]) + else + # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize + # See discussion in https://github.com/ruby/net-http/pull/224 + Socket.method(:tcp).parameters.include?([:key, :open_timeout]) end - - def do_finish - @command.quit - end - - - def connect( addr = @address, port = @port ) - @socket = type.socket_type.open( addr, port, @pipe ) - @command = type.command_type.new( @socket ) - end - - def disconnect - @command = nil - if @socket and not @socket.closed? then - @socket.close - end - @socket = nil - end - - end - - Session = Protocol - - - - class Response - - def initialize( ctype, cno, msg ) - @code_type = ctype - @code = cno - @message = msg - super() - end - - attr_reader :code_type, :code, :message - alias msg message - - def inspect - "#<#{type} #{code}>" - end - - def error!( data = nil ) - raise code_type.error_type.new( code + ' ' + Net.quote(msg), data ) - end - + private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT end + # :stopdoc: class ProtocolError < StandardError; end class ProtoSyntaxError < ProtocolError; end class ProtoFatalError < ProtocolError; end @@ -233,565 +77,483 @@ module Net class ProtoCommandError < ProtocolError; end class ProtoRetriableError < ProtocolError; end ProtocRetryError = ProtoRetriableError + # :startdoc: - class ProtocolError - - def initialize( msg, data = nil ) - super msg - @data = data - end - - attr :data - - def inspect - "#<#{type}>" - end - - end - + ## + # OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot + # be created within the open_timeout. - class Code + class OpenTimeout < Timeout::Error; end - def initialize( paren, err ) - @parents = paren - @err = err - - @parents.push self - end - - attr_reader :parents - - def inspect - "#<#{type}>" - end - - def error_type - @err - end - - def ===( response ) - response.code_type.parents.reverse_each {|i| return true if i == self } - false - end - - def mkchild( err = nil ) - type.new( @parents + [self], err || @err ) - end - - end - - ReplyCode = Code.new( [], ProtoUnknownError ) - InformationCode = ReplyCode.mkchild( ProtoUnknownError ) - SuccessCode = ReplyCode.mkchild( ProtoUnknownError ) - ContinueCode = ReplyCode.mkchild( ProtoUnknownError ) - ErrorCode = ReplyCode.mkchild( ProtocolError ) - SyntaxErrorCode = ErrorCode.mkchild( ProtoSyntaxError ) - FatalErrorCode = ErrorCode.mkchild( ProtoFatalError ) - ServerErrorCode = ErrorCode.mkchild( ProtoServerError ) - AuthErrorCode = ErrorCode.mkchild( ProtoAuthError ) - RetriableCode = ReplyCode.mkchild( ProtoRetriableError ) - UnknownCode = ReplyCode.mkchild( ProtoUnknownError ) - - - - module NetPrivate - - - class WriteAdapter + ## + # ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be read within the read_timeout. - def initialize( sock, mid ) - @sock = sock - @mid = mid + class ReadTimeout < Timeout::Error + # :stopdoc: + def initialize(io = nil) + @io = io end + attr_reader :io - def inspect - "#<#{type}>" - end - - def write( str ) - @sock.__send__ @mid, str + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" + end + msg end - alias << write - end - class ReadAdapter + ## + # WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be written within the write_timeout. Not raised on Windows. - def initialize( block ) - @block = block + class WriteTimeout < Timeout::Error + # :stopdoc: + def initialize(io = nil) + @io = io end + attr_reader :io - def inspect - "#<#{type}>" - end - - def <<( str ) - callblock( str, &@block ) if @block - end - - private - - def callblock( str ) - begin - user_break = true - yield str - user_break = false - rescue Exception - user_break = false - raise - ensure - if user_break then - @block = nil - return # stop break - end + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" end + msg end - end - - class Command - - def initialize( sock ) - @socket = sock - @last_reply = nil - @critical = false + class BufferedIO #:nodoc: internal use only + def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil) + @io = io + @read_timeout = read_timeout + @write_timeout = write_timeout + @continue_timeout = continue_timeout + @debug_output = debug_output + @rbuf = ''.b + @rbuf_empty = true + @rbuf_offset = 0 end - attr_accessor :socket - attr_reader :last_reply + attr_reader :io + attr_accessor :read_timeout + attr_accessor :write_timeout + attr_accessor :continue_timeout + attr_accessor :debug_output def inspect - "#<#{type}>" + "#<#{self.class} io=#{@io}>" end - # abstract quit - - - private - - # abstract get_reply() - - def check_reply( *oks ) - @last_reply = get_reply - reply_must( @last_reply, *oks ) + def eof? + @io.eof? end - def reply_must( rep, *oks ) - oks.each do |i| - if i === rep then - return rep - end - end - rep.error! + def closed? + @io.closed? end - def getok( line, expect = SuccessCode ) - @socket.writeline line - check_reply expect + def close + @io.close end - # - # error handle + # Read # public - def critical? - @critical - end - - def error_ok - @critical = false - end - - private - - def critical - @critical = true - ret = yield - @critical = false - ret - end - - def begin_critical - ret = @critical - @critical = true - not ret - end - - def end_critical - @critical = false - end - - end - - - class Socket - - def initialize( addr, port, pipe = nil ) - @addr = addr - @port = port - @pipe = pipe - @prepipe = nil - - @closed = true - @ipaddr = '' - @sending = '' - @buffer = '' - - @socket = TCPsocket.new( addr, port ) - @closed = false - @ipaddr = @socket.addr[3] - end - - attr :pipe, true - - class << self - alias open new - end - - def inspect - "#<#{type} open=#{!@closed}>" - end - - def reopen - unless closed? then - close - @buffer = '' - end - @socket = TCPsocket.new( @addr, @port ) - @closed = false - end - - attr :socket, true - - def close - @socket.close - @closed = true - end - - def closed? - @closed - end - - def address - @addr.dup - end - - alias addr address - - attr_reader :port - - def ip_address - @ipaddr.dup - end - - alias ipaddr ip_address - - attr_reader :sending - - - ### - ### read - ### - - CRLF = "\r\n" - - def read( len, dest = '' ) - @pipe << "reading #{len} bytes...\n" if @pipe; pipeoff - - rsize = 0 - while rsize + @buffer.size < len do - rsize += writeinto( dest, @buffer.size ) - fill_rbuf + def read(len, dest = ''.b, ignore_eof = false) + LOG "reading #{len} bytes..." + read_bytes = 0 + begin + while read_bytes + rbuf_size < len + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end + rbuf_fill + end + s = rbuf_consume(len - read_bytes) + read_bytes += s.bytesize + dest << s + rescue EOFError + raise unless ignore_eof end - writeinto( dest, len - rsize ) - - @pipe << "read #{len} bytes\n" if pipeon + LOG "read #{read_bytes} bytes" dest end - - def read_all( dest = '' ) - @pipe << "reading all...\n" if @pipe; pipeoff - - rsize = 0 + def read_all(dest = ''.b) + LOG 'reading all...' + read_bytes = 0 begin - while true do - rsize += writeinto( dest, @buffer.size ) - fill_rbuf + while true + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end + rbuf_fill end rescue EOFError ; end - - @pipe << "read #{rsize} bytes\n" if pipeon + LOG "read #{read_bytes} bytes" dest end - - def readuntil( target ) - while true do - idx = @buffer.index( target ) - break if idx - fill_rbuf + def readuntil(terminator, ignore_eof = false) + offset = @rbuf_offset + begin + until idx = @rbuf.index(terminator, offset) + offset = @rbuf.bytesize + rbuf_fill + end + return rbuf_consume(idx + terminator.bytesize - @rbuf_offset) + rescue EOFError + raise unless ignore_eof + return rbuf_consume end - - dest = '' - writeinto( dest, idx + target.size ) - dest end - def readline - ret = readuntil( "\n" ) - ret.chop! - ret + readuntil("\n").chop end + private - def read_pendstr( dest ) - @pipe << "reading text...\n" if @pipe; pipeoff + BUFSIZE = 1024 * 16 - rsize = 0 - while (str = readuntil("\r\n")) != ".\r\n" do - rsize += str.size - str.gsub!( /\A\./, '' ) - dest << str + def rbuf_fill + tmp = @rbuf_empty ? @rbuf : nil + case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false) + when String + @rbuf_empty = false + if rv.equal?(tmp) + @rbuf_offset = 0 + else + @rbuf << rv + rv.clear + end + return + when :wait_readable + (io = @io.to_io).wait_readable(@read_timeout) or raise Net::ReadTimeout.new(io) + # continue looping + when :wait_writable + # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable. + # http://www.openssl.org/support/faq.html#PROG10 + (io = @io.to_io).wait_writable(@read_timeout) or raise Net::ReadTimeout.new(io) + # continue looping + when nil + raise EOFError, 'end of file reached' + end while true + end + + def rbuf_flush + if @rbuf_empty + @rbuf.clear + @rbuf_offset = 0 end - - @pipe << "read #{rsize} bytes\n" if pipeon - dest + nil end - - # private use only (can not handle 'break') - def read_pendlist - @pipe << "reading list...\n" if @pipe; pipeoff - - str = nil - i = 0 - while (str = readuntil("\r\n")) != ".\r\n" do - i += 1 - str.chop! - yield str - end - - @pipe << "read #{i} items\n" if pipeon + def rbuf_size + @rbuf.bytesize - @rbuf_offset end - - private - - - READ_BLOCK = 1024 * 8 - - def fill_rbuf - @buffer << @socket.sysread( READ_BLOCK ) + def rbuf_consume_all + rbuf_consume if rbuf_size > 0 end - def writeinto( dest, len ) - bsi = @buffer.size - dest << @buffer[ 0, len ] - @buffer = @buffer[ len, bsi - len ] + def rbuf_consume(len = nil) + if @rbuf_offset == 0 && (len.nil? || len == @rbuf.bytesize) + s = @rbuf + @rbuf = ''.b + @rbuf_offset = 0 + @rbuf_empty = true + elsif len.nil? + s = @rbuf.byteslice(@rbuf_offset..-1) + @rbuf = ''.b + @rbuf_offset = 0 + @rbuf_empty = true + else + s = @rbuf.byteslice(@rbuf_offset, len) + @rbuf_offset += len + @rbuf_empty = @rbuf_offset == @rbuf.bytesize + rbuf_flush + end - @pipe << %{read "#{Net.quote dest}"\n} if @pipe - len + @debug_output << %Q[-> #{s.dump}\n] if @debug_output + s end - - ### - ### write - ### + # + # Write + # public - - def write( str ) + def write(*strs) writing { - do_write str + write0(*strs) } end + alias << write - def writeline( str ) + def writeline(str) writing { - do_write str - do_write "\r\n" + write0 str + "\r\n" } end + private - def write_bin( src, block ) - writing { - if block then - block.call WriteAdapter.new( self, :do_write ) - else - src.each do |bin| - do_write bin + def writing + @written_bytes = 0 + @debug_output << '<- ' if @debug_output + yield + @debug_output << "\n" if @debug_output + bytes = @written_bytes + @written_bytes = nil + bytes + end + + def write0(*strs) + @debug_output << strs.map(&:dump).join if @debug_output + orig_written_bytes = @written_bytes + strs.each_with_index do |str, i| + need_retry = true + case len = @io.write_nonblock(str, exception: false) + when Integer + @written_bytes += len + len -= str.bytesize + if len == 0 + if strs.size == i+1 + return @written_bytes - orig_written_bytes + else + need_retry = false + # next string + end + elsif len < 0 + str = str.byteslice(len, -len) + else # len > 0 + need_retry = false + # next string end - end - } + # continue looping + when :wait_writable + (io = @io.to_io).wait_writable(@write_timeout) or raise Net::WriteTimeout.new(io) + # continue looping + end while need_retry + end end + # + # Logging + # - def write_pendstr( src, block ) - @pipe << "writing text from #{src.type}\n" if @pipe; pipeoff + private - wsize = use_each_crlf_line { - if block then - block.call WriteAdapter.new( self, :wpend_in ) - else - wpend_in src - end - } + def LOG_off + @save_debug_out = @debug_output + @debug_output = nil + end + + def LOG_on + @debug_output = @save_debug_out + end - @pipe << "wrote #{wsize} bytes text\n" if pipeon - wsize + def LOG(msg) + return unless @debug_output + @debug_output << msg + "\n" end + end - private + class InternetMessageIO < BufferedIO #:nodoc: internal use only + def initialize(*, **) + super + @wbuf = nil + end + # + # Read + # - def wpend_in( src ) - line = nil - pre = @writtensize - each_crlf_line( src ) do |line| - do_write '.' if line[0] == ?. - do_write line + def each_message_chunk + LOG 'reading message...' + LOG_off() + read_bytes = 0 + while (line = readuntil("\r\n")) != ".\r\n" + read_bytes += line.size + yield line.delete_prefix('.') end + LOG_on() + LOG "read message (#{read_bytes} bytes)" + end - @writtensize - pre + # *library private* (cannot handle 'break') + def each_list_item + while (str = readuntil("\r\n")) != ".\r\n" + yield str.chop + end end - def use_each_crlf_line - writing { - @wbuf = '' + def write_message_0(src) + prev = @written_bytes + each_crlf_line(src) do |line| + write0 dot_stuff(line) + end + @written_bytes - prev + end - yield + # + # Write + # - if not @wbuf.empty? then # un-terminated last line - if @wbuf[-1] == ?\r then - @wbuf.chop! - end - @wbuf.concat "\r\n" - do_write @wbuf - elsif @writtensize == 0 then # empty src - do_write "\r\n" - end - do_write ".\r\n" + def write_message(src) + LOG "writing message from #{src.class}" + LOG_off() + len = writing { + using_each_crlf_line { + write_message_0 src + } + } + LOG_on() + LOG "wrote #{len} bytes" + len + end - @wbuf = nil + def write_message_by_block(&block) + LOG 'writing message from block' + LOG_off() + len = writing { + using_each_crlf_line { + begin + block.call(WriteAdapter.new(self.method(:write_message_0))) + rescue LocalJumpError + # allow `break' from writer block + end + } } + LOG_on() + LOG "wrote #{len} bytes" + len end - def each_crlf_line( src ) - str = m = beg = nil + private - adding( src ) do - beg = 0 - buf = @wbuf - while buf.index( /\n|\r\n|\r/, beg ) do - m = $~ - if m.begin(0) == buf.size - 1 and buf[-1] == ?\r then - # "...\r" : can follow "\n..." - break - end - str = buf[ beg, m.begin(0) - beg ] - str.concat "\r\n" - yield str - beg = m.end(0) - end - @wbuf = buf[ beg, buf.size - beg ] + def dot_stuff(s) + s.sub(/\A\./, '..') + end + + def using_each_crlf_line + @wbuf = ''.b + yield + if not @wbuf.empty? # unterminated last line + write0 dot_stuff(@wbuf.chomp) + "\r\n" + elsif @written_bytes == 0 # empty src + write0 "\r\n" end + write0 ".\r\n" + @wbuf = nil end - def adding( src ) - i = nil + def each_crlf_line(src) + buffer_filling(@wbuf, src) do + while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/) + yield line.chomp("\n") + "\r\n" + end + end + end + def buffer_filling(buf, src) case src - when String - 0.step( src.size - 1, 2048 ) do |i| - @wbuf << src[i,2048] + when String # for speeding up. + 0.step(src.size - 1, 1024) do |i| + buf << src[i, 1024] yield end - - when File - while true do - i = src.read( 2048 ) - break unless i - i[0,0] = @wbuf - @wbuf = i + when File # for speeding up. + while s = src.read(1024) + buf << s yield end - - else - src.each do |i| - @wbuf << i - if @wbuf.size > 2048 then - yield - end + else # generic reader + src.each do |str| + buf << str + yield if buf.size > 1024 end - yield unless @wbuf.empty? + yield unless buf.empty? end end + end - def writing - @writtensize = 0 - @sending = '' + # + # The writer adapter class + # + class WriteAdapter + # :stopdoc: + def initialize(writer) + @writer = writer + end - yield + def inspect + "#<#{self.class} writer=#{@writer.inspect}>" + end - if @pipe then - @pipe << 'write "' - @pipe << @sending - @pipe << "\"\n" - end - @socket.flush - @writtensize + def write(str) + @writer.call(str) end - def do_write( arg ) - if @pipe or @sending.size < 128 then - @sending << Net.quote( arg ) - else - @sending << '...' unless @sending[-1] == ?. - end + alias print write - s = @socket.write( arg ) - @writtensize += s - s + def <<(str) + write str + self + end + + def puts(str = '') + write str.chomp("\n") + "\n" + end + + def printf(*args) + write sprintf(*args) end + end - def pipeoff - @prepipe = @pipe - @pipe = nil - @prepipe + class ReadAdapter #:nodoc: internal use only + def initialize(block) + @block = block end - def pipeon - @pipe = @prepipe - @prepipe = nil - @pipe + def inspect + "#<#{self.class}>" end - end + def <<(str) + call_block(str, &@block) if @block + end + private - end # module Net::NetPrivate + # This method is needed because @block must be called by yield, + # not Proc#call. You can see difference when using `break' in + # the block. + def call_block(str) + yield str + end + end - def Net.quote( str ) - str = str.gsub( "\n", '\\n' ) - str.gsub!( "\r", '\\r' ) - str.gsub!( "\t", '\\t' ) - str + module NetPrivate #:nodoc: obsolete + Socket = ::Net::InternetMessageIO end end # module Net diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb deleted file mode 100644 index 8dd2a3c811..0000000000 --- a/lib/net/smtp.rb +++ /dev/null @@ -1,303 +0,0 @@ -=begin - -= net/smtp.rb version 1.2.0 - -written by Minero Aoki <aamine@dp.u-netsurf.ne.jp> - -This program is free software. -You can distribute/modify this program under -the terms of the Ruby Distribute License. - -Japanese version of this document is in "net" full package. -You can get it from RAA -(Ruby Application Archive: http://www.ruby-lang.org/en/raa.html). - - -== Net::SMTP - -=== Super Class - -Net::Protocol - -=== Class Methods - -: new( address = 'localhost', port = 25 ) - creates a new Net::SMTP object. - -: start( address = 'localhost', port = 25, *protoargs ) -: start( address = 'localhost', port = 25, *protoargs ) {|smtp| .... } - is equal to Net::SMTP.new( address, port ).start( *protoargs ) - -=== Methods - -: start( helo_domain = Socket.gethostname, \ - account = nil, password = nil, authtype = nil ) -: start( helo_domain = Socket.gethostname, \ - account = nil, password = nil, authtype = nil ) {|smtp| .... } - opens TCP connection and starts SMTP session. - If protocol had been started, do nothing and return false. - - When this methods is called with block, give a SMTP object to block and - close session after block call finished. - - If account and password are given, is trying to get authentication - by using AUTH command. "authtype" is :plain (symbol) or :cram_md5. - -: send_mail( mailsrc, from_addr, *to_addrs ) -: sendmail( mailsrc, from_addr, *to_addrs ) - This method sends 'mailsrc' as mail. SMTP read strings - from 'mailsrc' by calling 'each' iterator, and convert them - into "\r\n" terminated string when write. - - from_addr must be String. - to_addrs must be a String(s) or an Array of String. - - Exceptions which SMTP raises are: - * Net::ProtoSyntaxError: syntax error (errno.500) - * Net::ProtoFatalError: fatal error (errno.550) - * Net::ProtoUnknownError: unknown error - * Net::ProtoServerBusy: temporary error (errno.420/450) - - # usage example - - Net::SMTP.start( 'localhost', 25 ) do |smtp| - smtp.send_mail mail_string, 'from-addr@foo.or.jp', 'to-addr@bar.or.jp' - end - -: ready( from_addr, to_addrs ) {|adapter| .... } - This method stands by the SMTP object for sending mail. - "adapter" object accepts only "write" method. - - # usage example - - Net::SMTP.start( 'localhost', 25 ) do |smtp| - smtp.ready( from, to ) do |adapter| - adapter.write str1 - adapter.write str2 - adapter.write str3 - end - end - -: finish - finishes SMTP session. - If SMTP session had not started, do nothing and return false. - -=end - -require 'net/protocol' -require 'md5' - - -module Net - - - class SMTP < Protocol - - protocol_param :port, '25' - protocol_param :command_type, '::Net::NetPrivate::SMTPCommand' - - - def initialize( addr = nil, port = nil ) - super - @esmtp = true - end - - attr :esmtp - - def send_mail( mailsrc, from_addr, *to_addrs ) - do_ready from_addr, to_addrs.flatten - @command.write_mail mailsrc, nil - end - - alias sendmail send_mail - - def ready( from_addr, *to_addrs, &block ) - do_ready from_addr, to_addrs.flatten - @command.write_mail nil, block - end - - - private - - - def do_ready( from_addr, to_addrs ) - if to_addrs.empty? then - raise ArgumentError, 'mail destination does not given' - end - @command.mailfrom from_addr - @command.rcpt to_addrs - @command.data - end - - def do_start( helodom = nil, - user = nil, secret = nil, authtype = nil ) - helodom ||= ::Socket.gethostname - unless helodom then - raise ArgumentError, - "cannot get localhost name; try 'smtp.start(local_host_name)'" - end - - begin - if @esmtp then - @command.ehlo helodom - else - @command.helo helodom - end - rescue ProtocolError - if @esmtp then - @esmtp = false - @command.error_ok - retry - else - raise - end - end - - if user and secret then - mid = 'auth_' + (authtype || 'cram_md5').to_s - unless @command.respond_to? mid then - raise ArgumentError, "wrong auth type #{authtype.to_s}" - end - @command.send mid, user, secret - end - end - - end - - SMTPSession = SMTP - - - - module NetPrivate - - - class SMTPCommand < Command - - def initialize( sock ) - super - critical { - check_reply SuccessCode - } - end - - - def helo( fromdom ) - critical { - getok sprintf( 'HELO %s', fromdom ) - } - end - - - def ehlo( fromdom ) - critical { - getok sprintf( 'EHLO %s', fromdom ) - } - end - - - # "PLAIN" authentication [RFC2554] - def auth_plain( user, secret ) - critical { - getok sprintf( 'AUTH PLAIN %s', - ["\0#{user}\0#{secret}"].pack('m').chomp ) - } - end - - # "CRAM-MD5" authentication [RFC2195] - def auth_cram_md5( user, secret ) - critical { - rep = getok( 'AUTH CRAM-MD5', ContinueCode ) - challenge = rep.msg.split(' ')[1].unpack('m')[0] - secret = MD5.new( secret ).digest if secret.size > 64 - - isecret = secret + "\0" * (64 - secret.size) - osecret = isecret.dup - 0.upto( 63 ) do |i| - isecret[i] ^= 0x36 - osecret[i] ^= 0x5c - end - tmp = MD5.new( isecret + challenge ).digest - tmp = MD5.new( osecret + tmp ).hexdigest - - getok [user + ' ' + tmp].pack('m').chomp - } - end - - - def mailfrom( fromaddr ) - critical { - getok sprintf( 'MAIL FROM:<%s>', fromaddr ) - } - end - - - def rcpt( toaddrs ) - toaddrs.each do |i| - critical { - getok sprintf( 'RCPT TO:<%s>', i ) - } - end - end - - - def data - return unless begin_critical - getok 'DATA', ContinueCode - end - - def write_mail( mailsrc, block ) - @socket.write_pendstr mailsrc, block - check_reply SuccessCode - end_critical - end - - - def quit - critical { - getok 'QUIT' - } - end - - - private - - - def get_reply - arr = read_reply - stat = arr[0][0,3] - - klass = case stat[0] - when ?2 then SuccessCode - when ?3 then ContinueCode - when ?4 then ServerErrorCode - when ?5 then - case stat[1] - when ?0 then SyntaxErrorCode - when ?3 then AuthErrorCode - when ?5 then FatalErrorCode - end - end - klass ||= UnknownCode - - Response.new( klass, stat, arr.join('') ) - end - - - def read_reply - arr = [] - while true do - str = @socket.readline - break unless str[3] == ?- # ex: "210-..." - arr.push str - end - arr.push str - - arr - end - - end - - - end # module Net::NetPrivate - -end # module Net diff --git a/lib/net/telnet.rb b/lib/net/telnet.rb deleted file mode 100644 index 87790c0300..0000000000 --- a/lib/net/telnet.rb +++ /dev/null @@ -1,779 +0,0 @@ -=begin - -== NAME - -net/telnet.rb - simple telnet client library - -Version 1.6.2 - -Wakou Aoyama <wakou@fsinet.or.jp> - - -=== MAKE NEW TELNET OBJECT - - host = Net::Telnet::new({ - "Binmode" => false, # default: false - "Host" => "localhost", # default: "localhost" - "Output_log" => "output_log", # default: nil (no output) - "Dump_log" => "dump_log", # default: nil (no output) - "Port" => 23, # default: 23 - "Prompt" => /[$%#>] \z/n, # default: /[$%#>] \z/n - "Telnetmode" => true, # default: true - "Timeout" => 10, # default: 10 - # if ignore timeout then set "Timeout" to false. - "Waittime" => 0, # default: 0 - "Proxy" => proxy # default: nil - # proxy is Net::Telnet or IO object - }) - -Telnet object has socket class methods. - -if set "Telnetmode" option to false. not telnet command interpretation. -"Waittime" is time to confirm "Prompt". There is a possibility that -the same character as "Prompt" is included in the data, and, when -the network or the host is very heavy, the value is enlarged. - - -=== STATUS OUTPUT - - host = Net::Telnet::new({"Host" => "localhost"}){|c| print c } - -connection status output. - -example: - - Trying localhost... - Connected to localhost. - - -=== WAIT FOR MATCH - - line = host.waitfor(/match/) - line = host.waitfor({"Match" => /match/, - "String" => "string", - "Timeout" => secs}) - # if ignore timeout then set "Timeout" to false. - -if set "String" option, then Match == Regexp.new(quote("string")) - - -==== REALTIME OUTPUT - - host.waitfor(/match/){|c| print c } - host.waitfor({"Match" => /match/, - "String" => "string", - "Timeout" => secs}){|c| print c} - -of cource, set sync=true or flush is necessary. - - -=== SEND STRING AND WAIT PROMPT - - line = host.cmd("string") - line = host.cmd({"String" => "string", - "Match" => /[$%#>] \z/n, - "Timeout" => 10}) - - -==== REALTIME OUTPUT - - host.cmd("string"){|c| print c } - host.cmd({"String" => "string", - "Match" => /[$%#>] \z/n, - "Timeout" => 10}){|c| print c } - -of cource, set sync=true or flush is necessary. - - -=== SEND STRING - - host.print("string") - host.puts("string") - -Telnet#puts() adds "\n" to the last of "string". - -WARNING: Telnet#print() NOT adds "\n" to the last of "string", in the future. - -If "Telnetmode" option is true, then escape IAC code ("\xFF"). If -"Binmode" option is false, then convert "\n" to EOL(end of line) code. - -If "WILL SGA" and "DO BIN", then EOL is CR. If "WILL SGA", then EOL is -CR + NULL. If the other cases, EOL is CR + LF. - - -=== TOGGLE TELNET COMMAND INTERPRETATION - - host.telnetmode # return the current status (true or false) - host.telnetmode = true # do telnet command interpretation (default) - host.telnetmode = false # don't telnet command interpretation - - -=== TOGGLE NEWLINE TRANSLATION - - host.binmode # return the current status (true or false) - host.binmode = true # no translate newline - host.binmode = false # translate newline (default) - - -=== LOGIN - - host.login("username", "password") - host.login({"Name" => "username", - "Password" => "password"}) - -if no password prompt: - - host.login("username") - host.login({"Name" => "username"}) - - -==== REALTIME OUTPUT - - host.login("username", "password"){|c| print c } - host.login({"Name" => "username", - "Password" => "password"}){|c| print c } - -of cource, set sync=true or flush is necessary. - - - -== EXAMPLE - -=== LOGIN AND SEND COMMAND - - localhost = Net::Telnet::new({"Host" => "localhost", - "Timeout" => 10, - "Prompt" => /[$%#>] \z/n}) - localhost.login("username", "password"){|c| print c } - localhost.cmd("command"){|c| print c } - localhost.close - - -=== CHECKS A POP SERVER TO SEE IF YOU HAVE MAIL - - pop = Net::Telnet::new({"Host" => "your_destination_host_here", - "Port" => 110, - "Telnetmode" => false, - "Prompt" => /^\+OK/n}) - pop.cmd("user " + "your_username_here"){|c| print c} - pop.cmd("pass " + "your_password_here"){|c| print c} - pop.cmd("list"){|c| print c} - - -=end - - -require "socket" -require "delegate" -require "timeout" -require "English" - -module Net - class Telnet < SimpleDelegator - - IAC = 255.chr # "\377" # "\xff" # interpret as command: - DONT = 254.chr # "\376" # "\xfe" # you are not to use option - DO = 253.chr # "\375" # "\xfd" # please, you use option - WONT = 252.chr # "\374" # "\xfc" # I won't use option - WILL = 251.chr # "\373" # "\xfb" # I will use option - SB = 250.chr # "\372" # "\xfa" # interpret as subnegotiation - GA = 249.chr # "\371" # "\xf9" # you may reverse the line - EL = 248.chr # "\370" # "\xf8" # erase the current line - EC = 247.chr # "\367" # "\xf7" # erase the current character - AYT = 246.chr # "\366" # "\xf6" # are you there - AO = 245.chr # "\365" # "\xf5" # abort output--but let prog finish - IP = 244.chr # "\364" # "\xf4" # interrupt process--permanently - BREAK = 243.chr # "\363" # "\xf3" # break - DM = 242.chr # "\362" # "\xf2" # data mark--for connect. cleaning - NOP = 241.chr # "\361" # "\xf1" # nop - SE = 240.chr # "\360" # "\xf0" # end sub negotiation - EOR = 239.chr # "\357" # "\xef" # end of record (transparent mode) - ABORT = 238.chr # "\356" # "\xee" # Abort process - SUSP = 237.chr # "\355" # "\xed" # Suspend process - EOF = 236.chr # "\354" # "\xec" # End of file - SYNCH = 242.chr # "\362" # "\xf2" # for telfunc calls - - OPT_BINARY = 0.chr # "\000" # "\x00" # Binary Transmission - OPT_ECHO = 1.chr # "\001" # "\x01" # Echo - OPT_RCP = 2.chr # "\002" # "\x02" # Reconnection - OPT_SGA = 3.chr # "\003" # "\x03" # Suppress Go Ahead - OPT_NAMS = 4.chr # "\004" # "\x04" # Approx Message Size Negotiation - OPT_STATUS = 5.chr # "\005" # "\x05" # Status - OPT_TM = 6.chr # "\006" # "\x06" # Timing Mark - OPT_RCTE = 7.chr # "\a" # "\x07" # Remote Controlled Trans and Echo - OPT_NAOL = 8.chr # "\010" # "\x08" # Output Line Width - OPT_NAOP = 9.chr # "\t" # "\x09" # Output Page Size - OPT_NAOCRD = 10.chr # "\n" # "\x0a" # Output Carriage-Return Disposition - OPT_NAOHTS = 11.chr # "\v" # "\x0b" # Output Horizontal Tab Stops - OPT_NAOHTD = 12.chr # "\f" # "\x0c" # Output Horizontal Tab Disposition - OPT_NAOFFD = 13.chr # "\r" # "\x0d" # Output Formfeed Disposition - OPT_NAOVTS = 14.chr # "\016" # "\x0e" # Output Vertical Tabstops - OPT_NAOVTD = 15.chr # "\017" # "\x0f" # Output Vertical Tab Disposition - OPT_NAOLFD = 16.chr # "\020" # "\x10" # Output Linefeed Disposition - OPT_XASCII = 17.chr # "\021" # "\x11" # Extended ASCII - OPT_LOGOUT = 18.chr # "\022" # "\x12" # Logout - OPT_BM = 19.chr # "\023" # "\x13" # Byte Macro - OPT_DET = 20.chr # "\024" # "\x14" # Data Entry Terminal - OPT_SUPDUP = 21.chr # "\025" # "\x15" # SUPDUP - OPT_SUPDUPOUTPUT = 22.chr # "\026" # "\x16" # SUPDUP Output - OPT_SNDLOC = 23.chr # "\027" # "\x17" # Send Location - OPT_TTYPE = 24.chr # "\030" # "\x18" # Terminal Type - OPT_EOR = 25.chr # "\031" # "\x19" # End of Record - OPT_TUID = 26.chr # "\032" # "\x1a" # TACACS User Identification - OPT_OUTMRK = 27.chr # "\e" # "\x1b" # Output Marking - OPT_TTYLOC = 28.chr # "\034" # "\x1c" # Terminal Location Number - OPT_3270REGIME = 29.chr # "\035" # "\x1d" # Telnet 3270 Regime - OPT_X3PAD = 30.chr # "\036" # "\x1e" # X.3 PAD - OPT_NAWS = 31.chr # "\037" # "\x1f" # Negotiate About Window Size - OPT_TSPEED = 32.chr # " " # "\x20" # Terminal Speed - OPT_LFLOW = 33.chr # "!" # "\x21" # Remote Flow Control - OPT_LINEMODE = 34.chr # "\"" # "\x22" # Linemode - OPT_XDISPLOC = 35.chr # "#" # "\x23" # X Display Location - OPT_OLD_ENVIRON = 36.chr # "$" # "\x24" # Environment Option - OPT_AUTHENTICATION = 37.chr # "%" # "\x25" # Authentication Option - OPT_ENCRYPT = 38.chr # "&" # "\x26" # Encryption Option - OPT_NEW_ENVIRON = 39.chr # "'" # "\x27" # New Environment Option - OPT_EXOPL = 255.chr # "\377" # "\xff" # Extended-Options-List - - NULL = "\000" - CR = "\015" - LF = "\012" - EOL = CR + LF - VERSION = "1.6.2" - RELEASE_DATE = "2000-12-25" - VERSION_CODE = 162 - RELEASE_CODE = 20001225 - - def initialize(options) - @options = options - @options["Host"] = "localhost" unless @options.has_key?("Host") - @options["Port"] = 23 unless @options.has_key?("Port") - @options["Prompt"] = /[$%#>] \z/n unless @options.has_key?("Prompt") - @options["Timeout"] = 10 unless @options.has_key?("Timeout") - @options["Waittime"] = 0 unless @options.has_key?("Waittime") - unless @options.has_key?("Binmode") - @options["Binmode"] = false - else - unless (true == @options["Binmode"] or false == @options["Binmode"]) - raise ArgumentError, "Binmode option required true or false" - end - end - - unless @options.has_key?("Telnetmode") - @options["Telnetmode"] = true - else - unless (true == @options["Telnetmode"] or false == @options["Telnetmode"]) - raise ArgumentError, "Telnetmode option required true or false" - end - end - - @telnet_option = { "SGA" => false, "BINARY" => false } - - if @options.has_key?("Output_log") - @log = File.open(@options["Output_log"], 'a+') - @log.sync = true - @log.binmode - end - - if @options.has_key?("Dump_log") - @dumplog = File.open(@options["Dump_log"], 'a+') - @dumplog.sync = true - @dumplog.binmode - def @dumplog.log_dump(dir, x) - len = x.length - addr = 0 - offset = 0 - while 0 < len - if len < 16 - line = x[offset, len] - else - line = x[offset, 16] - end - hexvals = line.unpack('H*')[0] - hexvals.concat ' ' * (32 - hexvals.length) - hexvals = format "%s %s %s %s " * 4, *hexvals.unpack('a2' * 16) - line = line.gsub(/[\000-\037\177-\377]/n, '.') - printf "%s 0x%5.5x: %s%s\n", dir, addr, hexvals, line - addr += 16 - offset += 16 - len -= 16 - end - print "\n" - end - end - - if @options.has_key?("Proxy") - if @options["Proxy"].kind_of?(Net::Telnet) - @sock = @options["Proxy"].sock - elsif @options["Proxy"].kind_of?(IO) - @sock = @options["Proxy"] - else - raise "Error; Proxy is Net::Telnet or IO object." - end - else - message = "Trying " + @options["Host"] + "...\n" - yield(message) if block_given? - @log.write(message) if @options.has_key?("Output_log") - @dumplog.log_dump('#', message) if @options.has_key?("Dump_log") - - begin - if @options["Timeout"] == false - @sock = TCPsocket.open(@options["Host"], @options["Port"]) - else - timeout(@options["Timeout"]) do - @sock = TCPsocket.open(@options["Host"], @options["Port"]) - end - end - rescue TimeoutError - raise TimeoutError, "timed-out; opening of the host" - rescue - @log.write($ERROR_INFO.to_s + "\n") if @options.has_key?("Output_log") - @dumplog.log_dump('#', $ERROR_INFO.to_s + "\n") if @options.has_key?("Dump_log") - raise - end - @sock.sync = true - @sock.binmode - - message = "Connected to " + @options["Host"] + ".\n" - yield(message) if block_given? - @log.write(message) if @options.has_key?("Output_log") - @dumplog.log_dump('#', message) if @options.has_key?("Dump_log") - end - - super(@sock) - end # initialize - - attr :sock - - def telnetmode(mode = nil) - if mode - if (true == mode or false == mode) - @options["Telnetmode"] = mode - else - raise ArgumentError, "required true or false" - end - else - @options["Telnetmode"] - end - end - - def telnetmode=(mode) - if (true == mode or false == mode) - @options["Telnetmode"] = mode - else - raise ArgumentError, "required true or false" - end - end - - def binmode(mode = nil) - if mode - if (true == mode or false == mode) - @options["Binmode"] = mode - else - raise ArgumentError, "required true or false" - end - else - @options["Binmode"] - end - end - - def binmode=(mode) - if (true == mode or false == mode) - @options["Binmode"] = mode - else - raise ArgumentError, "required true or false" - end - end - - def preprocess(string) - # combine CR+NULL into CR - string = string.gsub(/#{CR}#{NULL}/no, CR) if @options["Telnetmode"] - - # combine EOL into "\n" - string = string.gsub(/#{EOL}/no, "\n") unless @options["Binmode"] - - string.gsub(/#{IAC}( - [#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]| - [#{DO}#{DONT}#{WILL}#{WONT}] - [#{OPT_BINARY}-#{OPT_NEW_ENVIRON}#{OPT_EXOPL}]| - #{SB}[^#{IAC}]*#{IAC}#{SE} - )/xno) do - if IAC == $1 # handle escaped IAC characters - IAC - elsif AYT == $1 # respond to "IAC AYT" (are you there) - self.write("nobody here but us pigeons" + EOL) - '' - elsif DO[0] == $1[0] # respond to "IAC DO x" - if OPT_BINARY[0] == $1[1] - @telnet_option["BINARY"] = true - self.write(IAC + WILL + OPT_BINARY) - else - self.write(IAC + WONT + $1[1..1]) - end - '' - elsif DONT[0] == $1[0] # respond to "IAC DON'T x" with "IAC WON'T x" - self.write(IAC + WONT + $1[1..1]) - '' - elsif WILL[0] == $1[0] # respond to "IAC WILL x" - if OPT_BINARY[0] == $1[1] - self.write(IAC + DO + OPT_BINARY) - elsif OPT_ECHO[0] == $1[1] - self.write(IAC + DO + OPT_ECHO) - elsif OPT_SGA[0] == $1[1] - @telnet_option["SGA"] = true - self.write(IAC + DO + OPT_SGA) - else - self.write(IAC + DONT + $1[1..1]) - end - '' - elsif WONT[0] == $1[0] # respond to "IAC WON'T x" - if OPT_ECHO[0] == $1[1] - self.write(IAC + DONT + OPT_ECHO) - elsif OPT_SGA[0] == $1[1] - @telnet_option["SGA"] = false - self.write(IAC + DONT + OPT_SGA) - else - self.write(IAC + DONT + $1[1..1]) - end - '' - else - '' - end - end - end # preprocess - - def waitfor(options) - time_out = @options["Timeout"] - waittime = @options["Waittime"] - - if options.kind_of?(Hash) - prompt = if options.has_key?("Match") - options["Match"] - elsif options.has_key?("Prompt") - options["Prompt"] - elsif options.has_key?("String") - Regexp.new( Regexp.quote(options["String"]) ) - end - time_out = options["Timeout"] if options.has_key?("Timeout") - waittime = options["Waittime"] if options.has_key?("Waittime") - else - prompt = options - end - - if time_out == false - time_out = nil - end - - line = '' - buf = '' - rest = '' - until(prompt === line and not IO::select([@sock], nil, nil, waittime)) - unless IO::select([@sock], nil, nil, time_out) - raise TimeoutError, "timed-out; wait for the next data" - end - begin - c = @sock.sysread(1024 * 1024) - @dumplog.log_dump('<', c) if @options.has_key?("Dump_log") - if @options["Telnetmode"] - if Integer(c.rindex(/#{IAC}#{SE}/no)) < - Integer(c.rindex(/#{IAC}#{SB}/no)) - buf = preprocess(rest + c[0 ... c.rindex(/#{IAC}#{SB}/no)]) - rest = c[c.rindex(/#{IAC}#{SB}/no) .. -1] - elsif pt = c.rindex(/#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no) - buf = preprocess(rest + c[0 ... pt]) - rest = c[pt .. -1] - else - buf = preprocess(c) - rest = '' - end - end - @log.print(buf) if @options.has_key?("Output_log") - line.concat(buf) - yield buf if block_given? - rescue EOFError # End of file reached - if line == '' - line = nil - yield nil if block_given? - end - break - end - end - line - end - - def write(string) - length = string.length - while 0 < length - IO::select(nil, [@sock]) - @dumplog.log_dump('>', string[-length..-1]) if @options.has_key?("Dump_log") - length -= @sock.syswrite(string[-length..-1]) - end - end - - def _print(string) - string = string.gsub(/#{IAC}/no, IAC + IAC) if @options["Telnetmode"] - - if @options["Binmode"] - self.write(string) - else - if @telnet_option["BINARY"] and @telnet_option["SGA"] - # IAC WILL SGA IAC DO BIN send EOL --> CR - self.write(string.gsub(/\n/n, CR)) - elsif @telnet_option["SGA"] - # IAC WILL SGA send EOL --> CR+NULL - self.write(string.gsub(/\n/n, CR + NULL)) - else - # NONE send EOL --> CR+LF - self.write(string.gsub(/\n/n, EOL)) - end - end - end - - def puts(string) - self._print(string + "\n") - end - - def print(string) - if $VERBOSE - $stderr.puts 'WARNING: Telnet#print("string") NOT adds "\n" to the last of "string", in the future.' - $stderr.puts ' cf. Telnet#puts().' - end - self.puts(string) - end - - def cmd(options) - match = @options["Prompt"] - time_out = @options["Timeout"] - - if options.kind_of?(Hash) - string = options["String"] - match = options["Match"] if options.has_key?("Match") - time_out = options["Timeout"] if options.has_key?("Timeout") - else - string = options - end - - self.puts(string) - if block_given? - waitfor({"Prompt" => match, "Timeout" => time_out}){|c| yield c } - else - waitfor({"Prompt" => match, "Timeout" => time_out}) - end - end - - def login(options, password = nil) - if options.kind_of?(Hash) - username = options["Name"] - password = options["Password"] - else - username = options - end - - if block_given? - line = waitfor(/login[: ]*\z/n){|c| yield c } - if password - line.concat( cmd({"String" => username, - "Match" => /Password[: ]*\z/n}){|c| yield c } ) - line.concat( cmd(password){|c| yield c } ) - else - line.concat( cmd(username){|c| yield c } ) - end - else - line = waitfor(/login[: ]*\z/n) - if password - line.concat( cmd({"String" => username, - "Match" => /Password[: ]*\z/n}) ) - line.concat( cmd(password) ) - else - line.concat( cmd(username) ) - end - end - line - end - - end -end - - -=begin - -== HISTORY - -* Mon Dec 25 01:37:43 JST 2000 - wakou - * version 1.6.2 - * Regexp::last_match[1] --> $1 - -* Mon Dec 11 00:16:51 JST 2000 - wakou - * version 1.6.1 - * $1 --> Regexp::last_match[1] - -* 2000/09/12 05:37:35 - matz - * change: iterator? --> block_given? - -* Tue Sep 12 06:52:48 JST 2000 - wakou - * version 1.6.0 - * correct: document. - thanks to Kazuhiro NISHIYAMA <zn@mbf.nifty.com> - * add: Telnet#puts(). - -* Sun Jun 18 23:31:44 JST 2000 - wakou - * version 1.5.0 - * change: version syntax. old: x.yz, now: x.y.z - -* 2000/05/24 06:57:38 - wakou - * version 1.40 - * improve: binmode(), telnetmode() interface. - thanks to Dave Thomas <Dave@thomases.com> - -* 2000/05/09 22:02:56 - wakou - * version 1.32 - * require English.rb - -* 2000/05/02 21:48:39 - wakou - * version 1.31 - * Proxy option: can receive IO object. - -* 2000/04/03 18:27:02 - wakou - * version 1.30 - * telnet.rb --> net/telnet.rb - -* 2000/01/24 17:02:57 - wakou - * version 1.20 - * respond to "IAC WILL x" with "IAC DONT x" - * respond to "IAC WONT x" with "IAC DONT x" - * better dumplog format. - thanks to WATANABE Hirofumi <Hirofumi.Watanabe@jp.sony.com> - -* 2000/01/18 17:47:31 - wakou - * version 1.10 - * bug fix: write method - * respond to "IAC WILL BINARY" with "IAC DO BINARY" - -* 1999/10/04 22:51:26 - wakou - * version 1.00 - * bug fix: waitfor(preprocess) method. - thanks to Shin-ichiro Hara <sinara@blade.nagaokaut.ac.jp> - * add simple support for AO, DM, IP, NOP, SB, SE - * COUTION! TimeOut --> TimeoutError - -* 1999/09/21 21:24:07 - wakou - * version 0.50 - * add write method - -* 1999/09/17 17:41:41 - wakou - * version 0.40 - * bug fix: preprocess method - -* 1999/09/14 23:09:05 - wakou - * version 0.30 - * change prompt check order. - not IO::select([@sock], nil, nil, waittime) and prompt === line - --> prompt === line and not IO::select([@sock], nil, nil, waittime) - -* 1999/09/13 22:28:33 - wakou - * version 0.24 - * Telnet#login: if ommit password, then not require password prompt. - -* 1999/08/10 05:20:21 - wakou - * version 0.232 - * STATUS OUTPUT sample code typo. - thanks to Tadayoshi Funaba <tadf@kt.rim.or.jp> - host = Telnet.new({"Hosh" => "localhost"){|c| print c } - --> host = Telnet.new({"Host" => "localhost"){|c| print c } - -* 1999/07/16 13:39:42 - wakou - * version 0.231 - * TRUE --> true, FALSE --> false - -* 1999/07/15 22:32:09 - wakou - * version 0.23 - * waitfor: if end of file reached, then return nil. - -* 1999/06/29 09:08:51 - wakou - * version 0.22 - * new, waitfor, cmd: {"Timeout" => false} # ignore timeout - -* 1999/06/28 18:18:55 - wakou - * version 0.21 - * waitfor: not rescue (EOFError) - -* 1999/06/04 06:24:58 - wakou - * version 0.20 - * waitfor: support for divided telnet command - -* 1999/05/22 - wakou - * version 0.181 - * bug fix: print method - -* 1999/05/14 - wakou - * version 0.18 - * respond to "IAC WON'T SGA" with "IAC DON'T SGA" - * DON'T SGA : end of line --> CR + LF - * bug fix: preprocess method - -* 1999/04/30 - wakou - * version 0.17 - * bug fix: $! + "\n" --> $!.to_s + "\n" - -* 1999/04/11 - wakou - * version 0.163 - * STDOUT.write(message) --> yield(message) if iterator? - -* 1999/03/17 - wakou - * version 0.162 - * add "Proxy" option - * required timeout.rb - -* 1999/02/03 - wakou - * version 0.161 - * select --> IO::select - -* 1998/10/09 - wakou - * version 0.16 - * preprocess method change for the better - * add binmode method. - * change default Binmode. TRUE --> FALSE - -* 1998/10/04 - wakou - * version 0.15 - * add telnetmode method. - -* 1998/09/22 - wakou - * version 0.141 - * change default prompt. /[$%#>] $/ --> /[$%#>] \Z/ - -* 1998/09/01 - wakou - * version 0.14 - * IAC WILL SGA send EOL --> CR+NULL - * IAC WILL SGA IAC DO BIN send EOL --> CR - * NONE send EOL --> LF - * add Dump_log option. - -* 1998/08/25 - wakou - * version 0.13 - * add print method. - -* 1998/08/05 - wakou - * version 0.122 - * support for HP-UX 10.20. - thanks to WATANABE Tetsuya <tetsu@jpn.hp.com> - * socket.<< --> socket.write - -* 1998/07/15 - wakou - * version 0.121 - * string.+= --> string.concat - -* 1998/06/01 - wakou - * version 0.12 - * add timeout, waittime. - -* 1998/04/21 - wakou - * version 0.11 - * add realtime output. - -* 1998/04/13 - wakou - * version 0.10 - * first release. - -$Date$ -=end |
