[pve-devel] [PATCH http-server 2/2] Replace own Websocket parser with Protocol::Websocket.
René Jochum
rene at jochums.at
Mon May 21 17:00:51 CEST 2018
It needs "libprotocol-websocket-perl" installed, currently
not available via Debian repos. I got a Debian package for that.
This partially fixes NoVNC over nginx and implements
more Websocket protocols.
I tested:
NoVNC with UTF-8
xterm.js with UTF-8
pventer
Run cacafire in all 3 a long time.
---
PVE/APIServer/AnyEvent.pm | 187 +++++++++++---------------------------
debian/control | 1 +
2 files changed, 54 insertions(+), 134 deletions(-)
diff --git a/PVE/APIServer/AnyEvent.pm b/PVE/APIServer/AnyEvent.pm
index 382eab4..275b5e7 100755
--- a/PVE/APIServer/AnyEvent.pm
+++ b/PVE/APIServer/AnyEvent.pm
@@ -45,6 +45,11 @@ use HTTP::Request;
use HTTP::Response;
use Data::Dumper;
+# WebSocket Handling
+use Encode ();
+use Protocol::WebSocket::Handshake::Server;
+use Protocol::WebSocket::Frame;
+
my $limit_max_headers = 30;
my $limit_max_header_size = 8*1024;
my $limit_max_post = 64*1024;
@@ -339,23 +344,12 @@ sub send_file_start {
}
sub websocket_proxy {
- my ($self, $reqstate, $wsaccept, $wsproto, $param) = @_;
+ my ($self, $reqstate, $hs, $param) = @_;
eval {
my $remhost;
my $remport;
- my $max_payload_size = 65536;
-
- my $binary;
- if ($wsproto eq 'binary') {
- $binary = 1;
- } elsif ($wsproto eq 'base64') {
- $binary = 0;
- } else {
- die "websocket_proxy: unsupported protocol '$wsproto'\n";
- }
-
if ($param->{port}) {
$remhost = 'localhost';
$remport = $param->{port};
@@ -366,12 +360,22 @@ sub websocket_proxy {
die "websocket_proxy: missing port or socket\n";
}
- tcp_connect $remhost, $remport, sub {
+ my $wsframe = $hs->build_frame();
+
+ AnyEvent::Socket::tcp_connect($remhost, $remport, sub {
my ($fh) = @_
or die "connect to '$remhost:$remport' failed: $!";
print "$$: CONNECTed to '$remhost:$remport'\n" if $self->{debug};
+ # Send WebSocket Server Headers.
+ if (!$hs->req->subprotocol =~ /binary/) {
+ die "Unknown subprotocol: " . $hs->req->subprotocol;
+ }
+ $hs->res->subprotocol($hs->req->subprotocol);
+ print $hs->res->to_string if $self->{debug};
+ $reqstate->{hdl}->push_write($hs->res->to_string);
+
$reqstate->{proxyhdl} = AnyEvent::Handle->new(
fh => $fh,
rbuf_max => 64*1024,
@@ -397,129 +401,49 @@ sub websocket_proxy {
my $proxyhdlreader = sub {
my ($hdl) = @_;
- my $len = length($hdl->{rbuf});
- my $data = substr($hdl->{rbuf}, 0, $len, '');
-
- my $string;
- my $payload;
-
- if ($binary) {
- $string = "\x82"; # binary frame
- $payload = $data;
- } else {
- $string = "\x81"; # text frame
- $payload = encode_base64($data, '');
- }
-
- my $payload_len = length($payload);
- if ($payload_len <= 125) {
- $string .= pack 'C', $payload_len;
- } elsif ($payload_len <= 0xffff) {
- $string .= pack 'C', 126;
- $string .= pack 'n', $payload_len;
- } else {
- $string .= pack 'C', 127;
- $string .= pack 'Q>', $payload_len;
- }
- $string .= $payload;
-
- $reqstate->{hdl}->push_write($string) if $reqstate->{hdl};
+ my $frame = Protocol::WebSocket::Frame->new(type => "binary", max_payload_size => 262144); # 2^18
+ $frame->append($hdl->{rbuf}); # clears rbuf
+ $reqstate->{hdl}->push_write($frame->to_bytes);
};
my $hdlreader = sub {
my ($hdl) = @_;
- my $len = length($hdl->{rbuf});
- return if $len < 2;
-
- my $hdr = unpack('C', substr($hdl->{rbuf}, 0, 1));
- my $opcode = $hdr & 0b00001111;
- my $fin = $hdr & 0b10000000;
-
- die "received fragmented websocket frame\n" if !$fin;
-
- my $rsv = $hdr & 0b01110000;
- die "received websocket frame with RSV flags\n" if $rsv;
-
- my $payload_len = unpack 'C', substr($hdl->{rbuf}, 1, 1);
-
- my $masked = $payload_len & 0b10000000;
- die "received unmasked websocket frame from client\n" if !$masked;
-
- my $offset = 2;
- $payload_len = $payload_len & 0b01111111;
- if ($payload_len == 126) {
- return if $len < 4;
- $payload_len = unpack('n', substr($hdl->{rbuf}, $offset, 2));
- $offset += 2;
- } elsif ($payload_len == 127) {
- return if $len < 10;
- $payload_len = unpack('Q>', substr($hdl->{rbuf}, $offset, 8));
- $offset += 8;
- }
-
- die "received too large websocket frame (len = $payload_len)\n"
- if ($payload_len > $max_payload_size) || ($payload_len < 0);
-
- return if $len < ($offset + 4 + $payload_len);
-
- my $data = substr($hdl->{rbuf}, 0, $len, ''); # now consume data
-
- my @mask = (unpack('C', substr($data, $offset+0, 1)),
- unpack('C', substr($data, $offset+1, 1)),
- unpack('C', substr($data, $offset+2, 1)),
- unpack('C', substr($data, $offset+3, 1)));
-
- $offset += 4;
-
- my $payload = substr($data, $offset, $payload_len);
-
- for (my $i = 0; $i < $payload_len; $i++) {
- my $d = unpack('C', substr($payload, $i, 1));
- my $n = $d ^ $mask[$i % 4];
- substr($payload, $i, 1, pack('C', $n));
- }
-
- $payload = decode_base64($payload) if !$binary;
-
- if ($opcode == 1 || $opcode == 2) {
- $reqstate->{proxyhdl}->push_write($payload) if $reqstate->{proxyhdl};
- } elsif ($opcode == 8) {
- my $statuscode = unpack ("n", $payload);
- print "websocket received close. status code: '$statuscode'\n" if $self->{debug};
+ my $chunk = $hdl->{rbuf};
+ $hdl->{rbuf} = undef;
+
+ $wsframe->append($chunk);
+
+ if ($wsframe->is_text || $wsframe->is_binary) {
+ $reqstate->{proxyhdl}->push_write($wsframe->next_bytes);
+ } elsif ($wsframe->is_ping) {
+ $reqstate->{hdl}->push_write($hs->build_frame(
+ type => 'pong',
+ buffer => $wsframe->next_bytes,
+ )->to_bytes
+ );
+ } elsif ($wsframe->is_close) {
+ # Send close frame back.
+ $reqstate->{hdl}->push_write($hs->build_frame(
+ type => 'close'
+ )->to_bytes
+ );
if ($reqstate->{proxyhdl}) {
$reqstate->{proxyhdl}->push_shutdown();
}
$hdl->push_shutdown();
- } else {
- die "received unhandled websocket opcode $opcode\n";
- }
+ }
+ # Ignore Pong here.
};
- my $proto = $reqstate->{proto} ? $reqstate->{proto}->{str} : 'HTTP/1.1';
-
$reqstate->{proxyhdl}->timeout(0);
$reqstate->{proxyhdl}->on_read($proxyhdlreader);
$reqstate->{hdl}->on_read($hdlreader);
- # todo: use stop_read/start_read if write buffer grows to much
-
- my $res = "$proto 101 Switching Protocols\015\012" .
- "Upgrade: websocket\015\012" .
- "Connection: upgrade\015\012" .
- "Sec-WebSocket-Accept: $wsaccept\015\012" .
- "Sec-WebSocket-Protocol: $wsproto\015\012" .
- "\015\012";
-
- print $res if $self->{debug};
-
- $reqstate->{hdl}->push_write($res);
-
# log early
$reqstate->{log}->{code} = 101;
- $self->log_request($reqstate);
- };
-
+ $self->log_request($reqstate);
+ });
};
if (my $err = $@) {
warn $err;
@@ -726,23 +650,18 @@ sub handle_api2_request {
return;
} elsif ($upgrade && ($method eq 'GET') && ($path =~ m|websocket$|)) {
- die "unable to upgrade to protocol '$upgrade'\n" if !$upgrade || ($upgrade ne 'websocket');
- my $wsver = $r->header('sec-websocket-version');
- die "unsupported websocket-version '$wsver'\n" if !$wsver || ($wsver ne '13');
- my $wsproto_str = $r->header('sec-websocket-protocol');
- die "missing websocket-protocol header" if !$wsproto_str;
- my $wsproto;
- foreach my $p (PVE::Tools::split_list($wsproto_str)) {
- $wsproto = $p if !$wsproto && $p eq 'base64';
- $wsproto = $p if $p eq 'binary';
+ my $proto = $reqstate->{proto} ? $reqstate->{proto}->{str} : 'HTTP/1.1';
+ my $headers = "GET $r->uri->as_string $proto\x0d\x0a" . $r->headers->as_string("\x0d\x0a") . "\x0d\x0a";
+ my $hs = Protocol::WebSocket::Handshake::Server->new;
+ $hs->parse($headers);
+ if (!defined($hs->parse($headers))) {
+ die "Error while parsing WebSocket Headers: " . $hs->error;
+ }
+ if (!$hs->is_done) {
+ die "Got incomplete WebSocket headers.";
}
- die "unsupported websocket-protocol protocol '$wsproto_str'\n" if !$wsproto;
- my $wskey = $r->header('sec-websocket-key');
- die "missing websocket-key\n" if !$wskey;
- # Note: Digest::SHA::sha1_base64 has wrong padding
- my $wsaccept = Digest::SHA::sha1_base64("${wskey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11") . "=";
if ($res->{status} == HTTP_OK) {
- $self->websocket_proxy($reqstate, $wsaccept, $wsproto, $res->{data});
+ $self->websocket_proxy($reqstate, $hs, $res->{data});
return;
}
}
diff --git a/debian/control b/debian/control
index bb01640..5bdc307 100644
--- a/debian/control
+++ b/debian/control
@@ -20,6 +20,7 @@ Depends: libanyevent-http-perl,
libnet-ip-perl,
libpve-common-perl,
liburi-perl,
+ libprotocol-websocket-perl,
${perl:Depends},
Description: Proxmox Asynchrounous HTTP Server Implementation
This is used to implement the PVE REST API.
--
2.17.0
More information about the pve-devel
mailing list