+ my $web_socket = IO::Socket::SSL->new(
+ PeerHost => $conn->{host},
+ PeerPort => $conn->{port},
+ SSL_verify_mode => SSL_VERIFY_NONE, # fixme: ???
+ timeout => 30) ||
+ die "failed to connect: $!\n";
+
+ # WebSocket Handshake
+
+ my ($request, $wskey) = $build_web_socket_request->(
+ $conn->{host}, "/$api_path/vncwebsocket", $conn->{ticket}, $termproxy);
+
+ $full_write->($web_socket, $request);
+
+ my $wsbuf = '';
+
+ my $wb_socket_read_available_bytes = sub {
+ my $nr = $web_socket->sysread($wsbuf, $max_payload_size, length($wsbuf));
+ die "web socket read error - $!\n" if $nr < 0;
+ return $nr;
+ };
+
+ my $raw_response = '';
+
+ while(1) {
+ my $nr = $wb_socket_read_available_bytes->();
+ if ($wsbuf =~ s/^(.*?)$CRLF$CRLF//s) {
+ $raw_response = $1;
+ last;
+ }
+ last if !$nr;
+ };
+
+ # Note: we keep any remaining data in $wsbuf
+
+ my $response = HTTP::Response->parse($raw_response);
+
+ # Note: Digest::SHA::sha1_base64 has wrong padding
+ my $wsaccept = Digest::SHA::sha1_base64("${wskey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11") . "=";
+
+ die "got invalid websocket reponse: $raw_response\n"
+ if !(($response->code == 101) &&
+ (lc $response->header('connection') eq 'upgrade') &&
+ (lc $response->header('upgrade') eq 'websocket') &&
+ ($response->header('sec-websocket-protocol') eq 'binary') &&
+ ($response->header('sec-websocket-accept') eq $wsaccept));
+
+ # send auth again...
+ my $frame = $create_websockt_frame->($termproxy->{user} . ":" . $termproxy->{ticket} . "\n");
+ $full_write->($web_socket, $frame);
+
+ # Send resize command
+ my ($columns, $rows) = PVE::PTY::tcgetsize(*STDIN);
+ $frame = $create_websockt_frame->("1:$columns:$rows:");
+ $full_write->($web_socket, $frame);
+
+ # Set STDIN to "raw -echo" mode
+ my $old_termios = PVE::PTY::tcgetattr(*STDIN);
+ my $raw_termios = {%$old_termios};
+
+ my $read_select = IO::Select->new;
+ my $write_select = IO::Select->new;
+
+ my $output_buffer = ''; # write buffer for STDOUT
+ my $websock_buffer = ''; # write buffer for $web_socket
+
+ eval {
+ $SIG{TERM} = $SIG{INT} = $SIG{KILL} = sub { die "received interrupt\n"; };
+
+ PVE::PTY::cfmakeraw($raw_termios);
+ PVE::PTY::tcsetattr(*STDIN, $raw_termios);
+
+ # And set it to non-blocking so we can every char with IO::Select.
+ STDIN->blocking(0);
+ $web_socket->blocking(1);
+ $read_select->add($web_socket);
+ my $input_fh = fileno(STDIN);
+ $read_select->add($input_fh);
+
+ my $output_fh = fileno(STDOUT);
+
+ my $ctrl_a_pressed_before = 0;
+
+ my $winch_received = 0;
+ $SIG{WINCH} = sub { $winch_received = 1; };
+
+ my $check_terminal_size = sub {
+ my ($ncols, $nrows) = PVE::PTY::tcgetsize(*STDIN);
+ if ($ncols != $columns or $nrows != $rows) {
+ $columns = $ncols;
+ $rows = $nrows;
+ $websock_buffer .= $create_websockt_frame->("1:$columns:$rows:");
+ $write_select->add($web_socket);
+ }
+ $winch_received = 0;
+ };
+
+ my $max_buffer_len = 256*1024;
+
+ my $drain_buffer = sub {
+ my ($fh, $buffer_ref) = @_;
+
+ my $len = length($$buffer_ref);
+ my $nr = syswrite($fh, $$buffer_ref);
+ if (!defined($nr)) {
+ next if $! == EINTR || $! == EAGAIN;
+ die "drain buffer - write error - $!\n";
+ }
+ return $nr if !$nr;
+ substr($$buffer_ref, 0, $nr, '');
+ $len = length($$buffer_ref);
+ $write_select->remove($fh) if !$len;
+ };
+
+ while (1) {
+ while(my ($readable, $writable) = IO::Select->select($read_select, $write_select, undef, 3)) {
+ $check_terminal_size->() if $winch_received;
+
+ foreach my $fh (@$writable) {
+ if ($fh == $output_fh) {
+ $drain_buffer->(\*STDOUT, \$output_buffer);
+ $read_select->add($web_socket) if length($output_buffer) <= $max_buffer_len;
+ } elsif ($fh == $web_socket) {
+ $drain_buffer->($web_socket, \$websock_buffer);
+ }
+ }
+
+ foreach my $fh (@$readable) {
+
+ if ($fh == $web_socket) {
+ # Read from WebSocket
+
+ my $nr = $wb_socket_read_available_bytes->();
+ if (!defined($nr)) {
+ die "web socket read error $!\n";
+ } elsif ($nr == 0) {
+ return; # EOF
+ } else {
+ my ($payload, $req_close) = $parse_web_socket_frame->(\$wsbuf);
+ if (defined($payload) && length($payload)) {
+ $output_buffer .= $payload;
+ $write_select->add($output_fh);
+ if (length($output_buffer) > $max_buffer_len) {
+ $read_select->remove($web_socket);
+ }
+ }
+ return if $req_close;
+ }
+
+ } elsif ($fh == $input_fh) {
+ # Read from STDIN
+
+ my $nr = read(\*STDIN, my $buff, 4096);
+ return if !$nr; # EOF or error
+
+ my $char = ord($buff);
+
+ # check for CTRL-a-q
+ return if $ctrl_a_pressed_before == 1 && $char == hex("0x71");
+
+ $ctrl_a_pressed_before = ($char == hex("0x01") && $ctrl_a_pressed_before == 0) ? 1 : 0;
+
+ $websock_buffer .= $create_websockt_frame->("0:" . $nr . ":" . $buff);
+ $write_select->add($web_socket);
+ }
+ }
+ }
+ $check_terminal_size->() if $winch_received;
+
+ # got timeout
+ $websock_buffer .= $create_websockt_frame->("2"); # ping server to keep connection alive
+ $write_select->add($web_socket);
+ }
+ };
+ my $err = $@;
+
+ eval { # cleanup
+
+ # switch back to blocking mode (else later shell commands will fail).
+ STDIN->blocking(1);
+
+ if ($web_socket->connected) {
+ # close connection
+ $websock_buffer .= "\x88" . pack('N', 0) . pack('n', 0); # Opcode, mask, statuscode
+ $full_write->($web_socket, $websock_buffer);
+ $websock_buffer = '';
+ close($web_socket);
+ }
+
+ # Reset the terminal parameters.
+ $output_buffer .= "\e[24H\r\n";
+ $full_write->(\*STDOUT, $output_buffer);
+ $output_buffer = '';
+
+ PVE::PTY::tcsetattr(*STDIN, $old_termios);
+ };
+ warn $@ if $@; # show cleanup errors
+
+ print STDERR "\nERROR: $err" if $err;