mirror of
https://github.com/urbanadventurer/WhatWeb
synced 2026-06-21 14:12:19 +00:00
357 lines
11 KiB
Ruby
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
|