summaryrefslogtreecommitdiff
path: root/lib/net
diff options
context:
space:
mode:
Diffstat (limited to 'lib/net')
-rw-r--r--lib/net/ftp.rb1118
-rw-r--r--lib/net/http.rb2512
-rw-r--r--lib/net/http/backward.rb25
-rw-r--r--lib/net/http/exceptions.rb54
-rw-r--r--lib/net/http/generic_request.rb216
-rw-r--r--lib/net/http/header.rb871
-rw-r--r--lib/net/http/net-http.gemspec39
-rw-r--r--lib/net/http/proxy_delta.rb1
-rw-r--r--lib/net/http/request.rb78
-rw-r--r--lib/net/http/requests.rb374
-rw-r--r--lib/net/http/response.rb404
-rw-r--r--lib/net/http/responses.rb1380
-rw-r--r--lib/net/http/status.rb84
-rw-r--r--lib/net/https.rb5
-rw-r--r--lib/net/imap.rb3760
-rw-r--r--lib/net/net-protocol.gemspec33
-rw-r--r--lib/net/pop.rb1021
-rw-r--r--lib/net/protocol.rb243
-rw-r--r--lib/net/smtp.rb1065
-rw-r--r--lib/net/telnet.rb763
20 files changed, 4995 insertions, 9051 deletions
diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb
deleted file mode 100644
index 07cb548406..0000000000
--- a/lib/net/ftp.rb
+++ /dev/null
@@ -1,1118 +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"
-require "net/protocol"
-
-module Net
-
- # :stopdoc:
- class FTPError < StandardError; end
- class FTPReplyError < FTPError; end
- class FTPTempError < FTPError; end
- class FTPPermError < FTPError; end
- class FTPProtoError < FTPError; end
- class FTPConnectionError < 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('example.com')
- # 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('example.com') 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 = BufferedIO::BUFSIZE
- # :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
-
- # Number of seconds to wait for the connection to open. Any number
- # may be used, including Floats for fractional seconds. If the FTP
- # object cannot open a connection in this many seconds, it raises a
- # Net::OpenTimeout exception. The default value is +nil+.
- attr_accessor :open_timeout
-
- # Number of seconds to wait for one block to be read (via one read(2)
- # call). Any number may be used, including Floats for fractional
- # seconds. If the FTP object cannot read data in this many seconds,
- # it raises a TimeoutError exception. The default value is 60 seconds.
- attr_reader :read_timeout
-
- # Setter for the read_timeout attribute.
- def read_timeout=(sec)
- @sock.read_timeout = sec
- @read_timeout = sec
- end
-
- # 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 = true
- @passive = false
- @debug_mode = false
- @resume = false
- @sock = NullSocket.new
- @logged_in = false
- @open_timeout = nil
- @read_timeout = 60
- if host
- connect(host)
- if user
- login(user, passwd, acct)
- end
- end
- end
-
- # A setter to toggle transfers in binary mode.
- # +newmode+ is either +true+ or +false+
- def binary=(newmode)
- if newmode != @binary
- @binary = newmode
- send_type_command if @logged_in
- end
- end
-
- # Sends a command to destination host, with the current binary sendmode
- # type.
- #
- # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
- # A" (ascii) is sent.
- def send_type_command # :nodoc:
- if @binary
- voidcmd("TYPE I")
- else
- voidcmd("TYPE A")
- end
- end
- private :send_type_command
-
- # Toggles transfers in binary mode and yields to a block.
- # This preserves your current binary send mode, but allows a temporary
- # transaction with binary sendmode of +newmode+.
- #
- # +newmode+ is either +true+ or +false+
- def with_binary(newmode) # :nodoc:
- oldmode = binary
- self.binary = newmode
- begin
- yield
- ensure
- self.binary = oldmode
- end
- end
- private :with_binary
-
- # Obsolete
- def return_code # :nodoc:
- $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
- return "\n"
- end
-
- # Obsolete
- def return_code=(s) # :nodoc:
- $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
- end
-
- # Contructs a socket with +host+ and +port+.
- #
- # If SOCKSSocket is defined and the environment (ENV) defines
- # SOCKS_SERVER, then a SOCKSSocket is returned, else a TCPSocket is
- # returned.
- def open_socket(host, port) # :nodoc:
- return Timeout.timeout(@open_timeout, Net::OpenTimeout) {
- if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
- @passive = true
- sock = SOCKSSocket.open(host, port)
- else
- sock = TCPSocket.open(host, port)
- end
- io = BufferedSocket.new(sock)
- io.read_timeout = @read_timeout
- io
- }
- 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
-
- # If string +s+ includes the PASS command (password), then the contents of
- # the password are cleaned from the string using "*"
- def sanitize(s) # :nodoc:
- if s =~ /^PASS /i
- return s[0, 5] + "*" * (s.length - 5)
- else
- return s
- end
- end
- private :sanitize
-
- # Ensures that +line+ has a control return / line feed (CRLF) and writes
- # it to the socket.
- def putline(line) # :nodoc:
- if @debug_mode
- print "put: ", sanitize(line), "\n"
- end
- line = line + CRLF
- @sock.write(line)
- end
- private :putline
-
- # Reads a line from the sock. If EOF, then it will raise EOFError
- def getline # :nodoc:
- 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
-
- # Receive a section of lines until the response code's match.
- def getmultiline # :nodoc:
- 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
-
- # Recieves a response from the destination host.
- #
- # Returns the response code or raises FTPTempError, FTPPermError, or
- # FTPProtoError
- def getresp # :nodoc:
- @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
-
- # Recieves a response.
- #
- # Raises FTPReplyError if the first position of the response code is not
- # equal 2.
- def voidresp # :nodoc:
- 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
-
- # Constructs and send the appropriate PORT (or EPRT) command
- def sendport(host, port) # :nodoc:
- af = (@sock.peeraddr)[0]
- if af == "AF_INET"
- cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
- elsif af == "AF_INET6"
- cmd = sprintf("EPRT |2|%s|%d|", host, port)
- else
- raise FTPProtoError, host
- end
- voidcmd(cmd)
- end
- private :sendport
-
- # Constructs a TCPServer socket, and sends it the PORT command
- #
- # Returns the constructed TCPServer socket
- def makeport # :nodoc:
- sock = TCPServer.open(@sock.addr[3], 0)
- port = sock.addr[1]
- host = sock.addr[3]
- sendport(host, port)
- return sock
- end
- private :makeport
-
- # sends the appropriate command to enable a passive connection
- def makepasv # :nodoc:
- 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
-
- # Constructs a connection for transferring data
- def transfercmd(cmd, rest_offset = nil) # :nodoc:
- 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)
- # skip 2XX for some ftp servers
- resp = getresp if resp[0] == ?2
- 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)
- # skip 2XX for some ftp servers
- resp = getresp if resp[0] == ?2
- if resp[0] != ?1
- raise FTPReplyError, resp
- end
- conn = BufferedSocket.new(sock.accept)
- conn.read_timeout = @read_timeout
- sock.shutdown(Socket::SHUT_WR) rescue nil
- sock.read rescue nil
- sock.close
- end
- return conn
- end
- private :transfercmd
-
- #
- # 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 = "anonymous@"
- end
-
- resp = ""
- synchronize do
- resp = sendcmd('USER ' + user)
- if resp[0] == ?3
- raise FTPReplyError, resp if passwd.nil?
- resp = sendcmd('PASS ' + passwd)
- end
- if resp[0] == ?3
- raise FTPReplyError, resp if acct.nil?
- resp = sendcmd('ACCT ' + acct)
- end
- end
- if resp[0] != ?2
- raise FTPReplyError, resp
- end
- @welcome = resp
- send_type_command
- @logged_in = 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
- begin
- conn = transfercmd(cmd, rest_offset)
- loop do
- data = conn.read(blocksize)
- break if data == nil
- yield(data)
- end
- conn.shutdown(Socket::SHUT_WR)
- conn.read_timeout = 1
- conn.read
- ensure
- conn.close if conn
- end
- 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
- begin
- conn = transfercmd(cmd)
- loop do
- line = conn.gets
- break if line == nil
- yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?)
- end
- conn.shutdown(Socket::SHUT_WR)
- conn.read_timeout = 1
- conn.read
- ensure
- conn.close if conn
- end
- 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) # :yield: data
- if rest_offset
- file.seek(rest_offset, IO::SEEK_SET)
- end
- synchronize do
- with_binary(true) do
- conn = transfercmd(cmd)
- loop do
- buf = file.read(blocksize)
- break if buf == nil
- conn.write(buf)
- yield(buf) if block_given?
- 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) # :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_given?
- 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.to_s, 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, newline|
- l = newline ? line + "\n" : line
- f.print(l) if localfile
- yield(line, newline) if block_given?
- result.concat(l) 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
- if rest_offset
- storbinary("APPE " + remotefile, f, blocksize, rest_offset, &block)
- else
- storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
- end
- 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.
- #
- # This is a less common FTP command, to send account
- # information if the destination host requires it.
- #
- 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.to_s
- 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 => e
- if e.message[0, 3] != "500"
- raise e
- 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.
- #
- # Does nothing except return a response.
- #
- 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
- if @sock and not @sock.closed?
- begin
- @sock.shutdown(Socket::SHUT_WR) rescue nil
- orig, self.read_timeout = self.read_timeout, 3
- @sock.read rescue nil
- ensure
- @sock.close
- self.read_timeout = orig
- end
- end
- end
-
- #
- # Returns +true+ iff the connection is closed.
- #
- def closed?
- @sock == nil or @sock.closed?
- end
-
- # handler for response code 227
- # (Entering Passive Mode (h1,h2,h3,h4,p1,p2))
- #
- # Returns host and port.
- def parse227(resp) # :nodoc:
- if resp[0, 3] != "227"
- raise FTPReplyError, resp
- end
- if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
- return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
- else
- raise FTPProtoError, resp
- end
- end
- private :parse227
-
- # handler for response code 228
- # (Entering Long Passive Mode)
- #
- # Returns host and port.
- def parse228(resp) # :nodoc:
- if resp[0, 3] != "228"
- raise FTPReplyError, resp
- end
- if m = /\(4,4,(?<host>\d+(,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp)
- return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
- elsif m = /\(6,16,(?<host>\d+(,(\d+)){15}),2,(?<port>\d+,\d+)\)/.match(resp)
- return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"])
- else
- raise FTPProtoError, resp
- end
- end
- private :parse228
-
- def parse_pasv_ipv4_host(s)
- return s.tr(",", ".")
- end
- private :parse_pasv_ipv4_host
-
- def parse_pasv_ipv6_host(s)
- return s.split(/,/).map { |i|
- "%02x" % i.to_i
- }.each_slice(2).map(&:join).join(":")
- end
- private :parse_pasv_ipv6_host
-
- def parse_pasv_port(s)
- return s.split(/,/).map(&:to_i).inject { |x, y|
- (x << 8) + y
- }
- end
- private :parse_pasv_port
-
- # handler for response code 229
- # (Extended Passive Mode Entered)
- #
- # Returns host and port.
- def parse229(resp) # :nodoc:
- if resp[0, 3] != "229"
- raise FTPReplyError, resp
- end
- if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp)
- return @sock.peeraddr[3], m["port"].to_i
- else
- raise FTPProtoError, resp
- end
- end
- private :parse229
-
- # handler for response code 257
- # ("PATHNAME" created)
- #
- # Returns host and port.
- def parse257(resp) # :nodoc:
- 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
-
- # :stopdoc:
- class NullSocket
- def read_timeout=(sec)
- end
-
- def close
- end
-
- def method_missing(mid, *args)
- raise FTPConnectionError, "not connected"
- end
- end
-
- class BufferedSocket < BufferedIO
- [:addr, :peeraddr, :send, :shutdown].each do |method|
- define_method(method) { |*args|
- @io.__send__(method, *args)
- }
- end
-
- def read(len = nil)
- if len
- s = super(len, "", true)
- return s.empty? ? nil : s
- else
- result = ""
- while s = super(DEFAULT_BLOCKSIZE, "", true)
- break if s.empty?
- result << s
- end
- return result
- end
- end
-
- def gets
- return readuntil("\n")
- rescue EOFError
- return nil
- end
-
- def readline
- return readuntil("\n")
- end
- end
- # :startdoc:
- 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 973998fdc3..98d6793aee 100644
--- a/lib/net/http.rb
+++ b/lib/net/http.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
#
# = net/http.rb
#
@@ -21,392 +22,729 @@
require 'net/protocol'
require 'uri'
+require 'resolv'
+autoload :OpenSSL, 'openssl'
module Net #:nodoc:
- autoload :OpenSSL, 'openssl'
# :stopdoc:
class HTTPBadResponse < StandardError; end
class HTTPHeaderSyntaxError < StandardError; end
# :startdoc:
- # == An HTTP client API for Ruby.
+ # \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
#
- # Net::HTTP provides a rich library which can be used to build HTTP
- # user-agents. For more details about HTTP see
- # [RFC2616](http://www.ietf.org/rfc/rfc2616.txt)
+ # 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].
#
- # Net::HTTP is designed to work closely with URI. URI::HTTP#host,
- # URI::HTTP#port and URI::HTTP#request_uri are designed to work with
- # Net::HTTP.
+ # == URIs
#
- # If you are only performing a few GET requests you should try OpenURI.
+ # 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].
#
- # == Simple Examples
+ # A Ruby {URI::Generic}[rdoc-ref:URI::Generic] object
+ # represents an internet URI.
+ # It provides, among others, methods
+ # +scheme+, +hostname+, +path+, +query+, and +fragment+.
#
- # All examples assume you have loaded Net::HTTP with:
+ # === Schemes
#
- # require 'net/http'
+ # An internet \URI has
+ # a {scheme}[https://en.wikipedia.org/wiki/List_of_URI_schemes].
#
- # This will also require 'uri' so you don't need to require it separately.
+ # The two schemes supported in \Net::HTTP are <tt>'https'</tt> and <tt>'http'</tt>:
#
- # The Net::HTTP methods in the following section do not persist
- # connections. They are not recommended if you are performing many HTTP
- # requests.
+ # uri.scheme # => "https"
+ # URI('http://example.com').scheme # => "http"
#
- # === GET
+ # === Hostnames
#
- # Net::HTTP.get('example.com', '/index.html') # => String
+ # A hostname identifies a server (host) to which requests may be sent:
#
- # === GET by URI
+ # hostname = uri.hostname # => "jsonplaceholder.typicode.com"
+ # Net::HTTP.start(hostname) do |http|
+ # # Some HTTP stuff.
+ # end
#
- # uri = URI('http://example.com/index.html?count=10')
- # Net::HTTP.get(uri) # => String
+ # === Paths
#
- # === GET with Dynamic Parameters
+ # A host-specific path identifies a resource on the host:
#
- # uri = URI('http://example.com/index.html')
- # params = { :limit => 10, :page => 3 }
- # uri.query = URI.encode_www_form(params)
+ # _uri = uri.dup
+ # _uri.path = '/todos/1'
+ # hostname = _uri.hostname
+ # path = _uri.path
+ # Net::HTTP.get(hostname, path)
#
- # res = Net::HTTP.get_response(uri)
- # puts res.body if res.is_a?(Net::HTTPSuccess)
+ # === Queries
#
- # === POST
+ # A host-specific query adds name/value pairs to the URI:
#
- # uri = URI('http://www.example.com/search.cgi')
- # res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50')
- # puts res.body
+ # _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)
#
- # === POST with Multiple Values
+ # === Fragments
#
- # uri = URI('http://www.example.com/search.cgi')
- # res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50')
- # puts res.body
+ # 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.
#
- # == How to use Net::HTTP
+ # == Request Headers
#
- # The following example code can be used as the basis of a HTTP user-agent
- # which can perform a variety of request types using persistent
- # connections.
+ # 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.
#
- # uri = URI('http://example.com/some_path?query=string')
+ # 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:
#
- # Net::HTTP.start(uri.host, uri.port) do |http|
- # request = Net::HTTP::Get.new uri
+ # headers = {Accept: 'application/json', Connection: 'Keep-Alive'}
+ # Net::HTTP.get(uri, headers)
#
- # response = http.request request # Net::HTTPResponse object
- # end
+ # 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.
#
- # Net::HTTP::start immediately creates a connection to an HTTP server which
- # is kept open for the duration of the block. The connection will remain
- # open for multiple requests in the block if the server indicates it
- # supports persistent connections.
+ # == \HTTP Sessions
#
- # The request types Net::HTTP supports are listed below in the section "HTTP
- # Request Classes".
+ # A _session_ is a connection between a server (host) and a client that:
#
- # If you wish to re-use a connection across multiple HTTP requests without
- # automatically closing it you can use ::new instead of ::start. #request
- # will automatically open a connection to the server if one is not currently
- # open. You can manually close the connection with #finish.
+ # - Is begun by instance method Net::HTTP#start.
+ # - May contain any number of requests.
+ # - Is ended by instance method Net::HTTP#finish.
#
- # For all the Net::HTTP request objects and shortcut request methods you may
- # supply either a String for the request path or a URI from which Net::HTTP
- # will extract the request path.
+ # See example sessions at {Strategies}[rdoc-ref:Net::HTTP@Strategies].
#
- # === Response Data
+ # === Session Using \Net::HTTP.start
#
- # uri = URI('http://example.com/index.html')
- # res = Net::HTTP.get_response(uri)
+ # 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:
#
- # # Headers
- # res['Set-Cookie'] # => String
- # res.get_fields('set-cookie') # => Array
- # res.to_hash['set-cookie'] # => Array
- # puts "Headers: #{res.to_hash.inspect}"
+ # - Calling #start before block execution.
+ # - Executing the block.
+ # - Calling #finish after block execution.
#
- # # Status
- # puts res.code # => '200'
- # puts res.message # => 'OK'
- # puts res.class.name # => 'HTTPOK'
+ # In the block, you can use these instance methods,
+ # each of which that sends a single request:
#
- # # Body
- # puts res.body if res.response_body_permitted?
+ # - {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods]:
#
- # === Following Redirection
+ # - #get, #request_get: GET.
+ # - #head, #request_head: HEAD.
+ # - #post, #request_post: POST.
+ # - #delete: DELETE.
+ # - #options: OPTIONS.
+ # - #trace: TRACE.
+ # - #patch: PATCH.
#
- # Each Net::HTTPResponse object belongs to a class for its response code.
+ # - {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]:
#
- # For example, all 2XX responses are instances of a Net::HTTPSuccess
- # subclass, a 3XX response is an instance of a Net::HTTPRedirection
- # subclass and a 200 response is an instance of the Net::HTTPOK class. For
- # details of response classes, see the section "HTTP Response Classes"
- # below.
+ # - #copy: COPY.
+ # - #lock: LOCK.
+ # - #mkcol: MKCOL.
+ # - #move: MOVE.
+ # - #propfind: PROPFIND.
+ # - #proppatch: PROPPATCH.
+ # - #unlock: UNLOCK.
#
- # Using a case statement you can handle various types of responses properly:
+ # === Session Using \Net::HTTP.start and \Net::HTTP.finish
#
- # def fetch(uri_str, limit = 10)
- # # You should choose a better exception.
- # raise ArgumentError, 'too many HTTP redirects' if limit == 0
+ # You can manage a session manually using methods #start and #finish:
#
- # response = Net::HTTP.get_response(URI(uri_str))
+ # 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.
#
- # case response
- # when Net::HTTPSuccess then
- # response
- # when Net::HTTPRedirection then
- # location = response['location']
- # warn "redirected to #{location}"
- # fetch(location, limit - 1)
- # else
- # response.value
- # end
- # end
+ # === Single-Request Session
#
- # print fetch('http://www.ruby-lang.org')
+ # Certain convenience methods automatically handle a session by:
#
- # === POST
+ # - Creating an \HTTP object
+ # - Starting a session.
+ # - Sending a single request.
+ # - Finishing the session.
+ # - Destroying the object.
#
- # A POST can be made using the Net::HTTP::Post request class. This example
- # creates a urlencoded POST body:
+ # Such methods that send GET requests:
#
- # uri = URI('http://www.example.com/todo.cgi')
- # req = Net::HTTP::Post.new(uri)
- # req.set_form_data('from' => '2005-01-01', 'to' => '2005-03-31')
+ # - ::get: Returns the string response body.
+ # - ::get_print: Writes the string response body to $stdout.
+ # - ::get_response: Returns a Net::HTTPResponse object.
#
- # res = Net::HTTP.start(uri.hostname, uri.port) do |http|
- # http.request(req)
- # end
+ # Such methods that send POST requests:
#
- # case res
- # when Net::HTTPSuccess, Net::HTTPRedirection
- # # OK
- # else
- # res.value
- # end
+ # - ::post: Posts data to the host.
+ # - ::post_form: Posts form data to the host.
#
- # At this time Net::HTTP does not support multipart/form-data. To send
- # multipart/form-data use Net::HTTPRequest#body= and
- # Net::HTTPRequest#content_type=:
+ # == \HTTP Requests and Responses
#
- # req = Net::HTTP::Post.new(uri)
- # req.body = multipart_data
- # req.content_type = 'multipart/form-data'
+ # 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.
#
- # Other requests that can contain a body such as PUT can be created in the
- # same way using the corresponding request class (Net::HTTP::Put).
+ # You can, however, directly create a request object, send the request,
+ # and retrieve the response object; see:
#
- # === Setting Headers
+ # - Net::HTTPRequest.
+ # - Net::HTTPResponse.
#
- # The following example performs a conditional GET using the
- # If-Modified-Since header. If the files has not been modified since the
- # time in the header a Not Modified response will be returned. See RFC 2616
- # section 9.3 for further details.
+ # == Following Redirection
#
- # uri = URI('http://example.com/cached_response')
- # file = File.stat 'cached_response'
+ # Each returned response is an instance of a subclass of Net::HTTPResponse.
+ # See the {response class hierarchy}[rdoc-ref:Net::HTTPResponse@Response+Subclasses].
#
- # req = Net::HTTP::Get.new(uri)
- # req['If-Modified-Since'] = file.mtime.rfc2822
+ # In particular, class Net::HTTPRedirection is the parent
+ # of all redirection classes.
+ # This allows you to craft a case statement to handle redirections properly:
#
- # res = Net::HTTP.start(uri.hostname, uri.port) {|http|
- # http.request(req)
- # }
+ # def fetch(uri, limit = 10)
+ # # You should choose a better exception.
+ # raise ArgumentError, 'Too many HTTP redirects' if limit == 0
+ #
+ # res = Net::HTTP.get_response(URI(uri))
+ # case res
+ # when Net::HTTPSuccess # Any success class.
+ # res
+ # when Net::HTTPRedirection # Any redirection class.
+ # location = res['Location']
+ # warn "Redirected to #{location}"
+ # fetch(location, limit - 1)
+ # else # Any other class.
+ # res.value
+ # end
+ # end
#
- # open 'cached_response', 'w' do |io|
- # io.write res.body
- # end if res.is_a?(Net::HTTPSuccess)
+ # fetch(uri)
#
- # === Basic Authentication
+ # == Basic Authentication
#
# Basic authentication is performed according to
- # [RFC2617](http://www.ietf.org/rfc/rfc2617.txt)
- #
- # uri = URI('http://example.com/index.html?key=value')
+ # {RFC2617}[http://www.ietf.org/rfc/rfc2617.txt]:
#
# req = Net::HTTP::Get.new(uri)
- # req.basic_auth 'user', 'pass'
- #
- # res = Net::HTTP.start(uri.hostname, uri.port) {|http|
+ # req.basic_auth('user', 'pass')
+ # res = Net::HTTP.start(hostname) do |http|
# http.request(req)
- # }
- # puts res.body
+ # end
#
- # === Streaming Response Bodies
+ # == Streaming Response Bodies
#
- # By default Net::HTTP reads an entire response into memory. If you are
+ # 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.
#
- # uri = URI('http://example.com/large_file')
- #
- # Net::HTTP.start(uri.host, uri.port) do |http|
- # request = Net::HTTP::Get.new uri
- #
- # http.request request do |response|
- # open 'large_file', 'w' do |io|
- # response.read_body do |chunk|
- # io.write chunk
+ # Net::HTTP.start(hostname) do |http|
+ # req = Net::HTTP::Get.new(uri)
+ # http.request(req) do |res|
+ # open('t.tmp', 'w') do |f|
+ # res.read_body do |chunk|
+ # f.write chunk
# end
# end
# end
# end
#
- # === HTTPS
+ # == HTTPS
#
- # HTTPS is enabled for an HTTP connection by Net::HTTP#use_ssl=.
+ # HTTPS is enabled for an \HTTP connection by Net::HTTP#use_ssl=:
#
- # uri = URI('https://secure.example.com/some_path?query=string')
+ # Net::HTTP.start(hostname, :use_ssl => true) do |http|
+ # req = Net::HTTP::Get.new(uri)
+ # res = http.request(req)
+ # end
#
- # Net::HTTP.start(uri.host, uri.port,
- # :use_ssl => uri.scheme == 'https') do |http|
- # request = Net::HTTP::Get.new uri
+ # 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:
#
- # response = http.request request # Net::HTTPResponse object
- # end
+ # 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
#
- # In previous versions of ruby you would need to require 'net/https' to use
- # HTTPS. This is no longer true.
+ # - 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
#
- # Net::HTTP will automatically create a proxy from the +http_proxy+
- # environment variable if it is present. To disable use of +http_proxy+,
- # pass +nil+ for the proxy address.
- #
- # You may also create a custom proxy:
- #
- # proxy_addr = 'your.proxy.host'
- # proxy_port = 8080
- #
- # Net::HTTP.new('example.com', nil, proxy_addr, proxy_port).start { |http|
- # # always proxy via your.proxy.addr:8080
- # }
- #
- # See Net::HTTP.new for further details and examples such as proxies that
- # require a username and password.
- #
- # === Compression
- #
- # Net::HTTP automatically adds Accept-Encoding for compression of response
- # bodies and automatically decompresses gzip and deflate responses unless a
- # Range header was sent.
- #
- # Compression can be disabled through the Accept-Encoding: identity header.
- #
- # == HTTP Request Classes
- #
- # Here is the HTTP request class hierarchy.
- #
- # * Net::HTTPRequest
- # * Net::HTTP::Get
- # * Net::HTTP::Head
- # * Net::HTTP::Post
- # * Net::HTTP::Patch
- # * 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 and are subclasses of Net::HTTPResponse.
- #
- # HTTPUnknownResponse:: For unhandled HTTP extensions
- # HTTPInformation:: 1xx
- # HTTPContinue:: 100
- # HTTPSwitchProtocol:: 101
- # HTTPSuccess:: 2xx
- # HTTPOK:: 200
- # HTTPCreated:: 201
- # HTTPAccepted:: 202
- # HTTPNonAuthoritativeInformation:: 203
- # HTTPNoContent:: 204
- # HTTPResetContent:: 205
- # HTTPPartialContent:: 206
- # HTTPMultiStatus:: 207
- # HTTPRedirection:: 3xx
- # HTTPMultipleChoices:: 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
- # HTTPUnprocessableEntity:: 422
- # HTTPLocked:: 423
- # HTTPFailedDependency:: 424
- # HTTPUpgradeRequired:: 426
- # HTTPPreconditionRequired:: 428
- # HTTPTooManyRequests:: 429
- # HTTPRequestHeaderFieldsTooLarge:: 431
- # HTTPServerError:: 5xx
- # HTTPInternalServerError:: 500
- # HTTPNotImplemented:: 501
- # HTTPBadGateway:: 502
- # HTTPServiceUnavailable:: 503
- # HTTPGatewayTimeOut:: 504
- # HTTPVersionNotSupported:: 505
- # HTTPInsufficientStorage:: 507
- # HTTPNetworkAuthenticationRequired:: 511
- #
- # There is also the Net::HTTPBadResponse exception which is raised when
- # there is a protocol error.
+ # - {: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'
begin
require 'zlib'
- require 'stringio' #for our purposes (unpacking gzip) lump these together
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 or later.
+ # Returns +true+; retained for compatibility.
def HTTP.version_1_2
true
end
- # Returns true if net/http is in version 1.2 mode.
- # Defaults to true.
+ # Returns +true+; retained for compatibility.
def HTTP.version_1_2?
true
end
+ # Returns +false+; retained for compatibility.
def HTTP.version_1_1? #:nodoc:
false
end
@@ -416,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
- #
-
- #
- # Gets the body text from the target and outputs it to $stdout. The
- # target can either be specified as
- # (+uri+), or as (+host+, +path+, +port+ = 80); so:
- #
- # Net::HTTP.get_print URI('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
@@ -440,64 +769,117 @@ module Net #:nodoc:
nil
end
- # Sends a GET request to the target and returns the HTTP response
- # as a string. The target can either be specified as
- # (+uri+), or as (+host+, +path+, +port+ = 80); so:
+ # :call-seq:
+ # Net::HTTP.get(hostname, path, port = 80) -> body
+ # Net::HTTP:get(uri, headers = {}, port = uri.port) -> body
#
- # print Net::HTTP.get(URI('http://www.example.com/index.html'))
+ # Sends a GET request and returns the \HTTP response body as a string.
#
- # or:
+ # With string arguments +hostname+ and +path+:
#
- # print Net::HTTP.get('www.example.com', '/index.html')
+ # hostname = 'jsonplaceholder.typicode.com'
+ # path = '/todos/1'
+ # puts Net::HTTP.get(hostname, path)
#
- def HTTP.get(uri_or_host, path = nil, port = nil)
- get_response(uri_or_host, path, port).body
- end
-
- # Sends a GET request to the target and returns the HTTP response
- # as a Net::HTTPResponse object. The target can either be specified as
- # (+uri+), or as (+host+, +path+, +port+ = 80); so:
+ # Output:
+ #
+ # {
+ # "userId": 1,
+ # "id": 1,
+ # "title": "delectus aut autem",
+ # "completed": false
+ # }
#
- # res = Net::HTTP.get_response(URI('http://www.example.com/index.html'))
- # print res.body
+ # With URI object +uri+ and optional hash argument +headers+:
#
- # or:
+ # uri = URI('https://jsonplaceholder.typicode.com/todos/1')
+ # headers = {'Content-type' => 'application/json; charset=UTF-8'}
+ # Net::HTTP.get(uri, headers)
#
- # res = Net::HTTP.get_response('www.example.com', '/index.html')
- # print res.body
+ # Related:
+ #
+ # - Net::HTTP::Get: request class for \HTTP method +GET+.
+ # - Net::HTTP#get: convenience method for \HTTP method +GET+.
+ #
+ def HTTP.get(uri_or_host, path_or_headers = nil, port = nil)
+ get_response(uri_or_host, path_or_headers, port).body
+ end
+
+ # :call-seq:
+ # Net::HTTP.get_response(hostname, path, port = 80) -> http_response
+ # Net::HTTP:get_response(uri, headers = {}, port = uri.port) -> http_response
#
- 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
+ headers = path_or_headers
start(uri.hostname, uri.port,
:use_ssl => uri.scheme == 'https') {|http|
- return http.request_get(uri, &block)
+ return http.request_get(uri, headers, &block)
}
end
end
- # Posts HTML form data to the specified URI object.
- # The form data must be provided as a Hash mapping from String to String.
- # Example:
+ # 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.
- # But userinfo for authentication is deprecated (RFC3986).
- # So this feature will be removed.
+ # _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+.
+ #
+ 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:
+ #
+ # _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
#
- # require 'net/http'
- # require 'uri'
+ # Output:
#
- # Net::HTTP.post_form URI('http://www.example.com/search.cgi'),
- # { "q" => "ruby", "max" => "50" }
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": "1",
+ # "id": 101
+ # }
#
def HTTP.post_form(url, params)
req = Post.new(url)
@@ -509,21 +891,63 @@ module Net #:nodoc:
}
end
+ # Sends a PUT request to the server; returns a Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a URL;
+ # argument +data+ must be a string:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # headers = {'content-type': 'application/json'}
+ # res = Net::HTTP.put(_uri, data, headers) # => #<Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": 1,
+ # "id": 101
+ # }
+ #
+ # Related:
+ #
+ # - Net::HTTP::Put: request class for \HTTP method +PUT+.
+ # - Net::HTTP#put: convenience method for \HTTP method +PUT+.
+ #
+ def HTTP.put(url, data, header = nil)
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.put(url, data, header)
+ }
+ end
+
#
- # HTTP session management
+ # \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
@@ -533,40 +957,98 @@ module Net #:nodoc:
end
# :call-seq:
- # HTTP.start(address, port, p_addr, p_port, p_user, p_pass, &block)
- # HTTP.start(address, port=nil, p_addr=nil, p_port=nil, p_user=nil, p_pass=nil, opt, &block)
- #
- # Creates a new Net::HTTP object, then additionally opens the TCP
- # connection and HTTP session.
- #
- # Arguments are the following:
- # _address_ :: hostname or IP address of the server
- # _port_ :: port of the server
- # _p_addr_ :: address of proxy
- # _p_port_ :: port of proxy
- # _p_user_ :: user of proxy
- # _p_pass_ :: pass of proxy
- # _opt_ :: optional hash
- #
- # _opt_ sets following values by its accessor.
- # The keys are ca_file, ca_path, cert, cert_store, ciphers,
- # close_on_empty_response, key, open_timeout, read_timeout, ssl_timeout,
- # ssl_version, use_ssl, verify_callback, verify_depth and verify_mode.
- # If you set :use_ssl as true, you can use https and default value of
- # verify_mode is set as OpenSSL::SSL::VERIFY_PEER.
- #
- # 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
- # using the finish() method.
+ # 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]
@@ -586,26 +1068,36 @@ module Net #:nodoc:
alias newobj new # :nodoc:
end
- # Creates a new Net::HTTP object without opening a TCP connection or
- # HTTP session.
+ # 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.
#
- # The +address+ should be a DNS hostname or IP address, the +port+ is the
- # port the server operates on. If no +port+ is given the default port for
- # HTTP or HTTPS is used.
+ # 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:
#
- # If none of the +p_+ arguments are given, the proxy host and port are
- # taken from the +http_proxy+ environment variable (or its uppercase
- # equivalent) if present. If the proxy requires authentication you must
- # supply it by hand. See URI::Generic#find_proxy for details of proxy
- # detection from the environment. To disable proxy detection set +p_addr+
- # to nil.
+ # http = Net::HTTP.new(hostname, 8000)
+ # # => #<Net::HTTP jsonplaceholder.typicode.com:8000 open=false>
+ # http.port # => 8000
#
- # If you are connecting to a custom proxy, +p_addr+ the DNS name or IP
- # address of the proxy host, +p_port+ the port to use to access the proxy,
- # and +p_user+ and +p_pass+ the username and password if authorization is
- # required to use the proxy.
+ # 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)
+ 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()
@@ -614,36 +1106,80 @@ module Net #:nodoc:
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 server address,
- # without opening the TCP connection or initializing the HTTP session.
+ 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)
+ 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
- @keep_alive_timeout = 2
+ @keep_alive_timeout = options[:keep_alive_timeout]
@last_communicated = nil
- @close_on_empty_response = false
+ @close_on_empty_response = options[:close_on_empty_response]
@socket = nil
@started = false
- @open_timeout = nil
- @read_timeout = 60
- @continue_timeout = nil
- @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
@@ -651,17 +1187,22 @@ module Net #:nodoc:
@proxy_port = nil
@proxy_user = nil
@proxy_pass = nil
+ @proxy_use_ssl = nil
@use_ssl = false
@ssl_context = nil
@ssl_session = nil
- @enable_post_connection_check = true
@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
@@ -669,88 +1210,303 @@ module Net #:nodoc:
# *WARNING* This method opens a serious security hole.
# Never use this method in production code.
#
- # Sets 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 DNS host name or IP address 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
- # The local host used to estabilish the connection.
+ # Sets or returns the string local host used to establish the connection;
+ # initially +nil+.
attr_accessor :local_host
- # The local port used to estabilish the connection.
+ # 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
- # Number of seconds to wait for the connection to open. Any number
- # may be used, including Floats for fractional seconds. If the HTTP
- # object cannot open a connection in this many seconds, it raises a
- # Net::OpenTimeout exception. The default value is +nil+.
+ # 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
- # Number of seconds to wait for one block to be read (via one read(2)
- # call). Any number may be used, including Floats for fractional
- # seconds. If the HTTP object cannot read data in this many seconds,
- # it raises a Net::ReadTimeout exception. The default value is 60 seconds.
+ # 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
- # Seconds to wait for 100 Continue response. If the HTTP object does not
- # receive a response in this many seconds it sends the request body. The
- # default value is +nil+.
+ # 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
- # Setter for the continue_timeout attribute.
+ # 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
- # Seconds to reuse the connection of the previous request.
- # If the idle time is less than this Keep-Alive Timeout,
- # Net::HTTP reuses the TCP/IP socket used by the previous communication.
- # The default value is 2 seconds.
+ # 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
- # Returns true if the HTTP session has been started.
+ # 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 SSL/TLS is being used with HTTP.
+ # Returns +true+ if +self+ uses SSL, +false+ otherwise.
+ # See Net::HTTP#use_ssl=.
def use_ssl?
@use_ssl
end
- # 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.
+ # 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
@@ -759,75 +1515,85 @@ module Net #:nodoc:
@use_ssl = flag
end
- SSL_IVNAMES = [
- :@ca_file,
- :@ca_path,
- :@cert,
- :@cert_store,
- :@ciphers,
- :@key,
- :@ssl_timeout,
- :@ssl_version,
- :@verify_callback,
- :@verify_depth,
- :@verify_mode,
- ]
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:
- # Sets path of a CA certification file in PEM format.
- #
- # The file can contain several CA certificates.
+ 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 path of a CA certification directory containing certifications in
- # PEM format.
+ # Sets or returns the path of to CA directory
+ # containing certification files in PEM format.
attr_accessor :ca_path
- # Sets an OpenSSL::X509::Certificate object as client certificate.
- # (This method is appeared in Michal Rokos's OpenSSL extension).
+ # Sets or returns the OpenSSL::X509::Certificate object
+ # to be used for client certification.
attr_accessor :cert
- # Sets the X509::Store to verify peer certificate.
+ # Sets or returns the X509::Store to be used for verifying peer certificate.
attr_accessor :cert_store
- # Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers=
+ # Sets or returns the available SSL ciphers.
+ # See {OpenSSL::SSL::SSLContext#ciphers=}[OpenSSL::SSL::SSL::Context#ciphers=].
attr_accessor :ciphers
- # Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
- # (This method is appeared in Michal Rokos's OpenSSL extension.)
+ # 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 the SSL timeout seconds.
+ # Sets or returns the SSL timeout seconds.
attr_accessor :ssl_timeout
- # Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version=
+ # Sets or returns the SSL version.
+ # See {OpenSSL::SSL::SSLContext#ssl_version=}[OpenSSL::SSL::SSL::Context#ssl_version=].
attr_accessor :ssl_version
- # Sets the verify callback for the server certification verification.
+ # 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 the maximum depth for the certificate chain verification.
+ # Sets or returns the maximum depth for the certificate chain verification.
attr_accessor :verify_depth
- # Sets the flags for server the certification verification at beginning of
- # SSL/TLS session.
- #
+ # 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
- # Returns the X.509 certificates the server presented.
+ # 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
@@ -835,14 +1601,26 @@ module Net #:nodoc:
@socket.io.peer_cert
end
- # Opens a TCP connection and HTTP session.
+ # Starts an \HTTP session.
+ #
+ # Without a block, returns +self+:
#
- # When this method is called with a block, it passes the Net::HTTP
- # object to the block, and closes the TCP connection and HTTP session
- # after the block has been executed.
+ # 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
#
- # When called with a block, it returns the return value of the
- # block; otherwise, it returns self.
+ # With a block, calls the block with +self+,
+ # finishes the session when the block exits,
+ # and returns the block's value:
+ #
+ # http.start do |http|
+ # http
+ # end
+ # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.started? # => false
#
def start # :yield: http
raise IOError, 'HTTP session already opened' if @started
@@ -858,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
@@ -865,85 +1658,150 @@ module Net #:nodoc:
private :do_start
def connect
+ if use_ssl?
+ # reference early to load OpenSSL before connecting,
+ # as OpenSSL may take time to load.
+ @ssl_context = OpenSSL::SSL::SSLContext.new
+ end
+
if proxy? then
- conn_address = proxy_address
- conn_port = proxy_port
+ conn_addr = proxy_address
+ conn_port = proxy_port
else
- conn_address = address
- conn_port = port
+ conn_addr = conn_address
+ conn_port = port
end
- D "opening connection to #{conn_address}:#{conn_port}..."
- s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
- TCPSocket.open(conn_address, conn_port, @local_host, @local_port)
- }
- D "opened"
+ debug "opening connection to #{conn_addr}:#{conn_port}..."
+ begin
+ s = timeouted_connect(conn_addr, conn_port)
+ rescue => e
+ if (defined?(IO::TimeoutError) && e.is_a?(IO::TimeoutError)) || e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions
+ e = Net::OpenTimeout.new(e)
+ end
+ raise e, "Failed to open TCP connection to " +
+ "#{conn_addr}:#{conn_port} (#{e.message})"
+ end
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ debug "opened"
if use_ssl?
+ if proxy?
+ if @proxy_use_ssl
+ proxy_sock = OpenSSL::SSL::SSLSocket.new(s)
+ ssl_socket_connect(proxy_sock, @open_timeout)
+ else
+ proxy_sock = s
+ end
+ proxy_sock = BufferedIO.new(proxy_sock, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ buf = +"CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n" \
+ "Host: #{@address}:#{@port}\r\n"
+ if proxy_user
+ credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
+ buf << "Proxy-Authorization: Basic #{credential}\r\n"
+ end
+ buf << "\r\n"
+ proxy_sock.write(buf)
+ HTTPResponse.read_new(proxy_sock).value
+ # assuming nothing left in buffers after successful CONNECT response
+ end
+
ssl_parameters = Hash.new
iv_list = instance_variables
SSL_IVNAMES.each_with_index do |ivname, i|
- if iv_list.include?(ivname) and
+ if iv_list.include?(ivname)
value = instance_variable_get(ivname)
- ssl_parameters[SSL_ATTRIBUTES[i]] = value if value
+ unless value.nil?
+ ssl_parameters[SSL_ATTRIBUTES[i]] = value
+ end
end
end
- @ssl_context = OpenSSL::SSL::SSLContext.new
@ssl_context.set_params(ssl_parameters)
- D "starting SSL for #{conn_address}:#{conn_port}..."
+ 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
- D "SSL established"
- end
- @socket = BufferedIO.new(s)
- @socket.read_timeout = @read_timeout
- @socket.continue_timeout = @continue_timeout
- @socket.debug_output = @debug_output
- if use_ssl?
- begin
- if proxy?
- buf = "CONNECT #{@address}:#{@port} HTTP/#{HTTPVersion}\r\n"
- buf << "Host: #{@address}:#{@port}\r\n"
- if proxy_user
- credential = ["#{proxy_user}:#{proxy_pass}"].pack('m')
- credential.delete!("\r\n")
- buf << "Proxy-Authorization: Basic #{credential}\r\n"
- end
- buf << "\r\n"
- @socket.write(buf)
- HTTPResponse.read_new(@socket).value
- end
- s.session = @ssl_session if @ssl_session
- # Server Name Indication (SNI) RFC 3546
- s.hostname = @address if s.respond_to? :hostname=
- Timeout.timeout(@open_timeout, Net::OpenTimeout) { s.connect }
- if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
- s.post_connection_check(@address)
- end
- @ssl_session = s.session
- rescue => exception
- D "Conn close because of connect error #{exception}"
- @socket.close if @socket and not @socket.closed?
- raise exception
+ 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 the HTTP session and closes the TCP connection.
- # Raises IOError if the session has not been 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
@@ -961,13 +1819,14 @@ module Net #:nodoc:
@proxy_port = nil
@proxy_user = nil
@proxy_pass = nil
+ @proxy_use_ssl = nil
- # Creates an HTTP proxy class which behaves like Net::HTTP, but
+ # 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)
+ # \Net::HTTP.new. See Net::HTTP.new for details of the arguments.
+ def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ssl = nil) #:nodoc:
return self unless p_addr
Class.new(self) {
@@ -985,85 +1844,118 @@ module Net #:nodoc:
@proxy_user = p_user
@proxy_pass = p_pass
+ @proxy_use_ssl = p_use_ssl
}
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?
defined?(@is_proxy_class) ? @is_proxy_class : false
end
- # Address of proxy host. If Net::HTTP does not use a proxy, nil.
+ # Returns the address of the proxy host, or +nil+ if none;
+ # see Net::HTTP@Proxy+Server.
attr_reader :proxy_address
- # Port number of proxy host. If Net::HTTP does not use a proxy, nil.
+ # Returns the port number of the proxy host, or +nil+ if none;
+ # see Net::HTTP@Proxy+Server.
attr_reader :proxy_port
- # User name for accessing proxy. If Net::HTTP does not use a proxy, nil.
+ # Returns the user name for accessing the proxy, or +nil+ if none;
+ # see Net::HTTP@Proxy+Server.
attr_reader :proxy_user
- # User password for accessing proxy. If Net::HTTP does not use a proxy,
- # nil.
+ # 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 requests for this connection will be proxied
+ # Returns +true+ if a proxy server is defined, +false+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy?
- !!if @proxy_from_env then
- proxy_uri
- else
- @proxy_address
- end
+ !!(@proxy_from_env ? proxy_uri : @proxy_address)
end
- # True if the proxy for this connection is determined from the environment
+ # 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
# The proxy URI determined from the environment for this connection.
def proxy_uri # :nodoc:
- @proxy_uri ||= URI("http://#{address}:#{port}").find_proxy
+ 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
- # The address of the proxy server, if one is configured.
+ # Returns the address of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy_address
if @proxy_from_env then
- proxy_uri && proxy_uri.hostname
+ proxy_uri&.hostname
else
@proxy_address
end
end
- # The port of the proxy server, if one is configured.
+ # Returns the port number of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy_port
if @proxy_from_env then
- proxy_uri && proxy_uri.port
+ proxy_uri&.port
else
@proxy_port
end
end
- # The proxy username, if one is configured
+ # Returns the user name of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy_user
- @proxy_user
+ if @proxy_from_env
+ user = proxy_uri&.user
+ unescape(user) if user
+ else
+ @proxy_user
+ end
end
- # The proxy password, if one is configured
+ # Returns the password of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy_pass
- @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, obsolete
def conn_address # :nodoc:
- address()
+ @ipaddr || address()
end
def conn_port # :nodoc:
@@ -1071,12 +1963,17 @@ module Net #:nodoc:
end
def edit_path(path)
- if proxy? and not use_ssl? then
- "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
@@ -1084,45 +1981,38 @@ module Net #:nodoc:
public
- # Retrieves data from +path+ on the connected-to host which may be an
- # absolute path String or a URI to extract the path from.
+ # :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+.
#
- # +initheader+ must be a Hash like { 'Accept' => '*/*', ... },
- # and it defaults to an empty hash.
- # If +initheader+ doesn't have the key 'accept-encoding', then
- # a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used,
- # so that gzip compression is used in preference to deflate
- # compression, which is used in preference to no compression.
- # Ruby doesn't have libraries to support the compress (Lempel-Ziv)
- # compression, so that is not supported. The intent of this is
- # to reduce bandwidth by default. If this routine sets up
- # compression, then it does the decompression also, removing
- # the header as well to prevent confusion. Otherwise
- # it leaves the body as it found it.
+ # With a block given, calls the block with the response body:
#
- # This method returns a Net::HTTPResponse object.
+ # http = Net::HTTP.new(hostname)
+ # http.get('/todos/1') do |res|
+ # p res
+ # end # => #<Net::HTTPOK 200 OK readbody=true>
#
- # 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.
+ # Output:
#
- # +dest+ argument is obsolete.
- # It still works but you must not use it.
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
#
- # This method never raises an exception.
+ # With no block given, simply returns the response object:
#
- # response = http.get('/index.html')
+ # http.get('/') # => #<Net::HTTPOK 200 OK readbody=true>
#
- # # using block
- # File.open('result.txt', 'w') {|f|
- # http.get('/~foo/') do |str|
- # f.write str
- # end
- # }
+ # Related:
#
- def get(path, initheader = {}, dest = nil, &block) # :yield: +body_segment+
+ # - 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
@@ -1130,198 +2020,317 @@ module Net #:nodoc:
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.
+ # Sends a HEAD request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
#
- # This method never raises an exception.
+ # The request is based on the Net::HTTP::Head object
+ # created from string +path+ and initial headers hash +initheader+:
#
- # response = nil
- # Net::HTTP.start('some.www.server', 80) {|http|
- # response = http.head('/index.html')
- # }
- # p response['content-type']
+ # 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' => '*/*', ... }.
+ # :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+.
#
- # 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.
+ # 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>
#
- # +dest+ argument is obsolete.
- # It still works but you must not use it.
+ # Output:
#
- # This method never raises exception.
+ # "{\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\",\n \"id\": 201\n}"
#
- # response = http.post('/cgi-bin/search.rb', 'query=foo')
+ # With no block given, simply returns the response object:
#
- # # using block
- # File.open('result.txt', 'w') {|f|
- # http.post('/cgi-bin/search.rb', 'query=foo') do |str|
- # f.write str
- # end
- # }
+ # http.post('/todos', data) # => #<Net::HTTPCreated 201 Created readbody=true>
#
- # 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.
+ # Related:
+ #
+ # - Net::HTTP::Post: request class for \HTTP method POST.
+ # - Net::HTTP.post: sends POST request, returns response body.
#
def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
send_entity(path, data, initheader, dest, Post, &block)
end
- # Sends a PATCH request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # :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
- def put(path, data, initheader = nil) #:nodoc:
+ # 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 +path+ and gets a response,
- # as an HTTPResponse object.
+ # 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+.
- # Returns the response as a Net::HTTPResponse object.
+ # Sends a GET request to the server;
+ # forms the response into a Net::HTTPResponse object.
#
- # When called with a block, passes an HTTPResponse object to the block.
- # The body of the response will not have been read yet;
- # the block can process it using HTTPResponse#read_body,
- # if desired.
+ # The request is based on the Net::HTTP::Get object
+ # created from string +path+ and initial headers hash +initheader+.
#
- # Returns the response.
+ # With no block given, returns the response object:
#
- # This method never raises Net::* exceptions.
+ # http = Net::HTTP.new(hostname)
+ # http.request_get('/todos') # => #<Net::HTTPOK 200 OK readbody=true>
#
- # response = http.request_get('/index.html')
- # # The entity body is already read in this case.
- # p response['content-type']
- # puts response.body
+ # With a block given, calls the block with the response object
+ # and returns the response object:
#
- # # Using a block
- # http.request_get('/index.html') {|response|
- # p response['content-type']
- # response.read_body do |str| # read body now
- # print str
- # end
- # }
+ # 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 returns the response
- # as a Net::HTTPResponse object.
- #
- # Returns the response.
+ # Sends a HEAD request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
#
- # This method never raises Net::* exceptions.
+ # The request is based on the Net::HTTP::Head object
+ # created from string +path+ and initial headers hash +initheader+.
#
- # 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+.
+ # Sends a POST request to the server;
+ # forms the response into a Net::HTTPResponse object.
#
- # Returns the response as 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+.
#
- # When called with a block, the block is passed an HTTPResponse
- # object. The body of that response will not have been read yet;
- # the block can process it using HTTPResponse#read_body, if desired.
+ # With no block given, returns the response object:
#
- # Returns the response.
+ # http = Net::HTTP.new(hostname)
+ # http.post('/todos', 'xyzzy')
+ # # => #<Net::HTTPCreated 201 Created readbody=true>
#
- # This method never raises Net::* exceptions.
+ # With a block given, calls the block with the response body
+ # and returns the response object:
#
- # # example
- # response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...')
- # p response.status
- # puts response.body # body is already read in this case
+ # http.post('/todos', 'xyzzy') do |res|
+ # p res
+ # end # => #<Net::HTTPCreated 201 Created readbody=true>
#
- # # 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
- # }
+ # 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
@@ -1331,36 +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.
- # Also sends a DATA string if +data+ is given.
+ # Sends an \HTTP request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
#
- # Returns a Net::HTTPResponse object.
+ # 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].
#
- # This method never raises Net::* exceptions.
+ # Examples:
#
- # response = http.send_request('GET', '/index.html')
- # puts response.body
+ # 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 +req+ to the HTTP server.
+ # 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:
#
- # If +req+ is a Net::HTTP::Post or Net::HTTP::Put request containing
- # data, the data is also sent. Providing data for a Net::HTTP::Head or
- # Net::HTTP::Get request results in an ArgumentError.
+ # http = Net::HTTP.new(hostname)
#
- # Returns an HTTPResponse object.
+ # req = Net::HTTP::Get.new('/todos/1')
+ # http.request(req)
+ # # => #<Net::HTTPOK 200 OK readbody=true>
#
- # When called with a block, passes an HTTPResponse object to the block.
- # The body of the response will not have been read yet;
- # the block can process it using HTTPResponse#read_body,
- # if desired.
+ # req = Net::HTTP::Post.new('/todos')
+ # http.request(req, 'xyzzy')
+ # # => #<Net::HTTPCreated 201 Created readbody=true>
#
- # This method never raises Net::* exceptions.
+ # With a block given, calls the block with the response and returns the response:
+ #
+ # req = Net::HTTP::Get.new('/todos/1')
+ # http.request(req) do |res|
+ # p res
+ # end # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # #<Net::HTTPOK 200 OK readbody=false>
#
def request(req, body = nil, &block) # :yield: +response+
unless started?
@@ -1394,69 +2428,85 @@ module Net #:nodoc:
res
end
- IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc:
+ # :stopdoc:
+
+ IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc:
def transport_request(req)
count = 0
begin
begin_transport req
res = catch(:response) {
- req.exec @socket, @curr_http_version, edit_path(req.path)
+ 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
- end while res.kind_of?(HTTPContinue)
+ res.body_encoding = @response_body_encoding
+ res.ignore_eof = @ignore_eof
+ end while res.kind_of?(HTTPInformation)
res.uri = req.uri
- res.reading_body(@socket, req.response_body_permitted?) {
- yield res if block_given?
- }
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::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
# avoid a dependency on OpenSSL
defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
Timeout::Error => exception
- if count == 0 && IDEMPOTENT_METHODS_.include?(req.method)
+ if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
count += 1
- @socket.close if @socket and not @socket.closed?
- D "Conn close because of error #{exception}, and retry"
+ @socket.close if @socket
+ debug "Conn close because of error #{exception}, and retry"
retry
end
- D "Conn close because of error #{exception}"
- @socket.close if @socket and not @socket.closed?
+ debug "Conn close because of error #{exception}"
+ @socket.close if @socket
raise
end
end_transport req, res
res
rescue => exception
- D "Conn close because of error #{exception}"
- @socket.close if @socket and not @socket.closed?
+ debug "Conn close because of error #{exception}"
+ @socket.close if @socket
raise exception
end
def begin_transport(req)
if @socket.closed?
connect
- elsif @last_communicated && @last_communicated + @keep_alive_timeout < Time.now
- D 'Conn close because of keep_alive_timeout'
- @socket.close
- 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
- host = req['host'] || address
- host = $1 if host =~ /(.*):\d+$/
- req.update_uri host, port, use_ssl?
-
+ req.update_uri address, port, use_ssl?
req['host'] ||= addr_port()
end
@@ -1464,15 +2514,15 @@ module Net #:nodoc:
@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'
- @last_communicated = Time.now
+ debug 'Conn keep-alive'
+ @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC)
else
- D 'Conn close'
+ debug 'Conn close'
@socket.close
end
end
@@ -1521,34 +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 'net/http/exceptions'
-
-require 'net/http/header'
-
-require 'net/http/generic_request'
-require 'net/http/request'
-require 'net/http/requests'
+require_relative 'http/exceptions'
-require 'net/http/response'
-require 'net/http/responses'
+require_relative 'http/header'
-require 'net/http/proxy_delta'
+require_relative 'http/generic_request'
+require_relative 'http/request'
+require_relative 'http/requests'
-require 'net/http/backward'
+require_relative 'http/response'
+require_relative 'http/responses'
+require_relative 'http/proxy_delta'
diff --git a/lib/net/http/backward.rb b/lib/net/http/backward.rb
deleted file mode 100644
index faf47b8489..0000000000
--- a/lib/net/http/backward.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# for backward compatibility
-
-# :enddoc:
-
-class Net::HTTP
- ProxyMod = ProxyDelta
-end
-
-module Net
- HTTPSession = Net::HTTP
-end
-
-module Net::NetPrivate
- HTTPRequest = ::Net::HTTPRequest
-end
-
-Net::HTTPInformationCode = Net::HTTPInformation
-Net::HTTPSuccessCode = Net::HTTPSuccess
-Net::HTTPRedirectionCode = Net::HTTPRedirection
-Net::HTTPRetriableCode = Net::HTTPRedirection
-Net::HTTPClientErrorCode = Net::HTTPClientError
-Net::HTTPFatalErrorCode = Net::HTTPClientError
-Net::HTTPServerErrorCode = Net::HTTPServerError
-Net::HTTPResponceReceiver = Net::HTTPResponse
-
diff --git a/lib/net/http/exceptions.rb b/lib/net/http/exceptions.rb
index 6c5d81cb04..4342cfc0ef 100644
--- a/lib/net/http/exceptions.rb
+++ b/lib/net/http/exceptions.rb
@@ -1,25 +1,35 @@
-# Net::HTTP exception class.
-# You cannot use Net::HTTPExceptions directly; instead, you must use
-# its subclasses.
-module Net::HTTPExceptions
- def initialize(msg, res) #:nodoc:
- super msg
- @response = res
+# 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
- attr_reader :response
- alias data response #:nodoc: obsolete
-end
-class Net::HTTPError < Net::ProtocolError
- include Net::HTTPExceptions
-end
-class Net::HTTPRetriableError < Net::ProtoRetriableError
- include Net::HTTPExceptions
-end
-class Net::HTTPServerException < Net::ProtoServerError
+
+ # :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.
- include Net::HTTPExceptions
-end
-class Net::HTTPFatalError < Net::ProtoFatalError
- include Net::HTTPExceptions
+ HTTPServerException = HTTPClientException # :nodoc:
+ deprecate_constant(:HTTPServerException)
end
-
diff --git a/lib/net/http/generic_request.rb b/lib/net/http/generic_request.rb
index b51034c7ea..5b01ea4abd 100644
--- a/lib/net/http/generic_request.rb
+++ b/lib/net/http/generic_request.rb
@@ -1,40 +1,44 @@
-# HTTPGenericRequest is the parent of the HTTPRequest class.
-# Do not use this directly; use a subclass of HTTPRequest.
+# frozen_string_literal: true
#
-# Mixes in the HTTPHeader module to provide easier access to HTTP headers.
+# \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)
+ 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
- host = @uri.hostname
- host += ":#{@uri.port}" if @uri.port != @uri.class::DEFAULT_PORT
- path = uri_or_path.request_uri
+ @path = uri_or_path.request_uri
+ raise ArgumentError, "no HTTP request path given" unless @path
else
@uri = nil
- host = nil
- path = uri_or_path
+ 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
- raise ArgumentError, "no HTTP request path given" unless path
- raise ArgumentError, "HTTP request path is empty" if path.empty?
- @path = path
-
@decode_content = false
- if @response_has_body and Net::HTTP::HAVE_ZLIB then
+ if Net::HTTP::HAVE_ZLIB then
if !initheader ||
!initheader.keys.any? { |k|
%w[accept-encoding range].include? k.downcase
} then
- @decode_content = true
+ @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"
@@ -44,25 +48,82 @@ class Net::HTTPGenericRequest
initialize_http_header initheader
self['Accept'] ||= '*/*'
self['User-Agent'] ||= 'Ruby'
- self['Host'] ||= host
+ 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
- # Automatically set to false if the user sets the Accept-Encoding header.
- # This indicates they wish to handle Content-encoding in responses
- # themselves.
+ # 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.
@@ -73,21 +134,45 @@ class Net::HTTPGenericRequest
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?
- warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE
+ 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
@@ -95,8 +180,24 @@ class Net::HTTPGenericRequest
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
@@ -117,15 +218,6 @@ class Net::HTTPGenericRequest
#
def exec(sock, ver, path) #:nodoc: internal use only
- if @uri
- if @uri.port == @uri.default_port
- # [Bug #7650] Amazon ECS API and GFE/1.3 disallow extra default port number
- self['host'] = @uri.host
- else
- self['host'] = "#{@uri.host}:#{@uri.port}"
- end
- end
-
if @body
send_request_with_body sock, ver, path, @body
elsif @body_stream
@@ -137,25 +229,40 @@ class Net::HTTPGenericRequest
end
end
- def update_uri(host, port, ssl) # :nodoc: internal use only
+ def update_uri(addr, port, ssl) # :nodoc: internal use only
+ # reflect the connection and @path to @uri
return unless @uri
- @uri.host ||= host
- @uri.port = port
-
- scheme = ssl ? 'https' : 'http'
+ 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
- unless scheme == @uri.scheme then
- new_uri = @uri.to_s.sub(/^https?/, scheme)
- @uri = URI new_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
-
- @uri
end
private
+ # :stopdoc:
+
class Chunker #:nodoc:
def initialize(sock)
@sock = sock
@@ -164,9 +271,8 @@ class Net::HTTPGenericRequest
def write(buf)
# avoid memcpy() of buf, buf can huge and eat memory bandwidth
- @sock.write("#{buf.bytesize.to_s(16)}\r\n")
- rv = @sock.write(buf)
- @sock.write("\r\n")
+ rv = buf.bytesize
+ @sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n")
rv
end
@@ -178,7 +284,6 @@ class Net::HTTPGenericRequest
def send_request_with_body(sock, ver, path, body)
self.content_length = body.bytesize
delete 'Transfer-Encoding'
- supply_default_content_type
write_header sock, ver, path
wait_for_continue sock, ver if sock.continue_timeout
sock.write body
@@ -189,7 +294,6 @@ class Net::HTTPGenericRequest
raise ArgumentError,
"Content-Length not given and Transfer-Encoding is not `chunked'"
end
- supply_default_content_type
write_header sock, ver, path
wait_for_continue sock, ver if sock.continue_timeout
if chunked?
@@ -197,9 +301,7 @@ class Net::HTTPGenericRequest
IO.copy_stream(f, chunker)
chunker.finish
else
- # copy_stream can sendfile() to sock.io unless we use SSL.
- # If sock.io is an SSLSocket, copy_stream will hit SSL_write()
- IO.copy_stream(f, sock.io)
+ IO.copy_stream(f, sock)
end
end
@@ -236,7 +338,7 @@ class Net::HTTPGenericRequest
boundary ||= SecureRandom.urlsafe_base64(40)
chunked_p = chunked?
- buf = ''
+ buf = +''
params.each do |key, value, h={}|
key = quote_string(key, charset)
filename =
@@ -293,12 +395,6 @@ class Net::HTTPGenericRequest
buf.clear
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
-
##
# 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.
@@ -306,7 +402,7 @@ class Net::HTTPGenericRequest
def wait_for_continue(sock, ver)
if ver >= '1.1' and @header['expect'] and
@header['expect'].include?('100-continue')
- if IO.select([sock.io], nil, nil, sock.continue_timeout)
+ 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
@@ -317,7 +413,12 @@ class Net::HTTPGenericRequest
end
def write_header(sock, ver, path)
- buf = "#{@method} #{path} HTTP/#{ver}\r\n"
+ 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
@@ -326,4 +427,3 @@ class Net::HTTPGenericRequest
end
end
-
diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb
index 7e46e04395..5dcdcc7d74 100644
--- a/lib/net/http/header.rb
+++ b/lib/net/http/header.rb
@@ -1,20 +1,209 @@
-# The HTTPHeader module defines methods for reading and writing
-# HTTP headers.
+# frozen_string_literal: true
#
-# It is used as a mixin by other classes, to provide hash-like
-# access to HTTP header values. Unlike raw hash access, HTTPHeader
-# provides access via case-insensitive keys. It also provides
-# methods for accessing commonly-used HTTP header values in more
-# convenient formats.
+# 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)
+ def initialize_http_header(initheader) #:nodoc:
@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]
+ 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
@@ -24,79 +213,160 @@ module Net::HTTPHeader
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"
+ # 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] or return nil
+ a = @header[key.downcase.to_s] or return nil
a.join(', ')
end
- # Sets the header field corresponding to the case-insensitive key.
+ # 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
+ @header.delete key.downcase.to_s
return val
end
- @header[key.downcase] = [val]
+ set_field(key, val)
end
- # [Ruby 1.8.3]
- # Adds a value to a named header field, instead of replacing its value.
- # Second argument +val+ must be a String.
- # See also #[]=, #[] and #get_fields.
+ # 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]:
#
- # 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"]
+ # 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)
- if @header.key?(key.downcase)
- @header[key.downcase].push val
+ stringified_downcased_key = key.downcase.to_s
+ if @header.key?(stringified_downcased_key)
+ append_field_value(@header[stringified_downcased_key], val)
else
- @header[key.downcase] = [val]
+ set_field(key, 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 #[].
+ # :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]:
#
- # 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"
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res.get_fields('Connection') # => ["keep-alive"]
+ # res.get_fields('Nosuch') # => nil
#
def get_fields(key)
- return nil unless @header[key.downcase]
- @header[key.downcase].dup
+ stringified_downcased_key = key.downcase.to_s
+ return nil unless @header[stringified_downcased_key]
+ @header[stringified_downcased_key].dup
end
- # Returns the header field corresponding to the case-insensitive key.
- # Returns the default value +args+, or the result of the block, or
- # raises an IndexError if there's no header field named +key+
- # See Hash#fetch
+ # 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, *args, &block)
+ a = @header.fetch(key.downcase.to_s, *args, &block)
a.kind_of?(Array) ? a.join(', ') : a
end
- # Iterates through the header names and values, passing in the name
- # and value to the code block supplied.
+ # 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
#
- # Example:
+ # Output:
#
- # response.header.each_header {|key,value| puts "#{key} = #{value}" }
+ # ["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__)
+ block_given? or return enum_for(__method__) { @header.size }
@header.each do |k,va|
yield k, va.join(', ')
end
@@ -104,63 +374,119 @@ module Net::HTTPHeader
alias each each_header
- # Iterates through the header names in the header, passing
- # each header name to the code block.
+ # 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__)
+ block_given? or return enum_for(__method__) { @header.size }
@header.each_key(&block)
end
alias each_key each_name
- # Iterates through the header names in the header, passing
- # capitalized header names to the code block.
+ # 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
#
- # Note that header names are capitalized systematically;
- # capitalization may not match that used by the remote HTTP
- # server in its response.
+ # 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__)
+ block_given? or return enum_for(__method__) { @header.size }
@header.each_key do |k|
yield capitalize(k)
end
end
- # Iterates through header values, passing each value to the
- # code block.
+ # 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__)
+ block_given? or return enum_for(__method__) { @header.size }
@header.each_value do |va|
yield va.join(', ')
end
end
- # Removes a header field, specified by case-insensitive key.
+ # 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)
+ @header.delete(key.downcase.to_s)
end
- # true if +key+ header exists.
+ # 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)
+ @header.key?(key.downcase.to_s)
end
- # Returns a Hash consisting of header names and values.
- # e.g.
- # {"cache-control" => "private",
- # "content-type" => "text/html",
- # "date" => "Wed, 22 Jun 2005 22:11:50 GMT"}
+ # 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
- # As for #each_header, except the keys are provided in capitalized form.
+ # Like #each_header, but the keys are returned in capitalized form.
#
- # Note that header names are capitalized systematically;
- # capitalization may not match that used by the remote HTTP
- # server in its response.
+ # Net::HTTPHeader#canonical_each is an alias for Net::HTTPHeader#each_capitalized.
def each_capitalized
- block_given? or return enum_for(__method__)
+ block_given? or return enum_for(__method__) { @header.size }
@header.each do |k,v|
yield capitalize(k), v.join(', ')
end
@@ -168,13 +494,22 @@ module Net::HTTPHeader
alias canonical_each each_capitalized
- def capitalize(name)
- name.split(/-/).map {|s| s.capitalize }.join('-')
+ def capitalize(name) # :nodoc:
+ name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze)
end
private :capitalize
- # Returns an Array of Range objects which represent the Range:
- # HTTP header field, or +nil+ if there is no such header.
+ # 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']
@@ -217,14 +552,31 @@ module Net::HTTPHeader
result
end
- # Sets the HTTP Range: header.
- # Accepts either a Range object as a single argument,
- # or a beginning index and a length from that index.
- # Example:
+ # 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.range = (0..1023)
- # req.set_range 0, 1023
+ # 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'
@@ -237,7 +589,7 @@ module Net::HTTPHeader
rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
when Range
first = r.first
- last = r.last
+ last = r.end
last -= 1 if r.exclude_end?
if last == -1
rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
@@ -256,8 +608,15 @@ module Net::HTTPHeader
alias range= set_range
- # Returns an Integer object which represents the HTTP Content-Length:
- # header field, or +nil+ if that field was not provided.
+ # 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
@@ -265,6 +624,20 @@ module Net::HTTPHeader
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'
@@ -273,53 +646,99 @@ module Net::HTTPHeader
@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.
+ # 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 which represents the value of the Content-Range:
- # header field.
- # For a partial entity body, this indicates where this fragment
- # fits inside the full entity body, as range of byte offsets.
+ # 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<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or
+ m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or
raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
- m[1].to_i .. m[2].to_i
+ return unless m[1] == 'bytes'
+ m[2].to_i .. m[3].to_i
end
- # The length of the range represented in Content-Range: header.
+ # 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 a content type string such as "text/html".
- # This method returns nil if Content-Type: header field does not exist.
+ # 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
- return nil unless main_type()
- if sub_type()
- then "#{main_type()}/#{sub_type()}"
- else main_type()
+ main = main_type()
+ return nil unless main
+
+ sub = sub_type()
+ if sub
+ "#{main}/#{sub}"
+ else
+ main
end
end
- # Returns a content type string such as "text".
- # This method returns nil if Content-Type: header field does not exist.
+ # 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 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").
+ # 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('/')
@@ -327,9 +746,14 @@ module Net::HTTPHeader
sub.strip
end
- # Any parameters specified for the content type, returned as a Hash.
- # For example, a header of Content-Type: text/html; charset=EUC-JP
- # would result in type_params returning {'charset' => 'EUC-JP'}
+ # 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(';')
@@ -341,29 +765,54 @@ module Net::HTTPHeader
result
end
- # Sets the content type in an HTTP header.
- # The +type+ should be a full HTTP content type, e.g. "text/html".
- # The +params+ are an optional Hash of parameters to add after the
- # content type, e.g. {'charset' => 'iso-8859-1'}
+ # 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
- # Set header fields and a body from HTML form data.
- # +params+ should be an Array of Arrays or
- # a Hash containing HTML form data.
- # Optional argument +sep+ means data record separator.
+ # 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>.
#
- # Values are URL encoded as necessary and the content-type is set to
- # application/x-www-form-urlencoded
+ # The resulting request is suitable for HTTP request +POST+ or +PUT+.
#
- # Example:
- # http.form_data = {"q" => "ruby", "lang" => "en"}
- # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"}
- # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';')
+ # 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 != '&'
@@ -373,33 +822,108 @@ module Net::HTTPHeader
alias form_data= set_form_data
- # Set a HTML form data set.
- # +params+ is the form data set; it is an Array of Arrays or a Hash
- # +enctype is the type to encode the form data set.
- # It is application/x-www-form-urlencoded or multipart/form-data.
- # +formpot+ is an optional hash to specify the detail.
+ # 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:
#
- # boundary:: the boundary of the multipart message
- # charset:: the charset of the message. All names and the values of
- # non-file fields are encoded as the charset.
+ # require 'stringio'
+ # req.set_form([['file', StringIO.new('Ruby is cool.')]])
#
- # Each item of params is an array and contains following items:
- # +name+:: the name of the field
- # +value+:: the value of the field, it should be a String or a File
- # +opt+:: an optional hash to specify additional information
+ # - A string name, an IO stream opened for reading,
+ # and an options hash, which may contain these entries:
#
- # Each item is a file field or a normal field.
- # If +value+ is a File object or the +opt+ have a filename key,
- # the item is treated as a file field.
+ # - +:filename+: The name of the file to use.
+ # - +:content_type+: The content type of the uploaded file.
#
- # If Transfer-Encoding is set as chunked, this send the request in
- # chunked encoding. Because chunked encoding is HTTP/1.1 feature,
- # you must confirm the server to support HTTP/1.1 before sending it.
+ # Example:
#
- # Example:
- # http.set_form([["q", "ruby"], ["lang", "en"]])
+ # req.set_form([['file', file, {filename: "other-filename.foo"}]]
#
- # See also RFC 2388, RFC 2616, HTML 4.01, and HTML5
+ # 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
@@ -415,38 +939,47 @@ module Net::HTTPHeader
end
end
- # Set the Authorization: header for "Basic" authorization.
+ # 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
- # Set Proxy-Authorization: header for "Basic" authorization.
+ # 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)
- 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n")
+ 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?
- tokens(@header['connection']).include?('close') or
- tokens(@header['proxy-connection']).include?('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?
- tokens(@header['connection']).include?('keep-alive') or
- tokens(@header['proxy-connection']).include?('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
- 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
-
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
index b16c9f1ed8..e7d30def64 100644
--- a/lib/net/http/proxy_delta.rb
+++ b/lib/net/http/proxy_delta.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
module Net::HTTP::ProxyDelta #:nodoc: internal use only
private
diff --git a/lib/net/http/request.rb b/lib/net/http/request.rb
index e8b0f48fcc..4a138572e9 100644
--- a/lib/net/http/request.rb
+++ b/lib/net/http/request.rb
@@ -1,7 +1,76 @@
-# HTTP request class.
-# This class wraps together the request header and the request path.
-# You cannot use this class directly. Instead, you should use one of its
-# subclasses: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Head.
+# 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+.
@@ -17,4 +86,3 @@ class Net::HTTPRequest < Net::HTTPGenericRequest
path, initheader
end
end
-
diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb
index ef719ea6e7..939d413f91 100644
--- a/lib/net/http/requests.rb
+++ b/lib/net/http/requests.rb
@@ -1,67 +1,271 @@
-#
+# frozen_string_literal: true
+
# HTTP/1.1 methods --- RFC2616
-#
-# See Net::HTTPGenericRequest for attributes and methods.
-# See Net::HTTP for usage examples.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
-# See Net::HTTP for usage examples.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
-# See Net::HTTP for usage examples.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
-# See Net::HTTP for usage examples.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
-# See Net::HTTP for usage examples.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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 = false
+ RESPONSE_HAS_BODY = true
end
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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]:
#
-# PATCH method --- RFC5789
+# 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.
#
-
-# See Net::HTTPGenericRequest for attributes and methods.
class Net::HTTP::Patch < Net::HTTPRequest
+ # :stopdoc:
METHOD = 'PATCH'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
@@ -71,52 +275,170 @@ end
# WebDAV methods --- RFC2518
#
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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
-# See Net::HTTPGenericRequest for attributes and methods.
+# \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
index da3e4b4c8c..8804a99c9e 100644
--- a/lib/net/http/response.rb
+++ b/lib/net/http/response.rb
@@ -1,17 +1,136 @@
-# HTTP response class.
+# 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:
#
-# This class wraps together the response header and the response body (the
-# entity requested).
+# - Net::HTTPContinue (100)
+# - Net::HTTPSwitchProtocol (101)
+# - Net::HTTPProcessing (102)
+# - Net::HTTPEarlyHints (103)
#
-# It mixes in the HTTPHeader module, which provides access to response
-# header values both via hash-like methods and via individual readers.
+# - Net::HTTPSuccess:
#
-# Note that each possible HTTP response code defines its own
-# HTTPResponse subclass. These are listed below.
+# - 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)
#
-# All classes are defined under the Net module. Indentation indicates
-# inheritance. For a list of the classes see Net::HTTP.
+# - 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
@@ -34,10 +153,11 @@ class Net::HTTPResponse
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
+ 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
@@ -81,6 +201,8 @@ class Net::HTTPResponse
@read = false
@uri = nil
@decode_content = false
+ @body_encoding = false
+ @ignore_eof = true
end
# The HTTP version supported by the server.
@@ -103,7 +225,42 @@ class Net::HTTPResponse
# Accept-Encoding header from the user.
attr_accessor :decode_content
- def inspect
+ # 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
@@ -116,7 +273,9 @@ class Net::HTTPResponse
end
def error! #:nodoc:
- raise error_type().new(@code + ' ' + @message.dump, self)
+ message = @code
+ message = "#{message} #{@message.dump}" if @message
+ raise error_type().new(message, self)
end
def error_type #:nodoc:
@@ -137,17 +296,17 @@ class Net::HTTPResponse
#
def response #:nodoc:
- warn "#{caller(1)[0]}: warning: Net::HTTPResponse#response is obsolete" if $VERBOSE
+ warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE
self
end
def header #:nodoc:
- warn "#{caller(1)[0]}: warning: Net::HTTPResponse#header is obsolete" if $VERBOSE
+ warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE
self
end
def read_header #:nodoc:
- warn "#{caller(1)[0]}: warning: Net::HTTPResponse#read_header is obsolete" if $VERBOSE
+ warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE
self
end
@@ -171,6 +330,10 @@ class Net::HTTPResponse
# 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.
#
@@ -204,30 +367,42 @@ class Net::HTTPResponse
@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 full entity body.
+ # Returns the string response body;
+ # note that repeated calls for the unmodified body return a cached string:
#
- # Calling this method a second or subsequent time will return the
- # string already read.
+ # path = '/todos/1'
+ # Net::HTTP.start(hostname) do |http|
+ # res = http.get(path)
+ # p res.body
+ # p http.head(path).body # No body.
+ # end
#
- # http.request_get('/index.html') {|res|
- # puts res.body
- # }
+ # Output:
#
- # http.request_get('/index.html') {|res|
- # p res.body.object_id # 538149362
- # p res.body.object_id # 538149362
- # }
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
+ # nil
#
def body
read_body()
end
- # Because it may be necessary to modify the body, Eg, decompression
- # this method facilitates that.
+ # Sets the body of the response to the given value.
def body=(value)
@body = value
end
@@ -236,13 +411,148 @@ class Net::HTTPResponse
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 unsupported or zlib is missing the plain socket is
+ # 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
+ # 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:
@@ -250,7 +560,8 @@ class Net::HTTPResponse
return yield @socket unless @decode_content
return yield @socket if self['content-range']
- case self['content-encoding']
+ v = self['content-encoding']
+ case v&.downcase
when 'deflate', 'gzip', 'x-gzip' then
self.delete 'content-encoding'
@@ -258,8 +569,17 @@ class Net::HTTPResponse
begin
yield inflate_body_io
+ success = true
ensure
- inflate_body_io.finish
+ 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'
@@ -281,7 +601,7 @@ class Net::HTTPResponse
clen = content_length()
if clen
- @socket.read clen, dest, true # ignore EOF
+ @socket.read clen, dest, @ignore_eof
return
end
clen = range_length()
@@ -301,7 +621,6 @@ class Net::HTTPResponse
# See RFC 2616 section 3.6.1 for definitions
def read_chunked(dest, chunk_data_io) # :nodoc:
- len = nil
total = 0
while true
line = @socket.readline
@@ -322,7 +641,7 @@ class Net::HTTPResponse
end
def stream_check
- raise IOError, 'attempt to read body out of block' if @socket.closed?
+ raise IOError, 'attempt to read body out of block' if @socket.nil? || @socket.closed?
end
def procdest(dest, block)
@@ -331,7 +650,7 @@ class Net::HTTPResponse
if block
Net::ReadAdapter.new(block)
else
- dest || ''
+ dest || +''
end
end
@@ -354,18 +673,33 @@ class Net::HTTPResponse
# 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
diff --git a/lib/net/http/responses.rb b/lib/net/http/responses.rb
index 38a5da2443..941a6fed80 100644
--- a/lib/net/http/responses.rb
+++ b/lib/net/http/responses.rb
@@ -1,214 +1,1183 @@
-# :stopdoc:
-class Net::HTTPUnknownResponse < Net::HTTPResponse
- HAS_BODY = true
- EXCEPTION_TYPE = Net::HTTPError
-end
-class Net::HTTPInformation < Net::HTTPResponse # 1xx
- HAS_BODY = false
- EXCEPTION_TYPE = Net::HTTPError
-end
-class Net::HTTPSuccess < Net::HTTPResponse # 2xx
- HAS_BODY = true
- EXCEPTION_TYPE = Net::HTTPError
-end
-class Net::HTTPRedirection < Net::HTTPResponse # 3xx
- HAS_BODY = true
- EXCEPTION_TYPE = Net::HTTPRetriableError
-end
-class Net::HTTPClientError < Net::HTTPResponse # 4xx
- HAS_BODY = true
- EXCEPTION_TYPE = Net::HTTPServerException # for backward compatibility
-end
-class Net::HTTPServerError < Net::HTTPResponse # 5xx
- HAS_BODY = true
- EXCEPTION_TYPE = Net::HTTPFatalError # for backward compatibility
-end
+# frozen_string_literal: true
+#--
+# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
-class Net::HTTPContinue < Net::HTTPInformation # 100
- HAS_BODY = false
-end
-class Net::HTTPSwitchProtocol < Net::HTTPInformation # 101
- HAS_BODY = false
-end
-# 102 - RFC 2518; removed in RFC 4918
+module Net
-class Net::HTTPOK < Net::HTTPSuccess # 200
- HAS_BODY = true
-end
-class Net::HTTPCreated < Net::HTTPSuccess # 201
- HAS_BODY = true
-end
-class Net::HTTPAccepted < Net::HTTPSuccess # 202
- HAS_BODY = true
-end
-class Net::HTTPNonAuthoritativeInformation < Net::HTTPSuccess # 203
- HAS_BODY = true
-end
-class Net::HTTPNoContent < Net::HTTPSuccess # 204
- HAS_BODY = false
-end
-class Net::HTTPResetContent < Net::HTTPSuccess # 205
- HAS_BODY = false
-end
-class Net::HTTPPartialContent < Net::HTTPSuccess # 206
- HAS_BODY = true
-end
-class Net::HTTPMultiStatus < Net::HTTPSuccess # 207 - RFC 4918
- HAS_BODY = true
-end
-# 208 Already Reported - RFC 5842; experimental
-# 226 IM Used - RFC 3229; no famous implementation known
+ # Unknown HTTP response
+ class HTTPUnknownResponse < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPError #
+ end
-class Net::HTTPMultipleChoices < Net::HTTPRedirection # 300
- HAS_BODY = true
-end
-Net::HTTPMultipleChoice = Net::HTTPMultipleChoices
-class Net::HTTPMovedPermanently < Net::HTTPRedirection # 301
- HAS_BODY = true
-end
-class Net::HTTPFound < Net::HTTPRedirection # 302
- HAS_BODY = true
-end
-Net::HTTPMovedTemporarily = Net::HTTPFound
-class Net::HTTPSeeOther < Net::HTTPRedirection # 303
- HAS_BODY = true
-end
-class Net::HTTPNotModified < Net::HTTPRedirection # 304
- HAS_BODY = false
-end
-class Net::HTTPUseProxy < Net::HTTPRedirection # 305
- HAS_BODY = false
-end
-# 306 Switch Proxy - no longer unused
-class Net::HTTPTemporaryRedirect < Net::HTTPRedirection # 307
- HAS_BODY = true
-end
-# 308 Permanent Redirect - in draft
+ # 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
-class Net::HTTPBadRequest < Net::HTTPClientError # 400
- HAS_BODY = true
-end
-class Net::HTTPUnauthorized < Net::HTTPClientError # 401
- HAS_BODY = true
-end
-class Net::HTTPPaymentRequired < Net::HTTPClientError # 402
- HAS_BODY = true
-end
-class Net::HTTPForbidden < Net::HTTPClientError # 403
- HAS_BODY = true
-end
-class Net::HTTPNotFound < Net::HTTPClientError # 404
- HAS_BODY = true
-end
-class Net::HTTPMethodNotAllowed < Net::HTTPClientError # 405
- HAS_BODY = true
-end
-class Net::HTTPNotAcceptable < Net::HTTPClientError # 406
- HAS_BODY = true
-end
-class Net::HTTPProxyAuthenticationRequired < Net::HTTPClientError # 407
- HAS_BODY = true
-end
-class Net::HTTPRequestTimeOut < Net::HTTPClientError # 408
- HAS_BODY = true
-end
-class Net::HTTPConflict < Net::HTTPClientError # 409
- HAS_BODY = true
-end
-class Net::HTTPGone < Net::HTTPClientError # 410
- HAS_BODY = true
-end
-class Net::HTTPLengthRequired < Net::HTTPClientError # 411
- HAS_BODY = true
-end
-class Net::HTTPPreconditionFailed < Net::HTTPClientError # 412
- HAS_BODY = true
-end
-class Net::HTTPRequestEntityTooLarge < Net::HTTPClientError # 413
- HAS_BODY = true
-end
-class Net::HTTPRequestURITooLong < Net::HTTPClientError # 414
- HAS_BODY = true
-end
-Net::HTTPRequestURITooLarge = Net::HTTPRequestURITooLong
-class Net::HTTPUnsupportedMediaType < Net::HTTPClientError # 415
- HAS_BODY = true
-end
-class Net::HTTPRequestedRangeNotSatisfiable < Net::HTTPClientError # 416
- HAS_BODY = true
-end
-class Net::HTTPExpectationFailed < Net::HTTPClientError # 417
- HAS_BODY = true
-end
-# 418 I'm a teapot - RFC 2324; a joke RFC
-# 420 Enhance Your Calm - Twitter
-class Net::HTTPUnprocessableEntity < Net::HTTPClientError # 422 - RFC 4918
- HAS_BODY = true
-end
-class Net::HTTPLocked < Net::HTTPClientError # 423 - RFC 4918
- HAS_BODY = true
-end
-class Net::HTTPFailedDependency < Net::HTTPClientError # 424 - RFC 4918
- HAS_BODY = true
-end
-# 425 Unordered Collection - existed only in draft
-class Net::HTTPUpgradeRequired < Net::HTTPClientError # 426 - RFC 2817
- HAS_BODY = true
-end
-class Net::HTTPPreconditionRequired < Net::HTTPClientError # 428 - RFC 6585
- HAS_BODY = true
-end
-class Net::HTTPTooManyRequests < Net::HTTPClientError # 429 - RFC 6585
- HAS_BODY = true
-end
-class Net::HTTPRequestHeaderFieldsTooLarge < Net::HTTPClientError # 431 - RFC 6585
- HAS_BODY = true
-end
-# 444 No Response - Nginx
-# 449 Retry With - Microsoft
-# 450 Blocked by Windows Parental Controls - Microsoft
-# 499 Client Closed Request - Nginx
+ # 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
-class Net::HTTPInternalServerError < Net::HTTPServerError # 500
- HAS_BODY = true
-end
-class Net::HTTPNotImplemented < Net::HTTPServerError # 501
- HAS_BODY = true
-end
-class Net::HTTPBadGateway < Net::HTTPServerError # 502
- HAS_BODY = true
-end
-class Net::HTTPServiceUnavailable < Net::HTTPServerError # 503
- HAS_BODY = true
-end
-class Net::HTTPGatewayTimeOut < Net::HTTPServerError # 504
- HAS_BODY = true
-end
-class Net::HTTPVersionNotSupported < Net::HTTPServerError # 505
- HAS_BODY = true
-end
-# 506 Variant Also Negotiates - RFC 2295; experimental
-class Net::HTTPInsufficientStorage < Net::HTTPServerError # 507 - RFC 4918
- HAS_BODY = true
-end
-# 508 Loop Detected - RFC 5842; experimental
-# 509 Bandwidth Limit Exceeded - Apache bw/limited extension
-# 510 Not Extended - RFC 2774; experimental
-class Net::HTTPNetworkAuthenticationRequired < Net::HTTPServerError # 511 - RFC 6585
- HAS_BODY = true
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,
@@ -218,6 +1187,8 @@ class Net::HTTPResponse
'205' => Net::HTTPResetContent,
'206' => Net::HTTPPartialContent,
'207' => Net::HTTPMultiStatus,
+ '208' => Net::HTTPAlreadyReported,
+ '226' => Net::HTTPIMUsed,
'300' => Net::HTTPMultipleChoices,
'301' => Net::HTTPMovedPermanently,
@@ -226,6 +1197,7 @@ class Net::HTTPResponse
'304' => Net::HTTPNotModified,
'305' => Net::HTTPUseProxy,
'307' => Net::HTTPTemporaryRedirect,
+ '308' => Net::HTTPPermanentRedirect,
'400' => Net::HTTPBadRequest,
'401' => Net::HTTPUnauthorized,
@@ -235,16 +1207,17 @@ class Net::HTTPResponse
'405' => Net::HTTPMethodNotAllowed,
'406' => Net::HTTPNotAcceptable,
'407' => Net::HTTPProxyAuthenticationRequired,
- '408' => Net::HTTPRequestTimeOut,
+ '408' => Net::HTTPRequestTimeout,
'409' => Net::HTTPConflict,
'410' => Net::HTTPGone,
'411' => Net::HTTPLengthRequired,
'412' => Net::HTTPPreconditionFailed,
- '413' => Net::HTTPRequestEntityTooLarge,
- '414' => Net::HTTPRequestURITooLong,
+ '413' => Net::HTTPPayloadTooLarge,
+ '414' => Net::HTTPURITooLong,
'415' => Net::HTTPUnsupportedMediaType,
- '416' => Net::HTTPRequestedRangeNotSatisfiable,
+ '416' => Net::HTTPRangeNotSatisfiable,
'417' => Net::HTTPExpectationFailed,
+ '421' => Net::HTTPMisdirectedRequest,
'422' => Net::HTTPUnprocessableEntity,
'423' => Net::HTTPLocked,
'424' => Net::HTTPFailedDependency,
@@ -252,17 +1225,18 @@ class Net::HTTPResponse
'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,
+ '504' => Net::HTTPGatewayTimeout,
'505' => Net::HTTPVersionNotSupported,
+ '506' => Net::HTTPVariantAlsoNegotiates,
'507' => Net::HTTPInsufficientStorage,
+ '508' => Net::HTTPLoopDetected,
+ '510' => Net::HTTPNotExtended,
'511' => Net::HTTPNetworkAuthenticationRequired,
- }
+ }.freeze
end
-
-# :startdoc:
-
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 d36f82002d..0f23e1fb13 100644
--- a/lib/net/https.rb
+++ b/lib/net/https.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
=begin
= net/https -- SSL/TLS enhancement for Net::HTTP.
@@ -13,10 +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'.)
=end
-require 'net/http'
+require_relative 'http'
require 'openssl'
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
deleted file mode 100644
index a816b4e263..0000000000
--- a/lib/net/imap.rb
+++ /dev/null
@@ -1,3760 +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::SSL)
- 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
-
- # Returns the max number of flags interned to symbols.
- def self.max_flag_count
- return @@max_flag_count
- end
-
- # Sets the max number of flags interned to symbols.
- def self.max_flag_count=(count)
- @@max_flag_count = count
- 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
-
- # The default port for IMAP connections, port 143
- def self.default_port
- return PORT
- end
-
- # The default port for IMAPS connections, port 993
- def self.default_tls_port
- return SSL_PORT
- end
-
- class << self
- alias default_imap_port default_port
- alias default_imaps_port default_tls_port
- alias default_ssl_port default_tls_port
- end
-
- # Disconnects from the server.
- def disconnect
- begin
- begin
- # try to call SSL::SSLSocket#io.
- @sock.io.shutdown
- rescue NoMethodError
- # @sock is not an SSL::SSLSocket.
- @sock.shutdown
- end
- rescue Errno::ENOTCONN
- # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
- rescue Exception => e
- @receiver_thread.raise(e)
- end
- @receiver_thread.join
- synchronize do
- unless @sock.closed?
- @sock.close
- end
- end
- raise e if e
- 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(options = {}, verify = true)
- send_command("STARTTLS") do |resp|
- if resp.kind_of?(TaggedResponse) && resp.name == "OK"
- begin
- # for backward compatibility
- certs = options.to_str
- options = create_ssl_params(certs, verify)
- rescue NoMethodError
- end
- start_tls_session(options)
- 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 a XLIST 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 XLIST command is like the LIST command except that the flags
- # returned refer to the function of the folder/mailbox, e.g. :Sent
- #
- # The return value is an array of +Net::IMAP::MailboxList+. For example:
- #
- # imap.create("foo/bar")
- # imap.create("foo/baz")
- # p imap.xlist("", "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 xlist(refname, mailbox)
- synchronize do
- send_command("XLIST", refname, mailbox)
- return @responses.delete("XLIST")
- 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
-
- # Sends an IDLE command that waits for notifications of new or expunged
- # messages. Yields responses from the server during the IDLE.
- #
- # Use #idle_done() to leave IDLE.
- def idle(&response_handler)
- raise LocalJumpError, "no block given" unless response_handler
-
- response = nil
-
- synchronize do
- tag = Thread.current[:net_imap_tag] = generate_tag
- put_string("#{tag} IDLE#{CRLF}")
-
- begin
- add_response_handler(response_handler)
- @idle_done_cond = new_cond
- @idle_done_cond.wait
- @idle_done_cond = nil
- if @receiver_thread_terminating
- raise Net::IMAP::Error, "connection closed"
- end
- ensure
- unless @receiver_thread_terminating
- remove_response_handler(response_handler)
- put_string("DONE#{CRLF}")
- response = get_tagged_response(tag, "IDLE")
- end
- end
- end
-
- return response
- end
-
- # Leaves IDLE.
- def idle_done
- synchronize do
- if @idle_done_cond.nil?
- raise Net::IMAP::Error, "not during IDLE"
- end
- @idle_done_cond.signal
- end
- 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
- ($1.tr(",", "/") + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE)
- else
- "&"
- end
- }
- end
-
- # Encode a string from UTF-8 format to modified UTF-7.
- def self.encode_utf7(s)
- return s.gsub(/(&)|[^\x20-\x7e]+/) {
- if $1
- "&-"
- else
- base64 = [$&.encode(Encoding::UTF_16BE)].pack("m")
- "&" + base64.delete("=\n").tr("/", ",") + "-"
- end
- }.force_encoding("ASCII-8BIT")
- end
-
- # Formats +time+ as an IMAP-style date.
- def self.format_date(time)
- return time.strftime('%d-%b-%Y')
- end
-
- # Formats +time+ as an IMAP-style date-time.
- def self.format_datetime(time)
- return time.strftime('%d-%b-%Y %H:%M %z')
- end
-
- private
-
- CRLF = "\r\n" # :nodoc:
- PORT = 143 # :nodoc:
- SSL_PORT = 993 # :nodoc:
-
- @@debug = false
- @@authenticators = {}
- @@max_flag_count = 10000
-
- # :call-seq:
- # Net::IMAP.new(host, options = {})
- #
- # Creates a new Net::IMAP object and connects it to the specified
- # +host+.
- #
- # +options+ is an option hash, each key of which is a symbol.
- #
- # The available options are:
- #
- # port:: port number (default value is 143 for imap, or 993 for imaps)
- # ssl:: if options[:ssl] 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.
- # if options[:ssl] is a hash, it's passed to
- # OpenSSL::SSL::SSLContext#set_params as parameters.
- #
- # 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_or_options = {},
- usessl = false, certs = nil, verify = true)
- super()
- @host = host
- begin
- options = port_or_options.to_hash
- rescue NoMethodError
- # for backward compatibility
- options = {}
- options[:port] = port_or_options
- if usessl
- options[:ssl] = create_ssl_params(certs, verify)
- end
- end
- @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
- @tag_prefix = "RUBY"
- @tagno = 0
- @parser = ResponseParser.new
- @sock = TCPSocket.open(@host, @port)
- if options[:ssl]
- start_tls_session(options[:ssl])
- @usessl = true
- else
- @usessl = false
- end
- @responses = Hash.new([].freeze)
- @tagged_responses = {}
- @response_handlers = []
- @tagged_response_arrival = new_cond
- @continuation_request_arrival = new_cond
- @idle_done_cond = nil
- @logout_command_tag = nil
- @debug_output_bol = true
- @exception = nil
-
- @greeting = get_response
- if @greeting.nil?
- @sock.close
- raise Error, "connection closed"
- end
- if @greeting.name == "BYE"
- @sock.close
- raise ByeResponseError, @greeting
- end
-
- @client_thread = Thread.current
- @receiver_thread = Thread.start {
- begin
- receive_responses
- rescue Exception
- end
- }
- @receiver_thread_terminating = false
- end
-
- def receive_responses
- connection_closed = false
- until connection_closed
- synchronize do
- @exception = nil
- end
- begin
- resp = get_response
- rescue Exception => e
- synchronize do
- @sock.close
- @exception = e
- end
- break
- end
- unless resp
- synchronize do
- @exception = EOFError.new("end of file reached")
- end
- break
- end
- 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
- @exception = ByeResponseError.new(resp)
- connection_closed = true
- end
- when ContinuationRequest
- @continuation_request_arrival.signal
- end
- @response_handlers.each do |handler|
- handler.call(resp)
- end
- end
- rescue Exception => e
- @exception = e
- synchronize do
- @tagged_response_arrival.broadcast
- @continuation_request_arrival.broadcast
- end
- end
- end
- synchronize do
- @receiver_thread_terminating = true
- @tagged_response_arrival.broadcast
- @continuation_request_arrival.broadcast
- if @idle_done_cond
- @idle_done_cond.signal
- end
- end
- end
-
- def get_tagged_response(tag, cmd)
- until @tagged_responses.key?(tag)
- raise @exception if @exception
- @tagged_response_arrival.wait
- end
- resp = @tagged_responses.delete(tag)
- case resp.name
- when /\A(?:NO)\z/ni
- raise NoResponseError, resp
- when /\A(?:BAD)\z/ni
- raise BadResponseError, resp
- 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
- args.each do |i|
- validate_data(i)
- end
- 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 validate_data(data)
- case data
- when nil
- when String
- when Integer
- if data < 0 || data >= 4294967296
- raise DataFormatError, num.to_s
- end
- when Array
- data.each do |i|
- validate_data(i)
- end
- when Time
- when Symbol
- else
- data.validate
- 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.bytesize.to_s + "}" + CRLF)
- @continuation_request_arrival.wait
- raise @exception if @exception
- put_string(str)
- end
-
- def send_number_data(num)
- 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)
- case attr
- when String then
- attr = RawData.new(attr)
- when Array then
- attr = attr.map { |arg|
- arg.is_a?(String) ? RawData.new(arg) : arg
- }
- 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
-
- def create_ssl_params(certs = nil, verify = true)
- params = {}
- if certs
- if File.file?(certs)
- params[:ca_file] = certs
- elsif File.directory?(certs)
- params[:ca_path] = certs
- end
- end
- if verify
- params[:verify_mode] = VERIFY_PEER
- else
- params[:verify_mode] = VERIFY_NONE
- end
- return params
- end
-
- def start_tls_session(params = {})
- unless defined?(OpenSSL::SSL)
- raise "SSL extension not installed"
- end
- if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
- raise RuntimeError, "already using SSL"
- end
- begin
- params = params.to_hash
- rescue NoMethodError
- params = {}
- end
- context = SSLContext.new
- context.set_params(params)
- if defined?(VerifyCallbackProc)
- context.verify_callback = VerifyCallbackProc
- end
- @sock = SSLSocket.new(@sock, context)
- @sock.sync_close = true
- @sock.connect
- if context.verify_mode != VERIFY_NONE
- @sock.post_connection_check(@host)
- end
- end
-
- class RawData # :nodoc:
- def send_data(imap)
- imap.send(:put_string, @data)
- end
-
- def validate
- end
-
- private
-
- def initialize(data)
- @data = data
- end
- end
-
- class Atom # :nodoc:
- def send_data(imap)
- imap.send(:put_string, @data)
- end
-
- def validate
- end
-
- private
-
- def initialize(data)
- @data = data
- end
- end
-
- class QuotedString # :nodoc:
- def send_data(imap)
- imap.send(:send_quoted_string, @data)
- end
-
- def validate
- end
-
- private
-
- def initialize(data)
- @data = data
- end
- end
-
- class Literal # :nodoc:
- def send_data(imap)
- imap.send(:send_literal, @data)
- end
-
- def validate
- end
-
- private
-
- def initialize(data)
- @data = data
- end
- end
-
- class MessageSet # :nodoc:
- def send_data(imap)
- imap.send(:put_string, format_internal(@data))
- end
-
- def validate
- validate_internal(@data)
- end
-
- private
-
- def initialize(data)
- @data = data
- end
-
- def format_internal(data)
- case data
- when "*"
- return data
- when Integer
- 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(",")}
- end
- end
-
- def validate_internal(data)
- case data
- when "*"
- when Integer
- ensure_nz_number(data)
- when Range
- when Array
- data.each do |i|
- validate_internal(i)
- end
- when ThreadMember
- data.children.each do |i|
- validate_internal(i)
- end
- 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, :mailbox)
-
- # 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::BodyTypeAttachment represents attachment body structures
- # of messages.
- #
- # ==== Fields:
- #
- # media_type:: Returns the content media type name.
- #
- # subtype:: Returns +nil+.
- #
- # param:: Returns a hash that represents parameters.
- #
- # multipart?:: Returns false.
- #
- class BodyTypeAttachment < Struct.new(:media_type, :subtype,
- :param)
- def multipart?
- return false
- 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 BodyTypeExtension < Struct.new(:media_type, :subtype,
- :params, :content_id,
- :description, :encoding, :size)
- def multipart?
- return false
- end
- end
-
- class ResponseParser # :nodoc:
- def initialize
- @str = nil
- @pos = nil
- @lex_state = nil
- @token = nil
- @flag_symbols = {}
- end
-
- 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|XLIST)\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(n))
- return UntaggedResponse.new(name, data, @str)
- end
- end
-
- def msg_att(n)
- match(T_LPAR)
- attr = {}
- while true
- token = lookahead
- case token.symbol
- when T_RPAR
- shift_token
- break
- when T_SPACE
- shift_token
- next
- 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' for {%d}", token.value, n)
- 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
- token = lookahead
- if token.symbol == T_LBRA
- shift_token
- match(T_RBRA)
- end
- 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
- when /\A(?:ATTACHMENT)\z/ni
- return body_type_attachment
- 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
-
- token = lookahead
- if token.symbol == T_RPAR
- # If this is not message/rfc822, we shouldn't apply the RFC822
- # spec to it. We should handle anything other than
- # message/rfc822 using multipart extension data [rfc3501] (i.e.
- # the data itself won't be returned, we would have to retrieve it
- # with BODYSTRUCTURE instead of with BODY
-
- # Also, sometimes a message/rfc822 is included as a large
- # attachment instead of having all of the other details
- # (e.g. attaching a .eml file to an email)
- if msubtype == "RFC822"
- return BodyTypeMessage.new(mtype, msubtype, param, content_id,
- desc, enc, size, nil, nil, nil, nil,
- nil, nil, nil)
- else
- return BodyTypeExtension.new(mtype, msubtype,
- param, content_id,
- desc, enc, size)
- end
- end
-
- 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_attachment
- mtype = case_insensitive_string
- match(T_SPACE)
- param = body_fld_param
- return BodyTypeAttachment.new(mtype, nil, param)
- 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
- token = lookahead
- if token.symbol != T_SPACE
- return mtype, nil
- end
- 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.bytesize.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
- data.push(MailboxACLItem.new(user, rights, mailbox))
- 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
- else
- data.push(number)
- end
- 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
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- @lex_state = EXPR_CTEXT
- token = match(T_TEXT)
- @lex_state = EXPR_BEG
- result = ResponseCode.new(name, token.value)
- else
- result = ResponseCode.new(name, nil)
- end
- 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|
- if atom
- atom
- else
- symbol = flag.capitalize.untaint.intern
- @flag_symbols[symbol] = true
- if @flag_symbols.length > IMAP.max_flag_count
- raise FlagCountError, "number of flag symbols exceeded"
- end
- symbol
- end
- }
- 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] DATA_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("invalid @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
- $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] = (k_ipad[i].ord ^ 0x36).chr
- k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
- 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 {|key| qdval(key.to_s, response[key]) }.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 responses need 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
-
- # The response that caused this error
- attr_accessor :response
-
- def initialize(response)
- @response = response
-
- super @response.data.text
- end
-
- 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
-
- # Error raised when too many flags are interned to symbols.
- class FlagCountError < Error
- end
- end
-end
-
-if __FILE__ == $0
- # :enddoc:
- require "getoptlong"
-
- $stdout.sync = true
- $port = nil
- $user = ENV["USER"] || ENV["LOGNAME"]
- $auth = "login"
- $ssl = false
- $starttls = false
-
- def usage
- <<EOF
-usage: #{$0} [options] <host>
-
- --help print this message
- --port=PORT specifies port
- --user=USER specifies user
- --auth=AUTH specifies auth type
- --starttls use starttls
- --ssl use ssl
-EOF
- end
-
- begin
- require 'io/console'
- rescue LoadError
- def _noecho(&block)
- system("stty", "-echo")
- begin
- yield STDIN
- ensure
- system("stty", "echo")
- end
- end
- else
- def _noecho(&block)
- STDIN.noecho(&block)
- end
- end
-
- def get_password
- print "password: "
- begin
- return _noecho(&:gets).chomp
- ensure
- puts
- 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],
- ['--starttls', GetoptLong::NO_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 "--starttls"
- $starttls = true
- when "--debug"
- Net::IMAP.debug = true
- when "--help"
- usage
- exit
- end
- end
- rescue
- abort usage
- end
-
- $host = ARGV.shift
- unless $host
- abort usage
- end
-
- imap = Net::IMAP.new($host, :port => $port, :ssl => $ssl)
- begin
- imap.starttls if $starttls
- class << password = method(:get_password)
- alias to_str call
- end
- 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 74a53951c7..0000000000
--- a/lib/net/pop.rb
+++ /dev/null
@@ -1,1021 +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/html/net_pop.html
-#
-# $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
-
- #
- # == 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
-
- # svn revision of this library
- Revision = %q$Revision$.split[1]
-
- #
- # Class Parameters
- #
-
- # returns the port for POP3
- 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
- #
-
- @ssl_params = nil
-
- # :call-seq:
- # Net::POP.enable_ssl(params = {})
- #
- # Enable SSL for all new instances.
- # +params+ is passed to OpenSSL::SSLContext#set_params.
- def POP3.enable_ssl(*args)
- @ssl_params = create_ssl_params(*args)
- end
-
- # Constructs proper parameters from arguments
- def POP3.create_ssl_params(verify_or_params = {}, certs = nil)
- begin
- params = verify_or_params.to_hash
- rescue NoMethodError
- params = {}
- params[:verify_mode] = verify_or_params
- if certs
- if File.file?(certs)
- params[:ca_file] = certs
- elsif File.directory?(certs)
- params[:ca_path] = certs
- end
- end
- end
- return params
- end
-
- # Disable SSL for all new instances.
- def POP3.disable_ssl
- @ssl_params = nil
- end
-
- # returns the SSL Parameters
- #
- # see also POP3.enable_ssl
- def POP3.ssl_params
- return @ssl_params
- end
-
- # returns +true+ if POP3.ssl_params is set
- def POP3.use_ssl?
- return !@ssl_params.nil?
- end
-
- # returns whether verify_mode is enable from POP3.ssl_params
- def POP3.verify
- return @ssl_params[:verify_mode]
- end
-
- # returns the :ca_file or :ca_path from POP3.ssl_params
- def POP3.certs
- return @ssl_params[:ca_file] || @ssl_params[:ca_path]
- 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
- @ssl_params = POP3.ssl_params
- @port = port
- @apop = isapop
-
- @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?
- return !@ssl_params.nil?
- end
-
- # :call-seq:
- # Net::POP#enable_ssl(params = {})
- #
- # Enables SSL for this instance. Must be called before the connection is
- # established to have any effect.
- # +params[:port]+ is port to establish the SSL connection on; Defaults to 995.
- # +params+ (except :port) is passed to OpenSSL::SSLContext#set_params.
- def enable_ssl(verify_or_params = {}, certs = nil, port = nil)
- begin
- @ssl_params = verify_or_params.to_hash.dup
- @port = @ssl_params.delete(:port) || @port
- rescue NoMethodError
- @ssl_params = POP3.create_ssl_params(verify_or_params, certs)
- @port = port || @port
- end
- end
-
- # Disable SSL for all new instances.
- def disable_ssl
- @ssl_params = 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.
- def port
- return @port || (use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port)
- end
-
- # Seconds to wait until a connection is opened.
- # If the POP3 object cannot open a connection within this time,
- # it raises a Net::OpenTimeout exception. The default value is 30 seconds.
- 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 Net::ReadTimeout exception. The default value is 60 seconds.
- 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
-
- # internal method for Net::POP3.start
- def do_start(account, password) # :nodoc:
- s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do
- TCPSocket.open(@address, port)
- end
- if use_ssl?
- raise 'openssl library not installed' unless defined?(OpenSSL)
- context = OpenSSL::SSL::SSLContext.new
- context.set_params(@ssl_params)
- s = OpenSSL::SSL::SSLSocket.new(s, context)
- s.sync_close = true
- s.connect
- if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
- s.post_connection_check(@address)
- end
- 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
-
- # Does nothing
- def on_connect # :nodoc:
- 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
-
- # nil's out the:
- # - mails
- # - number counter for mails
- # - number counter for bytes
- # - quits the current command, if any
- def do_finish # :nodoc:
- @mails = nil
- @n_mails = nil
- @n_bytes = nil
- @command.quit if @command
- ensure
- @started = false
- @command = nil
- @socket.close if @socket and not @socket.closed?
- @socket = nil
- end
- private :do_finish
-
- # Returns the current command.
- #
- # Raises IOError if there is no active socket
- def command # :nodoc:
- 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)
- uidl = command().uidl
- @mails.each {|m| m.uid = uidl[m.number] }
- end
-
- # deguging output for +msg+
- def logging(msg)
- @debug_output << msg + "\n" if @debug_output
- end
-
- end # class POP3
-
- # class aliases
- POP = POP3 # :nodoc:
- POPSession = POP3 # :nodoc:
- POP3Session = POP3 # :nodoc:
-
- #
- # 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
-
- attr_reader :socket
-
- 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 14a68e1115..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,6 +77,7 @@ 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
@@ -55,20 +89,58 @@ module Net # :nodoc:
# 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; end
+ 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
- @continue_timeout = nil
- @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
@@ -94,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
@@ -112,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
@@ -129,14 +206,16 @@ 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
@@ -149,27 +228,64 @@ module Net # :nodoc:
BUFSIZE = 1024 * 16
def rbuf_fill
- begin
- @rbuf << @io.read_nonblock(BUFSIZE)
- rescue IO::WaitReadable
- if IO.select([@io], nil, nil, @read_timeout)
- retry
+ 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
- raise Net::ReadTimeout
+ @rbuf << rv
+ rv.clear
end
- rescue IO::WaitWritable
+ 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
- if IO.select(nil, [@io], nil, @read_timeout)
- retry
- else
- raise Net::ReadTimeout
- end
+ (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_consume(len)
- s = @rbuf.slice!(0, len)
+ def rbuf_size
+ @rbuf.bytesize - @rbuf_offset
+ end
+
+ def rbuf_consume_all
+ rbuf_consume if rbuf_size > 0
+ end
+
+ 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
@@ -180,9 +296,9 @@ module Net # :nodoc:
public
- def write(str)
+ def write(*strs)
writing {
- write0 str
+ write0(*strs)
}
end
@@ -206,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
#
@@ -236,7 +375,7 @@ module Net # :nodoc:
class InternetMessageIO < BufferedIO #:nodoc: internal use only
- def initialize(io)
+ def initialize(*, **)
super
@wbuf = nil
end
@@ -251,7 +390,7 @@ 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)"
@@ -267,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
@@ -295,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
@@ -308,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
@@ -355,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 01f8b2b1f0..0000000000
--- a/lib/net/smtp.rb
+++ /dev/null
@@ -1,1065 +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/html/net_smtp.html
-#
-# $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
-
- #
- # == 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_address@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
-
- #
- # 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).
- #
- attr_accessor :esmtp
-
- # +true+ if the SMTP object uses ESMTP (which it does by default).
- 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
- # Net::OpenTimeout is raised. The default value is 30 seconds.
- 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
- # Net::ReadTimeout is raised. The default value is 60 seconds.
- 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'.
- #
- # 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
- # * Net::OpenTimeout
- # * Net::ReadTimeout
- # * IOError
- #
- def SMTP.start(address, port = nil, helo = 'localhost',
- 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
- # * Net::OpenTimeout
- # * Net::ReadTimeout
- # * IOError
- #
- def start(helo = 'localhost',
- 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 tcp_socket(address, port)
- TCPSocket.open address, port
- end
-
- 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.timeout(@open_timeout, Net::OpenTimeout) do
- tcp_socket(@address, @port)
- end
- 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 ssl_socket(socket, context)
- OpenSSL::SSL::SSLSocket.new socket, context
- end
-
- def tlsconnect(s)
- verified = false
- s = ssl_socket(s, @ssl_context)
- logging "TLS connection started"
- s.sync_close = true
- s.connect
- if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
- s.post_connection_check(@address)
- end
- verified = true
- s
- ensure
- s.close unless verified
- 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
- # * Net::ReadTimeout
- # * IOError
- #
- 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
- # * Net::ReadTimeout
- # * IOError
- #
- 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
- send 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, authtype = DEFAULT_AUTH_TYPE)
- 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
-
- # Aborts the current mail transaction
-
- def rset
- getok('RSET')
- end
-
- 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?
- ok_users = []
- unknown_users = []
- to_addrs.flatten.each do |addr|
- begin
- rcptto addr
- rescue SMTPAuthenticationError
- unknown_users << addr.dump
- else
- ok_users << addr
- end
- end
- raise ArgumentError, 'mail destination not given' if ok_users.empty?
- ret = yield
- unless unknown_users.empty?
- raise SMTPAuthenticationError, "failed to deliver for #{unknown_users.join(', ')}"
- end
- ret
- end
-
- def rcptto(to_addr)
- if $SAFE > 0
- raise SecurityError, 'tainted to_addr' if to_addr.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
- 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}: #{res.string})"
- 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
-
- # This class represents a response received by the SMTP server. Instances
- # of this class are created by the SMTP class; they should not be directly
- # created by the user. For more information on SMTP responses, view
- # {Section 4.2 of RFC 5321}[http://tools.ietf.org/html/rfc5321#section-4.2]
- class Response
- # Parses the received response and separates the reply code and the human
- # readable reply text
- def self.parse(str)
- new(str[0,3], str)
- end
-
- # Creates a new instance of the Response class and sets the status and
- # string attributes
- def initialize(status, string)
- @status = status
- @string = string
- end
-
- # The three digit reply code of the SMTP response
- attr_reader :status
-
- # The human readable reply text of the SMTP response
- attr_reader :string
-
- # Takes the first digit of the reply code to determine the status type
- def status_type_char
- @status[0, 1]
- end
-
- # Determines whether the response received was a Positive Completion
- # reply (2xx reply code)
- def success?
- status_type_char() == '2'
- end
-
- # Determines whether the response received was a Positive Intermediate
- # reply (3xx reply code)
- def continue?
- status_type_char() == '3'
- end
-
- # The first line of the human readable reply text
- def message
- @string.lines.first
- end
-
- # Creates a CRAM-MD5 challenge. You can view more information on CRAM-MD5
- # on Wikipedia: http://en.wikipedia.org/wiki/CRAM-MD5
- def cram_md5_challenge
- @string.split(/ /)[1].unpack('m')[0]
- end
-
- # Returns a hash of the human readable reply text in the response if it
- # is multiple lines. It does not return the first line. The key of the
- # hash is the first word the value of the hash is an array with each word
- # thereafter being a value in the array
- def capabilities
- return {} unless @string[3, 1] == '-'
- h = {}
- @string.lines.drop(1).each do |line|
- k, *v = line[4..-1].chomp.split
- h[k] = v
- end
- h
- end
-
- # Determines whether there was an error and raies the appropriate error
- # based on the reply code of the response
- 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 # :nodoc:
-
-end
diff --git a/lib/net/telnet.rb b/lib/net/telnet.rb
deleted file mode 100644
index 3c5d6e8e73..0000000000
--- a/lib/net/telnet.rb
+++ /dev/null
@@ -1,763 +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 "net/protocol"
-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
-
- # :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),
- # which raises a Net::OpenTimeout, and all attempts to read data
- # from the host, which raises a Net::ReadTimeout (in #waitfor(),
- # #cmd(), and #login()). 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 hang 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.timeout(@options["Timeout"], Net::OpenTimeout) do
- @sock = TCPSocket.open(@options["Host"], @options["Port"])
- end
- end
- rescue Net::OpenTimeout
- raise Net::OpenTimeout, "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
-
- 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 Timeout::Error. 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.
- # FailEOF:: if true, when the remote end closes the connection then an
- # EOFError will be raised. Otherwise, defaults to the old
- # behaviour that the function will return whatever data
- # has been received already, or nil if nothing was received.
- #
- def waitfor(options) # :yield: recvdata
- time_out = @options["Timeout"]
- waittime = @options["Waittime"]
- fail_eof = @options["FailEOF"]
-
- 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")
- fail_eof = options["FailEOF"] if options.has_key?("FailEOF")
- 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 Net::ReadTimeout, "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) || 0) <
- Integer(c.rindex(/#{IAC}#{SB}/no) || 0)
- 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) ||
- c.rindex(/\r\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 = rest + c
- rest = ''
- unless @options["Binmode"]
- if pt = buf.rindex(/\r\z/no)
- buf = buf[0 ... pt]
- rest = buf[pt .. -1]
- end
- buf.gsub!(/#{EOL}/no, "\n")
- end
- end
- @log.print(buf) if @options.has_key?("Output_log")
- line += buf
- yield buf if block_given?
- rescue EOFError # End of file reached
- raise if fail_eof
- 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"]
- fail_eof = @options["FailEOF"]
-
- 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")
- fail_eof = options["FailEOF"] if options.has_key?("FailEOF")
- else
- string = options
- end
-
- self.puts(string)
- if block_given?
- waitfor({"Prompt" => match, "Timeout" => time_out, "FailEOF" => fail_eof}){|c| yield c }
- else
- waitfor({"Prompt" => match, "Timeout" => time_out, "FailEOF" => fail_eof})
- 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 = /[Pp]ass(?:word|phrase)[: ]*\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
-
- # Closes the connection
- def close
- @sock.close
- end
-
- end # class Telnet
-end # module Net
-