# 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, debug: false) @url = URI.parse(url) @auth_header = auth_header @ssl_verify = ssl_verify @debug = debug end def run debug "WS URL: #{@url}" debug "Auth headers: #{@auth_header.map { |k, v| "#{k}: #{v[0, 8]}..." }.join(', ')}" socket = open_socket handshake = perform_handshake(socket) unless handshake.valid? raise "WebSocket handshake failed: #{handshake.error}\nServer response:\n#{handshake.instance_variable_get(:@data)}" end 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 ) debug "Handshake request:\n#{handshake.to_s.chomp}" socket.write(handshake.to_s) response = +'' loop do byte = socket.read(1) break if byte.nil? response << byte handshake << byte break if handshake.finished? end debug "Handshake response:\n#{response.chomp}" handshake end def debug(msg) return unless @debug $stderr.puts "\e[2m[debug] #{msg}\e[0m" 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