diff options
Diffstat (limited to 'lib/net')
| -rw-r--r-- | lib/net/ftp.rb | 923 | ||||
| -rw-r--r-- | lib/net/http.rb | 2892 | ||||
| -rw-r--r-- | lib/net/http/backward.rb | 40 | ||||
| -rw-r--r-- | lib/net/http/exceptions.rb | 34 | ||||
| -rw-r--r-- | lib/net/http/generic_request.rb | 338 | ||||
| -rw-r--r-- | lib/net/http/header.rb | 517 | ||||
| -rw-r--r-- | lib/net/http/net-http.gemspec | 34 | ||||
| -rw-r--r-- | lib/net/http/proxy_delta.rb | 17 | ||||
| -rw-r--r-- | lib/net/http/request.rb | 21 | ||||
| -rw-r--r-- | lib/net/http/requests.rb | 123 | ||||
| -rw-r--r-- | lib/net/http/response.rb | 602 | ||||
| -rw-r--r-- | lib/net/http/responses.rb | 312 | ||||
| -rw-r--r-- | lib/net/http/status.rb | 83 | ||||
| -rw-r--r-- | lib/net/https.rb | 168 | ||||
| -rw-r--r-- | lib/net/imap.rb | 3371 | ||||
| -rw-r--r-- | lib/net/net-protocol.gemspec | 36 | ||||
| -rw-r--r-- | lib/net/pop.rb | 879 | ||||
| -rw-r--r-- | lib/net/protocol.rb | 213 | ||||
| -rw-r--r-- | lib/net/smtp.rb | 697 | ||||
| -rw-r--r-- | lib/net/telnet.rb | 742 |
20 files changed, 3520 insertions, 8522 deletions
diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb deleted file mode 100644 index dfbcf1499f..0000000000 --- a/lib/net/ftp.rb +++ /dev/null @@ -1,923 +0,0 @@ -# -# = net/ftp.rb - FTP Client Library -# -# Written by Shugo Maeda <shugo@ruby-lang.org>. -# -# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas) -# and "Ruby In a Nutshell" (Matsumoto), used with permission. -# -# This library is distributed under the terms of the Ruby license. -# You can freely distribute/modify this library. -# -# It is included in the Ruby standard library. -# -# See the Net::FTP class for an overview. -# - -require "socket" -require "monitor" - -module Net - - # :stopdoc: - class FTPError < StandardError; end - class FTPReplyError < FTPError; end - class FTPTempError < FTPError; end - class FTPPermError < FTPError; end - class FTPProtoError < FTPError; end - # :startdoc: - - # - # This class implements the File Transfer Protocol. If you have used a - # command-line FTP program, and are familiar with the commands, you will be - # able to use this class easily. Some extra features are included to take - # advantage of Ruby's style and strengths. - # - # == Example - # - # require 'net/ftp' - # - # === Example 1 - # - # ftp = Net::FTP.new('ftp.netlab.co.jp') - # ftp.login - # files = ftp.chdir('pub/lang/ruby/contrib') - # files = ftp.list('n*') - # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) - # ftp.close - # - # === Example 2 - # - # Net::FTP.open('ftp.netlab.co.jp') do |ftp| - # ftp.login - # files = ftp.chdir('pub/lang/ruby/contrib') - # files = ftp.list('n*') - # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) - # end - # - # == Major Methods - # - # The following are the methods most likely to be useful to users: - # - FTP.open - # - #getbinaryfile - # - #gettextfile - # - #putbinaryfile - # - #puttextfile - # - #chdir - # - #nlst - # - #size - # - #rename - # - #delete - # - class FTP - include MonitorMixin - - # :stopdoc: - FTP_PORT = 21 - CRLF = "\r\n" - DEFAULT_BLOCKSIZE = 4096 - # :startdoc: - - # When +true+, transfers are performed in binary mode. Default: +true+. - attr_accessor :binary - - # When +true+, the connection is in passive mode. Default: +false+. - attr_accessor :passive - - # When +true+, all traffic to and from the server is written - # to +$stdout+. Default: +false+. - attr_accessor :debug_mode - - # Sets or retrieves the +resume+ status, which decides whether incomplete - # transfers are resumed or restarted. Default: +false+. - attr_accessor :resume - - # The server's welcome message. - attr_reader :welcome - - # The server's last response code. - attr_reader :last_response_code - alias lastresp last_response_code - - # The server's last response. - attr_reader :last_response - - # - # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter. - # - # If a block is given, it is passed the +FTP+ object, which will be closed - # when the block finishes, or when an exception is raised. - # - def FTP.open(host, user = nil, passwd = nil, acct = nil) - if block_given? - ftp = new(host, user, passwd, acct) - begin - yield ftp - ensure - ftp.close - end - else - new(host, user, passwd, acct) - end - end - - # - # Creates and returns a new +FTP+ object. If a +host+ is given, a connection - # is made. Additionally, if the +user+ is given, the given user name, - # password, and (optionally) account are used to log in. See #login. - # - def initialize(host = nil, user = nil, passwd = nil, acct = nil) - super() - @binary = true - @passive = false - @debug_mode = false - @resume = false - if host - connect(host) - if user - login(user, passwd, acct) - end - end - end - - # Obsolete - def return_code - $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing") - return "\n" - end - - # Obsolete - def return_code=(s) - $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing") - end - - def open_socket(host, port) - if defined? SOCKSsocket and ENV["SOCKS_SERVER"] - @passive = true - return SOCKSsocket.open(host, port) - else - return TCPSocket.open(host, port) - end - end - private :open_socket - - # - # Establishes an FTP connection to host, optionally overriding the default - # port. If the environment variable +SOCKS_SERVER+ is set, sets up the - # connection through a SOCKS proxy. Raises an exception (typically - # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established. - # - def connect(host, port = FTP_PORT) - if @debug_mode - print "connect: ", host, ", ", port, "\n" - end - synchronize do - @sock = open_socket(host, port) - voidresp - end - end - - # - # WRITEME or make private - # - def set_socket(sock, get_greeting = true) - synchronize do - @sock = sock - if get_greeting - voidresp - end - end - end - - def sanitize(s) - if s =~ /^PASS /i - return s[0, 5] + "*" * (s.length - 5) - else - return s - end - end - private :sanitize - - def putline(line) - if @debug_mode - print "put: ", sanitize(line), "\n" - end - line = line + CRLF - @sock.write(line) - end - private :putline - - def getline - line = @sock.readline # if get EOF, raise EOFError - line.sub!(/(\r\n|\n|\r)\z/n, "") - if @debug_mode - print "get: ", sanitize(line), "\n" - end - return line - end - private :getline - - def getmultiline - line = getline - buff = line - if line[3] == ?- - code = line[0, 3] - begin - line = getline - buff << "\n" << line - end until line[0, 3] == code and line[3] != ?- - end - return buff << "\n" - end - private :getmultiline - - def getresp - @last_response = getmultiline - @last_response_code = @last_response[0, 3] - case @last_response_code - when /\A[123]/ - return @last_response - when /\A4/ - raise FTPTempError, @last_response - when /\A5/ - raise FTPPermError, @last_response - else - raise FTPProtoError, @last_response - end - end - private :getresp - - def voidresp - resp = getresp - if resp[0] != ?2 - raise FTPReplyError, resp - end - end - private :voidresp - - # - # Sends a command and returns the response. - # - def sendcmd(cmd) - synchronize do - putline(cmd) - return getresp - end - end - - # - # Sends a command and expect a response beginning with '2'. - # - def voidcmd(cmd) - synchronize do - putline(cmd) - voidresp - end - end - - def sendport(host, port) - af = (@sock.peeraddr)[0] - if af == "AF_INET" - hbytes = host.split(".") - pbytes = [port / 256, port % 256] - bytes = hbytes + pbytes - cmd = "PORT " + bytes.join(",") - elsif af == "AF_INET6" - cmd = "EPRT |2|" + host + "|" + sprintf("%d", port) + "|" - else - raise FTPProtoError, host - end - voidcmd(cmd) - end - private :sendport - - def makeport - sock = TCPServer.open(@sock.addr[3], 0) - port = sock.addr[1] - host = sock.addr[3] - resp = sendport(host, port) - return sock - end - private :makeport - - def makepasv - if @sock.peeraddr[0] == "AF_INET" - host, port = parse227(sendcmd("PASV")) - else - host, port = parse229(sendcmd("EPSV")) - # host, port = parse228(sendcmd("LPSV")) - end - return host, port - end - private :makepasv - - def transfercmd(cmd, rest_offset = nil) - if @passive - host, port = makepasv - conn = open_socket(host, port) - if @resume and rest_offset - resp = sendcmd("REST " + rest_offset.to_s) - if resp[0] != ?3 - raise FTPReplyError, resp - end - end - resp = sendcmd(cmd) - if resp[0] != ?1 - raise FTPReplyError, resp - end - else - sock = makeport - if @resume and rest_offset - resp = sendcmd("REST " + rest_offset.to_s) - if resp[0] != ?3 - raise FTPReplyError, resp - end - end - resp = sendcmd(cmd) - if resp[0] != ?1 - raise FTPReplyError, resp - end - conn = sock.accept - sock.close - end - return conn - end - private :transfercmd - - def getaddress - thishost = Socket.gethostname - if not thishost.index(".") - thishost = Socket.gethostbyname(thishost)[0] - end - if ENV.has_key?("LOGNAME") - realuser = ENV["LOGNAME"] - elsif ENV.has_key?("USER") - realuser = ENV["USER"] - else - realuser = "anonymous" - end - return realuser + "@" + thishost - end - private :getaddress - - # - # Logs in to the remote host. The session must have been previously - # connected. If +user+ is the string "anonymous" and the +password+ is - # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+ - # parameter is not +nil+, an FTP ACCT command is sent following the - # successful login. Raises an exception on error (typically - # <tt>Net::FTPPermError</tt>). - # - def login(user = "anonymous", passwd = nil, acct = nil) - if user == "anonymous" and passwd == nil - passwd = getaddress - end - - resp = "" - synchronize do - resp = sendcmd('USER ' + user) - if resp[0] == ?3 - resp = sendcmd('PASS ' + passwd) - end - if resp[0] == ?3 - resp = sendcmd('ACCT ' + acct) - end - end - if resp[0] != ?2 - raise FTPReplyError, resp - end - @welcome = resp - 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 - voidcmd("TYPE I") - conn = transfercmd(cmd, rest_offset) - loop do - data = conn.read(blocksize) - break if data == nil - yield(data) - end - conn.close - voidresp - 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 - voidcmd("TYPE A") - conn = transfercmd(cmd) - loop do - line = conn.gets - break if line == nil - if line[-2, 2] == CRLF - line = line[0 .. -3] - elsif line[-1] == ?\n - line = line[0 .. -2] - end - yield(line) - end - conn.close - voidresp - end - end - - # - # Puts the connection into binary (image) mode, issues the given server-side - # command (such as "STOR myfile"), and sends the contents of the file named - # +file+ to the server. If the optional block is given, it also passes it - # the data, in chunks of +blocksize+ characters. - # - def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data - if rest_offset - file.seek(rest_offset, IO::SEEK_SET) - end - synchronize do - voidcmd("TYPE I") - conn = transfercmd(cmd, rest_offset) - loop do - buf = file.read(blocksize) - break if buf == nil - conn.write(buf) - yield(buf) if block - end - conn.close - voidresp - end - end - - # - # Puts the connection into ASCII (text) mode, issues the given server-side - # command (such as "STOR myfile"), and sends the contents of the file - # named +file+ to the server, one line at a time. If the optional block is - # given, it also passes it the lines. - # - def storlines(cmd, file, &block) # :yield: line - synchronize do - voidcmd("TYPE A") - conn = transfercmd(cmd) - loop do - buf = file.gets - break if buf == nil - if buf[-2, 2] != CRLF - buf = buf.chomp + CRLF - end - conn.write(buf) - yield(buf) if block - end - conn.close - voidresp - end - end - - # - # Retrieves +remotefile+ in binary mode, storing the result in +localfile+. - # If a block is supplied, it is passed the retrieved data in +blocksize+ - # chunks. - # - def getbinaryfile(remotefile, localfile = File.basename(remotefile), - blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data - if @resume - rest_offset = File.size?(localfile) - f = open(localfile, "a") - else - rest_offset = nil - f = open(localfile, "w") - end - begin - f.binmode - retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data| - f.write(data) - yield(data) if block - end - ensure - f.close - end - end - - # - # Retrieves +remotefile+ in ASCII (text) mode, storing the result in - # +localfile+. If a block is supplied, it is passed the retrieved data one - # line at a time. - # - def gettextfile(remotefile, localfile = File.basename(remotefile), &block) # :yield: line - f = open(localfile, "w") - begin - retrlines("RETR " + remotefile) do |line| - f.puts(line) - yield(line) if block - end - ensure - f.close - 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 - unless @binary - gettextfile(remotefile, localfile, &block) - else - getbinaryfile(remotefile, localfile, blocksize, &block) - end - end - - # - # Transfers +localfile+ to the server in binary mode, storing the result in - # +remotefile+. If a block is supplied, calls it, passing in the transmitted - # data in +blocksize+ chunks. - # - def putbinaryfile(localfile, remotefile = File.basename(localfile), - blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data - if @resume - begin - rest_offset = size(remotefile) - rescue Net::FTPPermError - rest_offset = nil - end - else - rest_offset = nil - end - f = open(localfile) - begin - f.binmode - storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block) - ensure - f.close - end - end - - # - # Transfers +localfile+ to the server in ASCII (text) mode, storing the result - # in +remotefile+. If callback or an associated block is supplied, calls it, - # passing in the transmitted data one line at a time. - # - def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line - f = open(localfile) - begin - storlines("STOR " + remotefile, f, &block) - ensure - f.close - end - end - - # - # Transfers +localfile+ to the server in whatever mode the session is set - # (text or binary). See #puttextfile and #putbinaryfile. - # - def put(localfile, remotefile = File.basename(localfile), - blocksize = DEFAULT_BLOCKSIZE, &block) - unless @binary - puttextfile(localfile, remotefile, &block) - else - putbinaryfile(localfile, remotefile, blocksize, &block) - end - end - - # - # Sends the ACCT command. TODO: more info. - # - def acct(account) - cmd = "ACCT " + account - voidcmd(cmd) - end - - # - # Returns an array of filenames in the remote directory. - # - def nlst(dir = nil) - cmd = "NLST" - if dir - cmd = cmd + " " + dir - end - files = [] - retrlines(cmd) do |line| - files.push(line) - end - return files - end - - # - # Returns an array of file information in the directory (the output is like - # `ls -l`). If a block is given, it iterates through the listing. - # - def list(*args, &block) # :yield: line - cmd = "LIST" - args.each do |arg| - cmd = cmd + " " + arg - end - if block - retrlines(cmd, &block) - else - lines = [] - retrlines(cmd) do |line| - lines << line - end - return lines - end - end - alias ls list - alias dir list - - # - # Renames a file on the server. - # - def rename(fromname, toname) - resp = sendcmd("RNFR " + fromname) - if resp[0] != ?3 - raise FTPReplyError, resp - end - voidcmd("RNTO " + toname) - end - - # - # Deletes a file on the server. - # - def delete(filename) - resp = sendcmd("DELE " + filename) - if resp[0, 3] == "250" - return - elsif resp[0] == ?5 - raise FTPPermError, resp - else - raise FTPReplyError, resp - end - end - - # - # Changes the (remote) directory. - # - def chdir(dirname) - if dirname == ".." - begin - voidcmd("CDUP") - return - rescue FTPPermError - if $![0, 3] != "500" - raise FTPPermError, $! - end - end - end - cmd = "CWD " + dirname - voidcmd(cmd) - end - - # - # Returns the size of the given (remote) filename. - # - def size(filename) - voidcmd("TYPE I") - resp = sendcmd("SIZE " + filename) - if resp[0, 3] != "213" - raise FTPReplyError, resp - end - return resp[3..-1].strip.to_i - end - - MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ # :nodoc: - - # - # Returns the last modification time of the (remote) file. If +local+ is - # +true+, it is returned as a local time, otherwise it's a UTC time. - # - def mtime(filename, local = false) - str = mdtm(filename) - ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i} - return local ? Time.local(*ary) : Time.gm(*ary) - end - - # - # Creates a remote directory. - # - def mkdir(dirname) - resp = sendcmd("MKD " + dirname) - return parse257(resp) - end - - # - # Removes a remote directory. - # - def rmdir(dirname) - voidcmd("RMD " + dirname) - end - - # - # Returns the current remote directory. - # - def pwd - resp = sendcmd("PWD") - return parse257(resp) - end - alias getdir pwd - - # - # Returns system information. - # - def system - resp = sendcmd("SYST") - if resp[0, 3] != "215" - raise FTPReplyError, resp - end - return resp[4 .. -1] - end - - # - # Aborts the previous command (ABOR command). - # - def abort - line = "ABOR" + CRLF - print "put: ABOR\n" if @debug_mode - @sock.send(line, Socket::MSG_OOB) - resp = getmultiline - unless ["426", "226", "225"].include?(resp[0, 3]) - raise FTPProtoError, resp - end - return resp - end - - # - # Returns the status (STAT command). - # - def status - line = "STAT" + CRLF - print "put: STAT\n" if @debug_mode - @sock.send(line, Socket::MSG_OOB) - return getresp - end - - # - # Issues the MDTM command. TODO: more info. - # - def mdtm(filename) - resp = sendcmd("MDTM " + filename) - if resp[0, 3] == "213" - return resp[3 .. -1].strip - end - end - - # - # Issues the HELP command. - # - def help(arg = nil) - cmd = "HELP" - if arg - cmd = cmd + " " + arg - end - sendcmd(cmd) - end - - # - # Exits the FTP session. - # - def quit - voidcmd("QUIT") - end - - # - # Issues a NOOP command. - # - def noop - voidcmd("NOOP") - end - - # - # Issues a SITE command. - # - def site(arg) - cmd = "SITE " + arg - voidcmd(cmd) - end - - # - # Closes the connection. Further operations are impossible until you open - # a new connection with #connect. - # - def close - @sock.close if @sock and not @sock.closed? - end - - # - # Returns +true+ iff the connection is closed. - # - def closed? - @sock == nil or @sock.closed? - end - - def parse227(resp) - if resp[0, 3] != "227" - raise FTPReplyError, resp - end - left = resp.index("(") - right = resp.index(")") - if left == nil or right == nil - raise FTPProtoError, resp - end - numbers = resp[left + 1 .. right - 1].split(",") - if numbers.length != 6 - raise FTPProtoError, resp - end - host = numbers[0, 4].join(".") - port = (numbers[4].to_i << 8) + numbers[5].to_i - return host, port - end - private :parse227 - - def parse228(resp) - if resp[0, 3] != "228" - raise FTPReplyError, resp - end - left = resp.index("(") - right = resp.index(")") - if left == nil or right == nil - raise FTPProtoError, resp - end - numbers = resp[left + 1 .. right - 1].split(",") - if numbers[0] == "4" - if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2" - raise FTPProtoError, resp - end - host = numbers[2, 4].join(".") - port = (numbers[7].to_i << 8) + numbers[8].to_i - elsif numbers[0] == "6" - if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2" - raise FTPProtoError, resp - end - v6 = ["", "", "", "", "", "", "", ""] - for i in 0 .. 7 - v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i, - numbers[(i * 2) + 3].to_i) - end - host = v6[0, 8].join(":") - port = (numbers[19].to_i << 8) + numbers[20].to_i - end - return host, port - end - private :parse228 - - def parse229(resp) - if resp[0, 3] != "229" - raise FTPReplyError, resp - end - left = resp.index("(") - right = resp.index(")") - if left == nil or right == nil - raise FTPProtoError, resp - end - numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1]) - if numbers.length != 4 - raise FTPProtoError, resp - end - port = numbers[3].to_i - host = (@sock.peeraddr())[3] - return host, port - end - private :parse229 - - def parse257(resp) - if resp[0, 3] != "257" - raise FTPReplyError, resp - end - if resp[3, 2] != ' "' - return "" - end - dirname = "" - i = 5 - n = resp.length - while i < n - c = resp[i, 1] - i = i + 1 - if c == '"' - if i > n or resp[i, 1] != '"' - break - end - i = i + 1 - end - dirname = dirname + c - end - return dirname - end - private :parse257 - end - -end - - -# Documentation comments: -# - sourced from pickaxe and nutshell, with improvements (hopefully) -# - three methods should be private (search WRITEME) -# - two methods need more information (search TODO) diff --git a/lib/net/http.rb b/lib/net/http.rb index 7dd1f24d4c..d0582f8867 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1,32 +1,29 @@ +# frozen_string_literal: false # # = net/http.rb # -# Copyright (c) 1999-2006 Yukihiro Matsumoto -# Copyright (c) 1999-2006 Minero Aoki +# Copyright (c) 1999-2007 Yukihiro Matsumoto +# Copyright (c) 1999-2007 Minero Aoki # Copyright (c) 2001 GOTOU Yuuzou -# +# # Written and maintained by Minero Aoki <aamine@loveruby.net>. # HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>. # # This file is derived from "http-access.rb". # # Documented by Minero Aoki; converted to RDoc by William Webber. -# +# # This program is free software. You can re-distribute and/or # modify this program under the same terms of ruby itself --- # Ruby Distribution License or GNU General Public License. # -# See Net::HTTP for an overview and examples. -# -# NOTE: You can find Japanese version of this document here: -# http://www.ruby-lang.org/ja/man/?cmd=view;name=net%2Fhttp.rb -# -#-- -# $Id$ -#++ +# See Net::HTTP for an overview and examples. +# require 'net/protocol' require 'uri' +require 'resolv' +autoload :OpenSSL, 'openssl' module Net #:nodoc: @@ -35,282 +32,396 @@ module Net #:nodoc: class HTTPHeaderSyntaxError < StandardError; end # :startdoc: - # == What Is This Library? - # - # This library provides your program functions to access WWW - # documents via HTTP, Hyper Text Transfer Protocol version 1.1. - # For details of HTTP, refer [RFC2616] - # (http://www.ietf.org/rfc/rfc2616.txt). - # - # == Examples - # - # === Getting Document From WWW Server - # - # Example #1: Simple GET+print - # - # require 'net/http' - # Net::HTTP.get_print 'www.example.com', '/index.html' - # - # Example #2: Simple GET+print by URL - # - # require 'net/http' - # require 'uri' - # Net::HTTP.get_print URI.parse('http://www.example.com/index.html') - # - # Example #3: More generic GET+print - # - # require 'net/http' - # require 'uri' - # - # url = URI.parse('http://www.example.com/index.html') - # res = Net::HTTP.start(url.host, url.port) {|http| - # http.get('/index.html') - # } - # puts res.body - # - # Example #4: More generic GET+print - # - # require 'net/http' - # - # url = URI.parse('http://www.example.com/index.html') - # req = Net::HTTP::Get.new(url.path) - # res = Net::HTTP.start(url.host, url.port) {|http| - # http.request(req) - # } - # puts res.body - # - # === Posting Form Data - # - # require 'net/http' - # require 'uri' - # - # #1: Simple POST - # res = Net::HTTP.post_form(URI.parse('http://www.example.com/search.cgi'), - # {'q'=>'ruby', 'max'=>'50'}) - # puts res.body - # - # #2: POST with basic authentication - # res = Net::HTTP.post_form(URI.parse('http://jack:pass@www.example.com/todo.cgi'), - # {'from'=>'2005-01-01', 'to'=>'2005-03-31'}) - # puts res.body - # - # #3: Detailed control - # url = URI.parse('http://www.example.com/todo.cgi') - # req = Net::HTTP::Post.new(url.path) - # req.basic_auth 'jack', 'pass' - # req.set_form_data({'from'=>'2005-01-01', 'to'=>'2005-03-31'}, ';') - # res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) } - # case res - # when Net::HTTPSuccess, Net::HTTPRedirection - # # OK + # == An HTTP client API for Ruby. + # + # 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). + # + # Net::HTTP is designed to work closely with URI. URI::HTTP#host, + # URI::HTTP#port and URI::HTTP#request_uri are designed to work with + # Net::HTTP. + # + # If you are only performing a few GET requests you should try OpenURI. + # + # == Simple Examples + # + # All examples assume you have loaded Net::HTTP with: + # + # require 'net/http' + # + # This will also require 'uri' so you don't need to require it separately. + # + # The Net::HTTP methods in the following section do not persist + # connections. They are not recommended if you are performing many HTTP + # requests. + # + # === GET + # + # Net::HTTP.get('example.com', '/index.html') # => String + # + # === GET by URI + # + # uri = URI('http://example.com/index.html?count=10') + # Net::HTTP.get(uri) # => String + # + # === GET with Dynamic Parameters + # + # uri = URI('http://example.com/index.html') + # params = { :limit => 10, :page => 3 } + # uri.query = URI.encode_www_form(params) + # + # res = Net::HTTP.get_response(uri) + # puts res.body if res.is_a?(Net::HTTPSuccess) + # + # === POST + # + # uri = URI('http://www.example.com/search.cgi') + # res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50') + # puts res.body + # + # === POST with Multiple Values + # + # uri = URI('http://www.example.com/search.cgi') + # res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50') + # puts res.body + # + # == How to use Net::HTTP + # + # 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. + # + # uri = URI('http://example.com/some_path?query=string') + # + # Net::HTTP.start(uri.host, uri.port) do |http| + # request = Net::HTTP::Get.new uri + # + # response = http.request request # Net::HTTPResponse object + # end + # + # Net::HTTP::start immediately creates a connection to an HTTP server which + # is kept open for the duration of the block. The connection will remain + # open for multiple requests in the block if the server indicates it + # supports persistent connections. + # + # 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". + # + # 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. + # + # === Response Data + # + # uri = URI('http://example.com/index.html') + # res = Net::HTTP.get_response(uri) + # + # # Headers + # res['Set-Cookie'] # => String + # res.get_fields('set-cookie') # => Array + # res.to_hash['set-cookie'] # => Array + # puts "Headers: #{res.to_hash.inspect}" + # + # # Status + # puts res.code # => '200' + # puts res.message # => 'OK' + # puts res.class.name # => 'HTTPOK' + # + # # Body + # puts res.body + # + # === Following Redirection + # + # Each Net::HTTPResponse object belongs to a class for its response code. + # + # For example, all 2XX responses are instances of a Net::HTTPSuccess + # subclass, a 3XX response is an instance of a Net::HTTPRedirection + # subclass and a 200 response is an instance of the Net::HTTPOK class. For + # details of response classes, see the section "HTTP Response Classes" + # below. + # + # Using a case statement you can handle various types of responses properly: + # + # def fetch(uri_str, limit = 10) + # # You should choose a better exception. + # raise ArgumentError, 'too many HTTP redirects' if limit == 0 + # + # response = Net::HTTP.get_response(URI(uri_str)) + # + # case response + # when Net::HTTPSuccess then + # response + # when Net::HTTPRedirection then + # location = response['location'] + # warn "redirected to #{location}" + # fetch(location, limit - 1) # else - # res.error! + # response.value # end - # - # === Accessing via Proxy - # - # Net::HTTP.Proxy creates http proxy class. It has same - # methods of Net::HTTP but its instances always connect to - # proxy, instead of given host. - # - # require 'net/http' - # - # proxy_addr = 'your.proxy.host' - # proxy_port = 8080 - # : - # Net::HTTP::Proxy(proxy_addr, proxy_port).start('www.example.com') {|http| - # # always connect to your.proxy.addr:8080 - # : - # } - # - # Since Net::HTTP.Proxy returns Net::HTTP itself when proxy_addr is nil, - # there's no need to change code if there's proxy or not. - # - # There are two additional parameters in Net::HTTP.Proxy which allow to - # specify proxy user name and password: - # - # Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user = nil, proxy_pass = nil) - # - # You may use them to work with authorization-enabled proxies: - # - # require 'net/http' - # require 'uri' - # - # proxy_host = 'your.proxy.host' - # proxy_port = 8080 - # uri = URI.parse(ENV['http_proxy']) - # proxy_user, proxy_pass = uri.userinfo.split(/:/) if uri.userinfo - # Net::HTTP::Proxy(proxy_host, proxy_port, - # proxy_user, proxy_pass).start('www.example.com') {|http| - # # always connect to your.proxy.addr:8080 using specified username and password - # : - # } - # - # Note that net/http never rely on HTTP_PROXY environment variable. - # If you want to use proxy, set it explicitly. - # - # === Following Redirection - # - # require 'net/http' - # require 'uri' - # - # def fetch(uri_str, limit = 10) - # # You should choose better exception. - # raise ArgumentError, 'HTTP redirect too deep' if limit == 0 - # - # response = Net::HTTP.get_response(URI.parse(uri_str)) - # case response - # when Net::HTTPSuccess then response - # when Net::HTTPRedirection then fetch(response['location'], limit - 1) - # else - # response.error! + # end + # + # print fetch('http://www.ruby-lang.org') + # + # === POST + # + # A POST can be made using the Net::HTTP::Post request class. This example + # creates a URL encoded POST body: + # + # uri = URI('http://www.example.com/todo.cgi') + # req = Net::HTTP::Post.new(uri) + # req.set_form_data('from' => '2005-01-01', 'to' => '2005-03-31') + # + # res = Net::HTTP.start(uri.hostname, uri.port) do |http| + # http.request(req) + # end + # + # case res + # when Net::HTTPSuccess, Net::HTTPRedirection + # # OK + # else + # res.value + # end + # + # To send multipart/form-data use Net::HTTPHeader#set_form: + # + # req = Net::HTTP::Post.new(uri) + # 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). + # + # === Setting Headers + # + # The following example performs a conditional GET using the + # If-Modified-Since header. If the files has not been modified since the + # time in the header a Not Modified response will be returned. See RFC 2616 + # section 9.3 for further details. + # + # uri = URI('http://example.com/cached_response') + # file = File.stat 'cached_response' + # + # req = Net::HTTP::Get.new(uri) + # req['If-Modified-Since'] = file.mtime.rfc2822 + # + # res = Net::HTTP.start(uri.hostname, uri.port) {|http| + # http.request(req) + # } + # + # open 'cached_response', 'w' do |io| + # io.write res.body + # end if res.is_a?(Net::HTTPSuccess) + # + # === Basic Authentication + # + # Basic authentication is performed according to + # [RFC2617](http://www.ietf.org/rfc/rfc2617.txt). + # + # uri = URI('http://example.com/index.html?key=value') + # + # req = Net::HTTP::Get.new(uri) + # req.basic_auth 'user', 'pass' + # + # res = Net::HTTP.start(uri.hostname, uri.port) {|http| + # http.request(req) + # } + # puts res.body + # + # === Streaming Response Bodies + # + # By default Net::HTTP reads an entire response into memory. If you are + # handling large files or wish to implement a progress bar you can instead + # stream the body directly to an IO. + # + # uri = URI('http://example.com/large_file') + # + # Net::HTTP.start(uri.host, uri.port) do |http| + # request = Net::HTTP::Get.new uri + # + # http.request request do |response| + # open 'large_file', 'w' do |io| + # response.read_body do |chunk| + # io.write chunk + # end # end # end - # - # print fetch('http://www.ruby-lang.org') - # - # Net::HTTPSuccess and Net::HTTPRedirection is a HTTPResponse class. - # All HTTPResponse objects belong to its own response class which - # indicate HTTP result status. For details of response classes, - # see section "HTTP Response Classes". - # - # === Basic Authentication - # - # require 'net/http' - # - # Net::HTTP.start('www.example.com') {|http| - # req = Net::HTTP::Get.new('/secret-page.html') - # req.basic_auth 'account', 'password' - # response = http.request(req) - # print response.body - # } - # - # === HTTP Request Classes - # - # Here is HTTP request class hierarchy. - # - # Net::HTTPRequest - # Net::HTTP::Get - # Net::HTTP::Head - # Net::HTTP::Post - # Net::HTTP::Put - # Net::HTTP::Proppatch - # Net::HTTP::Lock - # Net::HTTP::Unlock - # Net::HTTP::Options - # Net::HTTP::Propfind - # Net::HTTP::Delete - # Net::HTTP::Move - # Net::HTTP::Copy - # Net::HTTP::Mkcol - # Net::HTTP::Trace - # - # === HTTP Response Classes - # - # Here is HTTP response class hierarchy. - # All classes are defined in Net module. - # - # HTTPResponse - # HTTPUnknownResponse - # HTTPInformation # 1xx - # HTTPContinue # 100 - # HTTPSwitchProtocl # 101 - # HTTPSuccess # 2xx - # HTTPOK # 200 - # HTTPCreated # 201 - # HTTPAccepted # 202 - # HTTPNonAuthoritativeInformation # 203 - # HTTPNoContent # 204 - # HTTPResetContent # 205 - # HTTPPartialContent # 206 - # HTTPRedirection # 3xx - # HTTPMultipleChoice # 300 - # HTTPMovedPermanently # 301 - # HTTPFound # 302 - # HTTPSeeOther # 303 - # HTTPNotModified # 304 - # HTTPUseProxy # 305 - # HTTPTemporaryRedirect # 307 - # HTTPClientError # 4xx - # HTTPBadRequest # 400 - # HTTPUnauthorized # 401 - # HTTPPaymentRequired # 402 - # HTTPForbidden # 403 - # HTTPNotFound # 404 - # HTTPMethodNotAllowed # 405 - # HTTPNotAcceptable # 406 - # HTTPProxyAuthenticationRequired # 407 - # HTTPRequestTimeOut # 408 - # HTTPConflict # 409 - # HTTPGone # 410 - # HTTPLengthRequired # 411 - # HTTPPreconditionFailed # 412 - # HTTPRequestEntityTooLarge # 413 - # HTTPRequestURITooLong # 414 - # HTTPUnsupportedMediaType # 415 - # HTTPRequestedRangeNotSatisfiable # 416 - # HTTPExpectationFailed # 417 - # HTTPServerError # 5xx - # HTTPInternalServerError # 500 - # HTTPNotImplemented # 501 - # HTTPBadGateway # 502 - # HTTPServiceUnavailable # 503 - # HTTPGatewayTimeOut # 504 - # HTTPVersionNotSupported # 505 - # - # == Switching Net::HTTP versions - # - # You can use net/http.rb 1.1 features (bundled with Ruby 1.6) - # by calling HTTP.version_1_1. Calling Net::HTTP.version_1_2 - # allows you to use 1.2 features again. - # - # # example - # Net::HTTP.start {|http1| ...(http1 has 1.2 features)... } - # - # Net::HTTP.version_1_1 - # Net::HTTP.start {|http2| ...(http2 has 1.1 features)... } - # - # Net::HTTP.version_1_2 - # Net::HTTP.start {|http3| ...(http3 has 1.2 features)... } - # - # This function is NOT thread-safe. + # end + # + # === HTTPS + # + # HTTPS is enabled for an HTTP connection by Net::HTTP#use_ssl=. + # + # uri = URI('https://secure.example.com/some_path?query=string') + # + # 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. + # + # === Proxies + # + # Net::HTTP will automatically create a proxy from the +http_proxy+ + # environment variable if it is present. To disable use of +http_proxy+, + # pass +nil+ for the proxy address. + # + # You may also create a custom proxy: + # + # proxy_addr = 'your.proxy.host' + # proxy_port = 8080 + # + # Net::HTTP.new('example.com', nil, proxy_addr, proxy_port).start { |http| + # # always proxy via your.proxy.addr:8080 + # } + # + # See Net::HTTP.new for further details and examples such as proxies that + # require a username and password. + # + # === Compression + # + # Net::HTTP automatically adds Accept-Encoding for compression of response + # bodies and automatically decompresses gzip and deflate responses unless a + # Range header was sent. + # + # Compression can be disabled through the Accept-Encoding: identity header. + # + # == HTTP Request Classes + # + # Here is the HTTP request class hierarchy. + # + # * Net::HTTPRequest + # * Net::HTTP::Get + # * Net::HTTP::Head + # * Net::HTTP::Post + # * Net::HTTP::Patch + # * Net::HTTP::Put + # * Net::HTTP::Proppatch + # * Net::HTTP::Lock + # * Net::HTTP::Unlock + # * Net::HTTP::Options + # * Net::HTTP::Propfind + # * Net::HTTP::Delete + # * Net::HTTP::Move + # * Net::HTTP::Copy + # * Net::HTTP::Mkcol + # * Net::HTTP::Trace + # + # == HTTP Response Classes + # + # Here is HTTP response class hierarchy. All classes are defined in Net + # module and are subclasses of Net::HTTPResponse. + # + # HTTPUnknownResponse:: For unhandled HTTP extensions + # HTTPInformation:: 1xx + # HTTPContinue:: 100 + # HTTPSwitchProtocol:: 101 + # HTTPProcessing:: 102 + # HTTPEarlyHints:: 103 + # HTTPSuccess:: 2xx + # HTTPOK:: 200 + # HTTPCreated:: 201 + # HTTPAccepted:: 202 + # HTTPNonAuthoritativeInformation:: 203 + # HTTPNoContent:: 204 + # HTTPResetContent:: 205 + # HTTPPartialContent:: 206 + # HTTPMultiStatus:: 207 + # HTTPAlreadyReported:: 208 + # HTTPIMUsed:: 226 + # HTTPRedirection:: 3xx + # HTTPMultipleChoices:: 300 + # HTTPMovedPermanently:: 301 + # HTTPFound:: 302 + # HTTPSeeOther:: 303 + # HTTPNotModified:: 304 + # HTTPUseProxy:: 305 + # HTTPTemporaryRedirect:: 307 + # HTTPPermanentRedirect:: 308 + # HTTPClientError:: 4xx + # HTTPBadRequest:: 400 + # HTTPUnauthorized:: 401 + # HTTPPaymentRequired:: 402 + # HTTPForbidden:: 403 + # HTTPNotFound:: 404 + # HTTPMethodNotAllowed:: 405 + # HTTPNotAcceptable:: 406 + # HTTPProxyAuthenticationRequired:: 407 + # HTTPRequestTimeOut:: 408 + # HTTPConflict:: 409 + # HTTPGone:: 410 + # HTTPLengthRequired:: 411 + # HTTPPreconditionFailed:: 412 + # HTTPRequestEntityTooLarge:: 413 + # HTTPRequestURITooLong:: 414 + # HTTPUnsupportedMediaType:: 415 + # HTTPRequestedRangeNotSatisfiable:: 416 + # HTTPExpectationFailed:: 417 + # HTTPMisdirectedRequest:: 421 + # HTTPUnprocessableEntity:: 422 + # HTTPLocked:: 423 + # HTTPFailedDependency:: 424 + # HTTPUpgradeRequired:: 426 + # HTTPPreconditionRequired:: 428 + # HTTPTooManyRequests:: 429 + # HTTPRequestHeaderFieldsTooLarge:: 431 + # HTTPUnavailableForLegalReasons:: 451 + # HTTPServerError:: 5xx + # HTTPInternalServerError:: 500 + # HTTPNotImplemented:: 501 + # HTTPBadGateway:: 502 + # 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 + # there is a protocol error. # class HTTP < Protocol # :stopdoc: + VERSION = "0.3.0.1" Revision = %q$Revision$.split[1] HTTPVersion = '1.1' - @newimpl = true + begin + require 'zlib' + HAVE_ZLIB=true + rescue LoadError + HAVE_ZLIB=false + end # :startdoc: - # Turns on net/http 1.2 (ruby 1.8) features. - # Defaults to ON in ruby 1.8. - # - # I strongly recommend to call this method always. - # - # require 'net/http' - # Net::HTTP.version_1_2 - # + # Turns on net/http 1.2 (Ruby 1.8) features. + # Defaults to ON in Ruby 1.8 or later. def HTTP.version_1_2 - @newimpl = true + true end - # Turns on net/http 1.1 (ruby 1.6) features. - # Defaults to OFF in ruby 1.8. - def HTTP.version_1_1 - @newimpl = false - end - - # true if net/http is in version 1.2 mode. + # Returns true if net/http is in version 1.2 mode. # Defaults to true. def HTTP.version_1_2? - @newimpl + true end - # true if net/http is in version 1.1 compatible mode. - # Defaults to true. - def HTTP.version_1_1? - not @newimpl + def HTTP.version_1_1? #:nodoc: + false end class << HTTP @@ -323,18 +434,22 @@ module Net #:nodoc: # # - # Get body from target and output it to +$stdout+. The - # target can either be specified as (+uri+), or as - # (+host+, +path+, +port+ = 80); so: + # Gets the body text from the target and outputs it to $stdout. The + # target can either be specified as + # (+uri+, +headers+), or as (+host+, +path+, +port+ = 80); so: # - # Net::HTTP.get_print URI.parse('http://www.example.com/index.html') + # Net::HTTP.get_print URI('http://www.example.com/index.html') # # or: # # Net::HTTP.get_print 'www.example.com', '/index.html' # - def HTTP.get_print(uri_or_host, path = nil, port = nil) - get_response(uri_or_host, path, port) {|res| + # 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 @@ -342,25 +457,29 @@ module Net #:nodoc: nil end - # Send a GET request to the target and return the response + # 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: - # - # print Net::HTTP.get(URI.parse('http://www.example.com/index.html')) + # (+uri+, +headers+), or as (+host+, +path+, +port+ = 80); so: + # + # print Net::HTTP.get(URI('http://www.example.com/index.html')) # # or: # # 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 - # Send a GET request to the target and return the response + # 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: - # - # res = Net::HTTP.get_response(URI.parse('http://www.example.com/index.html')) + # (+uri+, +headers+), or as (+host+, +path+, +port+ = 80); so: + # + # res = Net::HTTP.get_response(URI('http://www.example.com/index.html')) # print res.body # # or: @@ -368,40 +487,68 @@ 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 - new(uri.host, uri.port).start {|http| - return http.request_get(uri.request_uri, &block) + headers = path_or_headers + start(uri.hostname, uri.port, + :use_ssl => uri.scheme == 'https') {|http| + return http.request_get(uri, headers, &block) } end end - # Posts HTML form data to the +URL+. - # Form data must be represented as a Hash of String to String, e.g: + # Posts data to 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' # - # HTTP.post_form URI.parse('http://www.example.com/search.cgi'), - # { "q" => "ruby", "max" => "50" } + # Net::HTTP.post_form URI('http://www.example.com/search.cgi'), + # { "q" => "ruby", "max" => "50" } # def HTTP.post_form(url, params) - req = Post.new(url.path) + req = Post.new(url) req.form_data = params req.basic_auth url.user, url.password if url.user - new(url.host, url.port).start {|http| + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| http.request(req) } end @@ -429,90 +576,275 @@ module Net #:nodoc: BufferedIO end - # creates a new Net::HTTP object and opens its TCP connection and - # HTTP session. If the optional block is given, the newly - # created Net::HTTP object is passed to it and closed when the + # :call-seq: + # HTTP.start(address, port, p_addr, p_port, p_user, p_pass, &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. + # + # Arguments are the following: + # _address_ :: hostname or IP address of the server + # _port_ :: port of the server + # _p_addr_ :: address of proxy + # _p_port_ :: port of proxy + # _p_user_ :: user of proxy + # _p_pass_ :: pass of proxy + # _opt_ :: optional hash + # + # _opt_ sets following values by its accessor. + # The keys are 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. + # + # If the optional block is given, the newly + # created Net::HTTP object is passed to it and closed when the # block finishes. In this case, the return value of this method # is the return value of the block. If no block is given, the # return value of this method is the newly created Net::HTTP object - # itself, and the caller is responsible for closing it upon completion. - def HTTP.start(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil, &block) # :yield: +http+ - new(address, port, p_addr, p_port, p_user, p_pass).start(&block) + # itself, and the caller is responsible for closing it upon completion + # using the finish() method. + def HTTP.start(address, *arg, &block) # :yield: +http+ + arg.pop if opt = Hash.try_convert(arg[-1]) + port, p_addr, p_port, p_user, p_pass = *arg + p_addr = :ENV if arg.size < 2 + port = https_default_port if !port && opt && opt[:use_ssl] + http = new(address, port, p_addr, p_port, p_user, p_pass) + http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr] + + if opt + if opt[:use_ssl] + opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt) + end + http.methods.grep(/\A(\w+)=\z/) do |meth| + key = $1.to_sym + opt.key?(key) or next + http.__send__(meth, opt[key]) + end + end + + http.start(&block) end class << HTTP - alias newobj new - end + alias newobj new # :nodoc: + end + + # Creates a new Net::HTTP object without opening a TCP connection or + # HTTP session. + # + # The +address+ should be a DNS hostname or IP address, the +port+ is the + # port the server operates on. If no +port+ is given the default port for + # HTTP or HTTPS is used. + # + # If none of the +p_+ arguments are given, the proxy host and port are + # taken from the +http_proxy+ environment variable (or its uppercase + # equivalent) if present. If the proxy requires authentication you must + # supply it by hand. See URI::Generic#find_proxy for details of proxy + # detection from the environment. To disable proxy detection set +p_addr+ + # to nil. + # + # 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, p_no_proxy = nil) + http = super address, port + + if proxy_class? then # from Net::HTTP::Proxy() + http.proxy_from_env = @proxy_from_env + http.proxy_address = @proxy_address + http.proxy_port = @proxy_port + http.proxy_user = @proxy_user + http.proxy_pass = @proxy_pass + 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 + http.proxy_pass = p_pass + end - # Creates a new Net::HTTP object. - # If +proxy_addr+ is given, creates an Net::HTTP object with proxy support. - # This method does not open the TCP connection. - def HTTP.new(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil) - h = Proxy(p_addr, p_port, p_user, p_pass).newobj(address, port) - h.instance_eval { - @newimpl = ::Net::HTTP.version_1_2? - } - h + http end - # Creates a new Net::HTTP object for the specified +address+. - # This method does not open the TCP connection. + # Creates a new Net::HTTP object for the specified server address, + # without opening the TCP connection or initializing the HTTP session. + # The +address+ should be a DNS hostname or IP address. def initialize(address, port = nil) @address = address @port = (port || HTTP.default_port) + @ipaddr = nil + @local_host = nil + @local_port = nil @curr_http_version = HTTPVersion - @seems_1_0_server = false + @keep_alive_timeout = 2 + @last_communicated = nil @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 + @proxy_address = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = nil + @use_ssl = false @ssl_context = nil + @ssl_session = nil + @sspi_enabled = false + SSL_IVNAMES.each do |ivname| + instance_variable_set ivname, nil + end end def inspect "#<#{self.class} #{@address}:#{@port} open=#{started?}>" end - # *WARNING* This method causes serious security hole. + # *WARNING* This method opens a serious security hole. # Never use this method in production code. # - # Set an output stream for debugging. + # Sets an output stream for debugging. # - # http = Net::HTTP.new + # http = Net::HTTP.new(hostname) # http.set_debug_output $stderr # 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 - # The host name to connect to. + # The DNS host name or IP address to connect to. attr_reader :address # The port number to connect to. attr_reader :port - # Seconds to wait until connection is opened. - # If the HTTP object cannot open a connection in this many seconds, - # it raises a TimeoutError exception. + # The local host used to establish the connection. + attr_accessor :local_host + + # 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 60 seconds. attr_accessor :open_timeout - # Seconds to wait until reading one block (by one read(2) call). - # If the HTTP object cannot open a connection in this many seconds, - # it raises a TimeoutError exception. + # Number of seconds to wait for one block to be read (via one read(2) + # call). Any number may be used, including Floats for fractional + # seconds. If the HTTP object cannot read data in this many seconds, + # it raises a Net::ReadTimeout exception. The default value is 60 seconds. 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 - # returns true if the HTTP session is started. + # 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+. + attr_reader :continue_timeout + + # Setter for the continue_timeout attribute. + def continue_timeout=(sec) + @socket.continue_timeout = sec if @socket + @continue_timeout = sec + end + + # Seconds to reuse the connection of the previous request. + # If the idle time is less than this Keep-Alive Timeout, + # Net::HTTP reuses the TCP/IP socket used by the previous communication. + # The default value is 2 seconds. + 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 end @@ -521,19 +853,129 @@ module Net #:nodoc: attr_accessor :close_on_empty_response - # returns true if use SSL/TLS with HTTP. + # Returns true if SSL/TLS is being used with HTTP. def use_ssl? - false # redefined in net/https + @use_ssl end - # Opens TCP connection and HTTP session. - # - # When this method is called with block, gives a HTTP object - # to the block and closes the TCP connection / HTTP session - # after the block executed. + # Turn on/off SSL. + # This flag must be set before starting session. + # If you change use_ssl value after session started, + # a Net::HTTP object raises IOError. + def use_ssl=(flag) + flag = flag ? true : false + if started? and @use_ssl != flag + raise IOError, "use_ssl value changed, but session already started" + end + @use_ssl = flag + end + + SSL_IVNAMES = [ + :@ca_file, + :@ca_path, + :@cert, + :@cert_store, + :@ciphers, + :@extra_chain_cert, + :@key, + :@ssl_timeout, + :@ssl_version, + :@min_version, + :@max_version, + :@verify_callback, + :@verify_depth, + :@verify_mode, + :@verify_hostname, + ] + SSL_ATTRIBUTES = [ + :ca_file, + :ca_path, + :cert, + :cert_store, + :ciphers, + :extra_chain_cert, + :key, + :ssl_timeout, + :ssl_version, + :min_version, + :max_version, + :verify_callback, + :verify_depth, + :verify_mode, + :verify_hostname, + ] + + # Sets path of a CA certification file in PEM format. + # + # The file can contain several CA certificates. + attr_accessor :ca_file + + # Sets path of a CA certification directory containing certifications in + # PEM format. + attr_accessor :ca_path + + # Sets an OpenSSL::X509::Certificate object as client certificate. + # (This method is appeared in Michal Rokos's OpenSSL extension). + attr_accessor :cert + + # Sets the X509::Store to verify peer certificate. + attr_accessor :cert_store + + # 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 + + # Sets the SSL timeout seconds. + attr_accessor :ssl_timeout + + # 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 + + # Sets the maximum depth for the certificate chain verification. + attr_accessor :verify_depth + + # Sets the flags for server the certification verification at beginning of + # SSL/TLS session. + # + # 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 + return nil + end + @socket.io.peer_cert + end + + # Opens a TCP connection and HTTP session. + # + # When this method is called with a block, it passes the Net::HTTP + # object to the block, and closes the TCP connection and HTTP session + # after the block has been executed. # - # When called with a block, returns the return value of the - # block; otherwise, returns self. + # When called with a block, it returns the return value of the + # block; otherwise, it returns self. # def start # :yield: http raise IOError, 'HTTP session already opened' if @started @@ -556,39 +998,112 @@ module Net #:nodoc: private :do_start def connect - D "opening connection to #{conn_address()}..." - s = timeout(@open_timeout) { TCPSocket.open(conn_address(), conn_port()) } - D "opened" if use_ssl? - unless @ssl_context.verify_mode - warn "warning: peer certificate won't be verified in this SSL session" - @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) - s.sync_close = true + # reference early to load OpenSSL before connecting, + # as OpenSSL may take time to load. + @ssl_context = OpenSSL::SSL::SSLContext.new end - @socket = BufferedIO.new(s) - @socket.read_timeout = @read_timeout - @socket.debug_output = @debug_output + + if proxy? then + conn_addr = proxy_address + conn_port = proxy_port + else + conn_addr = conn_address + conn_port = port + end + + debug "opening connection to #{conn_addr}:#{conn_port}..." + s = Timeout.timeout(@open_timeout, Net::OpenTimeout) { + 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) + debug "opened" if use_ssl? if proxy? - @socket.writeline sprintf('CONNECT %s:%s HTTP/%s', - @address, @port, HTTPVersion) - @socket.writeline "Host: #{@address}:#{@port}" + 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('m') - credential.delete!("\r\n") - @socket.writeline "Proxy-Authorization: Basic #{credential}" + credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0') + buf << "Proxy-Authorization: Basic #{credential}\r\n" end - @socket.writeline '' - HTTPResponse.read_new(@socket).value + 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) + value = instance_variable_get(ivname) + unless value.nil? + ssl_parameters[SSL_ATTRIBUTES[i]] = value + end + end + end + @ssl_context.set_params(ssl_parameters) + unless @ssl_context.session_cache_mode.nil? # a dummy method on JRuby + @ssl_context.session_cache_mode = + OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | + OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE end - s.connect - if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE + if @ssl_context.respond_to?(:session_new_cb) # not implemented under JRuby + @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess } + end + + # Still do the post_connection_check below even if connecting + # to IP address + verify_hostname = @ssl_context.verify_hostname + + # Server Name Indication (SNI) RFC 3546/6066 + case @address + when Resolv::IPv4::Regex, Resolv::IPv6::Regex + # don't set SNI, as IP addresses in SNI is not valid + # per RFC 6066, section 3. + + # Avoid openssl warning + @ssl_context.verify_hostname = false + else + ssl_host_address = @address + end + + debug "starting SSL for #{conn_addr}:#{conn_port}..." + s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) + s.sync_close = true + s.hostname = ssl_host_address if s.respond_to?(:hostname=) && ssl_host_address + + if @ssl_session and + Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout + s.session = @ssl_session + end + ssl_socket_connect(s, @open_timeout) + if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && verify_hostname s.post_connection_check(@address) end + debug "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}" end + @socket = BufferedIO.new(s, read_timeout: @read_timeout, + write_timeout: @write_timeout, + continue_timeout: @continue_timeout, + debug_output: @debug_output) + @last_communicated = nil on_connect + rescue => exception + if s + debug "Conn close because of connect error #{exception}" + s.close + end + raise end private :connect @@ -596,8 +1111,8 @@ module Net #:nodoc: end private :on_connect - # Finishes HTTP session and closes TCP connection. - # Raises IOError if not started. + # Finishes the HTTP session and closes the TCP connection. + # Raises IOError if the session has not been started. def finish raise IOError, 'HTTP session not yet started' unless started? do_finish @@ -605,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 @@ -618,77 +1133,113 @@ module Net #:nodoc: # no proxy @is_proxy_class = false + @proxy_from_env = false @proxy_addr = nil @proxy_port = nil @proxy_user = nil @proxy_pass = nil - # Creates an HTTP proxy class. - # Arguments are address/port of proxy host and username/password - # if authorization on proxy server is required. - # You can replace the HTTP class with created proxy class. - # - # If ADDRESS is nil, this method returns self (Net::HTTP). - # - # # Example - # proxy_class = Net::HTTP::Proxy('proxy.example.com', 8080) - # : - # proxy_class.start('www.ruby-lang.org') {|http| - # # connecting proxy.foo.org:8080 - # : - # } - # - def HTTP.Proxy(p_addr, p_port = nil, p_user = nil, p_pass = nil) + # Creates an HTTP proxy class which behaves like Net::HTTP, but + # performs all access via the specified proxy. + # + # This class is obsolete. You may pass these same parameters directly to + # Net::HTTP.new. See Net::HTTP.new for details of the arguments. + def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil) return self unless p_addr - delta = ProxyDelta - proxyclass = Class.new(self) - proxyclass.module_eval { - include delta - # with proxy + + Class.new(self) { @is_proxy_class = true - @proxy_address = p_addr - @proxy_port = p_port || default_port() - @proxy_user = p_user - @proxy_pass = p_pass + + if p_addr == :ENV then + @proxy_from_env = true + @proxy_address = nil + @proxy_port = nil + else + @proxy_from_env = false + @proxy_address = p_addr + @proxy_port = p_port || default_port + end + + @proxy_user = p_user + @proxy_pass = p_pass } - proxyclass end class << HTTP # returns true if self is a class which was created by HTTP::Proxy. def proxy_class? - @is_proxy_class + defined?(@is_proxy_class) ? @is_proxy_class : false end + # Address of proxy host. If Net::HTTP does not use a proxy, nil. attr_reader :proxy_address + + # Port number of proxy host. If Net::HTTP does not use a proxy, nil. attr_reader :proxy_port + + # User name for accessing proxy. If Net::HTTP does not use a proxy, nil. attr_reader :proxy_user + + # User password for accessing proxy. If Net::HTTP does not use a proxy, + # nil. attr_reader :proxy_pass end - # True if self is a HTTP proxy class. + # True if requests for this connection will be proxied def proxy? - self.class.proxy_class? + !!(@proxy_from_env ? proxy_uri : @proxy_address) + end + + # True if the proxy for this connection is determined from the environment + def proxy_from_env? + @proxy_from_env + end + + # 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 || false + @proxy_uri || nil end - # Address of proxy host. If self does not use a proxy, nil. + # The address of the proxy server, if one is configured. def proxy_address - self.class.proxy_address + if @proxy_from_env then + proxy_uri&.hostname + else + @proxy_address + end end - # Port number of proxy host. If self does not use a proxy, nil. + # The port of the proxy server, if one is configured. def proxy_port - self.class.proxy_port + if @proxy_from_env then + proxy_uri&.port + else + @proxy_port + end end - # User name for accessing proxy. If self does not use a proxy, nil. + # The username of the proxy server, if one is configured. def proxy_user - self.class.proxy_user + if @proxy_from_env + user = proxy_uri&.user + unescape(user) if user + else + @proxy_user + end end - # User password for accessing proxy. If self does not use a proxy, nil. + # The password of the proxy server, if one is configured. def proxy_pass - self.class.proxy_pass + if @proxy_from_env + pass = proxy_uri&.password + unescape(pass) if pass + else + @proxy_pass + end end alias proxyaddr proxy_address #:nodoc: obsolete @@ -696,33 +1247,30 @@ module Net #:nodoc: private - # without proxy + def unescape(value) + require 'cgi/util' + CGI.unescape(value) + end + + # without proxy, obsolete - def conn_address - address() + def conn_address # :nodoc: + @ipaddr || address() end - def conn_port + def conn_port # :nodoc: port() end def edit_path(path) - path - end - - module ProxyDelta #:nodoc: internal use only - private - - def conn_address - proxy_address() - end - - def conn_port - proxy_port() - end - - def edit_path(path) - use_ssl? ? path : "http://#{addr_port()}#{path}" + if proxy? + if path.start_with?("ftp://") || use_ssl? + path + else + "http://#{addr_port}#{path}" + end + else + path end end @@ -732,13 +1280,23 @@ module Net #:nodoc: public - # Gets data from +path+ on the connected-to host. - # +header+ must be a Hash like { 'Accept' => '*/*', ... }. + # Retrieves data from +path+ on the connected-to host which may be an + # absolute path String or a URI to extract the path from. + # + # +initheader+ must be a Hash like { 'Accept' => '*/*', ... }, + # and it defaults to an empty hash. + # If +initheader+ doesn't have the key 'accept-encoding', then + # a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used, + # so that gzip compression is used in preference to deflate + # compression, which is used in preference to no compression. + # Ruby doesn't have libraries to support the compress (Lempel-Ziv) + # compression, so that is not supported. The intent of this is + # to reduce bandwidth by default. If this routine sets up + # compression, then it does the decompression also, removing + # the header as well to prevent confusion. Otherwise + # it leaves the body as it found it. # - # In version 1.1 (ruby 1.6), this method returns a pair of objects, - # a Net::HTTPResponse object and the entity body string. - # In version 1.2 (ruby 1.8), this method returns a Net::HTTPResponse - # object. + # This method returns a Net::HTTPResponse object. # # If called with a block, yields each fragment of the # entity body in turn as a string as it is read from @@ -748,18 +1306,10 @@ module Net #:nodoc: # +dest+ argument is obsolete. # It still works but you must not use it. # - # In version 1.1, this method might raise an exception for - # 3xx (redirect). In this case you can get a HTTPResponse object - # by "anException.response". - # - # In version 1.2, this method never raises exception. - # - # # version 1.1 (bundled with Ruby 1.6) - # response, body = http.get('/index.html') + # This method never raises an exception. # - # # version 1.2 (bundled with Ruby 1.8 or later) # response = http.get('/index.html') - # + # # # using block # File.open('result.txt', 'w') {|f| # http.get('/~foo/') do |str| @@ -773,62 +1323,43 @@ module Net #:nodoc: r.read_body dest, &block res = r } - unless @newimpl - res.value - return res, res.body - end - res end # Gets only the header from +path+ on the connected-to host. # +header+ is a Hash like { 'Accept' => '*/*', ... }. - # + # # This method returns a Net::HTTPResponse object. - # - # In version 1.1, this method might raise an exception for - # 3xx (redirect). On the case you can get a HTTPResponse object - # by "anException.response". - # In version 1.2, this method never raises an exception. - # + # + # This method never raises an exception. + # # response = nil # Net::HTTP.start('some.www.server', 80) {|http| # response = http.head('/index.html') # } # p response['content-type'] # - def head(path, initheader = nil) - res = request(Head.new(path, initheader)) - res.value unless @newimpl - res + def head(path, initheader = nil) + request(Head.new(path, initheader)) end # Posts +data+ (must be a String) to +path+. +header+ must be a Hash # like { 'Accept' => '*/*', ... }. - # - # In version 1.1 (ruby 1.6), this method returns a pair of objects, a - # Net::HTTPResponse object and an entity body string. - # In version 1.2 (ruby 1.8), this method returns a Net::HTTPResponse object. - # + # + # This method returns a Net::HTTPResponse object. + # # If called with a block, yields each fragment of the - # entity body in turn as a string as it are read from + # entity body in turn as a string as it is read from # the socket. Note that in this case, the returned response # object will *not* contain a (meaningful) body. # # +dest+ argument is obsolete. # It still works but you must not use it. - # - # In version 1.1, this method might raise an exception for - # 3xx (redirect). In this case you can get an HTTPResponse object - # by "anException.response". - # In version 1.2, this method never raises exception. - # - # # version 1.1 - # response, body = http.post('/cgi-bin/search.rb', 'query=foo') - # - # # version 1.2 + # + # This method never raises exception. + # # response = http.post('/cgi-bin/search.rb', 'query=foo') - # + # # # using block # File.open('result.txt', 'w') {|f| # http.post('/cgi-bin/search.rb', 'query=foo') do |str| @@ -841,22 +1372,17 @@ module Net #:nodoc: # "application/x-www-form-urlencoded" by default. # def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ - res = nil - request(Post.new(path, initheader), data) {|r| - r.read_body dest, &block - res = r - } - unless @newimpl - res.value - return res, res.body - end - res + send_entity(path, data, initheader, dest, Post, &block) + end + + # Sends a PATCH request to the +path+ and gets a response, + # as an HTTPResponse object. + def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Patch, &block) end def put(path, data, initheader = nil) #:nodoc: - res = request(Put.new(path, initheader), data) - res.value unless @newimpl - res + request(Put.new(path, initheader), data) end # Sends a PROPPATCH request to the +path+ and gets a response, @@ -919,24 +1445,24 @@ module Net #:nodoc: request(Trace.new(path, initheader)) end - # Sends a GET request to the +path+ and gets a response, - # as an HTTPResponse object. - # - # When called with a block, yields an HTTPResponse object. - # The body of this response will not have been read yet; - # the caller can process it using HTTPResponse#read_body, + # Sends a GET request to the +path+. + # Returns the response as a Net::HTTPResponse object. + # + # When called with a block, passes an HTTPResponse object to the block. + # The body of the response will not have been read yet; + # the block can process it using HTTPResponse#read_body, # if desired. # # Returns the response. - # + # # This method never raises Net::* exceptions. - # + # # response = http.request_get('/index.html') - # # The entity body is already read here. + # # The entity body is already read in this case. # p response['content-type'] # puts response.body - # - # # using block + # + # # Using a block # http.request_get('/index.html') {|response| # p response['content-type'] # response.read_body do |str| # read body now @@ -948,13 +1474,13 @@ module Net #:nodoc: request(Get.new(path, initheader), &block) end - # Sends a HEAD request to the +path+ and gets a response, - # as an HTTPResponse object. + # Sends a HEAD request to the +path+ and returns the response + # as a Net::HTTPResponse object. # # Returns the response. - # + # # This method never raises Net::* exceptions. - # + # # response = http.request_head('/index.html') # p response['content-type'] # @@ -962,23 +1488,23 @@ module Net #:nodoc: request(Head.new(path, initheader), &block) end - # Sends a POST request to the +path+ and gets a response, - # as an HTTPResponse object. - # - # When called with a block, yields an HTTPResponse object. - # The body of this response will not have been read yet; - # the caller can process it using HTTPResponse#read_body, - # if desired. + # Sends a POST request to the +path+. + # + # Returns the response as a Net::HTTPResponse object. + # + # When called with a block, the block is passed an HTTPResponse + # object. The body of that response will not have been read yet; + # the block can process it using HTTPResponse#read_body, if desired. # # Returns the response. - # + # # This method never raises Net::* exceptions. - # + # # # example # response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...') # p response.status - # puts response.body # body is already read - # + # puts response.body # body is already read in this case + # # # using block # http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response| # p response.status @@ -1003,31 +1529,34 @@ module Net #:nodoc: # Sends an HTTP request to the HTTP server. - # This method also sends DATA string if DATA is given. + # Also sends a DATA string if +data+ is given. + # + # Returns a Net::HTTPResponse object. # - # Returns a HTTPResponse object. - # # This method never raises Net::* exceptions. # # response = http.send_request('GET', '/index.html') # puts response.body # def send_request(name, path, data = nil, header = nil) - r = HTTPGenericRequest.new(name,(data ? true : false),true,path,header) + has_response_body = name != 'HEAD' + r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header) request r, data end - # Sends an HTTPRequest object REQUEST to the HTTP server. - # This method also sends DATA string if REQUEST is a post/put request. - # Giving DATA for get/head request causes ArgumentError. - # - # When called with a block, yields an HTTPResponse object. - # The body of this response will not have been read yet; - # the caller can process it using HTTPResponse#read_body, + # Sends an HTTPRequest object +req+ to the HTTP server. + # + # If +req+ is a Net::HTTP::Post or Net::HTTP::Put request containing + # data, the data is also sent. Providing data for a Net::HTTP::Head or + # Net::HTTP::Get request results in an ArgumentError. + # + # Returns an HTTPResponse object. + # + # When called with a block, passes an HTTPResponse object to the block. + # The body of the response will not have been read yet; + # the block can process it using HTTPResponse#read_body, # if desired. # - # Returns a HTTPResponse object. - # # This method never raises Net::* exceptions. # def request(req, body = nil, &block) # :yield: +response+ @@ -1038,1240 +1567,197 @@ module Net #:nodoc: } end if proxy_user() - unless use_ssl? - req.proxy_basic_auth proxy_user(), proxy_pass() - end + req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? end - req.set_body_internal body - begin_transport req - req.exec @socket, @curr_http_version, edit_path(req.path) - begin - res = HTTPResponse.read_new(@socket) - end while res.kind_of?(HTTPContinue) + res = transport_request(req, &block) + if sspi_auth?(res) + sspi_auth(req) + res = transport_request(req, &block) + end + res + end + + private + + # Executes a request which uses a representation + # and returns its body. + def send_entity(path, data, initheader, dest, type, &block) + res = nil + request(type.new(path, initheader), data) {|r| + r.read_body dest, &block + res = r + } + res + end + + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc: + + def transport_request(req) + count = 0 + begin + begin_transport req + res = catch(:response) { + begin + req.exec @socket, @curr_http_version, edit_path(req.path) + rescue Errno::EPIPE + # Failure when writing full request, but we can probably + # still read the received response. + end + + begin + res = HTTPResponse.read_new(@socket) + res.decode_content = req.decode_content + res.body_encoding = @response_body_encoding + res.ignore_eof = @ignore_eof + end while res.kind_of?(HTTPInformation) + + res.uri = req.uri + + res + } res.reading_body(@socket, req.response_body_permitted?) { yield res if block_given? } - end_transport req, res + rescue Net::OpenTimeout + raise + rescue Net::ReadTimeout, IOError, EOFError, + Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT, + # avoid a dependency on OpenSSL + defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError, + Timeout::Error => exception + if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method) + count += 1 + @socket.close if @socket + debug "Conn close because of error #{exception}, and retry" + retry + end + debug "Conn close because of error #{exception}" + @socket.close if @socket + raise + end + end_transport req, res res + rescue => exception + debug "Conn close because of error #{exception}" + @socket.close if @socket + raise exception end - private - def begin_transport(req) if @socket.closed? connect + elsif @last_communicated + if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC) + debug 'Conn close because of keep_alive_timeout' + @socket.close + connect + elsif @socket.io.to_io.wait_readable(0) && @socket.eof? + debug "Conn close because of EOF" + @socket.close + connect + end end - if @seems_1_0_server - req['connection'] ||= 'close' - end + if not req.response_body_permitted? and @close_on_empty_response req['connection'] ||= 'close' end + + req.update_uri address, port, use_ssl? req['host'] ||= addr_port() end def end_transport(req, res) @curr_http_version = res.http_version - if not res.body and @close_on_empty_response - D 'Conn close' + @last_communicated = nil + if @socket.closed? + debug 'Conn socket closed' + elsif not res.body and @close_on_empty_response + debug 'Conn close' @socket.close elsif keep_alive?(req, res) - D 'Conn keep-alive' - if @socket.closed? - D 'Conn (but seems 1.0 server)' - @seems_1_0_server = true - end + debug 'Conn keep-alive' + @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC) else - D 'Conn close' + debug 'Conn close' @socket.close end end def keep_alive?(req, res) - return false if /close/i =~ req['connection'].to_s - return false if @seems_1_0_server - return true if /keep-alive/i =~ res['connection'].to_s - return false if /close/i =~ res['connection'].to_s - return true if /keep-alive/i =~ res['proxy-connection'].to_s - return false if /close/i =~ res['proxy-connection'].to_s - (@curr_http_version == '1.1') - end - - # - # utils - # - - private - - def addr_port - if use_ssl? - address() + (port == HTTP.https_default_port ? '' : ":#{port()}") - else - address() + (port == HTTP.http_default_port ? '' : ":#{port()}") + return false if req.connection_close? + if @curr_http_version <= '1.0' + res.connection_keep_alive? + else # HTTP/1.1 or later + not res.connection_close? end end - def D(msg) - return unless @debug_output - @debug_output << msg - @debug_output << "\n" - end - - end - - HTTPSession = HTTP - - - # - # Header module. - # - # Provides access to @header in the mixed-into class as a hash-like - # object, except with case-insensitive keys. Also provides - # methods for accessing commonly-used header values in a more - # convenient format. - # - module HTTPHeader - - def initialize_http_header(initheader) - @header = {} - return unless initheader - initheader.each do |key, value| - warn "net/http: warning: duplicated HTTP header: #{key}" if key?(key) and $VERBOSE - @header[key.downcase] = [value.strip] - end - end - - def size #:nodoc: obsolete - @header.size - end - - alias length size #:nodoc: obsolete - - # Returns the header field corresponding to the case-insensitive key. - # For example, a key of "Content-Type" might return "text/html" - def [](key) - a = @header[key.downcase] or return nil - a.join(', ') - end - - # Sets the header field corresponding to the case-insensitive key. - def []=(key, val) - unless val - @header.delete key.downcase - return val - end - @header[key.downcase] = [val] - end - - # [Ruby 1.8.3] - # Adds header field instead of replace. - # Second argument +val+ must be a String. - # See also #[]=, #[] and #get_fields. - # - # request.add_field 'X-My-Header', 'a' - # p request['X-My-Header'] #=> "a" - # p request.get_fields('X-My-Header') #=> ["a"] - # request.add_field 'X-My-Header', 'b' - # p request['X-My-Header'] #=> "a, b" - # p request.get_fields('X-My-Header') #=> ["a", "b"] - # request.add_field 'X-My-Header', 'c' - # p request['X-My-Header'] #=> "a, b, c" - # p request.get_fields('X-My-Header') #=> ["a", "b", "c"] - # - def add_field(key, val) - if @header.key?(key.downcase) - @header[key.downcase].push val - else - @header[key.downcase] = [val] - end - end - - # [Ruby 1.8.3] - # Returns an array of header field strings corresponding to the - # case-insensitive +key+. This method allows you to get duplicated - # header fields without any processing. See also #[]. - # - # p response.get_fields('Set-Cookie') - # #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23", - # "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"] - # p response['Set-Cookie'] - # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23" - # - def get_fields(key) - return nil unless @header[key.downcase] - @header[key.downcase].dup - end - - # Returns the header field corresponding to the case-insensitive key. - # Returns the default value +args+, or the result of the block, or nil, - # if there's no header field named key. See Hash#fetch - def fetch(key, *args, &block) #:yield: +key+ - a = @header.fetch(key.downcase, *args, &block) - a.join(', ') - end - - # Iterates for each header names and values. - def each_header #:yield: +key+, +value+ - @header.each do |k,va| - yield k, va.join(', ') - end - end - - alias each each_header - - # Iterates for each header names. - def each_name(&block) #:yield: +key+ - @header.each_key(&block) - end - - alias each_key each_name - - # Iterates for each capitalized header names. - def each_capitalized_name(&block) #:yield: +key+ - @header.each_key do |k| - yield capitalize(k) - end - end - - # Iterates for each header values. - def each_value #:yield: +value+ - @header.each_value do |va| - yield va.join(', ') - end - end - - # Removes a header field. - def delete(key) - @header.delete(key.downcase) - end - - # true if +key+ header exists. - def key?(key) - @header.key?(key.downcase) - end - - # Returns a Hash consist of header names and values. - def to_hash - @header.dup - end - - # As for #each_header, except the keys are provided in capitalized form. - def each_capitalized - @header.each do |k,v| - yield capitalize(k), v.join(', ') - end - end - - alias canonical_each each_capitalized - - def capitalize(name) - name.split(/-/).map {|s| s.capitalize }.join('-') - end - private :capitalize - - # Returns an Array of Range objects which represents Range: header field, - # or +nil+ if there is no such header. - def range - return nil unless @header['range'] - self['Range'].split(/,/).map {|spec| - m = /bytes\s*=\s*(\d+)?\s*-\s*(\d+)?/i.match(spec) or - raise HTTPHeaderSyntaxError, "wrong Range: #{spec}" - d1 = m[1].to_i - d2 = m[2].to_i - if m[1] and m[2] then d1..d2 - elsif m[1] then d1..-1 - elsif m[2] then -d2..-1 - else - raise HTTPHeaderSyntaxError, 'range is not specified' - end - } - end - - # Set Range: header from Range (arg r) or beginning index and - # length from it (arg idx&len). - # - # req.range = (0..1023) - # req.set_range 0, 1023 - # - def set_range(r, e = nil) - unless r - @header.delete 'range' - return r - end - r = (r...r+e) if e - case r - when Numeric - n = r.to_i - rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") - when Range - first = r.first - last = r.last - last -= 1 if r.exclude_end? - if last == -1 - rangestr = (first > 0 ? "#{first}-" : "-#{-first}") - else - raise HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 - raise HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 - raise HTTPHeaderSyntaxError, 'must be .first < .last' if first > last - rangestr = "#{first}-#{last}" + def sspi_auth?(res) + return false unless @sspi_enabled + if res.kind_of?(HTTPProxyAuthenticationRequired) and + proxy? and res["Proxy-Authenticate"].include?("Negotiate") + begin + require 'win32/sspi' + true + rescue LoadError + false end else - raise TypeError, 'Range/Integer is required' + false end - @header['range'] = ["bytes=#{rangestr}"] - r end - alias range= set_range - - # Returns an Integer object which represents the Content-Length: header field - # or +nil+ if that field is not provided. - def content_length - return nil unless key?('Content-Length') - len = self['Content-Length'].slice(/\d+/) or - raise HTTPHeaderSyntaxError, 'wrong Content-Length format' - len.to_i - end - - def content_length=(len) - unless len - @header.delete 'content-length' - return nil - end - @header['content-length'] = [len.to_i.to_s] - end - - # Returns "true" if the "transfer-encoding" header is present and - # set to "chunked". This is an HTTP/1.1 feature, allowing the - # the content to be sent in "chunks" without at the outset - # stating the entire content length. - def chunked? - return false unless @header['transfer-encoding'] - field = self['Transfer-Encoding'] - (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false - end - - # Returns a Range object which represents Content-Range: header field. - # This indicates, for a partial entity body, where this fragment - # fits inside the full entity body, as range of byte offsets. - def content_range - return nil unless @header['content-range'] - m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or - raise HTTPHeaderSyntaxError, 'wrong Content-Range format' - m[1].to_i .. m[2].to_i + 1 - end - - # The length of the range represented in Content-Range: header. - def range_length - r = content_range() or return nil - r.end - r.begin - end - - # Returns a content type string such as "text/html". - # This method returns nil if Content-Type: header field does not exist. - def content_type - return nil unless main_type() - if sub_type() - then "#{main_type()}/#{sub_type()}" - else main_type() - end - end - - # Returns a content type string such as "text". - # This method returns nil if Content-Type: header field does not exist. - def main_type - return nil unless @header['content-type'] - self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip - end - - # Returns a content type string such as "html". - # This method returns nil if Content-Type: header field does not exist - # or sub-type is not given (e.g. "Content-Type: text"). - def sub_type - return nil unless @header['content-type'] - main, sub = *self['Content-Type'].split(';').first.to_s.split('/') - return nil unless sub - sub.strip - end - - # Returns content type parameters as a Hash as like - # {"charset" => "iso-2022-jp"}. - def type_params - result = {} - list = self['Content-Type'].to_s.split(';') - list.shift - list.each do |param| - k, v = *param.split('=', 2) - result[k.strip] = v.strip - end - result - end - - # Set Content-Type: header field by +type+ and +params+. - # +type+ must be a String, +params+ must be a Hash. - def set_content_type(type, params = {}) - @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] + def sspi_auth(req) + n = Win32::SSPI::NegotiateAuth.new + req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}" + # Some versions of ISA will close the connection if this isn't present. + req["Connection"] = "Keep-Alive" + req["Proxy-Connection"] = "Keep-Alive" + res = transport_request(req) + authphrase = res["Proxy-Authenticate"] or return res + req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}" + rescue => err + raise HTTPAuthenticationError.new('HTTP authentication failed', err) end - alias content_type= set_content_type - - # Set header fields and a body from HTML form data. - # +params+ should be a Hash containing HTML form data. - # Optional argument +sep+ means data record separator. # - # This method also set Content-Type: header field to - # application/x-www-form-urlencoded. - def set_form_data(params, sep = '&') - self.body = params.map {|k,v| "#{urlencode(k.to_s)}=#{urlencode(v.to_s)}" }.join(sep) - self.content_type = 'application/x-www-form-urlencoded' - end - - alias form_data= set_form_data - - def urlencode(str) - str.gsub(/[^a-zA-Z0-9_\.\-]/n) {|s| sprintf('%%%02x', s[0]) } - end - private :urlencode - - # Set the Authorization: header for "Basic" authorization. - def basic_auth(account, password) - @header['authorization'] = [basic_encode(account, password)] - end - - # Set Proxy-Authorization: header for "Basic" authorization. - def proxy_basic_auth(account, password) - @header['proxy-authorization'] = [basic_encode(account, password)] - end - - def basic_encode(account, password) - 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n") - end - private :basic_encode - - end - - - # - # Parent of HTTPRequest class. Do not use this directly; use - # a subclass of HTTPRequest. - # - # Mixes in the HTTPHeader module. - # - class HTTPGenericRequest - - include HTTPHeader - - def initialize(m, reqbody, resbody, path, initheader = nil) - @method = m - @request_has_body = reqbody - @response_has_body = resbody - raise ArgumentError, "HTTP request path is empty" if path.empty? - @path = path - initialize_http_header initheader - self['Accept'] ||= '*/*' - @body = nil - @body_stream = nil - end - - attr_reader :method - attr_reader :path - - def inspect - "\#<#{self.class} #{@method}>" - end - - def request_body_permitted? - @request_has_body - end - - def response_body_permitted? - @response_has_body - end - - def body_exist? - warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE - response_body_permitted? - end - - attr_reader :body - - def body=(str) - @body = str - @body_stream = nil - str - end - - attr_reader :body_stream - - def body_stream=(input) - @body = nil - @body_stream = input - input - end - - def set_body_internal(str) #:nodoc: internal use only - raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream) - self.body = str if str - end - - # - # write + # utils # - def exec(sock, ver, path) #:nodoc: internal use only - if @body - send_request_with_body sock, ver, path, @body - elsif @body_stream - send_request_with_body_stream sock, ver, path, @body_stream - else - write_header sock, ver, path - end - end - private - def send_request_with_body(sock, ver, path, body) - self.content_length = body.length - delete 'Transfer-Encoding' - supply_default_content_type - write_header sock, ver, path - sock.write body - end - - def send_request_with_body_stream(sock, ver, path, f) - unless content_length() or chunked? - raise ArgumentError, - "Content-Length not given and Transfer-Encoding is not `chunked'" - end - supply_default_content_type - write_header sock, ver, path - if chunked? - while s = f.read(1024) - sock.write(sprintf("%x\r\n", s.length) << s << "\r\n") - end - sock.write "0\r\n\r\n" - else - while s = f.read(1024) - sock.write s - end - end - end - - def supply_default_content_type - return if content_type() - warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE - set_content_type 'application/x-www-form-urlencoded' - end - - def write_header(sock, ver, path) - buf = "#{@method} #{path} HTTP/#{ver}\r\n" - each_capitalized do |k,v| - buf << "#{k}: #{v}\r\n" - end - buf << "\r\n" - sock.write buf - end - - end - - - # - # HTTP request class. This class wraps request header and entity path. - # You *must* use its subclass, Net::HTTP::Get, Post, Head. - # - class HTTPRequest < HTTPGenericRequest - - # Creates HTTP request object. - def initialize(path, initheader = nil) - super self.class::METHOD, - self.class::REQUEST_HAS_BODY, - self.class::RESPONSE_HAS_BODY, - path, initheader - end - end - - - class HTTP # reopen - # - # HTTP 1.1 methods --- RFC2616 - # - - class Get < HTTPRequest - METHOD = 'GET' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - class Head < HTTPRequest - METHOD = 'HEAD' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = false - end - - class Post < HTTPRequest - METHOD = 'POST' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Put < HTTPRequest - METHOD = 'PUT' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Delete < HTTPRequest - METHOD = 'DELETE' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - class Options < HTTPRequest - METHOD = 'OPTIONS' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = false - end - - class Trace < HTTPRequest - METHOD = 'TRACE' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - # - # WebDAV methods --- RFC2518 - # - - class Propfind < HTTPRequest - METHOD = 'PROPFIND' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Proppatch < HTTPRequest - METHOD = 'PROPPATCH' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Mkcol < HTTPRequest - METHOD = 'MKCOL' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Copy < HTTPRequest - METHOD = 'COPY' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - class Move < HTTPRequest - METHOD = 'MOVE' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end - - class Lock < HTTPRequest - METHOD = 'LOCK' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true - end - - class Unlock < HTTPRequest - METHOD = 'UNLOCK' - REQUEST_HAS_BODY = true - RESPONSE_HAS_BODY = true + def addr_port + addr = address + addr = "[#{addr}]" if addr.include?(":") + default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port + default_port == port ? addr : "#{addr}:#{port}" end - end - - ### - ### Response - ### - - # HTTP exception class. - # You must use its subclasses. - module HTTPExceptions - def initialize(msg, res) #:nodoc: - super msg - @response = res + # Adds a message to debugging output + def debug(msg) + return unless @debug_output + @debug_output << msg + @debug_output << "\n" end - attr_reader :response - alias data response #:nodoc: obsolete - end - class HTTPError < ProtocolError - include HTTPExceptions - end - class HTTPRetriableError < ProtoRetriableError - include HTTPExceptions - end - class HTTPServerException < ProtoServerError - # We cannot use the name "HTTPServerError", it is the name of the response. - include HTTPExceptions - end - class HTTPFatalError < ProtoFatalError - include HTTPExceptions - end - - - # HTTP response class. This class wraps response header and entity. - # Mixes in the HTTPHeader module, which provides access to response - # header values both via hash-like methods and individual readers. - # Note that each possible HTTP response code defines its own - # HTTPResponse subclass. These are listed below. - # All classes are - # defined under the Net module. Indentation indicates inheritance. - # - # xxx HTTPResponse - # - # 1xx HTTPInformation - # 100 HTTPContinue - # 101 HTTPSwitchProtocol - # - # 2xx HTTPSuccess - # 200 HTTPOK - # 201 HTTPCreated - # 202 HTTPAccepted - # 203 HTTPNonAuthoritativeInformation - # 204 HTTPNoContent - # 205 HTTPResetContent - # 206 HTTPPartialContent - # - # 3xx HTTPRedirection - # 300 HTTPMultipleChoice - # 301 HTTPMovedPermanently - # 302 HTTPFound - # 303 HTTPSeeOther - # 304 HTTPNotModified - # 305 HTTPUseProxy - # 307 HTTPTemporaryRedirect - # - # 4xx HTTPClientError - # 400 HTTPBadRequest - # 401 HTTPUnauthorized - # 402 HTTPPaymentRequired - # 403 HTTPForbidden - # 404 HTTPNotFound - # 405 HTTPMethodNotAllowed - # 406 HTTPNotAcceptable - # 407 HTTPProxyAuthenticationRequired - # 408 HTTPRequestTimeOut - # 409 HTTPConflict - # 410 HTTPGone - # 411 HTTPLengthRequired - # 412 HTTPPreconditionFailed - # 413 HTTPRequestEntityTooLarge - # 414 HTTPRequestURITooLong - # 415 HTTPUnsupportedMediaType - # 416 HTTPRequestedRangeNotSatisfiable - # 417 HTTPExpectationFailed - # - # 5xx HTTPServerError - # 500 HTTPInternalServerError - # 501 HTTPNotImplemented - # 502 HTTPBadGateway - # 503 HTTPServiceUnavailable - # 504 HTTPGatewayTimeOut - # 505 HTTPVersionNotSupported - # - # xxx HTTPUnknownResponse - # - class HTTPResponse - # true if the response has body. - def HTTPResponse.body_permitted? - self::HAS_BODY - end - - def HTTPResponse.exception_type # :nodoc: internal use only - self::EXCEPTION_TYPE - end - end # reopened after - - # :stopdoc: - - class HTTPUnknownResponse < HTTPResponse - HAS_BODY = true - EXCEPTION_TYPE = HTTPError - end - class HTTPInformation < HTTPResponse # 1xx - HAS_BODY = false - EXCEPTION_TYPE = HTTPError - end - class HTTPSuccess < HTTPResponse # 2xx - HAS_BODY = true - EXCEPTION_TYPE = HTTPError - end - class HTTPRedirection < HTTPResponse # 3xx - HAS_BODY = true - EXCEPTION_TYPE = HTTPRetriableError - end - class HTTPClientError < HTTPResponse # 4xx - HAS_BODY = true - EXCEPTION_TYPE = HTTPServerException # for backward compatibility - end - class HTTPServerError < HTTPResponse # 5xx - HAS_BODY = true - EXCEPTION_TYPE = HTTPFatalError # for backward compatibility - end - - class HTTPContinue < HTTPInformation # 100 - HAS_BODY = false - end - class HTTPSwitchProtocol < HTTPInformation # 101 - HAS_BODY = false - end - - class HTTPOK < HTTPSuccess # 200 - HAS_BODY = true - end - class HTTPCreated < HTTPSuccess # 201 - HAS_BODY = true - end - class HTTPAccepted < HTTPSuccess # 202 - HAS_BODY = true - end - class HTTPNonAuthoritativeInformation < HTTPSuccess # 203 - HAS_BODY = true - end - class HTTPNoContent < HTTPSuccess # 204 - HAS_BODY = false - end - class HTTPResetContent < HTTPSuccess # 205 - HAS_BODY = false - end - class HTTPPartialContent < HTTPSuccess # 206 - HAS_BODY = true - end - - class HTTPMultipleChoice < HTTPRedirection # 300 - HAS_BODY = true - end - class HTTPMovedPermanently < HTTPRedirection # 301 - HAS_BODY = true - end - class HTTPFound < HTTPRedirection # 302 - HAS_BODY = true - end - HTTPMovedTemporarily = HTTPFound - class HTTPSeeOther < HTTPRedirection # 303 - HAS_BODY = true - end - class HTTPNotModified < HTTPRedirection # 304 - HAS_BODY = false - end - class HTTPUseProxy < HTTPRedirection # 305 - HAS_BODY = false - end - # 306 unused - class HTTPTemporaryRedirect < HTTPRedirection # 307 - HAS_BODY = true - end - class HTTPBadRequest < HTTPClientError # 400 - HAS_BODY = true - end - class HTTPUnauthorized < HTTPClientError # 401 - HAS_BODY = true - end - class HTTPPaymentRequired < HTTPClientError # 402 - HAS_BODY = true - end - class HTTPForbidden < HTTPClientError # 403 - HAS_BODY = true - end - class HTTPNotFound < HTTPClientError # 404 - HAS_BODY = true - end - class HTTPMethodNotAllowed < HTTPClientError # 405 - HAS_BODY = true - end - class HTTPNotAcceptable < HTTPClientError # 406 - HAS_BODY = true - end - class HTTPProxyAuthenticationRequired < HTTPClientError # 407 - HAS_BODY = true - end - class HTTPRequestTimeOut < HTTPClientError # 408 - HAS_BODY = true - end - class HTTPConflict < HTTPClientError # 409 - HAS_BODY = true - end - class HTTPGone < HTTPClientError # 410 - HAS_BODY = true - end - class HTTPLengthRequired < HTTPClientError # 411 - HAS_BODY = true - end - class HTTPPreconditionFailed < HTTPClientError # 412 - HAS_BODY = true - end - class HTTPRequestEntityTooLarge < HTTPClientError # 413 - HAS_BODY = true - end - class HTTPRequestURITooLong < HTTPClientError # 414 - HAS_BODY = true - end - HTTPRequestURITooLarge = HTTPRequestURITooLong - class HTTPUnsupportedMediaType < HTTPClientError # 415 - HAS_BODY = true - end - class HTTPRequestedRangeNotSatisfiable < HTTPClientError # 416 - HAS_BODY = true - end - class HTTPExpectationFailed < HTTPClientError # 417 - HAS_BODY = true - end - - class HTTPInternalServerError < HTTPServerError # 500 - HAS_BODY = true - end - class HTTPNotImplemented < HTTPServerError # 501 - HAS_BODY = true - end - class HTTPBadGateway < HTTPServerError # 502 - HAS_BODY = true - end - class HTTPServiceUnavailable < HTTPServerError # 503 - HAS_BODY = true - end - class HTTPGatewayTimeOut < HTTPServerError # 504 - HAS_BODY = true - end - class HTTPVersionNotSupported < HTTPServerError # 505 - HAS_BODY = true + alias_method :D, :debug end - # :startdoc: - - - class HTTPResponse # reopen - - CODE_CLASS_TO_OBJ = { - '1' => HTTPInformation, - '2' => HTTPSuccess, - '3' => HTTPRedirection, - '4' => HTTPClientError, - '5' => HTTPServerError - } - CODE_TO_OBJ = { - '100' => HTTPContinue, - '101' => HTTPSwitchProtocol, - - '200' => HTTPOK, - '201' => HTTPCreated, - '202' => HTTPAccepted, - '203' => HTTPNonAuthoritativeInformation, - '204' => HTTPNoContent, - '205' => HTTPResetContent, - '206' => HTTPPartialContent, - - '300' => HTTPMultipleChoice, - '301' => HTTPMovedPermanently, - '302' => HTTPFound, - '303' => HTTPSeeOther, - '304' => HTTPNotModified, - '305' => HTTPUseProxy, - '307' => HTTPTemporaryRedirect, - - '400' => HTTPBadRequest, - '401' => HTTPUnauthorized, - '402' => HTTPPaymentRequired, - '403' => HTTPForbidden, - '404' => HTTPNotFound, - '405' => HTTPMethodNotAllowed, - '406' => HTTPNotAcceptable, - '407' => HTTPProxyAuthenticationRequired, - '408' => HTTPRequestTimeOut, - '409' => HTTPConflict, - '410' => HTTPGone, - '411' => HTTPLengthRequired, - '412' => HTTPPreconditionFailed, - '413' => HTTPRequestEntityTooLarge, - '414' => HTTPRequestURITooLong, - '415' => HTTPUnsupportedMediaType, - '416' => HTTPRequestedRangeNotSatisfiable, - '417' => HTTPExpectationFailed, - - '500' => HTTPInternalServerError, - '501' => HTTPNotImplemented, - '502' => HTTPBadGateway, - '503' => HTTPServiceUnavailable, - '504' => HTTPGatewayTimeOut, - '505' => HTTPVersionNotSupported - } - - class << HTTPResponse - def read_new(sock) #:nodoc: internal use only - httpv, code, msg = read_status_line(sock) - res = response_class(code).new(httpv, code, msg) - each_response_header(sock) do |k,v| - res.add_field k, v - end - res - end +end - private +require_relative 'http/exceptions' - def read_status_line(sock) - str = sock.readline - m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in.match(str) or - raise HTTPBadResponse, "wrong status line: #{str.dump}" - m.captures - end +require_relative 'http/header' - def response_class(code) - CODE_TO_OBJ[code] or - CODE_CLASS_TO_OBJ[code[0,1]] or - HTTPUnknownResponse - end +require_relative 'http/generic_request' +require_relative 'http/request' +require_relative 'http/requests' - def each_response_header(sock) - while true - line = sock.readuntil("\n", true).sub(/\s+\z/, '') - break if line.empty? - m = /\A([^:]+):\s*/.match(line) or - raise HTTPBadResponse, 'wrong header line format' - yield m[1], m.post_match - end - end - end - - # next is to fix bug in RDoc, where the private inside class << self - # spills out. - public - - include HTTPHeader - - def initialize(httpv, code, msg) #:nodoc: internal use only - @http_version = httpv - @code = code - @message = msg - initialize_http_header nil - @body = nil - @read = false - end - - # The HTTP version supported by the server. - attr_reader :http_version - - # HTTP result code string. For example, '302'. You can also - # determine the response type by which response subclass the - # response object is an instance of. - attr_reader :code - - # HTTP result message. For example, 'Not Found'. - attr_reader :message - alias msg message # :nodoc: obsolete - - def inspect - "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" - end - - # For backward compatibility. - # To allow Net::HTTP 1.1 style assignment - # e.g. - # response, body = Net::HTTP.get(....) - # - def to_ary - warn "net/http.rb: warning: Net::HTTP v1.1 style assignment found at #{caller(1)[0]}; use `response = http.get(...)' instead." if $VERBOSE - res = self.dup - class << res - undef to_ary - end - [res, res.body] - end - - # - # response <-> exception relationship - # - - def code_type #:nodoc: - self.class - end - - def error! #:nodoc: - raise error_type().new(@code + ' ' + @message.dump, self) - end - - def error_type #:nodoc: - self.class::EXCEPTION_TYPE - end - - # Raises HTTP error if the response is not 2xx. - def value - error! unless self.kind_of?(HTTPSuccess) - end - - # - # header (for backward compatibility only; DO NOT USE) - # - - def response #:nodoc: - warn "#{caller(1)[0]}: warning: HTTPResponse#response is obsolete" if $VERBOSE - self - end - - def header #:nodoc: - warn "#{caller(1)[0]}: warning: HTTPResponse#header is obsolete" if $VERBOSE - self - end - - def read_header #:nodoc: - warn "#{caller(1)[0]}: warning: HTTPResponse#read_header is obsolete" if $VERBOSE - self - end - - # - # body - # - - def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only - @socket = sock - @body_exist = reqmethodallowbody && self.class.body_permitted? - begin - yield - self.body # ensure to read body - ensure - @socket = nil - end - end - - # Gets entity body. If the block given, yields it to +block+. - # The body is provided in fragments, as it is read in from the socket. - # - # Calling this method a second or subsequent time will return the - # already read string. - # - # http.request_get('/index.html') {|res| - # puts res.read_body - # } - # - # http.request_get('/index.html') {|res| - # p res.read_body.object_id # 538149362 - # p res.read_body.object_id # 538149362 - # } - # - # # using iterator - # http.request_get('/index.html') {|res| - # res.read_body do |segment| - # print segment - # end - # } - # - def read_body(dest = nil, &block) - if @read - raise IOError, "#{self.class}\#read_body called twice" if dest or block - return @body - end - to = procdest(dest, block) - stream_check - if @body_exist - read_body_0 to - @body = to - else - @body = nil - end - @read = true - - @body - end - - # Returns the entity body. - # - # Calling this method a second or subsequent time will return the - # already read string. - # - # http.request_get('/index.html') {|res| - # puts res.body - # } - # - # http.request_get('/index.html') {|res| - # p res.body.object_id # 538149362 - # p res.body.object_id # 538149362 - # } - # - def body - read_body() - end - - alias entity body #:nodoc: obsolete - - private - - def read_body_0(dest) - if chunked? - read_chunked dest - return - end - clen = content_length() - if clen - @socket.read clen, dest, true # ignore EOF - return - end - clen = range_length() - if clen - @socket.read clen, dest - return - end - @socket.read_all dest - end - - def read_chunked(dest) - len = nil - total = 0 - while true - line = @socket.readline - hexlen = line.slice(/[0-9a-fA-F]+/) or - raise HTTPBadResponse, "wrong chunk size line: #{line}" - len = hexlen.hex - break if len == 0 - @socket.read len, dest; total += len - @socket.read 2 # \r\n - end - until @socket.readline.empty? - # none - end - end - - def stream_check - raise IOError, 'attempt to read body out of block' if @socket.closed? - end - - def procdest(dest, block) - raise ArgumentError, 'both arg and block given for HTTP method' \ - if dest and block - if block - ReadAdapter.new(block) - else - dest || '' - end - end - - end - - - # :enddoc: - - #-- - # for backward compatibility - class HTTP - ProxyMod = ProxyDelta - end - module NetPrivate - HTTPRequest = ::Net::HTTPRequest - end +require_relative 'http/response' +require_relative 'http/responses' - HTTPInformationCode = HTTPInformation - HTTPSuccessCode = HTTPSuccess - HTTPRedirectionCode = HTTPRedirection - HTTPRetriableCode = HTTPRedirection - HTTPClientErrorCode = HTTPClientError - HTTPFatalErrorCode = HTTPClientError - HTTPServerErrorCode = HTTPServerError - HTTPResponceReceiver = HTTPResponse +require_relative 'http/proxy_delta' -end # module Net +require_relative 'http/backward' diff --git a/lib/net/http/backward.rb b/lib/net/http/backward.rb new file mode 100644 index 0000000000..691e41e4f1 --- /dev/null +++ b/lib/net/http/backward.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: false +# for backward compatibility + +# :enddoc: + +class Net::HTTP + ProxyMod = ProxyDelta + deprecate_constant :ProxyMod +end + +module Net::NetPrivate + HTTPRequest = ::Net::HTTPRequest + deprecate_constant :HTTPRequest +end + +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 new file mode 100644 index 0000000000..9c425cae16 --- /dev/null +++ b/lib/net/http/exceptions.rb @@ -0,0 +1,34 @@ +# 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 + + class HTTPError < ProtocolError + include HTTPExceptions + end + + class HTTPRetriableError < ProtoRetriableError + include HTTPExceptions + end + + class HTTPClientException < ProtoServerError + include HTTPExceptions + end + + class HTTPFatalError < ProtoFatalError + include HTTPExceptions + end + + # We cannot use the name "HTTPServerError", it is the name of the response. + HTTPServerException = HTTPClientException # :nodoc: + deprecate_constant(:HTTPServerException) +end diff --git a/lib/net/http/generic_request.rb b/lib/net/http/generic_request.rb new file mode 100644 index 0000000000..d56835c76f --- /dev/null +++ b/lib/net/http/generic_request.rb @@ -0,0 +1,338 @@ +# 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 Net::HTTPHeader module to provide easier access to HTTP headers. +# +class Net::HTTPGenericRequest + + include Net::HTTPHeader + + def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) + @method = m + @request_has_body = reqbody + @response_has_body = resbody + + if URI === uri_or_path then + raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path + hostname = uri_or_path.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 + @path = uri_or_path.request_uri + raise ArgumentError, "no HTTP request path given" unless @path + else + @uri = nil + host = nil + raise ArgumentError, "no HTTP request path given" unless uri_or_path + raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty? + @path = uri_or_path.dup + end + + @decode_content = false + + if Net::HTTP::HAVE_ZLIB then + if !initheader || + !initheader.keys.any? { |k| + %w[accept-encoding range].include? k.downcase + } then + @decode_content = true if @response_has_body + initheader = initheader ? initheader.dup : {} + initheader["accept-encoding"] = + "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" + end + end + + initialize_http_header initheader + self['Accept'] ||= '*/*' + self['User-Agent'] ||= 'Ruby' + self['Host'] ||= host if host + @body = nil + @body_stream = nil + @body_data = nil + end + + attr_reader :method + attr_reader :path + attr_reader :uri + + # Automatically set to false if the user sets the Accept-Encoding header. + # This indicates they wish to handle Content-encoding in responses + # themselves. + attr_reader :decode_content + + def inspect + "\#<#{self.class} #{@method}>" + end + + ## + # Don't automatically decode response content-encoding if the user indicates + # they want to handle it. + + def []=(key, val) # :nodoc: + @decode_content = false if key.downcase == 'accept-encoding' + + super key, val + end + + def request_body_permitted? + @request_has_body + end + + def response_body_permitted? + @response_has_body + end + + def body_exist? + warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE + response_body_permitted? + end + + attr_reader :body + + def body=(str) + @body = str + @body_stream = nil + @body_data = nil + str + end + + attr_reader :body_stream + + def body_stream=(input) + @body = nil + @body_stream = input + @body_data = nil + input + end + + def set_body_internal(str) #:nodoc: internal use only + raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream) + self.body = str if str + if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted? + self.body = '' + end + end + + # + # write + # + + def exec(sock, ver, path) #:nodoc: internal use only + if @body + send_request_with_body sock, ver, path, @body + elsif @body_stream + send_request_with_body_stream sock, ver, path, @body_stream + elsif @body_data + send_request_with_body_data sock, ver, path, @body_data + else + write_header sock, ver, path + end + end + + def update_uri(addr, port, ssl) # :nodoc: internal use only + # reflect the connection and @path to @uri + return unless @uri + + if ssl + scheme = 'https'.freeze + klass = URI::HTTPS + else + scheme = 'http'.freeze + klass = URI::HTTP + end + + if host = self['host'] + host.sub!(/:.*/m, ''.freeze) + elsif host = @uri.host + else + host = addr + end + # convert the class of the URI + if @uri.is_a?(klass) + @uri.host = host + @uri.port = port + else + @uri = klass.new( + scheme, @uri.userinfo, + host, port, nil, + @uri.path, nil, @uri.query, nil) + end + end + + private + + class Chunker #:nodoc: + def initialize(sock) + @sock = sock + @prev = nil + end + + def write(buf) + # avoid memcpy() of buf, buf can huge and eat memory bandwidth + rv = buf.bytesize + @sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n") + rv + end + + def finish + @sock.write("0\r\n\r\n") + end + end + + def send_request_with_body(sock, ver, path, body) + self.content_length = body.bytesize + delete 'Transfer-Encoding' + supply_default_content_type + write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout + sock.write body + end + + def send_request_with_body_stream(sock, ver, path, f) + unless content_length() or chunked? + raise ArgumentError, + "Content-Length not given and Transfer-Encoding is not `chunked'" + end + supply_default_content_type + write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout + if chunked? + chunker = Chunker.new(sock) + IO.copy_stream(f, chunker) + chunker.finish + else + IO.copy_stream(f, sock) + end + end + + def send_request_with_body_data(sock, ver, path, params) + if /\Amultipart\/form-data\z/i !~ self.content_type + self.content_type = 'application/x-www-form-urlencoded' + return send_request_with_body(sock, ver, path, URI.encode_www_form(params)) + end + + opt = @form_option.dup + require 'securerandom' unless defined?(SecureRandom) + opt[:boundary] ||= SecureRandom.urlsafe_base64(40) + self.set_content_type(self.content_type, boundary: opt[:boundary]) + if chunked? + write_header sock, ver, path + encode_multipart_form_data(sock, params, opt) + else + require 'tempfile' + file = Tempfile.new('multipart') + file.binmode + encode_multipart_form_data(file, params, opt) + file.rewind + self.content_length = file.size + write_header sock, ver, path + IO.copy_stream(file, sock) + file.close(true) + end + end + + def encode_multipart_form_data(out, params, opt) + charset = opt[:charset] + boundary = opt[:boundary] + require 'securerandom' unless defined?(SecureRandom) + boundary ||= SecureRandom.urlsafe_base64(40) + chunked_p = chunked? + + buf = '' + params.each do |key, value, h={}| + key = quote_string(key, charset) + filename = + h.key?(:filename) ? h[:filename] : + value.respond_to?(:to_path) ? File.basename(value.to_path) : + nil + + buf << "--#{boundary}\r\n" + if filename + filename = quote_string(filename, charset) + type = h[:content_type] || 'application/octet-stream' + buf << "Content-Disposition: form-data; " \ + "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \ + "Content-Type: #{type}\r\n\r\n" + if !out.respond_to?(:write) || !value.respond_to?(:read) + # if +out+ is not an IO or +value+ is not an IO + buf << (value.respond_to?(:read) ? value.read : value) + elsif value.respond_to?(:size) && chunked_p + # if +out+ is an IO and +value+ is a File, use IO.copy_stream + flush_buffer(out, buf, chunked_p) + out << "%x\r\n" % value.size if chunked_p + IO.copy_stream(value, out) + out << "\r\n" if chunked_p + else + # +out+ is an IO, and +value+ is not a File but an IO + flush_buffer(out, buf, chunked_p) + 1 while flush_buffer(out, value.read(4096), chunked_p) + end + else + # non-file field: + # HTML5 says, "The parts of the generated multipart/form-data + # resource that correspond to non-file fields must not have a + # Content-Type header specified." + buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" + buf << (value.respond_to?(:read) ? value.read : value) + end + buf << "\r\n" + end + buf << "--#{boundary}--\r\n" + flush_buffer(out, buf, chunked_p) + out << "0\r\n\r\n" if chunked_p + end + + def quote_string(str, charset) + str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset + str.gsub(/[\\"]/, '\\\\\&') + end + + def flush_buffer(out, buf, chunked_p) + return unless buf + out << "%x\r\n"%buf.bytesize if chunked_p + out << buf + out << "\r\n" if chunked_p + buf.clear + end + + def supply_default_content_type + return if content_type() + 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 + + ## + # Waits up to the continue timeout for a response from the server provided + # we're speaking HTTP 1.1 and are expecting a 100-continue response. + + def wait_for_continue(sock, ver) + if ver >= '1.1' and @header['expect'] and + @header['expect'].include?('100-continue') + if sock.io.to_io.wait_readable(sock.continue_timeout) + res = Net::HTTPResponse.read_new(sock) + unless res.kind_of?(Net::HTTPContinue) + res.decode_content = @decode_content + throw :response, res + end + end + end + end + + def write_header(sock, ver, path) + reqline = "#{@method} #{path} HTTP/#{ver}" + if /[\r\n]/ =~ reqline + raise ArgumentError, "A Request-Line must not contain CR or LF" + end + buf = "" + buf << reqline << "\r\n" + each_capitalized do |k,v| + buf << "#{k}: #{v}\r\n" + end + buf << "\r\n" + sock.write buf + end + +end + diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb new file mode 100644 index 0000000000..b0ec4b0625 --- /dev/null +++ b/lib/net/http/header.rb @@ -0,0 +1,517 @@ +# frozen_string_literal: false +# The HTTPHeader module defines methods for reading and writing +# HTTP headers. +# +# It is used as a mixin by other classes, to provide hash-like +# access to HTTP header values. Unlike raw hash access, HTTPHeader +# provides access via case-insensitive keys. It also provides +# methods for accessing commonly-used HTTP header values in more +# convenient formats. +# +module Net::HTTPHeader + + def initialize_http_header(initheader) + @header = {} + return unless initheader + initheader.each do |key, value| + warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE + if value.nil? + warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE + else + value = value.strip # raise error for invalid byte sequences + if value.count("\r\n") > 0 + raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF" + end + @header[key.downcase.to_s] = [value] + end + end + end + + def size #:nodoc: obsolete + @header.size + end + + alias length size #:nodoc: obsolete + + # Returns the 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.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.to_s + return val + end + set_field(key, val) + end + + # [Ruby 1.8.3] + # Adds a value to a named header field, instead of replacing its value. + # Second argument +val+ must be a String. + # See also #[]=, #[] and #get_fields. + # + # request.add_field 'X-My-Header', 'a' + # p request['X-My-Header'] #=> "a" + # p request.get_fields('X-My-Header') #=> ["a"] + # request.add_field 'X-My-Header', 'b' + # p request['X-My-Header'] #=> "a, b" + # p request.get_fields('X-My-Header') #=> ["a", "b"] + # request.add_field 'X-My-Header', 'c' + # p request['X-My-Header'] #=> "a, b, c" + # p request.get_fields('X-My-Header') #=> ["a", "b", "c"] + # + def add_field(key, val) + 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 + 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 + + # [Ruby 1.8.3] + # Returns an array of header field strings corresponding to the + # case-insensitive +key+. This method allows you to get duplicated + # header fields without any processing. See also #[]. + # + # p response.get_fields('Set-Cookie') + # #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23", + # "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"] + # p response['Set-Cookie'] + # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23" + # + def get_fields(key) + stringified_downcased_key = key.downcase.to_s + return nil unless @header[stringified_downcased_key] + @header[stringified_downcased_key].dup + end + + # Returns the header field corresponding to the case-insensitive key. + # Returns the default value +args+, or the result of the block, or + # raises an IndexError if there's no header field named +key+ + # See Hash#fetch + def fetch(key, *args, &block) #:yield: +key+ + 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__) { @header.size } + @header.each do |k,va| + yield k, va.join(', ') + end + end + + alias each each_header + + # 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__) { @header.size } + @header.each_key(&block) + end + + alias each_key each_name + + # Iterates through the header names in the header, passing + # capitalized header names to the code block. + # + # 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__) { @header.size } + @header.each_key do |k| + yield capitalize(k) + end + end + + # 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__) { @header.size } + @header.each_value do |va| + yield va.join(', ') + end + end + + # Removes a header field, specified by case-insensitive key. + def delete(key) + @header.delete(key.downcase.to_s) + end + + # true if +key+ header exists. + def key?(key) + @header.key?(key.downcase.to_s) + end + + # Returns a Hash consisting of header names and array of values. + # e.g. + # {"cache-control" => ["private"], + # "content-type" => ["text/html"], + # "date" => ["Wed, 22 Jun 2005 22:11:50 GMT"]} + def to_hash + @header.dup + end + + # As for #each_header, except the keys are provided in capitalized form. + # + # 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__) { @header.size } + @header.each do |k,v| + yield capitalize(k), v.join(', ') + end + end + + alias canonical_each each_capitalized + + def capitalize(name) + name.to_s.split(/-/).map {|s| s.capitalize }.join('-') + end + private :capitalize + + # Returns an Array of Range objects which represent the Range: + # HTTP header field, or +nil+ if there is no such header. + def range + return nil unless @header['range'] + + value = self['Range'] + # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec ) + # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] ) + # corrected collected ABNF + # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1 + # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C + # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5 + unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value + raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'" + end + + byte_range_set = $1 + result = byte_range_set.split(/,/).map {|spec| + m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or + raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'" + d1 = m[1].to_i + d2 = m[2].to_i + if m[1] and m[2] + if d1 > d2 + raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'" + end + d1..d2 + elsif m[1] + d1..-1 + elsif m[2] + -d2..-1 + else + raise Net::HTTPHeaderSyntaxError, 'range is not specified' + end + } + # if result.empty? + # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec + # but above regexp already denies it. + if result.size == 1 && result[0].begin == 0 && result[0].end == -1 + raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length' + end + result + end + + # Sets the HTTP Range: header. + # Accepts either a Range object as a single argument, + # or a beginning index and a length from that index. + # Example: + # + # req.range = (0..1023) + # req.set_range 0, 1023 + # + def set_range(r, e = nil) + unless r + @header.delete 'range' + return r + end + r = (r...r+e) if e + case r + when Numeric + n = r.to_i + rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") + when Range + first = r.first + last = r.end + last -= 1 if r.exclude_end? + if last == -1 + rangestr = (first > 0 ? "#{first}-" : "-#{-first}") + else + raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 + raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 + raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last + rangestr = "#{first}-#{last}" + end + else + raise TypeError, 'Range/Integer is required' + end + @header['range'] = ["bytes=#{rangestr}"] + r + end + + alias range= set_range + + # Returns an Integer object which represents the HTTP Content-Length: + # header field, or +nil+ if that field was not provided. + def content_length + return nil unless key?('Content-Length') + len = self['Content-Length'].slice(/\d+/) or + raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format' + len.to_i + end + + def content_length=(len) + unless len + @header.delete 'content-length' + return nil + end + @header['content-length'] = [len.to_i.to_s] + end + + # Returns "true" if the "transfer-encoding" header is present and + # set to "chunked". This is an HTTP/1.1 feature, allowing + # the content to be sent in "chunks" without at the outset + # stating the entire content length. + def chunked? + return false unless @header['transfer-encoding'] + field = self['Transfer-Encoding'] + (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false + end + + # Returns a Range object which represents the value of the Content-Range: + # header field. + # For a partial entity body, this indicates where this fragment + # fits inside the full entity body, as range of byte offsets. + def content_range + return nil unless @header['content-range'] + m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or + raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format' + return unless m[1] == 'bytes' + m[2].to_i .. m[3].to_i + end + + # The length of the range represented in Content-Range: header. + def range_length + r = content_range() or return nil + r.end - r.begin + 1 + end + + # Returns a content type string such as "text/html". + # This method returns nil if Content-Type: header field does not exist. + def content_type + return nil unless main_type() + if sub_type() + then "#{main_type()}/#{sub_type()}" + else main_type() + end + end + + # Returns a content type string such as "text". + # This method returns nil if Content-Type: header field does not exist. + def main_type + return nil unless @header['content-type'] + self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip + end + + # Returns a content type string such as "html". + # This method returns nil if Content-Type: header field does not exist + # or sub-type is not given (e.g. "Content-Type: text"). + def sub_type + return nil unless @header['content-type'] + _, sub = *self['Content-Type'].split(';').first.to_s.split('/') + return nil unless sub + sub.strip + end + + # Any parameters specified for the content type, returned as a Hash. + # For example, a header of Content-Type: text/html; charset=EUC-JP + # would result in type_params returning {'charset' => 'EUC-JP'} + def type_params + result = {} + list = self['Content-Type'].to_s.split(';') + list.shift + list.each do |param| + k, v = *param.split('=', 2) + result[k.strip] = v.strip + end + result + end + + # Sets the content type in an HTTP header. + # The +type+ should be a full HTTP content type, e.g. "text/html". + # The +params+ are an optional Hash of parameters to add after the + # content type, e.g. {'charset' => 'iso-8859-1'} + def set_content_type(type, params = {}) + @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] + end + + alias content_type= set_content_type + + # Set header fields and a body from HTML form data. + # +params+ should be an Array of Arrays or + # a Hash containing HTML form data. + # Optional argument +sep+ means data record separator. + # + # Values are URL encoded as necessary and the content-type is set to + # application/x-www-form-urlencoded + # + # Example: + # http.form_data = {"q" => "ruby", "lang" => "en"} + # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"} + # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';') + # + def set_form_data(params, sep = '&') + query = URI.encode_www_form(params) + query.gsub!(/&/, sep) if sep != '&' + self.body = query + self.content_type = 'application/x-www-form-urlencoded' + end + + alias form_data= set_form_data + + # 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. + # + # 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+ hash has a :filename key, + # the item is treated as a file field. + # + # If Transfer-Encoding is set as chunked, this sends the request using + # chunked encoding. Because chunked encoding is HTTP/1.1 feature, + # you should confirm that the server supports HTTP/1.1 before using + # chunked encoding. + # + # Example: + # 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 + # + def set_form(params, enctype='application/x-www-form-urlencoded', formopt={}) + @body_data = params + @body = nil + @body_stream = nil + @form_option = formopt + case enctype + when /\Aapplication\/x-www-form-urlencoded\z/i, + /\Amultipart\/form-data\z/i + self.content_type = enctype + else + raise ArgumentError, "invalid enctype: #{enctype}" + end + end + + # Set the Authorization: header for "Basic" authorization. + def basic_auth(account, password) + @header['authorization'] = [basic_encode(account, password)] + end + + # Set Proxy-Authorization: header for "Basic" authorization. + def proxy_basic_auth(account, password) + @header['proxy-authorization'] = [basic_encode(account, password)] + end + + def basic_encode(account, password) + 'Basic ' + ["#{account}:#{password}"].pack('m0') + end + private :basic_encode + + def connection_close? + token = /(?:\A|,)\s*close\s*(?:\z|,)/i + @header['connection']&.grep(token) {return true} + @header['proxy-connection']&.grep(token) {return true} + false + end + + def connection_keep_alive? + token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i + @header['connection']&.grep(token) {return true} + @header['proxy-connection']&.grep(token) {return true} + false + end + +end diff --git a/lib/net/http/net-http.gemspec b/lib/net/http/net-http.gemspec new file mode 100644 index 0000000000..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 new file mode 100644 index 0000000000..a2f770ebdb --- /dev/null +++ b/lib/net/http/proxy_delta.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: false +module Net::HTTP::ProxyDelta #:nodoc: internal use only + private + + def conn_address + proxy_address() + end + + def conn_port + proxy_port() + end + + def edit_path(path) + use_ssl? ? path : "http://#{addr_port()}#{path}" + end +end + diff --git a/lib/net/http/request.rb b/lib/net/http/request.rb new file mode 100644 index 0000000000..1e86f3e4b4 --- /dev/null +++ b/lib/net/http/request.rb @@ -0,0 +1,21 @@ +# 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 +# subclasses: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Head. +# +class Net::HTTPRequest < Net::HTTPGenericRequest + # Creates an HTTP request object for +path+. + # + # +initheader+ are the default headers to use. Net::HTTP adds + # Accept-Encoding to enable compression of the response body unless + # Accept-Encoding or Range are supplied in +initheader+. + + def initialize(path, initheader = nil) + super self.class::METHOD, + self.class::REQUEST_HAS_BODY, + self.class::RESPONSE_HAS_BODY, + path, initheader + end +end + diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb new file mode 100644 index 0000000000..d4c80a3812 --- /dev/null +++ b/lib/net/http/requests.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: false +# +# HTTP/1.1 methods --- RFC2616 +# + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Get < Net::HTTPRequest + METHOD = 'GET' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Head < Net::HTTPRequest + METHOD = 'HEAD' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = false +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Post < Net::HTTPRequest + METHOD = 'POST' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Put < Net::HTTPRequest + METHOD = 'PUT' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Delete < Net::HTTPRequest + METHOD = 'DELETE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Options < Net::HTTPRequest + METHOD = 'OPTIONS' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Trace < Net::HTTPRequest + METHOD = 'TRACE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# +# PATCH method --- RFC5789 +# + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Patch < Net::HTTPRequest + METHOD = 'PATCH' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# +# WebDAV methods --- RFC2518 +# + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Propfind < Net::HTTPRequest + METHOD = 'PROPFIND' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Proppatch < Net::HTTPRequest + METHOD = 'PROPPATCH' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Mkcol < Net::HTTPRequest + METHOD = 'MKCOL' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Copy < Net::HTTPRequest + METHOD = 'COPY' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Move < Net::HTTPRequest + METHOD = 'MOVE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Lock < Net::HTTPRequest + METHOD = 'LOCK' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Unlock < Net::HTTPRequest + METHOD = 'UNLOCK' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb new file mode 100644 index 0000000000..f8b522f1ff --- /dev/null +++ b/lib/net/http/response.rb @@ -0,0 +1,602 @@ +# frozen_string_literal: false +# HTTP response class. +# +# This class wraps together the response header and the response body (the +# entity requested). +# +# It mixes in the HTTPHeader module, which provides access to response +# header values both via hash-like methods and via individual readers. +# +# Note that each possible HTTP response code defines its own +# HTTPResponse subclass. 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 + # true if the response has a body. + def body_permitted? + self::HAS_BODY + end + + def exception_type # :nodoc: internal use only + self::EXCEPTION_TYPE + end + + def read_new(sock) #:nodoc: internal use only + httpv, code, msg = read_status_line(sock) + res = response_class(code).new(httpv, code, msg) + each_response_header(sock) do |k,v| + res.add_field k, v + end + res + end + + private + + def read_status_line(sock) + str = sock.readline + m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or + raise Net::HTTPBadResponse, "wrong status line: #{str.dump}" + m.captures + end + + def response_class(code) + CODE_TO_OBJ[code] or + CODE_CLASS_TO_OBJ[code[0,1]] or + Net::HTTPUnknownResponse + end + + def each_response_header(sock) + key = value = nil + while true + line = sock.readuntil("\n", true).sub(/\s+\z/, '') + break if line.empty? + if line[0] == ?\s or line[0] == ?\t and value + value << ' ' unless value.empty? + value << line.strip + else + yield key, value if key + key, value = line.strip.split(/\s*:\s*/, 2) + raise Net::HTTPBadResponse, 'wrong header line format' if value.nil? + end + end + yield key, value if key + end + end + + # next is to fix bug in RDoc, where the private inside class << self + # spills out. + public + + include Net::HTTPHeader + + def initialize(httpv, code, msg) #:nodoc: internal use only + @http_version = httpv + @code = code + @message = msg + initialize_http_header nil + @body = nil + @read = false + @uri = nil + @decode_content = false + @body_encoding = false + @ignore_eof = true + end + + # The HTTP version supported by the server. + attr_reader :http_version + + # The HTTP result code string. For example, '302'. You can also + # determine the response type by examining which response subclass + # the response object is an instance of. + attr_reader :code + + # The HTTP result message sent by the server. For example, 'Not Found'. + attr_reader :message + alias msg message # :nodoc: obsolete + + # The URI used to fetch this response. The response URI is only available + # if a URI was used to create the request. + attr_reader :uri + + # Set to true automatically when the request did not contain an + # Accept-Encoding header from the user. + attr_accessor :decode_content + + # 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 + + # + # response <-> exception relationship + # + + def code_type #:nodoc: + self.class + end + + def error! #:nodoc: + message = @code + message += ' ' + @message.dump if @message + raise error_type().new(message, self) + end + + def error_type #:nodoc: + self.class::EXCEPTION_TYPE + end + + # Raises an HTTP error if the response is not 2xx (success). + def value + error! unless self.kind_of?(Net::HTTPSuccess) + end + + def uri= uri # :nodoc: + @uri = uri.dup if uri + end + + # + # header (for backward compatibility only; DO NOT USE) + # + + def response #:nodoc: + warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE + self + end + + def header #:nodoc: + warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE + self + end + + def read_header #:nodoc: + warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE + self + end + + # + # body + # + + def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only + @socket = sock + @body_exist = reqmethodallowbody && self.class.body_permitted? + begin + yield + self.body # ensure to read body + ensure + @socket = nil + end + end + + # Gets the entity body returned by the remote HTTP server. + # + # If a block is given, the body is passed to the block, and + # the body is provided in fragments, as it is read in from the socket. + # + # If +dest+ argument is given, response is read into that variable, + # with <code>dest#<<</code> method (it could be String or IO, or any + # other object responding to <code><<</code>). + # + # Calling this method a second or subsequent time for the same + # HTTPResponse object will return the value already read. + # + # http.request_get('/index.html') {|res| + # puts res.read_body + # } + # + # http.request_get('/index.html') {|res| + # p res.read_body.object_id # 538149362 + # p res.read_body.object_id # 538149362 + # } + # + # # using iterator + # http.request_get('/index.html') {|res| + # res.read_body do |segment| + # print segment + # end + # } + # + def read_body(dest = nil, &block) + if @read + raise IOError, "#{self.class}\#read_body called twice" if dest or block + return @body + end + to = procdest(dest, block) + stream_check + if @body_exist + read_body_0 to + @body = to + else + @body = nil + end + @read = true + + case enc = @body_encoding + when Encoding, false, nil + # Encoding: force given encoding + # false/nil: do not force encoding + else + # other value: detect encoding from body + enc = detect_encoding(@body) + end + + @body.force_encoding(enc) if enc + + @body + end + + # Returns the full entity body. + # + # Calling this method a second or subsequent time will return the + # string already read. + # + # http.request_get('/index.html') {|res| + # puts res.body + # } + # + # http.request_get('/index.html') {|res| + # p res.body.object_id # 538149362 + # p res.body.object_id # 538149362 + # } + # + def body + read_body() + end + + # Because it may be necessary to modify the body, Eg, decompression + # this method facilitates that. + def body=(value) + @body = value + end + + alias entity body #:nodoc: obsolete + + private + + # :nodoc: + def detect_encoding(str, encoding=nil) + if encoding + elsif encoding = type_params['charset'] + elsif encoding = check_bom(str) + else + encoding = case content_type&.downcase + when %r{text/x(?:ht)?ml|application/(?:[^+]+\+)?xml} + /\A<xml[ \t\r\n]+ + version[ \t\r\n]*=[ \t\r\n]*(?:"[0-9.]+"|'[0-9.]*')[ \t\r\n]+ + encoding[ \t\r\n]*=[ \t\r\n]* + (?:"([A-Za-z][\-A-Za-z0-9._]*)"|'([A-Za-z][\-A-Za-z0-9._]*)')/x =~ str + encoding = $1 || $2 || Encoding::UTF_8 + when %r{text/html.*} + sniff_encoding(str) + end + end + return encoding + end + + # :nodoc: + def sniff_encoding(str, encoding=nil) + # the encoding sniffing algorithm + # http://www.w3.org/TR/html5/parsing.html#determining-the-character-encoding + if enc = scanning_meta(str) + enc + # 6. last visited page or something + # 7. frequency + elsif str.ascii_only? + Encoding::US_ASCII + elsif str.dup.force_encoding(Encoding::UTF_8).valid_encoding? + Encoding::UTF_8 + end + # 8. implementation-defined or user-specified + end + + # :nodoc: + def check_bom(str) + case str.byteslice(0, 2) + when "\xFE\xFF" + return Encoding::UTF_16BE + when "\xFF\xFE" + return Encoding::UTF_16LE + end + if "\xEF\xBB\xBF" == str.byteslice(0, 3) + return Encoding::UTF_8 + end + nil + end + + # :nodoc: + def scanning_meta(str) + require 'strscan' + ss = StringScanner.new(str) + if ss.scan_until(/<meta[\t\n\f\r ]*/) + attrs = {} # attribute_list + got_pragma = false + need_pragma = nil + charset = nil + + # step: Attributes + while attr = get_attribute(ss) + name, value = *attr + next if attrs[name] + attrs[name] = true + case name + when 'http-equiv' + got_pragma = true if value == 'content-type' + when 'content' + encoding = extracting_encodings_from_meta_elements(value) + unless charset + charset = encoding + end + need_pragma = true + when 'charset' + need_pragma = false + charset = value + end + end + + # step: Processing + return if need_pragma.nil? + return if need_pragma && !got_pragma + + charset = Encoding.find(charset) rescue nil + return unless charset + charset = Encoding::UTF_8 if charset == Encoding::UTF_16 + return charset # tentative + end + nil + end + + def get_attribute(ss) + ss.scan(/[\t\n\f\r \/]*/) + if ss.peek(1) == '>' + ss.getch + return nil + end + name = ss.scan(/[^=\t\n\f\r \/>]*/) + name.downcase! + raise if name.empty? + ss.skip(/[\t\n\f\r ]*/) + if ss.getch != '=' + value = '' + return [name, value] + end + ss.skip(/[\t\n\f\r ]*/) + case ss.peek(1) + when '"' + ss.getch + value = ss.scan(/[^"]+/) + value.downcase! + ss.getch + when "'" + ss.getch + value = ss.scan(/[^']+/) + value.downcase! + ss.getch + when '>' + value = '' + else + value = ss.scan(/[^\t\n\f\r >]+/) + value.downcase! + end + [name, value] + end + + def extracting_encodings_from_meta_elements(value) + # http://dev.w3.org/html5/spec/fetching-resources.html#algorithm-for-extracting-an-encoding-from-a-meta-element + if /charset[\t\n\f\r ]*=(?:"([^"]*)"|'([^']*)'|["']|\z|([^\t\n\f\r ;]+))/i =~ value + return $1 || $2 || $3 + end + return nil + end + + ## + # Checks for a supported Content-Encoding header and yields an Inflate + # wrapper for this response's socket when zlib is present. If the + # Content-Encoding is not supported or zlib is missing, the plain socket is + # yielded. + # + # If a Content-Range header is present, a plain socket is yielded as the + # bytes in the range may not be a complete deflate block. + + def inflater # :nodoc: + return yield @socket unless Net::HTTP::HAVE_ZLIB + return yield @socket unless @decode_content + return yield @socket if self['content-range'] + + v = self['content-encoding'] + case v&.downcase + when 'deflate', 'gzip', 'x-gzip' then + self.delete 'content-encoding' + + inflate_body_io = Inflater.new(@socket) + + begin + yield inflate_body_io + success = true + ensure + begin + inflate_body_io.finish + if self['content-length'] + self['content-length'] = inflate_body_io.bytes_inflated.to_s + end + rescue => err + # Ignore #finish's error if there is an exception from yield + raise err if success + end + end + when 'none', 'identity' then + self.delete 'content-encoding' + + yield @socket + else + yield @socket + end + end + + def read_body_0(dest) + inflater do |inflate_body_io| + if chunked? + read_chunked dest, inflate_body_io + return + end + + @socket = inflate_body_io + + clen = content_length() + if clen + @socket.read clen, dest, @ignore_eof + return + end + clen = range_length() + if clen + @socket.read clen, dest + return + end + @socket.read_all dest + end + end + + ## + # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF, + # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip + # encoded. + # + # See RFC 2616 section 3.6.1 for definitions + + def read_chunked(dest, chunk_data_io) # :nodoc: + total = 0 + while true + line = @socket.readline + hexlen = line.slice(/[0-9a-fA-F]+/) or + raise Net::HTTPBadResponse, "wrong chunk size line: #{line}" + len = hexlen.hex + break if len == 0 + begin + chunk_data_io.read len, dest + ensure + total += len + @socket.read 2 # \r\n + end + end + until @socket.readline.empty? + # none + end + end + + def stream_check + raise IOError, 'attempt to read body out of block' if @socket.closed? + end + + def procdest(dest, block) + raise ArgumentError, 'both arg and block given for HTTP method' if + dest and block + if block + Net::ReadAdapter.new(block) + else + dest || '' + end + end + + ## + # Inflater is a wrapper around Net::BufferedIO that transparently inflates + # zlib and gzip streams. + + class Inflater # :nodoc: + + ## + # Creates a new Inflater wrapping +socket+ + + def initialize socket + @socket = socket + # zlib with automatic gzip detection + @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS) + end + + ## + # Finishes the inflate stream. + + def finish + return if @inflate.total_in == 0 + @inflate.finish + end + + ## + # The number of bytes inflated, used to update the Content-Length of + # the response. + + def bytes_inflated + @inflate.total_out + end + + ## + # Returns a Net::ReadAdapter that inflates each read chunk into +dest+. + # + # This allows a large response body to be inflated without storing the + # entire body in memory. + + def inflate_adapter(dest) + if dest.respond_to?(:set_encoding) + dest.set_encoding(Encoding::ASCII_8BIT) + elsif dest.respond_to?(:force_encoding) + dest.force_encoding(Encoding::ASCII_8BIT) + end + block = proc do |compressed_chunk| + @inflate.inflate(compressed_chunk) do |chunk| + compressed_chunk.clear + dest << chunk + end + end + + Net::ReadAdapter.new(block) + end + + ## + # Reads +clen+ bytes from the socket, inflates them, then writes them to + # +dest+. +ignore_eof+ is passed down to Net::BufferedIO#read + # + # Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes. + # At this time there is no way for a user of Net::HTTPResponse to read a + # specific number of bytes from the HTTP response body, so this internal + # API does not return the same number of bytes as were requested. + # + # See https://bugs.ruby-lang.org/issues/6492 for further discussion. + + def read clen, dest, ignore_eof = false + temp_dest = inflate_adapter(dest) + + @socket.read clen, temp_dest, ignore_eof + end + + ## + # Reads the rest of the socket, inflates it, then writes it to +dest+. + + def read_all dest + temp_dest = inflate_adapter(dest) + + @socket.read_all temp_dest + end + + end + +end + diff --git a/lib/net/http/responses.rb b/lib/net/http/responses.rb new file mode 100644 index 0000000000..02a2fdaa4c --- /dev/null +++ b/lib/net/http/responses.rb @@ -0,0 +1,312 @@ +# frozen_string_literal: true +#-- +# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + +module Net + # :stopdoc: + + class HTTPUnknownResponse < HTTPResponse + HAS_BODY = true + EXCEPTION_TYPE = HTTPError # + end + class HTTPInformation < HTTPResponse # 1xx + HAS_BODY = false + EXCEPTION_TYPE = HTTPError # + end + class HTTPSuccess < HTTPResponse # 2xx + HAS_BODY = true + EXCEPTION_TYPE = HTTPError # + end + class HTTPRedirection < HTTPResponse # 3xx + HAS_BODY = true + EXCEPTION_TYPE = HTTPRetriableError # + end + class HTTPClientError < HTTPResponse # 4xx + HAS_BODY = true + EXCEPTION_TYPE = HTTPClientException # + end + class HTTPServerError < HTTPResponse # 5xx + HAS_BODY = true + EXCEPTION_TYPE = HTTPFatalError # + 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 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 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 + CODE_CLASS_TO_OBJ = { + '1' => Net::HTTPInformation, + '2' => Net::HTTPSuccess, + '3' => Net::HTTPRedirection, + '4' => Net::HTTPClientError, + '5' => Net::HTTPServerError + } + CODE_TO_OBJ = { + '100' => Net::HTTPContinue, + '101' => Net::HTTPSwitchProtocol, + '102' => Net::HTTPProcessing, + '103' => Net::HTTPEarlyHints, + + '200' => Net::HTTPOK, + '201' => Net::HTTPCreated, + '202' => Net::HTTPAccepted, + '203' => Net::HTTPNonAuthoritativeInformation, + '204' => Net::HTTPNoContent, + '205' => Net::HTTPResetContent, + '206' => Net::HTTPPartialContent, + '207' => Net::HTTPMultiStatus, + '208' => Net::HTTPAlreadyReported, + '226' => Net::HTTPIMUsed, + + '300' => Net::HTTPMultipleChoices, + '301' => Net::HTTPMovedPermanently, + '302' => Net::HTTPFound, + '303' => Net::HTTPSeeOther, + '304' => Net::HTTPNotModified, + '305' => Net::HTTPUseProxy, + '307' => Net::HTTPTemporaryRedirect, + '308' => Net::HTTPPermanentRedirect, + + '400' => Net::HTTPBadRequest, + '401' => Net::HTTPUnauthorized, + '402' => Net::HTTPPaymentRequired, + '403' => Net::HTTPForbidden, + '404' => Net::HTTPNotFound, + '405' => Net::HTTPMethodNotAllowed, + '406' => Net::HTTPNotAcceptable, + '407' => Net::HTTPProxyAuthenticationRequired, + '408' => Net::HTTPRequestTimeout, + '409' => Net::HTTPConflict, + '410' => Net::HTTPGone, + '411' => Net::HTTPLengthRequired, + '412' => Net::HTTPPreconditionFailed, + '413' => Net::HTTPPayloadTooLarge, + '414' => Net::HTTPURITooLong, + '415' => Net::HTTPUnsupportedMediaType, + '416' => Net::HTTPRangeNotSatisfiable, + '417' => Net::HTTPExpectationFailed, + '421' => Net::HTTPMisdirectedRequest, + '422' => Net::HTTPUnprocessableEntity, + '423' => Net::HTTPLocked, + '424' => Net::HTTPFailedDependency, + '426' => Net::HTTPUpgradeRequired, + '428' => Net::HTTPPreconditionRequired, + '429' => Net::HTTPTooManyRequests, + '431' => Net::HTTPRequestHeaderFieldsTooLarge, + '451' => Net::HTTPUnavailableForLegalReasons, + + '500' => Net::HTTPInternalServerError, + '501' => Net::HTTPNotImplemented, + '502' => Net::HTTPBadGateway, + '503' => Net::HTTPServiceUnavailable, + '504' => Net::HTTPGatewayTimeout, + '505' => Net::HTTPVersionNotSupported, + '506' => Net::HTTPVariantAlsoNegotiates, + '507' => Net::HTTPInsufficientStorage, + '508' => Net::HTTPLoopDetected, + '510' => Net::HTTPNotExtended, + '511' => Net::HTTPNetworkAuthenticationRequired, + } +end 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 e296dbbed4..d46721c82a 100644 --- a/lib/net/https.rb +++ b/lib/net/https.rb @@ -1,6 +1,12 @@ +# frozen_string_literal: false =begin -= $RCSfile$ -- SSL/TLS enhancement for Net::HTTP. += net/https -- SSL/TLS enhancement for Net::HTTP. + + This file has been merged with net/http. There is no longer any need to + require 'net/https' to use HTTPS. + + See Net::HTTP for details on how to make HTTPS connections. == Info 'OpenSSL for Ruby 2' project @@ -8,166 +14,10 @@ All rights reserved. == Licence - This program is licenced under the same licence as Ruby. + This program is licensed under the same licence as Ruby. (See the file 'LICENCE'.) -== Requirements - This program requires Net 1.2.0 or higher version. - You can get it from RAA or Ruby's CVS repository. - -== Version - $Id$ - - 2001-11-06: Contiributed to Ruby/OpenSSL project. - 2004-03-06: Some code is merged in to net/http. - -== Example - -Here is a simple HTTP client: - - require 'net/http' - require 'uri' - - uri = URI.parse(ARGV[0] || 'http://localhost/') - http = Net::HTTP.new(uri.host, uri.port) - http.start { - http.request_get(uri.path) {|res| - print res.body - } - } - -It can be replaced by the following code: - - require 'net/https' - require 'uri' - - uri = URI.parse(ARGV[0] || 'https://localhost/') - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true if uri.scheme == "https" # enable SSL/TLS - http.start { - http.request_get(uri.path) {|res| - print res.body - } - } - -== class Net::HTTP - -=== Instance Methods - -: use_ssl? - returns true if use SSL/TLS with HTTP. - -: use_ssl=((|true_or_false|)) - sets use_ssl. - -: peer_cert - return the X.509 certificates the server presented. - -: key, key=((|key|)) - Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. - (This method is appeared in Michal Rokos's OpenSSL extention.) - -: cert, cert=((|cert|)) - Sets an OpenSSL::X509::Certificate object as client certificate - (This method is appeared in Michal Rokos's OpenSSL extention). - -: ca_file, ca_file=((|path|)) - Sets path of a CA certification file in PEM format. - The file can contrain several CA certificats. - -: ca_path, ca_path=((|path|)) - Sets path of a CA certification directory containing certifications - in PEM format. - -: verify_mode, verify_mode=((|mode|)) - Sets the flags for server the certification verification at - begining of SSL/TLS session. - OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable. - -: verify_callback, verify_callback=((|proc|)) - Sets the verify callback for the server certification verification. - -: verify_depth, verify_depth=((|num|)) - Sets the maximum depth for the certificate chain verification. - -: cert_store, cert_store=((|store|)) - Sets the X509::Store to verify peer certificate. - -: ssl_timeout, ssl_timeout=((|sec|)) - Sets the SSL timeout seconds. - =end -require 'net/http' +require_relative 'http' require 'openssl' - -module Net - - class HTTP - remove_method :use_ssl? - def use_ssl? - @use_ssl - end - - # For backward compatibility. - alias use_ssl use_ssl? - - # Turn on/off SSL. - # This flag must be set before starting session. - # If you change use_ssl value after session started, - # a Net::HTTP object raises IOError. - def use_ssl=(flag) - flag = (flag ? true : false) - raise IOError, "use_ssl value changed, but session already started" \ - if started? and @use_ssl != flag - if flag and not @ssl_context - @ssl_context = OpenSSL::SSL::SSLContext.new - end - @use_ssl = flag - end - - def self.ssl_context_accessor(name) - module_eval(<<-End, __FILE__, __LINE__ + 1) - def #{name} - return nil unless @ssl_context - @ssl_context.#{name} - end - - def #{name}=(val) - @ssl_context ||= OpenSSL::SSL::SSLContext.new - @ssl_context.#{name} = val - end - End - end - - ssl_context_accessor :key - ssl_context_accessor :cert - ssl_context_accessor :ca_file - ssl_context_accessor :ca_path - ssl_context_accessor :verify_mode - ssl_context_accessor :verify_callback - ssl_context_accessor :verify_depth - ssl_context_accessor :cert_store - - def ssl_timeout - return nil unless @ssl_context - @ssl_context.timeout - end - - def ssl_timeout=(sec) - raise ArgumentError, 'Net::HTTP#ssl_timeout= called but use_ssl=false' \ - unless use_ssl? - @ssl_context ||= OpenSSL::SSL::SSLContext.new - @ssl_context.timeout = sec - end - - # For backward compatibility - alias timeout= ssl_timeout= - - def peer_cert - return nil if not use_ssl? or not @socket - @socket.io.peer_cert - end - end - -end diff --git a/lib/net/imap.rb b/lib/net/imap.rb deleted file mode 100644 index f8c0d3be4c..0000000000 --- a/lib/net/imap.rb +++ /dev/null @@ -1,3371 +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" -begin - require "openssl" -rescue LoadError -end - -module Net - - # - # Net::IMAP implements Internet Message Access Protocol (IMAP) client - # functionality. The protocol is described in [IMAP]. - # - # == IMAP Overview - # - # An IMAP client connects to a server, and then authenticates - # itself using either #authenticate() or #login(). Having - # authenticated itself, there is a range of commands - # available to it. Most work with mailboxes, which may be - # arranged in an hierarchical namespace, and each of which - # contains zero or more messages. How this is implemented on - # the server is implementation-dependent; on a UNIX server, it - # will frequently be implemented as a files in mailbox format - # within a hierarchy of directories. - # - # To work on the messages within a mailbox, the client must - # first select that mailbox, using either #select() or (for - # read-only access) #examine(). Once the client has successfully - # selected a mailbox, they enter _selected_ state, and that - # mailbox becomes the _current_ mailbox, on which mail-item - # related commands implicitly operate. - # - # Messages have two sorts of identifiers: message sequence - # numbers, and UIDs. - # - # Message sequence numbers number messages within a mail box - # from 1 up to the number of items in the mail box. If new - # message arrives during a session, it receives a sequence - # number equal to the new size of the mail box. If messages - # are expunged from the mailbox, remaining messages have their - # sequence numbers "shuffled down" to fill the gaps. - # - # UIDs, on the other hand, are permanently guaranteed not to - # identify another message within the same mailbox, even if - # the existing message is deleted. UIDs are required to - # be assigned in ascending (but not necessarily sequential) - # order within a mailbox; this means that if a non-IMAP client - # rearranges the order of mailitems within a mailbox, the - # UIDs have to be reassigned. An IMAP client cannot thus - # rearrange message orders. - # - # == Examples of Usage - # - # === List sender and subject of all recent messages in the default mailbox - # - # imap = Net::IMAP.new('mail.example.com') - # imap.authenticate('LOGIN', 'joe_user', 'joes_password') - # imap.examine('INBOX') - # imap.search(["RECENT"]).each do |message_id| - # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"] - # puts "#{envelope.from[0].name}: \t#{envelope.subject}" - # end - # - # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" - # - # imap = Net::IMAP.new('mail.example.com') - # imap.authenticate('LOGIN', 'joe_user', 'joes_password') - # imap.select('Mail/sent-mail') - # if not imap.list('Mail/', 'sent-apr03') - # imap.create('Mail/sent-apr03') - # end - # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id| - # imap.copy(message_id, "Mail/sent-apr03") - # imap.store(message_id, "+FLAGS", [:Deleted]) - # end - # imap.expunge - # - # == Thread Safety - # - # Net::IMAP supports concurrent threads. For example, - # - # imap = Net::IMAP.new("imap.foo.net", "imap2") - # imap.authenticate("cram-md5", "bar", "password") - # imap.select("inbox") - # fetch_thread = Thread.start { imap.fetch(1..-1, "UID") } - # search_result = imap.search(["BODY", "hello"]) - # fetch_result = fetch_thread.value - # imap.disconnect - # - # This script invokes the FETCH command and the SEARCH command concurrently. - # - # == Errors - # - # An IMAP server can send three different types of responses to indicate - # failure: - # - # NO:: the attempted command could not be successfully completed. For - # instance, the username/password used for logging in are incorrect; - # the selected mailbox does not exists; etc. - # - # BAD:: the request from the client does not follow the server's - # understanding of the IMAP protocol. This includes attempting - # commands from the wrong client state; for instance, attempting - # to perform a SEARCH command without having SELECTed a current - # mailbox. It can also signal an internal server - # failure (such as a disk crash) has occurred. - # - # BYE:: the server is saying goodbye. This can be part of a normal - # logout sequence, and can be used as part of a login sequence - # to indicate that the server is (for some reason) unwilling - # to accept our connection. As a response to any other command, - # it indicates either that the server is shutting down, or that - # the server is timing out the client connection due to inactivity. - # - # These three error response are represented by the errors - # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and - # Net::IMAP::ByeResponseError, all of which are subclasses of - # Net::IMAP::ResponseError. Essentially, all methods that involve - # sending a request to the server can generate one of these errors. - # Only the most pertinent instances have been documented below. - # - # Because the IMAP class uses Sockets for communication, its methods - # are also susceptible to the various errors that can occur when - # working with sockets. These are generally represented as - # Errno errors. For instance, any method that involves sending a - # request to the server and/or receiving a response from it could - # raise an Errno::EPIPE error if the network connection unexpectedly - # goes down. See the socket(7), ip(7), tcp(7), socket(2), connect(2), - # and associated man pages. - # - # Finally, a Net::IMAP::DataFormatError is thrown if low-level data - # is found to be in an incorrect format (for instance, when converting - # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is - # thrown if a server response is non-parseable. - # - # - # == References - # - # [[IMAP]] - # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1", - # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501) - # - # [[LANGUAGE-TAGS]] - # Alvestrand, H., "Tags for the Identification of - # Languages", RFC 1766, March 1995. - # - # [[MD5]] - # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC - # 1864, October 1995. - # - # [[MIME-IMB]] - # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet - # Mail Extensions) Part One: Format of Internet Message Bodies", RFC - # 2045, November 1996. - # - # [[RFC-822]] - # Crocker, D., "Standard for the Format of ARPA Internet Text - # Messages", STD 11, RFC 822, University of Delaware, August 1982. - # - # [[RFC-2087]] - # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997. - # - # [[RFC-2086]] - # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997. - # - # [[RFC-2195]] - # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension - # for Simple Challenge/Response", RFC 2195, September 1997. - # - # [[SORT-THREAD-EXT]] - # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD - # Extensions", draft-ietf-imapext-sort, May 2003. - # - # [[OSSL]] - # http://www.openssl.org - # - # [[RSSL]] - # http://savannah.gnu.org/projects/rubypki - # - # [[UTF7]] - # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of - # Unicode", RFC 2152, May 1997. - # - class IMAP - include MonitorMixin - if defined?(OpenSSL) - include OpenSSL - include SSL - end - - # Returns an initial greeting response from the server. - attr_reader :greeting - - # Returns recorded untagged responses. For example: - # - # imap.select("inbox") - # p imap.responses["EXISTS"][-1] - # #=> 2 - # p imap.responses["UIDVALIDITY"][-1] - # #=> 968263756 - attr_reader :responses - - # Returns all response handlers. - attr_reader :response_handlers - - # The thread to receive exceptions. - attr_accessor :client_thread - - # Flag indicating a message has been seen - SEEN = :Seen - - # Flag indicating a message has been answered - ANSWERED = :Answered - - # Flag indicating a message has been flagged for special or urgent - # attention - FLAGGED = :Flagged - - # Flag indicating a message has been marked for deletion. This - # will occur when the mailbox is closed or expunged. - DELETED = :Deleted - - # Flag indicating a message is only a draft or work-in-progress version. - DRAFT = :Draft - - # Flag indicating that the message is "recent", meaning that this - # session is the first session in which the client has been notified - # of this message. - RECENT = :Recent - - # Flag indicating that a mailbox context name cannot contain - # children. - NOINFERIORS = :Noinferiors - - # Flag indicating that a mailbox is not selected. - NOSELECT = :Noselect - - # Flag indicating that a mailbox has been marked "interesting" by - # the server; this commonly indicates that the mailbox contains - # new messages. - MARKED = :Marked - - # Flag indicating that the mailbox does not contains new messages. - UNMARKED = :Unmarked - - # Returns the debug mode. - def self.debug - return @@debug - end - - # Sets the debug mode. - def self.debug=(val) - return @@debug = val - end - - # Adds an authenticator for Net::IMAP#authenticate. +auth_type+ - # is the type of authentication this authenticator supports - # (for instance, "LOGIN"). The +authenticator+ is an object - # which defines a process() method to handle authentication with - # the server. See Net::IMAP::LoginAuthenticator and - # Net::IMAP::CramMD5Authenticator for examples. - # - # If +auth_type+ refers to an existing authenticator, it will be - # replaced by the new one. - def self.add_authenticator(auth_type, authenticator) - @@authenticators[auth_type] = authenticator - end - - # Disconnects from the server. - def disconnect - if SSL::SSLSocket === @sock - @sock.io.shutdown - else - @sock.shutdown - end - @receiver_thread.join - @sock.close - end - - # Returns true if disconnected from the server. - def disconnected? - return @sock.closed? - end - - # Sends a CAPABILITY command, and returns an array of - # capabilities that the server supports. Each capability - # is a string. See [IMAP] for a list of possible - # capabilities. - # - # Note that the Net::IMAP class does not modify its - # behaviour according to the capabilities of the server; - # it is up to the user of the class to ensure that - # a certain capability is supported by a server before - # using it. - def capability - synchronize do - send_command("CAPABILITY") - return @responses.delete("CAPABILITY")[-1] - end - end - - # Sends a NOOP command to the server. It does nothing. - def noop - send_command("NOOP") - end - - # Sends a LOGOUT command to inform the server that the client is - # done with the connection. - def logout - send_command("LOGOUT") - end - - # Sends an AUTHENTICATE command to authenticate the client. - # The +auth_type+ parameter is a string that represents - # the authentication mechanism to be used. Currently Net::IMAP - # supports authentication mechanisms: - # - # LOGIN:: login using cleartext user and password. - # CRAM-MD5:: login with cleartext user and encrypted password - # (see [RFC-2195] for a full description). This - # mechanism requires that the server have the user's - # password stored in clear-text password. - # - # For both these mechanisms, there should be two +args+: username - # and (cleartext) password. A server may not support one or other - # of these mechanisms; check #capability() for a capability of - # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5". - # - # Authentication is done using the appropriate authenticator object: - # see @@authenticators for more information on plugging in your own - # authenticator. - # - # For example: - # - # imap.authenticate('LOGIN', user, password) - # - # A Net::IMAP::NoResponseError is raised if authentication fails. - def authenticate(auth_type, *args) - auth_type = auth_type.upcase - unless @@authenticators.has_key?(auth_type) - raise ArgumentError, - format('unknown auth type - "%s"', auth_type) - end - authenticator = @@authenticators[auth_type].new(*args) - send_command("AUTHENTICATE", auth_type) do |resp| - if resp.instance_of?(ContinuationRequest) - data = authenticator.process(resp.data.text.unpack("m")[0]) - s = [data].pack("m").gsub(/\n/, "") - send_string_data(s) - put_string(CRLF) - end - end - end - - # Sends a LOGIN command to identify the client and carries - # the plaintext +password+ authenticating this +user+. Note - # that, unlike calling #authenticate() with an +auth_type+ - # of "LOGIN", #login() does *not* use the login authenticator. - # - # A Net::IMAP::NoResponseError is raised if authentication fails. - def login(user, password) - send_command("LOGIN", user, password) - end - - # Sends a SELECT command to select a +mailbox+ so that messages - # in the +mailbox+ can be accessed. - # - # After you have selected a mailbox, you may retrieve the - # number of items in that mailbox from @responses["EXISTS"][-1], - # and the number of recent messages from @responses["RECENT"][-1]. - # Note that these values can change if new messages arrive - # during a session; see #add_response_handler() for a way of - # detecting this event. - # - # A Net::IMAP::NoResponseError is raised if the mailbox does not - # exist or is for some reason non-selectable. - def select(mailbox) - synchronize do - @responses.clear - send_command("SELECT", mailbox) - end - end - - # Sends a EXAMINE command to select a +mailbox+ so that messages - # in the +mailbox+ can be accessed. Behaves the same as #select(), - # except that the selected +mailbox+ is identified as read-only. - # - # A Net::IMAP::NoResponseError is raised if the mailbox does not - # exist or is for some reason non-examinable. - def examine(mailbox) - synchronize do - @responses.clear - send_command("EXAMINE", mailbox) - end - end - - # Sends a CREATE command to create a new +mailbox+. - # - # A Net::IMAP::NoResponseError is raised if a mailbox with that name - # cannot be created. - def create(mailbox) - send_command("CREATE", mailbox) - end - - # Sends a DELETE command to remove the +mailbox+. - # - # A Net::IMAP::NoResponseError is raised if a mailbox with that name - # cannot be deleted, either because it does not exist or because the - # client does not have permission to delete it. - def delete(mailbox) - send_command("DELETE", mailbox) - end - - # Sends a RENAME command to change the name of the +mailbox+ to - # +newname+. - # - # A Net::IMAP::NoResponseError is raised if a mailbox with the - # name +mailbox+ cannot be renamed to +newname+ for whatever - # reason; for instance, because +mailbox+ does not exist, or - # because there is already a mailbox with the name +newname+. - def rename(mailbox, newname) - send_command("RENAME", mailbox, newname) - end - - # Sends a SUBSCRIBE command to add the specified +mailbox+ name to - # the server's set of "active" or "subscribed" mailboxes as returned - # by #lsub(). - # - # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be - # subscribed to, for instance because it does not exist. - def subscribe(mailbox) - send_command("SUBSCRIBE", mailbox) - end - - # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name - # from the server's set of "active" or "subscribed" mailboxes. - # - # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be - # unsubscribed from, for instance because the client is not currently - # subscribed to it. - def unsubscribe(mailbox) - send_command("UNSUBSCRIBE", mailbox) - end - - # Sends a LIST command, and returns a subset of names from - # the complete set of all names available to the client. - # +refname+ provides a context (for instance, a base directory - # in a directory-based mailbox hierarchy). +mailbox+ specifies - # a mailbox or (via wildcards) mailboxes under that context. - # Two wildcards may be used in +mailbox+: '*', which matches - # all characters *including* the hierarchy delimiter (for instance, - # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%', - # which matches all characters *except* the hierarchy delimiter. - # - # If +refname+ is empty, +mailbox+ is used directly to determine - # which mailboxes to match. If +mailbox+ is empty, the root - # name of +refname+ and the hierarchy delimiter are returned. - # - # The return value is an array of +Net::IMAP::MailboxList+. For example: - # - # imap.create("foo/bar") - # imap.create("foo/baz") - # p imap.list("", "foo/%") - # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\ - # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\ - # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">] - def list(refname, mailbox) - synchronize do - send_command("LIST", refname, mailbox) - return @responses.delete("LIST") - end - end - - # Sends the GETQUOTAROOT command along with specified +mailbox+. - # This command is generally available to both admin and user. - # If mailbox exists, returns an array containing objects of - # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota. - def getquotaroot(mailbox) - synchronize do - send_command("GETQUOTAROOT", mailbox) - result = [] - result.concat(@responses.delete("QUOTAROOT")) - result.concat(@responses.delete("QUOTA")) - return result - end - end - - # Sends the GETQUOTA command along with specified +mailbox+. - # If this mailbox exists, then an array containing a - # Net::IMAP::MailboxQuota object is returned. This - # command generally is only available to server admin. - def getquota(mailbox) - synchronize do - send_command("GETQUOTA", mailbox) - return @responses.delete("QUOTA") - end - end - - # Sends a SETQUOTA command along with the specified +mailbox+ and - # +quota+. If +quota+ is nil, then quota will be unset for that - # mailbox. Typically one needs to be logged in as server admin - # for this to work. The IMAP quota commands are described in - # [RFC-2087]. - def setquota(mailbox, quota) - if quota.nil? - data = '()' - else - data = '(STORAGE ' + quota.to_s + ')' - end - send_command("SETQUOTA", mailbox, RawData.new(data)) - end - - # Sends the SETACL command along with +mailbox+, +user+ and the - # +rights+ that user is to have on that mailbox. If +rights+ is nil, - # then that user will be stripped of any rights to that mailbox. - # The IMAP ACL commands are described in [RFC-2086]. - def setacl(mailbox, user, rights) - if rights.nil? - send_command("SETACL", mailbox, user, "") - else - send_command("SETACL", mailbox, user, rights) - end - end - - # Send the GETACL command along with specified +mailbox+. - # If this mailbox exists, an array containing objects of - # Net::IMAP::MailboxACLItem will be returned. - def getacl(mailbox) - synchronize do - send_command("GETACL", mailbox) - return @responses.delete("ACL")[-1] - end - end - - # Sends a LSUB command, and returns a subset of names from the set - # of names that the user has declared as being "active" or - # "subscribed". +refname+ and +mailbox+ are interpreted as - # for #list(). - # The return value is an array of +Net::IMAP::MailboxList+. - def lsub(refname, mailbox) - synchronize do - send_command("LSUB", refname, mailbox) - return @responses.delete("LSUB") - end - end - - # Sends a STATUS command, and returns the status of the indicated - # +mailbox+. +attr+ is a list of one or more attributes that - # we are request the status of. Supported attributes include: - # - # MESSAGES:: the number of messages in the mailbox. - # RECENT:: the number of recent messages in the mailbox. - # UNSEEN:: the number of unseen messages in the mailbox. - # - # The return value is a hash of attributes. For example: - # - # p imap.status("inbox", ["MESSAGES", "RECENT"]) - # #=> {"RECENT"=>0, "MESSAGES"=>44} - # - # A Net::IMAP::NoResponseError is raised if status values - # for +mailbox+ cannot be returned, for instance because it - # does not exist. - def status(mailbox, attr) - synchronize do - send_command("STATUS", mailbox, attr) - return @responses.delete("STATUS")[-1].attr - end - end - - # Sends a APPEND command to append the +message+ to the end of - # the +mailbox+. The optional +flags+ argument is an array of - # flags to initially passing to the new message. The optional - # +date_time+ argument specifies the creation time to assign to the - # new message; it defaults to the current time. - # For example: - # - # imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now) - # Subject: hello - # From: shugo@ruby-lang.org - # To: shugo@ruby-lang.org - # - # hello world - # EOF - # - # A Net::IMAP::NoResponseError is raised if the mailbox does - # not exist (it is not created automatically), or if the flags, - # date_time, or message arguments contain errors. - def append(mailbox, message, flags = nil, date_time = nil) - args = [] - if flags - args.push(flags) - end - args.push(date_time) if date_time - args.push(Literal.new(message)) - send_command("APPEND", mailbox, *args) - end - - # Sends a CHECK command to request a checkpoint of the currently - # selected mailbox. This performs implementation-specific - # housekeeping, for instance, reconciling the mailbox's - # in-memory and on-disk state. - def check - send_command("CHECK") - end - - # Sends a CLOSE command to close the currently selected mailbox. - # The CLOSE command permanently removes from the mailbox all - # messages that have the \Deleted flag set. - def close - send_command("CLOSE") - end - - # Sends a EXPUNGE command to permanently remove from the currently - # selected mailbox all messages that have the \Deleted flag set. - def expunge - synchronize do - send_command("EXPUNGE") - return @responses.delete("EXPUNGE") - end - end - - # Sends a SEARCH command to search the mailbox for messages that - # match the given searching criteria, and returns message sequence - # numbers. +keys+ can either be a string holding the entire - # search string, or a single-dimension array of search keywords and - # arguments. The following are some common search criteria; - # see [IMAP] section 6.4.4 for a full list. - # - # <message set>:: a set of message sequence numbers. ',' indicates - # an interval, ':' indicates a range. For instance, - # '2,10:12,15' means "2,10,11,12,15". - # - # BEFORE <date>:: messages with an internal date strictly before - # <date>. The date argument has a format similar - # to 8-Aug-2002. - # - # BODY <string>:: messages that contain <string> within their body. - # - # CC <string>:: messages containing <string> in their CC field. - # - # FROM <string>:: messages that contain <string> in their FROM field. - # - # NEW:: messages with the \Recent, but not the \Seen, flag set. - # - # NOT <search-key>:: negate the following search key. - # - # OR <search-key> <search-key>:: "or" two search keys together. - # - # ON <date>:: messages with an internal date exactly equal to <date>, - # which has a format similar to 8-Aug-2002. - # - # SINCE <date>:: messages with an internal date on or after <date>. - # - # SUBJECT <string>:: messages with <string> in their subject. - # - # TO <string>:: messages with <string> in their TO field. - # - # For example: - # - # p imap.search(["SUBJECT", "hello", "NOT", "NEW"]) - # #=> [1, 6, 7, 8] - def search(keys, charset = nil) - return search_internal("SEARCH", keys, charset) - end - - # As for #search(), but returns unique identifiers. - def uid_search(keys, charset = nil) - return search_internal("UID SEARCH", keys, charset) - end - - # Sends a FETCH command to retrieve data associated with a message - # in the mailbox. The +set+ parameter is a number or an array of - # numbers or a Range object. The number is a message sequence - # number. +attr+ is a list of attributes to fetch; see the - # documentation for Net::IMAP::FetchData for a list of valid - # attributes. - # The return value is an array of Net::IMAP::FetchData. For example: - # - # p imap.fetch(6..8, "UID") - # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\ - # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\ - # #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>] - # p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]") - # #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>] - # data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0] - # p data.seqno - # #=> 6 - # p data.attr["RFC822.SIZE"] - # #=> 611 - # p data.attr["INTERNALDATE"] - # #=> "12-Oct-2000 22:40:59 +0900" - # p data.attr["UID"] - # #=> 98 - def fetch(set, attr) - return fetch_internal("FETCH", set, attr) - end - - # As for #fetch(), but +set+ contains unique identifiers. - def uid_fetch(set, attr) - return fetch_internal("UID FETCH", set, attr) - end - - # Sends a STORE command to alter data associated with messages - # in the mailbox, in particular their flags. The +set+ parameter - # is a number or an array of numbers or a Range object. Each number - # is a message sequence number. +attr+ is the name of a data item - # to store: 'FLAGS' means to replace the message's flag list - # with the provided one; '+FLAGS' means to add the provided flags; - # and '-FLAGS' means to remove them. +flags+ is a list of flags. - # - # The return value is an array of Net::IMAP::FetchData. For example: - # - # p imap.store(6..8, "+FLAGS", [:Deleted]) - # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\ - # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\ - # #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>] - def store(set, attr, flags) - return store_internal("STORE", set, attr, flags) - end - - # As for #store(), but +set+ contains unique identifiers. - def uid_store(set, attr, flags) - return store_internal("UID STORE", set, attr, flags) - end - - # Sends a COPY command to copy the specified message(s) to the end - # of the specified destination +mailbox+. The +set+ parameter is - # a number or an array of numbers or a Range object. The number is - # a message sequence number. - def copy(set, mailbox) - copy_internal("COPY", set, mailbox) - end - - # As for #copy(), but +set+ contains unique identifiers. - def uid_copy(set, mailbox) - copy_internal("UID COPY", set, mailbox) - end - - # Sends a SORT command to sort messages in the mailbox. - # Returns an array of message sequence numbers. For example: - # - # p imap.sort(["FROM"], ["ALL"], "US-ASCII") - # #=> [1, 2, 3, 5, 6, 7, 8, 4, 9] - # p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII") - # #=> [6, 7, 8, 1] - # - # See [SORT-THREAD-EXT] for more details. - def sort(sort_keys, search_keys, charset) - return sort_internal("SORT", sort_keys, search_keys, charset) - end - - # As for #sort(), but returns an array of unique identifiers. - def uid_sort(sort_keys, search_keys, charset) - return sort_internal("UID SORT", sort_keys, search_keys, charset) - end - - # Adds a response handler. For example, to detect when - # the server sends us a new EXISTS response (which normally - # indicates new messages being added to the mail box), - # you could add the following handler after selecting the - # mailbox. - # - # imap.add_response_handler { |resp| - # if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS" - # puts "Mailbox now has #{resp.data} messages" - # end - # } - # - def add_response_handler(handler = Proc.new) - @response_handlers.push(handler) - end - - # Removes the response handler. - def remove_response_handler(handler) - @response_handlers.delete(handler) - end - - # As for #search(), but returns message sequence numbers in threaded - # format, as a Net::IMAP::ThreadMember tree. The supported algorithms - # are: - # - # ORDEREDSUBJECT:: split into single-level threads according to subject, - # ordered by date. - # REFERENCES:: split into threads by parent/child relationships determined - # by which message is a reply to which. - # - # Unlike #search(), +charset+ is a required argument. US-ASCII - # and UTF-8 are sample values. - # - # See [SORT-THREAD-EXT] for more details. - def thread(algorithm, search_keys, charset) - return thread_internal("THREAD", algorithm, search_keys, charset) - end - - # As for #thread(), but returns unique identifiers instead of - # message sequence numbers. - def uid_thread(algorithm, search_keys, charset) - return thread_internal("UID THREAD", algorithm, search_keys, charset) - end - - # Decode a string from modified UTF-7 format to UTF-8. - # - # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a - # slightly modified version of this to encode mailbox names - # containing non-ASCII characters; see [IMAP] section 5.1.3. - # - # Net::IMAP does _not_ automatically encode and decode - # mailbox names to and from utf7. - def self.decode_utf7(s) - return s.gsub(/&(.*?)-/n) { - if $1.empty? - "&" - else - base64 = $1.tr(",", "/") - x = base64.length % 4 - if x > 0 - base64.concat("=" * (4 - x)) - end - u16tou8(base64.unpack("m")[0]) - end - } - end - - # Encode a string from UTF-8 format to modified UTF-7. - def self.encode_utf7(s) - return s.gsub(/(&)|([^\x20-\x25\x27-\x7e]+)/n) { |x| - if $1 - "&-" - else - base64 = [u8tou16(x)].pack("m") - "&" + base64.delete("=\n").tr("/", ",") + "-" - end - } - end - - private - - CRLF = "\r\n" # :nodoc: - PORT = 143 # :nodoc: - - @@debug = false - @@authenticators = {} - - # Creates a new Net::IMAP object and connects it to the specified - # +port+ (143 by default) on the named +host+. If +usessl+ is true, - # then an attempt will - # be made to use SSL (now TLS) to connect to the server. For this - # to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] - # extensions need to be installed. The +certs+ parameter indicates - # the path or file containing the CA cert of the server, and the - # +verify+ parameter is for the OpenSSL verification callback. - # - # The most common errors are: - # - # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening - # firewall. - # Errno::ETIMEDOUT:: connection timed out (possibly due to packets - # being dropped by an intervening firewall). - # Errno::ENETUNREACH:: there is no route to that network. - # SocketError:: hostname not known or other socket error. - # Net::IMAP::ByeResponseError:: we connected to the host, but they - # immediately said goodbye to us. - def initialize(host, port = PORT, usessl = false, certs = nil, verify = false) - super() - @host = host - @port = port - @tag_prefix = "RUBY" - @tagno = 0 - @parser = ResponseParser.new - @sock = TCPSocket.open(host, port) - if usessl - unless defined?(OpenSSL) - raise "SSL extension not installed" - end - @usessl = true - - # verify the server. - context = SSLContext::new() - context.ca_file = certs if certs && FileTest::file?(certs) - context.ca_path = certs if certs && FileTest::directory?(certs) - context.verify_mode = VERIFY_PEER if verify - if defined?(VerifyCallbackProc) - context.verify_callback = VerifyCallbackProc - end - @sock = SSLSocket.new(@sock, context) - @sock.connect # start ssl session. - @sock.post_connection_check(@host) if verify - else - @usessl = false - end - @responses = Hash.new([].freeze) - @tagged_responses = {} - @response_handlers = [] - @response_arrival = new_cond - @continuation_request = nil - @logout_command_tag = nil - @debug_output_bol = true - - @greeting = get_response - if @greeting.name == "BYE" - @sock.close - raise ByeResponseError, @greeting.raw_data - end - - @client_thread = Thread.current - @receiver_thread = Thread.start { - receive_responses - } - end - - def receive_responses - while true - begin - resp = get_response - rescue Exception - @sock.close - @client_thread.raise($!) - break - end - break unless resp - begin - synchronize do - case resp - when TaggedResponse - @tagged_responses[resp.tag] = resp - @response_arrival.broadcast - if resp.tag == @logout_command_tag - return - end - when UntaggedResponse - record_response(resp.name, resp.data) - if resp.data.instance_of?(ResponseText) && - (code = resp.data.code) - record_response(code.name, code.data) - end - if resp.name == "BYE" && @logout_command_tag.nil? - @sock.close - raise ByeResponseError, resp.raw_data - end - when ContinuationRequest - @continuation_request = resp - @response_arrival.broadcast - end - @response_handlers.each do |handler| - handler.call(resp) - end - end - rescue Exception - @client_thread.raise($!) - end - end - end - - def get_tagged_response(tag) - until @tagged_responses.key?(tag) - @response_arrival.wait - end - return pick_up_tagged_response(tag) - end - - def pick_up_tagged_response(tag) - resp = @tagged_responses.delete(tag) - case resp.name - when /\A(?:NO)\z/ni - raise NoResponseError, resp.data.text - when /\A(?:BAD)\z/ni - raise BadResponseError, resp.data.text - else - return resp - end - end - - def get_response - buff = "" - while true - s = @sock.gets(CRLF) - break unless s - buff.concat(s) - if /\{(\d+)\}\r\n/n =~ s - s = @sock.read($1.to_i) - buff.concat(s) - else - break - end - end - return nil if buff.length == 0 - if @@debug - $stderr.print(buff.gsub(/^/n, "S: ")) - end - return @parser.parse(buff) - end - - def record_response(name, data) - unless @responses.has_key?(name) - @responses[name] = [] - end - @responses[name].push(data) - end - - def send_command(cmd, *args, &block) - synchronize do - tag = Thread.current[:net_imap_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) - ensure - if block - remove_response_handler(block) - end - end - end - end - - def generate_tag - @tagno += 1 - return format("%s%04d", @tag_prefix, @tagno) - end - - def put_string(str) - @sock.print(str) - if @@debug - if @debug_output_bol - $stderr.print("C: ") - end - $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: ")) - if /\r\n\z/n.match(str) - @debug_output_bol = true - else - @debug_output_bol = false - end - end - end - - def send_data(data) - case data - when nil - put_string("NIL") - when String - send_string_data(data) - when Integer - send_number_data(data) - when Array - send_list_data(data) - when Time - send_time_data(data) - when Symbol - send_symbol_data(data) - else - data.send_data(self) - end - end - - def send_string_data(str) - case str - when "" - put_string('""') - when /[\x80-\xff\r\n]/n - # literal - send_literal(str) - when /[(){ \x00-\x1f\x7f%*"\\]/n - # quoted string - send_quoted_string(str) - else - put_string(str) - end - end - - def send_quoted_string(str) - put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"') - end - - def send_literal(str) - put_string("{" + str.length.to_s + "}" + CRLF) - while @continuation_request.nil? && - !@tagged_responses.key?(Thread.current[:net_imap_tag]) - @response_arrival.wait - end - if @continuation_request.nil? - pick_up_tagged_response(Thread.current[:net_imap_tag]) - raise ResponseError.new("expected continuation request") - end - @continuation_request = nil - put_string(str) - end - - def send_number_data(num) - if num < 0 || num >= 4294967296 - raise DataFormatError, num.to_s - end - put_string(num.to_s) - end - - def send_list_data(list) - put_string("(") - first = true - list.each do |i| - if first - first = false - else - put_string(" ") - end - send_data(i) - end - put_string(")") - end - - DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) - - def send_time_data(time) - t = time.dup.gmtime - s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"', - t.day, DATE_MONTH[t.month - 1], t.year, - t.hour, t.min, t.sec) - put_string(s) - end - - def send_symbol_data(symbol) - put_string("\\" + symbol.to_s) - end - - def search_internal(cmd, keys, charset) - if keys.instance_of?(String) - keys = [RawData.new(keys)] - else - normalize_searching_criteria(keys) - end - synchronize do - if charset - send_command(cmd, "CHARSET", charset, *keys) - else - send_command(cmd, *keys) - end - return @responses.delete("SEARCH")[-1] - end - end - - def fetch_internal(cmd, set, attr) - if attr.instance_of?(String) - attr = RawData.new(attr) - end - synchronize do - @responses.delete("FETCH") - send_command(cmd, MessageSet.new(set), attr) - return @responses.delete("FETCH") - end - end - - def store_internal(cmd, set, attr, flags) - if attr.instance_of?(String) - attr = RawData.new(attr) - end - synchronize do - @responses.delete("FETCH") - send_command(cmd, MessageSet.new(set), attr, flags) - return @responses.delete("FETCH") - end - end - - def copy_internal(cmd, set, mailbox) - send_command(cmd, MessageSet.new(set), mailbox) - end - - def sort_internal(cmd, sort_keys, search_keys, charset) - if search_keys.instance_of?(String) - search_keys = [RawData.new(search_keys)] - else - normalize_searching_criteria(search_keys) - end - normalize_searching_criteria(search_keys) - synchronize do - send_command(cmd, sort_keys, charset, *search_keys) - return @responses.delete("SORT")[-1] - end - end - - def thread_internal(cmd, algorithm, search_keys, charset) - if search_keys.instance_of?(String) - search_keys = [RawData.new(search_keys)] - else - normalize_searching_criteria(search_keys) - end - normalize_searching_criteria(search_keys) - send_command(cmd, algorithm, charset, *search_keys) - return @responses.delete("THREAD")[-1] - end - - def normalize_searching_criteria(keys) - keys.collect! do |i| - case i - when -1, Range, Array - MessageSet.new(i) - else - i - end - end - end - - def self.u16tou8(s) - len = s.length - if len < 2 - return "" - end - buf = "" - i = 0 - while i < len - c = s[i] << 8 | s[i + 1] - i += 2 - if c == 0xfeff - next - elsif c < 0x0080 - buf.concat(c) - elsif c < 0x0800 - b2 = c & 0x003f - b1 = c >> 6 - buf.concat(b1 | 0xc0) - buf.concat(b2 | 0x80) - elsif c >= 0xdc00 && c < 0xe000 - raise DataFormatError, "invalid surrogate detected" - elsif c >= 0xd800 && c < 0xdc00 - if i + 2 > len - raise DataFormatError, "invalid surrogate detected" - end - low = s[i] << 8 | s[i + 1] - i += 2 - if low < 0xdc00 || low > 0xdfff - raise DataFormatError, "invalid surrogate detected" - end - c = (((c & 0x03ff)) << 10 | (low & 0x03ff)) + 0x10000 - b4 = c & 0x003f - b3 = (c >> 6) & 0x003f - b2 = (c >> 12) & 0x003f - b1 = c >> 18; - buf.concat(b1 | 0xf0) - buf.concat(b2 | 0x80) - buf.concat(b3 | 0x80) - buf.concat(b4 | 0x80) - else # 0x0800-0xffff - b3 = c & 0x003f - b2 = (c >> 6) & 0x003f - b1 = c >> 12 - buf.concat(b1 | 0xe0) - buf.concat(b2 | 0x80) - buf.concat(b3 | 0x80) - end - end - return buf - end - private_class_method :u16tou8 - - def self.u8tou16(s) - len = s.length - buf = "" - i = 0 - while i < len - c = s[i] - if (c & 0x80) == 0 - buf.concat(0x00) - buf.concat(c) - i += 1 - elsif (c & 0xe0) == 0xc0 && - len >= 2 && - (s[i + 1] & 0xc0) == 0x80 - if c == 0xc0 || c == 0xc1 - raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c) - end - u = ((c & 0x1f) << 6) | (s[i + 1] & 0x3f) - buf.concat(u >> 8) - buf.concat(u & 0x00ff) - i += 2 - elsif (c & 0xf0) == 0xe0 && - i + 2 < len && - (s[i + 1] & 0xc0) == 0x80 && - (s[i + 2] & 0xc0) == 0x80 - if c == 0xe0 && s[i + 1] < 0xa0 - raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c) - end - u = ((c & 0x0f) << 12) | ((s[i + 1] & 0x3f) << 6) | (s[i + 2] & 0x3f) - # surrogate chars - if u >= 0xd800 && u <= 0xdfff - raise DataFormatError, format("none-UTF-16 char detected (%04x)", u) - end - buf.concat(u >> 8) - buf.concat(u & 0x00ff) - i += 3 - elsif (c & 0xf8) == 0xf0 && - i + 3 < len && - (s[i + 1] & 0xc0) == 0x80 && - (s[i + 2] & 0xc0) == 0x80 && - (s[i + 3] & 0xc0) == 0x80 - if c == 0xf0 && s[i + 1] < 0x90 - raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c) - end - u = ((c & 0x07) << 18) | ((s[i + 1] & 0x3f) << 12) | - ((s[i + 2] & 0x3f) << 6) | (s[i + 3] & 0x3f) - if u < 0x10000 - buf.concat(u >> 8) - buf.concat(u & 0x00ff) - elsif u < 0x110000 - high = ((u - 0x10000) >> 10) | 0xd800 - low = (u & 0x03ff) | 0xdc00 - buf.concat(high >> 8) - buf.concat(high & 0x00ff) - buf.concat(low >> 8) - buf.concat(low & 0x00ff) - else - raise DataFormatError, format("none-UTF-16 char detected (%04x)", u) - end - i += 4 - else - raise DataFormatError, format("illegal UTF-8 sequence (%02x)", c) - end - end - return buf - end - private_class_method :u8tou16 - - class RawData # :nodoc: - def send_data(imap) - imap.send(:put_string, @data) - end - - private - - def initialize(data) - @data = data - end - end - - class Atom # :nodoc: - def send_data(imap) - imap.send(:put_string, @data) - end - - private - - def initialize(data) - @data = data - end - end - - class QuotedString # :nodoc: - def send_data(imap) - imap.send(:send_quoted_string, @data) - end - - private - - def initialize(data) - @data = data - end - end - - class Literal # :nodoc: - def send_data(imap) - imap.send(:send_literal, @data) - end - - private - - def initialize(data) - @data = data - end - end - - class MessageSet # :nodoc: - def send_data(imap) - imap.send(:put_string, format_internal(@data)) - end - - private - - def initialize(data) - @data = data - end - - def format_internal(data) - case data - when "*" - return data - when Integer - ensure_nz_number(data) - if data == -1 - return "*" - else - return data.to_s - end - when Range - return format_internal(data.first) + - ":" + format_internal(data.last) - when Array - return data.collect {|i| format_internal(i)}.join(",") - when ThreadMember - return data.seqno.to_s + - ":" + data.children.collect {|i| format_internal(i).join(",")} - else - raise DataFormatError, data.inspect - end - end - - def ensure_nz_number(num) - if num < -1 || num == 0 || num >= 4294967296 - msg = "nz_number must be non-zero unsigned 32-bit integer: " + - num.inspect - raise DataFormatError, msg - end - end - end - - # Net::IMAP::ContinuationRequest represents command continuation requests. - # - # The command continuation request response is indicated by a "+" token - # instead of a tag. This form of response indicates that the server is - # ready to accept the continuation of a command from the client. The - # remainder of this response is a line of text. - # - # continue_req ::= "+" SPACE (resp_text / base64) - # - # ==== Fields: - # - # data:: Returns the data (Net::IMAP::ResponseText). - # - # raw_data:: Returns the raw data string. - ContinuationRequest = Struct.new(:data, :raw_data) - - # Net::IMAP::UntaggedResponse represents untagged responses. - # - # Data transmitted by the server to the client and status responses - # that do not indicate command completion are prefixed with the token - # "*", and are called untagged responses. - # - # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye / - # mailbox_data / message_data / capability_data) - # - # ==== Fields: - # - # name:: Returns the name such as "FLAGS", "LIST", "FETCH".... - # - # data:: Returns the data such as an array of flag symbols, - # a ((<Net::IMAP::MailboxList>)) object.... - # - # raw_data:: Returns the raw data string. - UntaggedResponse = Struct.new(:name, :data, :raw_data) - - # Net::IMAP::TaggedResponse represents tagged responses. - # - # The server completion result response indicates the success or - # failure of the operation. It is tagged with the same tag as the - # client command which began the operation. - # - # response_tagged ::= tag SPACE resp_cond_state CRLF - # - # tag ::= 1*<any ATOM_CHAR except "+"> - # - # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text - # - # ==== Fields: - # - # tag:: Returns the tag. - # - # name:: Returns the name. the name is one of "OK", "NO", "BAD". - # - # data:: Returns the data. See ((<Net::IMAP::ResponseText>)). - # - # raw_data:: Returns the raw data string. - # - TaggedResponse = Struct.new(:tag, :name, :data, :raw_data) - - # Net::IMAP::ResponseText represents texts of responses. - # The text may be prefixed by the response code. - # - # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text) - # ;; text SHOULD NOT begin with "[" or "=" - # - # ==== Fields: - # - # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)). - # - # text:: Returns the text. - # - ResponseText = Struct.new(:code, :text) - - # - # Net::IMAP::ResponseCode represents response codes. - # - # resp_text_code ::= "ALERT" / "PARSE" / - # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" / - # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" / - # "UIDVALIDITY" SPACE nz_number / - # "UNSEEN" SPACE nz_number / - # atom [SPACE 1*<any TEXT_CHAR except "]">] - # - # ==== Fields: - # - # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY".... - # - # data:: Returns the data if it exists. - # - ResponseCode = Struct.new(:name, :data) - - # Net::IMAP::MailboxList represents contents of the LIST response. - # - # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" / - # "\Noselect" / "\Unmarked" / flag_extension) ")" - # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox - # - # ==== Fields: - # - # attr:: Returns the name attributes. Each name attribute is a symbol - # capitalized by String#capitalize, such as :Noselect (not :NoSelect). - # - # delim:: Returns the hierarchy delimiter - # - # name:: Returns the mailbox name. - # - MailboxList = Struct.new(:attr, :delim, :name) - - # Net::IMAP::MailboxQuota represents contents of GETQUOTA response. - # This object can also be a response to GETQUOTAROOT. In the syntax - # specification below, the delimiter used with the "#" construct is a - # single space (SPACE). - # - # quota_list ::= "(" #quota_resource ")" - # - # quota_resource ::= atom SPACE number SPACE number - # - # quota_response ::= "QUOTA" SPACE astring SPACE quota_list - # - # ==== Fields: - # - # mailbox:: The mailbox with the associated quota. - # - # usage:: Current storage usage of mailbox. - # - # quota:: Quota limit imposed on mailbox. - # - MailboxQuota = Struct.new(:mailbox, :usage, :quota) - - # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT - # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.) - # - # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring) - # - # ==== Fields: - # - # mailbox:: The mailbox with the associated quota. - # - # quotaroots:: Zero or more quotaroots that effect the quota on the - # specified mailbox. - # - MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots) - - # Net::IMAP::MailboxACLItem represents response from GETACL. - # - # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights) - # - # identifier ::= astring - # - # rights ::= astring - # - # ==== Fields: - # - # user:: Login name that has certain rights to the mailbox - # that was specified with the getacl command. - # - # rights:: The access rights the indicated user has to the - # mailbox. - # - MailboxACLItem = Struct.new(:user, :rights) - - # Net::IMAP::StatusData represents contents of the STATUS response. - # - # ==== Fields: - # - # mailbox:: Returns the mailbox name. - # - # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT", - # "UIDVALIDITY", "UNSEEN". Each value is a number. - # - StatusData = Struct.new(:mailbox, :attr) - - # Net::IMAP::FetchData represents contents of the FETCH response. - # - # ==== Fields: - # - # seqno:: Returns the message sequence number. - # (Note: not the unique identifier, even for the UID command response.) - # - # attr:: Returns a hash. Each key is a data item name, and each value is - # its value. - # - # The current data items are: - # - # [BODY] - # A form of BODYSTRUCTURE without extension data. - # [BODY[<section>]<<origin_octet>>] - # A string expressing the body contents of the specified section. - # [BODYSTRUCTURE] - # An object that describes the [MIME-IMB] body structure of a message. - # See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText, - # Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart. - # [ENVELOPE] - # A Net::IMAP::Envelope object that describes the envelope - # structure of a message. - # [FLAGS] - # A array of flag symbols that are set for this message. flag symbols - # are capitalized by String#capitalize. - # [INTERNALDATE] - # A string representing the internal date of the message. - # [RFC822] - # Equivalent to BODY[]. - # [RFC822.HEADER] - # Equivalent to BODY.PEEK[HEADER]. - # [RFC822.SIZE] - # A number expressing the [RFC-822] size of the message. - # [RFC822.TEXT] - # Equivalent to BODY[TEXT]. - # [UID] - # A number expressing the unique identifier of the message. - # - FetchData = Struct.new(:seqno, :attr) - - # Net::IMAP::Envelope represents envelope structures of messages. - # - # ==== Fields: - # - # date:: Returns a string that represents the date. - # - # subject:: Returns a string that represents the subject. - # - # from:: Returns an array of Net::IMAP::Address that represents the from. - # - # sender:: Returns an array of Net::IMAP::Address that represents the sender. - # - # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to. - # - # to:: Returns an array of Net::IMAP::Address that represents the to. - # - # cc:: Returns an array of Net::IMAP::Address that represents the cc. - # - # bcc:: Returns an array of Net::IMAP::Address that represents the bcc. - # - # in_reply_to:: Returns a string that represents the in-reply-to. - # - # message_id:: Returns a string that represents the message-id. - # - Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to, - :to, :cc, :bcc, :in_reply_to, :message_id) - - # - # Net::IMAP::Address represents electronic mail addresses. - # - # ==== Fields: - # - # name:: Returns the phrase from [RFC-822] mailbox. - # - # route:: Returns the route from [RFC-822] route-addr. - # - # mailbox:: nil indicates end of [RFC-822] group. - # If non-nil and host is nil, returns [RFC-822] group name. - # Otherwise, returns [RFC-822] local-part - # - # host:: nil indicates [RFC-822] group syntax. - # Otherwise, returns [RFC-822] domain name. - # - Address = Struct.new(:name, :route, :mailbox, :host) - - # - # Net::IMAP::ContentDisposition represents Content-Disposition fields. - # - # ==== Fields: - # - # dsp_type:: Returns the disposition type. - # - # param:: Returns a hash that represents parameters of the Content-Disposition - # field. - # - ContentDisposition = Struct.new(:dsp_type, :param) - - # Net::IMAP::ThreadMember represents a thread-node returned - # by Net::IMAP#thread - # - # ==== Fields: - # - # seqno:: The sequence number of this message. - # - # children:: an array of Net::IMAP::ThreadMember objects for mail - # items that are children of this in the thread. - # - ThreadMember = Struct.new(:seqno, :children) - - # Net::IMAP::BodyTypeBasic represents basic body structures of messages. - # - # ==== Fields: - # - # media_type:: Returns the content media type name as defined in [MIME-IMB]. - # - # subtype:: Returns the content subtype name as defined in [MIME-IMB]. - # - # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. - # - # content_id:: Returns a string giving the content id as defined in [MIME-IMB]. - # - # description:: Returns a string giving the content description as defined in - # [MIME-IMB]. - # - # encoding:: Returns a string giving the content transfer encoding as defined in - # [MIME-IMB]. - # - # size:: Returns a number giving the size of the body in octets. - # - # md5:: Returns a string giving the body MD5 value as defined in [MD5]. - # - # disposition:: Returns a Net::IMAP::ContentDisposition object giving - # the content disposition. - # - # language:: Returns a string or an array of strings giving the body - # language value as defined in [LANGUAGE-TAGS]. - # - # extension:: Returns extension data. - # - # multipart?:: Returns false. - # - class BodyTypeBasic < Struct.new(:media_type, :subtype, - :param, :content_id, - :description, :encoding, :size, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - - # Obsolete: use +subtype+ instead. Calling this will - # generate a warning message to +stderr+, then return - # the value of +subtype+. - def media_subtype - $stderr.printf("warning: media_subtype is obsolete.\n") - $stderr.printf(" use subtype instead.\n") - return subtype - end - end - - # Net::IMAP::BodyTypeText represents TEXT body structures of messages. - # - # ==== Fields: - # - # lines:: Returns the size of the body in text lines. - # - # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic. - # - class BodyTypeText < Struct.new(:media_type, :subtype, - :param, :content_id, - :description, :encoding, :size, - :lines, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - - # Obsolete: use +subtype+ instead. Calling this will - # generate a warning message to +stderr+, then return - # the value of +subtype+. - def media_subtype - $stderr.printf("warning: media_subtype is obsolete.\n") - $stderr.printf(" use subtype instead.\n") - return subtype - end - end - - # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages. - # - # ==== Fields: - # - # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure. - # - # body:: Returns an object giving the body structure. - # - # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText. - # - class BodyTypeMessage < Struct.new(:media_type, :subtype, - :param, :content_id, - :description, :encoding, :size, - :envelope, :body, :lines, - :md5, :disposition, :language, - :extension) - def multipart? - return false - end - - # Obsolete: use +subtype+ instead. Calling this will - # generate a warning message to +stderr+, then return - # the value of +subtype+. - def media_subtype - $stderr.printf("warning: media_subtype is obsolete.\n") - $stderr.printf(" use subtype instead.\n") - return subtype - end - end - - # Net::IMAP::BodyTypeMultipart represents multipart body structures - # of messages. - # - # ==== Fields: - # - # media_type:: Returns the content media type name as defined in [MIME-IMB]. - # - # subtype:: Returns the content subtype name as defined in [MIME-IMB]. - # - # parts:: Returns multiple parts. - # - # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. - # - # disposition:: Returns a Net::IMAP::ContentDisposition object giving - # the content disposition. - # - # language:: Returns a string or an array of strings giving the body - # language value as defined in [LANGUAGE-TAGS]. - # - # extension:: Returns extension data. - # - # multipart?:: Returns true. - # - class BodyTypeMultipart < Struct.new(:media_type, :subtype, - :parts, - :param, :disposition, :language, - :extension) - def multipart? - return true - end - - # Obsolete: use +subtype+ instead. Calling this will - # generate a warning message to +stderr+, then return - # the value of +subtype+. - def media_subtype - $stderr.printf("warning: media_subtype is obsolete.\n") - $stderr.printf(" use subtype instead.\n") - return subtype - end - end - - class ResponseParser # :nodoc: - def parse(str) - @str = str - @pos = 0 - @lex_state = EXPR_BEG - @token = nil - return response - end - - private - - EXPR_BEG = :EXPR_BEG - EXPR_DATA = :EXPR_DATA - EXPR_TEXT = :EXPR_TEXT - EXPR_RTEXT = :EXPR_RTEXT - EXPR_CTEXT = :EXPR_CTEXT - - T_SPACE = :SPACE - T_NIL = :NIL - T_NUMBER = :NUMBER - T_ATOM = :ATOM - T_QUOTED = :QUOTED - T_LPAR = :LPAR - T_RPAR = :RPAR - T_BSLASH = :BSLASH - T_STAR = :STAR - T_LBRA = :LBRA - T_RBRA = :RBRA - T_LITERAL = :LITERAL - T_PLUS = :PLUS - T_PERCENT = :PERCENT - T_CRLF = :CRLF - T_EOF = :EOF - T_TEXT = :TEXT - - BEG_REGEXP = /\G(?:\ -(?# 1: SPACE )( +)|\ -(?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\ -(?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\ -(?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\ -(?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ -(?# 6: LPAR )(\()|\ -(?# 7: RPAR )(\))|\ -(?# 8: BSLASH )(\\)|\ -(?# 9: STAR )(\*)|\ -(?# 10: LBRA )(\[)|\ -(?# 11: RBRA )(\])|\ -(?# 12: LITERAL )\{(\d+)\}\r\n|\ -(?# 13: PLUS )(\+)|\ -(?# 14: PERCENT )(%)|\ -(?# 15: CRLF )(\r\n)|\ -(?# 16: EOF )(\z))/ni - - DATA_REGEXP = /\G(?:\ -(?# 1: SPACE )( )|\ -(?# 2: NIL )(NIL)|\ -(?# 3: NUMBER )(\d+)|\ -(?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ -(?# 5: LITERAL )\{(\d+)\}\r\n|\ -(?# 6: LPAR )(\()|\ -(?# 7: RPAR )(\)))/ni - - TEXT_REGEXP = /\G(?:\ -(?# 1: TEXT )([^\x00\r\n]*))/ni - - RTEXT_REGEXP = /\G(?:\ -(?# 1: LBRA )(\[)|\ -(?# 2: TEXT )([^\x00\r\n]*))/ni - - CTEXT_REGEXP = /\G(?:\ -(?# 1: TEXT )([^\x00\r\n\]]*))/ni - - Token = Struct.new(:symbol, :value) - - def response - token = lookahead - case token.symbol - when T_PLUS - result = continue_req - when T_STAR - result = response_untagged - else - result = response_tagged - end - match(T_CRLF) - match(T_EOF) - return result - end - - def continue_req - match(T_PLUS) - match(T_SPACE) - return ContinuationRequest.new(resp_text, @str) - end - - def response_untagged - match(T_STAR) - match(T_SPACE) - token = lookahead - if token.symbol == T_NUMBER - return numeric_response - elsif token.symbol == T_ATOM - case token.value - when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni - return response_cond - when /\A(?:FLAGS)\z/ni - return flags_response - when /\A(?:LIST|LSUB)\z/ni - return list_response - when /\A(?:QUOTA)\z/ni - return getquota_response - when /\A(?:QUOTAROOT)\z/ni - return getquotaroot_response - when /\A(?:ACL)\z/ni - return getacl_response - when /\A(?:SEARCH|SORT)\z/ni - return search_response - when /\A(?:THREAD)\z/ni - return thread_response - when /\A(?:STATUS)\z/ni - return status_response - when /\A(?:CAPABILITY)\z/ni - return capability_response - else - return text_response - end - else - parse_error("unexpected token %s", token.symbol) - end - end - - def response_tagged - tag = atom - match(T_SPACE) - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return TaggedResponse.new(tag, name, resp_text, @str) - end - - def response_cond - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return UntaggedResponse.new(name, resp_text, @str) - end - - def numeric_response - n = number - match(T_SPACE) - token = match(T_ATOM) - name = token.value.upcase - case name - when "EXISTS", "RECENT", "EXPUNGE" - return UntaggedResponse.new(name, n, @str) - when "FETCH" - shift_token - match(T_SPACE) - data = FetchData.new(n, msg_att) - return UntaggedResponse.new(name, data, @str) - end - end - - def msg_att - match(T_LPAR) - attr = {} - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - break - when T_SPACE - shift_token - token = lookahead - end - case token.value - when /\A(?:ENVELOPE)\z/ni - name, val = envelope_data - when /\A(?:FLAGS)\z/ni - name, val = flags_data - when /\A(?:INTERNALDATE)\z/ni - name, val = internaldate_data - when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni - name, val = rfc822_text - when /\A(?:RFC822\.SIZE)\z/ni - name, val = rfc822_size - when /\A(?:BODY(?:STRUCTURE)?)\z/ni - name, val = body_data - when /\A(?:UID)\z/ni - name, val = uid_data - else - parse_error("unknown attribute `%s'", token.value) - end - attr[name] = val - end - return attr - end - - def envelope_data - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, envelope - end - - def envelope - @lex_state = EXPR_DATA - token = lookahead - if token.symbol == T_NIL - shift_token - result = nil - else - match(T_LPAR) - date = nstring - match(T_SPACE) - subject = nstring - match(T_SPACE) - from = address_list - match(T_SPACE) - sender = address_list - match(T_SPACE) - reply_to = address_list - match(T_SPACE) - to = address_list - match(T_SPACE) - cc = address_list - match(T_SPACE) - bcc = address_list - match(T_SPACE) - in_reply_to = nstring - match(T_SPACE) - message_id = nstring - match(T_RPAR) - result = Envelope.new(date, subject, from, sender, reply_to, - to, cc, bcc, in_reply_to, message_id) - end - @lex_state = EXPR_BEG - return result - end - - def flags_data - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, flag_list - end - - def internaldate_data - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - token = match(T_QUOTED) - return name, token.value - end - - def rfc822_text - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, nstring - end - - def rfc822_size - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, number - end - - def body_data - token = match(T_ATOM) - name = token.value.upcase - token = lookahead - if token.symbol == T_SPACE - shift_token - return name, body - end - name.concat(section) - token = lookahead - if token.symbol == T_ATOM - name.concat(token.value) - shift_token - end - match(T_SPACE) - data = nstring - return name, data - end - - def body - @lex_state = EXPR_DATA - token = lookahead - if token.symbol == T_NIL - shift_token - result = nil - else - match(T_LPAR) - token = lookahead - if token.symbol == T_LPAR - result = body_type_mpart - else - result = body_type_1part - end - match(T_RPAR) - end - @lex_state = EXPR_BEG - return result - end - - def body_type_1part - token = lookahead - case token.value - when /\A(?:TEXT)\z/ni - return body_type_text - when /\A(?:MESSAGE)\z/ni - return body_type_msg - else - return body_type_basic - end - end - - def body_type_basic - mtype, msubtype = media_type - token = lookahead - if token.symbol == T_RPAR - return BodyTypeBasic.new(mtype, msubtype) - end - match(T_SPACE) - param, content_id, desc, enc, size = body_fields - md5, disposition, language, extension = body_ext_1part - return BodyTypeBasic.new(mtype, msubtype, - param, content_id, - desc, enc, size, - md5, disposition, language, extension) - end - - def body_type_text - mtype, msubtype = media_type - match(T_SPACE) - param, content_id, desc, enc, size = body_fields - match(T_SPACE) - lines = number - md5, disposition, language, extension = body_ext_1part - return BodyTypeText.new(mtype, msubtype, - param, content_id, - desc, enc, size, - lines, - md5, disposition, language, extension) - end - - def body_type_msg - mtype, msubtype = media_type - match(T_SPACE) - param, content_id, desc, enc, size = body_fields - match(T_SPACE) - env = envelope - match(T_SPACE) - b = body - match(T_SPACE) - lines = number - md5, disposition, language, extension = body_ext_1part - return BodyTypeMessage.new(mtype, msubtype, - param, content_id, - desc, enc, size, - env, b, lines, - md5, disposition, language, extension) - end - - def body_type_mpart - parts = [] - while true - token = lookahead - if token.symbol == T_SPACE - shift_token - break - end - parts.push(body) - end - mtype = "MULTIPART" - msubtype = case_insensitive_string - param, disposition, language, extension = body_ext_mpart - return BodyTypeMultipart.new(mtype, msubtype, parts, - param, disposition, language, - extension) - end - - def media_type - mtype = case_insensitive_string - match(T_SPACE) - msubtype = case_insensitive_string - return mtype, msubtype - end - - def body_fields - param = body_fld_param - match(T_SPACE) - content_id = nstring - match(T_SPACE) - desc = nstring - match(T_SPACE) - enc = case_insensitive_string - match(T_SPACE) - size = number - return param, content_id, desc, enc, size - end - - def body_fld_param - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - match(T_LPAR) - param = {} - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - break - when T_SPACE - shift_token - end - name = case_insensitive_string - match(T_SPACE) - val = string - param[name] = val - end - return param - end - - def body_ext_1part - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return nil - end - md5 = nstring - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return md5 - end - disposition = body_fld_dsp - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return md5, disposition - end - language = body_fld_lang - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return md5, disposition, language - end - - extension = body_extensions - return md5, disposition, language, extension - end - - def body_ext_mpart - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return nil - end - param = body_fld_param - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return param - end - disposition = body_fld_dsp - match(T_SPACE) - language = body_fld_lang - - token = lookahead - if token.symbol == T_SPACE - shift_token - else - return param, disposition, language - end - - extension = body_extensions - return param, disposition, language, extension - end - - def body_fld_dsp - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - match(T_LPAR) - dsp_type = case_insensitive_string - match(T_SPACE) - param = body_fld_param - match(T_RPAR) - return ContentDisposition.new(dsp_type, param) - end - - def body_fld_lang - token = lookahead - if token.symbol == T_LPAR - shift_token - result = [] - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - return result - when T_SPACE - shift_token - end - result.push(case_insensitive_string) - end - else - lang = nstring - if lang - return lang.upcase - else - return lang - end - end - end - - def body_extensions - result = [] - while true - token = lookahead - case token.symbol - when T_RPAR - return result - when T_SPACE - shift_token - end - result.push(body_extension) - end - end - - def body_extension - token = lookahead - case token.symbol - when T_LPAR - shift_token - result = body_extensions - match(T_RPAR) - return result - when T_NUMBER - return number - else - return nstring - end - end - - def section - str = "" - token = match(T_LBRA) - str.concat(token.value) - token = match(T_ATOM, T_NUMBER, T_RBRA) - if token.symbol == T_RBRA - str.concat(token.value) - return str - end - str.concat(token.value) - token = lookahead - if token.symbol == T_SPACE - shift_token - str.concat(token.value) - token = match(T_LPAR) - str.concat(token.value) - while true - token = lookahead - case token.symbol - when T_RPAR - str.concat(token.value) - shift_token - break - when T_SPACE - shift_token - str.concat(token.value) - end - str.concat(format_string(astring)) - end - end - token = match(T_RBRA) - str.concat(token.value) - return str - end - - def format_string(str) - case str - when "" - return '""' - when /[\x80-\xff\r\n]/n - # literal - return "{" + str.length.to_s + "}" + CRLF + str - when /[(){ \x00-\x1f\x7f%*"\\]/n - # quoted string - return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"' - else - # atom - return str - end - end - - def uid_data - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return name, number - end - - def text_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - @lex_state = EXPR_TEXT - token = match(T_TEXT) - @lex_state = EXPR_BEG - return UntaggedResponse.new(name, token.value) - end - - def flags_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return UntaggedResponse.new(name, flag_list, @str) - end - - def list_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return UntaggedResponse.new(name, mailbox_list, @str) - end - - def mailbox_list - attr = flag_list - match(T_SPACE) - token = match(T_QUOTED, T_NIL) - if token.symbol == T_NIL - delim = nil - else - delim = token.value - end - match(T_SPACE) - name = astring - return MailboxList.new(attr, delim, name) - end - - def getquota_response - # If quota never established, get back - # `NO Quota root does not exist'. - # If quota removed, get `()' after the - # folder spec with no mention of `STORAGE'. - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - mailbox = astring - match(T_SPACE) - match(T_LPAR) - token = lookahead - case token.symbol - when T_RPAR - shift_token - data = MailboxQuota.new(mailbox, nil, nil) - return UntaggedResponse.new(name, data, @str) - when T_ATOM - shift_token - match(T_SPACE) - token = match(T_NUMBER) - usage = token.value - match(T_SPACE) - token = match(T_NUMBER) - quota = token.value - match(T_RPAR) - data = MailboxQuota.new(mailbox, usage, quota) - return UntaggedResponse.new(name, data, @str) - else - parse_error("unexpected token %s", token.symbol) - end - end - - def getquotaroot_response - # Similar to getquota, but only admin can use getquota. - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - mailbox = astring - quotaroots = [] - while true - token = lookahead - break unless token.symbol == T_SPACE - shift_token - quotaroots.push(astring) - end - data = MailboxQuotaRoot.new(mailbox, quotaroots) - return UntaggedResponse.new(name, data, @str) - end - - def getacl_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - mailbox = astring - data = [] - token = lookahead - if token.symbol == T_SPACE - shift_token - while true - token = lookahead - case token.symbol - when T_CRLF - break - when T_SPACE - shift_token - end - user = astring - match(T_SPACE) - rights = astring - ##XXX data.push([user, rights]) - data.push(MailboxACLItem.new(user, rights)) - end - end - return UntaggedResponse.new(name, data, @str) - end - - def search_response - token = match(T_ATOM) - name = token.value.upcase - token = lookahead - if token.symbol == T_SPACE - shift_token - data = [] - while true - token = lookahead - case token.symbol - when T_CRLF - break - when T_SPACE - shift_token - end - data.push(number) - end - else - data = [] - end - return UntaggedResponse.new(name, data, @str) - end - - def thread_response - token = match(T_ATOM) - name = token.value.upcase - token = lookahead - - if token.symbol == T_SPACE - threads = [] - - while true - shift_token - token = lookahead - - case token.symbol - when T_LPAR - threads << thread_branch(token) - when T_CRLF - break - end - end - else - # no member - threads = [] - end - - return UntaggedResponse.new(name, threads, @str) - end - - def thread_branch(token) - rootmember = nil - lastmember = nil - - while true - shift_token # ignore first T_LPAR - token = lookahead - - case token.symbol - when T_NUMBER - # new member - newmember = ThreadMember.new(number, []) - if rootmember.nil? - rootmember = newmember - else - lastmember.children << newmember - end - lastmember = newmember - when T_SPACE - # do nothing - when T_LPAR - if rootmember.nil? - # dummy member - lastmember = rootmember = ThreadMember.new(nil, []) - end - - lastmember.children << thread_branch(token) - when T_RPAR - break - end - end - - return rootmember - end - - def status_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - mailbox = astring - match(T_SPACE) - match(T_LPAR) - attr = {} - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - break - when T_SPACE - shift_token - end - token = match(T_ATOM) - key = token.value.upcase - match(T_SPACE) - val = number - attr[key] = val - end - data = StatusData.new(mailbox, attr) - return UntaggedResponse.new(name, data, @str) - end - - def capability_response - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - data = [] - while true - token = lookahead - case token.symbol - when T_CRLF - break - when T_SPACE - shift_token - end - data.push(atom.upcase) - end - return UntaggedResponse.new(name, data, @str) - end - - def resp_text - @lex_state = EXPR_RTEXT - token = lookahead - if token.symbol == T_LBRA - code = resp_text_code - else - code = nil - end - token = match(T_TEXT) - @lex_state = EXPR_BEG - return ResponseText.new(code, token.value) - end - - def resp_text_code - @lex_state = EXPR_BEG - match(T_LBRA) - token = match(T_ATOM) - name = token.value.upcase - case name - when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n - result = ResponseCode.new(name, nil) - when /\A(?:PERMANENTFLAGS)\z/n - match(T_SPACE) - result = ResponseCode.new(name, flag_list) - when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n - match(T_SPACE) - result = ResponseCode.new(name, number) - else - match(T_SPACE) - @lex_state = EXPR_CTEXT - token = match(T_TEXT) - @lex_state = EXPR_BEG - result = ResponseCode.new(name, token.value) - end - match(T_RBRA) - @lex_state = EXPR_RTEXT - return result - end - - def address_list - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - else - result = [] - match(T_LPAR) - while true - token = lookahead - case token.symbol - when T_RPAR - shift_token - break - when T_SPACE - shift_token - end - result.push(address) - end - return result - end - end - - ADDRESS_REGEXP = /\G\ -(?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ -(?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ -(?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ -(?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\ -\)/ni - - def address - match(T_LPAR) - if @str.index(ADDRESS_REGEXP, @pos) - # address does not include literal. - @pos = $~.end(0) - name = $1 - route = $2 - mailbox = $3 - host = $4 - for s in [name, route, mailbox, host] - if s - s.gsub!(/\\(["\\])/n, "\\1") - end - end - else - name = nstring - match(T_SPACE) - route = nstring - match(T_SPACE) - mailbox = nstring - match(T_SPACE) - host = nstring - match(T_RPAR) - end - return Address.new(name, route, mailbox, host) - end - -# def flag_list -# result = [] -# match(T_LPAR) -# while true -# token = lookahead -# case token.symbol -# when T_RPAR -# shift_token -# break -# when T_SPACE -# shift_token -# end -# result.push(flag) -# end -# return result -# end - -# def flag -# token = lookahead -# if token.symbol == T_BSLASH -# shift_token -# token = lookahead -# if token.symbol == T_STAR -# shift_token -# return token.value.intern -# else -# return atom.intern -# end -# else -# return atom -# end -# end - - FLAG_REGEXP = /\ -(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\ -(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n - - def flag_list - if @str.index(/\(([^)]*)\)/ni, @pos) - @pos = $~.end(0) - return $1.scan(FLAG_REGEXP).collect { |flag, atom| - atom || flag.capitalize.intern - } - else - parse_error("invalid flag list") - end - end - - def nstring - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - else - return string - end - end - - def astring - token = lookahead - if string_token?(token) - return string - else - return atom - end - end - - def string - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - token = match(T_QUOTED, T_LITERAL) - return token.value - end - - STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL] - - def string_token?(token) - return STRING_TOKENS.include?(token.symbol) - end - - def case_insensitive_string - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - token = match(T_QUOTED, T_LITERAL) - return token.value.upcase - end - - def atom - result = "" - while true - token = lookahead - if atom_token?(token) - result.concat(token.value) - shift_token - else - if result.empty? - parse_error("unexpected token %s", token.symbol) - else - return result - end - end - end - end - - ATOM_TOKENS = [ - T_ATOM, - T_NUMBER, - T_NIL, - T_LBRA, - T_RBRA, - T_PLUS - ] - - def atom_token?(token) - return ATOM_TOKENS.include?(token.symbol) - end - - def number - token = lookahead - if token.symbol == T_NIL - shift_token - return nil - end - token = match(T_NUMBER) - return token.value.to_i - end - - def nil_atom - match(T_NIL) - return nil - end - - def match(*args) - token = lookahead - unless args.include?(token.symbol) - parse_error('unexpected token %s (expected %s)', - token.symbol.id2name, - args.collect {|i| i.id2name}.join(" or ")) - end - shift_token - return token - end - - def lookahead - unless @token - @token = next_token - end - return @token - end - - def shift_token - @token = nil - end - - def next_token - case @lex_state - when EXPR_BEG - if @str.index(BEG_REGEXP, @pos) - @pos = $~.end(0) - if $1 - return Token.new(T_SPACE, $+) - elsif $2 - return Token.new(T_NIL, $+) - elsif $3 - return Token.new(T_NUMBER, $+) - elsif $4 - return Token.new(T_ATOM, $+) - elsif $5 - return Token.new(T_QUOTED, - $+.gsub(/\\(["\\])/n, "\\1")) - elsif $6 - return Token.new(T_LPAR, $+) - elsif $7 - return Token.new(T_RPAR, $+) - elsif $8 - return Token.new(T_BSLASH, $+) - elsif $9 - return Token.new(T_STAR, $+) - elsif $10 - return Token.new(T_LBRA, $+) - elsif $11 - return Token.new(T_RBRA, $+) - elsif $12 - len = $+.to_i - val = @str[@pos, len] - @pos += len - return Token.new(T_LITERAL, val) - elsif $13 - return Token.new(T_PLUS, $+) - elsif $14 - return Token.new(T_PERCENT, $+) - elsif $15 - return Token.new(T_CRLF, $+) - elsif $16 - return Token.new(T_EOF, $+) - else - parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid") - end - else - @str.index(/\S*/n, @pos) - parse_error("unknown token - %s", $&.dump) - end - when EXPR_DATA - if @str.index(DATA_REGEXP, @pos) - @pos = $~.end(0) - if $1 - return Token.new(T_SPACE, $+) - elsif $2 - return Token.new(T_NIL, $+) - elsif $3 - return Token.new(T_NUMBER, $+) - elsif $4 - return Token.new(T_QUOTED, - $+.gsub(/\\(["\\])/n, "\\1")) - elsif $5 - len = $+.to_i - val = @str[@pos, len] - @pos += len - return Token.new(T_LITERAL, val) - elsif $6 - return Token.new(T_LPAR, $+) - elsif $7 - return Token.new(T_RPAR, $+) - else - parse_error("[Net::IMAP BUG] 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("illegal @lex_state - %s", @lex_state.inspect) - end - end - - def parse_error(fmt, *args) - if IMAP.debug - $stderr.printf("@str: %s\n", @str.dump) - $stderr.printf("@pos: %d\n", @pos) - $stderr.printf("@lex_state: %s\n", @lex_state) - if @token - $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 "CRAM-MD5" authentication type. See - # #authenticate(). - class CramMD5Authenticator - def process(challenge) - digest = hmac_md5(challenge, @password) - return @user + " " + digest - end - - private - - def initialize(user, password) - @user = user - @password = password - end - - def hmac_md5(text, key) - if key.length > 64 - key = Digest::MD5.digest(key) - end - - k_ipad = key + "\0" * (64 - key.length) - k_opad = key + "\0" * (64 - key.length) - for i in 0..63 - k_ipad[i] ^= 0x36 - k_opad[i] ^= 0x5c - end - - digest = Digest::MD5.digest(k_ipad + text) - - return Digest::MD5.hexdigest(k_opad + digest) - end - end - add_authenticator "CRAM-MD5", CramMD5Authenticator - - # Superclass of IMAP errors. - class Error < StandardError - end - - # Error raised when data is in the incorrect format. - class DataFormatError < Error - end - - # Error raised when a response from the server is non-parseable. - class ResponseParseError < Error - end - - # Superclass of all errors used to encapsulate "fail" responses - # from the server. - class ResponseError < Error - end - - # Error raised upon a "NO" response from the server, indicating - # that the client command could not be completed successfully. - class NoResponseError < ResponseError - end - - # Error raised upon a "BAD" response from the server, indicating - # that the client command violated the IMAP protocol, or an internal - # server failure has occurred. - class BadResponseError < ResponseError - end - - # Error raised upon a "BYE" response from the server, indicating - # that the client is not being allowed to login, or has been timed - # out due to inactivity. - class ByeResponseError < ResponseError - end - end -end - -if __FILE__ == $0 - # :enddoc: - require "getoptlong" - - $stdout.sync = true - $port = nil - $user = ENV["USER"] || ENV["LOGNAME"] - $auth = "login" - $ssl = false - - def usage - $stderr.print <<EOF -usage: #{$0} [options] <host> - - --help print this message - --port=PORT specifies port - --user=USER specifies user - --auth=AUTH specifies auth type - --ssl use ssl -EOF - end - - def get_password - print "password: " - system("stty", "-echo") - begin - return gets.chop - ensure - system("stty", "echo") - print "\n" - end - end - - def get_command - printf("%s@%s> ", $user, $host) - if line = gets - return line.strip.split(/\s+/) - else - return nil - end - end - - parser = GetoptLong.new - parser.set_options(['--debug', GetoptLong::NO_ARGUMENT], - ['--help', GetoptLong::NO_ARGUMENT], - ['--port', GetoptLong::REQUIRED_ARGUMENT], - ['--user', GetoptLong::REQUIRED_ARGUMENT], - ['--auth', GetoptLong::REQUIRED_ARGUMENT], - ['--ssl', GetoptLong::NO_ARGUMENT]) - begin - parser.each_option do |name, arg| - case name - when "--port" - $port = arg - when "--user" - $user = arg - when "--auth" - $auth = arg - when "--ssl" - $ssl = true - when "--debug" - Net::IMAP.debug = true - when "--help" - usage - exit(1) - end - end - rescue - usage - exit(1) - end - - $host = ARGV.shift - unless $host - usage - exit(1) - end - $port ||= $ssl ? 993 : 143 - - imap = Net::IMAP.new($host, $port, $ssl) - begin - password = get_password - imap.authenticate($auth, $user, password) - while true - cmd, *args = get_command - break unless cmd - begin - case cmd - when "list" - for mbox in imap.list("", args[0] || "*") - if mbox.attr.include?(Net::IMAP::NOSELECT) - prefix = "!" - elsif mbox.attr.include?(Net::IMAP::MARKED) - prefix = "*" - else - prefix = " " - end - print prefix, mbox.name, "\n" - end - when "select" - imap.select(args[0] || "inbox") - print "ok\n" - when "close" - imap.close - print "ok\n" - when "summary" - unless messages = imap.responses["EXISTS"][-1] - puts "not selected" - next - end - if messages > 0 - for data in imap.fetch(1..-1, ["ENVELOPE"]) - print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n" - end - else - puts "no message" - end - when "fetch" - if args[0] - data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0] - puts data.attr["RFC822.HEADER"] - puts data.attr["RFC822.TEXT"] - else - puts "missing argument" - end - when "logout", "exit", "quit" - break - when "help", "?" - print <<EOF -list [pattern] list mailboxes -select [mailbox] select mailbox -close close mailbox -summary display summary -fetch [msgno] display message -logout logout -help, ? display help message -EOF - else - print "unknown command: ", cmd, "\n" - end - rescue Net::IMAP::Error - puts $! - end - end - ensure - imap.logout - imap.disconnect - end -end - diff --git a/lib/net/net-protocol.gemspec b/lib/net/net-protocol.gemspec new file mode 100644 index 0000000000..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 2ecbcbdf28..0000000000 --- a/lib/net/pop.rb +++ /dev/null @@ -1,879 +0,0 @@ -# = net/pop.rb -# -# Copyright (c) 1999-2003 Yukihiro Matsumoto. -# -# Copyright (c) 1999-2003 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 or GNU General Public License. -# -# NOTE: You can find Japanese version of this document in -# the doc/net directory of the standard ruby interpreter package. -# -# $Id$ -# -# See Net::POP3 for documentation. -# - -require 'net/protocol' -require 'digest/md5' - -module Net - - # Non-authentication POP3 protocol error - # (reply code "-ERR", except authentication). - class POPError < ProtocolError; end - - # POP3 authentication error. - class POPAuthenticationError < ProtoAuthError; end - - # Unexpected response from the server. - class POPBadResponse < POPError; end - - # - # = Net::POP3 - # - # == What is This Library? - # - # This library provides functionality for retrieving - # email via POP3, the Post Office Protocol version 3. For details - # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt). - # - # == Examples - # - # === Retrieving Messages - # - # This example retrieves messages from the server and deletes them - # on the server. - # - # Messages are written to files named 'inbox/1', 'inbox/2', .... - # Replace 'pop.example.com' with your POP3 server address, and - # 'YourAccount' and 'YourPassword' with the appropriate account - # details. - # - # require 'net/pop' - # - # pop = Net::POP3.new('pop.example.com') - # pop.start('YourAccount', 'YourPassword') # (1) - # if pop.mails.empty? - # puts 'No mail.' - # else - # i = 0 - # pop.each_mail do |m| # or "pop.mails.each ..." # (2) - # File.open("inbox/#{i}", 'w') do |f| - # f.write m.pop - # end - # m.delete - # i += 1 - # end - # puts "#{pop.mails.size} mails popped." - # end - # pop.finish # (3) - # - # 1. Call Net::POP3#start and start POP session. - # 2. Access messages by using POP3#each_mail and/or POP3#mails. - # 3. Close POP session by calling POP3#finish or use the block form of #start. - # - # === Shortened Code - # - # The example above is very verbose. You can shorten the code by using - # some utility methods. First, the block form of Net::POP3.start can - # be used instead of POP3.new, POP3#start and POP3#finish. - # - # require 'net/pop' - # - # Net::POP3.start('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |pop| - # if pop.mails.empty? - # puts 'No mail.' - # else - # i = 0 - # pop.each_mail do |m| # or "pop.mails.each ..." - # File.open("inbox/#{i}", 'w') do |f| - # f.write m.pop - # end - # m.delete - # i += 1 - # end - # puts "#{pop.mails.size} mails popped." - # end - # end - # - # POP3#delete_all is an alternative for #each_mail and #delete. - # - # require 'net/pop' - # - # Net::POP3.start('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |pop| - # if pop.mails.empty? - # puts 'No mail.' - # else - # i = 1 - # pop.delete_all do |m| - # File.open("inbox/#{i}", 'w') do |f| - # f.write m.pop - # end - # i += 1 - # end - # end - # end - # - # And here is an even shorter example. - # - # require 'net/pop' - # - # i = 0 - # Net::POP3.delete_all('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |m| - # File.open("inbox/#{i}", 'w') do |f| - # f.write m.pop - # end - # i += 1 - # end - # - # === Memory Space Issues - # - # All the examples above get each message as one big string. - # This example avoids this. - # - # require 'net/pop' - # - # i = 1 - # Net::POP3.delete_all('pop.example.com', 110, - # 'YourAccount', 'YourPassword') do |m| - # File.open("inbox/#{i}", 'w') do |f| - # m.pop do |chunk| # get a message little by little. - # f.write chunk - # end - # i += 1 - # end - # end - # - # === Using APOP - # - # The net/pop library supports APOP authentication. - # To use APOP, use the Net::APOP class instead of the Net::POP3 class. - # You can use the utility method, Net::POP3.APOP(). For example: - # - # require 'net/pop' - # - # # Use APOP authentication if $isapop == true - # pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110) - # pop.start(YourAccount', 'YourPassword') do |pop| - # # Rest of the code is the same. - # end - # - # === Fetch Only Selected Mail Using 'UIDL' POP Command - # - # If your POP server provides UIDL functionality, - # you can grab only selected mails from the POP server. - # e.g. - # - # def need_pop?( id ) - # # determine if we need pop this mail... - # end - # - # Net::POP3.start('pop.example.com', 110, - # 'Your account', 'Your password') do |pop| - # pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m| - # do_something(m.pop) - # end - # end - # - # The POPMail#unique_id() method returns the unique-id of the message as a - # String. Normally the unique-id is a hash of the message. - # - class POP3 < Protocol - - Revision = %q$Revision$.split[1] - - # - # Class Parameters - # - - # The default port for POP3 connections, port 110 - def POP3.default_port - 110 - 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 - - # - # 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; it defaults to 110. - # - # 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 - @port = port || self.class.default_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 - - # Provide human-readable stringification of class state. - def inspect - "#<#{self.class} #{@address}:#{@port} open=#{@started}>" - end - - # *WARNING*: This method causes a serious security hole. - # Use this method only for debugging. - # - # Set an output stream for debugging. - # - # === Example - # - # pop = Net::POP.new(addr, port) - # pop.set_debug_output $stderr - # pop.start(account, passwd) do |pop| - # .... - # end - # - def set_debug_output( arg ) - @debug_output = arg - end - - # The address to connect to. - attr_reader :address - - # The port number to connect to. - attr_reader :port - - # Seconds to wait until a connection is opened. - # If the POP3 object cannot open a connection within this time, - # it raises a TimeoutError exception. - attr_accessor :open_timeout - - # Seconds to wait until reading one block (by one read(1) call). - # If the POP3 object cannot complete a read() within this time, - # it raises a TimeoutError exception. - attr_reader :read_timeout - - # Set the read timeout. - def read_timeout=( sec ) - @command.socket.read_timeout = sec if @command - @read_timeout = sec - end - - # +true+ if the POP3 session has started. - def started? - @started - end - - alias active? started? #:nodoc: obsolete - - # Starts a POP3 session. - # - # When called with block, gives a POP3 object to the block and - # closes the session after block call finishes. - # - # This method raises a POPAuthenticationError if authentication fails. - def start( account, password ) # :yield: pop - raise IOError, 'POP session already started' if @started - - if block_given? - begin - do_start account, password - return yield(self) - ensure - do_finish - end - else - do_start account, password - return self - end - end - - def do_start( account, password ) - @socket = self.class.socket_type.old_open(@address, @port, - @open_timeout, @read_timeout, @debug_output) - on_connect - @command = POP3Command.new(@socket) - if apop? - @command.apop account, password - else - @command.auth account, password - end - @started = true - ensure - do_finish if not @started - end - private :do_start - - def on_connect - end - private :on_connect - - # Finishes a POP3 session and closes TCP connection. - def finish - raise IOError, 'POP session not yet started' unless started? - do_finish - end - - def do_finish - @mails = nil - @command.quit if @command - ensure - @started = false - @command = nil - @socket.close if @socket and not @socket.closed? - @socket = nil - end - private :do_finish - - def command - raise IOError, 'POP session not opened yet' \ - if not @socket or @socket.closed? - @command - end - private :command - - # - # POP protocol wrapper - # - - # Returns the number of messages on the POP server. - def n_mails - return @n_mails if @n_mails - @n_mails, @n_bytes = command().stat - @n_mails - end - - # Returns the total size in bytes of all the messages on the POP server. - def n_bytes - return @n_bytes if @n_bytes - @n_mails, @n_bytes = command().stat - @n_bytes - end - - # Returns an array of Net::POPMail objects, representing all the - # messages on the server. This array is renewed when the session - # restarts; otherwise, it is fetched from the server the first time - # this method is called (directly or indirectly) and cached. - # - # This method raises a POPError if an error occurs. - def mails - return @mails.dup if @mails - if n_mails() == 0 - # some popd raises error for LIST on the empty mailbox. - @mails = [] - return [] - end - - @mails = command().list.map {|num, size| - POPMail.new(num, size, self, command()) - } - @mails.dup - end - - # Yields each message to the passed-in block in turn. - # Equivalent to: - # - # pop3.mails.each do |popmail| - # .... - # end - # - # This method raises a POPError if an error occurs. - def each_mail( &block ) # :yield: message - mails().each(&block) - end - - alias each each_mail - - # Deletes all messages on the server. - # - # If called with a block, yields each message in turn before deleting it. - # - # === Example - # - # n = 1 - # pop.delete_all do |m| - # File.open("inbox/#{n}") do |f| - # f.write m.pop - # end - # n += 1 - # end - # - # This method raises a POPError if an error occurs. - # - def delete_all # :yield: message - mails().each do |m| - yield m if block_given? - m.delete unless m.deleted? - end - end - - # Resets the session. This clears all "deleted" marks from messages. - # - # This method raises a POPError if an error occurs. - def reset - command().rset - mails().each do |m| - m.instance_eval { - @deleted = false - } - end - end - - def set_all_uids #:nodoc: internal use only (called from POPMail#uidl) - command().uidl.each do |num, uid| - @mails.find {|m| m.number == num }.uid = uid - end - end - - end # class POP3 - - # class aliases - POP = POP3 - POPSession = POP3 - POP3Session = POP3 - - # - # This class is equivalent to POP3, except that it uses APOP authentication. - # - class APOP < POP3 - # Always returns true. - def apop? - true - end - end - - # class aliases - APOPSession = APOP - - # - # This class represents a message which exists on the POP server. - # Instances of this class are created by the POP3 class; they should - # not be directly created by the user. - # - class POPMail - - def initialize( num, len, pop, cmd ) #:nodoc: - @number = num - @length = len - @pop = pop - @command = cmd - @deleted = false - @uid = nil - end - - # The sequence number of the message on the server. - attr_reader :number - - # The length of the message in octets. - attr_reader :length - alias size length - - # Provide human-readable stringification of class state. - def inspect - "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" - end - - # - # This method fetches the message. If called with a block, the - # message is yielded to the block one chunk at a time. If called - # without a block, the message is returned as a String. The optional - # +dest+ argument will be prepended to the returned String; this - # argument is essentially obsolete. - # - # === Example without block - # - # POP3.start('pop.example.com', 110, - # 'YourAccount, 'YourPassword') do |pop| - # n = 1 - # pop.mails.each do |popmail| - # File.open("inbox/#{n}", 'w') do |f| - # f.write popmail.pop - # end - # popmail.delete - # n += 1 - # end - # end - # - # === Example with block - # - # POP3.start('pop.example.com', 110, - # 'YourAccount, 'YourPassword') do |pop| - # n = 1 - # pop.mails.each do |popmail| - # File.open("inbox/#{n}", 'w') do |f| - # popmail.pop do |chunk| #### - # f.write chunk - # end - # end - # n += 1 - # end - # end - # - # This method raises a POPError if an error occurs. - # - def pop( dest = '', &block ) # :yield: message_chunk - if block_given? - @command.retr(@number, &block) - nil - else - @command.retr(@number) do |chunk| - dest << chunk - end - dest - end - end - - alias all pop #:nodoc: obsolete - alias mail pop #:nodoc: obsolete - - # Fetches the message header and +lines+ lines of body. - # - # The optional +dest+ argument is obsolete. - # - # This method raises a POPError if an error occurs. - def top( lines, dest = '' ) - @command.top(@number, lines) do |chunk| - dest << chunk - end - dest - end - - # Fetches the message header. - # - # The optional +dest+ argument is obsolete. - # - # This method raises a POPError if an error occurs. - def header( dest = '' ) - top(0, dest) - end - - # Marks a message for deletion on the server. Deletion does not - # actually occur until the end of the session; deletion may be - # cancelled for _all_ marked messages by calling POP3#reset(). - # - # This method raises a POPError if an error occurs. - # - # === Example - # - # POP3.start('pop.example.com', 110, - # 'YourAccount, 'YourPassword') do |pop| - # n = 1 - # pop.mails.each do |popmail| - # File.open("inbox/#{n}", 'w') do |f| - # f.write popmail.pop - # end - # popmail.delete #### - # n += 1 - # end - # end - # - def delete - @command.dele @number - @deleted = true - end - - alias delete! delete #:nodoc: obsolete - - # True if the mail has been deleted. - def deleted? - @deleted - end - - # Returns the unique-id of the message. - # Normally the unique-id is a hash string of the message. - # - # This method raises a POPError if an error occurs. - def unique_id - return @uid if @uid - @pop.set_all_uids - @uid - end - - alias uidl unique_id - - def uid=( uid ) #:nodoc: internal use only (used from POP3#set_all_uids) - @uid = uid - end - - end # class POPMail - - - class POP3Command #:nodoc: internal use only - - def initialize( sock ) - @socket = sock - @error_occured = false - res = check_response(critical { recv_response() }) - @apop_stamp = res.slice(/<.+>/) - end - - def inspect - "#<#{self.class} socket=#{@socket}>" - end - - def auth( account, password ) - check_response_auth(critical { - check_response_auth(get_response('USER %s', account)) - get_response('PASS %s', password) - }) - end - - def apop( account, password ) - raise POPAuthenticationError, 'not APOP server; cannot login' \ - unless @apop_stamp - check_response_auth(critical { - get_response('APOP %s %s', - account, - Digest::MD5.hexdigest(@apop_stamp + password)) - }) - end - - def list - critical { - getok 'LIST' - list = [] - @socket.each_list_item do |line| - m = /\A(\d+)[ \t]+(\d+)/.match(line) or - raise POPBadResponse, "bad response: #{line}" - list.push [m[1].to_i, m[2].to_i] - end - return list - } - end - - def stat - res = check_response(critical { get_response('STAT') }) - m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or - raise POPBadResponse, "wrong response format: #{res}" - [m[1].to_i, m[2].to_i] - end - - def rset - check_response(critical { get_response 'RSET' }) - end - - def top( num, lines = 0, &block ) - critical { - getok('TOP %d %d', num, lines) - @socket.each_message_chunk(&block) - } - end - - def retr( num, &block ) - critical { - getok('RETR %d', num) - @socket.each_message_chunk(&block) - } - end - - def dele( num ) - check_response(critical { get_response('DELE %d', num) }) - end - - def uidl( num = nil ) - if num - res = check_response(critical { get_response('UIDL %d', num) }) - return res.split(/ /)[1] - else - critical { - getok('UIDL') - table = {} - @socket.each_list_item do |line| - num, uid = line.split - table[num.to_i] = uid - end - return table - } - end - end - - def quit - check_response(critical { get_response('QUIT') }) - end - - private - - def getok( fmt, *fargs ) - @socket.writeline sprintf(fmt, *fargs) - check_response(recv_response()) - end - - def get_response( fmt, *fargs ) - @socket.writeline sprintf(fmt, *fargs) - recv_response() - end - - def recv_response - @socket.readline - end - - def check_response( res ) - raise POPError, res unless /\A\+OK/i === res - res - end - - def check_response_auth( res ) - raise POPAuthenticationError, res unless /\A\+OK/i === res - res - end - - def critical - return '+OK dummy ok response' if @error_occured - begin - return yield() - rescue Exception - @error_occured = true - raise - end - end - - end # class POP3Command - -end # module Net - diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index d722fdcbd4..add2c87a71 100644 --- a/lib/net/protocol.rb +++ b/lib/net/protocol.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true # # = net/protocol.rb # #-- -# Copyright (c) 1999-2005 Yukihiro Matsumoto -# Copyright (c) 1999-2005 Minero Aoki +# Copyright (c) 1999-2004 Yukihiro Matsumoto +# Copyright (c) 1999-2004 Minero Aoki # # written and maintained by Minero Aoki <aamine@loveruby.net> # @@ -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 @@ -45,23 +67,75 @@ module Net # :nodoc: class ProtoRetriableError < ProtocolError; end ProtocRetryError = ProtoRetriableError + ## + # OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot + # be created within the open_timeout. + + class OpenTimeout < Timeout::Error; end + + ## + # ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be read within the read_timeout. + + class ReadTimeout < Timeout::Error + 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 - @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 def inspect "#<#{self.class} io=#{@io}>" end + def eof? + @io.eof? + end + def closed? @io.closed? end @@ -76,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 @@ -94,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 @@ -121,21 +198,43 @@ module Net # :nodoc: return rbuf_consume(@rbuf.size) end end - + def readline readuntil("\n").chop end private + BUFSIZE = 1024 * 16 + def rbuf_fill - timeout(@read_timeout) { - @rbuf << @io.sysread(1024) - } + 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 + (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 @@ -146,12 +245,14 @@ module Net # :nodoc: public - def write(str) + def write(*strs) writing { - write0 str + write0(*strs) } end + alias << write + def writeline(str) writing { write0 str + "\r\n" @@ -170,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 # @@ -200,17 +324,7 @@ module Net # :nodoc: class InternetMessageIO < BufferedIO #:nodoc: internal use only - def InternetMessageIO.old_open(addr, port, - open_timeout = nil, read_timeout = nil, debug_output = nil) - debug_output << "opening connection to #{addr}...\n" if debug_output - s = timeout(open_timeout) { TCPsocket.new(addr, port) } - io = new(s) - io.read_timeout = read_timeout - io.debug_output = debug_output - io - end - - def initialize(io) + def initialize(*, **) super @wbuf = nil end @@ -225,12 +339,12 @@ module Net # :nodoc: read_bytes = 0 while (line = readuntil("\r\n")) != ".\r\n" read_bytes += line.size - yield line.sub(/\A\./, '') + yield line.delete_prefix('.') end LOG_on() LOG "read message (#{read_bytes} bytes)" end - + # *library private* (cannot handle 'break') def each_list_item while (str = readuntil("\r\n")) != ".\r\n" @@ -241,7 +355,7 @@ module Net # :nodoc: def write_message_0(src) prev = @written_bytes each_crlf_line(src) do |line| - write0 line.sub(/\A\./, '..') + write0 dot_stuff(line) end @written_bytes - prev end @@ -269,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 @@ -282,11 +396,15 @@ module Net # :nodoc: private + def dot_stuff(s) + s.sub(/\A\./, '..') + end + def using_each_crlf_line - @wbuf = '' + @wbuf = ''.b yield if not @wbuf.empty? # unterminated last line - write0 @wbuf.chomp + "\r\n" + write0 dot_stuff(@wbuf.chomp) + "\r\n" elsif @written_bytes == 0 # empty src write0 "\r\n" end @@ -296,7 +414,7 @@ module Net # :nodoc: def each_crlf_line(src) buffer_filling(@wbuf, src) do - while line = @wbuf.slice!(/\A.*(?:\n|\r\n|\r(?!\z))/n) + while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/) yield line.chomp("\n") + "\r\n" end end @@ -315,8 +433,8 @@ module Net # :nodoc: yield end else # generic reader - src.each do |s| - buf << s + src.each do |str| + buf << str yield if buf.size > 1024 end yield unless buf.empty? @@ -329,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 dda9dab072..0000000000 --- a/lib/net/smtp.rb +++ /dev/null @@ -1,697 +0,0 @@ -# = net/smtp.rb -# -# Copyright (c) 1999-2003 Yukihiro Matsumoto. -# -# Copyright (c) 1999-2003 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 or GNU General Public License. -# -# NOTE: You can find Japanese version of this document in -# the doc/net directory of the standard ruby interpreter package. -# -# $Id$ -# -# See Net::SMTP for documentation. -# - -require 'net/protocol' -require 'digest/md5' - -module Net - - # Module mixed in to all SMTP error classes - module SMTPError - # This *class* is module for some reason. - # In ruby 1.9.x, 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 - - # - # = Net::SMTP - # - # == What is This Library? - # - # This library provides functionality to send internet - # mail via SMTP, the Simple Mail Transfer Protocol. For details of - # SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt). - # - # == What is This Library NOT? - # - # This library does NOT provide functions to compose internet mails. - # You must create them by yourself. If you want better mail support, - # try RubyMail or TMail. You can get both libraries from RAA. - # (http://www.ruby-lang.org/en/raa.html) - # - # FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt). - # - # == Examples - # - # === Sending Messages - # - # You must open a connection to an SMTP server before sending messages. - # The first argument is the address of your SMTP server, and the second - # argument is the port number. Using SMTP.start with a block is the simplest - # way to do this. This way, the SMTP connection is closed automatically - # after the block is executed. - # - # require 'net/smtp' - # Net::SMTP.start('your.smtp.server', 25) do |smtp| - # # Use the SMTP object smtp only in this block. - # end - # - # Replace 'your.smtp.server' with your SMTP server. Normally - # your system manager or internet provider supplies a server - # for you. - # - # Then you can send messages. - # - # msgstr = <<END_OF_MESSAGE - # From: Your Name <your@mail.address> - # To: Destination Address <someone@example.com> - # Subject: test message - # Date: Sat, 23 Jun 2001 16:26:43 +0900 - # Message-Id: <unique.message.id.string@example.com> - # - # This is a test message. - # END_OF_MESSAGE - # - # require 'net/smtp' - # Net::SMTP.start('your.smtp.server', 25) do |smtp| - # smtp.send_message msgstr, - # 'your@mail.address', - # 'his_addess@example.com' - # end - # - # === Closing the Session - # - # You MUST close the SMTP session after sending messages, by calling - # the #finish method: - # - # # using SMTP#finish - # smtp = Net::SMTP.start('your.smtp.server', 25) - # smtp.send_message msgstr, 'from@address', 'to@address' - # smtp.finish - # - # You can also use the block form of SMTP.start/SMTP#start. This closes - # the SMTP session automatically: - # - # # using block form of SMTP.start - # Net::SMTP.start('your.smtp.server', 25) do |smtp| - # smtp.send_message msgstr, 'from@address', 'to@address' - # end - # - # I strongly recommend this scheme. This form is simpler and more robust. - # - # === HELO domain - # - # In almost all situations, you must provide a third argument - # to SMTP.start/SMTP#start. This is the domain name which you are on - # (the host to send mail from). It is called the "HELO domain". - # The SMTP server will judge whether it should send or reject - # the SMTP session by inspecting the HELO domain. - # - # Net::SMTP.start('your.smtp.server', 25, - # 'mail.from.domain') { |smtp| ... } - # - # === SMTP Authentication - # - # The Net::SMTP class supports three authentication schemes; - # PLAIN, LOGIN and CRAM MD5. (SMTP Authentication: [RFC2554]) - # To use SMTP authentication, pass extra arguments to - # SMTP.start/SMTP#start. - # - # # PLAIN - # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', - # 'Your Account', 'Your Password', :plain) - # # LOGIN - # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', - # 'Your Account', 'Your Password', :login) - # - # # CRAM MD5 - # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', - # 'Your Account', 'Your Password', :cram_md5) - # - class SMTP - - Revision = %q$Revision$.split[1] - - # The default SMTP port, port 25. - def SMTP.default_port - 25 - 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 - @socket = nil - @started = false - @open_timeout = 30 - @read_timeout = 60 - @error_occured = false - @debug_output = nil - end - - # Provide human-readable stringification of class state. - def inspect - "#<#{self.class} #{@address}:#{@port} started=#{@started}>" - end - - # +true+ if the SMTP object uses ESMTP (which it does by default). - def esmtp? - @esmtp - end - - # - # Set whether to use ESMTP or not. This should be done before - # calling #start. Note that if #start is called in ESMTP mode, - # and the connection fails due to a ProtocolError, the SMTP - # object will automatically switch to plain SMTP mode and - # retry (but not vice versa). - # - def esmtp=( bool ) - @esmtp = bool - end - - alias esmtp esmtp? - - # The address of the SMTP server to connect to. - attr_reader :address - - # The port number of the SMTP server to connect to. - attr_reader :port - - # Seconds to wait while attempting to open a connection. - # If the connection cannot be opened within this time, a - # TimeoutError is raised. - attr_accessor :open_timeout - - # Seconds to wait while reading one block (by one read(2) call). - # If the read(2) call does not complete within this time, a - # TimeoutError is raised. - attr_reader :read_timeout - - # Set the number of seconds to wait until timing-out a read(2) - # call. - def read_timeout=( sec ) - @socket.read_timeout = sec if @socket - @read_timeout = sec - end - - # - # WARNING: This method causes serious security holes. - # Use this method for only debugging. - # - # Set an output stream for debug logging. - # You must call this before #start. - # - # # example - # smtp = Net::SMTP.new(addr, port) - # smtp.set_debug_output $stderr - # smtp.start do |smtp| - # .... - # end - # - def set_debug_output( arg ) - @debug_output = arg - end - - # - # SMTP session control - # - - # - # Creates a new Net::SMTP object and connects to the server. - # - # This method is equivalent to: - # - # Net::SMTP.new(address, port).start(helo_domain, account, password, authtype) - # - # === Example - # - # Net::SMTP.start('your.smtp.server') do |smtp| - # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] - # end - # - # === Block Usage - # - # If called with a block, the newly-opened Net::SMTP object is yielded - # to the block, and automatically closed when the block finishes. If called - # without a block, the newly-opened Net::SMTP object is returned to - # the caller, and it is the caller's responsibility to close it when - # finished. - # - # === Parameters - # - # +address+ is the hostname or ip address of your smtp server. - # - # +port+ is the port to connect to; it defaults to port 25. - # - # +helo+ is the _HELO_ _domain_ provided by the client to the - # server (see overview comments); it defaults to 'localhost.localdomain'. - # - # The remaining arguments are used for SMTP authentication, if required - # or desired. +user+ is the account name; +secret+ is your password - # or other authentication token; and +authtype+ is the authentication - # type, one of :plain, :login, or :cram_md5. See the discussion of - # SMTP Authentication in the overview notes. - # - # === Errors - # - # This method may raise: - # - # * Net::SMTPAuthenticationError - # * Net::SMTPServerBusy - # * Net::SMTPSyntaxError - # * Net::SMTPFatalError - # * Net::SMTPUnknownError - # * IOError - # * TimeoutError - # - def SMTP.start( address, port = nil, - helo = 'localhost.localdomain', - user = nil, secret = nil, authtype = nil, - &block) # :yield: smtp - new(address, port).start(helo, user, secret, authtype, &block) - end - - # +true+ if the SMTP session has been started. - def started? - @started - end - - # - # Opens a TCP connection and starts the SMTP session. - # - # === Parameters - # - # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see - # the discussion in the overview notes. - # - # If both of +user+ and +secret+ are given, SMTP authentication - # will be attempted using the AUTH command. +authtype+ specifies - # the type of authentication to attempt; it must be one of - # :login, :plain, and :cram_md5. See the notes on SMTP Authentication - # in the overview. - # - # === Block Usage - # - # When this methods is called with a block, the newly-started SMTP - # object is yielded to the block, and automatically closed after - # the block call finishes. Otherwise, it is the caller's - # responsibility to close the session when finished. - # - # === Example - # - # This is very similar to the class method SMTP.start. - # - # require 'net/smtp' - # smtp = Net::SMTP.new('smtp.mail.server', 25) - # smtp.start(helo_domain, account, password, authtype) do |smtp| - # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] - # end - # - # The primary use of this method (as opposed to SMTP.start) - # is probably to set debugging (#set_debug_output) or ESMTP - # (#esmtp=), which must be done before the session is - # started. - # - # === Errors - # - # If session has already been started, an IOError will be raised. - # - # This method may raise: - # - # * Net::SMTPAuthenticationError - # * Net::SMTPServerBusy - # * Net::SMTPSyntaxError - # * Net::SMTPFatalError - # * Net::SMTPUnknownError - # * IOError - # * TimeoutError - # - def start( helo = 'localhost.localdomain', - user = nil, secret = nil, authtype = nil ) # :yield: smtp - if block_given? - begin - do_start(helo, user, secret, authtype) - return yield(self) - ensure - do_finish - end - else - do_start(helo, user, secret, authtype) - return self - end - end - - def do_start( helodomain, user, secret, authtype ) - raise IOError, 'SMTP session already started' if @started - check_auth_args user, secret, authtype if user or secret - - @socket = InternetMessageIO.old_open(@address, @port, - @open_timeout, @read_timeout, - @debug_output) - check_response(critical { recv_response() }) - begin - if @esmtp - ehlo helodomain - else - helo helodomain - end - rescue ProtocolError - if @esmtp - @esmtp = false - @error_occured = false - retry - end - raise - end - authenticate user, secret, authtype if user - @started = true - ensure - @socket.close if not @started and @socket and not @socket.closed? - end - private :do_start - - # 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 - - def do_finish - quit if @socket and not @socket.closed? and not @error_occured - ensure - @started = false - @error_occured = false - @socket.close if @socket and not @socket.closed? - @socket = nil - end - private :do_finish - - # - # message send - # - - public - - # - # Sends +msgstr+ as a message. Single CR ("\r") and LF ("\n") found - # in the +msgstr+, are converted into the CR LF pair. You cannot send a - # binary message with this method. +msgstr+ should include both - # the message headers and body. - # - # +from_addr+ is a String representing the source mail address. - # - # +to_addr+ is a String or Strings or Array of Strings, representing - # the destination mail address or addresses. - # - # === Example - # - # Net::SMTP.start('smtp.example.com') do |smtp| - # smtp.send_message msgstr, - # 'from@example.com', - # ['dest@example.com', 'dest2@example.com'] - # end - # - # === Errors - # - # This method may raise: - # - # * Net::SMTPServerBusy - # * Net::SMTPSyntaxError - # * Net::SMTPFatalError - # * Net::SMTPUnknownError - # * IOError - # * TimeoutError - # - def send_message( msgstr, from_addr, *to_addrs ) - send0(from_addr, to_addrs.flatten) { - @socket.write_message msgstr - } - end - - alias send_mail send_message - alias sendmail send_message # obsolete - - # - # Opens a message writer stream and gives it to the block. - # The stream is valid only in the block, and has these methods: - # - # puts(str = ''):: outputs STR and CR LF. - # print(str):: outputs STR. - # printf(fmt, *args):: outputs sprintf(fmt,*args). - # write(str):: outputs STR and returns the length of written bytes. - # <<(str):: outputs STR and returns self. - # - # If a single CR ("\r") or LF ("\n") is found in the message, - # it is converted to the CR LF pair. You cannot send a binary - # message with this method. - # - # === Parameters - # - # +from_addr+ is a String representing the source mail address. - # - # +to_addr+ is a String or Strings or Array of Strings, representing - # the destination mail address or addresses. - # - # === Example - # - # Net::SMTP.start('smtp.example.com', 25) do |smtp| - # smtp.open_message_stream('from@example.com', ['dest@example.com']) do |f| - # f.puts 'From: from@example.com' - # f.puts 'To: dest@example.com' - # f.puts 'Subject: test message' - # f.puts - # f.puts 'This is a test message.' - # end - # end - # - # === Errors - # - # This method may raise: - # - # * Net::SMTPServerBusy - # * Net::SMTPSyntaxError - # * Net::SMTPFatalError - # * Net::SMTPUnknownError - # * IOError - # * TimeoutError - # - def open_message_stream( from_addr, *to_addrs, &block ) # :yield: stream - send0(from_addr, to_addrs.flatten) { - @socket.write_message_by_block(&block) - } - end - - alias ready open_message_stream # obsolete - - private - - def send0( from_addr, to_addrs ) - raise IOError, 'closed session' unless @socket - raise ArgumentError, 'mail destination not given' if to_addrs.empty? - if $SAFE > 0 - raise SecurityError, 'tainted from_addr' if from_addr.tainted? - to_addrs.each do |to| - raise SecurityError, 'tainted to_addr' if to.tainted? - end - end - - mailfrom from_addr - to_addrs.each do |to| - rcptto to - end - res = critical { - check_response(get_response('DATA'), true) - yield - recv_response() - } - check_response(res) - end - - # - # auth - # - - private - - def check_auth_args( user, secret, authtype ) - raise ArgumentError, 'both user and secret are required'\ - unless user and secret - auth_method = "auth_#{authtype || 'cram_md5'}" - raise ArgumentError, "wrong auth type #{authtype}"\ - unless respond_to?(auth_method, true) - end - - def authenticate( user, secret, authtype ) - __send__("auth_#{authtype || 'cram_md5'}", user, secret) - end - - def auth_plain( user, secret ) - res = critical { get_response('AUTH PLAIN %s', - base64_encode("\0#{user}\0#{secret}")) } - raise SMTPAuthenticationError, res unless /\A2../ === res - end - - def auth_login( user, secret ) - res = critical { - check_response(get_response('AUTH LOGIN'), true) - check_response(get_response(base64_encode(user)), true) - get_response(base64_encode(secret)) - } - raise SMTPAuthenticationError, res unless /\A2../ === res - end - - def auth_cram_md5( user, secret ) - # CRAM-MD5: [RFC2195] - res = nil - critical { - res = check_response(get_response('AUTH CRAM-MD5'), true) - challenge = res.split(/ /)[1].unpack('m')[0] - secret = Digest::MD5.digest(secret) if secret.size > 64 - - isecret = secret + "\0" * (64 - secret.size) - osecret = isecret.dup - 0.upto(63) do |i| - isecret[i] ^= 0x36 - osecret[i] ^= 0x5c - end - tmp = Digest::MD5.digest(isecret + challenge) - tmp = Digest::MD5.hexdigest(osecret + tmp) - - res = get_response(base64_encode(user + ' ' + tmp)) - } - raise SMTPAuthenticationError, res unless /\A2../ === res - end - - def base64_encode( str ) - # expects "str" may not become too long - [str].pack('m').gsub(/\s+/, '') - end - - # - # SMTP command dispatcher - # - - private - - def helo( domain ) - getok('HELO %s', domain) - end - - def ehlo( domain ) - getok('EHLO %s', domain) - end - - def mailfrom( fromaddr ) - getok('MAIL FROM:<%s>', fromaddr) - end - - def rcptto( to ) - getok('RCPT TO:<%s>', to) - end - - def quit - getok('QUIT') - end - - # - # row level library - # - - private - - def getok( fmt, *args ) - res = critical { - @socket.writeline sprintf(fmt, *args) - recv_response() - } - return check_response(res) - end - - def get_response( fmt, *args ) - @socket.writeline sprintf(fmt, *args) - recv_response() - end - - def recv_response - res = '' - while true - line = @socket.readline - res << line << "\n" - break unless line[3] == ?- # "210-PIPELINING" - end - res - end - - def check_response( res, allow_continue = false ) - return res if /\A2/ === res - return res if allow_continue and /\A3/ === res - err = case res - when /\A4/ then SMTPServerBusy - when /\A50/ then SMTPSyntaxError - when /\A55/ then SMTPFatalError - else SMTPUnknownError - end - raise err, res - end - - def critical( &block ) - return '200 dummy reply code' if @error_occured - begin - return yield() - rescue Exception - @error_occured = true - raise - end - end - - end # class SMTP - - SMTPSession = SMTP - -end # module Net diff --git a/lib/net/telnet.rb b/lib/net/telnet.rb deleted file mode 100644 index c5f8b0429a..0000000000 --- a/lib/net/telnet.rb +++ /dev/null @@ -1,742 +0,0 @@ -# = net/telnet.rb - Simple Telnet Client Library -# -# Author:: Wakou Aoyama <wakou@ruby-lang.org> -# Documentation:: William Webber and Wakou Aoyama -# -# This file holds the class Net::Telnet, which provides client-side -# telnet functionality. -# -# For documentation, see Net::Telnet. -# - -require "socket" -require "delegate" -require "timeout" -require "English" - -module Net - - # - # == Net::Telnet - # - # Provides telnet client functionality. - # - # This class also has, through delegation, all the methods of a - # socket object (by default, a +TCPSocket+, but can be set by the - # +Proxy+ option to <tt>new()</tt>). This provides methods such as - # <tt>close()</tt> to end the session and <tt>sysread()</tt> to read - # data directly from the host, instead of via the <tt>waitfor()</tt> - # mechanism. Note that if you do use <tt>sysread()</tt> directly - # when in telnet mode, you should probably pass the output through - # <tt>preprocess()</tt> to extract telnet command sequences. - # - # == Overview - # - # The telnet protocol allows a client to login remotely to a user - # account on a server and execute commands via a shell. The equivalent - # is done by creating a Net::Telnet class with the +Host+ option - # set to your host, calling #login() with your user and password, - # issuing one or more #cmd() calls, and then calling #close() - # to end the session. The #waitfor(), #print(), #puts(), and - # #write() methods, which #cmd() is implemented on top of, are - # only needed if you are doing something more complicated. - # - # A Net::Telnet object can also be used to connect to non-telnet - # services, such as SMTP or HTTP. In this case, you normally - # want to provide the +Port+ option to specify the port to - # connect to, and set the +Telnetmode+ option to false to prevent - # the client from attempting to interpret telnet command sequences. - # Generally, #login() will not work with other protocols, and you - # have to handle authentication yourself. - # - # For some protocols, it will be possible to specify the +Prompt+ - # option once when you create the Telnet object and use #cmd() calls; - # for others, you will have to specify the response sequence to - # look for as the Match option to every #cmd() call, or call - # #puts() and #waitfor() directly; for yet others, you will have - # to use #sysread() instead of #waitfor() and parse server - # responses yourself. - # - # It is worth noting that when you create a new Net::Telnet object, - # you can supply a proxy IO channel via the Proxy option. This - # can be used to attach the Telnet object to other Telnet objects, - # to already open sockets, or to any read-write IO object. This - # can be useful, for instance, for setting up a test fixture for - # unit testing. - # - # == Examples - # - # === Log in and send a command, echoing all output to stdout - # - # localhost = Net::Telnet::new("Host" => "localhost", - # "Timeout" => 10, - # "Prompt" => /[$%#>] \z/n) - # localhost.login("username", "password") { |c| print c } - # localhost.cmd("command") { |c| print c } - # localhost.close - # - # - # === Check a POP server to see if you have mail - # - # pop = Net::Telnet::new("Host" => "your_destination_host_here", - # "Port" => 110, - # "Telnetmode" => false, - # "Prompt" => /^\+OK/n) - # pop.cmd("user " + "your_username_here") { |c| print c } - # pop.cmd("pass " + "your_password_here") { |c| print c } - # pop.cmd("list") { |c| print c } - # - # == References - # - # There are a large number of RFCs relevant to the Telnet protocol. - # RFCs 854-861 define the base protocol. For a complete listing - # of relevant RFCs, see - # http://www.omnifarious.org/~hopper/technical/telnet-rfc.html - # - class Telnet < SimpleDelegator - - # :stopdoc: - IAC = 255.chr # "\377" # "\xff" # interpret as command - DONT = 254.chr # "\376" # "\xfe" # you are not to use option - DO = 253.chr # "\375" # "\xfd" # please, you use option - WONT = 252.chr # "\374" # "\xfc" # I won't use option - WILL = 251.chr # "\373" # "\xfb" # I will use option - SB = 250.chr # "\372" # "\xfa" # interpret as subnegotiation - GA = 249.chr # "\371" # "\xf9" # you may reverse the line - EL = 248.chr # "\370" # "\xf8" # erase the current line - EC = 247.chr # "\367" # "\xf7" # erase the current character - AYT = 246.chr # "\366" # "\xf6" # are you there - AO = 245.chr # "\365" # "\xf5" # abort output--but let prog finish - IP = 244.chr # "\364" # "\xf4" # interrupt process--permanently - BREAK = 243.chr # "\363" # "\xf3" # break - DM = 242.chr # "\362" # "\xf2" # data mark--for connect. cleaning - NOP = 241.chr # "\361" # "\xf1" # nop - SE = 240.chr # "\360" # "\xf0" # end sub negotiation - EOR = 239.chr # "\357" # "\xef" # end of record (transparent mode) - ABORT = 238.chr # "\356" # "\xee" # Abort process - SUSP = 237.chr # "\355" # "\xed" # Suspend process - EOF = 236.chr # "\354" # "\xec" # End of file - SYNCH = 242.chr # "\362" # "\xf2" # for telfunc calls - - OPT_BINARY = 0.chr # "\000" # "\x00" # Binary Transmission - OPT_ECHO = 1.chr # "\001" # "\x01" # Echo - OPT_RCP = 2.chr # "\002" # "\x02" # Reconnection - OPT_SGA = 3.chr # "\003" # "\x03" # Suppress Go Ahead - OPT_NAMS = 4.chr # "\004" # "\x04" # Approx Message Size Negotiation - OPT_STATUS = 5.chr # "\005" # "\x05" # Status - OPT_TM = 6.chr # "\006" # "\x06" # Timing Mark - OPT_RCTE = 7.chr # "\a" # "\x07" # Remote Controlled Trans and Echo - OPT_NAOL = 8.chr # "\010" # "\x08" # Output Line Width - OPT_NAOP = 9.chr # "\t" # "\x09" # Output Page Size - OPT_NAOCRD = 10.chr # "\n" # "\x0a" # Output Carriage-Return Disposition - OPT_NAOHTS = 11.chr # "\v" # "\x0b" # Output Horizontal Tab Stops - OPT_NAOHTD = 12.chr # "\f" # "\x0c" # Output Horizontal Tab Disposition - OPT_NAOFFD = 13.chr # "\r" # "\x0d" # Output Formfeed Disposition - OPT_NAOVTS = 14.chr # "\016" # "\x0e" # Output Vertical Tabstops - OPT_NAOVTD = 15.chr # "\017" # "\x0f" # Output Vertical Tab Disposition - OPT_NAOLFD = 16.chr # "\020" # "\x10" # Output Linefeed Disposition - OPT_XASCII = 17.chr # "\021" # "\x11" # Extended ASCII - OPT_LOGOUT = 18.chr # "\022" # "\x12" # Logout - OPT_BM = 19.chr # "\023" # "\x13" # Byte Macro - OPT_DET = 20.chr # "\024" # "\x14" # Data Entry Terminal - OPT_SUPDUP = 21.chr # "\025" # "\x15" # SUPDUP - OPT_SUPDUPOUTPUT = 22.chr # "\026" # "\x16" # SUPDUP Output - OPT_SNDLOC = 23.chr # "\027" # "\x17" # Send Location - OPT_TTYPE = 24.chr # "\030" # "\x18" # Terminal Type - OPT_EOR = 25.chr # "\031" # "\x19" # End of Record - OPT_TUID = 26.chr # "\032" # "\x1a" # TACACS User Identification - OPT_OUTMRK = 27.chr # "\e" # "\x1b" # Output Marking - OPT_TTYLOC = 28.chr # "\034" # "\x1c" # Terminal Location Number - OPT_3270REGIME = 29.chr # "\035" # "\x1d" # Telnet 3270 Regime - OPT_X3PAD = 30.chr # "\036" # "\x1e" # X.3 PAD - OPT_NAWS = 31.chr # "\037" # "\x1f" # Negotiate About Window Size - OPT_TSPEED = 32.chr # " " # "\x20" # Terminal Speed - OPT_LFLOW = 33.chr # "!" # "\x21" # Remote Flow Control - OPT_LINEMODE = 34.chr # "\"" # "\x22" # Linemode - OPT_XDISPLOC = 35.chr # "#" # "\x23" # X Display Location - OPT_OLD_ENVIRON = 36.chr # "$" # "\x24" # Environment Option - OPT_AUTHENTICATION = 37.chr # "%" # "\x25" # Authentication Option - OPT_ENCRYPT = 38.chr # "&" # "\x26" # Encryption Option - OPT_NEW_ENVIRON = 39.chr # "'" # "\x27" # New Environment Option - OPT_EXOPL = 255.chr # "\377" # "\xff" # Extended-Options-List - - NULL = "\000" - CR = "\015" - LF = "\012" - EOL = CR + LF - REVISION = '$Id$' - # :startdoc: - - # - # Creates a new Net::Telnet object. - # - # Attempts to connect to the host (unless the Proxy option is - # provided: see below). If a block is provided, it is yielded - # status messages on the attempt to connect to the server, of - # the form: - # - # Trying localhost... - # Connected to localhost. - # - # +options+ is a hash of options. The following example lists - # all options and their default values. - # - # host = Net::Telnet::new( - # "Host" => "localhost", # default: "localhost" - # "Port" => 23, # default: 23 - # "Binmode" => false, # default: false - # "Output_log" => "output_log", # default: nil (no output) - # "Dump_log" => "dump_log", # default: nil (no output) - # "Prompt" => /[$%#>] \z/n, # default: /[$%#>] \z/n - # "Telnetmode" => true, # default: true - # "Timeout" => 10, # default: 10 - # # if ignore timeout then set "Timeout" to false. - # "Waittime" => 0, # default: 0 - # "Proxy" => proxy # default: nil - # # proxy is Net::Telnet or IO object - # ) - # - # The options have the following meanings: - # - # Host:: the hostname or IP address of the host to connect to, as a String. - # Defaults to "localhost". - # - # Port:: the port to connect to. Defaults to 23. - # - # Binmode:: if false (the default), newline substitution is performed. - # Outgoing LF is - # converted to CRLF, and incoming CRLF is converted to LF. If - # true, this substitution is not performed. This value can - # also be set with the #binmode() method. The - # outgoing conversion only applies to the #puts() and #print() - # methods, not the #write() method. The precise nature of - # the newline conversion is also affected by the telnet options - # SGA and BIN. - # - # Output_log:: the name of the file to write connection status messages - # and all received traffic to. In the case of a proper - # Telnet session, this will include the client input as - # echoed by the host; otherwise, it only includes server - # responses. Output is appended verbatim to this file. - # By default, no output log is kept. - # - # Dump_log:: as for Output_log, except that output is written in hexdump - # format (16 bytes per line as hex pairs, followed by their - # printable equivalent), with connection status messages - # preceded by '#', sent traffic preceded by '>', and - # received traffic preceded by '<'. By default, not dump log - # is kept. - # - # Prompt:: a regular expression matching the host's command-line prompt - # sequence. This is needed by the Telnet class to determine - # when the output from a command has finished and the host is - # ready to receive a new command. By default, this regular - # expression is /[$%#>] \z/n. - # - # Telnetmode:: a boolean value, true by default. In telnet mode, - # traffic received from the host is parsed for special - # command sequences, and these sequences are escaped - # in outgoing traffic sent using #puts() or #print() - # (but not #write()). If you are using the Net::Telnet - # object to connect to a non-telnet service (such as - # SMTP or POP), this should be set to "false" to prevent - # undesired data corruption. This value can also be set - # by the #telnetmode() method. - # - # Timeout:: the number of seconds to wait before timing out both the - # initial attempt to connect to host (in this constructor), - # and all attempts to read data from the host (in #waitfor(), - # #cmd(), and #login()). Exceeding this timeout causes a - # TimeoutError to be raised. The default value is 10 seconds. - # You can disable the timeout by setting this value to false. - # In this case, the connect attempt will eventually timeout - # on the underlying connect(2) socket call with an - # Errno::ETIMEDOUT error (but generally only after a few - # minutes), but other attempts to read data from the host - # will hand indefinitely if no data is forthcoming. - # - # Waittime:: the amount of time to wait after seeing what looks like a - # prompt (that is, received data that matches the Prompt - # option regular expression) to see if more data arrives. - # If more data does arrive in this time, Net::Telnet assumes - # that what it saw was not really a prompt. This is to try to - # avoid false matches, but it can also lead to missing real - # prompts (if, for instance, a background process writes to - # the terminal soon after the prompt is displayed). By - # default, set to 0, meaning not to wait for more data. - # - # Proxy:: a proxy object to used instead of opening a direct connection - # to the host. Must be either another Net::Telnet object or - # an IO object. If it is another Net::Telnet object, this - # instance will use that one's socket for communication. If an - # IO object, it is used directly for communication. Any other - # kind of object will cause an error to be raised. - # - def initialize(options) # :yield: mesg - @options = options - @options["Host"] = "localhost" unless @options.has_key?("Host") - @options["Port"] = 23 unless @options.has_key?("Port") - @options["Prompt"] = /[$%#>] \z/n unless @options.has_key?("Prompt") - @options["Timeout"] = 10 unless @options.has_key?("Timeout") - @options["Waittime"] = 0 unless @options.has_key?("Waittime") - unless @options.has_key?("Binmode") - @options["Binmode"] = false - else - unless (true == @options["Binmode"] or false == @options["Binmode"]) - raise ArgumentError, "Binmode option must be true or false" - end - end - - unless @options.has_key?("Telnetmode") - @options["Telnetmode"] = true - else - unless (true == @options["Telnetmode"] or false == @options["Telnetmode"]) - raise ArgumentError, "Telnetmode option must be true or false" - end - end - - @telnet_option = { "SGA" => false, "BINARY" => false } - - if @options.has_key?("Output_log") - @log = File.open(@options["Output_log"], 'a+') - @log.sync = true - @log.binmode - end - - if @options.has_key?("Dump_log") - @dumplog = File.open(@options["Dump_log"], 'a+') - @dumplog.sync = true - @dumplog.binmode - def @dumplog.log_dump(dir, x) # :nodoc: - len = x.length - addr = 0 - offset = 0 - while 0 < len - if len < 16 - line = x[offset, len] - else - line = x[offset, 16] - end - hexvals = line.unpack('H*')[0] - hexvals += ' ' * (32 - hexvals.length) - hexvals = format("%s %s %s %s " * 4, *hexvals.unpack('a2' * 16)) - line = line.gsub(/[\000-\037\177-\377]/n, '.') - printf "%s 0x%5.5x: %s%s\n", dir, addr, hexvals, line - addr += 16 - offset += 16 - len -= 16 - end - print "\n" - end - end - - if @options.has_key?("Proxy") - if @options["Proxy"].kind_of?(Net::Telnet) - @sock = @options["Proxy"].sock - elsif @options["Proxy"].kind_of?(IO) - @sock = @options["Proxy"] - else - raise "Error: Proxy must be an instance of Net::Telnet or IO." - end - else - message = "Trying " + @options["Host"] + "...\n" - yield(message) if block_given? - @log.write(message) if @options.has_key?("Output_log") - @dumplog.log_dump('#', message) if @options.has_key?("Dump_log") - - begin - if @options["Timeout"] == false - @sock = TCPSocket.open(@options["Host"], @options["Port"]) - else - timeout(@options["Timeout"]) do - @sock = TCPSocket.open(@options["Host"], @options["Port"]) - end - end - rescue TimeoutError - raise TimeoutError, "timed out while opening a connection to the host" - rescue - @log.write($ERROR_INFO.to_s + "\n") if @options.has_key?("Output_log") - @dumplog.log_dump('#', $ERROR_INFO.to_s + "\n") if @options.has_key?("Dump_log") - raise - end - @sock.sync = true - @sock.binmode - - message = "Connected to " + @options["Host"] + ".\n" - yield(message) if block_given? - @log.write(message) if @options.has_key?("Output_log") - @dumplog.log_dump('#', message) if @options.has_key?("Dump_log") - end - - super(@sock) - end # initialize - - # The socket the Telnet object is using. Note that this object becomes - # a delegate of the Telnet object, so normally you invoke its methods - # directly on the Telnet object. - attr :sock - - # Set telnet command interpretation on (+mode+ == true) or off - # (+mode+ == false), or return the current value (+mode+ not - # provided). It should be on for true telnet sessions, off if - # using Net::Telnet to connect to a non-telnet service such - # as SMTP. - def telnetmode(mode = nil) - case mode - when nil - @options["Telnetmode"] - when true, false - @options["Telnetmode"] = mode - else - raise ArgumentError, "argument must be true or false, or missing" - end - end - - # Turn telnet command interpretation on (true) or off (false). It - # should be on for true telnet sessions, off if using Net::Telnet - # to connect to a non-telnet service such as SMTP. - def telnetmode=(mode) - if (true == mode or false == mode) - @options["Telnetmode"] = mode - else - raise ArgumentError, "argument must be true or false" - end - end - - # Turn newline conversion on (+mode+ == false) or off (+mode+ == true), - # or return the current value (+mode+ is not specified). - def binmode(mode = nil) - case mode - when nil - @options["Binmode"] - when true, false - @options["Binmode"] = mode - else - raise ArgumentError, "argument must be true or false" - end - end - - # Turn newline conversion on (false) or off (true). - def binmode=(mode) - if (true == mode or false == mode) - @options["Binmode"] = mode - else - raise ArgumentError, "argument must be true or false" - end - end - - # Preprocess received data from the host. - # - # Performs newline conversion and detects telnet command sequences. - # Called automatically by #waitfor(). You should only use this - # method yourself if you have read input directly using sysread() - # or similar, and even then only if in telnet mode. - def preprocess(string) - # combine CR+NULL into CR - string = string.gsub(/#{CR}#{NULL}/no, CR) if @options["Telnetmode"] - - # combine EOL into "\n" - string = string.gsub(/#{EOL}/no, "\n") unless @options["Binmode"] - - string.gsub(/#{IAC}( - [#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]| - [#{DO}#{DONT}#{WILL}#{WONT}] - [#{OPT_BINARY}-#{OPT_NEW_ENVIRON}#{OPT_EXOPL}]| - #{SB}[^#{IAC}]*#{IAC}#{SE} - )/xno) do - if IAC == $1 # handle escaped IAC characters - IAC - elsif AYT == $1 # respond to "IAC AYT" (are you there) - self.write("nobody here but us pigeons" + EOL) - '' - elsif DO[0] == $1[0] # respond to "IAC DO x" - if OPT_BINARY[0] == $1[1] - @telnet_option["BINARY"] = true - self.write(IAC + WILL + OPT_BINARY) - else - self.write(IAC + WONT + $1[1..1]) - end - '' - elsif DONT[0] == $1[0] # respond to "IAC DON'T x" with "IAC WON'T x" - self.write(IAC + WONT + $1[1..1]) - '' - elsif WILL[0] == $1[0] # respond to "IAC WILL x" - if OPT_BINARY[0] == $1[1] - self.write(IAC + DO + OPT_BINARY) - elsif OPT_ECHO[0] == $1[1] - self.write(IAC + DO + OPT_ECHO) - elsif OPT_SGA[0] == $1[1] - @telnet_option["SGA"] = true - self.write(IAC + DO + OPT_SGA) - else - self.write(IAC + DONT + $1[1..1]) - end - '' - elsif WONT[0] == $1[0] # respond to "IAC WON'T x" - if OPT_ECHO[0] == $1[1] - self.write(IAC + DONT + OPT_ECHO) - elsif OPT_SGA[0] == $1[1] - @telnet_option["SGA"] = false - self.write(IAC + DONT + OPT_SGA) - else - self.write(IAC + DONT + $1[1..1]) - end - '' - else - '' - end - end - end # preprocess - - # Read data from the host until a certain sequence is matched. - # - # If a block is given, the received data will be yielded as it - # is read in (not necessarily all in one go), or nil if EOF - # occurs before any data is received. Whether a block is given - # or not, all data read will be returned in a single string, or again - # nil if EOF occurs before any data is received. Note that - # received data includes the matched sequence we were looking for. - # - # +options+ can be either a regular expression or a hash of options. - # If a regular expression, this specifies the data to wait for. - # If a hash, this can specify the following options: - # - # Match:: a regular expression, specifying the data to wait for. - # Prompt:: as for Match; used only if Match is not specified. - # String:: as for Match, except a string that will be converted - # into a regular expression. Used only if Match and - # Prompt are not specified. - # Timeout:: the number of seconds to wait for data from the host - # before raising a TimeoutError. If set to false, - # no timeout will occur. If not specified, the - # Timeout option value specified when this instance - # was created will be used, or, failing that, the - # default value of 10 seconds. - # Waittime:: the number of seconds to wait after matching against - # the input data to see if more data arrives. If more - # data arrives within this time, we will judge ourselves - # not to have matched successfully, and will continue - # trying to match. If not specified, the Waittime option - # value specified when this instance was created will be - # used, or, failing that, the default value of 0 seconds, - # which means not to wait for more input. - # - def waitfor(options) # :yield: recvdata - time_out = @options["Timeout"] - waittime = @options["Waittime"] - - if options.kind_of?(Hash) - prompt = if options.has_key?("Match") - options["Match"] - elsif options.has_key?("Prompt") - options["Prompt"] - elsif options.has_key?("String") - Regexp.new( Regexp.quote(options["String"]) ) - end - time_out = options["Timeout"] if options.has_key?("Timeout") - waittime = options["Waittime"] if options.has_key?("Waittime") - else - prompt = options - end - - if time_out == false - time_out = nil - end - - line = '' - buf = '' - rest = '' - until(prompt === line and not IO::select([@sock], nil, nil, waittime)) - unless IO::select([@sock], nil, nil, time_out) - raise TimeoutError, "timed out while waiting for more data" - end - begin - c = @sock.readpartial(1024 * 1024) - @dumplog.log_dump('<', c) if @options.has_key?("Dump_log") - if @options["Telnetmode"] - c = rest + c - if Integer(c.rindex(/#{IAC}#{SE}/no)) < - Integer(c.rindex(/#{IAC}#{SB}/no)) - buf = preprocess(c[0 ... c.rindex(/#{IAC}#{SB}/no)]) - rest = c[c.rindex(/#{IAC}#{SB}/no) .. -1] - elsif pt = c.rindex(/#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no) - buf = preprocess(c[0 ... pt]) - rest = c[pt .. -1] - else - buf = preprocess(c) - rest = '' - end - else - # Not Telnetmode. - # - # We cannot use preprocess() on this data, because that - # method makes some Telnetmode-specific assumptions. - buf = c - buf.gsub!(/#{EOL}/no, "\n") unless @options["Binmode"] - rest = '' - end - @log.print(buf) if @options.has_key?("Output_log") - line += buf - yield buf if block_given? - rescue EOFError # End of file reached - if line == '' - line = nil - yield nil if block_given? - end - break - end - end - line - end - - # Write +string+ to the host. - # - # Does not perform any conversions on +string+. Will log +string+ to the - # dumplog, if the Dump_log option is set. - def write(string) - length = string.length - while 0 < length - IO::select(nil, [@sock]) - @dumplog.log_dump('>', string[-length..-1]) if @options.has_key?("Dump_log") - length -= @sock.syswrite(string[-length..-1]) - end - end - - # Sends a string to the host. - # - # This does _not_ automatically append a newline to the string. Embedded - # newlines may be converted and telnet command sequences escaped - # depending upon the values of telnetmode, binmode, and telnet options - # set by the host. - def print(string) - string = string.gsub(/#{IAC}/no, IAC + IAC) if @options["Telnetmode"] - - if @options["Binmode"] - self.write(string) - else - if @telnet_option["BINARY"] and @telnet_option["SGA"] - # IAC WILL SGA IAC DO BIN send EOL --> CR - self.write(string.gsub(/\n/n, CR)) - elsif @telnet_option["SGA"] - # IAC WILL SGA send EOL --> CR+NULL - self.write(string.gsub(/\n/n, CR + NULL)) - else - # NONE send EOL --> CR+LF - self.write(string.gsub(/\n/n, EOL)) - end - end - end - - # Sends a string to the host. - # - # Same as #print(), but appends a newline to the string. - def puts(string) - self.print(string + "\n") - end - - # Send a command to the host. - # - # More exactly, sends a string to the host, and reads in all received - # data until is sees the prompt or other matched sequence. - # - # If a block is given, the received data will be yielded to it as - # it is read in. Whether a block is given or not, the received data - # will be return as a string. Note that the received data includes - # the prompt and in most cases the host's echo of our command. - # - # +options+ is either a String, specified the string or command to - # send to the host; or it is a hash of options. If a hash, the - # following options can be specified: - # - # String:: the command or other string to send to the host. - # Match:: a regular expression, the sequence to look for in - # the received data before returning. If not specified, - # the Prompt option value specified when this instance - # was created will be used, or, failing that, the default - # prompt of /[$%#>] \z/n. - # Timeout:: the seconds to wait for data from the host before raising - # a Timeout error. If not specified, the Timeout option - # value specified when this instance was created will be - # used, or, failing that, the default value of 10 seconds. - # - # The command or other string will have the newline sequence appended - # to it. - def cmd(options) # :yield: recvdata - match = @options["Prompt"] - time_out = @options["Timeout"] - - if options.kind_of?(Hash) - string = options["String"] - match = options["Match"] if options.has_key?("Match") - time_out = options["Timeout"] if options.has_key?("Timeout") - else - string = options - end - - self.puts(string) - if block_given? - waitfor({"Prompt" => match, "Timeout" => time_out}){|c| yield c } - else - waitfor({"Prompt" => match, "Timeout" => time_out}) - end - end - - # Login to the host with a given username and password. - # - # The username and password can either be provided as two string - # arguments in that order, or as a hash with keys "Name" and - # "Password". - # - # This method looks for the strings "login" and "Password" from the - # host to determine when to send the username and password. If the - # login sequence does not follow this pattern (for instance, you - # are connecting to a service other than telnet), you will need - # to handle login yourself. - # - # The password can be omitted, either by only - # provided one String argument, which will be used as the username, - # or by providing a has that has no "Password" key. In this case, - # the method will not look for the "Password:" prompt; if it is - # sent, it will have to be dealt with by later calls. - # - # The method returns all data received during the login process from - # the host, including the echoed username but not the password (which - # the host should not echo). If a block is passed in, this received - # data is also yielded to the block as it is received. - def login(options, password = nil) # :yield: recvdata - login_prompt = /[Ll]ogin[: ]*\z/n - password_prompt = /Password[: ]*\z/n - if options.kind_of?(Hash) - username = options["Name"] - password = options["Password"] - login_prompt = options["LoginPrompt"] if options["LoginPrompt"] - password_prompt = options["PasswordPrompt"] if options["PasswordPrompt"] - else - username = options - end - - if block_given? - line = waitfor(login_prompt){|c| yield c } - if password - line += cmd({"String" => username, - "Match" => password_prompt}){|c| yield c } - line += cmd(password){|c| yield c } - else - line += cmd(username){|c| yield c } - end - else - line = waitfor(login_prompt) - if password - line += cmd({"String" => username, - "Match" => password_prompt}) - line += cmd(password) - else - line += cmd(username) - end - end - line - end - - end # class Telnet -end # module Net - |
