diff options
Diffstat (limited to 'lib/net')
| -rw-r--r-- | lib/net/.document | 8 | ||||
| -rw-r--r-- | lib/net/ftp.rb | 978 | ||||
| -rw-r--r-- | lib/net/http.rb | 3998 | ||||
| -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 | 166 | ||||
| -rw-r--r-- | lib/net/imap.rb | 3375 | ||||
| -rw-r--r-- | lib/net/net-protocol.gemspec | 33 | ||||
| -rw-r--r-- | lib/net/pop.rb | 988 | ||||
| -rw-r--r-- | lib/net/protocol.rb | 261 | ||||
| -rw-r--r-- | lib/net/smtp.rb | 1011 | ||||
| -rw-r--r-- | lib/net/telnet.rb | 745 |
20 files changed, 6499 insertions, 9166 deletions
diff --git a/lib/net/.document b/lib/net/.document deleted file mode 100644 index 6332bb9e7e..0000000000 --- a/lib/net/.document +++ /dev/null @@ -1,8 +0,0 @@ -ftp.rb -http.rb -https.rb -imap.rb -pop.rb -smtp.rb -smtps.rb -telnet.rb diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb deleted file mode 100644 index 9b231291af..0000000000 --- a/lib/net/ftp.rb +++ /dev/null @@ -1,978 +0,0 @@ -# -# = net/ftp.rb - FTP Client Library -# -# Written by Shugo Maeda <shugo@ruby-lang.org>. -# -# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas) -# and "Ruby In a Nutshell" (Matsumoto), used with permission. -# -# This library is distributed under the terms of the Ruby license. -# You can freely distribute/modify this library. -# -# It is included in the Ruby standard library. -# -# See the Net::FTP class for an overview. -# - -require "socket" -require "monitor" - -module Net - - # :stopdoc: - class FTPError < StandardError; end - class FTPReplyError < FTPError; end - class FTPTempError < FTPError; end - class FTPPermError < FTPError; end - class FTPProtoError < FTPError; end - # :startdoc: - - # - # This class implements the File Transfer Protocol. If you have used a - # command-line FTP program, and are familiar with the commands, you will be - # able to use this class easily. Some extra features are included to take - # advantage of Ruby's style and strengths. - # - # == Example - # - # require 'net/ftp' - # - # === Example 1 - # - # ftp = Net::FTP.new('ftp.netlab.co.jp') - # ftp.login - # files = ftp.chdir('pub/lang/ruby/contrib') - # files = ftp.list('n*') - # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) - # ftp.close - # - # === Example 2 - # - # Net::FTP.open('ftp.netlab.co.jp') do |ftp| - # ftp.login - # files = ftp.chdir('pub/lang/ruby/contrib') - # files = ftp.list('n*') - # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) - # end - # - # == Major Methods - # - # The following are the methods most likely to be useful to users: - # - FTP.open - # - #getbinaryfile - # - #gettextfile - # - #putbinaryfile - # - #puttextfile - # - #chdir - # - #nlst - # - #size - # - #rename - # - #delete - # - class FTP - include MonitorMixin - - # :stopdoc: - FTP_PORT = 21 - CRLF = "\r\n" - DEFAULT_BLOCKSIZE = 4096 - # :startdoc: - - # When +true+, transfers are performed in binary mode. Default: +true+. - attr_reader :binary - - # When +true+, the connection is in passive mode. Default: +false+. - attr_accessor :passive - - # When +true+, all traffic to and from the server is written - # to +$stdout+. Default: +false+. - attr_accessor :debug_mode - - # Sets or retrieves the +resume+ status, which decides whether incomplete - # transfers are resumed or restarted. Default: +false+. - attr_accessor :resume - - # The server's welcome message. - attr_reader :welcome - - # The server's last response code. - attr_reader :last_response_code - alias lastresp last_response_code - - # The server's last response. - attr_reader :last_response - - # - # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter. - # - # If a block is given, it is passed the +FTP+ object, which will be closed - # when the block finishes, or when an exception is raised. - # - def FTP.open(host, user = nil, passwd = nil, acct = nil) - if block_given? - ftp = new(host, user, passwd, acct) - begin - yield ftp - ensure - ftp.close - end - else - new(host, user, passwd, acct) - end - end - - # - # Creates and returns a new +FTP+ object. If a +host+ is given, a connection - # is made. Additionally, if the +user+ is given, the given user name, - # password, and (optionally) account are used to log in. See #login. - # - def initialize(host = nil, user = nil, passwd = nil, acct = nil) - super() - @binary = false - @passive = false - @debug_mode = false - @resume = false - if host - connect(host) - if user - login(user, passwd, acct) - end - end - end - - def binary=(newmode) - if newmode != @binary - @binary = newmode - @binary ? voidcmd("TYPE I") : voidcmd("TYPE A") - end - end - - def with_binary(newmode) - oldmode = binary - self.binary = newmode - begin - yield - ensure - self.binary = oldmode - end - end - private :with_binary - - # Obsolete - def return_code - $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing") - return "\n" - end - - # Obsolete - def return_code=(s) - $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing") - 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 - - # - # Establishes an FTP connection to host, optionally overriding the default - # port. If the environment variable +SOCKS_SERVER+ is set, sets up the - # connection through a SOCKS proxy. Raises an exception (typically - # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established. - # - 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 - - # - # WRITEME or make private - # - def set_socket(sock, get_greeting = true) - synchronize do - @sock = sock - if get_greeting - voidresp - end - 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 - line.sub!(/(\r\n|\n|\r)\z/n, "") - 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 - @last_response = getmultiline - @last_response_code = @last_response[0, 3] - case @last_response_code - when /\A[123]/ - return @last_response - when /\A4/ - raise FTPTempError, @last_response - when /\A5/ - raise FTPPermError, @last_response - else - raise FTPProtoError, @last_response - end - end - private :getresp - - def voidresp - resp = getresp - if resp[0] != ?2 - raise FTPReplyError, resp - end - end - private :voidresp - - # - # Sends a command and returns the response. - # - def sendcmd(cmd) - synchronize do - putline(cmd) - return getresp - end - end - - # - # Sends a command and expect a response beginning with '2'. - # - 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 - - # - # Logs in to the remote host. The session must have been previously - # connected. If +user+ is the string "anonymous" and the +password+ is - # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+ - # parameter is not +nil+, an FTP ACCT command is sent following the - # successful login. Raises an exception on error (typically - # <tt>Net::FTPPermError</tt>). - # - 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 - self.binary = true - end - - # - # Puts the connection into binary (image) mode, issues the given command, - # and fetches the data returned, passing it to the associated block in - # chunks of +blocksize+ characters. Note that +cmd+ is a server command - # (such as "RETR myfile"). - # - def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data - synchronize do - with_binary(true) do - conn = transfercmd(cmd, rest_offset) - loop do - data = conn.read(blocksize) - break if data == nil - yield(data) - end - conn.close - voidresp - end - end - end - - # - # Puts the connection into ASCII (text) mode, issues the given command, and - # passes the resulting data, one line at a time, to the associated block. If - # no block is given, prints the lines. Note that +cmd+ is a server command - # (such as "RETR myfile"). - # - def retrlines(cmd) # :yield: line - synchronize do - with_binary(false) do - 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 - yield(line) - end - conn.close - voidresp - end - end - end - - # - # Puts the connection into binary (image) mode, issues the given server-side - # command (such as "STOR myfile"), and sends the contents of the file named - # +file+ to the server. If the optional block is given, it also passes it - # the data, in chunks of +blocksize+ characters. - # - def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data - if rest_offset - file.seek(rest_offset, IO::SEEK_SET) - end - synchronize do - with_binary(true) do - conn = transfercmd(cmd, rest_offset) - loop do - buf = file.read(blocksize) - break if buf == nil - conn.write(buf) - yield(buf) if block - end - conn.close - voidresp - end - end - rescue Errno::EPIPE - # EPIPE, in this case, means that the data connection was unexpectedly - # terminated. Rather than just raising EPIPE to the caller, check the - # response on the control connection. If getresp doesn't raise a more - # appropriate exception, re-raise the original exception. - getresp - raise - end - - # - # Puts the connection into ASCII (text) mode, issues the given server-side - # command (such as "STOR myfile"), and sends the contents of the file - # named +file+ to the server, one line at a time. If the optional block is - # given, it also passes it the lines. - # - def storlines(cmd, file, &block) # :yield: line - synchronize do - with_binary(false) do - 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) - yield(buf) if block - end - conn.close - voidresp - end - end - rescue Errno::EPIPE - # EPIPE, in this case, means that the data connection was unexpectedly - # terminated. Rather than just raising EPIPE to the caller, check the - # response on the control connection. If getresp doesn't raise a more - # appropriate exception, re-raise the original exception. - getresp - raise - end - - # - # Retrieves +remotefile+ in binary mode, storing the result in +localfile+. - # If +localfile+ is nil, returns retrieved data. - # If a block is supplied, it is passed the retrieved data in +blocksize+ - # chunks. - # - def getbinaryfile(remotefile, localfile = File.basename(remotefile), - blocksize = DEFAULT_BLOCKSIZE) # :yield: data - result = nil - if localfile - if @resume - rest_offset = File.size?(localfile) - f = open(localfile, "a") - else - rest_offset = nil - f = open(localfile, "w") - end - elsif !block_given? - result = "" - end - begin - f.binmode if localfile - retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data| - f.write(data) if localfile - yield(data) if block_given? - result.concat(data) if result - end - return result - ensure - f.close if localfile - end - end - - # - # Retrieves +remotefile+ in ASCII (text) mode, storing the result in - # +localfile+. - # If +localfile+ is nil, returns retrieved data. - # If a block is supplied, it is passed the retrieved data one - # line at a time. - # - def gettextfile(remotefile, localfile = File.basename(remotefile)) # :yield: line - result = nil - if localfile - f = open(localfile, "w") - elsif !block_given? - result = "" - end - begin - retrlines("RETR " + remotefile) do |line| - f.puts(line) if localfile - yield(line) if block_given? - result.concat(line + "\n") if result - end - return result - ensure - f.close if localfile - end - end - - # - # Retrieves +remotefile+ in whatever mode the session is set (text or - # binary). See #gettextfile and #getbinaryfile. - # - def get(remotefile, localfile = File.basename(remotefile), - blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data - if @binary - getbinaryfile(remotefile, localfile, blocksize, &block) - else - gettextfile(remotefile, localfile, &block) - end - end - - # - # Transfers +localfile+ to the server in binary mode, storing the result in - # +remotefile+. If a block is supplied, calls it, passing in the transmitted - # data in +blocksize+ chunks. - # - def putbinaryfile(localfile, remotefile = File.basename(localfile), - blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data - if @resume - begin - rest_offset = size(remotefile) - rescue Net::FTPPermError - rest_offset = nil - end - else - rest_offset = nil - end - f = open(localfile) - begin - f.binmode - storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block) - ensure - f.close - end - end - - # - # Transfers +localfile+ to the server in ASCII (text) mode, storing the result - # in +remotefile+. If callback or an associated block is supplied, calls it, - # passing in the transmitted data one line at a time. - # - def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line - f = open(localfile) - begin - storlines("STOR " + remotefile, f, &block) - ensure - f.close - end - end - - # - # Transfers +localfile+ to the server in whatever mode the session is set - # (text or binary). See #puttextfile and #putbinaryfile. - # - def put(localfile, remotefile = File.basename(localfile), - blocksize = DEFAULT_BLOCKSIZE, &block) - if @binary - putbinaryfile(localfile, remotefile, blocksize, &block) - else - puttextfile(localfile, remotefile, &block) - end - end - - # - # Sends the ACCT command. TODO: more info. - # - def acct(account) - cmd = "ACCT " + account - voidcmd(cmd) - end - - # - # Returns an array of filenames in the remote directory. - # - def nlst(dir = nil) - cmd = "NLST" - if dir - cmd = cmd + " " + dir - end - files = [] - retrlines(cmd) do |line| - files.push(line) - end - return files - end - - # - # Returns an array of file information in the directory (the output is like - # `ls -l`). If a block is given, it iterates through the listing. - # - def list(*args, &block) # :yield: line - 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 - - # - # Renames a file on the server. - # - def rename(fromname, toname) - resp = sendcmd("RNFR " + fromname) - if resp[0] != ?3 - raise FTPReplyError, resp - end - voidcmd("RNTO " + toname) - end - - # - # Deletes a file on the server. - # - 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 - - # - # Changes the (remote) directory. - # - 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 - - # - # Returns the size of the given (remote) filename. - # - def size(filename) - with_binary(true) do - resp = sendcmd("SIZE " + filename) - if resp[0, 3] != "213" - raise FTPReplyError, resp - end - return resp[3..-1].strip.to_i - end - end - - MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ # :nodoc: - - # - # Returns the last modification time of the (remote) file. If +local+ is - # +true+, it is returned as a local time, otherwise it's a UTC time. - # - 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 - - # - # Creates a remote directory. - # - def mkdir(dirname) - resp = sendcmd("MKD " + dirname) - return parse257(resp) - end - - # - # Removes a remote directory. - # - def rmdir(dirname) - voidcmd("RMD " + dirname) - end - - # - # Returns the current remote directory. - # - def pwd - resp = sendcmd("PWD") - return parse257(resp) - end - alias getdir pwd - - # - # Returns system information. - # - def system - resp = sendcmd("SYST") - if resp[0, 3] != "215" - raise FTPReplyError, resp - end - return resp[4 .. -1] - end - - # - # Aborts the previous command (ABOR command). - # - 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 - - # - # Returns the status (STAT command). - # - def status - line = "STAT" + CRLF - print "put: STAT\n" if @debug_mode - @sock.send(line, Socket::MSG_OOB) - return getresp - end - - # - # Issues the MDTM command. TODO: more info. - # - def mdtm(filename) - resp = sendcmd("MDTM " + filename) - if resp[0, 3] == "213" - return resp[3 .. -1].strip - end - end - - # - # Issues the HELP command. - # - def help(arg = nil) - cmd = "HELP" - if arg - cmd = cmd + " " + arg - end - sendcmd(cmd) - end - - # - # Exits the FTP session. - # - def quit - voidcmd("QUIT") - end - - # - # Issues a NOOP command. - # - def noop - voidcmd("NOOP") - end - - # - # Issues a SITE command. - # - def site(arg) - cmd = "SITE " + arg - voidcmd(cmd) - end - - # - # Closes the connection. Further operations are impossible until you open - # a new connection with #connect. - # - def close - @sock.close if @sock and not @sock.closed? - end - - # - # Returns +true+ iff the connection is closed. - # - 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 - - -# Documentation comments: -# - sourced from pickaxe and nutshell, with improvements (hopefully) -# - three methods should be private (search WRITEME) -# - two methods need more information (search TODO) diff --git a/lib/net/http.rb b/lib/net/http.rb index 62b951db1a..98d6793aee 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1,32 +1,29 @@ +# 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. -# -# NOTE: You can find Japanese version of this document here: -# http://www.ruby-lang.org/ja/man/?cmd=view;name=net%2Fhttp.rb -# -#-- -# $Id$ -#++ +# See Net::HTTP for an overview and examples. +# require 'net/protocol' require 'uri' +require 'resolv' +autoload :OpenSSL, 'openssl' module Net #:nodoc: @@ -35,288 +32,721 @@ module Net #:nodoc: class HTTPHeaderSyntaxError < StandardError; end # :startdoc: - # == What Is This Library? - # - # This library provides your program functions to access WWW - # documents via HTTP, Hyper Text Transfer Protocol version 1.1. - # For details of HTTP, refer [RFC2616] - # (http://www.ietf.org/rfc/rfc2616.txt). - # - # == Examples - # - # === Getting Document From WWW Server - # - # Example #1: Simple GET+print - # - # require 'net/http' - # Net::HTTP.get_print 'www.example.com', '/index.html' - # - # Example #2: Simple GET+print by URL - # - # require 'net/http' - # require 'uri' - # Net::HTTP.get_print URI.parse('http://www.example.com/index.html') - # - # Example #3: More generic GET+print - # - # require 'net/http' - # require 'uri' - # - # url = URI.parse('http://www.example.com/index.html') - # res = Net::HTTP.start(url.host, url.port) {|http| - # http.get('/index.html') - # } - # puts res.body - # - # Example #4: More generic GET+print - # - # require 'net/http' - # - # url = URI.parse('http://www.example.com/index.html') - # req = Net::HTTP::Get.new(url.path) - # res = Net::HTTP.start(url.host, url.port) {|http| - # http.request(req) - # } - # puts res.body - # - # === Posting Form Data - # - # require 'net/http' - # require 'uri' - # - # #1: Simple POST - # res = Net::HTTP.post_form(URI.parse('http://www.example.com/search.cgi'), - # {'q' => 'ruby', 'max' => '50'}) - # puts res.body - # - # #2: POST with basic authentication - # res = Net::HTTP.post_form(URI.parse('http://jack:pass@www.example.com/todo.cgi'), - # {'from' => '2005-01-01', - # 'to' => '2005-03-31'}) - # puts res.body - # - # #3: Detailed control - # url = URI.parse('http://www.example.com/todo.cgi') - # req = Net::HTTP::Post.new(url.path) - # req.basic_auth 'jack', 'pass' - # req.set_form_data({'from' => '2005-01-01', 'to' => '2005-03-31'}, ';') - # res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) } + # \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, Net::HTTPRedirection - # # OK - # else - # res.error! + # 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 # - # #4: Multiple values - # res = Net::HTTP.post_form(URI.parse('http://www.example.com/search.cgi'), - # {'q' => ['ruby', 'perl'], 'max' => '50'}) - # puts res.body - # - # === Accessing via Proxy - # - # Net::HTTP.Proxy creates http proxy class. It has same - # methods of Net::HTTP but its instances always connect to - # proxy, instead of given host. - # - # require 'net/http' - # - # proxy_addr = 'your.proxy.host' - # proxy_port = 8080 - # : - # Net::HTTP::Proxy(proxy_addr, proxy_port).start('www.example.com') {|http| - # # always connect to your.proxy.addr:8080 - # : - # } - # - # Since Net::HTTP.Proxy returns Net::HTTP itself when proxy_addr is nil, - # there's no need to change code if there's proxy or not. - # - # There are two additional parameters in Net::HTTP.Proxy which allow to - # specify proxy user name and password: - # - # Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user = nil, proxy_pass = nil) - # - # You may use them to work with authorization-enabled proxies: - # - # require 'net/http' - # require 'uri' - # - # proxy_host = 'your.proxy.host' - # proxy_port = 8080 - # uri = URI.parse(ENV['http_proxy']) - # proxy_user, proxy_pass = uri.userinfo.split(/:/) if uri.userinfo - # Net::HTTP::Proxy(proxy_host, proxy_port, - # proxy_user, proxy_pass).start('www.example.com') {|http| - # # always connect to your.proxy.addr:8080 using specified username and password - # : - # } - # - # Note that net/http never rely on HTTP_PROXY environment variable. - # If you want to use proxy, set it explicitly. - # - # === Following Redirection - # - # require 'net/http' - # require 'uri' - # - # def fetch(uri_str, limit = 10) - # # You should choose better exception. - # raise ArgumentError, 'HTTP redirect too deep' if limit == 0 - # - # response = Net::HTTP.get_response(URI.parse(uri_str)) - # case response - # when Net::HTTPSuccess then response - # when Net::HTTPRedirection then fetch(response['location'], limit - 1) - # else - # response.error! + # 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 - # - # print fetch('http://www.ruby-lang.org') - # - # Net::HTTPSuccess and Net::HTTPRedirection is a HTTPResponse class. - # All HTTPResponse objects belong to its own response class which - # indicate HTTP result status. For details of response classes, - # see section "HTTP Response Classes". - # - # === Basic Authentication - # - # require 'net/http' - # - # Net::HTTP.start('www.example.com') {|http| - # req = Net::HTTP::Get.new('/secret-page.html') - # req.basic_auth 'account', 'password' - # response = http.request(req) - # print response.body - # } - # - # === HTTP Request Classes - # - # Here is HTTP request class hierarchy. - # - # Net::HTTPRequest - # Net::HTTP::Get - # Net::HTTP::Head - # Net::HTTP::Post - # Net::HTTP::Put - # Net::HTTP::Proppatch - # Net::HTTP::Lock - # Net::HTTP::Unlock - # Net::HTTP::Options - # Net::HTTP::Propfind - # Net::HTTP::Delete - # Net::HTTP::Move - # Net::HTTP::Copy - # Net::HTTP::Mkcol - # Net::HTTP::Trace - # - # === HTTP Response Classes - # - # Here is HTTP response class hierarchy. - # All classes are defined in Net module. - # - # HTTPResponse - # HTTPUnknownResponse - # HTTPInformation # 1xx - # HTTPContinue # 100 - # HTTPSwitchProtocl # 101 - # HTTPSuccess # 2xx - # HTTPOK # 200 - # HTTPCreated # 201 - # HTTPAccepted # 202 - # HTTPNonAuthoritativeInformation # 203 - # HTTPNoContent # 204 - # HTTPResetContent # 205 - # HTTPPartialContent # 206 - # HTTPRedirection # 3xx - # HTTPMultipleChoice # 300 - # HTTPMovedPermanently # 301 - # HTTPFound # 302 - # HTTPSeeOther # 303 - # HTTPNotModified # 304 - # HTTPUseProxy # 305 - # HTTPTemporaryRedirect # 307 - # HTTPClientError # 4xx - # HTTPBadRequest # 400 - # HTTPUnauthorized # 401 - # HTTPPaymentRequired # 402 - # HTTPForbidden # 403 - # HTTPNotFound # 404 - # HTTPMethodNotAllowed # 405 - # HTTPNotAcceptable # 406 - # HTTPProxyAuthenticationRequired # 407 - # HTTPRequestTimeOut # 408 - # HTTPConflict # 409 - # HTTPGone # 410 - # HTTPLengthRequired # 411 - # HTTPPreconditionFailed # 412 - # HTTPRequestEntityTooLarge # 413 - # HTTPRequestURITooLong # 414 - # HTTPUnsupportedMediaType # 415 - # HTTPRequestedRangeNotSatisfiable # 416 - # HTTPExpectationFailed # 417 - # HTTPServerError # 5xx - # HTTPInternalServerError # 500 - # HTTPNotImplemented # 501 - # HTTPBadGateway # 502 - # HTTPServiceUnavailable # 503 - # HTTPGatewayTimeOut # 504 - # HTTPVersionNotSupported # 505 - # - # == Switching Net::HTTP versions - # - # You can use net/http.rb 1.1 features (bundled with Ruby 1.6) - # by calling HTTP.version_1_1. Calling Net::HTTP.version_1_2 - # allows you to use 1.2 features again. - # - # # example - # Net::HTTP.start {|http1| ...(http1 has 1.2 features)... } - # - # Net::HTTP.version_1_1 - # Net::HTTP.start {|http2| ...(http2 has 1.1 features)... } - # - # Net::HTTP.version_1_2 - # Net::HTTP.start {|http3| ...(http3 has 1.2 features)... } - # - # This function is NOT thread-safe. + # 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 # :stopdoc: - Revision = %q$Revision$.split[1] + VERSION = "0.9.1" HTTPVersion = '1.1' - @newimpl = true + begin + require 'zlib' + HAVE_ZLIB=true + rescue LoadError + HAVE_ZLIB=false + end # :startdoc: - # Turns on net/http 1.2 (ruby 1.8) features. - # Defaults to ON in ruby 1.8. - # - # I strongly recommend to call this method always. - # - # require 'net/http' - # Net::HTTP.version_1_2 - # + # Returns +true+; retained for compatibility. def HTTP.version_1_2 - @newimpl = true - end - - # Turns on net/http 1.1 (ruby 1.6) features. - # Defaults to OFF in ruby 1.8. - def HTTP.version_1_1 - @newimpl = false + true end - # true if net/http is in version 1.2 mode. - # Defaults to true. + # Returns +true+; retained for compatibility. def HTTP.version_1_2? - @newimpl + true end - # true if net/http is in version 1.1 compatible mode. - # Defaults to true. - def HTTP.version_1_1? - not @newimpl + # Returns +false+; retained for compatibility. + def HTTP.version_1_1? #:nodoc: + false end class << HTTP @@ -324,23 +754,14 @@ module Net #:nodoc: alias is_version_1_2? version_1_2? #:nodoc: end + # :call-seq: + # Net::HTTP.get_print(hostname, path, port = 80) -> nil + # Net::HTTP:get_print(uri, headers = {}, port = uri.port) -> nil # - # short cut methods - # - - # - # Get body from target and output it to +$stdout+. The - # target can either be specified as (+uri+), or as - # (+host+, +path+, +port+ = 80); so: - # - # Net::HTTP.get_print URI.parse('http://www.example.com/index.html') - # - # or: - # - # Net::HTTP.get_print 'www.example.com', '/index.html' - # - def HTTP.get_print(uri_or_host, path = nil, port = nil) - get_response(uri_or_host, path, port) {|res| + # 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 @@ -348,85 +769,185 @@ module Net #:nodoc: nil end - # Send a GET request to the target and return the response - # as a string. The target can either be specified as - # (+uri+), or as (+host+, +path+, +port+ = 80); so: - # - # print Net::HTTP.get(URI.parse('http://www.example.com/index.html')) + # :call-seq: + # Net::HTTP.get(hostname, path, port = 80) -> body + # Net::HTTP:get(uri, headers = {}, port = uri.port) -> body # - # or: + # Sends a GET request and returns the \HTTP response body as a string. # - # print Net::HTTP.get('www.example.com', '/index.html') + # With string arguments +hostname+ and +path+: # - def HTTP.get(uri_or_host, path = nil, port = nil) - get_response(uri_or_host, path, port).body - end - - # Send a GET request to the target and return the response - # as a Net::HTTPResponse object. The target can either be specified as - # (+uri+), or as (+host+, +path+, +port+ = 80); so: - # - # res = Net::HTTP.get_response(URI.parse('http://www.example.com/index.html')) - # print res.body + # 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: # - # or: + # - Net::HTTP::Get: request class for \HTTP method +GET+. + # - Net::HTTP#get: convenience method for \HTTP method +GET+. # - # res = Net::HTTP.get_response('www.example.com', '/index.html') - # print res.body + 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 # - def HTTP.get_response(uri_or_host, path = nil, port = nil, &block) - if path + # 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 - new(uri.host, uri.port).start {|http| - return http.request_get(uri.request_uri, &block) + 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 HTML form data to the +URL+. - # Form data must be represented as a Hash of String to String, e.g: + # Posts data to a host; returns a Net::HTTPResponse object. # - # { "cmd" => "search", "q" => "ruby", "max" => "50" } + # Argument +url+ must be a URL; + # argument +data+ must be a string: # - # This method also does Basic Authentication iff +URL+.user exists. + # _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 # - # Example: + # 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+. # - # require 'net/http' - # require 'uri' + def HTTP.post(url, data, header = nil) + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.post(url, data, header) + } + end + + # Posts data to a host; returns a Net::HTTPResponse object. + # + # Argument +url+ must be a URI; + # argument +data+ must be a hash: # - # HTTP.post_form URI.parse('http://www.example.com/search.cgi'), - # { "q" => "ruby", "max" => "50" } + # _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.path) + req = Post.new(url) req.form_data = params req.basic_auth url.user, url.password if url.user - new(url.host, url.port).start {|http| + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| http.request(req) } end + # Sends a PUT request to the server; returns a Net::HTTPResponse object. # - # HTTP session management + # 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 + + # + # \HTTP session management # - # The default port to use for HTTP requests; defaults to 80. + # Returns integer +80+, the default port to use for \HTTP requests: + # + # Net::HTTP.default_port # => 80 + # def HTTP.default_port http_default_port() end - # The default port to use for HTTP requests; defaults to 80. + # Returns integer +80+, the default port to use for \HTTP requests: + # + # Net::HTTP.http_default_port # => 80 + # def HTTP.http_default_port 80 end - # The default port to use for HTTPS requests; defaults to 443. + # Returns integer +443+, the default port to use for HTTPS requests: + # + # Net::HTTP.https_default_port # => 443 + # def HTTP.https_default_port 443 end @@ -435,111 +956,671 @@ module Net #:nodoc: BufferedIO end - # creates a new Net::HTTP object and opens its TCP connection and - # HTTP session. If the optional block is given, the newly - # created Net::HTTP object is passed to it and closed when the - # block finishes. In this case, the return value of this method - # is the return value of the block. If no block is given, the - # return value of this method is the newly created Net::HTTP object - # itself, and the caller is responsible for closing it upon completion. - def HTTP.start(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil, &block) # :yield: +http+ - new(address, port, p_addr, p_port, p_user, p_pass).start(&block) + # :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 + + http.start(&block) end class << HTTP - alias newobj new + alias newobj new # :nodoc: end - # Creates a new Net::HTTP object. - # If +proxy_addr+ is given, creates an Net::HTTP object with proxy support. - # This method does not open the TCP connection. - def HTTP.new(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil) - h = Proxy(p_addr, p_port, p_user, p_pass).newobj(address, port) - h.instance_eval { - @newimpl = ::Net::HTTP.version_1_2? - } - h + # 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 + + http end - # Creates a new Net::HTTP object for the specified +address+. - # This method does not open the TCP connection. - def initialize(address, port = nil) + 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 || {}) + @address = address @port = (port || HTTP.default_port) + @ipaddr = nil + @local_host = nil + @local_port = nil @curr_http_version = HTTPVersion - @no_keepalive_server = false - @close_on_empty_response = false + @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 = nil - @read_timeout = 60 - @debug_output = nil + @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 causes serious security hole. + # *WARNING* This method opens a serious security hole. # Never use this method in production code. # - # Set an output stream for debugging. - # - # http = Net::HTTP.new - # http.set_debug_output $stderr - # http.start { .... } + # 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' if started? + warn 'Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started? @debug_output = output end - # The host name to connect to. + # Returns the string host name or host IP given as argument +address+ in ::new. attr_reader :address - # The port number to connect to. + # Returns the integer port number given as argument +port+ in ::new. attr_reader :port - # Seconds to wait until connection is opened. - # If the HTTP object cannot open a connection in this many seconds, - # it raises a TimeoutError exception. + # 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 - # Seconds to wait until reading one block (by one read(2) call). - # If the HTTP object cannot open a connection in this many seconds, - # it raises a TimeoutError exception. + # 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 - # Setter for the read_timeout attribute. + # 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 - # returns true if the HTTP session is started. + # 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 use SSL/TLS with HTTP. + # Returns +true+ if +self+ uses SSL, +false+ otherwise. + # See Net::HTTP#use_ssl=. def use_ssl? - false # redefined in net/https + @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 - # Opens TCP connection and HTTP session. - # - # When this method is called with block, gives a HTTP object - # to the block and closes the TCP connection / HTTP session - # after the block executed. + # 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: # - # When called with a block, returns the return value of the - # block; otherwise, returns self. + # 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 @@ -555,6 +1636,21 @@ module Net #:nodoc: self end + # 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 @@ -562,53 +1658,150 @@ module Net #:nodoc: private :do_start def connect - D "opening connection to #{conn_address()}..." - s = timeout(@open_timeout) { TCPSocket.open(conn_address(), conn_port()) } - D "opened" if use_ssl? - unless @ssl_context.verify_mode - warn "warning: peer certificate won't be verified in this SSL session" - @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + # reference early to load OpenSSL before connecting, + # as OpenSSL may take time to load. + @ssl_context = OpenSSL::SSL::SSLContext.new + end + + if proxy? then + conn_addr = proxy_address + conn_port = proxy_port + else + conn_addr = conn_address + conn_port = port + end + + 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 - s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) - s.sync_close = true + raise e, "Failed to open TCP connection to " + + "#{conn_addr}:#{conn_port} (#{e.message})" end - @socket = BufferedIO.new(s) - @socket.read_timeout = @read_timeout - @socket.debug_output = @debug_output + s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + debug "opened" if use_ssl? if proxy? - @socket.writeline sprintf('CONNECT %s:%s HTTP/%s', - @address, @port, HTTPVersion) - @socket.writeline "Host: #{@address}:#{@port}" + if @proxy_use_ssl + proxy_sock = OpenSSL::SSL::SSLSocket.new(s) + ssl_socket_connect(proxy_sock, @open_timeout) + else + 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('m') - credential.delete!("\r\n") - @socket.writeline "Proxy-Authorization: Basic #{credential}" + 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 + + 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 - @socket.writeline '' - HTTPResponse.read_new(@socket).value end - s.connect + @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 + + # Still do the post_connection_check below even if connecting + # to IP address + verify_hostname = @ssl_context.verify_hostname + + # 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 + + 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 + + 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 + @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 + raise end private :connect - def on_connect + 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 :on_connect + private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT - # Finishes HTTP session and closes TCP connection. - # Raises IOError if not started. - def finish - raise IOError, 'HTTP session not yet started' unless started? - do_finish + 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 + Timeout.timeout(@open_timeout, Net::OpenTimeout) { + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) + } + end end + private :timeouted_connect + + def on_connect + end + private :on_connect def do_finish @started = false - @socket.close if @socket and not @socket.closed? + @socket.close if @socket @socket = nil end private :do_finish @@ -621,113 +1814,166 @@ module Net #:nodoc: # 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. - # Arguments are address/port of proxy host and username/password - # if authorization on proxy server is required. - # You can replace the HTTP class with created proxy class. - # - # If ADDRESS is nil, this method returns self (Net::HTTP). - # - # # Example - # proxy_class = Net::HTTP::Proxy('proxy.example.com', 8080) - # : - # proxy_class.start('www.ruby-lang.org') {|http| - # # connecting proxy.foo.org:8080 - # : - # } - # - def HTTP.Proxy(p_addr, p_port = nil, p_user = nil, p_pass = 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 - delta = ProxyDelta - proxyclass = Class.new(self) - proxyclass.module_eval { - include delta - # with proxy + + Class.new(self) { @is_proxy_class = true - @proxy_address = p_addr - @proxy_port = p_port || default_port() - @proxy_user = p_user - @proxy_pass = p_pass + + 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 || default_port + end + + @proxy_user = p_user + @proxy_pass = p_pass + @proxy_use_ssl = p_use_ssl } - proxyclass end + # :startdoc: + class << HTTP - # returns true if self is a class which was created by HTTP::Proxy. + # Returns true if self is a class which was created by HTTP::Proxy. def proxy_class? - @is_proxy_class + defined?(@is_proxy_class) ? @is_proxy_class : false end + # Returns the address of the proxy host, or +nil+ if none; + # see Net::HTTP@Proxy+Server. attr_reader :proxy_address + + # Returns the port number of the proxy host, or +nil+ if none; + # see Net::HTTP@Proxy+Server. attr_reader :proxy_port + + # Returns the user name for accessing the proxy, or +nil+ if none; + # see Net::HTTP@Proxy+Server. attr_reader :proxy_user + + # Returns the password for accessing the proxy, or +nil+ if none; + # see Net::HTTP@Proxy+Server. attr_reader :proxy_pass + + # Use SSL when talking to the proxy. If Net::HTTP does not use a proxy, nil. + attr_reader :proxy_use_ssl end - # True if self is a HTTP proxy class. + # Returns +true+ if a proxy server is defined, +false+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. def proxy? - self.class.proxy_class? + !!(@proxy_from_env ? proxy_uri : @proxy_address) + end + + # 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 - # Address of proxy host. If self does not use a proxy, nil. + # 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 + + # Returns the address of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. def proxy_address - self.class.proxy_address + if @proxy_from_env then + proxy_uri&.hostname + else + @proxy_address + end end - # Port number of proxy host. If self does not use a proxy, nil. + # Returns the port number of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. def proxy_port - self.class.proxy_port + if @proxy_from_env then + proxy_uri&.port + else + @proxy_port + end end - # User name for accessing proxy. If self does not use a proxy, nil. + # Returns the user name of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. def proxy_user - self.class.proxy_user + if @proxy_from_env + user = proxy_uri&.user + unescape(user) if user + else + @proxy_user + end end - # User password for accessing proxy. If self does not use a proxy, nil. + # Returns the password of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. def proxy_pass - self.class.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 unescape(value) + require 'cgi/escape' + require 'cgi/util' unless defined?(CGI::EscapeExt) + CGI.unescape(value) + end - # without proxy + # without proxy, obsolete - def conn_address - address() + def conn_address # :nodoc: + @ipaddr || address() end - def conn_port + def conn_port # :nodoc: port() end def edit_path(path) - path - end - - module 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}" + if proxy? + if path.start_with?("ftp://") || use_ssl? + path + else + "http://#{addr_port}#{path}" + end + else + path end end + # :startdoc: # # HTTP operations @@ -735,266 +1981,356 @@ module Net #:nodoc: public - # Gets data from +path+ on the connected-to host. - # +header+ must be a Hash like { 'Accept' => '*/*', ... }. + # :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+. # - # In version 1.1 (ruby 1.6), this method returns a pair of objects, - # a Net::HTTPResponse object and the entity body string. - # In version 1.2 (ruby 1.8), this method returns a Net::HTTPResponse - # object. + # With a block given, calls the block with the response body: # - # If called with a block, yields each fragment of the - # entity body in turn as a string as it is read from - # the socket. Note that in this case, the returned response - # object will *not* contain a (meaningful) body. + # http = Net::HTTP.new(hostname) + # http.get('/todos/1') do |res| + # p res + # end # => #<Net::HTTPOK 200 OK readbody=true> # - # +dest+ argument is obsolete. - # It still works but you must not use it. + # Output: # - # In version 1.1, this method might raise an exception for - # 3xx (redirect). In this case you can get a HTTPResponse object - # by "anException.response". + # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}" # - # In version 1.2, this method never raises exception. + # With no block given, simply returns the response object: # - # # version 1.1 (bundled with Ruby 1.6) - # response, body = http.get('/index.html') + # http.get('/') # => #<Net::HTTPOK 200 OK readbody=true> # - # # version 1.2 (bundled with Ruby 1.8 or later) - # response = http.get('/index.html') - # - # # using block - # File.open('result.txt', 'w') {|f| - # http.get('/~foo/') do |str| - # f.write str - # end - # } + # 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 } - unless @newimpl - res.value - return res, res.body - end - res end - # Gets only the header from +path+ on the connected-to host. - # +header+ is a Hash like { 'Accept' => '*/*', ... }. - # - # This method returns a Net::HTTPResponse object. - # - # In version 1.1, this method might raise an exception for - # 3xx (redirect). On the case you can get a HTTPResponse object - # by "anException.response". - # In version 1.2, this method never raises an exception. - # - # response = nil - # Net::HTTP.start('some.www.server', 80) {|http| - # response = http.head('/index.html') - # } - # p response['content-type'] - # - def head(path, initheader = nil) - res = request(Head.new(path, initheader)) - res.value unless @newimpl - res + # 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 - # Posts +data+ (must be a String) to +path+. +header+ must be a Hash - # like { 'Accept' => '*/*', ... }. - # - # In version 1.1 (ruby 1.6), this method returns a pair of objects, a - # Net::HTTPResponse object and an entity body string. - # In version 1.2 (ruby 1.8), this method returns a Net::HTTPResponse object. - # - # If called with a block, yields each fragment of the - # entity body in turn as a string as it are read from - # the socket. Note that in this case, the returned response - # object will *not* contain a (meaningful) body. - # - # +dest+ argument is obsolete. - # It still works but you must not use it. - # - # In version 1.1, this method might raise an exception for - # 3xx (redirect). In this case you can get an HTTPResponse object - # by "anException.response". - # In version 1.2, this method never raises exception. - # - # # version 1.1 - # response, body = http.post('/cgi-bin/search.rb', 'query=foo') - # - # # version 1.2 - # response = http.post('/cgi-bin/search.rb', 'query=foo') - # - # # using block - # File.open('result.txt', 'w') {|f| - # http.post('/cgi-bin/search.rb', 'query=foo') do |str| - # f.write str - # end - # } - # - # You should set Content-Type: header field for POST. - # If no Content-Type: field given, this method uses - # "application/x-www-form-urlencoded" by default. + # :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+ - res = nil - request(Post.new(path, initheader), data) {|r| - r.read_body dest, &block - res = r - } - unless @newimpl - res.value - return res, res.body - end - res + send_entity(path, data, initheader, dest, Post, &block) end - def put(path, data, initheader = nil) #:nodoc: - res = request(Put.new(path, initheader), data) - res.value unless @newimpl - res + # :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 PROPPATCH request to the +path+ and gets a response, - # as an HTTPResponse object. + # 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 +path+ and gets a response, - # as an HTTPResponse object. + # 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 a UNLOCK request to the +path+ and gets a response, - # as an HTTPResponse object. + # 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 a OPTIONS request to the +path+ and gets a response, - # as an HTTPResponse object. + # 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 +path+ and gets a response, - # as an HTTPResponse object. + # 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 +path+ and gets a response, - # as an HTTPResponse object. + # 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 +path+ and gets a response, - # as an HTTPResponse object. + # 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 +path+ and gets a response, - # as an HTTPResponse object. + # 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 +path+ and gets a response, - # as an HTTPResponse object. + # 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 +path+ and gets a response, - # as an HTTPResponse object. + # 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 +path+ and gets a response, - # as an HTTPResponse object. - # - # When called with a block, yields an HTTPResponse object. - # The body of this response will not have been read yet; - # the caller can process it using HTTPResponse#read_body, - # if desired. - # - # Returns the response. - # - # This method never raises Net::* exceptions. - # - # response = http.request_get('/index.html') - # # The entity body is already read here. - # p response['content-type'] - # puts response.body - # - # # using block - # http.request_get('/index.html') {|response| - # p response['content-type'] - # response.read_body do |str| # read body now - # print str - # 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 +path+ and gets a response, - # as an HTTPResponse object. + # 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+. # - # Returns the response. - # - # This method never raises Net::* exceptions. - # - # response = http.request_head('/index.html') - # p response['content-type'] + # 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 +path+ and gets a response, - # as an HTTPResponse object. - # - # When called with a block, yields an HTTPResponse object. - # The body of this response will not have been read yet; - # the caller can process it using HTTPResponse#read_body, - # if desired. - # - # Returns the response. - # - # This method never raises Net::* exceptions. - # - # # example - # response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...') - # p response.status - # puts response.body # body is already read - # - # # using block - # http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response| - # p response.status - # p response['content-type'] - # response.read_body do |str| # read body now - # print str - # 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 @@ -1004,34 +2340,61 @@ module Net #:nodoc: alias post2 request_post #:nodoc: obsolete alias put2 request_put #:nodoc: obsolete - - # Sends an HTTP request to the HTTP server. - # This method also sends DATA string if DATA is given. + # Sends an \HTTP request to the server; + # returns an instance of a subclass of Net::HTTPResponse. # - # Returns a HTTPResponse object. - # - # This method never raises Net::* exceptions. + # 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]. # - # response = http.send_request('GET', '/index.html') - # puts response.body + # 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) - r = HTTPGenericRequest.new(name,(data ? true : false),true,path,header) + has_response_body = name != 'HEAD' + r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header) request r, data end - # Sends an HTTPRequest object REQUEST to the HTTP server. - # This method also sends DATA string if REQUEST is a post/put request. - # Giving DATA for get/head request causes ArgumentError. - # - # When called with a block, yields an HTTPResponse object. - # The body of this response will not have been read yet; - # the caller can process it using HTTPResponse#read_body, - # if desired. + # 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: # - # Returns a HTTPResponse object. - # - # This method never raises Net::* exceptions. + # #<Net::HTTPOK 200 OK readbody=false> # def request(req, body = nil, &block) # :yield: +response+ unless started? @@ -1054,38 +2417,112 @@ module Net #:nodoc: private + # 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: + + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc: + def transport_request(req) - begin_transport req - req.exec @socket, @curr_http_version, edit_path(req.path) + count = 0 begin - res = HTTPResponse.read_new(@socket) - end while res.kind_of?(HTTPContinue) - res.reading_body(@socket, req.response_body_permitted?) { - yield res if block_given? - } + 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) + + res.uri = req.uri + + 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 + 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) - connect if @socket.closed? + 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 + end + if not req.response_body_permitted? and @close_on_empty_response req['connection'] ||= 'close' end + + req.update_uri address, port, use_ssl? req['host'] ||= addr_port() end def end_transport(req, res) @curr_http_version = res.http_version + @last_communicated = nil if @socket.closed? - D 'Conn socket closed' + debug 'Conn socket closed' elsif not res.body and @close_on_empty_response - D 'Conn close' + debug 'Conn close' @socket.close elsif keep_alive?(req, res) - D 'Conn keep-alive' + debug 'Conn keep-alive' + @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC) else - D 'Conn close' + debug 'Conn close' @socket.close end end @@ -1134,1201 +2571,38 @@ module Net #:nodoc: private def addr_port - if use_ssl? - address() + (port == HTTP.https_default_port ? '' : ":#{port()}") - else - address() + (port == HTTP.http_default_port ? '' : ":#{port()}") - end + 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 D(msg) + # 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' - # - # Header module. - # - # Provides access to @header in the mixed-into class as a hash-like - # object, except with case-insensitive keys. Also provides - # methods for accessing commonly-used header values in a more - # convenient format. - # - module HTTPHeader - - def initialize_http_header(initheader) - @header = {} - return unless initheader - initheader.each do |key, value| - warn "net/http: warning: duplicated HTTP header: #{key}" if key?(key) and $VERBOSE - @header[key.downcase] = [value.strip] - end - end - - def size #:nodoc: obsolete - @header.size - end - - alias length size #:nodoc: obsolete - - # Returns the header field corresponding to the case-insensitive key. - # For example, a key of "Content-Type" might return "text/html" - def [](key) - a = @header[key.downcase] or return nil - a.join(', ') - end - - # Sets the header field corresponding to the case-insensitive key. - def []=(key, val) - unless val - @header.delete key.downcase - return val - end - @header[key.downcase] = [val] - end - - # [Ruby 1.8.3] - # Adds header field instead of replace. - # Second argument +val+ must be a String. - # See also #[]=, #[] and #get_fields. - # - # request.add_field 'X-My-Header', 'a' - # p request['X-My-Header'] #=> "a" - # p request.get_fields('X-My-Header') #=> ["a"] - # request.add_field 'X-My-Header', 'b' - # p request['X-My-Header'] #=> "a, b" - # p request.get_fields('X-My-Header') #=> ["a", "b"] - # request.add_field 'X-My-Header', 'c' - # p request['X-My-Header'] #=> "a, b, c" - # p request.get_fields('X-My-Header') #=> ["a", "b", "c"] - # - def add_field(key, val) - if @header.key?(key.downcase) - @header[key.downcase].push val - else - @header[key.downcase] = [val] - end - end - - # [Ruby 1.8.3] - # Returns an array of header field strings corresponding to the - # case-insensitive +key+. This method allows you to get duplicated - # header fields without any processing. See also #[]. - # - # p response.get_fields('Set-Cookie') - # #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23", - # "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"] - # p response['Set-Cookie'] - # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23" - # - def get_fields(key) - return nil unless @header[key.downcase] - @header[key.downcase].dup - end - - # Returns the header field corresponding to the case-insensitive key. - # Returns the default value +args+, or the result of the block, or nil, - # if there's no header field named key. See Hash#fetch - def fetch(key, *args, &block) #:yield: +key+ - a = @header.fetch(key.downcase, *args, &block) - a.join(', ') - end - - # Iterates for each header names and values. - def each_header #:yield: +key+, +value+ - @header.each do |k,va| - yield k, va.join(', ') - end - end - - alias each each_header - - # Iterates for each header names. - def each_name(&block) #:yield: +key+ - @header.each_key(&block) - end - - alias each_key each_name - - # Iterates for each capitalized header names. - def each_capitalized_name(&block) #:yield: +key+ - @header.each_key do |k| - yield capitalize(k) - end - end - - # Iterates for each header values. - def each_value #:yield: +value+ - @header.each_value do |va| - yield va.join(', ') - end - end - - # Removes a header field. - def delete(key) - @header.delete(key.downcase) - end - - # true if +key+ header exists. - def key?(key) - @header.key?(key.downcase) - end - - # Returns a Hash consist of header names and values. - def to_hash - @header.dup - end - - # As for #each_header, except the keys are provided in capitalized form. - def each_capitalized - @header.each do |k,v| - yield capitalize(k), v.join(', ') - end - end - - alias canonical_each each_capitalized - - def capitalize(name) - name.split(/-/).map {|s| s.capitalize }.join('-') - end - private :capitalize - - # Returns an Array of Range objects which represents Range: header field, - # or +nil+ if there is no such header. - def range - return nil unless @header['range'] - self['Range'].split(/,/).map {|spec| - m = /bytes\s*=\s*(\d+)?\s*-\s*(\d+)?/i.match(spec) or - raise HTTPHeaderSyntaxError, "wrong Range: #{spec}" - d1 = m[1].to_i - d2 = m[2].to_i - if m[1] and m[2] then d1..d2 - elsif m[1] then d1..-1 - elsif m[2] then -d2..-1 - else - raise HTTPHeaderSyntaxError, 'range is not specified' - end - } - end - - # Set Range: header from Range (arg r) or beginning index and - # length from it (arg idx&len). - # - # req.range = (0..1023) - # req.set_range 0, 1023 - # - 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.last - last -= 1 if r.exclude_end? - if last == -1 - rangestr = (first > 0 ? "#{first}-" : "-#{-first}") - else - raise HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 - raise HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 - raise 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 an Integer object which represents the Content-Length: header field - # or +nil+ if that field is not provided. - def content_length - return nil unless key?('Content-Length') - len = self['Content-Length'].slice(/\d+/) or - raise HTTPHeaderSyntaxError, 'wrong Content-Length format' - len.to_i - end - - 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 the "transfer-encoding" header is present and - # set to "chunked". This is an HTTP/1.1 feature, allowing the - # the content to be sent in "chunks" without at the outset - # stating the entire content length. - 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 which represents Content-Range: header field. - # This indicates, for a partial entity body, where this fragment - # fits inside the full entity body, as range of byte offsets. - def content_range - return nil unless @header['content-range'] - m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or - raise HTTPHeaderSyntaxError, 'wrong Content-Range format' - m[1].to_i .. m[2].to_i + 1 - end - - # The length of the range represented in Content-Range: header. - def range_length - r = content_range() or return nil - r.end - r.begin - end - - # Returns a content type string such as "text/html". - # This method returns nil if Content-Type: header field does not exist. - def content_type - return nil unless main_type() - if sub_type() - then "#{main_type()}/#{sub_type()}" - else main_type() - end - end - - # Returns a content type string such as "text". - # This method returns nil if Content-Type: header field does not exist. - def main_type - return nil unless @header['content-type'] - self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip - end - - # Returns a content type string such as "html". - # This method returns nil if Content-Type: header field does not exist - # or sub-type is not given (e.g. "Content-Type: text"). - def sub_type - return nil unless @header['content-type'] - main, sub = *self['Content-Type'].split(';').first.to_s.split('/') - return nil unless sub - sub.strip - end - - # Returns content type parameters as a Hash as like - # {"charset" => "iso-2022-jp"}. - 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 - - # Set Content-Type: header field by +type+ and +params+. - # +type+ must be a String, +params+ must be a Hash. - def set_content_type(type, params = {}) - @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] - end - - alias content_type= set_content_type - - # Set header fields and a body from HTML form data. - # +params+ should be a Hash containing HTML form data. - # Optional argument +sep+ means data record separator. - # - # This method also set Content-Type: header field to - # application/x-www-form-urlencoded. - # - # Example: - # http.form_data = {"q" => "ruby", "lang" => "en"} - # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"} - # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';') - # - def set_form_data(params, sep = '&') - self.body = params.map {|k, v| encode_kvpair(k, v) }.flatten.join(sep) - self.content_type = 'application/x-www-form-urlencoded' - end - - alias form_data= set_form_data - - def encode_kvpair(k, vs) - Array(vs).map {|v| "#{urlencode(k)}=#{urlencode(v.to_s)}" } - end - private :encode_kvpair - - def urlencode(str) - str.gsub(/[^a-zA-Z0-9_\.\-]/n) {|s| sprintf('%%%02x', s[0]) } - end - private :urlencode - - # Set the Authorization: header for "Basic" authorization. - def basic_auth(account, password) - @header['authorization'] = [basic_encode(account, password)] - end - - # Set Proxy-Authorization: header for "Basic" authorization. - def proxy_basic_auth(account, password) - @header['proxy-authorization'] = [basic_encode(account, password)] - end - - def basic_encode(account, password) - 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n") - end - private :basic_encode - - def connection_close? - tokens(@header['connection']).include?('close') or - tokens(@header['proxy-connection']).include?('close') - end - - def connection_keep_alive? - tokens(@header['connection']).include?('keep-alive') or - tokens(@header['proxy-connection']).include?('keep-alive') - end - - def tokens(vals) - return [] unless vals - vals.map {|v| v.split(',') }.flatten\ - .reject {|str| str.strip.empty? }\ - .map {|tok| tok.strip.downcase } - end - private :tokens - - end - - - # - # Parent of HTTPRequest class. Do not use this directly; use - # a subclass of HTTPRequest. - # - # Mixes in the HTTPHeader module. - # - class HTTPGenericRequest - - include HTTPHeader - - def initialize(m, reqbody, resbody, path, initheader = nil) - @method = m - @request_has_body = reqbody - @response_has_body = resbody - raise ArgumentError, "HTTP request path is empty" if path.empty? - @path = path - initialize_http_header initheader - self['Accept'] ||= '*/*' - @body = nil - @body_stream = nil - end - - attr_reader :method - attr_reader :path - - def inspect - "\#<#{self.class} #{@method}>" - end - - def request_body_permitted? - @request_has_body - end - - def response_body_permitted? - @response_has_body - end - - def body_exist? - warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE - response_body_permitted? - end - - attr_reader :body +require_relative 'http/header' - def body=(str) - @body = str - @body_stream = nil - str - end - - attr_reader :body_stream - - def body_stream=(input) - @body = nil - @body_stream = input - 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 - 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 - else - write_header sock, ver, path - end - end - - private - - def send_request_with_body(sock, ver, path, body) - self.content_length = body.length - delete 'Transfer-Encoding' - supply_default_content_type - write_header sock, ver, path - 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 - supply_default_content_type - write_header sock, ver, path - if chunked? - while s = f.read(1024) - sock.write(sprintf("%x\r\n", s.length) << s << "\r\n") - end - sock.write "0\r\n\r\n" - else - while s = f.read(1024) - sock.write s - end - end - end - - def supply_default_content_type - return if content_type() - warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE - set_content_type 'application/x-www-form-urlencoded' - end - - def write_header(sock, ver, path) - buf = "#{@method} #{path} HTTP/#{ver}\r\n" - each_capitalized do |k,v| - buf << "#{k}: #{v}\r\n" - end - buf << "\r\n" - sock.write buf - end - - end - - - # - # HTTP request class. This class wraps request header and entity path. - # You *must* use its subclass, Net::HTTP::Get, Post, Head. - # - class HTTPRequest < HTTPGenericRequest - - # Creates HTTP request object. - def initialize(path, initheader = nil) - super self.class::METHOD, - self.class::REQUEST_HAS_BODY, - self.class::RESPONSE_HAS_BODY, - path, initheader - end - end - - - class HTTP # reopen - # - # HTTP 1.1 methods --- RFC2616 - # - - class Get < HTTPRequest - METHOD = 'GET' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - class Head < HTTPRequest - METHOD = 'HEAD' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = false - end - - class Post < HTTPRequest - METHOD = 'POST' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Put < HTTPRequest - METHOD = 'PUT' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Delete < HTTPRequest - METHOD = 'DELETE' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - class Options < HTTPRequest - METHOD = 'OPTIONS' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = false - end - - class Trace < HTTPRequest - METHOD = 'TRACE' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - # - # WebDAV methods --- RFC2518 - # - - class Propfind < HTTPRequest - METHOD = 'PROPFIND' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Proppatch < HTTPRequest - METHOD = 'PROPPATCH' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Mkcol < HTTPRequest - METHOD = 'MKCOL' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Copy < HTTPRequest - METHOD = 'COPY' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - class Move < HTTPRequest - METHOD = 'MOVE' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - class Lock < HTTPRequest - METHOD = 'LOCK' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Unlock < HTTPRequest - METHOD = 'UNLOCK' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - end - - - ### - ### Response - ### - - # HTTP exception class. - # You must use its subclasses. - module HTTPExceptions - def initialize(msg, res) #:nodoc: - super msg - @response = res - end - attr_reader :response - alias data response #:nodoc: obsolete - end - class HTTPError < ProtocolError - include HTTPExceptions - end - class HTTPRetriableError < ProtoRetriableError - include HTTPExceptions - end - class HTTPServerException < ProtoServerError - # We cannot use the name "HTTPServerError", it is the name of the response. - include HTTPExceptions - end - class HTTPFatalError < ProtoFatalError - include HTTPExceptions - end - - - # HTTP response class. This class wraps response header and entity. - # Mixes in the HTTPHeader module, which provides access to response - # header values both via hash-like methods and individual readers. - # Note that each possible HTTP response code defines its own - # HTTPResponse subclass. These are listed below. - # All classes are - # defined under the Net module. Indentation indicates inheritance. - # - # xxx HTTPResponse - # - # 1xx HTTPInformation - # 100 HTTPContinue - # 101 HTTPSwitchProtocol - # - # 2xx HTTPSuccess - # 200 HTTPOK - # 201 HTTPCreated - # 202 HTTPAccepted - # 203 HTTPNonAuthoritativeInformation - # 204 HTTPNoContent - # 205 HTTPResetContent - # 206 HTTPPartialContent - # - # 3xx HTTPRedirection - # 300 HTTPMultipleChoice - # 301 HTTPMovedPermanently - # 302 HTTPFound - # 303 HTTPSeeOther - # 304 HTTPNotModified - # 305 HTTPUseProxy - # 307 HTTPTemporaryRedirect - # - # 4xx HTTPClientError - # 400 HTTPBadRequest - # 401 HTTPUnauthorized - # 402 HTTPPaymentRequired - # 403 HTTPForbidden - # 404 HTTPNotFound - # 405 HTTPMethodNotAllowed - # 406 HTTPNotAcceptable - # 407 HTTPProxyAuthenticationRequired - # 408 HTTPRequestTimeOut - # 409 HTTPConflict - # 410 HTTPGone - # 411 HTTPLengthRequired - # 412 HTTPPreconditionFailed - # 413 HTTPRequestEntityTooLarge - # 414 HTTPRequestURITooLong - # 415 HTTPUnsupportedMediaType - # 416 HTTPRequestedRangeNotSatisfiable - # 417 HTTPExpectationFailed - # - # 5xx HTTPServerError - # 500 HTTPInternalServerError - # 501 HTTPNotImplemented - # 502 HTTPBadGateway - # 503 HTTPServiceUnavailable - # 504 HTTPGatewayTimeOut - # 505 HTTPVersionNotSupported - # - # xxx HTTPUnknownResponse - # - class HTTPResponse - # true if the response has body. - def HTTPResponse.body_permitted? - self::HAS_BODY - end - - def HTTPResponse.exception_type # :nodoc: internal use only - self::EXCEPTION_TYPE - end - end # reopened after - - # :stopdoc: - - class HTTPUnknownResponse < HTTPResponse - HAS_BODY = true - EXCEPTION_TYPE = HTTPError - end - class HTTPInformation < HTTPResponse # 1xx - HAS_BODY = false - EXCEPTION_TYPE = HTTPError - end - class HTTPSuccess < HTTPResponse # 2xx - HAS_BODY = true - EXCEPTION_TYPE = HTTPError - end - class HTTPRedirection < HTTPResponse # 3xx - HAS_BODY = true - EXCEPTION_TYPE = HTTPRetriableError - end - class HTTPClientError < HTTPResponse # 4xx - HAS_BODY = true - EXCEPTION_TYPE = HTTPServerException # for backward compatibility - end - class HTTPServerError < HTTPResponse # 5xx - HAS_BODY = true - EXCEPTION_TYPE = HTTPFatalError # for backward compatibility - end - - class HTTPContinue < HTTPInformation # 100 - HAS_BODY = false - end - class HTTPSwitchProtocol < HTTPInformation # 101 - HAS_BODY = false - end - - class HTTPOK < HTTPSuccess # 200 - HAS_BODY = true - end - class HTTPCreated < HTTPSuccess # 201 - HAS_BODY = true - end - class HTTPAccepted < HTTPSuccess # 202 - HAS_BODY = true - end - class HTTPNonAuthoritativeInformation < HTTPSuccess # 203 - HAS_BODY = true - end - class HTTPNoContent < HTTPSuccess # 204 - HAS_BODY = false - end - class HTTPResetContent < HTTPSuccess # 205 - HAS_BODY = false - end - class HTTPPartialContent < HTTPSuccess # 206 - HAS_BODY = true - end - - class HTTPMultipleChoice < HTTPRedirection # 300 - HAS_BODY = true - end - class HTTPMovedPermanently < HTTPRedirection # 301 - HAS_BODY = true - end - class HTTPFound < HTTPRedirection # 302 - HAS_BODY = true - end - HTTPMovedTemporarily = HTTPFound - class HTTPSeeOther < HTTPRedirection # 303 - HAS_BODY = true - end - class HTTPNotModified < HTTPRedirection # 304 - HAS_BODY = false - end - class HTTPUseProxy < HTTPRedirection # 305 - HAS_BODY = false - end - # 306 unused - class HTTPTemporaryRedirect < HTTPRedirection # 307 - HAS_BODY = true - end - - class HTTPBadRequest < HTTPClientError # 400 - HAS_BODY = true - end - class HTTPUnauthorized < HTTPClientError # 401 - HAS_BODY = true - end - class HTTPPaymentRequired < HTTPClientError # 402 - HAS_BODY = true - end - class HTTPForbidden < HTTPClientError # 403 - HAS_BODY = true - end - class HTTPNotFound < HTTPClientError # 404 - HAS_BODY = true - end - class HTTPMethodNotAllowed < HTTPClientError # 405 - HAS_BODY = true - end - class HTTPNotAcceptable < HTTPClientError # 406 - HAS_BODY = true - end - class HTTPProxyAuthenticationRequired < HTTPClientError # 407 - HAS_BODY = true - end - class HTTPRequestTimeOut < HTTPClientError # 408 - HAS_BODY = true - end - class HTTPConflict < HTTPClientError # 409 - HAS_BODY = true - end - class HTTPGone < HTTPClientError # 410 - HAS_BODY = true - end - class HTTPLengthRequired < HTTPClientError # 411 - HAS_BODY = true - end - class HTTPPreconditionFailed < HTTPClientError # 412 - HAS_BODY = true - end - class HTTPRequestEntityTooLarge < HTTPClientError # 413 - HAS_BODY = true - end - class HTTPRequestURITooLong < HTTPClientError # 414 - HAS_BODY = true - end - HTTPRequestURITooLarge = HTTPRequestURITooLong - class HTTPUnsupportedMediaType < HTTPClientError # 415 - HAS_BODY = true - end - class HTTPRequestedRangeNotSatisfiable < HTTPClientError # 416 - HAS_BODY = true - end - class HTTPExpectationFailed < HTTPClientError # 417 - HAS_BODY = true - end - - class HTTPInternalServerError < HTTPServerError # 500 - HAS_BODY = true - end - class HTTPNotImplemented < HTTPServerError # 501 - HAS_BODY = true - end - class HTTPBadGateway < HTTPServerError # 502 - HAS_BODY = true - end - class HTTPServiceUnavailable < HTTPServerError # 503 - HAS_BODY = true - end - class HTTPGatewayTimeOut < HTTPServerError # 504 - HAS_BODY = true - end - class HTTPVersionNotSupported < HTTPServerError # 505 - HAS_BODY = true - end - - # :startdoc: - - - class HTTPResponse # reopen - - CODE_CLASS_TO_OBJ = { - '1' => HTTPInformation, - '2' => HTTPSuccess, - '3' => HTTPRedirection, - '4' => HTTPClientError, - '5' => HTTPServerError - } - CODE_TO_OBJ = { - '100' => HTTPContinue, - '101' => HTTPSwitchProtocol, - - '200' => HTTPOK, - '201' => HTTPCreated, - '202' => HTTPAccepted, - '203' => HTTPNonAuthoritativeInformation, - '204' => HTTPNoContent, - '205' => HTTPResetContent, - '206' => HTTPPartialContent, - - '300' => HTTPMultipleChoice, - '301' => HTTPMovedPermanently, - '302' => HTTPFound, - '303' => HTTPSeeOther, - '304' => HTTPNotModified, - '305' => HTTPUseProxy, - '307' => HTTPTemporaryRedirect, - - '400' => HTTPBadRequest, - '401' => HTTPUnauthorized, - '402' => HTTPPaymentRequired, - '403' => HTTPForbidden, - '404' => HTTPNotFound, - '405' => HTTPMethodNotAllowed, - '406' => HTTPNotAcceptable, - '407' => HTTPProxyAuthenticationRequired, - '408' => HTTPRequestTimeOut, - '409' => HTTPConflict, - '410' => HTTPGone, - '411' => HTTPLengthRequired, - '412' => HTTPPreconditionFailed, - '413' => HTTPRequestEntityTooLarge, - '414' => HTTPRequestURITooLong, - '415' => HTTPUnsupportedMediaType, - '416' => HTTPRequestedRangeNotSatisfiable, - '417' => HTTPExpectationFailed, - - '500' => HTTPInternalServerError, - '501' => HTTPNotImplemented, - '502' => HTTPBadGateway, - '503' => HTTPServiceUnavailable, - '504' => HTTPGatewayTimeOut, - '505' => HTTPVersionNotSupported - } - - class << HTTPResponse - 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 - - def read_status_line(sock) - str = sock.readline - m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in.match(str) or - raise 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 - HTTPUnknownResponse - end - - def each_response_header(sock) - while true - line = sock.readuntil("\n", true).sub(/\s+\z/, '') - break if line.empty? - m = /\A([^:]+):\s*/.match(line) or - raise HTTPBadResponse, 'wrong header line format' - yield m[1], m.post_match - end - end - end - - # next is to fix bug in RDoc, where the private inside class << self - # spills out. - public - - include 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 - end - - # The HTTP version supported by the server. - attr_reader :http_version - - # HTTP result code string. For example, '302'. You can also - # determine the response type by which response subclass the - # response object is an instance of. - attr_reader :code - - # HTTP result message. For example, 'Not Found'. - attr_reader :message - alias msg message # :nodoc: obsolete - - def inspect - "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" - end - - # For backward compatibility. - # To allow Net::HTTP 1.1 style assignment - # e.g. - # response, body = Net::HTTP.get(....) - # - def to_ary - warn "net/http.rb: warning: Net::HTTP v1.1 style assignment found at #{caller(1)[0]}; use `response = http.get(...)' instead." if $VERBOSE - res = self.dup - class << res - undef to_ary - end - [res, res.body] - end - - # - # response <-> exception relationship - # - - def code_type #:nodoc: - self.class - end - - def error! #:nodoc: - raise error_type().new(@code + ' ' + @message.dump, self) - end - - def error_type #:nodoc: - self.class::EXCEPTION_TYPE - end - - # Raises HTTP error if the response is not 2xx. - def value - error! unless self.kind_of?(HTTPSuccess) - end - - # - # header (for backward compatibility only; DO NOT USE) - # - - def response #:nodoc: - warn "#{caller(1)[0]}: warning: HTTPResponse#response is obsolete" if $VERBOSE - self - end - - def header #:nodoc: - warn "#{caller(1)[0]}: warning: HTTPResponse#header is obsolete" if $VERBOSE - self - end - - def read_header #:nodoc: - warn "#{caller(1)[0]}: warning: HTTPResponse#read_header is obsolete" 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 entity body. If the block given, yields it to +block+. - # The body is provided in fragments, as it is read in from the socket. - # - # Calling this method a second or subsequent time will return the - # already read string. - # - # 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 - - @body - end - - # Returns the entity body. - # - # Calling this method a second or subsequent time will return the - # already read string. - # - # http.request_get('/index.html') {|res| - # puts res.body - # } - # - # http.request_get('/index.html') {|res| - # p res.body.object_id # 538149362 - # p res.body.object_id # 538149362 - # } - # - def body - read_body() - end - - alias entity body #:nodoc: obsolete - - private - - def read_body_0(dest) - if chunked? - read_chunked dest - return - end - clen = content_length() - if clen - @socket.read clen, dest, true # ignore EOF - return - end - clen = range_length() - if clen - @socket.read clen, dest - return - end - @socket.read_all dest - end - - def read_chunked(dest) - len = nil - total = 0 - while true - line = @socket.readline - hexlen = line.slice(/[0-9a-fA-F]+/) or - raise HTTPBadResponse, "wrong chunk size line: #{line}" - len = hexlen.hex - break if len == 0 - @socket.read len, dest; total += len - @socket.read 2 # \r\n - end - until @socket.readline.empty? - # none - end - end - - def stream_check - raise IOError, 'attempt to read body out of block' if @socket.closed? - end - - def procdest(dest, block) - raise ArgumentError, 'both arg and block given for HTTP method' \ - if dest and block - if block - ReadAdapter.new(block) - else - dest || '' - end - end - - end - - - # :enddoc: - - #-- - # for backward compatibility - class HTTP - ProxyMod = ProxyDelta - end - module NetPrivate - HTTPRequest = ::Net::HTTPRequest - end +require_relative 'http/generic_request' +require_relative 'http/request' +require_relative 'http/requests' - HTTPInformationCode = HTTPInformation - HTTPSuccessCode = HTTPSuccess - HTTPRedirectionCode = HTTPRedirection - HTTPRetriableCode = HTTPRedirection - HTTPClientErrorCode = HTTPClientError - HTTPFatalErrorCode = HTTPClientError - HTTPServerErrorCode = HTTPServerError - HTTPResponceReceiver = HTTPResponse +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 index fb329df43d..0f23e1fb13 100644 --- a/lib/net/https.rb +++ b/lib/net/https.rb @@ -1,6 +1,12 @@ +# frozen_string_literal: true =begin -= $RCSfile$ -- SSL/TLS enhancement for Net::HTTP. += 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 @@ -8,164 +14,10 @@ All rights reserved. == Licence - This program is licenced under the same licence as Ruby. + This program is licensed under the same licence as Ruby. (See the file 'LICENCE'.) -== Requirements - This program requires Net 1.2.0 or higher version. - You can get it from RAA or Ruby's CVS repository. - -== Version - $Id$ - - 2001-11-06: Contiributed to Ruby/OpenSSL project. - 2004-03-06: Some code is merged in to net/http. - -== Example - -Here is a simple HTTP client: - - require 'net/http' - require 'uri' - - uri = URI.parse(ARGV[0] || 'http://localhost/') - http = Net::HTTP.new(uri.host, uri.port) - http.start { - http.request_get(uri.path) {|res| - print res.body - } - } - -It can be replaced by the following code: - - require 'net/https' - require 'uri' - - uri = URI.parse(ARGV[0] || 'https://localhost/') - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true if uri.scheme == "https" # enable SSL/TLS - http.start { - http.request_get(uri.path) {|res| - print res.body - } - } - -== class Net::HTTP - -=== Instance Methods - -: use_ssl? - returns true if use SSL/TLS with HTTP. - -: use_ssl=((|true_or_false|)) - sets use_ssl. - -: peer_cert - return the X.509 certificates the server presented. - -: key, key=((|key|)) - Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. - (This method is appeared in Michal Rokos's OpenSSL extention.) - -: cert, cert=((|cert|)) - Sets an OpenSSL::X509::Certificate object as client certificate - (This method is appeared in Michal Rokos's OpenSSL extention). - -: ca_file, ca_file=((|path|)) - Sets path of a CA certification file in PEM format. - The file can contrain several CA certificats. - -: ca_path, ca_path=((|path|)) - Sets path of a CA certification directory containing certifications - in PEM format. - -: verify_mode, verify_mode=((|mode|)) - Sets the flags for server the certification verification at - begining of SSL/TLS session. - OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable. - -: verify_callback, verify_callback=((|proc|)) - Sets the verify callback for the server certification verification. - -: verify_depth, verify_depth=((|num|)) - Sets the maximum depth for the certificate chain verification. - -: cert_store, cert_store=((|store|)) - Sets the X509::Store to verify peer certificate. - -: ssl_timeout, ssl_timeout=((|sec|)) - Sets the SSL timeout seconds. - =end -require 'net/http' +require_relative 'http' require 'openssl' - -module Net - - class HTTP - remove_method :use_ssl? - def use_ssl? - @use_ssl - end - - alias use_ssl use_ssl? # for backward compatibility - - # Turn on/off SSL. - # This flag must be set before starting session. - # If you change use_ssl value after session started, - # a Net::HTTP object raises IOError. - def use_ssl=(flag) - flag = (flag ? true : false) - raise IOError, "use_ssl value changed, but session already started" \ - if started? and @use_ssl != flag - if flag and not @ssl_context - @ssl_context = OpenSSL::SSL::SSLContext.new - end - @use_ssl = flag - end - - def self.ssl_context_accessor(name) - module_eval(<<-End, __FILE__, __LINE__ + 1) - def #{name} - return nil unless @ssl_context - @ssl_context.#{name} - end - - def #{name}=(val) - @ssl_context ||= OpenSSL::SSL::SSLContext.new - @ssl_context.#{name} = val - end - End - end - - ssl_context_accessor :key - ssl_context_accessor :cert - ssl_context_accessor :ca_file - ssl_context_accessor :ca_path - ssl_context_accessor :verify_mode - ssl_context_accessor :verify_callback - ssl_context_accessor :verify_depth - ssl_context_accessor :cert_store - - def ssl_timeout - return nil unless @ssl_context - @ssl_context.timeout - end - - def ssl_timeout=(sec) - raise ArgumentError, 'Net::HTTP#ssl_timeout= called but use_ssl=false' \ - unless use_ssl? - @ssl_context ||= OpenSSL::SSL::SSLContext.new - @ssl_context.timeout = sec - end - - alias timeout= ssl_timeout= # for backward compatibility - - def peer_cert - return nil if not use_ssl? or not @socket - @socket.io.peer_cert - end - end - -end diff --git a/lib/net/imap.rb b/lib/net/imap.rb deleted file mode 100644 index 75d027c9ee..0000000000 --- a/lib/net/imap.rb +++ /dev/null @@ -1,3375 +0,0 @@ -# -# = 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. -# -# Documentation: Shugo Maeda, with RDoc conversion and overview by William -# Webber. -# -# See Net::IMAP for documentation. -# - - -require "socket" -require "monitor" -require "digest/md5" -require "strscan" -begin - require "openssl" -rescue LoadError -end - -module Net - - # - # Net::IMAP implements Internet Message Access Protocol (IMAP) client - # functionality. The protocol is described in [IMAP]. - # - # == IMAP Overview - # - # An IMAP client connects to a server, and then authenticates - # itself using either #authenticate() or #login(). Having - # authenticated itself, there is a range of commands - # available to it. Most work with mailboxes, which may be - # arranged in an hierarchical namespace, and each of which - # contains zero or more messages. How this is implemented on - # the server is implementation-dependent; on a UNIX server, it - # will frequently be implemented as a files in mailbox format - # within a hierarchy of directories. - # - # To work on the messages within a mailbox, the client must - # first select that mailbox, using either #select() or (for - # read-only access) #examine(). Once the client has successfully - # selected a mailbox, they enter _selected_ state, and that - # mailbox becomes the _current_ mailbox, on which mail-item - # related commands implicitly operate. - # - # Messages have two sorts of identifiers: message sequence - # numbers, and UIDs. - # - # Message sequence numbers number messages within a mail box - # from 1 up to the number of items in the mail box. If new - # message arrives during a session, it receives a sequence - # number equal to the new size of the mail box. If messages - # are expunged from the mailbox, remaining messages have their - # sequence numbers "shuffled down" to fill the gaps. - # - # UIDs, on the other hand, are permanently guaranteed not to - # identify another message within the same mailbox, even if - # the existing message is deleted. UIDs are required to - # be assigned in ascending (but not necessarily sequential) - # order within a mailbox; this means that if a non-IMAP client - # rearranges the order of mailitems within a mailbox, the - # UIDs have to be reassigned. An IMAP client cannot thus - # rearrange message orders. - # - # == Examples of Usage - # - # === List sender and subject of all recent messages in the default mailbox - # - # imap = Net::IMAP.new('mail.example.com') - # imap.authenticate('LOGIN', 'joe_user', 'joes_password') - # imap.examine('INBOX') - # imap.search(["RECENT"]).each do |message_id| - # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"] - # puts "#{envelope.from[0].name}: \t#{envelope.subject}" - # end - # - # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" - # - # imap = Net::IMAP.new('mail.example.com') - # imap.authenticate('LOGIN', 'joe_user', 'joes_password') - # imap.select('Mail/sent-mail') - # if not imap.list('Mail/', 'sent-apr03') - # imap.create('Mail/sent-apr03') - # end - # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id| - # imap.copy(message_id, "Mail/sent-apr03") - # imap.store(message_id, "+FLAGS", [:Deleted]) - # end - # imap.expunge - # - # == Thread Safety - # - # Net::IMAP supports concurrent threads. For example, - # - # imap = Net::IMAP.new("imap.foo.net", "imap2") - # imap.authenticate("cram-md5", "bar", "password") - # imap.select("inbox") - # fetch_thread = Thread.start { imap.fetch(1..-1, "UID") } - # search_result = imap.search(["BODY", "hello"]) - # fetch_result = fetch_thread.value - # imap.disconnect - # - # This script invokes the FETCH command and the SEARCH command concurrently. - # - # == Errors - # - # An IMAP server can send three different types of responses to indicate - # failure: - # - # NO:: the attempted command could not be successfully completed. For - # instance, the username/password used for logging in are incorrect; - # the selected mailbox does not exists; etc. - # - # BAD:: the request from the client does not follow the server's - # understanding of the IMAP protocol. This includes attempting - # commands from the wrong client state; for instance, attempting - # to perform a SEARCH command without having SELECTed a current - # mailbox. It can also signal an internal server - # failure (such as a disk crash) has occurred. - # - # BYE:: the server is saying goodbye. This can be part of a normal - # logout sequence, and can be used as part of a login sequence - # to indicate that the server is (for some reason) unwilling - # to accept our connection. As a response to any other command, - # it indicates either that the server is shutting down, or that - # the server is timing out the client connection due to inactivity. - # - # These three error response are represented by the errors - # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and - # Net::IMAP::ByeResponseError, all of which are subclasses of - # Net::IMAP::ResponseError. Essentially, all methods that involve - # sending a request to the server can generate one of these errors. - # Only the most pertinent instances have been documented below. - # - # Because the IMAP class uses Sockets for communication, its methods - # are also susceptible to the various errors that can occur when - # working with sockets. These are generally represented as - # Errno errors. For instance, any method that involves sending a - # request to the server and/or receiving a response from it could - # raise an Errno::EPIPE error if the network connection unexpectedly - # goes down. See the socket(7), ip(7), tcp(7), socket(2), connect(2), - # and associated man pages. - # - # Finally, a Net::IMAP::DataFormatError is thrown if low-level data - # is found to be in an incorrect format (for instance, when converting - # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is - # thrown if a server response is non-parseable. - # - # - # == References - # - # [[IMAP]] - # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1", - # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501) - # - # [[LANGUAGE-TAGS]] - # Alvestrand, H., "Tags for the Identification of - # Languages", RFC 1766, March 1995. - # - # [[MD5]] - # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC - # 1864, October 1995. - # - # [[MIME-IMB]] - # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet - # Mail Extensions) Part One: Format of Internet Message Bodies", RFC - # 2045, November 1996. - # - # [[RFC-822]] - # Crocker, D., "Standard for the Format of ARPA Internet Text - # Messages", STD 11, RFC 822, University of Delaware, August 1982. - # - # [[RFC-2087]] - # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997. - # - # [[RFC-2086]] - # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997. - # - # [[RFC-2195]] - # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension - # for Simple Challenge/Response", RFC 2195, September 1997. - # - # [[SORT-THREAD-EXT]] - # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD - # Extensions", draft-ietf-imapext-sort, May 2003. - # - # [[OSSL]] - # http://www.openssl.org - # - # [[RSSL]] - # http://savannah.gnu.org/projects/rubypki - # - # [[UTF7]] - # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of - # Unicode", RFC 2152, May 1997. - # - class IMAP - include MonitorMixin - if defined?(OpenSSL) - include OpenSSL - include SSL - end - - # Returns an initial greeting response from the server. - attr_reader :greeting - - # Returns recorded untagged responses. For example: - # - # imap.select("inbox") - # p imap.responses["EXISTS"][-1] - # #=> 2 - # p imap.responses["UIDVALIDITY"][-1] - # #=> 968263756 - attr_reader :responses - - # Returns all response handlers. - attr_reader :response_handlers - - # The thread to receive exceptions. - attr_accessor :client_thread - - # Flag indicating a message has been seen - SEEN = :Seen - - # Flag indicating a message has been answered - ANSWERED = :Answered - - # Flag indicating a message has been flagged for special or urgent - # attention - FLAGGED = :Flagged - - # Flag indicating a message has been marked for deletion. This - # will occur when the mailbox is closed or expunged. - DELETED = :Deleted - - # Flag indicating a message is only a draft or work-in-progress version. - DRAFT = :Draft - - # Flag indicating that the message is "recent", meaning that this - # session is the first session in which the client has been notified - # of this message. - RECENT = :Recent - - # Flag indicating that a mailbox context name cannot contain - # children. - NOINFERIORS = :Noinferiors - - # Flag indicating that a mailbox is not selected. - NOSELECT = :Noselect - - # Flag indicating that a mailbox has been marked "interesting" by - # the server; this commonly indicates that the mailbox contains - # new messages. - MARKED = :Marked - - # Flag indicating that the mailbox does not contains new messages. - UNMARKED = :Unmarked - - # Returns the debug mode. - def self.debug - return @@debug - end - - # Sets the debug mode. - def self.debug=(val) - return @@debug = val - end - - # Adds an authenticator for Net::IMAP#authenticate. +auth_type+ - # is the type of authentication this authenticator supports - # (for instance, "LOGIN"). The +authenticator+ is an object - # which defines a process() method to handle authentication with - # the server. See Net::IMAP::LoginAuthenticator, - # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator - # for examples. - # - # - # If +auth_type+ refers to an existing authenticator, it will be - # replaced by the new one. - def self.add_authenticator(auth_type, authenticator) - @@authenticators[auth_type] = authenticator - end - - # Disconnects from the server. - def disconnect - if SSL::SSLSocket === @sock - @sock.io.shutdown - else - @sock.shutdown - end - @receiver_thread.join - @sock.close - end - - # Returns true if disconnected from the server. - def disconnected? - return @sock.closed? - end - - # Sends a CAPABILITY command, and returns an array of - # capabilities that the server supports. Each capability - # is a string. See [IMAP] for a list of possible - # capabilities. - # - # Note that the Net::IMAP class does not modify its - # behaviour according to the capabilities of the server; - # it is up to the user of the class to ensure that - # a certain capability is supported by a server before - # using it. - def capability - synchronize do - send_command("CAPABILITY") - return @responses.delete("CAPABILITY")[-1] - end - end - - # Sends a NOOP command to the server. It does nothing. - def noop - send_command("NOOP") - end - - # Sends a LOGOUT command to inform the server that the client is - # done with the connection. - def logout - send_command("LOGOUT") - end - - # Sends a STARTTLS command to start TLS session. - def starttls(ctx = nil) - if @sock.kind_of?(OpenSSL::SSL::SSLSocket) - raise RuntimeError, "already using SSL" - end - send_command("STARTTLS") do |resp| - if resp.kind_of?(TaggedResponse) && resp.name == "OK" - if ctx - @sock = OpenSSL::SSL::SSLSocket.new(@sock, ctx) - else - @sock = OpenSSL::SSL::SSLSocket.new(@sock) - end - @sock.sync_close = true - @sock.connect - end - end - end - - # Sends an AUTHENTICATE command to authenticate the client. - # The +auth_type+ parameter is a string that represents - # the authentication mechanism to be used. Currently Net::IMAP - # supports authentication mechanisms: - # - # LOGIN:: login using cleartext user and password. - # CRAM-MD5:: login with cleartext user and encrypted password - # (see [RFC-2195] for a full description). This - # mechanism requires that the server have the user's - # password stored in clear-text password. - # - # For both these mechanisms, there should be two +args+: username - # and (cleartext) password. A server may not support one or other - # of these mechanisms; check #capability() for a capability of - # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5". - # - # Authentication is done using the appropriate authenticator object: - # see @@authenticators for more information on plugging in your own - # authenticator. - # - # For example: - # - # imap.authenticate('LOGIN', user, password) - # - # A Net::IMAP::NoResponseError is raised if authentication fails. - 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?(ContinuationRequest) - data = authenticator.process(resp.data.text.unpack("m")[0]) - s = [data].pack("m").gsub(/\n/, "") - send_string_data(s) - put_string(CRLF) - end - end - end - - # Sends a LOGIN command to identify the client and carries - # the plaintext +password+ authenticating this +user+. Note - # that, unlike calling #authenticate() with an +auth_type+ - # of "LOGIN", #login() does *not* use the login authenticator. - # - # A Net::IMAP::NoResponseError is raised if authentication fails. - def login(user, password) - send_command("LOGIN", user, password) - end - - # Sends a SELECT command to select a +mailbox+ so that messages - # in the +mailbox+ can be accessed. - # - # After you have selected a mailbox, you may retrieve the - # number of items in that mailbox from @responses["EXISTS"][-1], - # and the number of recent messages from @responses["RECENT"][-1]. - # Note that these values can change if new messages arrive - # during a session; see #add_response_handler() for a way of - # detecting this event. - # - # A Net::IMAP::NoResponseError is raised if the mailbox does not - # exist or is for some reason non-selectable. - def select(mailbox) - synchronize do - @responses.clear - send_command("SELECT", mailbox) - end - end - - # Sends a EXAMINE command to select a +mailbox+ so that messages - # in the +mailbox+ can be accessed. Behaves the same as #select(), - # except that the selected +mailbox+ is identified as read-only. - # - # A Net::IMAP::NoResponseError is raised if the mailbox does not - # exist or is for some reason non-examinable. - def examine(mailbox) - synchronize do - @responses.clear - send_command("EXAMINE", mailbox) - end - end - - # Sends a CREATE command to create a new +mailbox+. - # - # A Net::IMAP::NoResponseError is raised if a mailbox with that name - # cannot be created. - def create(mailbox) - send_command("CREATE", mailbox) - end - - # Sends a DELETE command to remove the +mailbox+. - # - # A Net::IMAP::NoResponseError is raised if a mailbox with that name - # cannot be deleted, either because it does not exist or because the - # client does not have permission to delete it. - def delete(mailbox) - send_command("DELETE", mailbox) - end - - # Sends a RENAME command to change the name of the +mailbox+ to - # +newname+. - # - # A Net::IMAP::NoResponseError is raised if a mailbox with the - # name +mailbox+ cannot be renamed to +newname+ for whatever - # reason; for instance, because +mailbox+ does not exist, or - # because there is already a mailbox with the name +newname+. - def rename(mailbox, newname) - send_command("RENAME", mailbox, newname) - end - - # Sends a SUBSCRIBE command to add the specified +mailbox+ name to - # the server's set of "active" or "subscribed" mailboxes as returned - # by #lsub(). - # - # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be - # subscribed to, for instance because it does not exist. - def subscribe(mailbox) - send_command("SUBSCRIBE", mailbox) - end - - # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name - # from the server's set of "active" or "subscribed" mailboxes. - # - # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be - # unsubscribed from, for instance because the client is not currently - # subscribed to it. - def unsubscribe(mailbox) - send_command("UNSUBSCRIBE", mailbox) - end - - # Sends a LIST command, and returns a subset of names from - # the complete set of all names available to the client. - # +refname+ provides a context (for instance, a base directory - # in a directory-based mailbox hierarchy). +mailbox+ specifies - # a mailbox or (via wildcards) mailboxes under that context. - # Two wildcards may be used in +mailbox+: '*', which matches - # all characters *including* the hierarchy delimiter (for instance, - # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%', - # which matches all characters *except* the hierarchy delimiter. - # - # If +refname+ is empty, +mailbox+ is used directly to determine - # which mailboxes to match. If +mailbox+ is empty, the root - # name of +refname+ and the hierarchy delimiter are returned. - # - # The return value is an array of +Net::IMAP::MailboxList+. For example: - # - # 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">] - def list(refname, mailbox) - synchronize do - send_command("LIST", refname, mailbox) - return @responses.delete("LIST") - end - end - - # Sends the GETQUOTAROOT command along with specified +mailbox+. - # This command is generally available to both admin and user. - # If mailbox exists, returns an array containing objects of - # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota. - def getquotaroot(mailbox) - synchronize do - send_command("GETQUOTAROOT", mailbox) - result = [] - result.concat(@responses.delete("QUOTAROOT")) - result.concat(@responses.delete("QUOTA")) - return result - end - end - - # Sends the GETQUOTA command along with specified +mailbox+. - # If this mailbox exists, then an array containing a - # Net::IMAP::MailboxQuota object is returned. This - # command generally is only available to server admin. - def getquota(mailbox) - synchronize do - send_command("GETQUOTA", mailbox) - return @responses.delete("QUOTA") - end - end - - # Sends a SETQUOTA command along with the specified +mailbox+ and - # +quota+. If +quota+ is nil, then quota will be unset for that - # mailbox. Typically one needs to be logged in as server admin - # for this to work. The IMAP quota commands are described in - # [RFC-2087]. - def setquota(mailbox, quota) - if quota.nil? - data = '()' - else - data = '(STORAGE ' + quota.to_s + ')' - end - send_command("SETQUOTA", mailbox, RawData.new(data)) - end - - # Sends the SETACL command along with +mailbox+, +user+ and the - # +rights+ that user is to have on that mailbox. If +rights+ is nil, - # then that user will be stripped of any rights to that mailbox. - # The IMAP ACL commands are described in [RFC-2086]. - def setacl(mailbox, user, rights) - if rights.nil? - send_command("SETACL", mailbox, user, "") - else - send_command("SETACL", mailbox, user, rights) - end - end - - # Send the GETACL command along with specified +mailbox+. - # If this mailbox exists, an array containing objects of - # Net::IMAP::MailboxACLItem will be returned. - def getacl(mailbox) - synchronize do - send_command("GETACL", mailbox) - return @responses.delete("ACL")[-1] - end - end - - # 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". +refname+ and +mailbox+ are interpreted as - # for #list(). - # The return value is an array of +Net::IMAP::MailboxList+. - def lsub(refname, mailbox) - synchronize do - send_command("LSUB", refname, mailbox) - return @responses.delete("LSUB") - end - end - - # Sends a STATUS command, and returns the status of the indicated - # +mailbox+. +attr+ is a list of one or more attributes that - # we are request the status of. Supported attributes include: - # - # MESSAGES:: the number of messages in the mailbox. - # RECENT:: the number of recent messages in the mailbox. - # UNSEEN:: the number of unseen messages in the mailbox. - # - # The return value is a hash of attributes. For example: - # - # p imap.status("inbox", ["MESSAGES", "RECENT"]) - # #=> {"RECENT"=>0, "MESSAGES"=>44} - # - # A Net::IMAP::NoResponseError is raised if status values - # for +mailbox+ cannot be returned, for instance because it - # does not exist. - def status(mailbox, attr) - synchronize do - send_command("STATUS", mailbox, attr) - return @responses.delete("STATUS")[-1].attr - end - end - - # Sends a APPEND command to append the +message+ to the end of - # the +mailbox+. The optional +flags+ argument is an array of - # flags to initially passing to the new message. The optional - # +date_time+ argument specifies the creation time to assign to the - # new message; it defaults to the current time. - # For example: - # - # 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 - # - # A Net::IMAP::NoResponseError is raised if the mailbox does - # not exist (it is not created automatically), or if the flags, - # date_time, or message arguments contain errors. - 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 - - # Sends a CHECK command to request a checkpoint of the currently - # selected mailbox. This performs implementation-specific - # housekeeping, for instance, reconciling the mailbox's - # in-memory and on-disk state. - def check - send_command("CHECK") - end - - # 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. - def close - send_command("CLOSE") - end - - # Sends a EXPUNGE command to permanently remove from the currently - # selected mailbox all messages that have the \Deleted flag set. - def expunge - synchronize do - send_command("EXPUNGE") - return @responses.delete("EXPUNGE") - end - end - - # Sends a SEARCH command to search the mailbox for messages that - # match the given searching criteria, and returns message sequence - # numbers. +keys+ can either be a string holding the entire - # search string, or a single-dimension array of search keywords and - # arguments. The following are some common search criteria; - # see [IMAP] section 6.4.4 for a full list. - # - # <message set>:: a set of message sequence numbers. ',' indicates - # an interval, ':' indicates a range. For instance, - # '2,10:12,15' means "2,10,11,12,15". - # - # BEFORE <date>:: messages with an internal date strictly before - # <date>. The date argument has a format similar - # to 8-Aug-2002. - # - # BODY <string>:: messages that contain <string> within their body. - # - # CC <string>:: messages containing <string> in their CC field. - # - # FROM <string>:: messages that contain <string> in their FROM field. - # - # NEW:: messages with the \Recent, but not the \Seen, flag set. - # - # NOT <search-key>:: negate the following search key. - # - # OR <search-key> <search-key>:: "or" two search keys together. - # - # ON <date>:: messages with an internal date exactly equal to <date>, - # which has a format similar to 8-Aug-2002. - # - # SINCE <date>:: messages with an internal date on or after <date>. - # - # SUBJECT <string>:: messages with <string> in their subject. - # - # TO <string>:: messages with <string> in their TO field. - # - # For example: - # - # p imap.search(["SUBJECT", "hello", "NOT", "NEW"]) - # #=> [1, 6, 7, 8] - def search(keys, charset = nil) - return search_internal("SEARCH", keys, charset) - end - - # As for #search(), but returns unique identifiers. - def uid_search(keys, charset = nil) - return search_internal("UID SEARCH", keys, charset) - end - - # 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. +attr+ is a list of attributes to fetch; see the - # documentation for Net::IMAP::FetchData for a list of valid - # attributes. - # The return value is an array of Net::IMAP::FetchData. For example: - # - # 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 - def fetch(set, attr) - return fetch_internal("FETCH", set, attr) - end - - # As for #fetch(), but +set+ contains unique identifiers. - def uid_fetch(set, attr) - return fetch_internal("UID FETCH", set, attr) - end - - # Sends a STORE command to alter data associated with messages - # in the mailbox, in particular their flags. The +set+ parameter - # is a number or an array of numbers or a Range object. Each number - # is a message sequence number. +attr+ is the name of a data item - # to store: 'FLAGS' means to replace the message's flag list - # with the provided one; '+FLAGS' means to add the provided flags; - # and '-FLAGS' means to remove them. +flags+ is a list of flags. - # - # The return value is an array of Net::IMAP::FetchData. For example: - # - # 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]}>] - def store(set, attr, flags) - return store_internal("STORE", set, attr, flags) - end - - # As for #store(), but +set+ contains unique identifiers. - def uid_store(set, attr, flags) - return store_internal("UID STORE", set, attr, flags) - end - - # 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. - def copy(set, mailbox) - copy_internal("COPY", set, mailbox) - end - - # As for #copy(), but +set+ contains unique identifiers. - def uid_copy(set, mailbox) - copy_internal("UID COPY", set, mailbox) - end - - # Sends a SORT command to sort messages in the mailbox. - # Returns an array of message sequence numbers. For example: - # - # 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] - # - # See [SORT-THREAD-EXT] for more details. - def sort(sort_keys, search_keys, charset) - return sort_internal("SORT", sort_keys, search_keys, charset) - end - - # As for #sort(), but returns an array of unique identifiers. - def uid_sort(sort_keys, search_keys, charset) - return sort_internal("UID SORT", sort_keys, search_keys, charset) - end - - # Adds a response handler. For example, to detect when - # the server sends us a new EXISTS response (which normally - # indicates new messages being added to the mail box), - # you could add the following handler after selecting the - # mailbox. - # - # imap.add_response_handler { |resp| - # if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS" - # puts "Mailbox now has #{resp.data} messages" - # end - # } - # - def add_response_handler(handler = Proc.new) - @response_handlers.push(handler) - end - - # Removes the response handler. - def remove_response_handler(handler) - @response_handlers.delete(handler) - end - - # As for #search(), but returns message sequence numbers in threaded - # format, as a Net::IMAP::ThreadMember tree. The supported algorithms - # are: - # - # ORDEREDSUBJECT:: split into single-level threads according to subject, - # ordered by date. - # REFERENCES:: split into threads by parent/child relationships determined - # by which message is a reply to which. - # - # Unlike #search(), +charset+ is a required argument. US-ASCII - # and UTF-8 are sample values. - # - # See [SORT-THREAD-EXT] for more details. - def thread(algorithm, search_keys, charset) - return thread_internal("THREAD", algorithm, search_keys, charset) - end - - # As for #thread(), but returns unique identifiers instead of - # message sequence numbers. - def uid_thread(algorithm, search_keys, charset) - return thread_internal("UID THREAD", algorithm, search_keys, charset) - end - - # Decode a string from modified UTF-7 format to UTF-8. - # - # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a - # slightly modified version of this to encode mailbox names - # containing non-ASCII characters; see [IMAP] section 5.1.3. - # - # Net::IMAP does _not_ automatically encode and decode - # mailbox names to and from utf7. - def self.decode_utf7(s) - return s.gsub(/&(.*?)-/n) { - if $1.empty? - "&" - else - base64 = $1.tr(",", "/") - x = base64.length % 4 - if x > 0 - base64.concat("=" * (4 - x)) - end - base64.unpack("m")[0].unpack("n*").pack("U*") - end - } - end - - # Encode a string from UTF-8 format to modified UTF-7. - def self.encode_utf7(s) - return s.gsub(/(&)|([^\x20-\x25\x27-\x7e]+)/n) { |x| - if $1 - "&-" - else - base64 = [x.unpack("U*").pack("n*")].pack("m") - "&" + base64.delete("=\n").tr("/", ",") + "-" - end - } - end - - private - - CRLF = "\r\n" # :nodoc: - PORT = 143 # :nodoc: - - @@debug = false - @@authenticators = {} - - # Creates a new Net::IMAP object and connects it to the specified - # +port+ (143 by default) on the named +host+. If +usessl+ is true, - # then an attempt will - # be made to use SSL (now TLS) to connect to the server. For this - # to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] - # extensions need to be installed. The +certs+ parameter indicates - # the path or file containing the CA cert of the server, and the - # +verify+ parameter is for the OpenSSL verification callback. - # - # The most common errors are: - # - # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening - # firewall. - # Errno::ETIMEDOUT:: connection timed out (possibly due to packets - # being dropped by an intervening firewall). - # Errno::ENETUNREACH:: there is no route to that network. - # SocketError:: hostname not known or other socket error. - # Net::IMAP::ByeResponseError:: we connected to the host, but they - # immediately said goodbye to us. - def initialize(host, port = PORT, usessl = false, certs = nil, verify = false) - super() - @host = host - @port = port - @tag_prefix = "RUBY" - @tagno = 0 - @parser = ResponseParser.new - @sock = TCPSocket.open(host, port) - if usessl - unless defined?(OpenSSL) - raise "SSL extension not installed" - end - @usessl = true - - # verify the server. - context = SSLContext::new() - context.ca_file = certs if certs && FileTest::file?(certs) - context.ca_path = certs if certs && FileTest::directory?(certs) - context.verify_mode = VERIFY_PEER if verify - if defined?(VerifyCallbackProc) - context.verify_callback = VerifyCallbackProc - end - @sock = SSLSocket.new(@sock, context) - @sock.connect # start ssl session. - else - @usessl = false - end - @responses = Hash.new([].freeze) - @tagged_responses = {} - @response_handlers = [] - @tagged_response_arrival = new_cond - @continuation_request_arrival = new_cond - @logout_command_tag = nil - @debug_output_bol = true - - @greeting = get_response - if @greeting.name == "BYE" - @sock.close - raise ByeResponseError, @greeting.raw_data - end - - @client_thread = Thread.current - @receiver_thread = Thread.start { - receive_responses - } - end - - def receive_responses - while true - begin - resp = get_response - rescue Exception - @sock.close - @client_thread.raise($!) - break - end - break unless resp - begin - synchronize do - case resp - when TaggedResponse - @tagged_responses[resp.tag] = resp - @tagged_response_arrival.broadcast - if resp.tag == @logout_command_tag - return - end - when UntaggedResponse - record_response(resp.name, resp.data) - if resp.data.instance_of?(ResponseText) && - (code = resp.data.code) - record_response(code.name, code.data) - end - if resp.name == "BYE" && @logout_command_tag.nil? - @sock.close - raise ByeResponseError, resp.raw_data - end - when ContinuationRequest - @continuation_request_arrival.signal - end - @response_handlers.each do |handler| - handler.call(resp) - end - end - rescue Exception - @client_thread.raise($!) - end - end - end - - def get_tagged_response(tag, cmd) - until @tagged_responses.key?(tag) - @tagged_response_arrival.wait - end - resp = @tagged_responses.delete(tag) - 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 - 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 send_command(cmd, *args, &block) - synchronize do - tag = generate_tag - put_string(tag + " " + cmd) - args.each do |i| - put_string(" ") - send_data(i) - end - put_string(CRLF) - if cmd == "LOGOUT" - @logout_command_tag = tag - end - if block - add_response_handler(block) - end - begin - return get_tagged_response(tag, cmd) - ensure - if block - remove_response_handler(block) - end - end - end - end - - def generate_tag - @tagno += 1 - return format("%s%04d", @tag_prefix, @tagno) - end - - def put_string(str) - @sock.print(str) - if @@debug - if @debug_output_bol - $stderr.print("C: ") - end - $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: ")) - if /\r\n\z/n.match(str) - @debug_output_bol = true - else - @debug_output_bol = false - end - end - end - - def send_data(data) - case data - when nil - put_string("NIL") - when String - send_string_data(data) - when Integer - send_number_data(data) - when Array - send_list_data(data) - when Time - send_time_data(data) - when Symbol - send_symbol_data(data) - else - data.send_data(self) - end - end - - def send_string_data(str) - case str - when "" - put_string('""') - when /[\x80-\xff\r\n]/n - # literal - send_literal(str) - when /[(){ \x00-\x1f\x7f%*"\\]/n - # quoted string - send_quoted_string(str) - else - put_string(str) - end - end - - def send_quoted_string(str) - put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"') - end - - def send_literal(str) - put_string("{" + str.length.to_s + "}" + CRLF) - @continuation_request_arrival.wait - put_string(str) - end - - def send_number_data(num) - if num < 0 || num >= 4294967296 - raise DataFormatError, num.to_s - end - put_string(num.to_s) - end - - def send_list_data(list) - put_string("(") - first = true - list.each do |i| - if first - first = false - else - put_string(" ") - end - send_data(i) - end - put_string(")") - end - - DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) - - def send_time_data(time) - t = time.dup.gmtime - s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"', - t.day, DATE_MONTH[t.month - 1], t.year, - t.hour, t.min, t.sec) - put_string(s) - end - - def send_symbol_data(symbol) - put_string("\\" + 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 - synchronize do - if charset - send_command(cmd, "CHARSET", charset, *keys) - else - send_command(cmd, *keys) - end - return @responses.delete("SEARCH")[-1] - end - end - - def fetch_internal(cmd, set, attr) - if attr.instance_of?(String) - attr = RawData.new(attr) - end - synchronize do - @responses.delete("FETCH") - send_command(cmd, MessageSet.new(set), attr) - return @responses.delete("FETCH") - end - end - - def store_internal(cmd, set, attr, flags) - if attr.instance_of?(String) - attr = RawData.new(attr) - end - synchronize do - @responses.delete("FETCH") - send_command(cmd, MessageSet.new(set), attr, flags) - return @responses.delete("FETCH") - end - 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) - synchronize do - send_command(cmd, sort_keys, charset, *search_keys) - return @responses.delete("SORT")[-1] - end - end - - def thread_internal(cmd, algorithm, 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, algorithm, charset, *search_keys) - return @responses.delete("THREAD")[-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 # :nodoc: - def send_data(imap) - imap.funcall(:put_string, @data) - end - - private - - def initialize(data) - @data = data - end - end - - class Atom # :nodoc: - def send_data(imap) - imap.funcall(:put_string, @data) - end - - private - - def initialize(data) - @data = data - end - end - - class QuotedString # :nodoc: - def send_data(imap) - imap.funcall(:send_quoted_string, @data) - end - - private - - def initialize(data) - @data = data - end - end - - class Literal # :nodoc: - def send_data(imap) - imap.funcall(:send_literal, @data) - end - - private - - def initialize(data) - @data = data - end - end - - class MessageSet # :nodoc: - def send_data(imap) - imap.funcall(:put_string, 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(",") - when ThreadMember - return data.seqno.to_s + - ":" + data.children.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 - msg = "nz_number must be non-zero unsigned 32-bit integer: " + - num.inspect - raise DataFormatError, msg - end - end - end - - # Net::IMAP::ContinuationRequest represents command continuation requests. - # - # The command continuation request response is indicated by a "+" token - # instead of a tag. This form of response indicates that the server is - # ready to accept the continuation of a command from the client. The - # remainder of this response is a line of text. - # - # continue_req ::= "+" SPACE (resp_text / base64) - # - # ==== Fields: - # - # data:: Returns the data (Net::IMAP::ResponseText). - # - # raw_data:: Returns the raw data string. - ContinuationRequest = Struct.new(:data, :raw_data) - - # Net::IMAP::UntaggedResponse represents untagged responses. - # - # Data transmitted by the server to the client and status responses - # that do not indicate command completion are prefixed with the token - # "*", and are called untagged responses. - # - # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye / - # mailbox_data / message_data / capability_data) - # - # ==== Fields: - # - # name:: Returns the name such as "FLAGS", "LIST", "FETCH".... - # - # data:: Returns the data such as an array of flag symbols, - # a ((<Net::IMAP::MailboxList>)) object.... - # - # raw_data:: Returns the raw data string. - UntaggedResponse = Struct.new(:name, :data, :raw_data) - - # Net::IMAP::TaggedResponse represents tagged responses. - # - # The server completion result response indicates the success or - # failure of the operation. It is tagged with the same tag as the - # client command which began the operation. - # - # response_tagged ::= tag SPACE resp_cond_state CRLF - # - # tag ::= 1*<any ATOM_CHAR except "+"> - # - # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text - # - # ==== Fields: - # - # tag:: Returns the tag. - # - # name:: Returns the name. the name is one of "OK", "NO", "BAD". - # - # data:: Returns the data. See ((<Net::IMAP::ResponseText>)). - # - # raw_data:: Returns the raw data string. - # - TaggedResponse = Struct.new(:tag, :name, :data, :raw_data) - - # Net::IMAP::ResponseText represents texts of responses. - # The text may be prefixed by the response code. - # - # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text) - # ;; text SHOULD NOT begin with "[" or "=" - # - # ==== Fields: - # - # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)). - # - # text:: Returns the text. - # - ResponseText = Struct.new(:code, :text) - - # - # Net::IMAP::ResponseCode represents response codes. - # - # resp_text_code ::= "ALERT" / "PARSE" / - # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" / - # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" / - # "UIDVALIDITY" SPACE nz_number / - # "UNSEEN" SPACE nz_number / - # atom [SPACE 1*<any TEXT_CHAR except "]">] - # - # ==== Fields: - # - # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY".... - # - # data:: Returns the data if it exists. - # - ResponseCode = Struct.new(:name, :data) - - # Net::IMAP::MailboxList represents contents of the LIST response. - # - # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" / - # "\Noselect" / "\Unmarked" / flag_extension) ")" - # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox - # - # ==== Fields: - # - # attr:: Returns the name attributes. Each name attribute is a symbol - # capitalized by String#capitalize, such as :Noselect (not :NoSelect). - # - # delim:: Returns the hierarchy delimiter - # - # name:: Returns the mailbox name. - # - MailboxList = Struct.new(:attr, :delim, :name) - - # Net::IMAP::MailboxQuota represents contents of GETQUOTA response. - # This object can also be a response to GETQUOTAROOT. In the syntax - # specification below, the delimiter used with the "#" construct is a - # single space (SPACE). - # - # quota_list ::= "(" #quota_resource ")" - # - # quota_resource ::= atom SPACE number SPACE number - # - # quota_response ::= "QUOTA" SPACE astring SPACE quota_list - # - # ==== Fields: - # - # mailbox:: The mailbox with the associated quota. - # - # usage:: Current storage usage of mailbox. - # - # quota:: Quota limit imposed on mailbox. - # - MailboxQuota = Struct.new(:mailbox, :usage, :quota) - - # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT - # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.) - # - # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring) - # - # ==== Fields: - # - # mailbox:: The mailbox with the associated quota. - # - # quotaroots:: Zero or more quotaroots that effect the quota on the - # specified mailbox. - # - MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots) - - # Net::IMAP::MailboxACLItem represents response from GETACL. - # - # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights) - # - # identifier ::= astring - # - # rights ::= astring - # - # ==== Fields: - # - # user:: Login name that has certain rights to the mailbox - # that was specified with the getacl command. - # - # rights:: The access rights the indicated user has to the - # mailbox. - # - MailboxACLItem = Struct.new(:user, :rights) - - # Net::IMAP::StatusData represents contents of the STATUS response. - # - # ==== Fields: - # - # mailbox:: Returns the mailbox name. - # - # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT", - # "UIDVALIDITY", "UNSEEN". Each value is a number. - # - StatusData = Struct.new(:mailbox, :attr) - - # Net::IMAP::FetchData represents contents of the FETCH response. - # - # ==== Fields: - # - # seqno:: Returns the message sequence number. - # (Note: not the unique identifier, even for the UID command response.) - # - # attr:: Returns a hash. Each key is a data item name, and each value is - # its value. - # - # The current data items are: - # - # [BODY] - # A form of BODYSTRUCTURE without extension data. - # [BODY[<section>]<<origin_octet>>] - # A string expressing the body contents of the specified section. - # [BODYSTRUCTURE] - # An object that describes the [MIME-IMB] body structure of a message. - # See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText, - # Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart. - # [ENVELOPE] - # A Net::IMAP::Envelope object that describes the envelope - # structure of a message. - # [FLAGS] - # A array of flag symbols that are set for this message. flag symbols - # are capitalized by String#capitalize. - # [INTERNALDATE] - # A string representing the internal date of the message. - # [RFC822] - # Equivalent to BODY[]. - # [RFC822.HEADER] - # Equivalent to BODY.PEEK[HEADER]. - # [RFC822.SIZE] - # A number expressing the [RFC-822] size of the message. - # [RFC822.TEXT] - # Equivalent to BODY[TEXT]. - # [UID] - # A number expressing the unique identifier of the message. - # - FetchData = Struct.new(:seqno, :attr) - - # Net::IMAP::Envelope represents envelope structures of messages. - # - # ==== Fields: - # - # date:: Returns a string that represents the date. - # - # subject:: Returns a string that represents the subject. - # - # from:: Returns an array of Net::IMAP::Address that represents the from. - # - # sender:: Returns an array of Net::IMAP::Address that represents the sender. - # - # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to. - # - # to:: Returns an array of Net::IMAP::Address that represents the to. - # - # cc:: Returns an array of Net::IMAP::Address that represents the cc. - # - # bcc:: Returns an array of Net::IMAP::Address that represents the bcc. - # - # in_reply_to:: Returns a string that represents the in-reply-to. - # - # message_id:: Returns a string that represents the message-id. - # - Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to, - :to, :cc, :bcc, :in_reply_to, :message_id) - - # - # Net::IMAP::Address represents electronic mail addresses. - # - # ==== Fields: - # - # name:: Returns the phrase from [RFC-822] mailbox. - # - # route:: Returns the route from [RFC-822] route-addr. - # - # mailbox:: nil indicates end of [RFC-822] group. - # If non-nil and host is nil, returns [RFC-822] group name. - # Otherwise, returns [RFC-822] local-part - # - # host:: nil indicates [RFC-822] group syntax. - # Otherwise, returns [RFC-822] domain name. - # - Address = Struct.new(:name, :route, :mailbox, :host) - - # - # Net::IMAP::ContentDisposition represents Content-Disposition fields. - # - # ==== Fields: - # - # dsp_type:: Returns the disposition type. - # - # param:: Returns a hash that represents parameters of the Content-Disposition - # field. - # - ContentDisposition = Struct.new(:dsp_type, :param) - - # Net::IMAP::ThreadMember represents a thread-node returned - # by Net::IMAP#thread - # - # ==== Fields: - # - # seqno:: The sequence number of this message. - # - # children:: an array of Net::IMAP::ThreadMember objects for mail - # items that are children of this in the thread. - # - ThreadMember = Struct.new(:seqno, :children) - - # Net::IMAP::BodyTypeBasic represents basic body structures of messages. - # - # ==== Fields: - # - # media_type:: Returns the content media type name as defined in [MIME-IMB]. - # - # subtype:: Returns the content subtype name as defined in [MIME-IMB]. - # - # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. - # - # content_id:: Returns a string giving the content id as defined in [MIME-IMB]. - # - # description:: Returns a string giving the content description as defined in - # [MIME-IMB]. - # - # encoding:: Returns a string giving the content transfer encoding as defined in - # [MIME-IMB]. - # - # size:: Returns a number giving the size of the body in octets. - # - # md5:: Returns a string giving the body MD5 value as defined in [MD5]. - # - # disposition:: Returns a Net::IMAP::ContentDisposition object giving - # the content disposition. - # - # language:: Returns a string or an array of strings giving the body - # language value as defined in [LANGUAGE-TAGS]. - # - # extension:: Returns extension data. - # - # multipart?:: Returns false. - # - class BodyTypeBasic < Struct.new(:media_type, :subtype, - :param, :content_id, - :description, :encoding, :size, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - - # Obsolete: use +subtype+ instead. Calling this will - # generate a warning message to +stderr+, then return - # the value of +subtype+. - def media_subtype - $stderr.printf("warning: media_subtype is obsolete.\n") - $stderr.printf(" use subtype instead.\n") - return subtype - end - end - - # Net::IMAP::BodyTypeText represents TEXT body structures of messages. - # - # ==== Fields: - # - # lines:: Returns the size of the body in text lines. - # - # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic. - # - class BodyTypeText < Struct.new(:media_type, :subtype, - :param, :content_id, - :description, :encoding, :size, - :lines, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - - # Obsolete: use +subtype+ instead. Calling this will - # generate a warning message to +stderr+, then return - # the value of +subtype+. - def media_subtype - $stderr.printf("warning: media_subtype is obsolete.\n") - $stderr.printf(" use subtype instead.\n") - return subtype - end - end - - # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages. - # - # ==== Fields: - # - # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure. - # - # body:: Returns an object giving the body structure. - # - # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText. - # - class BodyTypeMessage < Struct.new(:media_type, :subtype, - :param, :content_id, - :description, :encoding, :size, - :envelope, :body, :lines, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - - # Obsolete: use +subtype+ instead. Calling this will - # generate a warning message to +stderr+, then return - # the value of +subtype+. - def media_subtype - $stderr.printf("warning: media_subtype is obsolete.\n") - $stderr.printf(" use subtype instead.\n") - return subtype - end - end - - # Net::IMAP::BodyTypeMultipart represents multipart body structures - # of messages. - # - # ==== Fields: - # - # media_type:: Returns the content media type name as defined in [MIME-IMB]. - # - # subtype:: Returns the content subtype name as defined in [MIME-IMB]. - # - # parts:: Returns multiple parts. - # - # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. - # - # disposition:: Returns a Net::IMAP::ContentDisposition object giving - # the content disposition. - # - # language:: Returns a string or an array of strings giving the body - # language value as defined in [LANGUAGE-TAGS]. - # - # extension:: Returns extension data. - # - # multipart?:: Returns true. - # - class BodyTypeMultipart < Struct.new(:media_type, :subtype, - :parts, - :param, :disposition, :language, - :extension) - def multipart? - return true - end - - # Obsolete: use +subtype+ instead. Calling this will - # generate a warning message to +stderr+, then return - # the value of +subtype+. - def media_subtype - $stderr.printf("warning: media_subtype is obsolete.\n") - $stderr.printf(" use subtype instead.\n") - return subtype - end - end - - class ResponseParser # :nodoc: - 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 )"((?:[^\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 )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ -(?# 5: LITERAL )\{(\d+)\}\r\n|\ -(?# 6: LPAR )(\()|\ -(?# 7: RPAR )(\)))/ni - - TEXT_REGEXP = /\G(?:\ -(?# 1: TEXT )([^\x00\r\n]*))/ni - - RTEXT_REGEXP = /\G(?:\ -(?# 1: LBRA )(\[)|\ -(?# 2: TEXT )([^\x00\r\n]*))/ni - - CTEXT_REGEXP = /\G(?:\ -(?# 1: TEXT )([^\x00\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 ContinuationRequest.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(?:QUOTA)\z/ni - return getquota_response - when /\A(?:QUOTAROOT)\z/ni - return getquotaroot_response - when /\A(?:ACL)\z/ni - return getacl_response - when /\A(?:SEARCH|SORT)\z/ni - return search_response - when /\A(?:THREAD)\z/ni - return thread_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 - token = lookahead - if token.symbol == T_NIL - shift_token - result = nil - else - 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) - result = Envelope.new(date, subject, from, sender, reply_to, - to, cc, bcc, in_reply_to, message_id) - end - @lex_state = EXPR_BEG - return result - 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 - token = lookahead - if token.symbol == T_NIL - shift_token - result = nil - else - match(T_LPAR) - token = lookahead - if token.symbol == T_LPAR - result = body_type_mpart - else - result = body_type_1part - end - match(T_RPAR) - end - @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 - token = lookahead - if token.symbol == T_RPAR - return BodyTypeBasic.new(mtype, msubtype) - end - 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 = case_insensitive_string - param, disposition, language, extension = body_ext_mpart - return BodyTypeMultipart.new(mtype, msubtype, parts, - param, disposition, language, - extension) - end - - def media_type - mtype = case_insensitive_string - match(T_SPACE) - msubtype = case_insensitive_string - 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 = case_insensitive_string - 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 = case_insensitive_string - 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 = case_insensitive_string - 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(case_insensitive_string) - 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 getquota_response - # If quota never established, get back - # `NO Quota root does not exist'. - # If quota removed, get `()' after the - # folder spec with no mention of `STORAGE'. - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - mailbox = astring - match(T_SPACE) - match(T_LPAR) - token = lookahead - case token.symbol - when T_RPAR - shift_token - data = MailboxQuota.new(mailbox, nil, nil) - return UntaggedResponse.new(name, data, @str) - when T_ATOM - shift_token - match(T_SPACE) - token = match(T_NUMBER) - usage = token.value - match(T_SPACE) - token = match(T_NUMBER) - quota = token.value - match(T_RPAR) - data = MailboxQuota.new(mailbox, usage, quota) - return UntaggedResponse.new(name, data, @str) - else - parse_error("unexpected token %s", token.symbol) - end - end - - def getquotaroot_response - # Similar to getquota, but only admin can use getquota. - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - mailbox = astring - quotaroots = [] - while true - token = lookahead - break unless token.symbol == T_SPACE - shift_token - quotaroots.push(astring) - end - data = MailboxQuotaRoot.new(mailbox, quotaroots) - return UntaggedResponse.new(name, data, @str) - end - - def getacl_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - mailbox = astring - data = [] - token = lookahead - if token.symbol == T_SPACE - shift_token - while true - token = lookahead - case token.symbol - when T_CRLF - break - when T_SPACE - shift_token - end - user = astring - match(T_SPACE) - rights = astring - ##XXX data.push([user, rights]) - data.push(MailboxACLItem.new(user, rights)) - end - end - return UntaggedResponse.new(name, data, @str) - 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 thread_response - token = match(T_ATOM) - name = token.value.upcase - token = lookahead - - if token.symbol == T_SPACE - threads = [] - - while true - shift_token - token = lookahead - - case token.symbol - when T_LPAR - threads << thread_branch(token) - when T_CRLF - break - end - end - else - # no member - threads = [] - end - - return UntaggedResponse.new(name, threads, @str) - end - - def thread_branch(token) - rootmember = nil - lastmember = nil - - while true - shift_token # ignore first T_LPAR - token = lookahead - - case token.symbol - when T_NUMBER - # new member - newmember = ThreadMember.new(number, []) - if rootmember.nil? - rootmember = newmember - else - lastmember.children << newmember - end - lastmember = newmember - when T_SPACE - # do nothing - when T_LPAR - if rootmember.nil? - # dummy member - lastmember = rootmember = ThreadMember.new(nil, []) - end - - lastmember.children << thread_branch(token) - when T_RPAR - break - end - end - - return rootmember - 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|NOMODSEQ)\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) - else - match(T_SPACE) - @lex_state = EXPR_CTEXT - token = match(T_TEXT) - @lex_state = EXPR_BEG - result = ResponseCode.new(name, token.value) - 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.capitalize.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 = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - token = match(T_QUOTED, T_LITERAL) - return token.value - end - - STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL] - - def string_token?(token) - return STRING_TOKENS.include?(token.symbol) - end - - def case_insensitive_string - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - token = match(T_QUOTED, T_LITERAL) - return token.value.upcase - 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 = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - 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 - - # Authenticator for the "LOGIN" authentication type. See - # #authenticate(). - 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 - - # Authenticator for the "PLAIN" authentication type. See - # #authenticate(). - class PlainAuthenticator - def process(data) - return "\0#{@user}\0#{@password}" - end - - private - - def initialize(user, password) - @user = user - @password = password - end - end - add_authenticator "PLAIN", PlainAuthenticator - - # Authenticator for the "CRAM-MD5" authentication type. See - # #authenticate(). - 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 - key = Digest::MD5.digest(key) - 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 - - digest = Digest::MD5.digest(k_ipad + text) - - return Digest::MD5.hexdigest(k_opad + digest) - end - end - add_authenticator "CRAM-MD5", CramMD5Authenticator - - # Authenticator for the "DIGEST-MD5" authentication type. See - # #authenticate(). - class DigestMD5Authenticator - def process(challenge) - case @stage - when STAGE_ONE - @stage = STAGE_TWO - sparams = {} - c = StringScanner.new(challenge) - while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/) - k, v = c[1], c[2] - if v =~ /^"(.*)"$/ - v = $1 - if v =~ /,/ - v = v.split(',') - end - end - sparams[k] = v - end - - raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0 - raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth") - - response = { - :nonce => sparams['nonce'], - :username => @user, - :realm => sparams['realm'], - :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]), - :'digest-uri' => 'imap/' + sparams['realm'], - :qop => 'auth', - :maxbuf => 65535, - :nc => "%08d" % nc(sparams['nonce']), - :charset => sparams['charset'], - } - - response[:authzid] = @authname unless @authname.nil? - - # now, the real thing - a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') ) - - a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':') - a1 << ':' + response[:authzid] unless response[:authzid].nil? - - a2 = "AUTHENTICATE:" + response[:'digest-uri'] - a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/ - - response[:response] = Digest::MD5.hexdigest( - [ - Digest::MD5.hexdigest(a1), - response.values_at(:nonce, :nc, :cnonce, :qop), - Digest::MD5.hexdigest(a2) - ].join(':') - ) - - return response.keys.map { |k| qdval(k.to_s, response[k]) }.join(',') - when STAGE_TWO - @stage = nil - # if at the second stage, return an empty string - if challenge =~ /rspauth=/ - return '' - else - raise ResponseParseError, challenge - end - else - raise ResponseParseError, challenge - end - end - - def initialize(user, password, authname = nil) - @user, @password, @authname = user, password, authname - @nc, @stage = {}, STAGE_ONE - end - - private - - STAGE_ONE = :stage_one - STAGE_TWO = :stage_two - - def nc(nonce) - if @nc.has_key? nonce - @nc[nonce] = @nc[nonce] + 1 - else - @nc[nonce] = 1 - end - return @nc[nonce] - end - - # some reponses needs quoting - def qdval(k, v) - return if k.nil? or v.nil? - if %w"username authzid realm nonce cnonce digest-uri qop".include? k - v.gsub!(/([\\"])/, "\\\1") - return '%s="%s"' % [k, v] - else - return '%s=%s' % [k, v] - end - end - end - add_authenticator "DIGEST-MD5", DigestMD5Authenticator - - # Superclass of IMAP errors. - class Error < StandardError - end - - # Error raised when data is in the incorrect format. - class DataFormatError < Error - end - - # Error raised when a response from the server is non-parseable. - class ResponseParseError < Error - end - - # Superclass of all errors used to encapsulate "fail" responses - # from the server. - class ResponseError < Error - end - - # Error raised upon a "NO" response from the server, indicating - # that the client command could not be completed successfully. - class NoResponseError < ResponseError - end - - # Error raised upon a "BAD" response from the server, indicating - # that the client command violated the IMAP protocol, or an internal - # server failure has occurred. - class BadResponseError < ResponseError - end - - # Error raised upon a "BYE" response from the server, indicating - # that the client is not being allowed to login, or has been timed - # out due to inactivity. - class ByeResponseError < ResponseError - end - end -end - -if __FILE__ == $0 - # :enddoc: - require "getoptlong" - - $stdout.sync = true - $port = nil - $user = ENV["USER"] || ENV["LOGNAME"] - $auth = "login" - $ssl = false - - def usage - $stderr.print <<EOF -usage: #{$0} [options] <host> - - --help print this message - --port=PORT specifies port - --user=USER specifies user - --auth=AUTH specifies auth type - --ssl use ssl -EOF - end - - def get_password - print "password: " - system("stty", "-echo") - begin - return gets.chop - ensure - system("stty", "echo") - print "\n" - end - end - - def get_command - printf("%s@%s> ", $user, $host) - if line = gets - return line.strip.split(/\s+/) - else - return nil - end - end - - parser = GetoptLong.new - parser.set_options(['--debug', GetoptLong::NO_ARGUMENT], - ['--help', GetoptLong::NO_ARGUMENT], - ['--port', GetoptLong::REQUIRED_ARGUMENT], - ['--user', GetoptLong::REQUIRED_ARGUMENT], - ['--auth', GetoptLong::REQUIRED_ARGUMENT], - ['--ssl', GetoptLong::NO_ARGUMENT]) - begin - parser.each_option do |name, arg| - case name - when "--port" - $port = arg - when "--user" - $user = arg - when "--auth" - $auth = arg - when "--ssl" - $ssl = true - when "--debug" - Net::IMAP.debug = true - when "--help" - usage - exit(1) - end - end - rescue - usage - exit(1) - end - - $host = ARGV.shift - unless $host - usage - exit(1) - end - $port ||= $ssl ? 993 : 143 - - imap = Net::IMAP.new($host, $port, $ssl) - begin - password = get_password - imap.authenticate($auth, $user, password) - while true - cmd, *args = get_command - break unless cmd - begin - case cmd - when "list" - for mbox in imap.list("", args[0] || "*") - if mbox.attr.include?(Net::IMAP::NOSELECT) - prefix = "!" - elsif mbox.attr.include?(Net::IMAP::MARKED) - prefix = "*" - else - prefix = " " - end - print prefix, mbox.name, "\n" - end - when "select" - imap.select(args[0] || "inbox") - print "ok\n" - when "close" - imap.close - print "ok\n" - when "summary" - unless messages = imap.responses["EXISTS"][-1] - puts "not selected" - next - end - if messages > 0 - for data in imap.fetch(1..-1, ["ENVELOPE"]) - print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n" - end - else - puts "no message" - end - when "fetch" - if args[0] - data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0] - puts data.attr["RFC822.HEADER"] - puts data.attr["RFC822.TEXT"] - else - puts "missing argument" - end - when "logout", "exit", "quit" - break - when "help", "?" - print <<EOF -list [pattern] list mailboxes -select [mailbox] select mailbox -close close mailbox -summary display summary -fetch [msgno] display message -logout logout -help, ? display help message -EOF - else - print "unknown command: ", cmd, "\n" - end - rescue Net::IMAP::Error - puts $! - end - end - ensure - imap.logout - imap.disconnect - 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 d0145cb5f0..0000000000 --- a/lib/net/pop.rb +++ /dev/null @@ -1,988 +0,0 @@ -# = net/pop.rb -# -# Copyright (c) 1999-2007 Yukihiro Matsumoto. -# -# Copyright (c) 1999-2007 Minero Aoki. -# -# Written & maintained by Minero Aoki <aamine@loveruby.net>. -# -# Documented by William Webber and Minero Aoki. -# -# This program is free software. You can re-distribute and/or -# modify this program under the same terms as Ruby itself, -# Ruby Distribute License. -# -# NOTE: You can find Japanese version of this document at: -# http://www.ruby-lang.org/ja/man/index.cgi?cmd=view;name=net%2Fpop.rb -# -# $Id$ -# -# See Net::POP3 for documentation. -# - -require 'net/protocol' -require 'digest/md5' -require 'timeout' - -begin - require "openssl" -rescue LoadError -end - -module Net - - # Non-authentication POP3 protocol error - # (reply code "-ERR", except authentication). - class POPError < ProtocolError; end - - # POP3 authentication error. - class POPAuthenticationError < ProtoAuthError; end - - # Unexpected response from the server. - class POPBadResponse < POPError; end - - # - # = Net::POP3 - # - # == What is This Library? - # - # This library provides functionality for retrieving - # email via POP3, the Post Office Protocol version 3. For details - # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt). - # - # == Examples - # - # === Retrieving Messages - # - # This example retrieves messages from the server and deletes them - # on the server. - # - # Messages are written to files named 'inbox/1', 'inbox/2', .... - # Replace 'pop.example.com' with your POP3 server address, and - # 'YourAccount' and 'YourPassword' with the appropriate account - # details. - # - # require 'net/pop' - # - # pop = Net::POP3.new('pop.example.com') - # pop.start('YourAccount', 'YourPassword') # (1) - # if pop.mails.empty? - # puts 'No mail.' - # else - # i = 0 - # pop.each_mail do |m| # or "pop.mails.each ..." # (2) - # File.open("inbox/#{i}", 'w') do |f| - # f.write m.pop - # end - # m.delete - # i += 1 - # end - # puts "#{pop.mails.size} mails popped." - # end - # pop.finish # (3) - # - # 1. Call Net::POP3#start and start POP session. - # 2. Access messages by using POP3#each_mail and/or POP3#mails. - # 3. Close POP session by calling POP3#finish or use the block form of #start. - # - # === Shortened Code - # - # The example above is very verbose. You can shorten the code by using - # some utility methods. First, the block form of Net::POP3.start can - # be used instead of POP3.new, POP3#start and POP3#finish. - # - # require 'net/pop' - # - # Net::POP3.start('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |pop| - # if pop.mails.empty? - # puts 'No mail.' - # else - # i = 0 - # pop.each_mail do |m| # or "pop.mails.each ..." - # File.open("inbox/#{i}", 'w') do |f| - # f.write m.pop - # end - # m.delete - # i += 1 - # end - # puts "#{pop.mails.size} mails popped." - # end - # end - # - # POP3#delete_all is an alternative for #each_mail and #delete. - # - # require 'net/pop' - # - # Net::POP3.start('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |pop| - # if pop.mails.empty? - # puts 'No mail.' - # else - # i = 1 - # pop.delete_all do |m| - # File.open("inbox/#{i}", 'w') do |f| - # f.write m.pop - # end - # i += 1 - # end - # end - # end - # - # And here is an even shorter example. - # - # require 'net/pop' - # - # i = 0 - # Net::POP3.delete_all('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |m| - # File.open("inbox/#{i}", 'w') do |f| - # f.write m.pop - # end - # i += 1 - # end - # - # === Memory Space Issues - # - # All the examples above get each message as one big string. - # This example avoids this. - # - # require 'net/pop' - # - # i = 1 - # Net::POP3.delete_all('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |m| - # File.open("inbox/#{i}", 'w') do |f| - # m.pop do |chunk| # get a message little by little. - # f.write chunk - # end - # i += 1 - # end - # end - # - # === Using APOP - # - # The net/pop library supports APOP authentication. - # To use APOP, use the Net::APOP class instead of the Net::POP3 class. - # You can use the utility method, Net::POP3.APOP(). For example: - # - # require 'net/pop' - # - # # Use APOP authentication if $isapop == true - # pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110) - # pop.start(YourAccount', 'YourPassword') do |pop| - # # Rest of the code is the same. - # end - # - # === Fetch Only Selected Mail Using 'UIDL' POP Command - # - # If your POP server provides UIDL functionality, - # you can grab only selected mails from the POP server. - # e.g. - # - # def need_pop?( id ) - # # determine if we need pop this mail... - # end - # - # Net::POP3.start('pop.example.com', 110, - # 'Your account', 'Your password') do |pop| - # pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m| - # do_something(m.pop) - # end - # end - # - # The POPMail#unique_id() method returns the unique-id of the message as a - # String. Normally the unique-id is a hash of the message. - # - class POP3 < Protocol - - Revision = %q$Revision$.split[1] - - # - # Class Parameters - # - - def POP3.default_port - default_pop3_port() - end - - # The default port for POP3 connections, port 110 - def POP3.default_pop3_port - 110 - end - - # The default port for POP3S connections, port 995 - def POP3.default_pop3s_port - 995 - end - - def POP3.socket_type #:nodoc: obsolete - Net::InternetMessageIO - end - - # - # Utilities - # - - # Returns the APOP class if +isapop+ is true; otherwise, returns - # the POP class. For example: - # - # # Example 1 - # pop = Net::POP3::APOP($is_apop).new(addr, port) - # - # # Example 2 - # Net::POP3::APOP($is_apop).start(addr, port) do |pop| - # .... - # end - # - def POP3.APOP(isapop) - isapop ? APOP : POP3 - end - - # Starts a POP3 session and iterates over each POPMail object, - # yielding it to the +block+. - # This method is equivalent to: - # - # Net::POP3.start(address, port, account, password) do |pop| - # pop.each_mail do |m| - # yield m - # end - # end - # - # This method raises a POPAuthenticationError if authentication fails. - # - # === Example - # - # Net::POP3.foreach('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |m| - # file.write m.pop - # m.delete if $DELETE - # end - # - def POP3.foreach(address, port = nil, - account = nil, password = nil, - isapop = false, &block) # :yields: message - start(address, port, account, password, isapop) {|pop| - pop.each_mail(&block) - } - end - - # Starts a POP3 session and deletes all messages on the server. - # If a block is given, each POPMail object is yielded to it before - # being deleted. - # - # This method raises a POPAuthenticationError if authentication fails. - # - # === Example - # - # Net::POP3.delete_all('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |m| - # file.write m.pop - # end - # - def POP3.delete_all(address, port = nil, - account = nil, password = nil, - isapop = false, &block) - start(address, port, account, password, isapop) {|pop| - pop.delete_all(&block) - } - end - - # Opens a POP3 session, attempts authentication, and quits. - # - # This method raises POPAuthenticationError if authentication fails. - # - # === Example: normal POP3 - # - # Net::POP3.auth_only('pop.example.com', 110, - # 'YourAccount', 'YourPassword') - # - # === Example: APOP - # - # Net::POP3.auth_only('pop.example.com', 110, - # 'YourAccount', 'YourPassword', true) - # - def POP3.auth_only(address, port = nil, - account = nil, password = nil, - isapop = false) - new(address, port, isapop).auth_only account, password - end - - # Starts a pop3 session, attempts authentication, and quits. - # This method must not be called while POP3 session is opened. - # This method raises POPAuthenticationError if authentication fails. - def auth_only(account, password) - raise IOError, 'opening previously opened POP session' if started? - start(account, password) { - ; - } - end - - # - # SSL - # - - @use_ssl = false - @verify = nil - @certs = nil - - # Enable SSL for all new instances. - # +verify+ is the type of verification to do on the Server Cert; Defaults - # to OpenSSL::SSL::VERIFY_NONE. - # +certs+ is a file or directory holding CA certs to use to verify the - # server cert; Defaults to nil. - def POP3.enable_ssl(verify = OpenSSL::SSL::VERIFY_NONE, certs = nil) - @use_ssl = true - @verify = verify - @certs = certs - end - - # Disable SSL for all new instances. - def POP3.disable_ssl - @use_ssl = nil - @verify = nil - @certs = nil - end - - def POP3.use_ssl? - @use_ssl - end - - def POP3.verify - @verify - end - - def POP3.certs - @certs - end - - # - # Session management - # - - # Creates a new POP3 object and open the connection. Equivalent to - # - # Net::POP3.new(address, port, isapop).start(account, password) - # - # If +block+ is provided, yields the newly-opened POP3 object to it, - # and automatically closes it at the end of the session. - # - # === Example - # - # Net::POP3.start(addr, port, account, password) do |pop| - # pop.each_mail do |m| - # file.write m.pop - # m.delete - # end - # end - # - def POP3.start(address, port = nil, - account = nil, password = nil, - isapop = false, &block) # :yield: pop - new(address, port, isapop).start(account, password, &block) - end - - # Creates a new POP3 object. - # - # +address+ is the hostname or ip address of your POP3 server. - # - # The optional +port+ is the port to connect to. - # - # The optional +isapop+ specifies whether this connection is going - # to use APOP authentication; it defaults to +false+. - # - # This method does *not* open the TCP connection. - def initialize(addr, port = nil, isapop = false) - @address = addr - @use_ssl = POP3.use_ssl? - @port = port || (POP3.use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port) - @apop = isapop - @certs = POP3.certs - @verify = POP3.verify - - @command = nil - @socket = nil - @started = false - @open_timeout = 30 - @read_timeout = 60 - @debug_output = nil - - @mails = nil - @n_mails = nil - @n_bytes = nil - end - - # Does this instance use APOP authentication? - def apop? - @apop - end - - # does this instance use SSL? - def use_ssl? - @use_ssl - end - - # Enables SSL for this instance. Must be called before the connection is - # established to have any effect. - # +verify+ is the type of verification to do on the Server Cert; Defaults - # to OpenSSL::SSL::VERIFY_NONE. - # +certs+ is a file or directory holding CA certs to use to verify the - # server cert; Defaults to nil. - # +port+ is port to establish the SSL connection on; Defaults to 995. - def enable_ssl(verify = OpenSSL::SSL::VERIFY_NONE, certs = nil, - port = POP3.default_pop3s_port) - @use_ssl = true - @verify = verify - @certs = certs - @port = port - end - - def disable_ssl - @use_ssl = false - @verify = nil - @certs = nil - end - - # Provide human-readable stringification of class state. - def inspect - "#<#{self.class} #{@address}:#{@port} open=#{@started}>" - end - - # *WARNING*: This method causes a serious security hole. - # Use this method only for debugging. - # - # Set an output stream for debugging. - # - # === Example - # - # pop = Net::POP.new(addr, port) - # pop.set_debug_output $stderr - # pop.start(account, passwd) do |pop| - # .... - # end - # - def set_debug_output(arg) - @debug_output = arg - end - - # The address to connect to. - attr_reader :address - - # The port number to connect to. - attr_reader :port - - # Seconds to wait until a connection is opened. - # If the POP3 object cannot open a connection within this time, - # it raises a TimeoutError exception. - attr_accessor :open_timeout - - # Seconds to wait until reading one block (by one read(1) call). - # If the POP3 object cannot complete a read() within this time, - # it raises a TimeoutError exception. - attr_reader :read_timeout - - # Set the read timeout. - def read_timeout=(sec) - @command.socket.read_timeout = sec if @command - @read_timeout = sec - end - - # +true+ if the POP3 session has started. - def started? - @started - end - - alias active? started? #:nodoc: obsolete - - # Starts a POP3 session. - # - # When called with block, gives a POP3 object to the block and - # closes the session after block call finishes. - # - # This method raises a POPAuthenticationError if authentication fails. - def start(account, password) # :yield: pop - raise IOError, 'POP session already started' if @started - if block_given? - begin - do_start account, password - return yield(self) - ensure - do_finish - end - else - do_start account, password - return self - end - end - - def do_start(account, password) - s = timeout(@open_timeout) { TCPSocket.open(@address, @port) } - if use_ssl? - raise 'openssl library not installed' unless defined?(OpenSSL) - context = OpenSSL::SSL::SSLContext.new - context.verify_mode = @verify - if @certs - if File.file?(@certs) - context.ca_file = @certs - elsif File.directory?(@certs) - context.ca_path = @certs - else - raise ArgumentError, "certs path is not file/directory: #{@certs}" - end - end - s = OpenSSL::SSL::SSLSocket.new(s, context) - s.sync_close = true - s.connect - end - @socket = InternetMessageIO.new(s) - logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})" - @socket.read_timeout = @read_timeout - @socket.debug_output = @debug_output - on_connect - @command = POP3Command.new(@socket) - if apop? - @command.apop account, password - else - @command.auth account, password - end - @started = true - ensure - # Authentication failed, clean up connection. - unless @started - s.close if s and not s.closed? - @socket = nil - @command = nil - end - end - private :do_start - - def on_connect - end - private :on_connect - - # Finishes a POP3 session and closes TCP connection. - def finish - raise IOError, 'POP session not yet started' unless started? - do_finish - end - - def do_finish - @mails = nil - @command.quit if @command - ensure - @started = false - @command = nil - @socket.close if @socket and not @socket.closed? - @socket = nil - end - private :do_finish - - def command - raise IOError, 'POP session not opened yet' \ - if not @socket or @socket.closed? - @command - end - private :command - - # - # POP protocol wrapper - # - - # Returns the number of messages on the POP server. - def n_mails - return @n_mails if @n_mails - @n_mails, @n_bytes = command().stat - @n_mails - end - - # Returns the total size in bytes of all the messages on the POP server. - def n_bytes - return @n_bytes if @n_bytes - @n_mails, @n_bytes = command().stat - @n_bytes - end - - # Returns an array of Net::POPMail objects, representing all the - # messages on the server. This array is renewed when the session - # restarts; otherwise, it is fetched from the server the first time - # this method is called (directly or indirectly) and cached. - # - # This method raises a POPError if an error occurs. - def mails - return @mails.dup if @mails - if n_mails() == 0 - # some popd raises error for LIST on the empty mailbox. - @mails = [] - return [] - end - - @mails = command().list.map {|num, size| - POPMail.new(num, size, self, command()) - } - @mails.dup - end - - # Yields each message to the passed-in block in turn. - # Equivalent to: - # - # pop3.mails.each do |popmail| - # .... - # end - # - # This method raises a POPError if an error occurs. - def each_mail(&block) # :yield: message - mails().each(&block) - end - - alias each each_mail - - # Deletes all messages on the server. - # - # If called with a block, yields each message in turn before deleting it. - # - # === Example - # - # n = 1 - # pop.delete_all do |m| - # File.open("inbox/#{n}") do |f| - # f.write m.pop - # end - # n += 1 - # end - # - # This method raises a POPError if an error occurs. - # - def delete_all # :yield: message - mails().each do |m| - yield m if block_given? - m.delete unless m.deleted? - end - end - - # Resets the session. This clears all "deleted" marks from messages. - # - # This method raises a POPError if an error occurs. - def reset - command().rset - mails().each do |m| - m.instance_eval { - @deleted = false - } - end - end - - def set_all_uids #:nodoc: internal use only (called from POPMail#uidl) - command().uidl.each do |num, uid| - @mails.find {|m| m.number == num }.uid = uid - end - end - - def logging(msg) - @debug_output << msg + "\n" if @debug_output - end - - end # class POP3 - - # class aliases - POP = POP3 - POPSession = POP3 - POP3Session = POP3 - - # - # This class is equivalent to POP3, except that it uses APOP authentication. - # - class APOP < POP3 - # Always returns true. - def apop? - true - end - end - - # class aliases - APOPSession = APOP - - # - # This class represents a message which exists on the POP server. - # Instances of this class are created by the POP3 class; they should - # not be directly created by the user. - # - class POPMail - - def initialize(num, len, pop, cmd) #:nodoc: - @number = num - @length = len - @pop = pop - @command = cmd - @deleted = false - @uid = nil - end - - # The sequence number of the message on the server. - attr_reader :number - - # The length of the message in octets. - attr_reader :length - alias size length - - # Provide human-readable stringification of class state. - def inspect - "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" - end - - # - # This method fetches the message. If called with a block, the - # message is yielded to the block one chunk at a time. If called - # without a block, the message is returned as a String. The optional - # +dest+ argument will be prepended to the returned String; this - # argument is essentially obsolete. - # - # === Example without block - # - # POP3.start('pop.example.com', 110, - # 'YourAccount, 'YourPassword') do |pop| - # n = 1 - # pop.mails.each do |popmail| - # File.open("inbox/#{n}", 'w') do |f| - # f.write popmail.pop - # end - # popmail.delete - # n += 1 - # end - # end - # - # === Example with block - # - # POP3.start('pop.example.com', 110, - # 'YourAccount, 'YourPassword') do |pop| - # n = 1 - # pop.mails.each do |popmail| - # File.open("inbox/#{n}", 'w') do |f| - # popmail.pop do |chunk| #### - # f.write chunk - # end - # end - # n += 1 - # end - # end - # - # This method raises a POPError if an error occurs. - # - def pop( dest = '', &block ) # :yield: message_chunk - if block_given? - @command.retr(@number, &block) - nil - else - @command.retr(@number) do |chunk| - dest << chunk - end - dest - end - end - - alias all pop #:nodoc: obsolete - alias mail pop #:nodoc: obsolete - - # Fetches the message header and +lines+ lines of body. - # - # The optional +dest+ argument is obsolete. - # - # This method raises a POPError if an error occurs. - def top(lines, dest = '') - @command.top(@number, lines) do |chunk| - dest << chunk - end - dest - end - - # Fetches the message header. - # - # The optional +dest+ argument is obsolete. - # - # This method raises a POPError if an error occurs. - def header(dest = '') - top(0, dest) - end - - # Marks a message for deletion on the server. Deletion does not - # actually occur until the end of the session; deletion may be - # cancelled for _all_ marked messages by calling POP3#reset(). - # - # This method raises a POPError if an error occurs. - # - # === Example - # - # POP3.start('pop.example.com', 110, - # 'YourAccount, 'YourPassword') do |pop| - # n = 1 - # pop.mails.each do |popmail| - # File.open("inbox/#{n}", 'w') do |f| - # f.write popmail.pop - # end - # popmail.delete #### - # n += 1 - # end - # end - # - def delete - @command.dele @number - @deleted = true - end - - alias delete! delete #:nodoc: obsolete - - # True if the mail has been deleted. - def deleted? - @deleted - end - - # Returns the unique-id of the message. - # Normally the unique-id is a hash string of the message. - # - # This method raises a POPError if an error occurs. - def unique_id - return @uid if @uid - @pop.set_all_uids - @uid - end - - alias uidl unique_id - - def uid=(uid) #:nodoc: internal use only - @uid = uid - end - - end # class POPMail - - - class POP3Command #:nodoc: internal use only - - def initialize(sock) - @socket = sock - @error_occured = false - res = check_response(critical { recv_response() }) - @apop_stamp = res.slice(/<.+>/) - end - - def inspect - "#<#{self.class} socket=#{@socket}>" - end - - def auth(account, password) - check_response_auth(critical { - check_response_auth(get_response('USER %s', account)) - get_response('PASS %s', password) - }) - end - - def apop(account, password) - raise POPAuthenticationError, 'not APOP server; cannot login' \ - unless @apop_stamp - check_response_auth(critical { - get_response('APOP %s %s', - account, - Digest::MD5.hexdigest(@apop_stamp + password)) - }) - end - - def list - critical { - getok 'LIST' - list = [] - @socket.each_list_item do |line| - m = /\A(\d+)[ \t]+(\d+)/.match(line) or - raise POPBadResponse, "bad response: #{line}" - list.push [m[1].to_i, m[2].to_i] - end - return list - } - end - - def stat - res = check_response(critical { get_response('STAT') }) - m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or - raise POPBadResponse, "wrong response format: #{res}" - [m[1].to_i, m[2].to_i] - end - - def rset - check_response(critical { get_response('RSET') }) - end - - def top(num, lines = 0, &block) - critical { - getok('TOP %d %d', num, lines) - @socket.each_message_chunk(&block) - } - end - - def retr(num, &block) - critical { - getok('RETR %d', num) - @socket.each_message_chunk(&block) - } - end - - def dele(num) - check_response(critical { get_response('DELE %d', num) }) - end - - def uidl(num = nil) - if num - res = check_response(critical { get_response('UIDL %d', num) }) - return res.split(/ /)[1] - else - critical { - getok('UIDL') - table = {} - @socket.each_list_item do |line| - num, uid = line.split - table[num.to_i] = uid - end - return table - } - end - end - - def quit - check_response(critical { get_response('QUIT') }) - end - - private - - def getok(fmt, *fargs) - @socket.writeline sprintf(fmt, *fargs) - check_response(recv_response()) - end - - def get_response(fmt, *fargs) - @socket.writeline sprintf(fmt, *fargs) - recv_response() - end - - def recv_response - @socket.readline - end - - def check_response(res) - raise POPError, res unless /\A\+OK/i =~ res - res - end - - def check_response_auth(res) - raise POPAuthenticationError, res unless /\A\+OK/i =~ res - res - end - - def critical - return '+OK dummy ok response' if @error_occured - begin - return yield() - rescue Exception - @error_occured = true - raise - end - end - - end # class POP3Command - -end # module Net diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index 0d489cdbc8..8c81298c0e 100644 --- a/lib/net/protocol.rb +++ b/lib/net/protocol.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # # = net/protocol.rb # @@ -20,10 +21,13 @@ require 'socket' require 'timeout' +require 'io/wait' module Net # :nodoc: class Protocol #:nodoc: internal use only + VERSION = "0.2.2" + private def Protocol.protocol_param(name, val) module_eval(<<-End, __FILE__, __LINE__ + 1) @@ -32,9 +36,38 @@ module Net # :nodoc: end 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 + timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + end + else + s.connect + end + end + + 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 end + # :stopdoc: class ProtocolError < StandardError; end class ProtoSyntaxError < ProtocolError; end class ProtoFatalError < ProtocolError; end @@ -44,24 +77,81 @@ module Net # :nodoc: class ProtoCommandError < ProtocolError; end class ProtoRetriableError < ProtocolError; end ProtocRetryError = ProtoRetriableError + # :startdoc: + + ## + # OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot + # be created within the open_timeout. + + class OpenTimeout < Timeout::Error; end + + ## + # ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be read within the read_timeout. + + class ReadTimeout < Timeout::Error + # :stopdoc: + def initialize(io = nil) + @io = io + end + attr_reader :io + + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" + end + msg + end + end + + ## + # 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. + + class WriteTimeout < Timeout::Error + # :stopdoc: + def initialize(io = nil) + @io = io + end + attr_reader :io + + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" + end + msg + end + end class BufferedIO #:nodoc: internal use only - def initialize(io) + def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil) @io = io - @read_timeout = 60 - @debug_output = nil - @rbuf = '' + @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_reader :io attr_accessor :read_timeout + attr_accessor :write_timeout + attr_accessor :continue_timeout attr_accessor :debug_output def inspect "#<#{self.class} io=#{@io}>" end + def eof? + @io.eof? + end + def closed? @io.closed? end @@ -76,17 +166,20 @@ module Net # :nodoc: public - def read(len, dest = '', ignore_eof = false) + def read(len, dest = ''.b, ignore_eof = false) LOG "reading #{len} bytes..." read_bytes = 0 begin - while read_bytes + @rbuf.size < len - dest << (s = rbuf_consume(@rbuf.size)) - read_bytes += s.size + while read_bytes + rbuf_size < len + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end rbuf_fill end - dest << (s = rbuf_consume(len - read_bytes)) - read_bytes += s.size + s = rbuf_consume(len - read_bytes) + read_bytes += s.bytesize + dest << s rescue EOFError raise unless ignore_eof end @@ -94,13 +187,15 @@ module Net # :nodoc: dest end - def read_all(dest = '') + def read_all(dest = ''.b) LOG 'reading all...' read_bytes = 0 begin while true - dest << (s = rbuf_consume(@rbuf.size)) - read_bytes += s.size + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end rbuf_fill end rescue EOFError @@ -111,17 +206,19 @@ module Net # :nodoc: end def readuntil(terminator, ignore_eof = false) + offset = @rbuf_offset begin - until idx = @rbuf.index(terminator) + until idx = @rbuf.index(terminator, offset) + offset = @rbuf.bytesize rbuf_fill end - return rbuf_consume(idx + terminator.size) + return rbuf_consume(idx + terminator.bytesize - @rbuf_offset) rescue EOFError raise unless ignore_eof - return rbuf_consume(@rbuf.size) + return rbuf_consume end end - + def readline readuntil("\n").chop end @@ -131,13 +228,64 @@ module Net # :nodoc: BUFSIZE = 1024 * 16 def rbuf_fill - timeout(@read_timeout) { - @rbuf << @io.sysread(BUFSIZE) - } + 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 + nil + end + + def rbuf_size + @rbuf.bytesize - @rbuf_offset + end + + def rbuf_consume_all + rbuf_consume if rbuf_size > 0 end - def rbuf_consume(len) - s = @rbuf.slice!(0, 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 + @debug_output << %Q[-> #{s.dump}\n] if @debug_output s end @@ -148,12 +296,14 @@ module Net # :nodoc: public - def write(str) + def write(*strs) writing { - write0 str + write0(*strs) } end + alias << write + def writeline(str) writing { write0 str + "\r\n" @@ -172,11 +322,34 @@ module Net # :nodoc: bytes end - def write0(str) - @debug_output << str.dump if @debug_output - len = @io.write(str) - @written_bytes += len - len + 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 + # 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 # @@ -202,7 +375,7 @@ module Net # :nodoc: class InternetMessageIO < BufferedIO #:nodoc: internal use only - def initialize(io) + def initialize(*, **) super @wbuf = nil end @@ -217,12 +390,12 @@ module Net # :nodoc: read_bytes = 0 while (line = readuntil("\r\n")) != ".\r\n" read_bytes += line.size - yield line.sub(/\A\./, '') + yield line.delete_prefix('.') end LOG_on() LOG "read message (#{read_bytes} bytes)" end - + # *library private* (cannot handle 'break') def each_list_item while (str = readuntil("\r\n")) != ".\r\n" @@ -233,7 +406,7 @@ module Net # :nodoc: def write_message_0(src) prev = @written_bytes each_crlf_line(src) do |line| - write0 line.sub(/\A\./, '..') + write0 dot_stuff(line) end @written_bytes - prev end @@ -261,7 +434,7 @@ module Net # :nodoc: len = writing { using_each_crlf_line { begin - block.call(WriteAdapter.new(self, :write_message_0)) + block.call(WriteAdapter.new(self.method(:write_message_0))) rescue LocalJumpError # allow `break' from writer block end @@ -274,11 +447,15 @@ module Net # :nodoc: private + def dot_stuff(s) + s.sub(/\A\./, '..') + end + def using_each_crlf_line - @wbuf = '' + @wbuf = ''.b yield if not @wbuf.empty? # unterminated last line - write0 @wbuf.chomp + "\r\n" + write0 dot_stuff(@wbuf.chomp) + "\r\n" elsif @written_bytes == 0 # empty src write0 "\r\n" end @@ -288,7 +465,7 @@ module Net # :nodoc: def each_crlf_line(src) buffer_filling(@wbuf, src) do - while line = @wbuf.slice!(/\A.*(?:\n|\r\n|\r(?!\z))/n) + while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/) yield line.chomp("\n") + "\r\n" end end @@ -321,17 +498,17 @@ module Net # :nodoc: # The writer adapter class # class WriteAdapter - def initialize(socket, method) - @socket = socket - @method_id = method + # :stopdoc: + def initialize(writer) + @writer = writer end def inspect - "#<#{self.class} socket=#{@socket.inspect}>" + "#<#{self.class} writer=#{@writer.inspect}>" end def write(str) - @socket.__send__(@method_id, str) + @writer.call(str) end alias print write diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb deleted file mode 100644 index 3c69846d15..0000000000 --- a/lib/net/smtp.rb +++ /dev/null @@ -1,1011 +0,0 @@ -# = net/smtp.rb -# -# Copyright (c) 1999-2007 Yukihiro Matsumoto. -# -# Copyright (c) 1999-2007 Minero Aoki. -# -# Written & maintained by Minero Aoki <aamine@loveruby.net>. -# -# Documented by William Webber and Minero Aoki. -# -# This program is free software. You can re-distribute and/or -# modify this program under the same terms as Ruby itself. -# -# NOTE: You can find Japanese version of this document at: -# http://www.ruby-lang.org/ja/man/index.cgi?cmd=view;name=net%2Fsmtp.rb -# -# $Id$ -# -# See Net::SMTP for documentation. -# - -require 'net/protocol' -require 'digest/md5' -require 'timeout' -begin - require 'openssl' -rescue LoadError -end - -module Net - - # Module mixed in to all SMTP error classes - module SMTPError - # This *class* is a module for backward compatibility. - # In later release, this module becomes a class. - end - - # Represents an SMTP authentication error. - class SMTPAuthenticationError < ProtoAuthError - include SMTPError - end - - # Represents SMTP error code 420 or 450, a temporary error. - class SMTPServerBusy < ProtoServerError - include SMTPError - end - - # Represents an SMTP command syntax error (error code 500) - class SMTPSyntaxError < ProtoSyntaxError - include SMTPError - end - - # Represents a fatal SMTP error (error code 5xx, except for 500) - class SMTPFatalError < ProtoFatalError - include SMTPError - end - - # Unexpected reply code returned from server. - class SMTPUnknownError < ProtoUnknownError - include SMTPError - end - - # Command is not supported on server. - class SMTPUnsupportedCommand < ProtocolError - include SMTPError - end - - # - # = Net::SMTP - # - # == What is This Library? - # - # This library provides functionality to send internet - # mail via SMTP, the Simple Mail Transfer Protocol. For details of - # SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt). - # - # == What is This Library NOT? - # - # This library does NOT provide functions to compose internet mails. - # You must create them by yourself. If you want better mail support, - # try RubyMail or TMail. You can get both libraries from RAA. - # (http://www.ruby-lang.org/en/raa.html) - # - # FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt). - # - # == Examples - # - # === Sending Messages - # - # You must open a connection to an SMTP server before sending messages. - # The first argument is the address of your SMTP server, and the second - # argument is the port number. Using SMTP.start with a block is the simplest - # way to do this. This way, the SMTP connection is closed automatically - # after the block is executed. - # - # require 'net/smtp' - # Net::SMTP.start('your.smtp.server', 25) do |smtp| - # # Use the SMTP object smtp only in this block. - # end - # - # Replace 'your.smtp.server' with your SMTP server. Normally - # your system manager or internet provider supplies a server - # for you. - # - # Then you can send messages. - # - # msgstr = <<END_OF_MESSAGE - # From: Your Name <your@mail.address> - # To: Destination Address <someone@example.com> - # Subject: test message - # Date: Sat, 23 Jun 2001 16:26:43 +0900 - # Message-Id: <unique.message.id.string@example.com> - # - # This is a test message. - # END_OF_MESSAGE - # - # require 'net/smtp' - # Net::SMTP.start('your.smtp.server', 25) do |smtp| - # smtp.send_message msgstr, - # 'your@mail.address', - # 'his_addess@example.com' - # end - # - # === Closing the Session - # - # You MUST close the SMTP session after sending messages, by calling - # the #finish method: - # - # # using SMTP#finish - # smtp = Net::SMTP.start('your.smtp.server', 25) - # smtp.send_message msgstr, 'from@address', 'to@address' - # smtp.finish - # - # You can also use the block form of SMTP.start/SMTP#start. This closes - # the SMTP session automatically: - # - # # using block form of SMTP.start - # Net::SMTP.start('your.smtp.server', 25) do |smtp| - # smtp.send_message msgstr, 'from@address', 'to@address' - # end - # - # I strongly recommend this scheme. This form is simpler and more robust. - # - # === HELO domain - # - # In almost all situations, you must provide a third argument - # to SMTP.start/SMTP#start. This is the domain name which you are on - # (the host to send mail from). It is called the "HELO domain". - # The SMTP server will judge whether it should send or reject - # the SMTP session by inspecting the HELO domain. - # - # Net::SMTP.start('your.smtp.server', 25, - # 'mail.from.domain') { |smtp| ... } - # - # === SMTP Authentication - # - # The Net::SMTP class supports three authentication schemes; - # PLAIN, LOGIN and CRAM MD5. (SMTP Authentication: [RFC2554]) - # To use SMTP authentication, pass extra arguments to - # SMTP.start/SMTP#start. - # - # # PLAIN - # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', - # 'Your Account', 'Your Password', :plain) - # # LOGIN - # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', - # 'Your Account', 'Your Password', :login) - # - # # CRAM MD5 - # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', - # 'Your Account', 'Your Password', :cram_md5) - # - class SMTP - - Revision = %q$Revision$.split[1] - - # The default SMTP port number, 25. - def SMTP.default_port - 25 - end - - # The default mail submission port number, 587. - def SMTP.default_submission_port - 587 - end - - # The default SMTPS port number, 465. - def SMTP.default_tls_port - 465 - end - - class << self - alias default_ssl_port default_tls_port - end - - def SMTP.default_ssl_context - OpenSSL::SSL::SSLContext.new - end - - # - # Creates a new Net::SMTP object. - # - # +address+ is the hostname or ip address of your SMTP - # server. +port+ is the port to connect to; it defaults to - # port 25. - # - # This method does not open the TCP connection. You can use - # SMTP.start instead of SMTP.new if you want to do everything - # at once. Otherwise, follow SMTP.new with SMTP#start. - # - def initialize(address, port = nil) - @address = address - @port = (port || SMTP.default_port) - @esmtp = true - @capabilities = nil - @socket = nil - @started = false - @open_timeout = 30 - @read_timeout = 60 - @error_occured = false - @debug_output = nil - @tls = false - @starttls = false - @ssl_context = nil - end - - # Provide human-readable stringification of class state. - def inspect - "#<#{self.class} #{@address}:#{@port} started=#{@started}>" - end - - # +true+ if the SMTP object uses ESMTP (which it does by default). - def esmtp? - @esmtp - end - - # - # Set whether to use ESMTP or not. This should be done before - # calling #start. Note that if #start is called in ESMTP mode, - # and the connection fails due to a ProtocolError, the SMTP - # object will automatically switch to plain SMTP mode and - # retry (but not vice versa). - # - def esmtp=(bool) - @esmtp = bool - end - - alias esmtp esmtp? - - # true if server advertises STARTTLS. - # You cannot get valid value before opening SMTP session. - def capable_starttls? - capable?('STARTTLS') - end - - def capable?(key) - return nil unless @capabilities - @capabilities[key] ? true : false - end - private :capable? - - # true if server advertises AUTH PLAIN. - # You cannot get valid value before opening SMTP session. - def capable_plain_auth? - auth_capable?('PLAIN') - end - - # true if server advertises AUTH LOGIN. - # You cannot get valid value before opening SMTP session. - def capable_login_auth? - auth_capable?('LOGIN') - end - - # true if server advertises AUTH CRAM-MD5. - # You cannot get valid value before opening SMTP session. - def capable_cram_md5_auth? - auth_capable?('CRAM-MD5') - end - - def auth_capable?(type) - return nil unless @capabilities - return false unless @capabilities['AUTH'] - @capabilities['AUTH'].include?(type) - end - private :auth_capable? - - # Returns supported authentication methods on this server. - # You cannot get valid value before opening SMTP session. - def capable_auth_types - return [] unless @capabilities - return [] unless @capabilities['AUTH'] - @capabilities['AUTH'] - end - - # true if this object uses SMTP/TLS (SMTPS). - def tls? - @tls - end - - alias ssl? tls? - - # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for - # this object. Must be called before the connection is established - # to have any effect. +context+ is a OpenSSL::SSL::SSLContext object. - def enable_tls(context = SMTP.default_ssl_context) - raise 'openssl library not installed' unless defined?(OpenSSL) - raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls - @tls = true - @ssl_context = context - end - - alias enable_ssl enable_tls - - # Disables SMTP/TLS for this object. Must be called before the - # connection is established to have any effect. - def disable_tls - @tls = false - @ssl_context = nil - end - - alias disable_ssl disable_tls - - # Returns truth value if this object uses STARTTLS. - # If this object always uses STARTTLS, returns :always. - # If this object uses STARTTLS when the server support TLS, returns :auto. - def starttls? - @starttls - end - - # true if this object uses STARTTLS. - def starttls_always? - @starttls == :always - end - - # true if this object uses STARTTLS when server advertises STARTTLS. - def starttls_auto? - @starttls == :auto - end - - # Enables SMTP/TLS (STARTTLS) for this object. - # +context+ is a OpenSSL::SSL::SSLContext object. - def enable_starttls(context = SMTP.default_ssl_context) - raise 'openssl library not installed' unless defined?(OpenSSL) - raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls - @starttls = :always - @ssl_context = context - end - - # Enables SMTP/TLS (STARTTLS) for this object if server accepts. - # +context+ is a OpenSSL::SSL::SSLContext object. - def enable_starttls_auto(context = SMTP.default_ssl_context) - raise 'openssl library not installed' unless defined?(OpenSSL) - raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls - @starttls = :auto - @ssl_context = context - end - - # Disables SMTP/TLS (STARTTLS) for this object. Must be called - # before the connection is established to have any effect. - def disable_starttls - @starttls = false - @ssl_context = nil - end - - # The address of the SMTP server to connect to. - attr_reader :address - - # The port number of the SMTP server to connect to. - attr_reader :port - - # Seconds to wait while attempting to open a connection. - # If the connection cannot be opened within this time, a - # TimeoutError is raised. - attr_accessor :open_timeout - - # Seconds to wait while reading one block (by one read(2) call). - # If the read(2) call does not complete within this time, a - # TimeoutError is raised. - attr_reader :read_timeout - - # Set the number of seconds to wait until timing-out a read(2) - # call. - def read_timeout=(sec) - @socket.read_timeout = sec if @socket - @read_timeout = sec - end - - # - # WARNING: This method causes serious security holes. - # Use this method for only debugging. - # - # Set an output stream for debug logging. - # You must call this before #start. - # - # # example - # smtp = Net::SMTP.new(addr, port) - # smtp.set_debug_output $stderr - # smtp.start do |smtp| - # .... - # end - # - def debug_output=(arg) - @debug_output = arg - end - - alias set_debug_output debug_output= - - # - # SMTP session control - # - - # - # Creates a new Net::SMTP object and connects to the server. - # - # This method is equivalent to: - # - # Net::SMTP.new(address, port).start(helo_domain, account, password, authtype) - # - # === Example - # - # Net::SMTP.start('your.smtp.server') do |smtp| - # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] - # end - # - # === Block Usage - # - # If called with a block, the newly-opened Net::SMTP object is yielded - # to the block, and automatically closed when the block finishes. If called - # without a block, the newly-opened Net::SMTP object is returned to - # the caller, and it is the caller's responsibility to close it when - # finished. - # - # === Parameters - # - # +address+ is the hostname or ip address of your smtp server. - # - # +port+ is the port to connect to; it defaults to port 25. - # - # +helo+ is the _HELO_ _domain_ provided by the client to the - # server (see overview comments); it defaults to 'localhost.localdomain'. - # - # The remaining arguments are used for SMTP authentication, if required - # or desired. +user+ is the account name; +secret+ is your password - # or other authentication token; and +authtype+ is the authentication - # type, one of :plain, :login, or :cram_md5. See the discussion of - # SMTP Authentication in the overview notes. - # - # === Errors - # - # This method may raise: - # - # * Net::SMTPAuthenticationError - # * Net::SMTPServerBusy - # * Net::SMTPSyntaxError - # * Net::SMTPFatalError - # * Net::SMTPUnknownError - # * IOError - # * TimeoutError - # - def SMTP.start(address, port = nil, helo = 'localhost.localdomain', - user = nil, secret = nil, authtype = nil, - &block) # :yield: smtp - new(address, port).start(helo, user, secret, authtype, &block) - end - - # +true+ if the SMTP session has been started. - def started? - @started - end - - # - # Opens a TCP connection and starts the SMTP session. - # - # === Parameters - # - # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see - # the discussion in the overview notes. - # - # If both of +user+ and +secret+ are given, SMTP authentication - # will be attempted using the AUTH command. +authtype+ specifies - # the type of authentication to attempt; it must be one of - # :login, :plain, and :cram_md5. See the notes on SMTP Authentication - # in the overview. - # - # === Block Usage - # - # When this methods is called with a block, the newly-started SMTP - # object is yielded to the block, and automatically closed after - # the block call finishes. Otherwise, it is the caller's - # responsibility to close the session when finished. - # - # === Example - # - # This is very similar to the class method SMTP.start. - # - # require 'net/smtp' - # smtp = Net::SMTP.new('smtp.mail.server', 25) - # smtp.start(helo_domain, account, password, authtype) do |smtp| - # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] - # end - # - # The primary use of this method (as opposed to SMTP.start) - # is probably to set debugging (#set_debug_output) or ESMTP - # (#esmtp=), which must be done before the session is - # started. - # - # === Errors - # - # If session has already been started, an IOError will be raised. - # - # This method may raise: - # - # * Net::SMTPAuthenticationError - # * Net::SMTPServerBusy - # * Net::SMTPSyntaxError - # * Net::SMTPFatalError - # * Net::SMTPUnknownError - # * IOError - # * TimeoutError - # - def start(helo = 'localhost.localdomain', - user = nil, secret = nil, authtype = nil) # :yield: smtp - if block_given? - begin - do_start helo, user, secret, authtype - return yield(self) - ensure - do_finish - end - else - do_start helo, user, secret, authtype - return self - end - end - - # Finishes the SMTP session and closes TCP connection. - # Raises IOError if not started. - def finish - raise IOError, 'not yet started' unless started? - do_finish - end - - private - - def do_start(helo_domain, user, secret, authtype) - raise IOError, 'SMTP session already started' if @started - if user or secret - check_auth_method(authtype || DEFAULT_AUTH_TYPE) - check_auth_args user, secret - end - s = timeout(@open_timeout) { TCPSocket.open(@address, @port) } - logging "Connection opened: #{@address}:#{@port}" - @socket = new_internet_message_io(tls? ? tlsconnect(s) : s) - check_response critical { recv_response() } - do_helo helo_domain - if starttls_always? or (capable_starttls? and starttls_auto?) - unless capable_starttls? - raise SMTPUnsupportedCommand, - "STARTTLS is not supported on this server" - end - starttls - @socket = new_internet_message_io(tlsconnect(s)) - # helo response may be different after STARTTLS - do_helo helo_domain - end - authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user - @started = true - ensure - unless @started - # authentication failed, cancel connection. - s.close if s and not s.closed? - @socket = nil - end - end - - def tlsconnect(s) - s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) - logging "TLS connection started" - s.sync_close = true - s.connect - s - end - - def new_internet_message_io(s) - io = InternetMessageIO.new(s) - io.read_timeout = @read_timeout - io.debug_output = @debug_output - io - end - - def do_helo(helo_domain) - res = @esmtp ? ehlo(helo_domain) : helo(helo_domain) - @capabilities = res.capabilities - rescue SMTPError - if @esmtp - @esmtp = false - @error_occured = false - retry - end - raise - end - - def do_finish - quit if @socket and not @socket.closed? and not @error_occured - ensure - @started = false - @error_occured = false - @socket.close if @socket and not @socket.closed? - @socket = nil - end - - # - # Message Sending - # - - public - - # - # Sends +msgstr+ as a message. Single CR ("\r") and LF ("\n") found - # in the +msgstr+, are converted into the CR LF pair. You cannot send a - # binary message with this method. +msgstr+ should include both - # the message headers and body. - # - # +from_addr+ is a String representing the source mail address. - # - # +to_addr+ is a String or Strings or Array of Strings, representing - # the destination mail address or addresses. - # - # === Example - # - # Net::SMTP.start('smtp.example.com') do |smtp| - # smtp.send_message msgstr, - # 'from@example.com', - # ['dest@example.com', 'dest2@example.com'] - # end - # - # === Errors - # - # This method may raise: - # - # * Net::SMTPServerBusy - # * Net::SMTPSyntaxError - # * Net::SMTPFatalError - # * Net::SMTPUnknownError - # * IOError - # * TimeoutError - # - def send_message(msgstr, from_addr, *to_addrs) - raise IOError, 'closed session' unless @socket - mailfrom from_addr - rcptto_list to_addrs - data msgstr - end - - alias send_mail send_message - alias sendmail send_message # obsolete - - # - # Opens a message writer stream and gives it to the block. - # The stream is valid only in the block, and has these methods: - # - # puts(str = ''):: outputs STR and CR LF. - # print(str):: outputs STR. - # printf(fmt, *args):: outputs sprintf(fmt,*args). - # write(str):: outputs STR and returns the length of written bytes. - # <<(str):: outputs STR and returns self. - # - # If a single CR ("\r") or LF ("\n") is found in the message, - # it is converted to the CR LF pair. You cannot send a binary - # message with this method. - # - # === Parameters - # - # +from_addr+ is a String representing the source mail address. - # - # +to_addr+ is a String or Strings or Array of Strings, representing - # the destination mail address or addresses. - # - # === Example - # - # Net::SMTP.start('smtp.example.com', 25) do |smtp| - # smtp.open_message_stream('from@example.com', ['dest@example.com']) do |f| - # f.puts 'From: from@example.com' - # f.puts 'To: dest@example.com' - # f.puts 'Subject: test message' - # f.puts - # f.puts 'This is a test message.' - # end - # end - # - # === Errors - # - # This method may raise: - # - # * Net::SMTPServerBusy - # * Net::SMTPSyntaxError - # * Net::SMTPFatalError - # * Net::SMTPUnknownError - # * IOError - # * TimeoutError - # - def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream - raise IOError, 'closed session' unless @socket - mailfrom from_addr - rcptto_list to_addrs - data(&block) - end - - alias ready open_message_stream # obsolete - - # - # Authentication - # - - public - - DEFAULT_AUTH_TYPE = :plain - - def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE) - check_auth_method authtype - check_auth_args user, secret - funcall auth_method(authtype), user, secret - end - - def auth_plain(user, secret) - check_auth_args user, secret - res = critical { - get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}")) - } - check_auth_response res - res - end - - def auth_login(user, secret) - check_auth_args user, secret - res = critical { - check_auth_continue get_response('AUTH LOGIN') - check_auth_continue get_response(base64_encode(user)) - get_response(base64_encode(secret)) - } - check_auth_response res - res - end - - def auth_cram_md5(user, secret) - check_auth_args user, secret - res = critical { - res0 = get_response('AUTH CRAM-MD5') - check_auth_continue res0 - crammed = cram_md5_response(secret, res0.cram_md5_challenge) - get_response(base64_encode("#{user} #{crammed}")) - } - check_auth_response res - res - end - - private - - def check_auth_method(type) - unless respond_to?(auth_method(type), true) - raise ArgumentError, "wrong authentication type #{type}" - end - end - - def auth_method(type) - "auth_#{type.to_s.downcase}".intern - end - - def check_auth_args(user, secret) - unless user - raise ArgumentError, 'SMTP-AUTH requested but missing user name' - end - unless secret - raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase' - end - end - - def base64_encode(str) - # expects "str" may not become too long - [str].pack('m').gsub(/\s+/, '') - end - - IMASK = 0x36 - OMASK = 0x5c - - # CRAM-MD5: [RFC2195] - def cram_md5_response(secret, challenge) - tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge) - Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp) - end - - CRAM_BUFSIZE = 64 - - def cram_secret(secret, mask) - secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE - buf = secret.ljust(CRAM_BUFSIZE, "\0") - 0.upto(buf.size - 1) do |i| - buf[i] = (buf[i].ord ^ mask).chr - end - buf - end - - # - # SMTP command dispatcher - # - - public - - def starttls - getok('STARTTLS') - end - - def helo(domain) - getok("HELO #{domain}") - end - - def ehlo(domain) - getok("EHLO #{domain}") - end - - def mailfrom(from_addr) - if $SAFE > 0 - raise SecurityError, 'tainted from_addr' if from_addr.tainted? - end - getok("MAIL FROM:<#{from_addr}>") - end - - def rcptto_list(to_addrs) - raise ArgumentError, 'mail destination not given' if to_addrs.empty? - to_addrs.flatten.each do |addr| - rcptto addr - end - end - - def rcptto(to_addr) - if $SAFE > 0 - raise SecurityError, 'tainted to_addr' if to.tainted? - end - getok("RCPT TO:<#{to_addr}>") - end - - # This method sends a message. - # If +msgstr+ is given, sends it as a message. - # If block is given, yield a message writer stream. - # You must write message before the block is closed. - # - # # Example 1 (by string) - # smtp.data(<<EndMessage) - # From: john@example.com - # To: betty@example.com - # Subject: I found a bug - # - # Check vm.c:58879. - # EndMessage - # - # # Example 2 (by block) - # smtp.data {|f| - # f.puts "From: john@example.com" - # f.puts "To: betty@example.com" - # f.puts "Subject: I found a bug" - # f.puts "" - # f.puts "Check vm.c:58879." - # } - # - def data(msgstr = nil, &block) #:yield: stream - if msgstr and block - raise ArgumentError, "message and block are exclusive" - end - unless msgstr or block - raise ArgumentError, "message or block is required" - end - res = critical { - check_continue get_response('DATA') - if msgstr - @socket.write_message msgstr - else - @socket.write_message_by_block(&block) - end - recv_response() - } - check_response res - res - end - - def quit - getok('QUIT') - end - - private - - def getok(reqline) - res = critical { - @socket.writeline reqline - recv_response() - } - check_response res - res - end - - def get_response(reqline) - @socket.writeline reqline - recv_response() - end - - def recv_response - buf = '' - while true - line = @socket.readline - buf << line << "\n" - break unless line[3,1] == '-' # "210-PIPELINING" - end - Response.parse(buf) - end - - def critical(&block) - return '200 dummy reply code' if @error_occured - begin - return yield() - rescue Exception - @error_occured = true - raise - end - end - - def check_response(res) - unless res.success? - raise res.exception_class, res.message - end - end - - def check_continue(res) - unless res.continue? - raise SMTPUnknownError, "could not get 3xx (#{res.status})" - end - end - - def check_auth_response(res) - unless res.success? - raise SMTPAuthenticationError, res.message - end - end - - def check_auth_continue(res) - unless res.continue? - raise res.exception_class, res.message - end - end - - class Response - def Response.parse(str) - new(str[0,3], str) - end - - def initialize(status, string) - @status = status - @string = string - end - - attr_reader :status - attr_reader :string - - def status_type_char - @status[0, 1] - end - - def success? - status_type_char() == '2' - end - - def continue? - status_type_char() == '3' - end - - def message - @string.lines.first - end - - def cram_md5_challenge - @string.split(/ /)[1].unpack('m')[0] - end - - def capabilities - return {} unless @string[3, 1] == '-' - h = {} - @string.lines.drop(1).each do |line| - k, *v = line[4..-1].chomp.split(nil) - h[k] = v - end - h - end - - def exception_class - case @status - when /\A4/ then SMTPServerBusy - when /\A50/ then SMTPSyntaxError - when /\A53/ then SMTPAuthenticationError - when /\A5/ then SMTPFatalError - else SMTPUnknownError - end - end - end - - def logging(msg) - @debug_output << msg + "\n" if @debug_output - end - - end # class SMTP - - SMTPSession = SMTP - -end diff --git a/lib/net/telnet.rb b/lib/net/telnet.rb deleted file mode 100644 index 16459d7c65..0000000000 --- a/lib/net/telnet.rb +++ /dev/null @@ -1,745 +0,0 @@ -# = net/telnet.rb - Simple Telnet Client Library -# -# Author:: Wakou Aoyama <wakou@ruby-lang.org> -# Documentation:: William Webber and Wakou Aoyama -# -# This file holds the class Net::Telnet, which provides client-side -# telnet functionality. -# -# For documentation, see Net::Telnet. -# - -require "socket" -require "delegate" -require "timeout" -require "English" - -module Net - - # - # == Net::Telnet - # - # Provides telnet client functionality. - # - # This class also has, through delegation, all the methods of a - # socket object (by default, a +TCPSocket+, but can be set by the - # +Proxy+ option to <tt>new()</tt>). This provides methods such as - # <tt>close()</tt> to end the session and <tt>sysread()</tt> to read - # data directly from the host, instead of via the <tt>waitfor()</tt> - # mechanism. Note that if you do use <tt>sysread()</tt> directly - # when in telnet mode, you should probably pass the output through - # <tt>preprocess()</tt> to extract telnet command sequences. - # - # == Overview - # - # The telnet protocol allows a client to login remotely to a user - # account on a server and execute commands via a shell. The equivalent - # is done by creating a Net::Telnet class with the +Host+ option - # set to your host, calling #login() with your user and password, - # issuing one or more #cmd() calls, and then calling #close() - # to end the session. The #waitfor(), #print(), #puts(), and - # #write() methods, which #cmd() is implemented on top of, are - # only needed if you are doing something more complicated. - # - # A Net::Telnet object can also be used to connect to non-telnet - # services, such as SMTP or HTTP. In this case, you normally - # want to provide the +Port+ option to specify the port to - # connect to, and set the +Telnetmode+ option to false to prevent - # the client from attempting to interpret telnet command sequences. - # Generally, #login() will not work with other protocols, and you - # have to handle authentication yourself. - # - # For some protocols, it will be possible to specify the +Prompt+ - # option once when you create the Telnet object and use #cmd() calls; - # for others, you will have to specify the response sequence to - # look for as the Match option to every #cmd() call, or call - # #puts() and #waitfor() directly; for yet others, you will have - # to use #sysread() instead of #waitfor() and parse server - # responses yourself. - # - # It is worth noting that when you create a new Net::Telnet object, - # you can supply a proxy IO channel via the Proxy option. This - # can be used to attach the Telnet object to other Telnet objects, - # to already open sockets, or to any read-write IO object. This - # can be useful, for instance, for setting up a test fixture for - # unit testing. - # - # == Examples - # - # === Log in and send a command, echoing all output to stdout - # - # 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 - # - # - # === Check 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 } - # - # == References - # - # There are a large number of RFCs relevant to the Telnet protocol. - # RFCs 854-861 define the base protocol. For a complete listing - # of relevant RFCs, see - # http://www.omnifarious.org/~hopper/technical/telnet-rfc.html - # - class Telnet < SimpleDelegator - - # :stopdoc: - 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 - REVISION = '$Id$' - # :startdoc: - - # - # Creates a new Net::Telnet object. - # - # Attempts to connect to the host (unless the Proxy option is - # provided: see below). If a block is provided, it is yielded - # status messages on the attempt to connect to the server, of - # the form: - # - # Trying localhost... - # Connected to localhost. - # - # +options+ is a hash of options. The following example lists - # all options and their default values. - # - # host = Net::Telnet::new( - # "Host" => "localhost", # default: "localhost" - # "Port" => 23, # default: 23 - # "Binmode" => false, # default: false - # "Output_log" => "output_log", # default: nil (no output) - # "Dump_log" => "dump_log", # default: nil (no output) - # "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 - # ) - # - # The options have the following meanings: - # - # Host:: the hostname or IP address of the host to connect to, as a String. - # Defaults to "localhost". - # - # Port:: the port to connect to. Defaults to 23. - # - # Binmode:: if false (the default), newline substitution is performed. - # Outgoing LF is - # converted to CRLF, and incoming CRLF is converted to LF. If - # true, this substitution is not performed. This value can - # also be set with the #binmode() method. The - # outgoing conversion only applies to the #puts() and #print() - # methods, not the #write() method. The precise nature of - # the newline conversion is also affected by the telnet options - # SGA and BIN. - # - # Output_log:: the name of the file to write connection status messages - # and all received traffic to. In the case of a proper - # Telnet session, this will include the client input as - # echoed by the host; otherwise, it only includes server - # responses. Output is appended verbatim to this file. - # By default, no output log is kept. - # - # Dump_log:: as for Output_log, except that output is written in hexdump - # format (16 bytes per line as hex pairs, followed by their - # printable equivalent), with connection status messages - # preceded by '#', sent traffic preceded by '>', and - # received traffic preceded by '<'. By default, not dump log - # is kept. - # - # Prompt:: a regular expression matching the host's command-line prompt - # sequence. This is needed by the Telnet class to determine - # when the output from a command has finished and the host is - # ready to receive a new command. By default, this regular - # expression is /[$%#>] \z/n. - # - # Telnetmode:: a boolean value, true by default. In telnet mode, - # traffic received from the host is parsed for special - # command sequences, and these sequences are escaped - # in outgoing traffic sent using #puts() or #print() - # (but not #write()). If you are using the Net::Telnet - # object to connect to a non-telnet service (such as - # SMTP or POP), this should be set to "false" to prevent - # undesired data corruption. This value can also be set - # by the #telnetmode() method. - # - # Timeout:: the number of seconds to wait before timing out both the - # initial attempt to connect to host (in this constructor), - # and all attempts to read data from the host (in #waitfor(), - # #cmd(), and #login()). Exceeding this timeout causes a - # TimeoutError to be raised. The default value is 10 seconds. - # You can disable the timeout by setting this value to false. - # In this case, the connect attempt will eventually timeout - # on the underlying connect(2) socket call with an - # Errno::ETIMEDOUT error (but generally only after a few - # minutes), but other attempts to read data from the host - # will hand indefinitely if no data is forthcoming. - # - # Waittime:: the amount of time to wait after seeing what looks like a - # prompt (that is, received data that matches the Prompt - # option regular expression) to see if more data arrives. - # If more data does arrive in this time, Net::Telnet assumes - # that what it saw was not really a prompt. This is to try to - # avoid false matches, but it can also lead to missing real - # prompts (if, for instance, a background process writes to - # the terminal soon after the prompt is displayed). By - # default, set to 0, meaning not to wait for more data. - # - # Proxy:: a proxy object to used instead of opening a direct connection - # to the host. Must be either another Net::Telnet object or - # an IO object. If it is another Net::Telnet object, this - # instance will use that one's socket for communication. If an - # IO object, it is used directly for communication. Any other - # kind of object will cause an error to be raised. - # - def initialize(options) # :yield: mesg - @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 must be 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 must be 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) # :nodoc: - 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 += ' ' * (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 must be an instance of Net::Telnet or IO." - 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 while opening a connection to 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 - - # The socket the Telnet object is using. Note that this object becomes - # a delegate of the Telnet object, so normally you invoke its methods - # directly on the Telnet object. - attr :sock - - # Set telnet command interpretation on (+mode+ == true) or off - # (+mode+ == false), or return the current value (+mode+ not - # provided). It should be on for true telnet sessions, off if - # using Net::Telnet to connect to a non-telnet service such - # as SMTP. - def telnetmode(mode = nil) - case mode - when nil - @options["Telnetmode"] - when true, false - @options["Telnetmode"] = mode - else - raise ArgumentError, "argument must be true or false, or missing" - end - end - - # Turn telnet command interpretation on (true) or off (false). It - # should be on for true telnet sessions, off if using Net::Telnet - # to connect to a non-telnet service such as SMTP. - def telnetmode=(mode) - if (true == mode or false == mode) - @options["Telnetmode"] = mode - else - raise ArgumentError, "argument must be true or false" - end - end - - # Turn newline conversion on (+mode+ == false) or off (+mode+ == true), - # or return the current value (+mode+ is not specified). - def binmode(mode = nil) - case mode - when nil - @options["Binmode"] - when true, false - @options["Binmode"] = mode - else - raise ArgumentError, "argument must be true or false" - end - end - - # Turn newline conversion on (false) or off (true). - def binmode=(mode) - if (true == mode or false == mode) - @options["Binmode"] = mode - else - raise ArgumentError, "argument must be true or false" - end - end - - # Preprocess received data from the host. - # - # Performs newline conversion and detects telnet command sequences. - # Called automatically by #waitfor(). You should only use this - # method yourself if you have read input directly using sysread() - # or similar, and even then only if in telnet mode. - 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"] - - # remove NULL - string = string.gsub(/#{NULL}/no, '') 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 - - # Read data from the host until a certain sequence is matched. - # - # If a block is given, the received data will be yielded as it - # is read in (not necessarily all in one go), or nil if EOF - # occurs before any data is received. Whether a block is given - # or not, all data read will be returned in a single string, or again - # nil if EOF occurs before any data is received. Note that - # received data includes the matched sequence we were looking for. - # - # +options+ can be either a regular expression or a hash of options. - # If a regular expression, this specifies the data to wait for. - # If a hash, this can specify the following options: - # - # Match:: a regular expression, specifying the data to wait for. - # Prompt:: as for Match; used only if Match is not specified. - # String:: as for Match, except a string that will be converted - # into a regular expression. Used only if Match and - # Prompt are not specified. - # Timeout:: the number of seconds to wait for data from the host - # before raising a TimeoutError. If set to false, - # no timeout will occur. If not specified, the - # Timeout option value specified when this instance - # was created will be used, or, failing that, the - # default value of 10 seconds. - # Waittime:: the number of seconds to wait after matching against - # the input data to see if more data arrives. If more - # data arrives within this time, we will judge ourselves - # not to have matched successfully, and will continue - # trying to match. If not specified, the Waittime option - # value specified when this instance was created will be - # used, or, failing that, the default value of 0 seconds, - # which means not to wait for more input. - # - def waitfor(options) # :yield: recvdata - 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 while waiting for more data" - end - begin - c = @sock.readpartial(1024 * 1024) - @dumplog.log_dump('<', c) if @options.has_key?("Dump_log") - if @options["Telnetmode"] - c = rest + c - if Integer(c.rindex(/#{IAC}#{SE}/no)) < - Integer(c.rindex(/#{IAC}#{SB}/no)) - buf = preprocess(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(c[0 ... pt]) - rest = c[pt .. -1] - else - buf = preprocess(c) - rest = '' - end - else - # Not Telnetmode. - # - # We cannot use preprocess() on this data, because that - # method makes some Telnetmode-specific assumptions. - buf = c - buf.gsub!(/#{EOL}/no, "\n") unless @options["Binmode"] - rest = '' - end - @log.print(buf) if @options.has_key?("Output_log") - line += 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 - - # Write +string+ to the host. - # - # Does not perform any conversions on +string+. Will log +string+ to the - # dumplog, if the Dump_log option is set. - 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 - - # Sends a string to the host. - # - # This does _not_ automatically append a newline to the string. Embedded - # newlines may be converted and telnet command sequences escaped - # depending upon the values of telnetmode, binmode, and telnet options - # set by the host. - 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 - - # Sends a string to the host. - # - # Same as #print(), but appends a newline to the string. - def puts(string) - self.print(string + "\n") - end - - # Send a command to the host. - # - # More exactly, sends a string to the host, and reads in all received - # data until is sees the prompt or other matched sequence. - # - # If a block is given, the received data will be yielded to it as - # it is read in. Whether a block is given or not, the received data - # will be return as a string. Note that the received data includes - # the prompt and in most cases the host's echo of our command. - # - # +options+ is either a String, specified the string or command to - # send to the host; or it is a hash of options. If a hash, the - # following options can be specified: - # - # String:: the command or other string to send to the host. - # Match:: a regular expression, the sequence to look for in - # the received data before returning. If not specified, - # the Prompt option value specified when this instance - # was created will be used, or, failing that, the default - # prompt of /[$%#>] \z/n. - # Timeout:: the seconds to wait for data from the host before raising - # a Timeout error. If not specified, the Timeout option - # value specified when this instance was created will be - # used, or, failing that, the default value of 10 seconds. - # - # The command or other string will have the newline sequence appended - # to it. - def cmd(options) # :yield: recvdata - 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 - - # Login to the host with a given username and password. - # - # The username and password can either be provided as two string - # arguments in that order, or as a hash with keys "Name" and - # "Password". - # - # This method looks for the strings "login" and "Password" from the - # host to determine when to send the username and password. If the - # login sequence does not follow this pattern (for instance, you - # are connecting to a service other than telnet), you will need - # to handle login yourself. - # - # The password can be omitted, either by only - # provided one String argument, which will be used as the username, - # or by providing a has that has no "Password" key. In this case, - # the method will not look for the "Password:" prompt; if it is - # sent, it will have to be dealt with by later calls. - # - # The method returns all data received during the login process from - # the host, including the echoed username but not the password (which - # the host should not echo). If a block is passed in, this received - # data is also yielded to the block as it is received. - def login(options, password = nil) # :yield: recvdata - login_prompt = /[Ll]ogin[: ]*\z/n - password_prompt = /Password[: ]*\z/n - if options.kind_of?(Hash) - username = options["Name"] - password = options["Password"] - login_prompt = options["LoginPrompt"] if options["LoginPrompt"] - password_prompt = options["PasswordPrompt"] if options["PasswordPrompt"] - else - username = options - end - - if block_given? - line = waitfor(login_prompt){|c| yield c } - if password - line += cmd({"String" => username, - "Match" => password_prompt}){|c| yield c } - line += cmd(password){|c| yield c } - else - line += cmd(username){|c| yield c } - end - else - line = waitfor(login_prompt) - if password - line += cmd({"String" => username, - "Match" => password_prompt}) - line += cmd(password) - else - line += cmd(username) - end - end - line - end - - end # class Telnet -end # module Net - |
