Files
portainer-cli/lib/portainer_cli/websocket_exec.rb
T
grzlus c59deca51e Add ssl_verify config option to fix certificate errors
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>
2026-03-22 13:07:07 +01:00

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