Files
2025-10-18 17:31:52 +11:00

357 lines
11 KiB
Ruby

require 'net/protocol'
require 'uri'
require 'timeout'
# This works with Ruby 2.x. Based on HTTP library from Ruby 2.2.3p173
# Modified to return the HTTP headers as a string
# added @raw
# added @raw_lines
#
#
# The ExtendedHTTP class is used in place of the HTTP class
# for example,
# http=ExtendedHTTP.new(@uri.host,@uri.port)
# The ExtendedHTTP class uses the ExtendedHTTPResponse class
class ExtendedHTTP < Net::HTTP #:nodoc:
include Net
SSL_ATTRIBUTES = [ :verify_mode ]
# 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
@keep_alive_timeout = 2
@last_communicated = nil
@close_on_empty_response = false
@socket = nil
@started = false
@open_timeout = nil
@read_timeout = 60
@continue_timeout = nil
@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
@enable_post_connection_check = true
@sspi_enabled = false
SSL_IVNAMES.each do |ivname|
instance_variable_set ivname, nil
end
# added for whatweb
@raw = []
end
# ExtendedHTTP :: raw
# added for whatweb
attr_reader :raw
# added @raw for whatweb
def connect
@raw = []
if proxy?
conn_address = proxy_address
conn_port = proxy_port
else
conn_address = address
conn_port = port
end
D "opening connection to #{conn_address}:#{conn_port}..."
s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do
TCPSocket.open(conn_address, conn_port, @local_host, @local_port)
end
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
D 'opened'
# Handle HTTPS through proxy - CONNECT must happen before SSL
if use_ssl? && proxy?
# First create a plain socket connection to proxy
@socket = BufferedIO.new(s)
@socket.read_timeout = @read_timeout
@socket.continue_timeout = @continue_timeout
@socket.debug_output = @debug_output
# Send CONNECT request through plain socket
buf = "CONNECT #{@address}:#{@port} HTTP/#{HTTPVersion}\r\n"
buf << "Host: #{@address}:#{@port}\r\n"
if proxy_user
credential = ["#{proxy_user}:#{proxy_pass}"].pack('m')
credential.delete!("\r\n")
buf << "Proxy-Authorization: Basic #{credential}\r\n"
end
buf << "\r\n"
@socket.write(buf)
# Read and validate CONNECT response
response, raw = ExtendedHTTPResponse.read_new(@socket)
@raw << raw
response.value # This will raise an exception if not 2xx status
# Now establish SSL over the tunneled connection
@ssl_context = OpenSSL::SSL::SSLContext.new
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
# Configure SSL context for maximum compatibility with ALL protocols
begin
if @ssl_context.respond_to?(:ssl_version=)
@ssl_context.ssl_version = :SSLv23_method rescue nil
@ssl_context.ssl_version = :TLS_method rescue nil
end
if @ssl_context.respond_to?(:min_version=)
begin
@ssl_context.min_version = 0
rescue
if defined?(OpenSSL::SSL::SSL2_VERSION)
@ssl_context.min_version = OpenSSL::SSL::SSL2_VERSION rescue nil
elsif defined?(OpenSSL::SSL::SSL3_VERSION)
@ssl_context.min_version = OpenSSL::SSL::SSL3_VERSION rescue nil
end
end
end
@ssl_context.options &= ~OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
@ssl_context.options &= ~OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
@ssl_context.options &= ~OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
rescue => e
# Ignore errors from unsupported SSL options
end
D "starting SSL for #{@address}:#{@port}..."
ssl_socket = OpenSSL::SSL::SSLSocket.new(@socket.io, @ssl_context)
ssl_socket.sync_close = true
# Server Name Indication (SNI) RFC 3546
ssl_socket.hostname = @address if ssl_socket.respond_to? :hostname=
begin
Timeout.timeout(@open_timeout, Net::OpenTimeout) { ssl_socket.connect }
if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
ssl_socket.post_connection_check(@address)
end
@ssl_session = ssl_socket.session
rescue => exception
D "Conn close because of SSL connect error #{exception}"
@socket.close if @socket && !@socket.closed?
raise exception
end
# Replace the socket with SSL socket
@socket = BufferedIO.new(ssl_socket)
@socket.read_timeout = @read_timeout
@socket.continue_timeout = @continue_timeout
@socket.debug_output = @debug_output
D 'SSL established'
elsif use_ssl?
# Direct SSL connection (no proxy)
@ssl_context = OpenSSL::SSL::SSLContext.new
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
# Configure SSL context for maximum compatibility with ALL protocols
begin
if @ssl_context.respond_to?(:ssl_version=)
@ssl_context.ssl_version = :SSLv23_method rescue nil
@ssl_context.ssl_version = :TLS_method rescue nil
end
if @ssl_context.respond_to?(:min_version=)
begin
@ssl_context.min_version = 0
rescue
if defined?(OpenSSL::SSL::SSL2_VERSION)
@ssl_context.min_version = OpenSSL::SSL::SSL2_VERSION rescue nil
elsif defined?(OpenSSL::SSL::SSL3_VERSION)
@ssl_context.min_version = OpenSSL::SSL::SSL3_VERSION rescue nil
end
end
end
@ssl_context.options &= ~OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
@ssl_context.options &= ~OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
@ssl_context.options &= ~OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
rescue => e
# Ignore errors from unsupported SSL options
end
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=
begin
if @ssl_session &&
Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
s.session = @ssl_session if @ssl_session
end
Timeout.timeout(@open_timeout, Net::OpenTimeout) { s.connect }
if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
s.post_connection_check(@address)
end
@ssl_session = s.session
rescue => exception
D "Conn close because of SSL connect error #{exception}"
s.close if s && !s.closed?
raise exception
end
@socket = BufferedIO.new(s)
@socket.read_timeout = @read_timeout
@socket.continue_timeout = @continue_timeout
@socket.debug_output = @debug_output
D 'SSL established'
else
# Plain HTTP connection
@socket = BufferedIO.new(s)
@socket.read_timeout = @read_timeout
@socket.continue_timeout = @continue_timeout
@socket.debug_output = @debug_output
end
on_connect
end
private :connect
def transport_request(req)
count = 0
begin
begin_transport req
res = catch(:response) do
req.exec @socket, @curr_http_version, edit_path(req.path)
begin
# added for whatweb
# res = HTTPResponse.read_new(@socket)
res, y = ExtendedHTTPResponse.read_new(@socket)
@raw << y
#
res.decode_content = req.decode_content
end while res.is_a?(HTTPContinue)
res.uri = req.uri
res.reading_body(@socket, req.response_body_permitted?) do
yield res if block_given?
end
res
end
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 == 0 && IDEMPOTENT_METHODS_.include?(req.method)
count += 1
@socket.close if @socket && !@socket.closed?
D "Conn close because of error #{exception}, and retry"
retry
end
D "Conn close because of error #{exception}"
@socket.close if @socket && !@socket.closed?
raise
end
end_transport req, res
res
rescue => exception
D "Conn close because of error #{exception}"
@socket.close if @socket && !@socket.closed?
raise exception
end
end
# added @raw
class ExtendedHTTPResponse < Net::HTTPResponse # reopen
include Net
class << self
def read_new(sock) #:nodoc: internal use only
x, httpv, code, msg = read_status_line(sock)
@rawlines = x + "\n"
res = response_class(code).new(httpv, code, msg)
each_response_header(sock) do |k, v|
res.add_field k, v
end
# added for whatweb
real = @rawlines
[res, real]
end
private
def read_status_line(sock)
str = sock.readline
(m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in.match(str)) ||
raise(HTTPBadResponse, "wrong status line: #{str.dump}")
[str] + m.captures
end
def each_response_header(sock)
key = value = nil
loop do
line = sock.readuntil("\n", true).sub(/\s+\z/, '')
# added for whatweb
@rawlines << line + "\n" unless line.nil?
#
break if line.empty?
if line[0] == ' ' || line[0] == "\t" && 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
###################
public
# include HTTPHeader
def initialize(httpv, code, msg) #:nodoc: internal use only
@http_version = httpv
@code = code
@message = msg
initialize_http_header nil
@body = nil
@read = false
@uri = nil
@decode_content = false
# added for whatweb
@rawlines = ''
end
end