Skip to content

Commit

Permalink
Add HTTP proxy support to k8s websocket
Browse files Browse the repository at this point in the history
  • Loading branch information
micahlee committed Apr 11, 2023
1 parent 2bdef32 commit 964d06e
Show file tree
Hide file tree
Showing 5 changed files with 397 additions and 91 deletions.
72 changes: 72 additions & 0 deletions app/domain/authentication/authn_k8s/proxied_tcp_socket.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require 'delegate'

module Authentication
module AuthnK8s
# A proxy socket first establishes a TCP connection through a configured
# proxy server
class ProxiedTcpSocket < SimpleDelegator

def initialize(
proxy_uri:,
destination_host:,
destination_port:,

# Injected dependencies
logger: ::Rails.logger
)
@proxy_uri = proxy_uri
@destination_host = destination_host
@destination_port = destination_port

# Connect to the proxy
super(connect_proxy_socket)
end

protected

def connect_proxy_socket
proxy_socket = TCPSocket.new(@proxy_uri.host, @proxy_uri.port)

# Send proxy connection handshake
proxy_socket.write(proxy_connect_string)

# Wait for connect response
response = ''
until response.include?("\r\n\r\n")
response += proxy_socket.read(1)
end

# Verify we received a valid connection response
if !response.downcase.include?('200 connection established')
raise "Failed to connect through proxy " \
"('#{@proxy_uri.host}:#{@proxy_uri.port}'). " \
"Response: #{response.strip}"
end
end

# For spec details, see:
# https://httpwg.org/specs/rfc9110.html#CONNECT
def proxy_connect_string
connect_string = \
"CONNECT #{@destination_host}:#{@destination_port} HTTP/1.1\r\n" \
"Host: #{@destination_host}\r\n"

if proxy_authorization
connect_string += \
"Proxy-Authorization: Basic #{proxy_authorization}\r\n"
end

connect_string += "\r\n"
end

# :reek:DuplicateMethodCall because of accessing #user and #password twice
def proxy_authorization
return unless @proxy_uri.user && @proxy_uri.password

Base64.strict_encode64("#{@proxy_uri.user}:#{@proxy_uri.password}")
end
end
end
end
72 changes: 72 additions & 0 deletions app/domain/authentication/authn_k8s/secure_tcp_socket.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require 'delegate'

module Authentication
module AuthnK8s
# A proxy socket first establishes a TCP connection through a configured
# proxy server
class SecureTcpSocket < SimpleDelegator
def self.default_cert_store
OpenSSL::X509::Store.new.tap do |cert_store|
cert_store.set_default_paths
end
end

def initialize(
socket:,

# Optional keyword arguments to configure the TLS behavior
hostname: nil,
headers: nil,
cert_store: SecureTcpSocket.default_cert_store,
verify_mode: OpenSSL::SSL::VERIFY_PEER,

# Injected dependencies
logger: ::Rails.logger
)
@socket = socket

@hostname = hostname
@headers = headers
@cert_store = cert_store
@verify_mode = verify_mode

super(secure_socket)
end

protected

def secure_socket
# Wrap the provided TCP socket with an SSLSocket
OpenSSL::SSL::SSLSocket.new(@socket, openssl_context).tap do |socket|
# support SNI, see https://www.cloudflare.com/en-gb/learning/ssl/what-is-sni/
# don't set SNI hostname for IP address per RFC 6066, section 3.
socket.hostname = @hostname unless ip_address?

# Establish secure connection
socket.connect
socket.post_connection_check(@hostname)
end
end

def ip_address?
@hostname.match?(Resolv::IPv4::Regex) ||
@hostname.match?(Resolv::IPv6::Regex)
end

def openssl_context
OpenSSL::SSL::SSLContext.new.tap do |ctx|
# Set the certificate store
ctx.cert_store = @cert_store

# Verify the TLS peer by default unless a verify mode is specified
ctx.verify_mode = @verify_mode

# Avoid openssl warning on hostname verification for IP address
ctx.verify_hostname = false if ip_address?
end
end
end
end
end
Loading

0 comments on commit 964d06e

Please sign in to comment.