diff options
Diffstat (limited to 'lib/net')
| -rw-r--r-- | lib/net/ftp.rb | 1128 | ||||
| -rw-r--r-- | lib/net/http.rb | 486 | ||||
| -rw-r--r-- | lib/net/http/backward.rb | 39 | ||||
| -rw-r--r-- | lib/net/http/exceptions.rb | 53 | ||||
| -rw-r--r-- | lib/net/http/generic_request.rb | 31 | ||||
| -rw-r--r-- | lib/net/http/header.rb | 171 | ||||
| -rw-r--r-- | lib/net/http/net-http.gemspec | 34 | ||||
| -rw-r--r-- | lib/net/http/proxy_delta.rb | 1 | ||||
| -rw-r--r-- | lib/net/http/request.rb | 1 | ||||
| -rw-r--r-- | lib/net/http/requests.rb | 1 | ||||
| -rw-r--r-- | lib/net/http/response.rb | 212 | ||||
| -rw-r--r-- | lib/net/http/responses.rb | 449 | ||||
| -rw-r--r-- | lib/net/http/status.rb | 83 | ||||
| -rw-r--r-- | lib/net/https.rb | 5 | ||||
| -rw-r--r-- | lib/net/imap.rb | 3622 | ||||
| -rw-r--r-- | lib/net/net-protocol.gemspec | 36 | ||||
| -rw-r--r-- | lib/net/pop.rb | 1021 | ||||
| -rw-r--r-- | lib/net/protocol.rb | 173 | ||||
| -rw-r--r-- | lib/net/smtp.rb | 1073 | ||||
| -rw-r--r-- | lib/net/telnet.rb | 763 |
20 files changed, 1269 insertions, 8113 deletions
diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb deleted file mode 100644 index c9b80c6804..0000000000 --- a/lib/net/ftp.rb +++ /dev/null @@ -1,1128 +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 Timeout::Error 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 - - # Constructs 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 - - # - # Set the socket used to connect to the FTP server. - # - # May raise FTPReplyError if +get_greeting+ is false. - 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 - if /[\r\n]/ =~ line - raise ArgumentError, "A line must not contain CR or LF" - 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 - - # Receives 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 - - # Receives 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 - def makeport # :nodoc: - TCPServer.open(@sock.addr[3], 0) - 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 - begin - sendport(sock.addr[3], sock.addr[1]) - 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 - ensure - sock.close - end - 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+, "anonymous@" is used as a password. 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 - - # - # Returns the raw last modification time of the (remote) file in the format - # "YYYYMMDDhhmmss" (MDTM command). - # - # Use +mtime+ if you want a parsed Time instance. - # - 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 closed? - true - 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 - line = readuntil("\n", true) - return line.empty? ? nil : line - end - - def readline - line = gets - if line.nil? - raise EOFError, "end of file reached" - end - return line - end - end - # :startdoc: - end -end - - -# Documentation comments: -# - sourced from pickaxe and nutshell, with improvements (hopefully) diff --git a/lib/net/http.rb b/lib/net/http.rb index bceee1c069..d0582f8867 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false # # = net/http.rb # @@ -21,9 +22,10 @@ require 'net/protocol' require 'uri' +require 'resolv' +autoload :OpenSSL, 'openssl' module Net #:nodoc: - autoload :OpenSSL, 'openssl' # :stopdoc: class HTTPBadResponse < StandardError; end @@ -34,7 +36,7 @@ module Net #:nodoc: # # 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) + # [RFC2616](http://www.ietf.org/rfc/rfc2616.txt). # # 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 @@ -86,7 +88,7 @@ module Net #:nodoc: # # == How to use Net::HTTP # - # The following example code can be used as the basis of a HTTP user-agent + # The following example code can be used as the basis of an HTTP user-agent # which can perform a variety of request types using persistent # connections. # @@ -103,14 +105,13 @@ module Net #:nodoc: # open for multiple requests in the block if the server indicates it # supports persistent connections. # + # If you wish to re-use a connection across multiple HTTP requests without + # automatically closing it you can use ::new and then call #start and + # #finish manually. + # # The request types Net::HTTP supports are listed below in the section "HTTP # Request Classes". # - # 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. - # # 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. @@ -132,7 +133,7 @@ module Net #:nodoc: # puts res.class.name # => 'HTTPOK' # # # Body - # puts res.body if res.response_body_permitted? + # puts res.body # # === Following Redirection # @@ -169,7 +170,7 @@ module Net #:nodoc: # === POST # # A POST can be made using the Net::HTTP::Post request class. This example - # creates a urlencoded POST body: + # creates a URL encoded POST body: # # uri = URI('http://www.example.com/todo.cgi') # req = Net::HTTP::Post.new(uri) @@ -186,13 +187,10 @@ module Net #:nodoc: # res.value # end # - # 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=: + # To send multipart/form-data use Net::HTTPHeader#set_form: # # req = Net::HTTP::Post.new(uri) - # req.body = multipart_data - # req.content_type = 'multipart/form-data' + # req.set_form([['upload', File.open('foo.bar')]], 'multipart/form-data') # # 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). @@ -221,7 +219,7 @@ module Net #:nodoc: # === Basic Authentication # # Basic authentication is performed according to - # [RFC2617](http://www.ietf.org/rfc/rfc2617.txt) + # [RFC2617](http://www.ietf.org/rfc/rfc2617.txt). # # uri = URI('http://example.com/index.html?key=value') # @@ -259,15 +257,20 @@ module Net #:nodoc: # # uri = URI('https://secure.example.com/some_path?query=string') # - # Net::HTTP.start(uri.host, uri.port, - # :use_ssl => uri.scheme == 'https') do |http| + # Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http| # request = Net::HTTP::Get.new uri - # # response = http.request request # Net::HTTPResponse object # end # + # Or if you simply want to make a GET request, you may pass in an URI + # object that has an HTTPS URL. Net::HTTP automatically turns on TLS + # verification if the URI object has a 'https' URI scheme. + # + # uri = URI('https://example.com/') + # Net::HTTP.get(uri) # => String + # # In previous versions of Ruby you would need to require 'net/https' to use - # HTTPS. This is no longer true. + # HTTPS. This is no longer true. # # === Proxies # @@ -325,6 +328,8 @@ module Net #:nodoc: # HTTPInformation:: 1xx # HTTPContinue:: 100 # HTTPSwitchProtocol:: 101 + # HTTPProcessing:: 102 + # HTTPEarlyHints:: 103 # HTTPSuccess:: 2xx # HTTPOK:: 200 # HTTPCreated:: 201 @@ -334,6 +339,7 @@ module Net #:nodoc: # HTTPResetContent:: 205 # HTTPPartialContent:: 206 # HTTPMultiStatus:: 207 + # HTTPAlreadyReported:: 208 # HTTPIMUsed:: 226 # HTTPRedirection:: 3xx # HTTPMultipleChoices:: 300 @@ -343,6 +349,7 @@ module Net #:nodoc: # HTTPNotModified:: 304 # HTTPUseProxy:: 305 # HTTPTemporaryRedirect:: 307 + # HTTPPermanentRedirect:: 308 # HTTPClientError:: 4xx # HTTPBadRequest:: 400 # HTTPUnauthorized:: 401 @@ -362,6 +369,7 @@ module Net #:nodoc: # HTTPUnsupportedMediaType:: 415 # HTTPRequestedRangeNotSatisfiable:: 416 # HTTPExpectationFailed:: 417 + # HTTPMisdirectedRequest:: 421 # HTTPUnprocessableEntity:: 422 # HTTPLocked:: 423 # HTTPFailedDependency:: 424 @@ -369,6 +377,7 @@ module Net #:nodoc: # HTTPPreconditionRequired:: 428 # HTTPTooManyRequests:: 429 # HTTPRequestHeaderFieldsTooLarge:: 431 + # HTTPUnavailableForLegalReasons:: 451 # HTTPServerError:: 5xx # HTTPInternalServerError:: 500 # HTTPNotImplemented:: 501 @@ -376,7 +385,10 @@ module Net #:nodoc: # HTTPServiceUnavailable:: 503 # HTTPGatewayTimeOut:: 504 # HTTPVersionNotSupported:: 505 + # HTTPVariantAlsoNegotiates:: 506 # HTTPInsufficientStorage:: 507 + # HTTPLoopDetected:: 508 + # HTTPNotExtended:: 510 # HTTPNetworkAuthenticationRequired:: 511 # # There is also the Net::HTTPBadResponse exception which is raised when @@ -385,11 +397,11 @@ module Net #:nodoc: class HTTP < Protocol # :stopdoc: + VERSION = "0.3.0.1" Revision = %q$Revision$.split[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 @@ -424,7 +436,7 @@ module Net #:nodoc: # # 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: + # (+uri+, +headers+), or as (+host+, +path+, +port+ = 80); so: # # Net::HTTP.get_print URI('http://www.example.com/index.html') # @@ -432,8 +444,12 @@ module Net #:nodoc: # # 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| + # you can also specify request headers: + # + # Net::HTTP.get_print URI('http://www.example.com/index.html'), { 'Accept' => 'text/html' } + # + 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 @@ -443,7 +459,7 @@ module Net #:nodoc: # 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: + # (+uri+, +headers+), or as (+host+, +path+, +port+ = 80); so: # # print Net::HTTP.get(URI('http://www.example.com/index.html')) # @@ -451,13 +467,17 @@ module Net #:nodoc: # # print Net::HTTP.get('www.example.com', '/index.html') # - def HTTP.get(uri_or_host, path = nil, port = nil) - get_response(uri_or_host, path, port).body + # you can also specify request headers: + # + # Net::HTTP.get(URI('http://www.example.com/index.html'), { 'Accept' => 'text/html' }) + # + def HTTP.get(uri_or_host, path_or_headers = nil, port = nil) + get_response(uri_or_host, path_or_headers, 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: + # (+uri+, +headers+), or as (+host+, +path+, +port+ = 80); so: # # res = Net::HTTP.get_response(URI('http://www.example.com/index.html')) # print res.body @@ -467,35 +487,58 @@ module Net #:nodoc: # res = Net::HTTP.get_response('www.example.com', '/index.html') # print res.body # - def HTTP.get_response(uri_or_host, path = nil, port = nil, &block) - if path + # you can also specify request headers: + # + # Net::HTTP.get_response(URI('http://www.example.com/index.html'), { 'Accept' => 'text/html' }) + # + 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 data to the specified URI object. + # + # Example: + # + # require 'net/http' + # require 'uri' + # + # Net::HTTP.post URI('http://www.example.com/api/search'), + # { "q" => "ruby", "max" => "50" }.to_json, + # "Content-Type" => "application/json" + # + 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 HTML form data to the specified URI object. # The form data must be provided as a Hash mapping from String to String. # Example: # # { "cmd" => "search", "q" => "ruby", "max" => "50" } # - # This method also does Basic Authentication iff +url+.user exists. + # This method also does Basic Authentication if and only if +url+.user exists. # But userinfo for authentication is deprecated (RFC3986). # So this feature will be removed. # # Example: # # require 'net/http' - # require 'uri' # # Net::HTTP.post_form URI('http://www.example.com/search.cgi'), # { "q" => "ruby", "max" => "50" } @@ -535,7 +578,7 @@ module Net #:nodoc: # :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) + # HTTP.start(address, port=nil, p_addr=:ENV, 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. @@ -550,8 +593,8 @@ module Net #:nodoc: # _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, + # The keys are ipaddr, ca_file, ca_path, cert, cert_store, ciphers, keep_alive_timeout, + # close_on_empty_response, key, open_timeout, read_timeout, write_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. @@ -566,8 +609,10 @@ module Net #:nodoc: 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] @@ -601,12 +646,13 @@ module Net #:nodoc: # detection from the environment. To disable proxy detection set +p_addr+ # to nil. # - # 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. + # If you are connecting to a custom proxy, +p_addr+ specifies the DNS name + # or IP address of the proxy host, +p_port+ the port to use to access the + # proxy, +p_user+ and +p_pass+ the username and password if authorization + # is required to use the proxy, and p_no_proxy hosts which do not + # use the proxy. # - 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) http = super address, port if proxy_class? then # from Net::HTTP::Proxy() @@ -618,6 +664,10 @@ module Net #:nodoc: elsif p_addr == :ENV then http.proxy_from_env = true else + if p_addr && p_no_proxy && !URI::Generic.use_proxy?(p_addr, p_addr, p_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 @@ -633,6 +683,7 @@ module Net #:nodoc: def initialize(address, port = nil) @address = address @port = (port || HTTP.default_port) + @ipaddr = nil @local_host = nil @local_port = nil @curr_http_version = HTTPVersion @@ -641,10 +692,14 @@ module Net #:nodoc: @close_on_empty_response = false @socket = nil @started = false - @open_timeout = nil + @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 @proxy_from_env = false @proxy_uri = nil @@ -656,7 +711,6 @@ module Net #:nodoc: @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 @@ -677,7 +731,7 @@ module Net #:nodoc: # http.start { .... } # 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 @@ -693,16 +747,39 @@ module Net #:nodoc: # The local port used to establish the connection. attr_accessor :local_port + # The encoding to use for the response body. If Encoding, uses the + # specified encoding. If other true value, tries to detect the response + # body encoding. + attr_reader :response_body_encoding + + # Set the encoding to use for the response body. If given a String, find + # the related Encoding. + def response_body_encoding=(value) + value = Encoding.find(value) if value.is_a?(String) + @response_body_encoding = value + end + attr_writer :proxy_from_env attr_writer :proxy_address attr_writer :proxy_port attr_writer :proxy_user attr_writer :proxy_pass + # The IP address to connect to/used to connect to + def ipaddr + started? ? @socket.io.peeraddr[3] : @ipaddr + end + + # Set the IP address to connect to + def ipaddr=(addr) + raise IOError, "ipaddr value changed, but session already started" if started? + @ipaddr = addr + end + # 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+. + # Net::OpenTimeout exception. The default value is 60 seconds. attr_accessor :open_timeout # Number of seconds to wait for one block to be read (via one read(2) @@ -711,12 +788,41 @@ module Net #:nodoc: # it raises a Net::ReadTimeout exception. The default value is 60 seconds. attr_reader :read_timeout + # Number of seconds to wait for one block to be written (via one write(2) + # call). Any number may be used, including Floats for fractional + # seconds. If the HTTP object cannot write data in this many seconds, + # it raises a Net::WriteTimeout exception. The default value is 60 seconds. + # Net::WriteTimeout is not raised on Windows. + attr_reader :write_timeout + + # 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. + # Should be a non-negative integer number. Zero means no retries. + # The default value is 1. + 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 + + attr_reader :max_retries + # Setter for the read_timeout attribute. def read_timeout=(sec) @socket.read_timeout = sec if @socket @read_timeout = sec end + # Setter for the write_timeout attribute. + def write_timeout=(sec) + @socket.write_timeout = sec if @socket + @write_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+. @@ -734,6 +840,10 @@ module Net #:nodoc: # The default value is 2 seconds. attr_accessor :keep_alive_timeout + # Whether to ignore EOF when reading response bodies with defined + # Content-Length headers. For backwards compatibility, the default is true. + attr_accessor :ignore_eof + # Returns true if the HTTP session has been started. def started? @started @@ -766,12 +876,16 @@ module Net #:nodoc: :@cert, :@cert_store, :@ciphers, + :@extra_chain_cert, :@key, :@ssl_timeout, :@ssl_version, + :@min_version, + :@max_version, :@verify_callback, :@verify_depth, :@verify_mode, + :@verify_hostname, ] SSL_ATTRIBUTES = [ :ca_file, @@ -779,12 +893,16 @@ module Net #:nodoc: :cert, :cert_store, :ciphers, + :extra_chain_cert, :key, :ssl_timeout, :ssl_version, + :min_version, + :max_version, :verify_callback, :verify_depth, :verify_mode, + :verify_hostname, ] # Sets path of a CA certification file in PEM format. @@ -806,6 +924,10 @@ module Net #:nodoc: # Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers= attr_accessor :ciphers + # Sets the extra X509 certificates to be added to the certificate chain. + # See OpenSSL::SSL::SSLContext#extra_chain_cert= + attr_accessor :extra_chain_cert + # Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. # (This method is appeared in Michal Rokos's OpenSSL extension.) attr_accessor :key @@ -816,6 +938,12 @@ module Net #:nodoc: # Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version= attr_accessor :ssl_version + # Sets the minimum SSL version. See OpenSSL::SSL::SSLContext#min_version= + attr_accessor :min_version + + # Sets the maximum SSL version. See OpenSSL::SSL::SSLContext#max_version= + attr_accessor :max_version + # Sets the verify callback for the server certification verification. attr_accessor :verify_callback @@ -828,6 +956,10 @@ module Net #:nodoc: # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable. attr_accessor :verify_mode + # Sets to check the server certificate is valid for the hostname. + # See OpenSSL::SSL::SSLContext#verify_hostname= + attr_accessor :verify_hostname + # Returns the X.509 certificates the server presented. def peer_cert if not use_ssl? or not @socket @@ -866,72 +998,112 @@ 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}..." + debug "opening connection to #{conn_addr}:#{conn_port}..." s = Timeout.timeout(@open_timeout, Net::OpenTimeout) { - TCPSocket.open(conn_address, conn_port, @local_host, @local_port) + begin + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) + rescue => e + raise e, "Failed to open TCP connection to " + + "#{conn_addr}:#{conn_port} (#{e.message})" + end } s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) - D "opened" + debug "opened" if use_ssl? + if proxy? + plain_sock = BufferedIO.new(s, 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" + buf << "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" + plain_sock.write(buf) + HTTPResponse.read_new(plain_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 - # Server Name Indication (SNI) RFC 3546 - s.hostname = @address if s.respond_to? :hostname= - if @ssl_session and - Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout - s.session = @ssl_session if @ssl_session - end - 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 @@ -948,7 +1120,7 @@ module Net #:nodoc: def do_finish @started = false - @socket.close if @socket and not @socket.closed? + @socket.close if @socket @socket = nil end private :do_finish @@ -1015,11 +1187,7 @@ module Net #:nodoc: # True if requests for this connection will be proxied 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 @@ -1029,15 +1197,17 @@ module Net #:nodoc: # The proxy URI determined from the environment for this connection. def proxy_uri # :nodoc: + return if @proxy_uri == false @proxy_uri ||= URI::HTTP.new( "http".freeze, nil, address, port, nil, nil, nil, nil, nil - ).find_proxy + ).find_proxy || false + @proxy_uri || nil end # The address of the proxy server, if one is configured. def proxy_address if @proxy_from_env then - proxy_uri && proxy_uri.hostname + proxy_uri&.hostname else @proxy_address end @@ -1046,20 +1216,30 @@ module Net #:nodoc: # The port of the proxy server, if one is configured. 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 + # The username of the proxy server, if one is configured. 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 + # The password of the proxy server, if one is configured. 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 @@ -1067,10 +1247,15 @@ module Net #:nodoc: private + def unescape(value) + require 'cgi/util' + CGI.unescape(value) + end + # without proxy, obsolete def conn_address # :nodoc: - address() + @ipaddr || address() end def conn_port # :nodoc: @@ -1078,8 +1263,12 @@ 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 @@ -1409,10 +1598,18 @@ module Net #:nodoc: 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 + res.body_encoding = @response_body_encoding + res.ignore_eof = @ignore_eof end while res.kind_of?(HTTPInformation) res.uri = req.uri @@ -1425,36 +1622,42 @@ module Net #:nodoc: 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 @@ -1469,15 +1672,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 @@ -1526,34 +1729,35 @@ 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 end -require 'net/http/exceptions' - -require 'net/http/header' +require_relative 'http/exceptions' -require 'net/http/generic_request' -require 'net/http/request' -require 'net/http/requests' +require_relative 'http/header' -require 'net/http/response' -require 'net/http/responses' +require_relative 'http/generic_request' +require_relative 'http/request' +require_relative 'http/requests' -require 'net/http/proxy_delta' +require_relative 'http/response' +require_relative 'http/responses' -require 'net/http/backward' +require_relative 'http/proxy_delta' +require_relative 'http/backward' diff --git a/lib/net/http/backward.rb b/lib/net/http/backward.rb index faf47b8489..691e41e4f1 100644 --- a/lib/net/http/backward.rb +++ b/lib/net/http/backward.rb @@ -1,25 +1,40 @@ +# frozen_string_literal: false # for backward compatibility # :enddoc: class Net::HTTP ProxyMod = ProxyDelta -end - -module Net - HTTPSession = Net::HTTP + deprecate_constant :ProxyMod end module Net::NetPrivate HTTPRequest = ::Net::HTTPRequest + deprecate_constant :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 +module Net + HTTPSession = HTTP + HTTPInformationCode = HTTPInformation + HTTPSuccessCode = HTTPSuccess + HTTPRedirectionCode = HTTPRedirection + HTTPRetriableCode = HTTPRedirection + HTTPClientErrorCode = HTTPClientError + HTTPFatalErrorCode = HTTPClientError + HTTPServerErrorCode = HTTPServerError + HTTPResponseReceiver = HTTPResponse + + HTTPResponceReceiver = HTTPResponse # Typo since 2001 + + deprecate_constant :HTTPSession, + :HTTPInformationCode, + :HTTPSuccessCode, + :HTTPRedirectionCode, + :HTTPRetriableCode, + :HTTPClientErrorCode, + :HTTPFatalErrorCode, + :HTTPServerErrorCode, + :HTTPResponseReceiver, + :HTTPResponceReceiver +end diff --git a/lib/net/http/exceptions.rb b/lib/net/http/exceptions.rb index 6c5d81cb04..9c425cae16 100644 --- a/lib/net/http/exceptions.rb +++ b/lib/net/http/exceptions.rb @@ -1,25 +1,34 @@ -# 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: false +module Net + # Net::HTTP exception class. + # You cannot use Net::HTTPExceptions directly; instead, you must use + # its subclasses. + module HTTPExceptions + def initialize(msg, res) #:nodoc: + super msg + @response = res + end + attr_reader :response + alias data response #:nodoc: obsolete end - 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 + + 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 959a3c6510..d56835c76f 100644 --- a/lib/net/http/generic_request.rb +++ b/lib/net/http/generic_request.rb @@ -1,7 +1,8 @@ -# HTTPGenericRequest is the parent of the HTTPRequest class. -# Do not use this directly; use a subclass of HTTPRequest. +# frozen_string_literal: false +# HTTPGenericRequest is the parent of the Net::HTTPRequest class. +# Do not use this directly; use a subclass of Net::HTTPRequest. # -# Mixes in the HTTPHeader module to provide easier access to HTTP headers. +# Mixes in the Net::HTTPHeader module to provide easier access to HTTP headers. # class Net::HTTPGenericRequest @@ -13,6 +14,9 @@ class Net::HTTPGenericRequest @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.hostname + raise ArgumentError, "no host component for URI" unless (hostname && hostname.length > 0) @uri = uri_or_path.dup host = @uri.hostname.dup host << ":".freeze << @uri.port.to_s if @uri.port != @uri.default_port @@ -28,12 +32,12 @@ class Net::HTTPGenericRequest @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" @@ -81,7 +85,7 @@ class Net::HTTPGenericRequest end def body_exist? - warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE + warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE response_body_permitted? end @@ -140,7 +144,7 @@ class Net::HTTPGenericRequest end if host = self['host'] - host.sub!(/:.*/s, ''.freeze) + host.sub!(/:.*/m, ''.freeze) elsif host = @uri.host else host = addr @@ -167,9 +171,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 @@ -200,9 +203,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 @@ -298,7 +299,7 @@ class Net::HTTPGenericRequest 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 + warn 'net/http: Content-Type did not set; using application/x-www-form-urlencoded', uplevel: 1 if $VERBOSE set_content_type 'application/x-www-form-urlencoded' end @@ -309,7 +310,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 diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb index 912419df55..b0ec4b0625 100644 --- a/lib/net/http/header.rb +++ b/lib/net/http/header.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false # The HTTPHeader module defines methods for reading and writing # HTTP headers. # @@ -13,8 +14,16 @@ module Net::HTTPHeader @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 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 @@ -27,17 +36,17 @@ module Net::HTTPHeader # Returns the header field corresponding to the case-insensitive key. # For example, a key of "Content-Type" might return "text/html" def [](key) - a = @header[key.downcase] or return nil + a = @header[key.downcase.to_s] or return nil a.join(', ') end # Sets the header field corresponding to the case-insensitive key. def []=(key, val) unless val - @header.delete key.downcase + @header.delete key.downcase.to_s return val end - @header[key.downcase] = [val] + set_field(key, val) end # [Ruby 1.8.3] @@ -56,10 +65,39 @@ module Net::HTTPHeader # p request.get_fields('X-My-Header') #=> ["a", "b", "c"] # def add_field(key, val) - if @header.key?(key.downcase) - @header[key.downcase].push val + stringified_downcased_key = key.downcase.to_s + if @header.key?(stringified_downcased_key) + append_field_value(@header[stringified_downcased_key], val) + else + set_field(key, val) + end + end + + 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 - @header[key.downcase] = [val] + 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 @@ -75,8 +113,9 @@ module Net::HTTPHeader # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23" # def get_fields(key) - return nil unless @header[key.downcase] - @header[key.downcase].dup + 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. @@ -84,19 +123,21 @@ module Net::HTTPHeader # raises an IndexError if there's no header field named +key+ # See Hash#fetch def fetch(key, *args, &block) #:yield: +key+ - a = @header.fetch(key.downcase, *args, &block) + a = @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. # + # Returns an enumerator if no block is given. + # # Example: # # response.header.each_header {|key,value| puts "#{key} = #{value}" } # 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 @@ -106,8 +147,10 @@ module Net::HTTPHeader # Iterates through the header names in the header, passing # each header name to the code block. + # + # Returns an enumerator if no block is given. 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 @@ -119,8 +162,10 @@ module Net::HTTPHeader # Note that header names are capitalized systematically; # capitalization may not match that used by the remote HTTP # server in its response. + # + # 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 @@ -128,8 +173,10 @@ module Net::HTTPHeader # Iterates through header values, passing each value to the # code block. + # + # 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 @@ -137,12 +184,12 @@ module Net::HTTPHeader # Removes a header field, specified by case-insensitive key. def delete(key) - @header.delete(key.downcase) + @header.delete(key.downcase.to_s) end # true if +key+ header exists. def key?(key) - @header.key?(key.downcase) + @header.key?(key.downcase.to_s) end # Returns a Hash consisting of header names and array of values. @@ -159,8 +206,10 @@ module Net::HTTPHeader # Note that header names are capitalized systematically; # capitalization may not match that used by the remote HTTP # server in its response. + # + # Returns an enumerator if no block is given. 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 @@ -169,7 +218,7 @@ module Net::HTTPHeader alias canonical_each each_capitalized def capitalize(name) - name.split(/-/).map {|s| s.capitalize }.join('-') + name.to_s.split(/-/).map {|s| s.capitalize }.join('-') end private :capitalize @@ -274,7 +323,7 @@ module Net::HTTPHeader end # Returns "true" if the "transfer-encoding" header is present and - # set to "chunked". This is an HTTP/1.1 feature, allowing the + # set to "chunked". This is an HTTP/1.1 feature, allowing # the content to be sent in "chunks" without at the outset # stating the entire content length. def chunked? @@ -289,9 +338,10 @@ module Net::HTTPHeader # fits inside the full entity body, as range of byte offsets. def content_range return nil unless @header['content-range'] - m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or + 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. @@ -373,31 +423,51 @@ 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. + # Set an HTML form data set. + # +params+ :: The form data to set, which should be an enumerable. + # See below for more details. + # +enctype+ :: The content type to use to encode the form submission, + # which should be application/x-www-form-urlencoded or + # multipart/form-data. + # +formopt+ :: An options hash, supporting the following options: + # :boundary :: The boundary of the multipart message. If + # not given, a random boundary will be used. + # :charset :: The charset of the form submission. All + # field names and values of non-file fields + # should be encoded with this charset. # - # 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. - # - # 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 + # Each item of params should respond to +each+ and yield 2-3 arguments, + # or an array of 2-3 elements. The arguments yielded should be: + # * The name of the field. + # * The value of the field, it should be a String or a File or IO-like. + # * An options hash, supporting the following options, only + # used for file uploads: + # :filename :: The name of the file to use. + # :content_type :: The content type of the uploaded file. # # Each item is a file field or a normal field. - # If +value+ is a File object or the +opt+ have a filename key, + # If +value+ is a File object or the +opt+ hash has a :filename key, # the item is treated as a file field. # - # If Transfer-Encoding is set as chunked, this send the request in + # If Transfer-Encoding is set as chunked, this sends the request using # chunked encoding. Because chunked encoding is HTTP/1.1 feature, - # you must confirm the server to support HTTP/1.1 before sending it. + # you should confirm that the server supports HTTP/1.1 before using + # chunked encoding. # # Example: - # http.set_form([["q", "ruby"], ["lang", "en"]]) + # req.set_form([["q", "ruby"], ["lang", "en"]]) + # + # req.set_form({"f"=>File.open('/path/to/filename')}, + # "multipart/form-data", + # charset: "UTF-8", + # ) + # + # req.set_form([["f", + # File.open('/path/to/filename.bar'), + # {filename: "other-filename.foo"} + # ]], + # "multipart/form-data", + # ) # # See also RFC 2388, RFC 2616, HTML 4.01, and HTML5 # @@ -426,27 +496,22 @@ module Net::HTTPHeader end def basic_encode(account, password) - 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n") + 'Basic ' + ["#{account}:#{password}"].pack('m0') end private :basic_encode 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 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..a7f122fc0e --- /dev/null +++ b/lib/net/http/net-http.gemspec @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +name = File.basename(__FILE__, ".gemspec") +version = ["lib", Array.new(name.count("-")+1, "..").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 = ["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.6.0") + spec.licenses = ["Ruby", "BSD-2-Clause"] + + 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. + spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do + `git ls-files -z 2>/dev/null`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + end + spec.bindir = "exe" + spec.require_paths = ["lib"] + + spec.add_dependency "uri" +end diff --git a/lib/net/http/proxy_delta.rb b/lib/net/http/proxy_delta.rb index b16c9f1ed8..a2f770ebdb 100644 --- a/lib/net/http/proxy_delta.rb +++ b/lib/net/http/proxy_delta.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false 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..1e86f3e4b4 100644 --- a/lib/net/http/request.rb +++ b/lib/net/http/request.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false # 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 diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb index c1f8360479..d4c80a3812 100644 --- a/lib/net/http/requests.rb +++ b/lib/net/http/requests.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false # # HTTP/1.1 methods --- RFC2616 # diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb index 126c22160d..f8b522f1ff 100644 --- a/lib/net/http/response.rb +++ b/lib/net/http/response.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false # HTTP response class. # # This class wraps together the response header and the response body (the @@ -7,11 +8,13 @@ # header values both via hash-like methods and via individual readers. # # Note that each possible HTTP response code defines its own -# HTTPResponse subclass. These are listed below. +# HTTPResponse subclass. All classes are defined under the Net module. +# Indentation indicates inheritance. For a list of the classes see Net::HTTP. # -# All classes are defined under the Net module. Indentation indicates -# inheritance. For a list of the classes see Net::HTTP. +# Correspondence <code>HTTP code => class</code> is stored in CODE_TO_OBJ +# constant: # +# Net::HTTPResponse::CODE_TO_OBJ['404'] #=> Net::HTTPNotFound # class Net::HTTPResponse class << self @@ -81,6 +84,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,6 +108,22 @@ class Net::HTTPResponse # Accept-Encoding header from the user. attr_accessor :decode_content + # The encoding to use for the response body. If Encoding, use that encoding. + # If other true value, attempt to detect the appropriate encoding, and use + # that. + attr_reader :body_encoding + + # Set the encoding to use for the response body. If given a String, find + # the related Encoding. + 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 "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" end @@ -116,7 +137,9 @@ class Net::HTTPResponse end def error! #:nodoc: - raise error_type().new(@code + ' ' + @message.dump, self) + message = @code + message += ' ' + @message.dump if @message + raise error_type().new(message, self) end def error_type #:nodoc: @@ -137,17 +160,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 +194,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. # @@ -205,6 +232,17 @@ class Net::HTTPResponse end @read = true + 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 @@ -236,13 +274,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: @@ -251,7 +424,7 @@ class Net::HTTPResponse return yield @socket if self['content-range'] v = self['content-encoding'] - case v && v.downcase + case v&.downcase when 'deflate', 'gzip', 'x-gzip' then self.delete 'content-encoding' @@ -259,12 +432,16 @@ class Net::HTTPResponse begin yield inflate_body_io + success = true ensure - orig_err = $! begin inflate_body_io.finish + if self['content-length'] + self['content-length'] = inflate_body_io.bytes_inflated.to_s + end rescue => err - raise orig_err || err + # Ignore #finish's error if there is an exception from yield + raise err if success end end when 'none', 'identity' then @@ -287,7 +464,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() @@ -364,6 +541,14 @@ class Net::HTTPResponse 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 @@ -377,6 +562,7 @@ class Net::HTTPResponse 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 1454a27a3e..02a2fdaa4c 100644 --- a/lib/net/http/responses.rb +++ b/lib/net/http/responses.rb @@ -1,205 +1,238 @@ -# :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 + # :stopdoc: -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 -class Net::HTTPIMUsed < Net::HTTPSuccess # 226 - RFC 3229 - HAS_BODY = true -end + class HTTPUnknownResponse < HTTPResponse + HAS_BODY = true + EXCEPTION_TYPE = HTTPError # + end + class HTTPInformation < HTTPResponse # 1xx + HAS_BODY = false + EXCEPTION_TYPE = HTTPError # + end + class HTTPSuccess < HTTPResponse # 2xx + HAS_BODY = true + EXCEPTION_TYPE = HTTPError # + end + class HTTPRedirection < HTTPResponse # 3xx + HAS_BODY = true + EXCEPTION_TYPE = HTTPRetriableError # + end + class HTTPClientError < HTTPResponse # 4xx + HAS_BODY = true + EXCEPTION_TYPE = HTTPClientException # + end + class HTTPServerError < HTTPResponse # 5xx + HAS_BODY = true + EXCEPTION_TYPE = HTTPFatalError # + 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 -class Net::HTTPPermanentRedirect < Net::HTTPRedirection # 308 - HAS_BODY = true -end + class HTTPContinue < HTTPInformation # 100 + HAS_BODY = false + end + class HTTPSwitchProtocol < HTTPInformation # 101 + HAS_BODY = false + end + class HTTPProcessing < HTTPInformation # 102 + HAS_BODY = false + end + class HTTPEarlyHints < HTTPInformation # 103 - RFC 8297 + HAS_BODY = false + 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 + class HTTPOK < HTTPSuccess # 200 + HAS_BODY = true + end + class HTTPCreated < HTTPSuccess # 201 + HAS_BODY = true + end + class HTTPAccepted < HTTPSuccess # 202 + HAS_BODY = true + end + class HTTPNonAuthoritativeInformation < HTTPSuccess # 203 + HAS_BODY = true + end + class HTTPNoContent < HTTPSuccess # 204 + HAS_BODY = false + end + class HTTPResetContent < HTTPSuccess # 205 + HAS_BODY = false + end + class HTTPPartialContent < HTTPSuccess # 206 + HAS_BODY = true + end + class HTTPMultiStatus < HTTPSuccess # 207 - RFC 4918 + HAS_BODY = true + end + class HTTPAlreadyReported < HTTPSuccess # 208 - RFC 5842 + HAS_BODY = true + end + class HTTPIMUsed < HTTPSuccess # 226 - RFC 3229 + 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 + class HTTPMultipleChoices < HTTPRedirection # 300 + HAS_BODY = true + end + HTTPMultipleChoice = HTTPMultipleChoices + class HTTPMovedPermanently < HTTPRedirection # 301 + HAS_BODY = true + end + class HTTPFound < HTTPRedirection # 302 + HAS_BODY = true + end + HTTPMovedTemporarily = HTTPFound + class HTTPSeeOther < HTTPRedirection # 303 + HAS_BODY = true + end + class HTTPNotModified < HTTPRedirection # 304 + HAS_BODY = false + end + class HTTPUseProxy < HTTPRedirection # 305 + HAS_BODY = false + end + # 306 Switch Proxy - no longer unused + class HTTPTemporaryRedirect < HTTPRedirection # 307 + HAS_BODY = true + end + class HTTPPermanentRedirect < HTTPRedirection # 308 + HAS_BODY = true + end + + class HTTPBadRequest < HTTPClientError # 400 + HAS_BODY = true + end + class HTTPUnauthorized < HTTPClientError # 401 + HAS_BODY = true + end + class HTTPPaymentRequired < HTTPClientError # 402 + HAS_BODY = true + end + class HTTPForbidden < HTTPClientError # 403 + HAS_BODY = true + end + class HTTPNotFound < HTTPClientError # 404 + HAS_BODY = true + end + class HTTPMethodNotAllowed < HTTPClientError # 405 + HAS_BODY = true + end + class HTTPNotAcceptable < HTTPClientError # 406 + HAS_BODY = true + end + class HTTPProxyAuthenticationRequired < HTTPClientError # 407 + HAS_BODY = true + end + class HTTPRequestTimeout < HTTPClientError # 408 + HAS_BODY = true + end + HTTPRequestTimeOut = HTTPRequestTimeout + class HTTPConflict < HTTPClientError # 409 + HAS_BODY = true + end + class HTTPGone < HTTPClientError # 410 + HAS_BODY = true + end + class HTTPLengthRequired < HTTPClientError # 411 + HAS_BODY = true + end + class HTTPPreconditionFailed < HTTPClientError # 412 + HAS_BODY = true + end + class HTTPPayloadTooLarge < HTTPClientError # 413 + HAS_BODY = true + end + HTTPRequestEntityTooLarge = HTTPPayloadTooLarge + class HTTPURITooLong < HTTPClientError # 414 + HAS_BODY = true + end + HTTPRequestURITooLong = HTTPURITooLong + HTTPRequestURITooLarge = HTTPRequestURITooLong + class HTTPUnsupportedMediaType < HTTPClientError # 415 + HAS_BODY = true + end + class HTTPRangeNotSatisfiable < HTTPClientError # 416 + HAS_BODY = true + end + HTTPRequestedRangeNotSatisfiable = HTTPRangeNotSatisfiable + class HTTPExpectationFailed < HTTPClientError # 417 + HAS_BODY = true + end + # 418 I'm a teapot - RFC 2324; a joke RFC + # 420 Enhance Your Calm - Twitter + class HTTPMisdirectedRequest < HTTPClientError # 421 - RFC 7540 + HAS_BODY = true + end + class HTTPUnprocessableEntity < HTTPClientError # 422 - RFC 4918 + HAS_BODY = true + end + class HTTPLocked < HTTPClientError # 423 - RFC 4918 + HAS_BODY = true + end + class HTTPFailedDependency < HTTPClientError # 424 - RFC 4918 + HAS_BODY = true + end + # 425 Unordered Collection - existed only in draft + class HTTPUpgradeRequired < HTTPClientError # 426 - RFC 2817 + HAS_BODY = true + end + class HTTPPreconditionRequired < HTTPClientError # 428 - RFC 6585 + HAS_BODY = true + end + class HTTPTooManyRequests < HTTPClientError # 429 - RFC 6585 + HAS_BODY = true + end + class HTTPRequestHeaderFieldsTooLarge < HTTPClientError # 431 - RFC 6585 + HAS_BODY = true + end + class HTTPUnavailableForLegalReasons < HTTPClientError # 451 - RFC 7725 + HAS_BODY = true + end + # 444 No Response - Nginx + # 449 Retry With - Microsoft + # 450 Blocked by Windows Parental Controls - Microsoft + # 499 Client Closed Request - Nginx + + class HTTPInternalServerError < HTTPServerError # 500 + HAS_BODY = true + end + class HTTPNotImplemented < HTTPServerError # 501 + HAS_BODY = true + end + class HTTPBadGateway < HTTPServerError # 502 + HAS_BODY = true + end + class HTTPServiceUnavailable < HTTPServerError # 503 + HAS_BODY = true + end + class HTTPGatewayTimeout < HTTPServerError # 504 + HAS_BODY = true + end + HTTPGatewayTimeOut = HTTPGatewayTimeout + class HTTPVersionNotSupported < HTTPServerError # 505 + HAS_BODY = true + end + class HTTPVariantAlsoNegotiates < HTTPServerError # 506 + HAS_BODY = true + end + class HTTPInsufficientStorage < HTTPServerError # 507 - RFC 4918 + HAS_BODY = true + end + class HTTPLoopDetected < HTTPServerError # 508 - RFC 5842 + HAS_BODY = true + end + # 509 Bandwidth Limit Exceeded - Apache bw/limited extension + class HTTPNotExtended < HTTPServerError # 510 - RFC 2774 + HAS_BODY = true + end + class HTTPNetworkAuthenticationRequired < HTTPServerError # 511 - RFC 6585 + HAS_BODY = true + end + + # :startdoc: end class Net::HTTPResponse @@ -213,6 +246,8 @@ class Net::HTTPResponse CODE_TO_OBJ = { '100' => Net::HTTPContinue, '101' => Net::HTTPSwitchProtocol, + '102' => Net::HTTPProcessing, + '103' => Net::HTTPEarlyHints, '200' => Net::HTTPOK, '201' => Net::HTTPCreated, @@ -222,6 +257,7 @@ class Net::HTTPResponse '205' => Net::HTTPResetContent, '206' => Net::HTTPPartialContent, '207' => Net::HTTPMultiStatus, + '208' => Net::HTTPAlreadyReported, '226' => Net::HTTPIMUsed, '300' => Net::HTTPMultipleChoices, @@ -231,6 +267,7 @@ class Net::HTTPResponse '304' => Net::HTTPNotModified, '305' => Net::HTTPUseProxy, '307' => Net::HTTPTemporaryRedirect, + '308' => Net::HTTPPermanentRedirect, '400' => Net::HTTPBadRequest, '401' => Net::HTTPUnauthorized, @@ -240,16 +277,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, @@ -257,17 +295,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, } end - -# :startdoc: - diff --git a/lib/net/http/status.rb b/lib/net/http/status.rb new file mode 100644 index 0000000000..8db3f7d9e3 --- /dev/null +++ b/lib/net/http/status.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require_relative '../http' + +if $0 == __FILE__ + require 'open-uri' + IO.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 "}" +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 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 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', + 511 => 'Network Authentication Required', +} diff --git a/lib/net/https.rb b/lib/net/https.rb index d36f82002d..d46721c82a 100644 --- a/lib/net/https.rb +++ b/lib/net/https.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false =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 0517ca1385..0000000000 --- a/lib/net/imap.rb +++ /dev/null @@ -1,3622 +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 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 mailbox - # from 1 up to the number of items in the mailbox. If a new - # message arrives during a session, it receives a sequence - # number equal to the new size of the mailbox. 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 thus cannot - # 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 exist; 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 your 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 the 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 of these mechanisms, there should be two +args+: username - # and (cleartext) password. A server may not support one or the 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 the specified +mailbox+. - # This command is generally available to both admin and user. - # If this mailbox exists, it returns an array containing objects of type - # 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 is generally 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 a 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 a 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 whose - # statuses are to be requested. 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 initially passed 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 - - # Similar to #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 a range between two numbers, - # or an array of those. The number is a message sequence number, - # where -1 repesents a '*' for use in range notation like 100..-1 - # being interpreted as '100:*'. Beware that the +exclude_end?+ - # property of a Range object is ignored, and the contents of a - # range are independent of the order of the range endpoints as per - # the protocol specification, so 1...5, 5..1 and 5...1 are all - # equivalent to 1..5. - # - # +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 or nil - # (instead of an empty array) if there is no matching message. - # - # 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 - - # Similar to #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, 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' will replace the message's flag list - # with the provided one, '+FLAGS' will add the provided flags, - # and '-FLAGS' will 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 - - # Similar to #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, 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 - - # Similar to #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 - - # Similar to #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 a new EXISTS response (which normally - # indicates new messages being added to the mailbox), - # 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 - - # Similar to #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 - - # Similar to #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 UTF-7. - 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:: The connected to the host was successful, but - # it immediately said goodbye. - 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) - begin - 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? - raise Error, "connection closed" - end - if @greeting.name == "BYE" - raise ByeResponseError, @greeting - end - - @client_thread = Thread.current - @receiver_thread = Thread.start { - begin - receive_responses - rescue Exception - end - } - @receiver_thread_terminating = false - rescue Exception - @sock.close - raise - end - 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 - NumValidator.ensure_number(data) - 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 - NumValidator.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 - end - - # Common validators of number and nz_number types - module NumValidator # :nodoc - class << self - # Check is passed argument valid 'number' in RFC 3501 terminology - def valid_number?(num) - # [RFC 3501] - # number = 1*DIGIT - # ; Unsigned 32-bit integer - # ; (0 <= n < 4,294,967,296) - num >= 0 && num < 4294967296 - end - - # Check is passed argument valid 'nz_number' in RFC 3501 terminology - def valid_nz_number?(num) - # [RFC 3501] - # nz-number = digit-nz *DIGIT - # ; Non-zero unsigned 32-bit integer - # ; (0 < n < 4,294,967,296) - num != 0 && valid_number?(num) - end - - # Ensure argument is 'number' or raise DataFormatError - def ensure_number(num) - return if valid_number?(num) - - msg = "number must be unsigned 32-bit integer: #{num}" - raise DataFormatError, msg - end - - # Ensure argument is 'nz_number' or raise DataFormatError - def ensure_nz_number(num) - return if valid_nz_number?(num) - - msg = "nz_number must be non-zero unsigned 32-bit integer: #{num}" - 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", or "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, one of "OK", "NO", or "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", or "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 the mailbox. - # - # quota:: Quota limit imposed on the 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 affect the quota on the - # specified mailbox. - # - MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots) - - # Net::IMAP::MailboxACLItem represents the 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 the 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 the 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 - when /\A(?:MIXED)\z/ni - return body_type_mixed - 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_mixed - mtype = "MULTIPART" - msubtype = case_insensitive_string - param, disposition, language, extension = body_ext_mpart - return BodyTypeBasic.new(mtype, msubtype, param, nil, nil, nil, nil, nil, disposition, language, extension) - end - - def body_type_mpart - parts = [] - while true - token = lookahead - if token.symbol == T_SPACE - shift_token - break - end - parts.push(body) - end - mtype = "MULTIPART" - msubtype = case_insensitive_string - param, disposition, language, extension = body_ext_mpart - return BodyTypeMultipart.new(mtype, msubtype, parts, - param, disposition, language, - extension) - end - - def media_type - mtype = case_insensitive_string - 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 - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return param, disposition - end - 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 - when T_NUMBER - data.push(number) - when T_LPAR - # TODO: include the MODSEQ value in a response - shift_token - match(T_ATOM) - match(T_SPACE) - match(T_NUMBER) - match(T_RPAR) - 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 - next - 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 - - 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 diff --git a/lib/net/net-protocol.gemspec b/lib/net/net-protocol.gemspec new file mode 100644 index 0000000000..021a5dcbf3 --- /dev/null +++ b/lib/net/net-protocol.gemspec @@ -0,0 +1,36 @@ +# 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 abstruct interface for net-* client.} + spec.description = %q{The abstruct 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 + + # 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. + spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do + `git ls-files -z 2>/dev/null`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "timeout" + spec.add_dependency "io-wait" +end diff --git a/lib/net/pop.rb b/lib/net/pop.rb deleted file mode 100644 index db7192c95a..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 - - # debugging 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_occurred = 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_occurred - begin - return yield() - rescue Exception - @error_occurred = true - raise - end - end - - end # class POP3Command - -end # module Net diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index 25477014fb..add2c87a71 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.1.2" + private def Protocol.protocol_param(name, val) module_eval(<<-End, __FILE__, __LINE__ + 1) @@ -32,6 +36,24 @@ 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 end @@ -55,20 +77,54 @@ 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 + 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 + 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 end attr_reader :io attr_accessor :read_timeout + attr_accessor :write_timeout attr_accessor :continue_timeout attr_accessor :debug_output @@ -94,17 +150,19 @@ 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)) + s = rbuf_consume(@rbuf.size) read_bytes += s.size + dest << s rbuf_fill end - dest << (s = rbuf_consume(len - read_bytes)) + s = rbuf_consume(len - read_bytes) read_bytes += s.size + dest << s rescue EOFError raise unless ignore_eof end @@ -112,13 +170,14 @@ 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)) + s = rbuf_consume(@rbuf.size) read_bytes += s.size + dest << s rbuf_fill end rescue EOFError @@ -149,27 +208,33 @@ 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 - else - raise Net::ReadTimeout - end - rescue IO::WaitWritable + tmp = @rbuf.empty? ? @rbuf : nil + case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false) + when String + return if rv.equal?(tmp) + @rbuf << rv + rv.clear + 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 - 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_consume(len) - s = @rbuf.slice!(0, len) + if len == @rbuf.size + s = @rbuf + @rbuf = ''.b + else + s = @rbuf.slice!(0, len) + end @debug_output << %Q[-> #{s.dump}\n] if @debug_output s end @@ -180,9 +245,9 @@ module Net # :nodoc: public - def write(str) + def write(*strs) writing { - write0 str + write0(*strs) } end @@ -206,11 +271,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 +324,7 @@ module Net # :nodoc: class InternetMessageIO < BufferedIO #:nodoc: internal use only - def initialize(io) + def initialize(*, **) super @wbuf = nil end @@ -251,7 +339,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)" @@ -295,7 +383,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 @@ -313,7 +401,7 @@ module Net # :nodoc: end def using_each_crlf_line - @wbuf = '' + @wbuf = ''.b yield if not @wbuf.empty? # unterminated last line write0 dot_stuff(@wbuf.chomp) + "\r\n" @@ -359,17 +447,16 @@ module Net # :nodoc: # The writer adapter class # class WriteAdapter - def initialize(socket, method) - @socket = socket - @method_id = method + 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 64e536a12a..0000000000 --- a/lib/net/smtp.rb +++ /dev/null @@ -1,1073 +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 or search for alternatives in - # {RubyGems.org}[https://rubygems.org/] or {The Ruby - # Toolbox}[https://www.ruby-toolbox.com/]. - # - # 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_occurred = 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_occurred = false - retry - end - raise - end - - def do_finish - quit if @socket and not @socket.closed? and not @error_occurred - ensure - @started = false - @error_occurred = 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') - socket_sync_bak = @socket.io.sync - begin - @socket.io.sync = false - if msgstr - @socket.write_message msgstr - else - @socket.write_message_by_block(&block) - end - ensure - @socket.io.flush - @socket.io.sync = socket_sync_bak - 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 Response.parse('200 dummy reply code') if @error_occurred - begin - return yield() - rescue Exception - @error_occurred = 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 raises 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 2260e7fb10..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_reader :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 - |
