summaryrefslogtreecommitdiff
path: root/lib/net
diff options
context:
space:
mode:
Diffstat (limited to 'lib/net')
-rw-r--r--lib/net/ftp.rb1436
-rw-r--r--lib/net/http.rb2757
-rw-r--r--lib/net/http/backward.rb26
-rw-r--r--lib/net/http/exceptions.rb26
-rw-r--r--lib/net/http/generic_request.rb338
-rw-r--r--lib/net/http/header.rb494
-rw-r--r--lib/net/http/proxy_delta.rb17
-rw-r--r--lib/net/http/request.rb21
-rw-r--r--lib/net/http/requests.rb123
-rw-r--r--lib/net/http/response.rb419
-rw-r--r--lib/net/http/responses.rb299
-rw-r--r--lib/net/http/status.rb83
-rw-r--r--lib/net/https.rb166
-rw-r--r--lib/net/imap.rb1745
-rw-r--r--lib/net/pop.rb393
-rw-r--r--lib/net/protocol.rb120
-rw-r--r--lib/net/smtp.rb851
-rw-r--r--lib/net/telnet.rb749
18 files changed, 5945 insertions, 4118 deletions
diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb
index dfbcf1499f..9902f9dc65 100644
--- a/lib/net/ftp.rb
+++ b/lib/net/ftp.rb
@@ -1,11 +1,12 @@
-#
+# frozen_string_literal: true
+#
# = 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.
#
@@ -16,15 +17,22 @@
require "socket"
require "monitor"
+require "net/protocol"
+require "time"
+begin
+ require "openssl"
+rescue LoadError
+end
module Net
# :stopdoc:
class FTPError < StandardError; end
class FTPReplyError < FTPError; end
- class FTPTempError < FTPError; end
- class FTPPermError < FTPError; end
+ class FTPTempError < FTPError; end
+ class FTPPermError < FTPError; end
class FTPProtoError < FTPError; end
+ class FTPConnectionError < FTPError; end
# :startdoc:
#
@@ -34,12 +42,12 @@ module Net
# advantage of Ruby's style and strengths.
#
# == Example
- #
+ #
# require 'net/ftp'
#
# === Example 1
- #
- # ftp = Net::FTP.new('ftp.netlab.co.jp')
+ #
+ # ftp = Net::FTP.new('example.com')
# ftp.login
# files = ftp.chdir('pub/lang/ruby/contrib')
# files = ftp.list('n*')
@@ -48,7 +56,7 @@ module Net
#
# === Example 2
#
- # Net::FTP.open('ftp.netlab.co.jp') do |ftp|
+ # Net::FTP.open('example.com') do |ftp|
# ftp.login
# files = ftp.chdir('pub/lang/ruby/contrib')
# files = ftp.list('n*')
@@ -69,19 +77,24 @@ module Net
# - #rename
# - #delete
#
- class FTP
+ class FTP < Protocol
include MonitorMixin
-
+ if defined?(OpenSSL::SSL)
+ include OpenSSL
+ include SSL
+ end
+
# :stopdoc:
FTP_PORT = 21
CRLF = "\r\n"
- DEFAULT_BLOCKSIZE = 4096
+ DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
+ @@default_passive = true
# :startdoc:
-
+
# When +true+, transfers are performed in binary mode. Default: +true+.
- attr_accessor :binary
+ attr_reader :binary
- # When +true+, the connection is in passive mode. Default: +false+.
+ # When +true+, the connection is in passive mode. Default: +true+.
attr_accessor :passive
# When +true+, all traffic to and from the server is written
@@ -92,6 +105,31 @@ module Net
# transfers are resumed or restarted. Default: +false+.
attr_accessor :resume
+ # Number of seconds to wait for the connection to open. Any number
+ # may be used, including Floats for fractional seconds. If the FTP
+ # object cannot open a connection in this many seconds, it raises a
+ # Net::OpenTimeout exception. The default value is +nil+.
+ attr_accessor :open_timeout
+
+ # Number of seconds to wait for the TLS handshake. Any number
+ # may be used, including Floats for fractional seconds. If the FTP
+ # object cannot complete the TLS handshake in this many seconds, it
+ # raises a Net::OpenTimeout exception. The default value is +nil+.
+ # If +ssl_handshake_timeout+ is +nil+, +open_timeout+ is used instead.
+ attr_accessor :ssl_handshake_timeout
+
+ # Number of seconds to wait for one block to be read (via one read(2)
+ # call). Any number may be used, including Floats for fractional
+ # seconds. If the FTP object cannot read data in this many seconds,
+ # it raises a Timeout::Error exception. The default value is 60 seconds.
+ attr_reader :read_timeout
+
+ # Setter for the read_timeout attribute.
+ def read_timeout=(sec)
+ @sock.read_timeout = sec
+ @read_timeout = sec
+ end
+
# The server's welcome message.
attr_reader :welcome
@@ -101,66 +139,224 @@ module Net
# The server's last response.
attr_reader :last_response
-
+
+ # When +true+, connections are in passive mode per default.
+ # Default: +true+.
+ def self.default_passive=(value)
+ @@default_passive = value
+ end
+
+ # When +true+, connections are in passive mode per default.
+ # Default: +true+.
+ def self.default_passive
+ @@default_passive
+ end
+
#
# 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)
+ def FTP.open(host, *args)
if block_given?
- ftp = new(host, user, passwd, acct)
+ ftp = new(host, *args)
begin
yield ftp
ensure
ftp.close
end
else
- new(host, user, passwd, acct)
+ new(host, *args)
end
end
-
+
+ # :call-seq:
+ # Net::FTP.new(host = nil, options = {})
#
# 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)
+ # is made.
+ #
+ # +options+ is an option hash, each key of which is a symbol.
+ #
+ # The available options are:
+ #
+ # port:: Port number (default value is 21)
+ # ssl:: If options[:ssl] is true, then an attempt will be made
+ # to use SSL (now TLS) to connect to the server. For this
+ # to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL]
+ # extensions need to be installed. If options[:ssl] is a
+ # hash, it's passed to OpenSSL::SSL::SSLContext#set_params
+ # as parameters.
+ # private_data_connection:: If true, TLS is used for data connections.
+ # Default: +true+ when options[:ssl] is true.
+ # username:: Username for login. If options[:username] is the string
+ # "anonymous" and the options[:password] is +nil+,
+ # "anonymous@" is used as a password.
+ # password:: Password for login.
+ # account:: Account information for ACCT.
+ # passive:: When +true+, the connection is in passive mode. Default:
+ # +true+.
+ # open_timeout:: Number of seconds to wait for the connection to open.
+ # See Net::FTP#open_timeout for details. Default: +nil+.
+ # read_timeout:: Number of seconds to wait for one block to be read.
+ # See Net::FTP#read_timeout for details. Default: +60+.
+ # ssl_handshake_timeout:: Number of seconds to wait for the TLS
+ # handshake.
+ # See Net::FTP#ssl_handshake_timeout for
+ # details. Default: +nil+.
+ # debug_mode:: When +true+, all traffic to and from the server is
+ # written to +$stdout+. Default: +false+.
+ #
+ def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil)
super()
+ begin
+ options = user_or_options.to_hash
+ rescue NoMethodError
+ # for backward compatibility
+ options = {}
+ options[:username] = user_or_options
+ options[:password] = passwd
+ options[:account] = acct
+ end
+ @host = nil
+ if options[:ssl]
+ unless defined?(OpenSSL::SSL)
+ raise "SSL extension not installed"
+ end
+ ssl_params = options[:ssl] == true ? {} : options[:ssl]
+ @ssl_context = SSLContext.new
+ @ssl_context.set_params(ssl_params)
+ if defined?(VerifyCallbackProc)
+ @ssl_context.verify_callback = VerifyCallbackProc
+ end
+ @ssl_context.session_cache_mode =
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
+ @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
+ @ssl_session = nil
+ if options[:private_data_connection].nil?
+ @private_data_connection = true
+ else
+ @private_data_connection = options[:private_data_connection]
+ end
+ else
+ @ssl_context = nil
+ if options[:private_data_connection]
+ raise ArgumentError,
+ "private_data_connection can be set to true only when ssl is enabled"
+ end
+ @private_data_connection = false
+ end
@binary = true
- @passive = false
- @debug_mode = false
+ if options[:passive].nil?
+ @passive = @@default_passive
+ else
+ @passive = options[:passive]
+ end
+ if options[:debug_mode].nil?
+ @debug_mode = false
+ else
+ @debug_mode = options[:debug_mode]
+ end
@resume = false
+ @bare_sock = @sock = NullSocket.new
+ @logged_in = false
+ @open_timeout = options[:open_timeout]
+ @ssl_handshake_timeout = options[:ssl_handshake_timeout]
+ @read_timeout = options[:read_timeout] || 60
if host
- connect(host)
- if user
- login(user, passwd, acct)
- end
+ connect(host, options[:port] || FTP_PORT)
+ if options[:username]
+ login(options[:username], options[:password], options[:account])
+ end
+ end
+ end
+
+ # A setter to toggle transfers in binary mode.
+ # +newmode+ is either +true+ or +false+
+ def binary=(newmode)
+ if newmode != @binary
+ @binary = newmode
+ send_type_command if @logged_in
+ end
+ end
+
+ # Sends a command to destination host, with the current binary sendmode
+ # type.
+ #
+ # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
+ # A" (ascii) is sent.
+ def send_type_command # :nodoc:
+ if @binary
+ voidcmd("TYPE I")
+ else
+ voidcmd("TYPE A")
end
end
+ private :send_type_command
+
+ # Toggles transfers in binary mode and yields to a block.
+ # This preserves your current binary send mode, but allows a temporary
+ # transaction with binary sendmode of +newmode+.
+ #
+ # +newmode+ is either +true+ or +false+
+ def with_binary(newmode) # :nodoc:
+ oldmode = binary
+ self.binary = newmode
+ begin
+ yield
+ ensure
+ self.binary = oldmode
+ end
+ end
+ private :with_binary
# Obsolete
- def return_code
- $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
+ def return_code # :nodoc:
+ warn("Net::FTP#return_code is obsolete and do nothing", uplevel: 1)
return "\n"
end
# Obsolete
- def return_code=(s)
- $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
+ def return_code=(s) # :nodoc:
+ warn("Net::FTP#return_code= is obsolete and do nothing", uplevel: 1)
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
+ # Constructs a socket with +host+ and +port+.
+ #
+ # If SOCKSSocket is defined and the environment (ENV) defines
+ # SOCKS_SERVER, then a SOCKSSocket is returned, else a Socket is
+ # returned.
+ def open_socket(host, port) # :nodoc:
+ return Timeout.timeout(@open_timeout, OpenTimeout) {
+ if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
+ @passive = true
+ SOCKSSocket.open(host, port)
+ else
+ Socket.tcp(host, port)
+ end
+ }
end
private :open_socket
-
+
+ def start_tls_session(sock)
+ ssl_sock = SSLSocket.new(sock, @ssl_context)
+ ssl_sock.sync_close = true
+ ssl_sock.hostname = @host if ssl_sock.respond_to? :hostname=
+ if @ssl_session &&
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
+ # ProFTPD returns 425 for data connections if session is not reused.
+ ssl_sock.session = @ssl_session
+ end
+ ssl_socket_connect(ssl_sock, @ssl_handshake_timeout || @open_timeout)
+ if @ssl_context.verify_mode != VERIFY_NONE
+ ssl_sock.post_connection_check(@host)
+ end
+ return ssl_sock
+ end
+ private :start_tls_session
+
#
# Establishes an FTP connection to host, optionally overriding the default
# port. If the environment variable +SOCKS_SERVER+ is set, sets up the
@@ -169,226 +365,261 @@ module Net
#
def connect(host, port = FTP_PORT)
if @debug_mode
- print "connect: ", host, ", ", port, "\n"
+ print "connect: ", host, ", ", port, "\n"
end
synchronize do
- @sock = open_socket(host, port)
- voidresp
+ @host = host
+ @bare_sock = open_socket(host, port)
+ @sock = BufferedSocket.new(@bare_sock, read_timeout: @read_timeout)
+ voidresp
+ if @ssl_context
+ begin
+ voidcmd("AUTH TLS")
+ ssl_sock = start_tls_session(@bare_sock)
+ @sock = BufferedSSLSocket.new(ssl_sock, read_timeout: @read_timeout)
+ if @private_data_connection
+ voidcmd("PBSZ 0")
+ voidcmd("PROT P")
+ end
+ rescue OpenSSL::SSL::SSLError, OpenTimeout
+ @sock.close
+ raise
+ end
+ end
end
end
#
- # WRITEME or make private
+ # Set the socket used to connect to the FTP server.
#
+ # May raise FTPReplyError if +get_greeting+ is false.
def set_socket(sock, get_greeting = true)
synchronize do
- @sock = sock
- if get_greeting
- voidresp
- end
+ @sock = sock
+ if get_greeting
+ voidresp
+ end
end
end
- def sanitize(s)
+ # If string +s+ includes the PASS command (password), then the contents of
+ # the password are cleaned from the string using "*"
+ def sanitize(s) # :nodoc:
if s =~ /^PASS /i
- return s[0, 5] + "*" * (s.length - 5)
+ return s[0, 5] + "*" * (s.length - 5)
else
- return s
+ return s
end
end
private :sanitize
-
- def putline(line)
+
+ # Ensures that +line+ has a control return / line feed (CRLF) and writes
+ # it to the socket.
+ def putline(line) # :nodoc:
if @debug_mode
- print "put: ", sanitize(line), "\n"
+ print "put: ", sanitize(line), "\n"
+ end
+ if /[\r\n]/ =~ line
+ raise ArgumentError, "A line must not contain CR or LF"
end
line = line + CRLF
@sock.write(line)
end
private :putline
-
- def getline
+
+ # Reads a line from the sock. If EOF, then it will raise EOFError
+ def getline # :nodoc:
line = @sock.readline # if get EOF, raise EOFError
line.sub!(/(\r\n|\n|\r)\z/n, "")
if @debug_mode
- print "get: ", sanitize(line), "\n"
+ 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"
+
+ # Receive a section of lines until the response code's match.
+ def getmultiline # :nodoc:
+ lines = []
+ lines << getline
+ code = lines.last.slice(/\A([0-9a-zA-Z]{3})-/, 1)
+ if code
+ delimiter = code + " "
+ begin
+ lines << getline
+ end until lines.last.start_with?(delimiter)
+ end
+ return lines.join("\n") + "\n"
end
private :getmultiline
-
- def getresp
+
+ # Receives a response from the destination host.
+ #
+ # Returns the response code or raises FTPTempError, FTPPermError, or
+ # FTPProtoError
+ def getresp # :nodoc:
@last_response = getmultiline
@last_response_code = @last_response[0, 3]
case @last_response_code
when /\A[123]/
- return @last_response
+ return @last_response
when /\A4/
- raise FTPTempError, @last_response
+ raise FTPTempError, @last_response
when /\A5/
- raise FTPPermError, @last_response
+ raise FTPPermError, @last_response
else
- raise FTPProtoError, @last_response
+ raise FTPProtoError, @last_response
end
end
private :getresp
-
- def voidresp
+
+ # Receives a response.
+ #
+ # Raises FTPReplyError if the first position of the response code is not
+ # equal 2.
+ def voidresp # :nodoc:
resp = getresp
- if resp[0] != ?2
- raise FTPReplyError, resp
+ if !resp.start_with?("2")
+ raise FTPReplyError, resp
end
end
private :voidresp
-
+
#
# Sends a command and returns the response.
#
def sendcmd(cmd)
synchronize do
- putline(cmd)
- return getresp
+ 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) + "|"
+ putline(cmd)
+ voidresp
+ end
+ end
+
+ # Constructs and send the appropriate PORT (or EPRT) command
+ def sendport(host, port) # :nodoc:
+ remote_address = @bare_sock.remote_address
+ if remote_address.ipv4?
+ cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
+ elsif remote_address.ipv6?
+ cmd = sprintf("EPRT |2|%s|%d|", host, port)
else
- raise FTPProtoError, host
+ 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
+
+ # Constructs a TCPServer socket
+ def makeport # :nodoc:
+ Addrinfo.tcp(@bare_sock.local_address.ip_address, 0).listen
end
private :makeport
-
- def makepasv
- if @sock.peeraddr[0] == "AF_INET"
- host, port = parse227(sendcmd("PASV"))
+
+ # sends the appropriate command to enable a passive connection
+ def makepasv # :nodoc:
+ if @bare_sock.remote_address.ipv4?
+ host, port = parse227(sendcmd("PASV"))
else
- host, port = parse229(sendcmd("EPSV"))
- # host, port = parse228(sendcmd("LPSV"))
+ host, port = parse229(sendcmd("EPSV"))
+ # host, port = parse228(sendcmd("LPSV"))
end
return host, port
end
private :makepasv
-
- def transfercmd(cmd, rest_offset = nil)
+
+ # Constructs a connection for transferring data
+ def transfercmd(cmd, rest_offset = nil) # :nodoc:
if @passive
- host, port = makepasv
- conn = open_socket(host, port)
- if @resume and rest_offset
- resp = sendcmd("REST " + rest_offset.to_s)
- if resp[0] != ?3
- raise FTPReplyError, resp
- end
- end
- resp = sendcmd(cmd)
- if resp[0] != ?1
- raise FTPReplyError, resp
- end
+ host, port = makepasv
+ conn = open_socket(host, port)
+ if @resume and rest_offset
+ resp = sendcmd("REST " + rest_offset.to_s)
+ if !resp.start_with?("3")
+ raise FTPReplyError, resp
+ end
+ end
+ resp = sendcmd(cmd)
+ # skip 2XX for some ftp servers
+ resp = getresp if resp.start_with?("2")
+ if !resp.start_with?("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"]
+ sock = makeport
+ begin
+ addr = sock.local_address
+ sendport(addr.ip_address, addr.ip_port)
+ if @resume and rest_offset
+ resp = sendcmd("REST " + rest_offset.to_s)
+ if !resp.start_with?("3")
+ raise FTPReplyError, resp
+ end
+ end
+ resp = sendcmd(cmd)
+ # skip 2XX for some ftp servers
+ resp = getresp if resp.start_with?("2")
+ if !resp.start_with?("1")
+ raise FTPReplyError, resp
+ end
+ conn, = sock.accept
+ sock.shutdown(Socket::SHUT_WR) rescue nil
+ sock.read rescue nil
+ ensure
+ sock.close
+ end
+ end
+ if @private_data_connection
+ return BufferedSSLSocket.new(start_tls_session(conn),
+ read_timeout: @read_timeout)
else
- realuser = "anonymous"
+ return BufferedSocket.new(conn, read_timeout: @read_timeout)
end
- return realuser + "@" + thishost
end
- private :getaddress
-
+ private :transfercmd
+
#
- # Logs in to the remote host. The session must have been previously
- # connected. If +user+ is the string "anonymous" and the +password+ is
- # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+
- # parameter is not +nil+, an FTP ACCT command is sent following the
- # successful login. Raises an exception on error (typically
- # <tt>Net::FTPPermError</tt>).
+ # Logs in to the remote host. The session must have been
+ # previously connected. If +user+ is the string "anonymous" and
+ # the +password+ is +nil+, "anonymous@" is used as a password. If
+ # the +acct+ parameter is not +nil+, an FTP ACCT command is sent
+ # following the successful login. Raises an exception on error
+ # (typically <tt>Net::FTPPermError</tt>).
#
def login(user = "anonymous", passwd = nil, acct = nil)
if user == "anonymous" and passwd == nil
- passwd = getaddress
+ passwd = "anonymous@"
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
+ resp = sendcmd('USER ' + user)
+ if resp.start_with?("3")
+ raise FTPReplyError, resp if passwd.nil?
+ resp = sendcmd('PASS ' + passwd)
+ end
+ if resp.start_with?("3")
+ raise FTPReplyError, resp if acct.nil?
+ resp = sendcmd('ACCT ' + acct)
+ end
end
- if resp[0] != ?2
- raise FTPReplyError, resp
+ if !resp.start_with?("2")
+ raise FTPReplyError, resp
end
@welcome = resp
+ send_type_command
+ @logged_in = true
end
-
+
#
# Puts the connection into binary (image) mode, issues the given command,
# and fetches the data returned, passing it to the associated block in
@@ -397,18 +628,25 @@ module Net
#
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
+ with_binary(true) do
+ begin
+ conn = transfercmd(cmd, rest_offset)
+ loop do
+ data = conn.read(blocksize)
+ break if data == nil
+ yield(data)
+ end
+ conn.shutdown(Socket::SHUT_WR)
+ conn.read_timeout = 1
+ conn.read
+ ensure
+ conn.close if conn
+ end
+ voidresp
+ end
end
end
-
+
#
# Puts the connection into ASCII (text) mode, issues the given command, and
# passes the resulting data, one line at a time, to the associated block. If
@@ -417,110 +655,149 @@ module Net
#
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
-
+ with_binary(false) do
+ begin
+ conn = transfercmd(cmd)
+ loop do
+ line = conn.gets
+ break if line == nil
+ yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?)
+ end
+ conn.shutdown(Socket::SHUT_WR)
+ conn.read_timeout = 1
+ conn.read
+ ensure
+ conn.close if conn
+ end
+ voidresp
+ end
+ end
+ end
+
#
# Puts the connection into binary (image) mode, issues the given server-side
# command (such as "STOR myfile"), and sends the contents of the file named
# +file+ to the server. If the optional block is given, it also passes it
# the data, in chunks of +blocksize+ characters.
#
- def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data
+ def storbinary(cmd, file, blocksize, rest_offset = nil) # :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
+ with_binary(true) do
+ conn = transfercmd(cmd)
+ loop do
+ buf = file.read(blocksize)
+ break if buf == nil
+ conn.write(buf)
+ yield(buf) if block_given?
+ end
+ conn.close
+ voidresp
+ end
end
+ rescue Errno::EPIPE
+ # EPIPE, in this case, means that the data connection was unexpectedly
+ # terminated. Rather than just raising EPIPE to the caller, check the
+ # response on the control connection. If getresp doesn't raise a more
+ # appropriate exception, re-raise the original exception.
+ getresp
+ raise
end
-
+
#
# Puts the connection into ASCII (text) mode, issues the given server-side
# command (such as "STOR myfile"), and sends the contents of the file
# named +file+ to the server, one line at a time. If the optional block is
# given, it also passes it the lines.
#
- def storlines(cmd, file, &block) # :yield: line
+ def storlines(cmd, file) # :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
+ with_binary(false) do
+ conn = transfercmd(cmd)
+ loop do
+ buf = file.gets
+ break if buf == nil
+ if buf[-2, 2] != CRLF
+ buf = buf.chomp + CRLF
+ end
+ conn.write(buf)
+ yield(buf) if block_given?
+ end
+ conn.close
+ voidresp
+ end
end
+ rescue Errno::EPIPE
+ # EPIPE, in this case, means that the data connection was unexpectedly
+ # terminated. Rather than just raising EPIPE to the caller, check the
+ # response on the control connection. If getresp doesn't raise a more
+ # appropriate exception, re-raise the original exception.
+ getresp
+ raise
end
#
# Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
+ # If +localfile+ is nil, returns retrieved data.
# If a block is supplied, it is passed the retrieved data in +blocksize+
# chunks.
#
def getbinaryfile(remotefile, localfile = File.basename(remotefile),
- blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
- if @resume
- rest_offset = File.size?(localfile)
- f = open(localfile, "a")
- else
- rest_offset = nil
- f = open(localfile, "w")
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
+ f = nil
+ result = nil
+ if localfile
+ if @resume
+ rest_offset = File.size?(localfile)
+ f = File.open(localfile, "a")
+ else
+ rest_offset = nil
+ f = File.open(localfile, "w")
+ end
+ elsif !block_given?
+ result = String.new
end
begin
- f.binmode
- retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data|
- f.write(data)
- yield(data) if block
- end
+ f&.binmode
+ retrbinary("RETR #{remotefile}", blocksize, rest_offset) do |data|
+ f&.write(data)
+ block&.(data)
+ result&.concat(data)
+ end
+ return result
ensure
- f.close
+ 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
+ # +localfile+.
+ # If +localfile+ is nil, returns retrieved data.
+ # If a block is supplied, it is passed the retrieved data one
# line at a time.
#
- def gettextfile(remotefile, localfile = File.basename(remotefile), &block) # :yield: line
- f = open(localfile, "w")
+ def gettextfile(remotefile, localfile = File.basename(remotefile),
+ &block) # :yield: line
+ f = nil
+ result = nil
+ if localfile
+ f = File.open(localfile, "w")
+ elsif !block_given?
+ result = String.new
+ end
begin
- retrlines("RETR " + remotefile) do |line|
- f.puts(line)
- yield(line) if block
- end
+ retrlines("RETR #{remotefile}") do |line, newline|
+ l = newline ? line + "\n" : line
+ f&.print(l)
+ block&.(line, newline)
+ result&.concat(l)
+ end
+ return result
ensure
- f.close
+ f&.close
end
end
@@ -529,21 +806,21 @@ module Net
# binary). See #gettextfile and #getbinaryfile.
#
def get(remotefile, localfile = File.basename(remotefile),
- blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
- unless @binary
- gettextfile(remotefile, localfile, &block)
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
+ if @binary
+ getbinaryfile(remotefile, localfile, blocksize, &block)
else
- getbinaryfile(remotefile, localfile, blocksize, &block)
+ gettextfile(remotefile, localfile, &block)
end
end
-
+
#
# Transfers +localfile+ to the server in binary mode, storing the result in
# +remotefile+. If a block is supplied, calls it, passing in the transmitted
# data in +blocksize+ chunks.
#
def putbinaryfile(localfile, remotefile = File.basename(localfile),
- blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
if @resume
begin
rest_offset = size(remotefile)
@@ -551,28 +828,32 @@ module Net
rest_offset = nil
end
else
- rest_offset = nil
+ rest_offset = nil
end
- f = open(localfile)
+ f = File.open(localfile)
begin
- f.binmode
- storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
+ f.binmode
+ if rest_offset
+ storbinary("APPE #{remotefile}", f, blocksize, rest_offset, &block)
+ else
+ storbinary("STOR #{remotefile}", f, blocksize, rest_offset, &block)
+ end
ensure
- f.close
+ 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)
+ f = File.open(localfile)
begin
- storlines("STOR " + remotefile, f, &block)
+ storlines("STOR #{remotefile}", f, &block)
ensure
- f.close
+ f.close
end
end
@@ -581,37 +862,40 @@ module Net
# (text or binary). See #puttextfile and #putbinaryfile.
#
def put(localfile, remotefile = File.basename(localfile),
- blocksize = DEFAULT_BLOCKSIZE, &block)
- unless @binary
- puttextfile(localfile, remotefile, &block)
+ blocksize = DEFAULT_BLOCKSIZE, &block)
+ if @binary
+ putbinaryfile(localfile, remotefile, blocksize, &block)
else
- putbinaryfile(localfile, remotefile, blocksize, &block)
+ puttextfile(localfile, remotefile, &block)
end
end
#
- # Sends the ACCT command. TODO: more info.
+ # Sends the ACCT command.
+ #
+ # This is a less common FTP command, to send account
+ # information if the destination host requires it.
#
def acct(account)
cmd = "ACCT " + account
voidcmd(cmd)
end
-
+
#
# Returns an array of filenames in the remote directory.
#
def nlst(dir = nil)
cmd = "NLST"
if dir
- cmd = cmd + " " + dir
+ cmd = "#{cmd} #{dir}"
end
files = []
retrlines(cmd) do |line|
- files.push(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.
@@ -619,103 +903,303 @@ module Net
def list(*args, &block) # :yield: line
cmd = "LIST"
args.each do |arg|
- cmd = cmd + " " + arg
+ cmd = "#{cmd} #{arg}"
+ end
+ lines = []
+ retrlines(cmd) do |line|
+ lines << line
end
if block
- retrlines(cmd, &block)
- else
- lines = []
- retrlines(cmd) do |line|
- lines << line
- end
- return lines
+ lines.each(&block)
end
+ return lines
end
alias ls list
alias dir list
-
+
+ #
+ # MLSxEntry represents an entry in responses of MLST/MLSD.
+ # Each entry has the facts (e.g., size, last modification time, etc.)
+ # and the pathname.
+ #
+ class MLSxEntry
+ attr_reader :facts, :pathname
+
+ def initialize(facts, pathname)
+ @facts = facts
+ @pathname = pathname
+ end
+
+ standard_facts = %w(size modify create type unique perm
+ lang media-type charset)
+ standard_facts.each do |factname|
+ define_method factname.gsub(/-/, "_") do
+ facts[factname]
+ end
+ end
+
+ #
+ # Returns +true+ if the entry is a file (i.e., the value of the type
+ # fact is file).
+ #
+ def file?
+ return facts["type"] == "file"
+ end
+
+ #
+ # Returns +true+ if the entry is a directory (i.e., the value of the
+ # type fact is dir, cdir, or pdir).
+ #
+ def directory?
+ if /\A[cp]?dir\z/.match(facts["type"])
+ return true
+ else
+ return false
+ end
+ end
+
+ #
+ # Returns +true+ if the APPE command may be applied to the file.
+ #
+ def appendable?
+ return facts["perm"].include?(?a)
+ end
+
+ #
+ # Returns +true+ if files may be created in the directory by STOU,
+ # STOR, APPE, and RNTO.
+ #
+ def creatable?
+ return facts["perm"].include?(?c)
+ end
+
+ #
+ # Returns +true+ if the file or directory may be deleted by DELE/RMD.
+ #
+ def deletable?
+ return facts["perm"].include?(?d)
+ end
+
+ #
+ # Returns +true+ if the directory may be entered by CWD/CDUP.
+ #
+ def enterable?
+ return facts["perm"].include?(?e)
+ end
+
+ #
+ # Returns +true+ if the file or directory may be renamed by RNFR.
+ #
+ def renamable?
+ return facts["perm"].include?(?f)
+ end
+
+ #
+ # Returns +true+ if the listing commands, LIST, NLST, and MLSD are
+ # applied to the directory.
+ #
+ def listable?
+ return facts["perm"].include?(?l)
+ end
+
+ #
+ # Returns +true+ if the MKD command may be used to create a new
+ # directory within the directory.
+ #
+ def directory_makable?
+ return facts["perm"].include?(?m)
+ end
+
+ #
+ # Returns +true+ if the objects in the directory may be deleted, or
+ # the directory may be purged.
+ #
+ def purgeable?
+ return facts["perm"].include?(?p)
+ end
+
+ #
+ # Returns +true+ if the RETR command may be applied to the file.
+ #
+ def readable?
+ return facts["perm"].include?(?r)
+ end
+
+ #
+ # Returns +true+ if the STOR command may be applied to the file.
+ #
+ def writable?
+ return facts["perm"].include?(?w)
+ end
+ end
+
+ CASE_DEPENDENT_PARSER = ->(value) { value }
+ CASE_INDEPENDENT_PARSER = ->(value) { value.downcase }
+ DECIMAL_PARSER = ->(value) { value.to_i }
+ OCTAL_PARSER = ->(value) { value.to_i(8) }
+ TIME_PARSER = ->(value, local = false) {
+ unless /\A(?<year>\d{4})(?<month>\d{2})(?<day>\d{2})
+ (?<hour>\d{2})(?<min>\d{2})(?<sec>\d{2})
+ (\.(?<fractions>\d+))?/x =~ value
+ raise FTPProtoError, "invalid time-val: #{value}"
+ end
+ usec = fractions.to_i * 10 ** (6 - fractions.to_s.size)
+ Time.send(local ? :local : :utc, year, month, day, hour, min, sec, usec)
+ }
+ FACT_PARSERS = Hash.new(CASE_DEPENDENT_PARSER)
+ FACT_PARSERS["size"] = DECIMAL_PARSER
+ FACT_PARSERS["modify"] = TIME_PARSER
+ FACT_PARSERS["create"] = TIME_PARSER
+ FACT_PARSERS["type"] = CASE_INDEPENDENT_PARSER
+ FACT_PARSERS["unique"] = CASE_DEPENDENT_PARSER
+ FACT_PARSERS["perm"] = CASE_INDEPENDENT_PARSER
+ FACT_PARSERS["lang"] = CASE_INDEPENDENT_PARSER
+ FACT_PARSERS["media-type"] = CASE_INDEPENDENT_PARSER
+ FACT_PARSERS["charset"] = CASE_INDEPENDENT_PARSER
+ FACT_PARSERS["unix.mode"] = OCTAL_PARSER
+ FACT_PARSERS["unix.owner"] = DECIMAL_PARSER
+ FACT_PARSERS["unix.group"] = DECIMAL_PARSER
+ FACT_PARSERS["unix.ctime"] = TIME_PARSER
+ FACT_PARSERS["unix.atime"] = TIME_PARSER
+
+ def parse_mlsx_entry(entry)
+ facts, pathname = entry.chomp.split(/ /, 2)
+ unless pathname
+ raise FTPProtoError, entry
+ end
+ return MLSxEntry.new(
+ facts.scan(/(.*?)=(.*?);/).each_with_object({}) {
+ |(factname, value), h|
+ name = factname.downcase
+ h[name] = FACT_PARSERS[name].(value)
+ },
+ pathname)
+ end
+ private :parse_mlsx_entry
+
+ #
+ # Returns data (e.g., size, last modification time, entry type, etc.)
+ # about the file or directory specified by +pathname+.
+ # If +pathname+ is omitted, the current directory is assumed.
+ #
+ def mlst(pathname = nil)
+ cmd = pathname ? "MLST #{pathname}" : "MLST"
+ resp = sendcmd(cmd)
+ if !resp.start_with?("250")
+ raise FTPReplyError, resp
+ end
+ line = resp.lines[1]
+ unless line
+ raise FTPProtoError, resp
+ end
+ entry = line.sub(/\A(250-| *)/, "")
+ return parse_mlsx_entry(entry)
+ end
+
+ #
+ # Returns an array of the entries of the directory specified by
+ # +pathname+.
+ # Each entry has the facts (e.g., size, last modification time, etc.)
+ # and the pathname.
+ # If a block is given, it iterates through the listing.
+ # If +pathname+ is omitted, the current directory is assumed.
+ #
+ def mlsd(pathname = nil, &block) # :yield: entry
+ cmd = pathname ? "MLSD #{pathname}" : "MLSD"
+ entries = []
+ retrlines(cmd) do |line|
+ entries << parse_mlsx_entry(line)
+ end
+ if block
+ entries.each(&block)
+ end
+ return entries
+ end
+
#
# Renames a file on the server.
#
def rename(fromname, toname)
- resp = sendcmd("RNFR " + fromname)
- if resp[0] != ?3
- raise FTPReplyError, resp
+ resp = sendcmd("RNFR #{fromname}")
+ if !resp.start_with?("3")
+ raise FTPReplyError, resp
end
- voidcmd("RNTO " + toname)
+ 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
+ resp = sendcmd("DELE #{filename}")
+ if resp.start_with?("250")
+ return
+ elsif resp.start_with?("5")
+ raise FTPPermError, resp
else
- raise FTPReplyError, resp
+ 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
+ begin
+ voidcmd("CDUP")
+ return
+ rescue FTPPermError => e
+ if e.message[0, 3] != "500"
+ raise e
+ end
+ end
+ end
+ cmd = "CWD #{dirname}"
voidcmd(cmd)
end
-
+
+ def get_body(resp) # :nodoc:
+ resp.slice(/\A[0-9a-zA-Z]{3} (.*)$/, 1)
+ end
+ private :get_body
+
#
# 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
+ with_binary(true) do
+ resp = sendcmd("SIZE #{filename}")
+ if !resp.start_with?("213")
+ raise FTPReplyError, resp
+ end
+ return get_body(resp).to_i
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)
+ return TIME_PARSER.(mdtm(filename), local)
end
-
+
#
# Creates a remote directory.
#
def mkdir(dirname)
- resp = sendcmd("MKD " + dirname)
+ resp = sendcmd("MKD #{dirname}")
return parse257(resp)
end
-
+
#
# Removes a remote directory.
#
def rmdir(dirname)
- voidcmd("RMD " + dirname)
+ voidcmd("RMD #{dirname}")
end
-
+
#
# Returns the current remote directory.
#
@@ -724,18 +1208,18 @@ module Net
return parse257(resp)
end
alias getdir pwd
-
+
#
# Returns system information.
#
def system
resp = sendcmd("SYST")
- if resp[0, 3] != "215"
- raise FTPReplyError, resp
+ if !resp.start_with?("215")
+ raise FTPReplyError, resp
end
- return resp[4 .. -1]
+ return get_body(resp)
end
-
+
#
# Aborts the previous command (ABOR command).
#
@@ -745,42 +1229,50 @@ module Net
@sock.send(line, Socket::MSG_OOB)
resp = getmultiline
unless ["426", "226", "225"].include?(resp[0, 3])
- raise FTPProtoError, resp
+ raise FTPProtoError, resp
end
return resp
end
-
+
#
# Returns the status (STAT command).
+ # pathname - when stat is invoked with pathname as a parameter it acts like
+ # list but alot faster and over the same tcp session.
#
- def status
- line = "STAT" + CRLF
- print "put: STAT\n" if @debug_mode
- @sock.send(line, Socket::MSG_OOB)
+ def status(pathname = nil)
+ line = pathname ? "STAT #{pathname}" : "STAT"
+ if /[\r\n]/ =~ line
+ raise ArgumentError, "A line must not contain CR or LF"
+ end
+ print "put: #{line}\n" if @debug_mode
+ @sock.send(line + CRLF, Socket::MSG_OOB)
return getresp
end
-
+
#
- # Issues the MDTM command. TODO: more info.
+ # Returns the raw last modification time of the (remote) file in the format
+ # "YYYYMMDDhhmmss" (MDTM command).
+ #
+ # Use +mtime+ if you want a parsed Time instance.
#
def mdtm(filename)
- resp = sendcmd("MDTM " + filename)
- if resp[0, 3] == "213"
- return resp[3 .. -1].strip
+ resp = sendcmd("MDTM #{filename}")
+ if resp.start_with?("213")
+ return get_body(resp)
end
end
-
+
#
# Issues the HELP command.
#
def help(arg = nil)
cmd = "HELP"
if arg
- cmd = cmd + " " + arg
+ cmd = cmd + " " + arg
end
sendcmd(cmd)
end
-
+
#
# Exits the FTP session.
#
@@ -791,6 +1283,8 @@ module Net
#
# Issues a NOOP command.
#
+ # Does nothing except return a response.
+ #
def noop
voidcmd("NOOP")
end
@@ -802,122 +1296,198 @@ module Net
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?
+ if @sock and not @sock.closed?
+ begin
+ @sock.shutdown(Socket::SHUT_WR) rescue nil
+ orig, self.read_timeout = self.read_timeout, 3
+ @sock.read rescue nil
+ ensure
+ @sock.close
+ self.read_timeout = orig
+ end
+ end
end
-
+
#
# Returns +true+ iff the connection is closed.
#
def closed?
@sock == nil or @sock.closed?
end
-
- 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
+
+ # handler for response code 227
+ # (Entering Passive Mode (h1,h2,h3,h4,p1,p2))
+ #
+ # Returns host and port.
+ def parse227(resp) # :nodoc:
+ if !resp.start_with?("227")
+ raise FTPReplyError, resp
end
- numbers = resp[left + 1 .. right - 1].split(",")
- if numbers.length != 6
- raise FTPProtoError, resp
+ if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
+ return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
+ else
+ raise FTPProtoError, resp
end
- 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
+
+ # handler for response code 228
+ # (Entering Long Passive Mode)
+ #
+ # Returns host and port.
+ def parse228(resp) # :nodoc:
+ if !resp.start_with?("228")
+ raise FTPReplyError, resp
+ end
+ if m = /\(4,4,(?<host>\d+(,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp)
+ return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
+ elsif m = /\(6,16,(?<host>\d+(,(\d+)){15}),2,(?<port>\d+,\d+)\)/.match(resp)
+ return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"])
+ else
+ raise FTPProtoError, resp
+ end
end
private :parse228
-
- def 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
+
+ def parse_pasv_ipv4_host(s)
+ return s.tr(",", ".")
+ end
+ private :parse_pasv_ipv4_host
+
+ def parse_pasv_ipv6_host(s)
+ return s.split(/,/).map { |i|
+ "%02x" % i.to_i
+ }.each_slice(2).map(&:join).join(":")
+ end
+ private :parse_pasv_ipv6_host
+
+ def parse_pasv_port(s)
+ return s.split(/,/).map(&:to_i).inject { |x, y|
+ (x << 8) + y
+ }
+ end
+ private :parse_pasv_port
+
+ # handler for response code 229
+ # (Extended Passive Mode Entered)
+ #
+ # Returns host and port.
+ def parse229(resp) # :nodoc:
+ if !resp.start_with?("229")
+ raise FTPReplyError, resp
+ end
+ if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp)
+ return @bare_sock.remote_address.ip_address, m["port"].to_i
+ else
+ raise FTPProtoError, resp
+ end
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
+
+ # handler for response code 257
+ # ("PATHNAME" created)
+ #
+ # Returns host and port.
+ def parse257(resp) # :nodoc:
+ if !resp.start_with?("257")
+ raise FTPReplyError, resp
+ end
+ return resp.slice(/"(([^"]|"")*)"/, 1).to_s.gsub(/""/, '"')
end
private :parse257
- end
+ # :stopdoc:
+ class NullSocket
+ def read_timeout=(sec)
+ end
+
+ def closed?
+ true
+ end
+
+ def close
+ end
+
+ def method_missing(mid, *args)
+ raise FTPConnectionError, "not connected"
+ end
+ end
+
+ class BufferedSocket < BufferedIO
+ [:local_address, :remote_address, :addr, :peeraddr, :send, :shutdown].each do |method|
+ define_method(method) { |*args|
+ @io.__send__(method, *args)
+ }
+ end
+
+ def read(len = nil)
+ if len
+ s = super(len, String.new, true)
+ return s.empty? ? nil : s
+ else
+ result = String.new
+ while s = super(DEFAULT_BLOCKSIZE, String.new, true)
+ break if s.empty?
+ result << s
+ end
+ return result
+ end
+ end
+
+ def gets
+ line = readuntil("\n", true)
+ return line.empty? ? nil : line
+ end
+
+ def readline
+ line = gets
+ if line.nil?
+ raise EOFError, "end of file reached"
+ end
+ return line
+ end
+ end
+
+ if defined?(OpenSSL::SSL::SSLSocket)
+ class BufferedSSLSocket < BufferedSocket
+ def initialize(*args)
+ super
+ @is_shutdown = false
+ end
+
+ def shutdown(*args)
+ # SSL_shutdown() will be called from SSLSocket#close, and
+ # SSL_shutdown() will send the "close notify" alert to the peer,
+ # so shutdown(2) should not be called.
+ @is_shutdown = true
+ end
+
+ def send(mesg, flags, dest = nil)
+ # Ignore flags and dest.
+ @io.write(mesg)
+ end
+
+ private
+
+ def rbuf_fill
+ if @is_shutdown
+ raise EOFError, "shutdown has been called"
+ else
+ super
+ end
+ end
+ end
+ end
+ # :startdoc:
+ end
end
# Documentation comments:
# - sourced from pickaxe and nutshell, with improvements (hopefully)
-# - three methods should be private (search WRITEME)
-# - two methods need more information (search TODO)
diff --git a/lib/net/http.rb b/lib/net/http.rb
index d518f32cbb..961ef398c3 100644
--- a/lib/net/http.rb
+++ b/lib/net/http.rb
@@ -1,32 +1,28 @@
+# 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: http.rb,v 1.100.2.14 2006/07/26 13:27:18 aamine Exp $
-#++
-
-require 'net/protocol'
+# See Net::HTTP for an overview and examples.
+#
+
+require_relative 'protocol'
require 'uri'
+autoload :OpenSSL, 'openssl'
module Net #:nodoc:
@@ -35,282 +31,392 @@ 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 a 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.
+ #
+ # The request types Net::HTTP supports are listed below in the section "HTTP
+ # Request Classes".
+ #
+ # If you wish to re-use a connection across multiple HTTP requests without
+ # automatically closing it you can use ::new instead of ::start. #request
+ # will automatically open a connection to the server if one is not currently
+ # open. You can manually close the connection with #finish.
+ #
+ # For all the Net::HTTP request objects and shortcut request methods you may
+ # supply either a String for the request path or a URI from which Net::HTTP
+ # will extract the request path.
+ #
+ # === 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 if res.response_body_permitted?
+ #
+ # === 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 urlencoded 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
+ #
+ # At this time Net::HTTP does not support multipart/form-data. To send
+ # multipart/form-data use Net::HTTPRequest#body= and
+ # Net::HTTPRequest#content_type=:
+ #
+ # req = Net::HTTP::Post.new(uri)
+ # req.body = multipart_data
+ # req.content_type = '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 a HTTPS URL. Net::HTTP automatically turn 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
+ # HTTPSuccess:: 2xx
+ # HTTPOK:: 200
+ # HTTPCreated:: 201
+ # HTTPAccepted:: 202
+ # HTTPNonAuthoritativeInformation:: 203
+ # HTTPNoContent:: 204
+ # HTTPResetContent:: 205
+ # HTTPPartialContent:: 206
+ # HTTPMultiStatus:: 207
+ # HTTPIMUsed:: 226
+ # HTTPRedirection:: 3xx
+ # HTTPMultipleChoices:: 300
+ # HTTPMovedPermanently:: 301
+ # HTTPFound:: 302
+ # HTTPSeeOther:: 303
+ # HTTPNotModified:: 304
+ # HTTPUseProxy:: 305
+ # HTTPTemporaryRedirect:: 307
+ # HTTPClientError:: 4xx
+ # HTTPBadRequest:: 400
+ # HTTPUnauthorized:: 401
+ # HTTPPaymentRequired:: 402
+ # HTTPForbidden:: 403
+ # HTTPNotFound:: 404
+ # HTTPMethodNotAllowed:: 405
+ # HTTPNotAcceptable:: 406
+ # HTTPProxyAuthenticationRequired:: 407
+ # HTTPRequestTimeOut:: 408
+ # HTTPConflict:: 409
+ # HTTPGone:: 410
+ # HTTPLengthRequired:: 411
+ # HTTPPreconditionFailed:: 412
+ # HTTPRequestEntityTooLarge:: 413
+ # HTTPRequestURITooLong:: 414
+ # HTTPUnsupportedMediaType:: 415
+ # HTTPRequestedRangeNotSatisfiable:: 416
+ # HTTPExpectationFailed:: 417
+ # HTTPUnprocessableEntity:: 422
+ # HTTPLocked:: 423
+ # HTTPFailedDependency:: 424
+ # HTTPUpgradeRequired:: 426
+ # HTTPPreconditionRequired:: 428
+ # HTTPTooManyRequests:: 429
+ # HTTPRequestHeaderFieldsTooLarge:: 431
+ # HTTPUnavailableForLegalReasons:: 451
+ # HTTPServerError:: 5xx
+ # HTTPInternalServerError:: 500
+ # HTTPNotImplemented:: 501
+ # HTTPBadGateway:: 502
+ # HTTPServiceUnavailable:: 503
+ # HTTPGatewayTimeOut:: 504
+ # HTTPVersionNotSupported:: 505
+ # HTTPInsufficientStorage:: 507
+ # HTTPNetworkAuthenticationRequired:: 511
+ #
+ # There is also the Net::HTTPBadResponse exception which is raised when
+ # there is a protocol error.
#
class HTTP < Protocol
# :stopdoc:
- Revision = %q$Revision: 1.100.2.14 $.split[1]
+ Revision = %q$Revision$.split[1]
HTTPVersion = '1.1'
- @newimpl = true
+ begin
+ require 'zlib'
+ require 'stringio' #for our purposes (unpacking gzip) lump these together
+ HAVE_ZLIB=true
+ rescue LoadError
+ HAVE_ZLIB=false
+ end
# :startdoc:
- # Turns on net/http 1.2 (ruby 1.8) features.
- # Defaults to ON in ruby 1.8.
- #
- # 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,11 +429,11 @@ 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+), 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:
#
@@ -342,11 +448,11 @@ 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'))
+ #
+ # print Net::HTTP.get(URI('http://www.example.com/index.html'))
#
# or:
#
@@ -356,11 +462,11 @@ module Net #:nodoc:
get_response(uri_or_host, path, 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'))
+ #
+ # res = Net::HTTP.get_response(URI('http://www.example.com/index.html'))
# print res.body
#
# or:
@@ -376,32 +482,55 @@ module Net #:nodoc:
}
else
uri = uri_or_host
- new(uri.host, uri.port).start {|http|
- return http.request_get(uri.request_uri, &block)
+ start(uri.hostname, uri.port,
+ :use_ssl => uri.scheme == 'https') {|http|
+ return http.request_get(uri, &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 iff +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 +558,230 @@ 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 ca_file, ca_path, cert, cert_store, ciphers,
+ # close_on_empty_response, key, open_timeout, read_timeout, ssl_timeout,
+ # ssl_version, use_ssl, verify_callback, verify_depth and verify_mode.
+ # If you set :use_ssl as true, you can use https and default value of
+ # verify_mode is set as OpenSSL::SSL::VERIFY_PEER.
+ #
+ # If the optional block is given, the newly
+ # created Net::HTTP object is passed to it and closed when the
# block finishes. In this case, the return value of this method
# is the return value of the block. If no block is given, the
# return value of this method is the newly created Net::HTTP object
- # itself, and the caller is responsible for closing it upon completion.
- 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)
+
+ 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)
+ @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
+ @continue_timeout = nil
+ @max_retries = 1
@debug_output = nil
+
+ @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
+
+ attr_writer :proxy_from_env
+ attr_writer :proxy_address
+ attr_writer :proxy_port
+ attr_writer :proxy_user
+ attr_writer :proxy_pass
+
+ # Number of seconds to wait for the connection to open. Any number
+ # may be used, including Floats for fractional seconds. If the HTTP
+ # object cannot open a connection in this many seconds, it raises a
+ # Net::OpenTimeout exception. The default value is 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
+ # 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.
+ # 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
+
+ # Returns true if the HTTP session has been started.
def started?
@started
end
@@ -521,19 +790,117 @@ 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,
+ :@key,
+ :@ssl_timeout,
+ :@ssl_version,
+ :@min_version,
+ :@max_version,
+ :@verify_callback,
+ :@verify_depth,
+ :@verify_mode,
+ ]
+ SSL_ATTRIBUTES = [
+ :ca_file,
+ :ca_path,
+ :cert,
+ :cert_store,
+ :ciphers,
+ :key,
+ :ssl_timeout,
+ :ssl_version,
+ :min_version,
+ :max_version,
+ :verify_callback,
+ :verify_depth,
+ :verify_mode,
+ ]
+
+ # 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 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
+
+ # 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 +923,81 @@ 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
+ if proxy? then
+ conn_address = proxy_address
+ conn_port = proxy_port
+ else
+ conn_address = address
+ conn_port = port
end
- @socket = BufferedIO.new(s)
- @socket.read_timeout = @read_timeout
- @socket.debug_output = @debug_output
+
+ D "opening connection to #{conn_address}:#{conn_port}..."
+ s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
+ begin
+ TCPSocket.open(conn_address, conn_port, @local_host, @local_port)
+ rescue => e
+ raise e, "Failed to open TCP connection to " +
+ "#{conn_address}:#{conn_port} (#{e.message})"
+ end
+ }
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ D "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,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ buf = "CONNECT #{@address}:#{@port} HTTP/#{HTTPVersion}\r\n"
+ buf << "Host: #{@address}:#{@port}\r\n"
if proxy_user
- credential = ["#{proxy_user}:#{proxy_pass}"].pack('m')
- credential.delete!("\r\n")
- @socket.writeline "Proxy-Authorization: Basic #{credential}"
+ credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
+ buf << "Proxy-Authorization: Basic #{credential}\r\n"
+ end
+ buf << "\r\n"
+ plain_sock.write(buf)
+ HTTPResponse.read_new(plain_sock).value
+ # assuming nothing left in buffers after successful CONNECT response
+ end
+
+ ssl_parameters = Hash.new
+ iv_list = instance_variables
+ SSL_IVNAMES.each_with_index do |ivname, i|
+ if iv_list.include?(ivname) and
+ value = instance_variable_get(ivname)
+ ssl_parameters[SSL_ATTRIBUTES[i]] = value if value
end
- @socket.writeline ''
- HTTPResponse.read_new(@socket).value
end
- s.connect
+ @ssl_context = OpenSSL::SSL::SSLContext.new
+ @ssl_context.set_params(ssl_parameters)
+ @ssl_context.session_cache_mode =
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
+ @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
+ D "starting SSL for #{conn_address}:#{conn_port}..."
+ s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
+ s.sync_close = true
+ # Server Name Indication (SNI) RFC 3546
+ s.hostname = @address if s.respond_to? :hostname=
+ if @ssl_session and
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
+ s.session = @ssl_session
+ end
+ ssl_socket_connect(s, @open_timeout)
if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
s.post_connection_check(@address)
end
+ D "SSL established"
end
+ @socket = BufferedIO.new(s, read_timeout: @read_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
on_connect
+ rescue => exception
+ if s
+ D "Conn close because of connect error #{exception}"
+ s.close
+ end
+ raise
end
private :connect
@@ -596,8 +1005,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 +1014,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 +1027,118 @@ 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
+
+ # [Bug #12921]
+ if /linux|freebsd|darwin/ =~ RUBY_PLATFORM
+ ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = true
+ else
+ ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = false
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 ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
+ proxy_uri&.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 ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
+ proxy_uri&.password
+ else
+ @proxy_pass
+ end
end
alias proxyaddr proxy_address #:nodoc: obsolete
@@ -696,33 +1146,25 @@ module Net #:nodoc:
private
- # without proxy
+ # without proxy, obsolete
- def conn_address
+ def conn_address # :nodoc:
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 +1174,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 +1200,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 +1217,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 +1266,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 +1339,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 +1368,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 +1382,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 +1423,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,51 +1461,110 @@ 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) {
+ req.exec @socket, @curr_http_version, edit_path(req.path)
+ begin
+ res = HTTPResponse.read_new(@socket)
+ res.decode_content = req.decode_content
+ 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,
+ # 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
+ D "Conn close because of error #{exception}, and retry"
+ retry
+ end
+ D "Conn close because of error #{exception}"
+ @socket.close if @socket
+ raise
+ end
+ end_transport req, res
res
+ rescue => exception
+ D "Conn close because of error #{exception}"
+ @socket.close if @socket
+ 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)
+ D 'Conn close because of keep_alive_timeout'
+ @socket.close
+ connect
+ elsif @socket.io.to_io.wait_readable(0) && @socket.eof?
+ D "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
+ @last_communicated = nil
+ if @socket.closed?
+ D 'Conn socket closed'
+ elsif not res.body and @close_on_empty_response
D '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
+ @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC)
else
D 'Conn close'
@socket.close
@@ -1090,1188 +1572,75 @@ module Net #:nodoc:
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'
- 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
+ false
end
- result
- end
-
- # Set Content-Type: header field by +type+ and +params+.
- # +type+ must be a String, +params+ must be a Hash.
- def set_content_type(type, params = {})
- @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
- end
-
- alias content_type= set_content_type
-
- # Set header fields and a body from HTML form data.
- # +params+ should be a Hash containing HTML form data.
- # Optional argument +sep+ means data record separator.
- #
- # This method also set Content-Type: header field to
- # application/x-www-form-urlencoded.
- 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
+ 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
#
- # 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
+ def D(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
+end
- class HTTPBadRequest < HTTPClientError # 400
- HAS_BODY = true
- end
- class HTTPUnauthorized < HTTPClientError # 401
- HAS_BODY = true
- end
- class HTTPPaymentRequired < HTTPClientError # 402
- HAS_BODY = true
- end
- class HTTPForbidden < HTTPClientError # 403
- HAS_BODY = true
- end
- class HTTPNotFound < HTTPClientError # 404
- HAS_BODY = true
- end
- class HTTPMethodNotAllowed < HTTPClientError # 405
- HAS_BODY = true
- end
- class HTTPNotAcceptable < HTTPClientError # 406
- HAS_BODY = true
- end
- class HTTPProxyAuthenticationRequired < HTTPClientError # 407
- HAS_BODY = true
- end
- class HTTPRequestTimeOut < HTTPClientError # 408
- HAS_BODY = true
- end
- class HTTPConflict < HTTPClientError # 409
- HAS_BODY = true
- end
- class HTTPGone < HTTPClientError # 410
- HAS_BODY = true
- end
- class HTTPLengthRequired < HTTPClientError # 411
- HAS_BODY = true
- end
- class HTTPPreconditionFailed < HTTPClientError # 412
- HAS_BODY = true
- end
- class HTTPRequestEntityTooLarge < HTTPClientError # 413
- HAS_BODY = true
- end
- class HTTPRequestURITooLong < HTTPClientError # 414
- HAS_BODY = true
- end
- HTTPRequestURITooLarge = HTTPRequestURITooLong
- class HTTPUnsupportedMediaType < HTTPClientError # 415
- HAS_BODY = true
- end
- class HTTPRequestedRangeNotSatisfiable < HTTPClientError # 416
- HAS_BODY = true
- end
- class HTTPExpectationFailed < HTTPClientError # 417
- HAS_BODY = true
- end
-
- class HTTPInternalServerError < HTTPServerError # 500
- HAS_BODY = true
- end
- class HTTPNotImplemented < HTTPServerError # 501
- HAS_BODY = true
- end
- class HTTPBadGateway < HTTPServerError # 502
- HAS_BODY = true
- end
- class HTTPServiceUnavailable < HTTPServerError # 503
- HAS_BODY = true
- end
- class HTTPGatewayTimeOut < HTTPServerError # 504
- HAS_BODY = true
- end
- class HTTPVersionNotSupported < HTTPServerError # 505
- HAS_BODY = true
- end
-
- # :startdoc:
-
-
- class HTTPResponse # reopen
-
- CODE_CLASS_TO_OBJ = {
- '1' => HTTPInformation,
- '2' => HTTPSuccess,
- '3' => HTTPRedirection,
- '4' => HTTPClientError,
- '5' => HTTPServerError
- }
- CODE_TO_OBJ = {
- '100' => HTTPContinue,
- '101' => HTTPSwitchProtocol,
-
- '200' => HTTPOK,
- '201' => HTTPCreated,
- '202' => HTTPAccepted,
- '203' => HTTPNonAuthoritativeInformation,
- '204' => HTTPNoContent,
- '205' => HTTPResetContent,
- '206' => HTTPPartialContent,
-
- '300' => HTTPMultipleChoice,
- '301' => HTTPMovedPermanently,
- '302' => HTTPFound,
- '303' => HTTPSeeOther,
- '304' => HTTPNotModified,
- '305' => HTTPUseProxy,
- '307' => HTTPTemporaryRedirect,
-
- '400' => HTTPBadRequest,
- '401' => HTTPUnauthorized,
- '402' => HTTPPaymentRequired,
- '403' => HTTPForbidden,
- '404' => HTTPNotFound,
- '405' => HTTPMethodNotAllowed,
- '406' => HTTPNotAcceptable,
- '407' => HTTPProxyAuthenticationRequired,
- '408' => HTTPRequestTimeOut,
- '409' => HTTPConflict,
- '410' => HTTPGone,
- '411' => HTTPLengthRequired,
- '412' => HTTPPreconditionFailed,
- '413' => HTTPRequestEntityTooLarge,
- '414' => HTTPRequestURITooLong,
- '415' => HTTPUnsupportedMediaType,
- '416' => HTTPRequestedRangeNotSatisfiable,
- '417' => HTTPExpectationFailed,
-
- '500' => HTTPInternalServerError,
- '501' => HTTPNotImplemented,
- '502' => HTTPBadGateway,
- '503' => HTTPServiceUnavailable,
- '504' => HTTPGatewayTimeOut,
- '505' => HTTPVersionNotSupported
- }
-
- class << HTTPResponse
- def read_new(sock) #:nodoc: internal use only
- httpv, code, msg = read_status_line(sock)
- res = response_class(code).new(httpv, code, msg)
- each_response_header(sock) do |k,v|
- res.add_field k, v
- end
- res
- end
-
- private
-
- def read_status_line(sock)
- str = sock.readline
- m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in.match(str) or
- raise HTTPBadResponse, "wrong status line: #{str.dump}"
- m.captures
- end
-
- def response_class(code)
- CODE_TO_OBJ[code] or
- CODE_CLASS_TO_OBJ[code[0,1]] or
- HTTPUnknownResponse
- end
-
- def each_response_header(sock)
- while true
- line = sock.readuntil("\n", true).sub(/\s+\z/, '')
- break if line.empty?
- m = /\A([^:]+):\s*/.match(line) or
- raise HTTPBadResponse, 'wrong header line format'
- yield m[1], m.post_match
- end
- end
- end
+require_relative 'http/exceptions'
- # next is to fix bug in RDoc, where the private inside class << self
- # spills out.
- public
+require_relative 'http/header'
- include HTTPHeader
+require_relative 'http/generic_request'
+require_relative 'http/request'
+require_relative 'http/requests'
- 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..9e24eae32c
--- /dev/null
+++ b/lib/net/http/backward.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: false
+# for backward compatibility
+
+# :enddoc:
+
+class Net::HTTP
+ ProxyMod = ProxyDelta
+end
+
+module Net
+ HTTPSession = Net::HTTP
+end
+
+module Net::NetPrivate
+ HTTPRequest = ::Net::HTTPRequest
+end
+
+Net::HTTPInformationCode = Net::HTTPInformation
+Net::HTTPSuccessCode = Net::HTTPSuccess
+Net::HTTPRedirectionCode = Net::HTTPRedirection
+Net::HTTPRetriableCode = Net::HTTPRedirection
+Net::HTTPClientErrorCode = Net::HTTPClientError
+Net::HTTPFatalErrorCode = Net::HTTPClientError
+Net::HTTPServerErrorCode = Net::HTTPServerError
+Net::HTTPResponceReceiver = Net::HTTPResponse
+
diff --git a/lib/net/http/exceptions.rb b/lib/net/http/exceptions.rb
new file mode 100644
index 0000000000..0d34526616
--- /dev/null
+++ b/lib/net/http/exceptions.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: false
+# Net::HTTP exception class.
+# You cannot use Net::HTTPExceptions directly; instead, you must use
+# its subclasses.
+module Net::HTTPExceptions
+ def initialize(msg, res) #:nodoc:
+ super msg
+ @response = res
+ end
+ attr_reader :response
+ alias data response #:nodoc: obsolete
+end
+class Net::HTTPError < Net::ProtocolError
+ include Net::HTTPExceptions
+end
+class Net::HTTPRetriableError < Net::ProtoRetriableError
+ include Net::HTTPExceptions
+end
+class Net::HTTPServerException < Net::ProtoServerError
+ # We cannot use the name "HTTPServerError", it is the name of the response.
+ include Net::HTTPExceptions
+end
+class Net::HTTPFatalError < Net::ProtoFatalError
+ include Net::HTTPExceptions
+end
+
diff --git a/lib/net/http/generic_request.rb b/lib/net/http/generic_request.rb
new file mode 100644
index 0000000000..526cc333fc
--- /dev/null
+++ b/lib/net/http/generic_request.rb
@@ -0,0 +1,338 @@
+# frozen_string_literal: false
+# HTTPGenericRequest is the parent of the HTTPRequest class.
+# Do not use this directly; use a subclass of HTTPRequest.
+#
+# Mixes in the 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
+ @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 @response_has_body and Net::HTTP::HAVE_ZLIB then
+ if !initheader ||
+ !initheader.keys.any? { |k|
+ %w[accept-encoding range].include? k.downcase
+ } then
+ @decode_content = true
+ 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!(/:.*/s, ''.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
+ @sock.write("#{buf.bytesize.to_s(16)}\r\n")
+ rv = @sock.write(buf)
+ @sock.write("\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
+ # copy_stream can sendfile() to sock.io unless we use SSL.
+ # If sock.io is an SSLSocket, copy_stream will hit SSL_write()
+ IO.copy_stream(f, sock.io)
+ 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..96d898c89f
--- /dev/null
+++ b/lib/net/http/header.rb
@@ -0,0 +1,494 @@
+# 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: 1 if key?(key) and $VERBOSE
+ if value.nil?
+ warn "net/http: nil HTTP header: #{key}", uplevel: 1 if $VERBOSE
+ else
+ value = value.strip # raise error for invalid byte sequences
+ if value.count("\r\n") > 0
+ raise ArgumentError, 'header field value cannot include CR/LF'
+ end
+ @header[key.downcase] = [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] 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
+ 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)
+ if @header.key?(key.downcase)
+ append_field_value(@header[key.downcase], 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] = 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] = [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)
+ 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
+ # raises an IndexError if there's no header field named +key+
+ # See Hash#fetch
+ def fetch(key, *args, &block) #:yield: +key+
+ a = @header.fetch(key.downcase, *args, &block)
+ a.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)
+ end
+
+ # true if +key+ header exists.
+ def key?(key)
+ @header.key?(key.downcase)
+ 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
+ # 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<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or
+ raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
+ m[1].to_i .. m[2].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+ is the form data set; it is an Array of Arrays or a Hash
+ # +enctype is the type to encode the form data set.
+ # It is application/x-www-form-urlencoded or multipart/form-data.
+ # +formopt+ is an optional hash to specify the detail.
+ #
+ # boundary:: the boundary of the multipart message
+ # charset:: the charset of the message. All names and the values of
+ # non-file fields are encoded as the charset.
+ #
+ # Each item of params is an array and contains following items:
+ # +name+:: the name of the field
+ # +value+:: the value of the field, it should be a String or a File
+ # +opt+:: an optional hash to specify additional information
+ #
+ # Each item is a file field or a normal field.
+ # If +value+ is a File object or the +opt+ have a filename key,
+ # the item is treated as a file field.
+ #
+ # If Transfer-Encoding is set as chunked, this send the request in
+ # chunked encoding. Because chunked encoding is HTTP/1.1 feature,
+ # you must confirm the server to support HTTP/1.1 before sending it.
+ #
+ # Example:
+ # http.set_form([["q", "ruby"], ["lang", "en"]])
+ #
+ # 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/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..6a78272ac8
--- /dev/null
+++ b/lib/net/http/response.rb
@@ -0,0 +1,419 @@
+# 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. These are listed below.
+#
+# All classes are defined under the Net module. Indentation indicates
+# inheritance. For a list of the classes see Net::HTTP.
+#
+#
+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
+ 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
+
+ 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.
+ #
+ # 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
+
+ @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
+
+ ##
+ # 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
+ ensure
+ orig_err = $!
+ begin
+ inflate_body_io.finish
+ rescue => err
+ raise orig_err || err
+ 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, true # 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
+
+ ##
+ # 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|
+ 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..c4259e1a02
--- /dev/null
+++ b/lib/net/http/responses.rb
@@ -0,0 +1,299 @@
+# frozen_string_literal: false
+# :stopdoc:
+# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+class Net::HTTPUnknownResponse < Net::HTTPResponse
+ HAS_BODY = true
+ EXCEPTION_TYPE = Net::HTTPError
+end
+class Net::HTTPInformation < Net::HTTPResponse # 1xx
+ HAS_BODY = false
+ EXCEPTION_TYPE = Net::HTTPError
+end
+class Net::HTTPSuccess < Net::HTTPResponse # 2xx
+ HAS_BODY = true
+ EXCEPTION_TYPE = Net::HTTPError
+end
+class Net::HTTPRedirection < Net::HTTPResponse # 3xx
+ HAS_BODY = true
+ EXCEPTION_TYPE = Net::HTTPRetriableError
+end
+class Net::HTTPClientError < Net::HTTPResponse # 4xx
+ HAS_BODY = true
+ EXCEPTION_TYPE = Net::HTTPServerException # for backward compatibility
+end
+class Net::HTTPServerError < Net::HTTPResponse # 5xx
+ HAS_BODY = true
+ EXCEPTION_TYPE = Net::HTTPFatalError # for backward compatibility
+end
+
+class Net::HTTPContinue < Net::HTTPInformation # 100
+ HAS_BODY = false
+end
+class Net::HTTPSwitchProtocol < Net::HTTPInformation # 101
+ HAS_BODY = false
+end
+class Net::HTTPProcessing < Net::HTTPInformation # 102
+ HAS_BODY = false
+end
+
+class Net::HTTPOK < Net::HTTPSuccess # 200
+ HAS_BODY = true
+end
+class Net::HTTPCreated < Net::HTTPSuccess # 201
+ HAS_BODY = true
+end
+class Net::HTTPAccepted < Net::HTTPSuccess # 202
+ HAS_BODY = true
+end
+class Net::HTTPNonAuthoritativeInformation < Net::HTTPSuccess # 203
+ HAS_BODY = true
+end
+class Net::HTTPNoContent < Net::HTTPSuccess # 204
+ HAS_BODY = false
+end
+class Net::HTTPResetContent < Net::HTTPSuccess # 205
+ HAS_BODY = false
+end
+class Net::HTTPPartialContent < Net::HTTPSuccess # 206
+ HAS_BODY = true
+end
+class Net::HTTPMultiStatus < Net::HTTPSuccess # 207 - RFC 4918
+ HAS_BODY = true
+end
+class Net::HTTPAlreadyReported < Net::HTTPSuccess # 208 - RFC 5842
+ HAS_BODY = true
+end
+class Net::HTTPIMUsed < Net::HTTPSuccess # 226 - RFC 3229
+ HAS_BODY = true
+end
+
+class Net::HTTPMultipleChoices < Net::HTTPRedirection # 300
+ HAS_BODY = true
+end
+Net::HTTPMultipleChoice = Net::HTTPMultipleChoices
+class Net::HTTPMovedPermanently < Net::HTTPRedirection # 301
+ HAS_BODY = true
+end
+class Net::HTTPFound < Net::HTTPRedirection # 302
+ HAS_BODY = true
+end
+Net::HTTPMovedTemporarily = Net::HTTPFound
+class Net::HTTPSeeOther < Net::HTTPRedirection # 303
+ HAS_BODY = true
+end
+class Net::HTTPNotModified < Net::HTTPRedirection # 304
+ HAS_BODY = false
+end
+class Net::HTTPUseProxy < Net::HTTPRedirection # 305
+ HAS_BODY = false
+end
+# 306 Switch Proxy - no longer unused
+class Net::HTTPTemporaryRedirect < Net::HTTPRedirection # 307
+ HAS_BODY = true
+end
+class Net::HTTPPermanentRedirect < Net::HTTPRedirection # 308
+ HAS_BODY = true
+end
+
+class Net::HTTPBadRequest < Net::HTTPClientError # 400
+ HAS_BODY = true
+end
+class Net::HTTPUnauthorized < Net::HTTPClientError # 401
+ HAS_BODY = true
+end
+class Net::HTTPPaymentRequired < Net::HTTPClientError # 402
+ HAS_BODY = true
+end
+class Net::HTTPForbidden < Net::HTTPClientError # 403
+ HAS_BODY = true
+end
+class Net::HTTPNotFound < Net::HTTPClientError # 404
+ HAS_BODY = true
+end
+class Net::HTTPMethodNotAllowed < Net::HTTPClientError # 405
+ HAS_BODY = true
+end
+class Net::HTTPNotAcceptable < Net::HTTPClientError # 406
+ HAS_BODY = true
+end
+class Net::HTTPProxyAuthenticationRequired < Net::HTTPClientError # 407
+ HAS_BODY = true
+end
+class Net::HTTPRequestTimeOut < Net::HTTPClientError # 408
+ HAS_BODY = true
+end
+class Net::HTTPConflict < Net::HTTPClientError # 409
+ HAS_BODY = true
+end
+class Net::HTTPGone < Net::HTTPClientError # 410
+ HAS_BODY = true
+end
+class Net::HTTPLengthRequired < Net::HTTPClientError # 411
+ HAS_BODY = true
+end
+class Net::HTTPPreconditionFailed < Net::HTTPClientError # 412
+ HAS_BODY = true
+end
+class Net::HTTPRequestEntityTooLarge < Net::HTTPClientError # 413
+ HAS_BODY = true
+end
+class Net::HTTPRequestURITooLong < Net::HTTPClientError # 414
+ HAS_BODY = true
+end
+Net::HTTPRequestURITooLarge = Net::HTTPRequestURITooLong
+class Net::HTTPUnsupportedMediaType < Net::HTTPClientError # 415
+ HAS_BODY = true
+end
+class Net::HTTPRequestedRangeNotSatisfiable < Net::HTTPClientError # 416
+ HAS_BODY = true
+end
+class Net::HTTPExpectationFailed < Net::HTTPClientError # 417
+ HAS_BODY = true
+end
+# 418 I'm a teapot - RFC 2324; a joke RFC
+# 420 Enhance Your Calm - Twitter
+class Net::HTTPMisdirectedRequest < Net::HTTPClientError # 421 - RFC 7540
+ HAS_BODY = true
+end
+class Net::HTTPUnprocessableEntity < Net::HTTPClientError # 422 - RFC 4918
+ HAS_BODY = true
+end
+class Net::HTTPLocked < Net::HTTPClientError # 423 - RFC 4918
+ HAS_BODY = true
+end
+class Net::HTTPFailedDependency < Net::HTTPClientError # 424 - RFC 4918
+ HAS_BODY = true
+end
+# 425 Unordered Collection - existed only in draft
+class Net::HTTPUpgradeRequired < Net::HTTPClientError # 426 - RFC 2817
+ HAS_BODY = true
+end
+class Net::HTTPPreconditionRequired < Net::HTTPClientError # 428 - RFC 6585
+ HAS_BODY = true
+end
+class Net::HTTPTooManyRequests < Net::HTTPClientError # 429 - RFC 6585
+ HAS_BODY = true
+end
+class Net::HTTPRequestHeaderFieldsTooLarge < Net::HTTPClientError # 431 - RFC 6585
+ HAS_BODY = true
+end
+class Net::HTTPUnavailableForLegalReasons < Net::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 Net::HTTPInternalServerError < Net::HTTPServerError # 500
+ HAS_BODY = true
+end
+class Net::HTTPNotImplemented < Net::HTTPServerError # 501
+ HAS_BODY = true
+end
+class Net::HTTPBadGateway < Net::HTTPServerError # 502
+ HAS_BODY = true
+end
+class Net::HTTPServiceUnavailable < Net::HTTPServerError # 503
+ HAS_BODY = true
+end
+class Net::HTTPGatewayTimeOut < Net::HTTPServerError # 504
+ HAS_BODY = true
+end
+class Net::HTTPVersionNotSupported < Net::HTTPServerError # 505
+ HAS_BODY = true
+end
+class Net::HTTPVariantAlsoNegotiates < Net::HTTPServerError # 506
+ HAS_BODY = true
+end
+class Net::HTTPInsufficientStorage < Net::HTTPServerError # 507 - RFC 4918
+ HAS_BODY = true
+end
+class Net::HTTPLoopDetected < Net::HTTPServerError # 508 - RFC 5842
+ HAS_BODY = true
+end
+# 509 Bandwidth Limit Exceeded - Apache bw/limited extension
+class Net::HTTPNotExtended < Net::HTTPServerError # 510 - RFC 2774
+ HAS_BODY = true
+end
+class Net::HTTPNetworkAuthenticationRequired < Net::HTTPServerError # 511 - RFC 6585
+ HAS_BODY = true
+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,
+
+ '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::HTTPRequestEntityTooLarge,
+ '414' => Net::HTTPRequestURITooLong,
+ '415' => Net::HTTPUnsupportedMediaType,
+ '416' => Net::HTTPRequestedRangeNotSatisfiable,
+ '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
+
+# :startdoc:
+
diff --git a/lib/net/http/status.rb b/lib/net/http/status.rb
new file mode 100644
index 0000000000..c7a4c0cee3
--- /dev/null
+++ b/lib/net/http/status.rb
@@ -0,0 +1,83 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'net/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',
+ 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 1111e94590..58cb6ddf19 100644
--- a/lib/net/https.rb
+++ b/lib/net/https.rb
@@ -1,6 +1,12 @@
+# frozen_string_literal: false
=begin
-= $RCSfile: https.rb,v $ -- 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: https.rb,v 1.3.4.2 2006/02/05 09:56:34 aamine Exp $
-
- 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 '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
index 57e78ec135..da7d0d555c 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
#
# = net/imap.rb
#
@@ -9,13 +10,15 @@
# Documentation: Shugo Maeda, with RDoc conversion and overview by William
# Webber.
#
-# See Net::IMAP for documentation.
+# See Net::IMAP for documentation.
#
require "socket"
require "monitor"
require "digest/md5"
+require "strscan"
+require 'net/protocol'
begin
require "openssl"
rescue LoadError
@@ -36,7 +39,7 @@ module Net
# 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
+ # will frequently be implemented as files in mailbox format
# within a hierarchy of directories.
#
# To work on the messages within a mailbox, the client must
@@ -44,25 +47,25 @@ module Net
# 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.
+ # related commands implicitly operate.
#
# Messages have two sorts of identifiers: message sequence
- # numbers, and UIDs.
+ # 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 sequence numbers number messages within a mailbox
+ # from 1 up to the number of items in the mailbox. If a new
# message arrives during a session, it receives a sequence
- # number equal to the new size of the mail box. If messages
+ # number equal to the new size of the mailbox. If messages
# are expunged from the mailbox, remaining messages have their
# sequence numbers "shuffled down" to fill the gaps.
#
# UIDs, on the other hand, are permanently guaranteed not to
- # identify another message within the same mailbox, even if
+ # 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
+ # UIDs have to be reassigned. An IMAP client thus cannot
# rearrange message orders.
#
# == Examples of Usage
@@ -90,11 +93,11 @@ module Net
# 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")
@@ -102,7 +105,7 @@ module Net
# 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
@@ -112,9 +115,9 @@ module Net
#
# 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.
+ # the selected mailbox does not exist; etc.
#
- # BAD:: the request from the client does not follow the server's
+ # 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
@@ -124,7 +127,7 @@ module Net
# 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,
+ # to accept your connection. As a response to any other command,
# it indicates either that the server is shutting down, or that
# the server is timing out the client connection due to inactivity.
#
@@ -146,8 +149,8 @@ module Net
#
# 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.
+ # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is
+ # thrown if a server response is non-parseable.
#
#
# == References
@@ -197,9 +200,9 @@ module Net
# Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of
# Unicode", RFC 2152, May 1997.
#
- class IMAP
+ class IMAP < Protocol
include MonitorMixin
- if defined?(OpenSSL)
+ if defined?(OpenSSL::SSL)
include OpenSSL
include SSL
end
@@ -219,17 +222,22 @@ module Net
# Returns all response handlers.
attr_reader :response_handlers
+ # Seconds to wait until a connection is opened.
+ # If the IMAP object cannot open a connection within this time,
+ # it raises a Net::OpenTimeout exception. The default value is 30 seconds.
+ attr_reader :open_timeout
+
# The thread to receive exceptions.
attr_accessor :client_thread
- # Flag indicating a message has been seen
+ # Flag indicating a message has been seen.
SEEN = :Seen
- # Flag indicating a message has been answered
+ # Flag indicating a message has been answered.
ANSWERED = :Answered
# Flag indicating a message has been flagged for special or urgent
- # attention
+ # attention.
FLAGGED = :Flagged
# Flag indicating a message has been marked for deletion. This
@@ -239,7 +247,7 @@ module Net
# 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
+ # 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
@@ -269,12 +277,24 @@ module Net
return @@debug = val
end
+ # Returns the max number of flags interned to symbols.
+ def self.max_flag_count
+ return @@max_flag_count
+ end
+
+ # Sets the max number of flags interned to symbols.
+ def self.max_flag_count=(count)
+ @@max_flag_count = count
+ end
+
# Adds an authenticator for Net::IMAP#authenticate. +auth_type+
# is the type of authentication this authenticator supports
# (for instance, "LOGIN"). The +authenticator+ is an object
# which defines a process() method to handle authentication with
- # the server. See Net::IMAP::LoginAuthenticator and
- # Net::IMAP::CramMD5Authenticator for examples.
+ # the server. See Net::IMAP::LoginAuthenticator,
+ # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
+ # for examples.
+ #
#
# If +auth_type+ refers to an existing authenticator, it will be
# replaced by the new one.
@@ -282,15 +302,43 @@ module Net
@@authenticators[auth_type] = authenticator
end
+ # The default port for IMAP connections, port 143
+ def self.default_port
+ return PORT
+ end
+
+ # The default port for IMAPS connections, port 993
+ def self.default_tls_port
+ return SSL_PORT
+ end
+
+ class << self
+ alias default_imap_port default_port
+ alias default_imaps_port default_tls_port
+ alias default_ssl_port default_tls_port
+ end
+
# Disconnects from the server.
def disconnect
- if SSL::SSLSocket === @sock
- @sock.io.shutdown
- else
- @sock.shutdown
+ return if disconnected?
+ begin
+ begin
+ # try to call SSL::SSLSocket#io.
+ @sock.io.shutdown
+ rescue NoMethodError
+ # @sock is not an SSL::SSLSocket.
+ @sock.shutdown
+ end
+ rescue Errno::ENOTCONN
+ # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
+ rescue Exception => e
+ @receiver_thread.raise(e)
end
@receiver_thread.join
- @sock.close
+ synchronize do
+ @sock.close
+ end
+ raise e if e
end
# Returns true if disconnected from the server.
@@ -305,7 +353,7 @@ module Net
#
# 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
+ # 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
@@ -326,19 +374,34 @@ module Net
send_command("LOGOUT")
end
+ # Sends a STARTTLS command to start TLS session.
+ def starttls(options = {}, verify = true)
+ send_command("STARTTLS") do |resp|
+ if resp.kind_of?(TaggedResponse) && resp.name == "OK"
+ begin
+ # for backward compatibility
+ certs = options.to_str
+ options = create_ssl_params(certs, verify)
+ rescue NoMethodError
+ end
+ start_tls_session(options)
+ end
+ end
+ end
+
# Sends an AUTHENTICATE command to authenticate the client.
# The +auth_type+ parameter is a string that represents
# the authentication mechanism to be used. Currently Net::IMAP
- # supports authentication mechanisms:
+ # supports the authentication mechanisms:
#
- # LOGIN:: login using cleartext user and password.
+ # 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
+ # For both of these mechanisms, there should be two +args+: username
+ # and (cleartext) password. A server may not support one or the other
# of these mechanisms; check #capability() for a capability of
# the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
#
@@ -361,7 +424,7 @@ module Net
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/, "")
+ s = [data].pack("m0")
send_string_data(s)
put_string(CRLF)
end
@@ -379,7 +442,7 @@ module Net
end
# Sends a SELECT command to select a +mailbox+ so that messages
- # in the +mailbox+ can be accessed.
+ # 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],
@@ -430,7 +493,7 @@ module Net
# Sends a RENAME command to change the name of the +mailbox+ to
# +newname+.
#
- # A Net::IMAP::NoResponseError is raised if a mailbox with the
+ # 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+.
@@ -443,7 +506,7 @@ module Net
# by #lsub().
#
# A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
- # subscribed to, for instance because it does not exist.
+ # subscribed to; for instance, because it does not exist.
def subscribe(mailbox)
send_command("SUBSCRIBE", mailbox)
end
@@ -452,7 +515,7 @@ module Net
# 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
+ # unsubscribed from; for instance, because the client is not currently
# subscribed to it.
def unsubscribe(mailbox)
send_command("UNSUBSCRIBE", mailbox)
@@ -477,8 +540,8 @@ module Net
# 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=[: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
@@ -487,9 +550,41 @@ module Net
end
end
- # Sends the GETQUOTAROOT command along with specified +mailbox+.
+ # Sends a XLIST command, and returns a subset of names from
+ # the complete set of all names available to the client.
+ # +refname+ provides a context (for instance, a base directory
+ # in a directory-based mailbox hierarchy). +mailbox+ specifies
+ # a mailbox or (via wildcards) mailboxes under that context.
+ # Two wildcards may be used in +mailbox+: '*', which matches
+ # all characters *including* the hierarchy delimiter (for instance,
+ # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%',
+ # which matches all characters *except* the hierarchy delimiter.
+ #
+ # If +refname+ is empty, +mailbox+ is used directly to determine
+ # which mailboxes to match. If +mailbox+ is empty, the root
+ # name of +refname+ and the hierarchy delimiter are returned.
+ #
+ # The XLIST command is like the LIST command except that the flags
+ # returned refer to the function of the folder/mailbox, e.g. :Sent
+ #
+ # The return value is an array of +Net::IMAP::MailboxList+. For example:
+ #
+ # imap.create("foo/bar")
+ # imap.create("foo/baz")
+ # p imap.xlist("", "foo/%")
+ # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\
+ # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\
+ # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
+ def xlist(refname, mailbox)
+ synchronize do
+ send_command("XLIST", refname, mailbox)
+ return @responses.delete("XLIST")
+ end
+ end
+
+ # Sends the GETQUOTAROOT command along with the specified +mailbox+.
# This command is generally available to both admin and user.
- # If mailbox exists, returns an array containing objects of
+ # If this mailbox exists, it returns an array containing objects of type
# Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota.
def getquotaroot(mailbox)
synchronize do
@@ -504,7 +599,7 @@ module Net
# 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.
+ # command is generally only available to server admin.
def getquota(mailbox)
synchronize do
send_command("GETQUOTA", mailbox)
@@ -513,8 +608,8 @@ module Net
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
+ # +quota+. If +quota+ is nil, then +quota+ will be unset for that
+ # mailbox. Typically one needs to be logged in as a server admin
# for this to work. The IMAP quota commands are described in
# [RFC-2087].
def setquota(mailbox, quota)
@@ -531,14 +626,14 @@ module Net
# 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?
+ 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+.
+ # Send the GETACL command along with a specified +mailbox+.
# If this mailbox exists, an array containing objects of
# Net::IMAP::MailboxACLItem will be returned.
def getacl(mailbox)
@@ -550,7 +645,7 @@ module Net
# 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
+ # "subscribed." +refname+ and +mailbox+ are interpreted as
# for #list().
# The return value is an array of +Net::IMAP::MailboxList+.
def lsub(refname, mailbox)
@@ -561,8 +656,8 @@ module Net
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:
+ # +mailbox+. +attr+ is a list of one or more attributes whose
+ # statuses are to be requested. Supported attributes include:
#
# MESSAGES:: the number of messages in the mailbox.
# RECENT:: the number of recent messages in the mailbox.
@@ -573,8 +668,8 @@ module Net
# 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
+ # 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
@@ -584,9 +679,9 @@ module Net
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
+ # the +mailbox+. The optional +flags+ argument is an array of
+ # flags initially passed to the new message. The optional
+ # +date_time+ argument specifies the creation time to assign to the
# new message; it defaults to the current time.
# For example:
#
@@ -594,7 +689,7 @@ module Net
# Subject: hello
# From: shugo@ruby-lang.org
# To: shugo@ruby-lang.org
- #
+ #
# hello world
# EOF
#
@@ -613,7 +708,7 @@ module Net
# Sends a CHECK command to request a checkpoint of the currently
# selected mailbox. This performs implementation-specific
- # housekeeping, for instance, reconciling the mailbox's
+ # housekeeping; for instance, reconciling the mailbox's
# in-memory and on-disk state.
def check
send_command("CHECK")
@@ -637,8 +732,8 @@ module Net
# 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
+ # 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.
#
@@ -662,7 +757,7 @@ module Net
#
# OR <search-key> <search-key>:: "or" two search keys together.
#
- # ON <date>:: messages with an internal date exactly equal to <date>,
+ # 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>.
@@ -670,7 +765,7 @@ module Net
# 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"])
@@ -679,22 +774,34 @@ module Net
return search_internal("SEARCH", keys, charset)
end
- # As for #search(), but returns unique identifiers.
+ # Similar to #search(), but returns unique identifiers.
def uid_search(keys, charset = nil)
return search_internal("UID SEARCH", keys, charset)
end
# Sends a FETCH command to retrieve data associated with a message
- # in the mailbox. The +set+ parameter is a number or 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:
+ # in the mailbox.
+ #
+ # The +set+ parameter is a number or a range between two numbers,
+ # or an array of those. The number is a message sequence number,
+ # where -1 represents a '*' for use in range notation like 100..-1
+ # being interpreted as '100:*'. Beware that the +exclude_end?+
+ # property of a Range object is ignored, and the contents of a
+ # range are independent of the order of the range endpoints as per
+ # the protocol specification, so 1...5, 5..1 and 5...1 are all
+ # equivalent to 1..5.
+ #
+ # +attr+ is a list of attributes to fetch; see the documentation
+ # for Net::IMAP::FetchData for a list of valid attributes.
+ #
+ # The return value is an array of Net::IMAP::FetchData or nil
+ # (instead of an empty array) if there is no matching message.
+ #
+ # For example:
#
# p imap.fetch(6..8, "UID")
- # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\
- # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\
+ # #=> [#<Net::IMAP::FetchData seqno=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"}>]
@@ -707,51 +814,65 @@ module Net
# #=> "12-Oct-2000 22:40:59 +0900"
# p data.attr["UID"]
# #=> 98
- def fetch(set, attr)
- return fetch_internal("FETCH", set, attr)
+ def fetch(set, attr, mod = nil)
+ return fetch_internal("FETCH", set, attr, mod)
end
- # As for #fetch(), but +set+ contains unique identifiers.
- def uid_fetch(set, attr)
- return fetch_internal("UID FETCH", set, attr)
+ # Similar to #fetch(), but +set+ contains unique identifiers.
+ def uid_fetch(set, attr, mod = nil)
+ return fetch_internal("UID FETCH", set, attr, mod)
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.
+ # in the mailbox, in particular their flags. The +set+ parameter
+ # is a number, an array of numbers, or a Range object. Each number
+ # is a message sequence number. +attr+ is the name of a data item
+ # to store: 'FLAGS' will replace the message's flag list
+ # with the provided one, '+FLAGS' will add the provided flags,
+ # and '-FLAGS' will remove them. +flags+ is a list of flags.
#
# The return value is an array of Net::IMAP::FetchData. For example:
#
# p imap.store(6..8, "+FLAGS", [:Deleted])
- # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
- # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
+ # #=> [#<Net::IMAP::FetchData seqno=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.
+ # Similar to #store(), but +set+ contains unique identifiers.
def uid_store(set, attr, flags)
return store_internal("UID STORE", set, attr, flags)
end
# Sends a COPY command to copy the specified message(s) to the end
# of the specified destination +mailbox+. The +set+ parameter is
- # a number or an array of numbers or a Range object. The number is
+ # a number, an array of numbers, or a Range object. The number is
# a message sequence number.
def copy(set, mailbox)
copy_internal("COPY", set, mailbox)
end
- # As for #copy(), but +set+ contains unique identifiers.
+ # Similar to #copy(), but +set+ contains unique identifiers.
def uid_copy(set, mailbox)
copy_internal("UID COPY", set, mailbox)
end
+ # Sends a MOVE command to move the specified message(s) to the end
+ # of the specified destination +mailbox+. The +set+ parameter is
+ # a number, an array of numbers, or a Range object. The number is
+ # a message sequence number.
+ # The IMAP MOVE extension is described in [RFC-6851].
+ def move(set, mailbox)
+ copy_internal("MOVE", set, mailbox)
+ end
+
+ # Similar to #move(), but +set+ contains unique identifiers.
+ def uid_move(set, mailbox)
+ copy_internal("UID MOVE", set, mailbox)
+ end
+
# Sends a SORT command to sort messages in the mailbox.
# Returns an array of message sequence numbers. For example:
#
@@ -765,16 +886,16 @@ module Net
return sort_internal("SORT", sort_keys, search_keys, charset)
end
- # As for #sort(), but returns an array of unique identifiers.
+ # Similar to #sort(), but returns an array of unique identifiers.
def uid_sort(sort_keys, search_keys, charset)
return sort_internal("UID SORT", sort_keys, search_keys, charset)
end
- # Adds a response handler. For example, to detect when
- # the server sends 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.
+ # Adds a response handler. For example, to detect when
+ # the server sends a new EXISTS response (which normally
+ # indicates new messages being added to the mailbox),
+ # add the following handler after selecting the
+ # mailbox:
#
# imap.add_response_handler { |resp|
# if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS"
@@ -791,7 +912,7 @@ module Net
@response_handlers.delete(handler)
end
- # As for #search(), but returns message sequence numbers in threaded
+ # Similar to #search(), but returns message sequence numbers in threaded
# format, as a Net::IMAP::ThreadMember tree. The supported algorithms
# are:
#
@@ -808,12 +929,65 @@ module Net
return thread_internal("THREAD", algorithm, search_keys, charset)
end
- # As for #thread(), but returns unique identifiers instead of
+ # Similar to #thread(), but returns unique identifiers instead of
# message sequence numbers.
def uid_thread(algorithm, search_keys, charset)
return thread_internal("UID THREAD", algorithm, search_keys, charset)
end
+ # Sends an IDLE command that waits for notifications of new or expunged
+ # messages. Yields responses from the server during the IDLE.
+ #
+ # Use #idle_done() to leave IDLE.
+ #
+ # If +timeout+ is given, this method returns after +timeout+ seconds passed.
+ # +timeout+ can be used for keep-alive. For example, the following code
+ # checks the connection for each 60 seconds.
+ #
+ # loop do
+ # imap.idle(60) do |res|
+ # ...
+ # end
+ # end
+ def idle(timeout = nil, &response_handler)
+ raise LocalJumpError, "no block given" unless response_handler
+
+ response = nil
+
+ synchronize do
+ tag = Thread.current[:net_imap_tag] = generate_tag
+ put_string("#{tag} IDLE#{CRLF}")
+
+ begin
+ add_response_handler(response_handler)
+ @idle_done_cond = new_cond
+ @idle_done_cond.wait(timeout)
+ @idle_done_cond = nil
+ if @receiver_thread_terminating
+ raise @exception || Net::IMAP::Error.new("connection closed")
+ end
+ ensure
+ unless @receiver_thread_terminating
+ remove_response_handler(response_handler)
+ put_string("DONE#{CRLF}")
+ response = get_tagged_response(tag, "IDLE")
+ end
+ end
+ end
+
+ return response
+ end
+
+ # Leaves IDLE.
+ def idle_done
+ synchronize do
+ if @idle_done_cond.nil?
+ raise Net::IMAP::Error, "not during IDLE"
+ end
+ @idle_done_cond.signal
+ end
+ end
+
# Decode a string from modified UTF-7 format to UTF-8.
#
# UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
@@ -821,127 +995,180 @@ module Net
# containing non-ASCII characters; see [IMAP] section 5.1.3.
#
# Net::IMAP does _not_ automatically encode and decode
- # mailbox names to and from utf7.
+ # mailbox names to and from UTF-7.
def self.decode_utf7(s)
- return s.gsub(/&(.*?)-/n) {
- if $1.empty?
- "&"
+ return s.gsub(/&([^-]+)?-/n) {
+ if $1
+ ($1.tr(",", "/") + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE)
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|
+ return s.gsub(/(&)|[^\x20-\x7e]+/) {
if $1
"&-"
else
- base64 = [u8tou16(x)].pack("m")
- "&" + base64.delete("=\n").tr("/", ",") + "-"
+ base64 = [$&.encode(Encoding::UTF_16BE)].pack("m0")
+ "&" + base64.delete("=").tr("/", ",") + "-"
end
- }
+ }.force_encoding("ASCII-8BIT")
+ end
+
+ # Formats +time+ as an IMAP-style date.
+ def self.format_date(time)
+ return time.strftime('%d-%b-%Y')
+ end
+
+ # Formats +time+ as an IMAP-style date-time.
+ def self.format_datetime(time)
+ return time.strftime('%d-%b-%Y %H:%M %z')
end
private
CRLF = "\r\n" # :nodoc:
PORT = 143 # :nodoc:
+ SSL_PORT = 993 # :nodoc:
@@debug = false
@@authenticators = {}
+ @@max_flag_count = 10000
+ # :call-seq:
+ # Net::IMAP.new(host, options = {})
+ #
# Creates a new Net::IMAP object and connects it to the specified
- # +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.
+ # +host+.
+ #
+ # +options+ is an option hash, each key of which is a symbol.
+ #
+ # The available options are:
+ #
+ # port:: Port number (default value is 143 for imap, or 993 for imaps)
+ # ssl:: If options[:ssl] is true, then an attempt will be made
+ # to use SSL (now TLS) to connect to the server. For this to work
+ # OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to
+ # be installed.
+ # If options[:ssl] is a hash, it's passed to
+ # OpenSSL::SSL::SSLContext#set_params as parameters.
+ # open_timeout:: Seconds to wait until a connection is opened
#
# The most common errors are:
#
- # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening
+ # Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening
# firewall.
- # Errno::ETIMEDOUT:: connection timed out (possibly due to packets
+ # 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)
+ # Errno::ENETUNREACH:: There is no route to that network.
+ # SocketError:: Hostname not known or other socket error.
+ # Net::IMAP::ByeResponseError:: The connected to the host was successful, but
+ # it immediately said goodbye.
+ def initialize(host, port_or_options = {},
+ usessl = false, certs = nil, verify = true)
super()
@host = host
- @port = port
+ begin
+ options = port_or_options.to_hash
+ rescue NoMethodError
+ # for backward compatibility
+ options = {}
+ options[:port] = port_or_options
+ if usessl
+ options[:ssl] = create_ssl_params(certs, verify)
+ end
+ end
+ @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
@tag_prefix = "RUBY"
@tagno = 0
+ @open_timeout = options[:open_timeout] || 30
@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 = []
- @tagged_response_arrival = new_cond
- @continuation_request_arrival = new_cond
- @logout_command_tag = nil
- @debug_output_bol = true
-
- @greeting = get_response
- if @greeting.name == "BYE"
+ @sock = tcp_socket(@host, @port)
+ begin
+ if options[:ssl]
+ start_tls_session(options[:ssl])
+ @usessl = true
+ else
+ @usessl = false
+ end
+ @responses = Hash.new([].freeze)
+ @tagged_responses = {}
+ @response_handlers = []
+ @tagged_response_arrival = new_cond
+ @continued_command_tag = nil
+ @continuation_request_arrival = new_cond
+ @continuation_request_exception = nil
+ @idle_done_cond = nil
+ @logout_command_tag = nil
+ @debug_output_bol = true
+ @exception = nil
+
+ @greeting = get_response
+ if @greeting.nil?
+ raise Error, "connection closed"
+ end
+ if @greeting.name == "BYE"
+ raise ByeResponseError, @greeting
+ end
+
+ @client_thread = Thread.current
+ @receiver_thread = Thread.start {
+ begin
+ receive_responses
+ rescue Exception
+ end
+ }
+ @receiver_thread_terminating = false
+ rescue Exception
@sock.close
- raise ByeResponseError, @greeting.raw_data
+ raise
end
+ end
- @client_thread = Thread.current
- @receiver_thread = Thread.start {
- receive_responses
- }
+ def tcp_socket(host, port)
+ Socket.tcp(host, port, :connect_timeout => @open_timeout)
+ rescue Errno::ETIMEDOUT
+ raise Net::OpenTimeout, "Timeout to open TCP connection to " +
+ "#{host}:#{port} (exceeds #{@open_timeout} seconds)"
end
def receive_responses
- while true
+ connection_closed = false
+ until connection_closed
+ synchronize do
+ @exception = nil
+ end
begin
resp = get_response
- rescue Exception
- @sock.close
- @client_thread.raise($!)
+ rescue Exception => e
+ synchronize do
+ @sock.close
+ @exception = e
+ end
+ break
+ end
+ unless resp
+ synchronize do
+ @exception = EOFError.new("end of file reached")
+ end
break
end
- break unless resp
begin
synchronize do
case resp
when TaggedResponse
@tagged_responses[resp.tag] = resp
@tagged_response_arrival.broadcast
- if resp.tag == @logout_command_tag
+ case resp.tag
+ when @logout_command_tag
return
+ when @continued_command_tag
+ @continuation_request_exception =
+ RESPONSE_ERRORS[resp.name].new(resp)
+ @continuation_request_arrival.signal
end
when UntaggedResponse
record_response(resp.name, resp.data)
@@ -951,7 +1178,8 @@ module Net
end
if resp.name == "BYE" && @logout_command_tag.nil?
@sock.close
- raise ByeResponseError, resp.raw_data
+ @exception = ByeResponseError.new(resp)
+ connection_closed = true
end
when ContinuationRequest
@continuation_request_arrival.signal
@@ -960,29 +1188,42 @@ module Net
handler.call(resp)
end
end
- rescue Exception
- @client_thread.raise($!)
+ rescue Exception => e
+ @exception = e
+ synchronize do
+ @tagged_response_arrival.broadcast
+ @continuation_request_arrival.broadcast
+ end
+ end
+ end
+ synchronize do
+ @receiver_thread_terminating = true
+ @tagged_response_arrival.broadcast
+ @continuation_request_arrival.broadcast
+ if @idle_done_cond
+ @idle_done_cond.signal
end
end
end
def get_tagged_response(tag, cmd)
until @tagged_responses.key?(tag)
+ raise @exception if @exception
@tagged_response_arrival.wait
end
resp = @tagged_responses.delete(tag)
case resp.name
when /\A(?:NO)\z/ni
- raise NoResponseError, resp.data.text
+ raise NoResponseError, resp
when /\A(?:BAD)\z/ni
- raise BadResponseError, resp.data.text
+ raise BadResponseError, resp
else
return resp
end
end
def get_response
- buff = ""
+ buff = String.new
while true
s = @sock.gets(CRLF)
break unless s
@@ -1010,11 +1251,14 @@ module Net
def send_command(cmd, *args, &block)
synchronize do
+ args.each do |i|
+ validate_data(i)
+ end
tag = generate_tag
put_string(tag + " " + cmd)
args.each do |i|
put_string(" ")
- send_data(i)
+ send_data(i, tag)
end
put_string(CRLF)
if cmd == "LOGOUT"
@@ -1037,7 +1281,7 @@ module Net
@tagno += 1
return format("%s%04d", @tag_prefix, @tagno)
end
-
+
def put_string(str)
@sock.print(str)
if @@debug
@@ -1053,32 +1297,53 @@ module Net
end
end
- def send_data(data)
+ def validate_data(data)
+ case data
+ when nil
+ when String
+ when Integer
+ NumValidator.ensure_number(data)
+ when Array
+ if data[0] == 'CHANGEDSINCE'
+ NumValidator.ensure_mod_sequence_value(data[1])
+ else
+ data.each do |i|
+ validate_data(i)
+ end
+ end
+ when Time
+ when Symbol
+ else
+ data.validate
+ end
+ end
+
+ def send_data(data, tag = nil)
case data
when nil
put_string("NIL")
when String
- send_string_data(data)
+ send_string_data(data, tag)
when Integer
send_number_data(data)
when Array
- send_list_data(data)
+ send_list_data(data, tag)
when Time
send_time_data(data)
when Symbol
send_symbol_data(data)
else
- data.send_data(self)
+ data.send_data(self, tag)
end
end
- def send_string_data(str)
+ def send_string_data(str, tag = nil)
case str
when ""
put_string('""')
when /[\x80-\xff\r\n]/n
# literal
- send_literal(str)
+ send_literal(str, tag)
when /[(){ \x00-\x1f\x7f%*"\\]/n
# quoted string
send_quoted_string(str)
@@ -1086,25 +1351,33 @@ module Net
put_string(str)
end
end
-
+
def send_quoted_string(str)
put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
end
- def send_literal(str)
- put_string("{" + str.length.to_s + "}" + CRLF)
- @continuation_request_arrival.wait
- put_string(str)
+ def send_literal(str, tag = nil)
+ synchronize do
+ put_string("{" + str.bytesize.to_s + "}" + CRLF)
+ @continued_command_tag = tag
+ @continuation_request_exception = nil
+ begin
+ @continuation_request_arrival.wait
+ e = @continuation_request_exception || @exception
+ raise e if e
+ put_string(str)
+ ensure
+ @continued_command_tag = nil
+ @continuation_request_exception = nil
+ end
+ end
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)
+ def send_list_data(list, tag = nil)
put_string("(")
first = true
list.each do |i|
@@ -1113,7 +1386,7 @@ module Net
else
put_string(" ")
end
- send_data(i)
+ send_data(i, tag)
end
put_string(")")
end
@@ -1148,13 +1421,23 @@ module Net
end
end
- def fetch_internal(cmd, set, attr)
- if attr.instance_of?(String)
+ def fetch_internal(cmd, set, attr, mod = nil)
+ case attr
+ when String then
attr = RawData.new(attr)
+ when Array then
+ attr = attr.map { |arg|
+ arg.is_a?(String) ? RawData.new(arg) : arg
+ }
end
+
synchronize do
@responses.delete("FETCH")
- send_command(cmd, MessageSet.new(set), attr)
+ if mod
+ send_command(cmd, MessageSet.new(set), attr, mod)
+ else
+ send_command(cmd, MessageSet.new(set), attr)
+ end
return @responses.delete("FETCH")
end
end
@@ -1209,130 +1492,57 @@ module Net
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)
+ def create_ssl_params(certs = nil, verify = true)
+ params = {}
+ if certs
+ if File.file?(certs)
+ params[:ca_file] = certs
+ elsif File.directory?(certs)
+ params[:ca_path] = certs
end
end
- return buf
+ if verify
+ params[:verify_mode] = VERIFY_PEER
+ else
+ params[:verify_mode] = VERIFY_NONE
+ end
+ return params
+ end
+
+ def start_tls_session(params = {})
+ unless defined?(OpenSSL::SSL)
+ raise "SSL extension not installed"
+ end
+ if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
+ raise RuntimeError, "already using SSL"
+ end
+ begin
+ params = params.to_hash
+ rescue NoMethodError
+ params = {}
+ end
+ context = SSLContext.new
+ context.set_params(params)
+ if defined?(VerifyCallbackProc)
+ context.verify_callback = VerifyCallbackProc
+ end
+ @sock = SSLSocket.new(@sock, context)
+ @sock.sync_close = true
+ @sock.hostname = @host if @sock.respond_to? :hostname=
+ ssl_socket_connect(@sock, @open_timeout)
+ if context.verify_mode != VERIFY_NONE
+ @sock.post_connection_check(@host)
+ end
end
- private_class_method :u8tou16
class RawData # :nodoc:
- def send_data(imap)
+ def send_data(imap, tag)
imap.send(:put_string, @data)
end
+ def validate
+ end
+
private
def initialize(data)
@@ -1341,10 +1551,13 @@ module Net
end
class Atom # :nodoc:
- def send_data(imap)
+ def send_data(imap, tag)
imap.send(:put_string, @data)
end
+ def validate
+ end
+
private
def initialize(data)
@@ -1353,10 +1566,13 @@ module Net
end
class QuotedString # :nodoc:
- def send_data(imap)
+ def send_data(imap, tag)
imap.send(:send_quoted_string, @data)
end
+ def validate
+ end
+
private
def initialize(data)
@@ -1365,8 +1581,11 @@ module Net
end
class Literal # :nodoc:
- def send_data(imap)
- imap.send(:send_literal, @data)
+ def send_data(imap, tag)
+ imap.send(:send_literal, @data, tag)
+ end
+
+ def validate
end
private
@@ -1377,10 +1596,14 @@ module Net
end
class MessageSet # :nodoc:
- def send_data(imap)
+ def send_data(imap, tag)
imap.send(:put_string, format_internal(@data))
end
+ def validate
+ validate_internal(@data)
+ end
+
private
def initialize(data)
@@ -1392,7 +1615,6 @@ module Net
when "*"
return data
when Integer
- ensure_nz_number(data)
if data == -1
return "*"
else
@@ -1406,124 +1628,188 @@ module Net
when ThreadMember
return data.seqno.to_s +
":" + data.children.collect {|i| format_internal(i).join(",")}
+ end
+ end
+
+ def validate_internal(data)
+ case data
+ when "*"
+ when Integer
+ NumValidator.ensure_nz_number(data)
+ when Range
+ when Array
+ data.each do |i|
+ validate_internal(i)
+ end
+ when ThreadMember
+ data.children.each do |i|
+ validate_internal(i)
+ end
else
raise DataFormatError, data.inspect
end
end
+ end
+
+ # Common validators of number and nz_number types
+ module NumValidator # :nodoc
+ class << self
+ # Check is passed argument valid 'number' in RFC 3501 terminology
+ def valid_number?(num)
+ # [RFC 3501]
+ # number = 1*DIGIT
+ # ; Unsigned 32-bit integer
+ # ; (0 <= n < 4,294,967,296)
+ num >= 0 && num < 4294967296
+ end
+
+ # Check is passed argument valid 'nz_number' in RFC 3501 terminology
+ def valid_nz_number?(num)
+ # [RFC 3501]
+ # nz-number = digit-nz *DIGIT
+ # ; Non-zero unsigned 32-bit integer
+ # ; (0 < n < 4,294,967,296)
+ num != 0 && valid_number?(num)
+ end
- 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
+ # Check is passed argument valid 'mod_sequence_value' in RFC 4551 terminology
+ def valid_mod_sequence_value?(num)
+ # mod-sequence-value = 1*DIGIT
+ # ; Positive unsigned 64-bit integer
+ # ; (mod-sequence)
+ # ; (1 <= n < 18,446,744,073,709,551,615)
+ num >= 1 && num < 18446744073709551615
+ end
+
+ # Ensure argument is 'number' or raise DataFormatError
+ def ensure_number(num)
+ return if valid_number?(num)
+
+ msg = "number must be unsigned 32-bit integer: #{num}"
+ raise DataFormatError, msg
+ end
+
+ # Ensure argument is 'nz_number' or raise DataFormatError
+ def ensure_nz_number(num)
+ return if valid_nz_number?(num)
+
+ msg = "nz_number must be non-zero unsigned 32-bit integer: #{num}"
+ raise DataFormatError, msg
+ end
+
+ # Ensure argument is 'mod_sequence_value' or raise DataFormatError
+ def ensure_mod_sequence_value(num)
+ return if valid_mod_sequence_value?(num)
+
+ msg = "mod_sequence_value must be unsigned 64-bit integer: #{num}"
raise DataFormatError, msg
end
end
end
# Net::IMAP::ContinuationRequest represents command continuation requests.
- #
+ #
# The command continuation request response is indicated by a "+" token
# instead of a tag. This form of response indicates that the server is
# ready to accept the continuation of a command from the client. The
# remainder of this response is a line of text.
- #
+ #
# continue_req ::= "+" SPACE (resp_text / base64)
- #
+ #
# ==== Fields:
- #
+ #
# data:: Returns the data (Net::IMAP::ResponseText).
- #
+ #
# raw_data:: Returns the raw data string.
ContinuationRequest = Struct.new(:data, :raw_data)
# Net::IMAP::UntaggedResponse represents untagged responses.
- #
+ #
# Data transmitted by the server to the client and status responses
# that do not indicate command completion are prefixed with the token
# "*", and are called untagged responses.
- #
+ #
# response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye /
# mailbox_data / message_data / capability_data)
- #
+ #
# ==== Fields:
- #
- # name:: Returns the name such as "FLAGS", "LIST", "FETCH"....
- #
+ #
+ # name:: Returns the name, such as "FLAGS", "LIST", or "FETCH".
+ #
# data:: Returns the data such as an array of flag symbols,
- # a ((<Net::IMAP::MailboxList>)) object....
- #
+ # 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".
- #
+ #
+ # name:: Returns the name, one of "OK", "NO", or "BAD".
+ #
# data:: Returns the data. See ((<Net::IMAP::ResponseText>)).
- #
+ #
# raw_data:: Returns the raw data string.
#
TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
-
+
# Net::IMAP::ResponseText represents texts of responses.
# The text may be prefixed by the response code.
- #
+ #
# resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
# ;; text SHOULD NOT begin with "[" or "="
- #
+ #
# ==== Fields:
- #
+ #
# code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)).
- #
+ #
# text:: Returns the text.
- #
+ #
ResponseText = Struct.new(:code, :text)
- #
# Net::IMAP::ResponseCode represents response codes.
- #
+ #
# resp_text_code ::= "ALERT" / "PARSE" /
# "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
# "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
# "UIDVALIDITY" SPACE nz_number /
# "UNSEEN" SPACE nz_number /
# atom [SPACE 1*<any TEXT_CHAR except "]">]
- #
+ #
# ==== Fields:
- #
- # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
- #
- # data:: Returns the data if it exists.
+ #
+ # name:: Returns the name, such as "ALERT", "PERMANENTFLAGS", or "UIDVALIDITY".
+ #
+ # data:: Returns the data, if it exists.
#
ResponseCode = Struct.new(:name, :data)
# Net::IMAP::MailboxList represents contents of the LIST response.
- #
+ #
# mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
# "\Noselect" / "\Unmarked" / flag_extension) ")"
# SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
- #
+ #
# ==== Fields:
- #
+ #
# attr:: Returns the name attributes. Each name attribute is a symbol
# capitalized by String#capitalize, such as :Noselect (not :NoSelect).
- #
- # delim:: Returns the hierarchy delimiter
- #
+ #
+ # delim:: Returns the hierarchy delimiter.
+ #
# name:: Returns the mailbox name.
#
MailboxList = Struct.new(:attr, :delim, :name)
@@ -1532,78 +1818,78 @@ module Net
# 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.
+ #
+ # usage:: Current storage usage of the mailbox.
+ #
+ # quota:: Quota limit imposed on the mailbox.
#
MailboxQuota = Struct.new(:mailbox, :usage, :quota)
# Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
# response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)
- #
+ #
# quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring)
- #
+ #
# ==== Fields:
- #
+ #
# mailbox:: The mailbox with the associated quota.
- #
- # quotaroots:: Zero or more quotaroots that effect the quota on the
+ #
+ # quotaroots:: Zero or more quotaroots that affect the quota on the
# specified mailbox.
#
MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
- # Net::IMAP::MailboxACLItem represents response from GETACL.
- #
+ # Net::IMAP::MailboxACLItem represents the response from GETACL.
+ #
# acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights)
- #
+ #
# identifier ::= astring
- #
+ #
# rights ::= astring
- #
+ #
# ==== Fields:
- #
+ #
# user:: Login name that has certain rights to the mailbox
# that was specified with the getacl command.
- #
+ #
# rights:: The access rights the indicated user has to the
# mailbox.
#
- MailboxACLItem = Struct.new(:user, :rights)
+ MailboxACLItem = Struct.new(:user, :rights, :mailbox)
- # Net::IMAP::StatusData represents contents of the STATUS response.
- #
+ # Net::IMAP::StatusData represents the contents of the STATUS response.
+ #
# ==== Fields:
- #
+ #
# mailbox:: Returns the mailbox name.
- #
+ #
# attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
# "UIDVALIDITY", "UNSEEN". Each value is a number.
- #
+ #
StatusData = Struct.new(:mailbox, :attr)
- # Net::IMAP::FetchData represents contents of the FETCH response.
- #
+ # Net::IMAP::FetchData represents the contents of the FETCH response.
+ #
# ==== Fields:
- #
+ #
# seqno:: Returns the message sequence number.
# (Note: not the unique identifier, even for the UID command response.)
- #
+ #
# attr:: Returns a hash. Each key is a data item name, and each value is
# its value.
- #
+ #
# The current data items are:
- #
+ #
# [BODY]
# A form of BODYSTRUCTURE without extension data.
# [BODY[<section>]<<origin_octet>>]
@@ -1616,7 +1902,7 @@ module Net
# 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
+ # 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.
@@ -1630,110 +1916,110 @@ module Net
# 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
- #
+ # 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
+ # 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.
+ # 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,
@@ -1744,23 +2030,22 @@ module Net
end
# Obsolete: use +subtype+ instead. Calling this will
- # generate a warning message to +stderr+, then return
+ # 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")
+ warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
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,
@@ -1772,23 +2057,22 @@ module Net
end
# Obsolete: use +subtype+ instead. Calling this will
- # generate a warning message to +stderr+, then return
+ # 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")
+ warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
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,
@@ -1802,38 +2086,57 @@ module Net
end
# Obsolete: use +subtype+ instead. Calling this will
- # generate a warning message to +stderr+, then return
+ # 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")
+ warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
return subtype
end
end
- # Net::IMAP::BodyTypeMultipart represents multipart body structures
+ # Net::IMAP::BodyTypeAttachment represents attachment body structures
# of messages.
- #
+ #
# ==== Fields:
- #
+ #
+ # media_type:: Returns the content media type name.
+ #
+ # subtype:: Returns +nil+.
+ #
+ # param:: Returns a hash that represents parameters.
+ #
+ # multipart?:: Returns false.
+ #
+ class BodyTypeAttachment < Struct.new(:media_type, :subtype,
+ :param)
+ def multipart?
+ return false
+ end
+ end
+
+ # Net::IMAP::BodyTypeMultipart represents multipart body structures
+ # of messages.
+ #
+ # ==== Fields:
+ #
# media_type:: Returns the content media type name as defined in [MIME-IMB].
- #
+ #
# subtype:: Returns the content subtype name as defined in [MIME-IMB].
- #
+ #
# parts:: Returns multiple parts.
- #
+ #
# param:: Returns a hash that represents parameters as defined in [MIME-IMB].
- #
+ #
# disposition:: Returns a Net::IMAP::ContentDisposition object giving
# the content disposition.
- #
+ #
# language:: Returns a string or an array of strings giving the body
# language value as defined in [LANGUAGE-TAGS].
- #
+ #
# extension:: Returns extension data.
- #
+ #
# multipart?:: Returns true.
- #
+ #
class BodyTypeMultipart < Struct.new(:media_type, :subtype,
:parts,
:param, :disposition, :language,
@@ -1843,16 +2146,31 @@ module Net
end
# Obsolete: use +subtype+ instead. Calling this will
- # generate a warning message to +stderr+, then return
+ # 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")
+ warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
return subtype
end
end
+ class BodyTypeExtension < Struct.new(:media_type, :subtype,
+ :params, :content_id,
+ :description, :encoding, :size)
+ def multipart?
+ return false
+ end
+ end
+
class ResponseParser # :nodoc:
+ def initialize
+ @str = nil
+ @pos = nil
+ @lex_state = nil
+ @token = nil
+ @flag_symbols = {}
+ end
+
def parse(str)
@str = str
@pos = 0
@@ -1888,7 +2206,7 @@ module Net
T_TEXT = :TEXT
BEG_REGEXP = /\G(?:\
-(?# 1: SPACE )( )|\
+(?# 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%*"\\\[\]+]+)|\
@@ -1936,6 +2254,10 @@ module Net
else
result = response_tagged
end
+ while lookahead.symbol == T_SPACE
+ # Ignore trailing space for Microsoft Exchange Server
+ shift_token
+ end
match(T_CRLF)
match(T_EOF)
return result
@@ -1943,8 +2265,13 @@ module Net
def continue_req
match(T_PLUS)
- match(T_SPACE)
- return ContinuationRequest.new(resp_text, @str)
+ token = lookahead
+ if token.symbol == T_SPACE
+ shift_token
+ return ContinuationRequest.new(resp_text, @str)
+ else
+ return ContinuationRequest.new(ResponseText.new(nil, ""), @str)
+ end
end
def response_untagged
@@ -1959,7 +2286,7 @@ module Net
return response_cond
when /\A(?:FLAGS)\z/ni
return flags_response
- when /\A(?:LIST|LSUB)\z/ni
+ when /\A(?:LIST|LSUB|XLIST)\z/ni
return list_response
when /\A(?:QUOTA)\z/ni
return getquota_response
@@ -2010,12 +2337,12 @@ module Net
when "FETCH"
shift_token
match(T_SPACE)
- data = FetchData.new(n, msg_att)
+ data = FetchData.new(n, msg_att(n))
return UntaggedResponse.new(name, data, @str)
end
end
- def msg_att
+ def msg_att(n)
match(T_LPAR)
attr = {}
while true
@@ -2026,7 +2353,7 @@ module Net
break
when T_SPACE
shift_token
- token = lookahead
+ next
end
case token.value
when /\A(?:ENVELOPE)\z/ni
@@ -2043,8 +2370,10 @@ module Net
name, val = body_data
when /\A(?:UID)\z/ni
name, val = uid_data
+ when /\A(?:MODSEQ)\z/ni
+ name, val = modseq_data
else
- parse_error("unknown attribute `%s'", token.value)
+ parse_error("unknown attribute `%s' for {%d}", token.value, n)
end
attr[name] = val
end
@@ -2111,6 +2440,11 @@ module Net
def rfc822_text
token = match(T_ATOM)
name = token.value.upcase
+ token = lookahead
+ if token.symbol == T_LBRA
+ shift_token
+ match(T_RBRA)
+ end
match(T_SPACE)
return name, nstring
end
@@ -2168,6 +2502,10 @@ module Net
return body_type_text
when /\A(?:MESSAGE)\z/ni
return body_type_msg
+ when /\A(?:ATTACHMENT)\z/ni
+ return body_type_attachment
+ when /\A(?:MIXED)\z/ni
+ return body_type_mixed
else
return body_type_basic
end
@@ -2206,6 +2544,29 @@ module Net
mtype, msubtype = media_type
match(T_SPACE)
param, content_id, desc, enc, size = body_fields
+
+ token = lookahead
+ if token.symbol == T_RPAR
+ # If this is not message/rfc822, we shouldn't apply the RFC822
+ # spec to it. We should handle anything other than
+ # message/rfc822 using multipart extension data [rfc3501] (i.e.
+ # the data itself won't be returned, we would have to retrieve it
+ # with BODYSTRUCTURE instead of with BODY
+
+ # Also, sometimes a message/rfc822 is included as a large
+ # attachment instead of having all of the other details
+ # (e.g. attaching a .eml file to an email)
+ if msubtype == "RFC822"
+ return BodyTypeMessage.new(mtype, msubtype, param, content_id,
+ desc, enc, size, nil, nil, nil, nil,
+ nil, nil, nil)
+ else
+ return BodyTypeExtension.new(mtype, msubtype,
+ param, content_id,
+ desc, enc, size)
+ end
+ end
+
match(T_SPACE)
env = envelope
match(T_SPACE)
@@ -2220,6 +2581,20 @@ module Net
md5, disposition, language, extension)
end
+ def body_type_attachment
+ mtype = case_insensitive_string
+ match(T_SPACE)
+ param = body_fld_param
+ return BodyTypeAttachment.new(mtype, nil, param)
+ end
+
+ def body_type_mixed
+ mtype = "MULTIPART"
+ msubtype = case_insensitive_string
+ param, disposition, language, extension = body_ext_mpart
+ return BodyTypeBasic.new(mtype, msubtype, param, nil, nil, nil, nil, nil, disposition, language, extension)
+ end
+
def body_type_mpart
parts = []
while true
@@ -2240,6 +2615,10 @@ module Net
def media_type
mtype = case_insensitive_string
+ token = lookahead
+ if token.symbol != T_SPACE
+ return mtype, nil
+ end
match(T_SPACE)
msubtype = case_insensitive_string
return mtype, msubtype
@@ -2335,7 +2714,13 @@ module Net
return param
end
disposition = body_fld_dsp
- match(T_SPACE)
+
+ token = lookahead
+ if token.symbol == T_SPACE
+ shift_token
+ else
+ return param, disposition
+ end
language = body_fld_lang
token = lookahead
@@ -2419,7 +2804,7 @@ module Net
end
def section
- str = ""
+ str = String.new
token = match(T_LBRA)
str.concat(token.value)
token = match(T_ATOM, T_NUMBER, T_RBRA)
@@ -2459,7 +2844,7 @@ module Net
return '""'
when /[\x80-\xff\r\n]/n
# literal
- return "{" + str.length.to_s + "}" + CRLF + str
+ return "{" + str.bytesize.to_s + "}" + CRLF + str
when /[(){ \x00-\x1f\x7f%*"\\]/n
# quoted string
return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
@@ -2476,6 +2861,16 @@ module Net
return name, number
end
+ def modseq_data
+ token = match(T_ATOM)
+ name = token.value.upcase
+ match(T_SPACE)
+ match(T_LPAR)
+ modseq = number
+ match(T_RPAR)
+ return name, modseq
+ end
+
def text_response
token = match(T_ATOM)
name = token.value.upcase
@@ -2584,8 +2979,7 @@ module Net
user = astring
match(T_SPACE)
rights = astring
- ##XXX data.push([user, rights])
- data.push(MailboxACLItem.new(user, rights))
+ data.push(MailboxACLItem.new(user, rights, mailbox))
end
end
return UntaggedResponse.new(name, data, @str)
@@ -2605,8 +2999,16 @@ module Net
break
when T_SPACE
shift_token
+ when T_NUMBER
+ data.push(number)
+ when T_LPAR
+ # TODO: include the MODSEQ value in a response
+ shift_token
+ match(T_ATOM)
+ match(T_SPACE)
+ match(T_NUMBER)
+ match(T_RPAR)
end
- data.push(number)
end
else
data = []
@@ -2644,35 +3046,35 @@ module Net
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
+ else
lastmember.children << newmember
- end
+ end
lastmember = newmember
- when T_SPACE
- # do nothing
+ when T_SPACE
+ # do nothing
when T_LPAR
if rootmember.nil?
# dummy member
lastmember = rootmember = ThreadMember.new(nil, [])
- end
-
+ end
+
lastmember.children << thread_branch(token)
when T_RPAR
- break
- end
+ break
+ end
end
-
+
return rootmember
end
@@ -2715,6 +3117,7 @@ module Net
break
when T_SPACE
shift_token
+ next
end
data.push(atom.upcase)
end
@@ -2740,7 +3143,7 @@ module Net
token = match(T_ATOM)
name = token.value.upcase
case name
- when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE)\z/n
+ 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)
@@ -2749,11 +3152,16 @@ module Net
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)
+ token = lookahead
+ if token.symbol == T_SPACE
+ shift_token
+ @lex_state = EXPR_CTEXT
+ token = match(T_TEXT)
+ @lex_state = EXPR_BEG
+ result = ResponseCode.new(name, token.value)
+ else
+ result = ResponseCode.new(name, nil)
+ end
end
match(T_RBRA)
@lex_state = EXPR_RTEXT
@@ -2817,39 +3225,6 @@ module Net
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
@@ -2858,7 +3233,16 @@ module Net
if @str.index(/\(([^)]*)\)/ni, @pos)
@pos = $~.end(0)
return $1.scan(FLAG_REGEXP).collect { |flag, atom|
- atom || flag.capitalize.intern
+ if atom
+ atom
+ else
+ symbol = flag.capitalize.untaint.intern
+ @flag_symbols[symbol] = true
+ if @flag_symbols.length > IMAP.max_flag_count
+ raise FlagCountError, "number of flag symbols exceeded"
+ end
+ symbol
+ end
}
else
parse_error("invalid flag list")
@@ -2911,7 +3295,7 @@ module Net
end
def atom
- result = ""
+ result = String.new
while true
token = lookahead
if atom_token?(token)
@@ -3092,7 +3476,7 @@ module Net
parse_error("unknown token - %s", $&.dump)
end
else
- parse_error("illegal @lex_state - %s", @lex_state.inspect)
+ parse_error("invalid @lex_state - %s", @lex_state.inspect)
end
end
@@ -3136,6 +3520,22 @@ module Net
end
add_authenticator "LOGIN", LoginAuthenticator
+ # Authenticator for the "PLAIN" authentication type. See
+ # #authenticate().
+ class PlainAuthenticator
+ def process(data)
+ return "\0#{@user}\0#{@password}"
+ end
+
+ private
+
+ def initialize(user, password)
+ @user = user
+ @password = password
+ end
+ end
+ add_authenticator "PLAIN", PlainAuthenticator
+
# Authenticator for the "CRAM-MD5" authentication type. See
# #authenticate().
class CramMD5Authenticator
@@ -3159,8 +3559,8 @@ module Net
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
+ k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
+ k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
end
digest = Digest::MD5.digest(k_ipad + text)
@@ -3170,6 +3570,106 @@ module Net
end
add_authenticator "CRAM-MD5", CramMD5Authenticator
+ # Authenticator for the "DIGEST-MD5" authentication type. See
+ # #authenticate().
+ class DigestMD5Authenticator
+ def process(challenge)
+ case @stage
+ when STAGE_ONE
+ @stage = STAGE_TWO
+ sparams = {}
+ c = StringScanner.new(challenge)
+ while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
+ k, v = c[1], c[2]
+ if v =~ /^"(.*)"$/
+ v = $1
+ if v =~ /,/
+ v = v.split(',')
+ end
+ end
+ sparams[k] = v
+ end
+
+ raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
+ raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
+
+ response = {
+ :nonce => sparams['nonce'],
+ :username => @user,
+ :realm => sparams['realm'],
+ :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
+ :'digest-uri' => 'imap/' + sparams['realm'],
+ :qop => 'auth',
+ :maxbuf => 65535,
+ :nc => "%08d" % nc(sparams['nonce']),
+ :charset => sparams['charset'],
+ }
+
+ response[:authzid] = @authname unless @authname.nil?
+
+ # now, the real thing
+ a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
+
+ a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
+ a1 << ':' + response[:authzid] unless response[:authzid].nil?
+
+ a2 = "AUTHENTICATE:" + response[:'digest-uri']
+ a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
+
+ response[:response] = Digest::MD5.hexdigest(
+ [
+ Digest::MD5.hexdigest(a1),
+ response.values_at(:nonce, :nc, :cnonce, :qop),
+ Digest::MD5.hexdigest(a2)
+ ].join(':')
+ )
+
+ return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
+ when STAGE_TWO
+ @stage = nil
+ # if at the second stage, return an empty string
+ if challenge =~ /rspauth=/
+ return ''
+ else
+ raise ResponseParseError, challenge
+ end
+ else
+ raise ResponseParseError, challenge
+ end
+ end
+
+ def initialize(user, password, authname = nil)
+ @user, @password, @authname = user, password, authname
+ @nc, @stage = {}, STAGE_ONE
+ end
+
+ private
+
+ STAGE_ONE = :stage_one
+ STAGE_TWO = :stage_two
+
+ def nc(nonce)
+ if @nc.has_key? nonce
+ @nc[nonce] = @nc[nonce] + 1
+ else
+ @nc[nonce] = 1
+ end
+ return @nc[nonce]
+ end
+
+ # some responses need quoting
+ def qdval(k, v)
+ return if k.nil? or v.nil?
+ if %w"username authzid realm nonce cnonce digest-uri qop".include? k
+ v.gsub!(/([\\"])/, "\\\1")
+ return '%s="%s"' % [k, v]
+ else
+ return '%s=%s' % [k, v]
+ end
+ end
+ end
+ add_authenticator "DIGEST-MD5", DigestMD5Authenticator
+
# Superclass of IMAP errors.
class Error < StandardError
end
@@ -3185,6 +3685,16 @@ module Net
# Superclass of all errors used to encapsulate "fail" responses
# from the server.
class ResponseError < Error
+
+ # The response that caused this error
+ attr_accessor :response
+
+ def initialize(response)
+ @response = response
+
+ super @response.data.text
+ end
+
end
# Error raised upon a "NO" response from the server, indicating
@@ -3198,161 +3708,18 @@ module Net
class BadResponseError < ResponseError
end
- # Error raised upon a "BYE" response from the server, indicating
+ # 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
+ RESPONSE_ERRORS = Hash.new(ResponseError)
+ RESPONSE_ERRORS["NO"] = NoResponseError
+ RESPONSE_ERRORS["BAD"] = BadResponseError
- 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
+ # Error raised when too many flags are interned to symbols.
+ class FlagCountError < Error
end
- ensure
- imap.logout
- imap.disconnect
end
end
-
diff --git a/lib/net/pop.rb b/lib/net/pop.rb
index 39d446f9e6..92a4fe7303 100644
--- a/lib/net/pop.rb
+++ b/lib/net/pop.rb
@@ -1,27 +1,34 @@
+# frozen_string_literal: true
# = net/pop.rb
#
-# Copyright (c) 1999-2003 Yukihiro Matsumoto.
+# Copyright (c) 1999-2007 Yukihiro Matsumoto.
+#
+# Copyright (c) 1999-2007 Minero Aoki.
#
-# 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: pop.rb,v 1.62.2.4 2005/09/13 07:27:18 aamine Exp $
+# Ruby Distribute License.
+#
+# NOTE: You can find Japanese version of this document at:
+# http://docs.ruby-lang.org/ja/latest/library/net=2fpop.html
+#
+# $Id$
#
# See Net::POP3 for documentation.
#
require 'net/protocol'
require 'digest/md5'
+require 'timeout'
+
+begin
+ require "openssl"
+rescue LoadError
+end
module Net
@@ -36,28 +43,26 @@ module Net
class POPBadResponse < POPError; end
#
- # = Net::POP3
- #
# == What is This Library?
- #
- # This library provides functionality for retrieving
+ #
+ # 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
+ #
+ # === 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?
@@ -74,19 +79,19 @@ module Net
# 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?
@@ -103,11 +108,11 @@ module Net
# 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?
@@ -122,11 +127,11 @@ module Net
# 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|
@@ -135,14 +140,14 @@ module Net
# 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|
@@ -153,54 +158,65 @@ module Net
# 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|
+ # pop = Net::POP3.APOP($isapop).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: 1.62.2.4 $.split[1]
+ # svn revision of this library
+ Revision = %q$Revision$.split[1]
#
# Class Parameters
#
- # The default port for POP3 connections, port 110
+ # returns the port for POP3
def POP3.default_port
+ default_pop3_port()
+ end
+
+ # The default port for POP3 connections, port 110
+ def POP3.default_pop3_port
110
end
+ # The default port for POP3S connections, port 995
+ def POP3.default_pop3s_port
+ 995
+ end
+
def POP3.socket_type #:nodoc: obsolete
Net::InternetMessageIO
end
@@ -220,7 +236,7 @@ module Net
# ....
# end
#
- def POP3.APOP( isapop )
+ def POP3.APOP(isapop)
isapop ? APOP : POP3
end
@@ -244,9 +260,9 @@ module Net
# m.delete if $DELETE
# end
#
- def POP3.foreach( address, port = nil,
- account = nil, password = nil,
- isapop = false, &block ) # :yields: message
+ 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)
}
@@ -265,9 +281,9 @@ module Net
# file.write m.pop
# end
#
- def POP3.delete_all( address, port = nil,
- account = nil, password = nil,
- isapop = false, &block )
+ 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)
}
@@ -287,16 +303,16 @@ module Net
# Net::POP3.auth_only('pop.example.com', 110,
# 'YourAccount', 'YourPassword', true)
#
- def POP3.auth_only( address, port = nil,
- account = nil, password = nil,
- isapop = false )
+ 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 )
+ def auth_only(account, password)
raise IOError, 'opening previously opened POP session' if started?
start(account, password) {
;
@@ -304,10 +320,70 @@ module Net
end
#
+ # SSL
+ #
+
+ @ssl_params = nil
+
+ # :call-seq:
+ # Net::POP.enable_ssl(params = {})
+ #
+ # Enable SSL for all new instances.
+ # +params+ is passed to OpenSSL::SSLContext#set_params.
+ def POP3.enable_ssl(*args)
+ @ssl_params = create_ssl_params(*args)
+ end
+
+ # Constructs proper parameters from arguments
+ def POP3.create_ssl_params(verify_or_params = {}, certs = nil)
+ begin
+ params = verify_or_params.to_hash
+ rescue NoMethodError
+ params = {}
+ params[:verify_mode] = verify_or_params
+ if certs
+ if File.file?(certs)
+ params[:ca_file] = certs
+ elsif File.directory?(certs)
+ params[:ca_path] = certs
+ end
+ end
+ end
+ return params
+ end
+
+ # Disable SSL for all new instances.
+ def POP3.disable_ssl
+ @ssl_params = nil
+ end
+
+ # returns the SSL Parameters
+ #
+ # see also POP3.enable_ssl
+ def POP3.ssl_params
+ return @ssl_params
+ end
+
+ # returns +true+ if POP3.ssl_params is set
+ def POP3.use_ssl?
+ return !@ssl_params.nil?
+ end
+
+ # returns whether verify_mode is enable from POP3.ssl_params
+ def POP3.verify
+ return @ssl_params[:verify_mode]
+ end
+
+ # returns the :ca_file or :ca_path from POP3.ssl_params
+ def POP3.certs
+ return @ssl_params[:ca_file] || @ssl_params[:ca_path]
+ end
+
+ #
# Session management
#
- # Creates a new POP3 object and open the connection. Equivalent to
+ # Creates a new POP3 object and open the connection. Equivalent to
#
# Net::POP3.new(address, port, isapop).start(account, password)
#
@@ -323,9 +399,9 @@ module Net
# end
# end
#
- def POP3.start( address, port = nil,
- account = nil, password = nil,
- isapop = false, &block ) # :yield: pop
+ def POP3.start(address, port = nil,
+ account = nil, password = nil,
+ isapop = false, &block) # :yield: pop
new(address, port, isapop).start(account, password, &block)
end
@@ -333,15 +409,16 @@ module Net
#
# +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 +port+ is the port to connect to.
#
# The optional +isapop+ specifies whether this connection is going
# to use APOP authentication; it defaults to +false+.
#
# This method does *not* open the TCP connection.
- def initialize( addr, port = nil, isapop = false )
+ def initialize(addr, port = nil, isapop = false)
@address = addr
- @port = port || self.class.default_port
+ @ssl_params = POP3.ssl_params
+ @port = port
@apop = isapop
@command = nil
@@ -361,9 +438,36 @@ module Net
@apop
end
+ # does this instance use SSL?
+ def use_ssl?
+ return !@ssl_params.nil?
+ end
+
+ # :call-seq:
+ # Net::POP#enable_ssl(params = {})
+ #
+ # Enables SSL for this instance. Must be called before the connection is
+ # established to have any effect.
+ # +params[:port]+ is port to establish the SSL connection on; Defaults to 995.
+ # +params+ (except :port) is passed to OpenSSL::SSLContext#set_params.
+ def enable_ssl(verify_or_params = {}, certs = nil, port = nil)
+ begin
+ @ssl_params = verify_or_params.to_hash.dup
+ @port = @ssl_params.delete(:port) || @port
+ rescue NoMethodError
+ @ssl_params = POP3.create_ssl_params(verify_or_params, certs)
+ @port = port || @port
+ end
+ end
+
+ # Disable SSL for all new instances.
+ def disable_ssl
+ @ssl_params = nil
+ end
+
# Provide human-readable stringification of class state.
def inspect
- "#<#{self.class} #{@address}:#{@port} open=#{@started}>"
+ +"#<#{self.class} #{@address}:#{@port} open=#{@started}>"
end
# *WARNING*: This method causes a serious security hole.
@@ -379,7 +483,7 @@ module Net
# ....
# end
#
- def set_debug_output( arg )
+ def set_debug_output(arg)
@debug_output = arg
end
@@ -387,20 +491,22 @@ module Net
attr_reader :address
# The port number to connect to.
- attr_reader :port
+ def port
+ return @port || (use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port)
+ end
# Seconds to wait until a connection is opened.
# If the POP3 object cannot open a connection within this time,
- # it raises a TimeoutError exception.
+ # it raises a Net::OpenTimeout exception. The default value is 30 seconds.
attr_accessor :open_timeout
# Seconds to wait until reading one block (by one read(1) call).
# If the POP3 object cannot complete a read() within this time,
- # it raises a TimeoutError exception.
+ # it raises a Net::ReadTimeout exception. The default value is 60 seconds.
attr_reader :read_timeout
# Set the read timeout.
- def read_timeout=( sec )
+ def read_timeout=(sec)
@command.socket.read_timeout = sec if @command
@read_timeout = sec
end
@@ -418,9 +524,8 @@ module Net
# closes the session after block call finishes.
#
# This method raises a POPAuthenticationError if authentication fails.
- def start( account, password ) # :yield: pop
+ def start(account, password) # :yield: pop
raise IOError, 'POP session already started' if @started
-
if block_given?
begin
do_start account, password
@@ -434,9 +539,26 @@ module Net
end
end
- def do_start( account, password )
- @socket = self.class.socket_type.old_open(@address, @port,
- @open_timeout, @read_timeout, @debug_output)
+ # internal method for Net::POP3.start
+ def do_start(account, password) # :nodoc:
+ s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do
+ TCPSocket.open(@address, port)
+ end
+ if use_ssl?
+ raise 'openssl library not installed' unless defined?(OpenSSL)
+ context = OpenSSL::SSL::SSLContext.new
+ context.set_params(@ssl_params)
+ s = OpenSSL::SSL::SSLSocket.new(s, context)
+ s.sync_close = true
+ ssl_socket_connect(s, @open_timeout)
+ if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
+ s.post_connection_check(@address)
+ end
+ end
+ @socket = InternetMessageIO.new(s,
+ read_timeout: @read_timeout,
+ debug_output: @debug_output)
+ logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})"
on_connect
@command = POP3Command.new(@socket)
if apop?
@@ -446,11 +568,17 @@ module Net
end
@started = true
ensure
- do_finish if not @started
+ # Authentication failed, clean up connection.
+ unless @started
+ s.close if s
+ @socket = nil
+ @command = nil
+ end
end
private :do_start
- def on_connect
+ # Does nothing
+ def on_connect # :nodoc:
end
private :on_connect
@@ -460,7 +588,12 @@ module Net
do_finish
end
- def do_finish
+ # nil's out the:
+ # - mails
+ # - number counter for mails
+ # - number counter for bytes
+ # - quits the current command, if any
+ def do_finish # :nodoc:
@mails = nil
@n_mails = nil
@n_bytes = nil
@@ -468,12 +601,15 @@ module Net
ensure
@started = false
@command = nil
- @socket.close if @socket and not @socket.closed?
+ @socket.close if @socket
@socket = nil
end
private :do_finish
- def command
+ # Returns the current command.
+ #
+ # Raises IOError if there is no active socket
+ def command # :nodoc:
raise IOError, 'POP session not opened yet' \
if not @socket or @socket.closed?
@command
@@ -520,13 +656,13 @@ module Net
# 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
+ def each_mail(&block) # :yield: message
mails().each(&block)
end
@@ -568,17 +704,21 @@ module Net
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
+ uidl = command().uidl
+ @mails.each {|m| m.uid = uidl[m.number] }
+ end
+
+ # debugging output for +msg+
+ def logging(msg)
+ @debug_output << msg + "\n" if @debug_output
end
end # class POP3
# class aliases
- POP = POP3
- POPSession = POP3
- POP3Session = POP3
+ POP = POP3 # :nodoc:
+ POPSession = POP3 # :nodoc:
+ POP3Session = POP3 # :nodoc:
#
# This class is equivalent to POP3, except that it uses APOP authentication.
@@ -600,7 +740,7 @@ module Net
#
class POPMail
- def initialize( num, len, pop, cmd ) #:nodoc:
+ def initialize(num, len, pop, cmd) #:nodoc:
@number = num
@length = len
@pop = pop
@@ -618,24 +758,24 @@ module Net
# Provide human-readable stringification of class state.
def inspect
- "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>"
+ +"#<#{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
+ # 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|
+ # 'YourAccount', 'YourPassword') do |pop|
# n = 1
# pop.mails.each do |popmail|
# File.open("inbox/#{n}", 'w') do |f|
- # f.write popmail.pop
+ # f.write popmail.pop
# end
# popmail.delete
# n += 1
@@ -645,7 +785,7 @@ module Net
# === Example with block
#
# POP3.start('pop.example.com', 110,
- # 'YourAccount, 'YourPassword') do |pop|
+ # 'YourAccount', 'YourPassword') do |pop|
# n = 1
# pop.mails.each do |popmail|
# File.open("inbox/#{n}", 'w') do |f|
@@ -659,7 +799,7 @@ module Net
#
# This method raises a POPError if an error occurs.
#
- def pop( dest = '', &block ) # :yield: message_chunk
+ def pop( dest = +'', &block ) # :yield: message_chunk
if block_given?
@command.retr(@number, &block)
nil
@@ -674,24 +814,24 @@ module Net
alias all pop #:nodoc: obsolete
alias mail pop #:nodoc: obsolete
- # Fetches the message header and +lines+ lines of body.
+ # 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 = '' )
+ def top(lines, dest = +'')
@command.top(@number, lines) do |chunk|
dest << chunk
end
dest
end
- # Fetches the message header.
+ # Fetches the message header.
#
# The optional +dest+ argument is obsolete.
#
# This method raises a POPError if an error occurs.
- def header( dest = '' )
+ def header(dest = +'')
top(0, dest)
end
@@ -704,7 +844,7 @@ module Net
# === Example
#
# POP3.start('pop.example.com', 110,
- # 'YourAccount, 'YourPassword') do |pop|
+ # 'YourAccount', 'YourPassword') do |pop|
# n = 1
# pop.mails.each do |popmail|
# File.open("inbox/#{n}", 'w') do |f|
@@ -739,7 +879,7 @@ module Net
alias uidl unique_id
- def uid=( uid ) #:nodoc: internal use only (used from POP3#set_all_uids)
+ def uid=(uid) #:nodoc: internal use only
@uid = uid
end
@@ -748,25 +888,27 @@ module Net
class POP3Command #:nodoc: internal use only
- def initialize( sock )
+ def initialize(sock)
@socket = sock
- @error_occured = false
+ @error_occurred = false
res = check_response(critical { recv_response() })
- @apop_stamp = res.slice(/<.+>/)
+ @apop_stamp = res.slice(/<[!-~]+@[!-~]+>/)
end
+ attr_reader :socket
+
def inspect
- "#<#{self.class} socket=#{@socket}>"
+ +"#<#{self.class} socket=#{@socket}>"
end
- def auth( account, password )
+ 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 )
+ def apop(account, password)
raise POPAuthenticationError, 'not APOP server; cannot login' \
unless @apop_stamp
check_response_auth(critical {
@@ -797,28 +939,28 @@ module Net
end
def rset
- check_response(critical { get_response 'RSET' })
+ check_response(critical { get_response('RSET') })
end
- def top( num, lines = 0, &block )
+ def top(num, lines = 0, &block)
critical {
getok('TOP %d %d', num, lines)
@socket.each_message_chunk(&block)
}
end
- def retr( num, &block )
+ def retr(num, &block)
critical {
getok('RETR %d', num)
@socket.each_message_chunk(&block)
}
end
-
- def dele( num )
+
+ def dele(num)
check_response(critical { get_response('DELE %d', num) })
end
- def uidl( num = nil )
+ def uidl(num = nil)
if num
res = check_response(critical { get_response('UIDL %d', num) })
return res.split(/ /)[1]
@@ -841,12 +983,12 @@ module Net
private
- def getok( fmt, *fargs )
+ def getok(fmt, *fargs)
@socket.writeline sprintf(fmt, *fargs)
check_response(recv_response())
end
- def get_response( fmt, *fargs )
+ def get_response(fmt, *fargs)
@socket.writeline sprintf(fmt, *fargs)
recv_response()
end
@@ -855,22 +997,22 @@ module Net
@socket.readline
end
- def check_response( res )
- raise POPError, res unless /\A\+OK/i === res
+ 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
+ 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
+ return '+OK dummy ok response' if @error_occurred
begin
return yield()
rescue Exception
- @error_occured = true
+ @error_occurred = true
raise
end
end
@@ -878,4 +1020,3 @@ module Net
end # class POP3Command
end # module Net
-
diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb
index 0fee78c63a..0e887d5aa9 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>
#
@@ -11,7 +12,7 @@
# modify this program under the same terms as Ruby itself,
# Ruby Distribute License or GNU General Public License.
#
-# $Id: protocol.rb,v 1.73.2.3 2005/09/13 07:27:18 aamine Exp $
+# $Id$
#++
#
# WARNING: This file is going to remove.
@@ -20,6 +21,7 @@
require 'socket'
require 'timeout'
+require 'io/wait'
module Net # :nodoc:
@@ -32,6 +34,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 +65,41 @@ 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; end
+
class BufferedIO #:nodoc: internal use only
- def initialize(io)
+ def initialize(io, read_timeout: 60, continue_timeout: nil, debug_output: nil)
@io = io
- @read_timeout = 60
- @debug_output = nil
- @rbuf = ''
+ @read_timeout = read_timeout
+ @continue_timeout = continue_timeout
+ @debug_output = debug_output
+ @rbuf = ''.dup
end
attr_reader :io
attr_accessor :read_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 +114,19 @@ module Net # :nodoc:
public
- def read(len, dest = '', ignore_eof = false)
+ def read(len, dest = ''.dup, 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 +134,14 @@ module Net # :nodoc:
dest
end
- def read_all(dest = '')
+ def read_all(dest = ''.dup)
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,17 +162,32 @@ 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)
- }
+ case rv = @io.read_nonblock(BUFSIZE, exception: false)
+ when String
+ @rbuf << rv
+ rv.clear
+ return
+ when :wait_readable
+ @io.to_io.wait_readable(@read_timeout) or raise Net::ReadTimeout
+ # continue looping
+ when :wait_writable
+ # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
+ # http://www.openssl.org/support/faq.html#PROG10
+ @io.to_io.wait_writable(@read_timeout) or raise Net::ReadTimeout
+ # continue looping
+ when nil
+ raise EOFError, 'end of file reached'
+ end while true
end
def rbuf_consume(len)
@@ -152,6 +208,8 @@ module Net # :nodoc:
}
end
+ alias << write
+
def writeline(str)
writing {
write0 str + "\r\n"
@@ -200,17 +258,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
@@ -230,7 +278,7 @@ module Net # :nodoc:
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 +289,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
@@ -282,11 +330,15 @@ module Net # :nodoc:
private
+ def dot_stuff(s)
+ s.sub(/\A\./, '..')
+ end
+
def using_each_crlf_line
- @wbuf = ''
+ @wbuf = ''.dup
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 +348,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 +367,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?
diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb
index 89929b1c6e..1777a7fa7e 100644
--- a/lib/net/smtp.rb
+++ b/lib/net/smtp.rb
@@ -1,34 +1,36 @@
+# frozen_string_literal: true
# = net/smtp.rb
-#
-# Copyright (c) 1999-2003 Yukihiro Matsumoto.
#
-# Copyright (c) 1999-2003 Minero Aoki.
-#
+# Copyright (c) 1999-2007 Yukihiro Matsumoto.
+#
+# Copyright (c) 1999-2007 Minero Aoki.
+#
# Written & maintained by Minero Aoki <aamine@loveruby.net>.
#
# Documented by William Webber and Minero Aoki.
-#
+#
# This program is free software. You can re-distribute and/or
-# modify this program under the same terms as Ruby itself,
-# Ruby Distribute License 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: smtp.rb,v 1.69.2.3 2005/09/13 07:27:18 aamine Exp $
+# modify this program under the same terms as Ruby itself.
+#
+# $Id$
+#
+# See Net::SMTP for documentation.
#
-# See Net::SMTP for documentation.
-#
require 'net/protocol'
require 'digest/md5'
+require 'timeout'
+begin
+ require 'openssl'
+rescue LoadError
+end
module Net
# Module mixed in to all SMTP error classes
module SMTPError
- # This *class* is module for some reason.
- # In ruby 1.9.x, this module becomes a class.
+ # This *class* is a module for backward compatibility.
+ # In later release, this module becomes a class.
end
# Represents an SMTP authentication error.
@@ -56,120 +58,142 @@ module Net
include SMTPError
end
- #
- # = Net::SMTP
+ # Command is not supported on server.
+ class SMTPUnsupportedCommand < ProtocolError
+ include SMTPError
+ end
+
#
# == What is This Library?
- #
+ #
# This library provides functionality to send internet
# mail via SMTP, the Simple Mail Transfer Protocol. For details of
# SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt).
- #
+ #
# == What is This Library NOT?
- #
+ #
# This library does NOT provide functions to compose internet mails.
# You must create them by yourself. If you want better mail support,
- # try RubyMail or TMail. You can get both libraries from RAA.
- # (http://www.ruby-lang.org/en/raa.html)
- #
+ # try RubyMail or TMail or search for alternatives in
+ # {RubyGems.org}[https://rubygems.org/] or {The Ruby
+ # Toolbox}[https://www.ruby-toolbox.com/].
+ #
# FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt).
- #
+ #
# == Examples
- #
+ #
# === Sending Messages
- #
+ #
# You must open a connection to an SMTP server before sending messages.
- # The first argument is the address of your SMTP server, and the second
- # argument is the port number. Using SMTP.start with a block is the simplest
- # way to do this. This way, the SMTP connection is closed automatically
+ # 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'
+ # 'his_address@example.com'
# end
- #
+ #
# === Closing the Session
- #
- # You MUST close the SMTP session after sending messages, by calling
+ #
+ # 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
+ # 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
+ class SMTP < Protocol
- Revision = %q$Revision: 1.69.2.3 $.split[1]
+ Revision = %q$Revision$.split[1]
- # The default SMTP port, port 25.
+ # The default SMTP port number, 25.
def SMTP.default_port
25
end
+ # The default mail submission port number, 587.
+ def SMTP.default_submission_port
+ 587
+ end
+
+ # The default SMTPS port number, 465.
+ def SMTP.default_tls_port
+ 465
+ end
+
+ class << self
+ alias default_ssl_port default_tls_port
+ end
+
+ def SMTP.default_ssl_context
+ OpenSSL::SSL::SSLContext.new
+ end
+
#
# Creates a new Net::SMTP object.
#
@@ -181,16 +205,20 @@ module Net
# 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 )
+ def initialize(address, port = nil)
@address = address
@port = (port || SMTP.default_port)
@esmtp = true
+ @capabilities = nil
@socket = nil
@started = false
@open_timeout = 30
@read_timeout = 60
- @error_occured = false
+ @error_occurred = false
@debug_output = nil
+ @tls = false
+ @starttls = false
+ @ssl_context = nil
end
# Provide human-readable stringification of class state.
@@ -198,23 +226,132 @@ module Net
"#<#{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
+ # 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
+ attr_accessor :esmtp
+
+ # +true+ if the SMTP object uses ESMTP (which it does by default).
+ alias :esmtp? :esmtp
+
+ # true if server advertises STARTTLS.
+ # You cannot get valid value before opening SMTP session.
+ def capable_starttls?
+ capable?('STARTTLS')
+ end
+
+ def capable?(key)
+ return nil unless @capabilities
+ @capabilities[key] ? true : false
+ end
+ private :capable?
+
+ # true if server advertises AUTH PLAIN.
+ # You cannot get valid value before opening SMTP session.
+ def capable_plain_auth?
+ auth_capable?('PLAIN')
+ end
+
+ # true if server advertises AUTH LOGIN.
+ # You cannot get valid value before opening SMTP session.
+ def capable_login_auth?
+ auth_capable?('LOGIN')
+ end
+
+ # true if server advertises AUTH CRAM-MD5.
+ # You cannot get valid value before opening SMTP session.
+ def capable_cram_md5_auth?
+ auth_capable?('CRAM-MD5')
+ end
+
+ def auth_capable?(type)
+ return nil unless @capabilities
+ return false unless @capabilities['AUTH']
+ @capabilities['AUTH'].include?(type)
+ end
+ private :auth_capable?
+
+ # Returns supported authentication methods on this server.
+ # You cannot get valid value before opening SMTP session.
+ def capable_auth_types
+ return [] unless @capabilities
+ return [] unless @capabilities['AUTH']
+ @capabilities['AUTH']
+ end
+
+ # true if this object uses SMTP/TLS (SMTPS).
+ def tls?
+ @tls
+ end
+
+ alias ssl? tls?
+
+ # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for
+ # this object. Must be called before the connection is established
+ # to have any effect. +context+ is a OpenSSL::SSL::SSLContext object.
+ def enable_tls(context = SMTP.default_ssl_context)
+ raise 'openssl library not installed' unless defined?(OpenSSL)
+ raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls
+ @tls = true
+ @ssl_context = context
+ end
+
+ alias enable_ssl enable_tls
+
+ # Disables SMTP/TLS for this object. Must be called before the
+ # connection is established to have any effect.
+ def disable_tls
+ @tls = false
+ @ssl_context = nil
+ end
+
+ alias disable_ssl disable_tls
+
+ # Returns truth value if this object uses STARTTLS.
+ # If this object always uses STARTTLS, returns :always.
+ # If this object uses STARTTLS when the server support TLS, returns :auto.
+ def starttls?
+ @starttls
+ end
+
+ # true if this object uses STARTTLS.
+ def starttls_always?
+ @starttls == :always
end
- alias esmtp esmtp?
+ # true if this object uses STARTTLS when server advertises STARTTLS.
+ def starttls_auto?
+ @starttls == :auto
+ end
+
+ # Enables SMTP/TLS (STARTTLS) for this object.
+ # +context+ is a OpenSSL::SSL::SSLContext object.
+ def enable_starttls(context = SMTP.default_ssl_context)
+ raise 'openssl library not installed' unless defined?(OpenSSL)
+ raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
+ @starttls = :always
+ @ssl_context = context
+ end
+
+ # Enables SMTP/TLS (STARTTLS) for this object if server accepts.
+ # +context+ is a OpenSSL::SSL::SSLContext object.
+ def enable_starttls_auto(context = SMTP.default_ssl_context)
+ raise 'openssl library not installed' unless defined?(OpenSSL)
+ raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
+ @starttls = :auto
+ @ssl_context = context
+ end
+
+ # Disables SMTP/TLS (STARTTLS) for this object. Must be called
+ # before the connection is established to have any effect.
+ def disable_starttls
+ @starttls = false
+ @ssl_context = nil
+ end
# The address of the SMTP server to connect to.
attr_reader :address
@@ -224,17 +361,17 @@ module Net
# Seconds to wait while attempting to open a connection.
# If the connection cannot be opened within this time, a
- # TimeoutError is raised.
+ # Net::OpenTimeout is raised. The default value is 30 seconds.
attr_accessor :open_timeout
# Seconds to wait while reading one block (by one read(2) call).
# If the read(2) call does not complete within this time, a
- # TimeoutError is raised.
+ # Net::ReadTimeout is raised. The default value is 60 seconds.
attr_reader :read_timeout
# Set the number of seconds to wait until timing-out a read(2)
# call.
- def read_timeout=( sec )
+ def read_timeout=(sec)
@socket.read_timeout = sec if @socket
@read_timeout = sec
end
@@ -253,10 +390,12 @@ module Net
# ....
# end
#
- def set_debug_output( arg )
+ def debug_output=(arg)
@debug_output = arg
end
+ alias set_debug_output debug_output=
+
#
# SMTP session control
#
@@ -265,7 +404,7 @@ module Net
# 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
@@ -289,7 +428,7 @@ module Net
# +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'.
+ # server (see overview comments); it defaults to 'localhost'.
#
# The remaining arguments are used for SMTP authentication, if required
# or desired. +user+ is the account name; +secret+ is your password
@@ -306,13 +445,13 @@ module Net
# * Net::SMTPSyntaxError
# * Net::SMTPFatalError
# * Net::SMTPUnknownError
+ # * Net::OpenTimeout
+ # * Net::ReadTimeout
# * IOError
- # * TimeoutError
#
- def SMTP.start( address, port = nil,
- helo = 'localhost.localdomain',
- user = nil, secret = nil, authtype = nil,
- &block) # :yield: smtp
+ def SMTP.start(address, port = nil, helo = 'localhost',
+ user = nil, secret = nil, authtype = nil,
+ &block) # :yield: smtp
new(address, port).start(helo, user, secret, authtype, &block)
end
@@ -329,33 +468,33 @@ module Net
# +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
+ # 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.
+ # 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
+ # 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'
+ # 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
+ # 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.
+ # started.
#
# === Errors
#
@@ -368,72 +507,118 @@ module Net
# * Net::SMTPSyntaxError
# * Net::SMTPFatalError
# * Net::SMTPUnknownError
+ # * Net::OpenTimeout
+ # * Net::ReadTimeout
# * IOError
- # * TimeoutError
#
- def start( helo = 'localhost.localdomain',
- user = nil, secret = nil, authtype = nil ) # :yield: smtp
+ def start(helo = 'localhost',
+ user = nil, secret = nil, authtype = nil) # :yield: smtp
if block_given?
begin
- do_start(helo, user, secret, authtype)
+ do_start helo, user, secret, authtype
return yield(self)
ensure
do_finish
end
else
- do_start(helo, user, secret, authtype)
+ 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
+ # 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
- @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
+ private
+
+ def tcp_socket(address, port)
+ TCPSocket.open address, port
+ end
+
+ def do_start(helo_domain, user, secret, authtype)
+ raise IOError, 'SMTP session already started' if @started
+ if user or secret
+ check_auth_method(authtype || DEFAULT_AUTH_TYPE)
+ check_auth_args user, secret
+ end
+ s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do
+ tcp_socket(@address, @port)
+ end
+ logging "Connection opened: #{@address}:#{@port}"
+ @socket = new_internet_message_io(tls? ? tlsconnect(s) : s)
+ check_response critical { recv_response() }
+ do_helo helo_domain
+ if starttls_always? or (capable_starttls? and starttls_auto?)
+ unless capable_starttls?
+ raise SMTPUnsupportedCommand,
+ "STARTTLS is not supported on this server"
end
- raise
+ starttls
+ @socket = new_internet_message_io(tlsconnect(s))
+ # helo response may be different after STARTTLS
+ do_helo helo_domain
end
- authenticate user, secret, authtype if user
+ authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user
@started = true
ensure
- @socket.close if not @started and @socket and not @socket.closed?
+ unless @started
+ # authentication failed, cancel connection.
+ s.close if s
+ @socket = nil
+ end
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
+ def ssl_socket(socket, context)
+ OpenSSL::SSL::SSLSocket.new socket, context
+ end
+
+ def tlsconnect(s)
+ verified = false
+ s = ssl_socket(s, @ssl_context)
+ logging "TLS connection started"
+ s.sync_close = true
+ ssl_socket_connect(s, @open_timeout)
+ if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
+ s.post_connection_check(@address)
+ end
+ verified = true
+ s
+ ensure
+ s.close unless verified
+ end
+
+ def new_internet_message_io(s)
+ InternetMessageIO.new(s, read_timeout: @read_timeout,
+ debug_output: @debug_output)
+ end
+
+ def do_helo(helo_domain)
+ res = @esmtp ? ehlo(helo_domain) : helo(helo_domain)
+ @capabilities = res.capabilities
+ rescue SMTPError
+ if @esmtp
+ @esmtp = false
+ @error_occurred = false
+ retry
+ end
+ raise
end
def do_finish
- quit if @socket and not @socket.closed? and not @error_occured
+ quit if @socket and not @socket.closed? and not @error_occurred
ensure
@started = false
- @error_occured = false
- @socket.close if @socket and not @socket.closed?
+ @error_occurred = false
+ @socket.close if @socket
@socket = nil
end
- private :do_finish
#
- # message send
+ # Message Sending
#
public
@@ -441,7 +626,7 @@ module Net
#
# 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
+ # binary message with this method. +msgstr+ should include both
# the message headers and body.
#
# +from_addr+ is a String representing the source mail address.
@@ -465,13 +650,13 @@ module Net
# * Net::SMTPSyntaxError
# * Net::SMTPFatalError
# * Net::SMTPUnknownError
+ # * Net::ReadTimeout
# * IOError
- # * TimeoutError
#
- def send_message( msgstr, from_addr, *to_addrs )
- send0(from_addr, to_addrs.flatten) {
- @socket.write_message msgstr
- }
+ def send_message(msgstr, from_addr, *to_addrs)
+ raise IOError, 'closed session' unless @socket
+ mailfrom from_addr
+ rcptto_list(to_addrs) {data msgstr}
end
alias send_mail send_message
@@ -518,180 +703,376 @@ module Net
# * Net::SMTPSyntaxError
# * Net::SMTPFatalError
# * Net::SMTPUnknownError
+ # * Net::ReadTimeout
# * 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)
- }
+ def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream
+ raise IOError, 'closed session' unless @socket
+ mailfrom from_addr
+ rcptto_list(to_addrs) {data(&block)}
end
alias ready open_message_stream # obsolete
- private
+ #
+ # Authentication
+ #
- 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
+ public
- mailfrom from_addr
- to_addrs.each do |to|
- rcptto to
- end
+ DEFAULT_AUTH_TYPE = :plain
+
+ def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE)
+ check_auth_method authtype
+ check_auth_args user, secret
+ send auth_method(authtype), user, secret
+ end
+
+ def auth_plain(user, secret)
+ check_auth_args user, secret
res = critical {
- check_response(get_response('DATA'), true)
- yield
- recv_response()
+ get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}"))
}
- check_response(res)
+ check_auth_response res
+ res
end
- #
- # auth
- #
+ def auth_login(user, secret)
+ check_auth_args user, secret
+ res = critical {
+ check_auth_continue get_response('AUTH LOGIN')
+ check_auth_continue get_response(base64_encode(user))
+ get_response(base64_encode(secret))
+ }
+ check_auth_response res
+ res
+ end
+
+ def auth_cram_md5(user, secret)
+ check_auth_args user, secret
+ res = critical {
+ res0 = get_response('AUTH CRAM-MD5')
+ check_auth_continue res0
+ crammed = cram_md5_response(secret, res0.cram_md5_challenge)
+ get_response(base64_encode("#{user} #{crammed}"))
+ }
+ check_auth_response res
+ res
+ end
private
- def check_auth_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)
+ def check_auth_method(type)
+ unless respond_to?(auth_method(type), true)
+ raise ArgumentError, "wrong authentication type #{type}"
+ end
end
- def authenticate( user, secret, authtype )
- __send__("auth_#{authtype || 'cram_md5'}", user, secret)
+ def auth_method(type)
+ "auth_#{type.to_s.downcase}".intern
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
+ def check_auth_args(user, secret, authtype = DEFAULT_AUTH_TYPE)
+ unless user
+ raise ArgumentError, 'SMTP-AUTH requested but missing user name'
+ end
+ unless secret
+ raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase'
+ end
end
- def 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)
+ def base64_encode(str)
+ # expects "str" may not become too long
+ [str].pack('m0')
+ end
- res = get_response(base64_encode(user + ' ' + tmp))
- }
- raise SMTPAuthenticationError, res unless /\A2../ === res
+ IMASK = 0x36
+ OMASK = 0x5c
+
+ # CRAM-MD5: [RFC2195]
+ def cram_md5_response(secret, challenge)
+ tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge)
+ Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp)
end
- def base64_encode( str )
- # expects "str" may not become too long
- [str].pack('m').gsub(/\s+/, '')
+ CRAM_BUFSIZE = 64
+
+ def cram_secret(secret, mask)
+ secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE
+ buf = secret.ljust(CRAM_BUFSIZE, "\0")
+ 0.upto(buf.size - 1) do |i|
+ buf[i] = (buf[i].ord ^ mask).chr
+ end
+ buf
end
#
# SMTP command dispatcher
#
- private
+ public
+
+ # Aborts the current mail transaction
- def helo( domain )
- getok('HELO %s', domain)
+ def rset
+ getok('RSET')
end
- def ehlo( domain )
- getok('EHLO %s', domain)
+ def starttls
+ getok('STARTTLS')
end
- def mailfrom( fromaddr )
- getok('MAIL FROM:<%s>', fromaddr)
+ def helo(domain)
+ getok("HELO #{domain}")
end
- def rcptto( to )
- getok('RCPT TO:<%s>', to)
+ def ehlo(domain)
+ getok("EHLO #{domain}")
+ end
+
+ def mailfrom(from_addr)
+ if $SAFE > 0
+ raise SecurityError, 'tainted from_addr' if from_addr.tainted?
+ end
+ getok("MAIL FROM:<#{from_addr}>")
+ end
+
+ def rcptto_list(to_addrs)
+ raise ArgumentError, 'mail destination not given' if to_addrs.empty?
+ ok_users = []
+ unknown_users = []
+ to_addrs.flatten.each do |addr|
+ begin
+ rcptto addr
+ rescue SMTPAuthenticationError
+ unknown_users << addr.dump
+ else
+ ok_users << addr
+ end
+ end
+ raise ArgumentError, 'mail destination not given' if ok_users.empty?
+ ret = yield
+ unless unknown_users.empty?
+ raise SMTPAuthenticationError, "failed to deliver for #{unknown_users.join(', ')}"
+ end
+ ret
+ end
+
+ def rcptto(to_addr)
+ if $SAFE > 0
+ raise SecurityError, 'tainted to_addr' if to_addr.tainted?
+ end
+ getok("RCPT TO:<#{to_addr}>")
+ end
+
+ # This method sends a message.
+ # If +msgstr+ is given, sends it as a message.
+ # If block is given, yield a message writer stream.
+ # You must write message before the block is closed.
+ #
+ # # Example 1 (by string)
+ # smtp.data(<<EndMessage)
+ # From: john@example.com
+ # To: betty@example.com
+ # Subject: I found a bug
+ #
+ # Check vm.c:58879.
+ # EndMessage
+ #
+ # # Example 2 (by block)
+ # smtp.data {|f|
+ # f.puts "From: john@example.com"
+ # f.puts "To: betty@example.com"
+ # f.puts "Subject: I found a bug"
+ # f.puts ""
+ # f.puts "Check vm.c:58879."
+ # }
+ #
+ def data(msgstr = nil, &block) #:yield: stream
+ if msgstr and block
+ raise ArgumentError, "message and block are exclusive"
+ end
+ unless msgstr or block
+ raise ArgumentError, "message or block is required"
+ end
+ res = critical {
+ check_continue get_response('DATA')
+ socket_sync_bak = @socket.io.sync
+ begin
+ @socket.io.sync = false
+ if msgstr
+ @socket.write_message msgstr
+ else
+ @socket.write_message_by_block(&block)
+ end
+ ensure
+ @socket.io.flush
+ @socket.io.sync = socket_sync_bak
+ end
+ recv_response()
+ }
+ check_response res
+ res
end
def quit
getok('QUIT')
end
- #
- # row level library
- #
-
private
- def getok( fmt, *args )
+ def validate_line(line)
+ # A bare CR or LF is not allowed in RFC5321.
+ if /[\r\n]/ =~ line
+ raise ArgumentError, "A line must not contain CR or LF"
+ end
+ end
+
+ def getok(reqline)
+ validate_line reqline
res = critical {
- @socket.writeline sprintf(fmt, *args)
+ @socket.writeline reqline
recv_response()
}
- return check_response(res)
+ check_response res
+ res
end
- def get_response( fmt, *args )
- @socket.writeline sprintf(fmt, *args)
+ def get_response(reqline)
+ validate_line reqline
+ @socket.writeline reqline
recv_response()
end
def recv_response
- res = ''
+ buf = ''.dup
while true
line = @socket.readline
- res << line << "\n"
- break unless line[3] == ?- # "210-PIPELINING"
+ buf << line << "\n"
+ break unless line[3,1] == '-' # "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
+ Response.parse(buf)
end
- def critical( &block )
- return '200 dummy reply code' if @error_occured
+ def critical
+ return Response.parse('200 dummy reply code') if @error_occurred
begin
return yield()
rescue Exception
- @error_occured = true
+ @error_occurred = true
raise
end
end
+ def check_response(res)
+ unless res.success?
+ raise res.exception_class, res.message
+ end
+ end
+
+ def check_continue(res)
+ unless res.continue?
+ raise SMTPUnknownError, "could not get 3xx (#{res.status}: #{res.string})"
+ end
+ end
+
+ def check_auth_response(res)
+ unless res.success?
+ raise SMTPAuthenticationError, res.message
+ end
+ end
+
+ def check_auth_continue(res)
+ unless res.continue?
+ raise res.exception_class, res.message
+ end
+ end
+
+ # This class represents a response received by the SMTP server. Instances
+ # of this class are created by the SMTP class; they should not be directly
+ # created by the user. For more information on SMTP responses, view
+ # {Section 4.2 of RFC 5321}[http://tools.ietf.org/html/rfc5321#section-4.2]
+ class Response
+ # Parses the received response and separates the reply code and the human
+ # readable reply text
+ def self.parse(str)
+ new(str[0,3], str)
+ end
+
+ # Creates a new instance of the Response class and sets the status and
+ # string attributes
+ def initialize(status, string)
+ @status = status
+ @string = string
+ end
+
+ # The three digit reply code of the SMTP response
+ attr_reader :status
+
+ # The human readable reply text of the SMTP response
+ attr_reader :string
+
+ # Takes the first digit of the reply code to determine the status type
+ def status_type_char
+ @status[0, 1]
+ end
+
+ # Determines whether the response received was a Positive Completion
+ # reply (2xx reply code)
+ def success?
+ status_type_char() == '2'
+ end
+
+ # Determines whether the response received was a Positive Intermediate
+ # reply (3xx reply code)
+ def continue?
+ status_type_char() == '3'
+ end
+
+ # The first line of the human readable reply text
+ def message
+ @string.lines.first
+ end
+
+ # Creates a CRAM-MD5 challenge. You can view more information on CRAM-MD5
+ # on Wikipedia: https://en.wikipedia.org/wiki/CRAM-MD5
+ def cram_md5_challenge
+ @string.split(/ /)[1].unpack1('m')
+ end
+
+ # Returns a hash of the human readable reply text in the response if it
+ # is multiple lines. It does not return the first line. The key of the
+ # hash is the first word the value of the hash is an array with each word
+ # thereafter being a value in the array
+ def capabilities
+ return {} unless @string[3, 1] == '-'
+ h = {}
+ @string.lines.drop(1).each do |line|
+ k, *v = line[4..-1].chomp.split
+ h[k] = v
+ end
+ h
+ end
+
+ # Determines whether there was an error and raises the appropriate error
+ # based on the reply code of the response
+ def exception_class
+ case @status
+ when /\A4/ then SMTPServerBusy
+ when /\A50/ then SMTPSyntaxError
+ when /\A53/ then SMTPAuthenticationError
+ when /\A5/ then SMTPFatalError
+ else SMTPUnknownError
+ end
+ end
+ end
+
+ def logging(msg)
+ @debug_output << msg + "\n" if @debug_output
+ end
+
end # class SMTP
- SMTPSession = SMTP
+ SMTPSession = SMTP # :nodoc:
-end # module Net
+end
diff --git a/lib/net/telnet.rb b/lib/net/telnet.rb
deleted file mode 100644
index a79537b649..0000000000
--- a/lib/net/telnet.rb
+++ /dev/null
@@ -1,749 +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: telnet.rb,v 1.23.2.4 2005/09/14 15:21:31 matz Exp $'
- # :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) ||
- c.rindex(/\r\z/no)
- buf = preprocess(c[0 ... pt])
- rest = c[pt .. -1]
- else
- buf = preprocess(c)
- rest = ''
- end
- else
- # Not Telnetmode.
- #
- # We cannot use preprocess() on this data, because that
- # method makes some Telnetmode-specific assumptions.
- buf = rest + c
- rest = ''
- unless @options["Binmode"]
- if pt = buf.rindex(/\r\z/no)
- buf = buf[0 ... pt]
- rest = buf[pt .. -1]
- end
- buf.gsub!(/#{EOL}/no, "\n")
- end
- end
- @log.print(buf) if @options.has_key?("Output_log")
- line += buf
- yield buf if block_given?
- rescue EOFError # End of file reached
- 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 = /[Pp]ass(?:word|phrase)[: ]*\z/n
- if options.kind_of?(Hash)
- username = options["Name"]
- password = options["Password"]
- login_prompt = options["LoginPrompt"] if options["LoginPrompt"]
- password_prompt = options["PasswordPrompt"] if options["PasswordPrompt"]
- else
- username = options
- end
-
- if block_given?
- line = waitfor(login_prompt){|c| yield c }
- if password
- line += cmd({"String" => username,
- "Match" => password_prompt}){|c| yield c }
- line += cmd(password){|c| yield c }
- else
- line += cmd(username){|c| yield c }
- end
- else
- line = waitfor(login_prompt)
- if password
- line += cmd({"String" => username,
- "Match" => password_prompt})
- line += cmd(password)
- else
- line += cmd(username)
- end
- end
- line
- end
-
- end # class Telnet
-end # module Net
-