Skip to content

Commit

Permalink
WIP: Add proxy support
Browse files Browse the repository at this point in the history
  • Loading branch information
micahlee committed Apr 4, 2023
1 parent 3343960 commit 15373bf
Showing 1 changed file with 83 additions and 40 deletions.
123 changes: 83 additions & 40 deletions app/domain/authentication/authn_k8s/web_socket_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,62 +19,47 @@ def self.connect(url, options = {})
client
end

# connect provides options :hostname, :headers, :ssl_version, :cert_store, :verify_mode
# connect provides the options:
# - :hostname
# - :headers
# - :ssl_version
# - :cert_store
# - :verify_mode
def connect(url, options = {})
return if @socket

# Parse the given url to ensure it's valid
@url = url
uri = URI.parse(url)
is_secure_connection = %w[https wss].include?(uri.scheme)
@socket = TCPSocket.new(uri.host,
uri.port || (is_secure_connection ? 443 : 80))
if is_secure_connection
ctx = OpenSSL::SSL::SSLContext.new
ssl_version = options[:ssl_version]
ctx.ssl_version = ssl_version if ssl_version
ctx.verify_mode = options[:verify_mode] || OpenSSL::SSL::VERIFY_PEER # use VERIFY_PEER for verification
cert_store = options[:cert_store]

unless cert_store
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
end
ctx.cert_store = cert_store

use_sni = false
ssl_host_address = options[:hostname] || uri.host # use the param :hostname or default to the host of the url argument

case uri.host
when Resolv::IPv4::Regex, Resolv::IPv6::Regex
# don't set SNI, as IP addresses in SNI is not valid
# per RFC 6066, section 3.

# Avoid openssl warning
ctx.verify_hostname = false
else
use_sni = true
end

@socket = ::OpenSSL::SSL::SSLSocket.new(@socket, ctx)

# support SNI, see https://www.cloudflare.com/en-gb/learning/ssl/what-is-sni/
@socket.hostname = ssl_host_address if use_sni

@socket.connect
# Get HTTP(s) proxy settings
proxy_url = if %w[https wss].include?(uri.scheme)
ENV['https_proxy'] || ENV['HTTPS_PROXY']
else
ENV['http_proxy'] || ENV['HTTP_PROXY']
end

# mandatory hostname verification applicable to both hostnames and IP addresses
@socket.post_connection_check(ssl_host_address)
# Connect the socket
@socket = if proxy_url
connect_with_proxy
else
connect_without_proxy
end
@handshake = ::WebSocket::Handshake::Client.new(url: url, headers: options[:headers])

# Set the initial websocket state
@handshake = WebSocket::Handshake::Client.new(url: url, headers: options[:headers])
@handshaked = false
@pipe_broken = false
frame = ::WebSocket::Frame::Incoming::Client.new
frame = WebSocket::Frame::Incoming::Client.new
@closed = false

# Set up event handler with the websocket is closed
once(:__close) do |err|
close
emit(:close, err)
end

# Begin websocket IO loop
@thread = Thread.new do
until @closed
begin
Expand Down Expand Up @@ -135,6 +120,64 @@ def open?
@handshake.finished? && !@closed
end

protected

def connect_with_proxy(proxy_url, uri, options)
proxy_uri = URI.parse(proxy_url)

connect_string = "CONNECT #{uri.host}:#{uri.port} HTTP/1.1\r\n" +
"Host: #{uri.host}\r\n"

if !proxy_uri.user.nil? && !proxy_uri.password.nil?
auth = Base64.strict_encode64("#{proxy_uri.user}:#{proxy_uri.password}")
connect_string += "Proxy-Authorization: Basic #{auth}\r\n"
end

connect_string += "\r\n"
proxy_socket = TCPSocket.new(proxy_uri.host, proxy_uri.port)
proxy_socket.write(connect_string)
response = ''
until response.include?("\r\n\r\n")
response += proxy_socket.read(1)
end
if !response.include?('200 Connection Established')
raise "Failed to connect through the proxy. Response: #{response}"
end
@socket = open_ssl_socket_with_sni(uri.host, proxy_socket, options) if uri.scheme == 'https'
@socket ||= proxy_socket
end

def connect_without_proxy(uri, options)
@socket = open_ssl_socket_with_sni(uri.host, TCPSocket.new(uri.host, uri.port), options) if uri.scheme == 'https'
@socket ||= TCPSocket.new(uri.host, uri.port)
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
end

def open_ssl_socket_with_sni(host_address, tcp_socket, options)
ctx = OpenSSL::SSL::SSLContext.new
ssl_version = options[:ssl_version]
ctx.ssl_version = ssl_version if ssl_version
ctx.verify_mode = options[:verify_mode] || OpenSSL::SSL::VERIFY_PEER
cert_store = options[:cert_store]
unless cert_store
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
end
ctx.cert_store = cert_store

socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ctx)
use_sni = false
case host_address
when Resolv::IPv4::Regex, Resolv::IPv6::Regex
ctx.verify_hostname = false
else
use_sni = true
end
socket.hostname = host_address if use_sni
socket.connect
socket.post_connection_check(host_address)
socket
end
end
end
end

0 comments on commit 15373bf

Please sign in to comment.