# 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