c59deca51e
Raw OpenSSL sockets don't find the system CA bundle the same way Net::HTTP does. ssl_verify (default: true) is now a config setting that applies to both HTTP REST calls and WebSocket connections. Set via `portainer-cli configure`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
2.8 KiB
Ruby
114 lines
2.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'socket'
|
|
require 'openssl'
|
|
require 'base64'
|
|
require 'io/console'
|
|
require 'websocket'
|
|
|
|
module PortainerCli
|
|
class WebsocketExec
|
|
READ_SIZE = 4096
|
|
|
|
def initialize(url, auth_header, ssl_verify: true)
|
|
@url = URI.parse(url)
|
|
@auth_header = auth_header
|
|
@ssl_verify = ssl_verify
|
|
end
|
|
|
|
def run
|
|
socket = open_socket
|
|
|
|
handshake = perform_handshake(socket)
|
|
raise "WebSocket handshake failed: #{handshake.error}" unless handshake.valid?
|
|
|
|
run_io_loop(socket)
|
|
ensure
|
|
socket&.close rescue nil
|
|
end
|
|
|
|
private
|
|
|
|
def open_socket
|
|
raw = TCPSocket.new(@url.host, @url.port)
|
|
|
|
if @url.scheme == 'wss'
|
|
ctx = OpenSSL::SSL::SSLContext.new
|
|
ctx.verify_mode = @ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
ssl = OpenSSL::SSL::SSLSocket.new(raw, ctx)
|
|
ssl.hostname = @url.host
|
|
ssl.sync_close = true
|
|
ssl.connect
|
|
ssl
|
|
else
|
|
raw
|
|
end
|
|
end
|
|
|
|
def perform_handshake(socket)
|
|
handshake = WebSocket::Handshake::Client.new(
|
|
url: @url.to_s,
|
|
headers: @auth_header
|
|
)
|
|
socket.write(handshake.to_s)
|
|
|
|
loop do
|
|
byte = socket.read(1)
|
|
break if byte.nil?
|
|
handshake << byte
|
|
break if handshake.finished?
|
|
end
|
|
|
|
handshake
|
|
end
|
|
|
|
def run_io_loop(socket)
|
|
incoming = WebSocket::Frame::Incoming::Client.new
|
|
|
|
# Put terminal in raw mode so all keypresses go directly to the container
|
|
$stdin.raw do
|
|
loop do
|
|
readable, = IO.select([$stdin, socket], nil, nil, nil)
|
|
|
|
readable.each do |io|
|
|
if io == $stdin
|
|
data = io.readpartial(READ_SIZE) rescue nil
|
|
break if data.nil?
|
|
frame = WebSocket::Frame::Outgoing::Client.new(
|
|
data: data,
|
|
type: :binary,
|
|
version: 13
|
|
)
|
|
socket.write(frame.to_s)
|
|
|
|
elsif io == socket
|
|
data = io.readpartial(READ_SIZE) rescue nil
|
|
if data.nil?
|
|
# Server closed the connection
|
|
return
|
|
end
|
|
incoming << data
|
|
while (frame = incoming.next)
|
|
case frame.type
|
|
when :binary, :text
|
|
$stdout.write(frame.data)
|
|
$stdout.flush
|
|
when :close
|
|
return
|
|
when :ping
|
|
pong = WebSocket::Frame::Outgoing::Client.new(
|
|
data: frame.data,
|
|
type: :pong,
|
|
version: 13
|
|
)
|
|
socket.write(pong.to_s)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|