Skip to content

Commit

Permalink
removed support for permessage-deflate WebSocket compression, since t…
Browse files Browse the repository at this point in the history
…here have been too many problems with Chrome
  • Loading branch information
kraih committed Feb 2, 2014
1 parent 30e520a commit e98254f
Show file tree
Hide file tree
Showing 6 changed files with 24 additions and 168 deletions.
2 changes: 2 additions & 0 deletions Changes
@@ -1,6 +1,8 @@

4.74 2014-02-02
- Added all_contents method to Mojo::DOM.
- Removed support for permessage-deflate WebSocket compression, since there
have been too many problems with Chrome.

4.73 2014-02-01
- Improved xml_escape performance significantly.
Expand Down
60 changes: 3 additions & 57 deletions lib/Mojo/Transaction/WebSocket.pm
@@ -1,7 +1,6 @@
package Mojo::Transaction::WebSocket;
use Mojo::Base 'Mojo::Transaction';

use Compress::Raw::Zlib 'Z_SYNC_FLUSH';
use Config;
use Mojo::JSON;
use Mojo::Transaction::HTTP;
Expand All @@ -26,9 +25,8 @@ use constant {
PONG => 10
};

has [qw(compressed masked)];
has context_takeover => 1;
has handshake => sub { Mojo::Transaction::HTTP->new };
has 'masked';
has handshake => sub { Mojo::Transaction::HTTP->new };
has max_websocket_size => sub { $ENV{MOJO_MAX_WEBSOCKET_SIZE} || 262144 };

sub build_frame {
Expand Down Expand Up @@ -90,29 +88,13 @@ sub build_message {
if (exists $frame->{text}) { $frame = [1, 0, 0, 0, TEXT, $frame->{text}] }
else { $frame = [1, 0, 0, 0, BINARY, $frame->{binary}] }

# "permessage-deflate" extension
return $self->build_frame(@$frame) unless $self->compressed;
my $deflate = $self->{deflate}
|| Compress::Raw::Zlib::Deflate->new(WindowBits => -15, MemLevel => 8);
$self->{deflate} = $deflate if $self->context_takeover;
$deflate->deflate(\$frame->[5], my $out);
$deflate->flush($out, Z_SYNC_FLUSH);
@$frame[1, 5] = (1, substr($out, 0, length($out) - 4));
return $self->build_frame(@$frame);
}

sub client_challenge {
my $self = shift;

# "permessage-deflate" extension
my $headers = $self->res->headers;
my $extensions = $headers->sec_websocket_extensions // '';
$self->context_takeover(0)
if $self->_deflate($extensions)
&& $extensions =~ /client_no_context_takeover/i;

return _challenge($self->req->headers->sec_websocket_key) eq
$headers->sec_websocket_accept;
$self->res->headers->sec_websocket_accept;
}

sub client_handshake {
Expand All @@ -122,8 +104,6 @@ sub client_handshake {
$headers->upgrade('websocket') unless $headers->upgrade;
$headers->connection('Upgrade') unless $headers->connection;
$headers->sec_websocket_version(13) unless $headers->sec_websocket_version;
$headers->sec_websocket_extensions('permessage-deflate')
unless $headers->sec_websocket_extensions;

# Generate 16 byte WebSocket challenge
my $challenge = b64_encode sprintf('%16u', int(rand 9 x 16)), '';
Expand Down Expand Up @@ -259,10 +239,6 @@ sub server_handshake {
and $res_headers->sec_websocket_protocol($1);
$res_headers->sec_websocket_accept(
_challenge($req_headers->sec_websocket_key));

# "permessage-deflate" extension
$res_headers->sec_websocket_extensions('permessage-deflate')
if $self->_deflate($req_headers->sec_websocket_extensions // '');
}

sub server_read {
Expand All @@ -289,8 +265,6 @@ sub server_write {

sub _challenge { b64_encode(sha1_bytes(($_[0] || '') . GUID), '') }

sub _deflate { $_[1] =~ /permessage-deflate/i && $_[0]->compressed(1) }

sub _message {
my ($self, $frame) = @_;

Expand All @@ -317,20 +291,7 @@ sub _message {
# No FIN bit (Continuation)
return unless $frame->[0];

# "permessage-deflate" extension (handshake and RSV1)
my $msg = delete $self->{message};
if ($self->compressed && $frame->[1]) {
my $inflate = $self->{inflate} || Compress::Raw::Zlib::Inflate->new(
Bufsize => $max,
LimitOutput => 1,
WindowBits => -15
);
$self->{inflate} = $inflate if $self->context_takeover;
$inflate->inflate(\($msg .= "\x00\x00\xff\xff"), my $out);
return $self->finish(1009) if length $msg;
$msg = $out;
}

$self->emit(json => Mojo::JSON->new->decode($msg))
if $self->has_subscribers('json');
$op = delete $self->{op};
Expand Down Expand Up @@ -482,21 +443,6 @@ Emitted when a complete WebSocket text message has been received.
L<Mojo::Transaction::WebSocket> inherits all attributes from
L<Mojo::Transaction> and implements the following new ones.
=head2 compressed
my $bool = $ws->compressed;
$ws = $ws->compressed($bool);
Compress messages with C<permessage-deflate> extension.
=head2 context_takeover
my $bool = $ws->context_takeover;
$ws = $ws->context_takeover($bool);
Reuse LZ77 sliding window for C<permessage-deflate> extension, defaults to
true.
=head2 handshake
my $handshake = $ws->handshake;
Expand Down
5 changes: 0 additions & 5 deletions lib/Test/Mojo.pm
Expand Up @@ -827,11 +827,6 @@ Perform request and check for transport errors.
my $tx = $t->ua->build_tx(FOO => '/test.json' => json => {foo => 1});
$t->request_ok($tx)->status_is(200)->json_is({success => 1});
# WebSocket handshake without extensions
my $tx = $t->ua->build_websocket_tx('/foo');
$tx->req->headers->remove('Sec-WebSocket-Extensions');
$t->request_ok($tx)->message_ok->message_is('bar')->finish_ok;
=head2 reset_session
$t = $t->reset_session;
Expand Down
10 changes: 3 additions & 7 deletions t/mojo/websocket.t
Expand Up @@ -455,17 +455,15 @@ is $stash->{finished}, 1, 'finish event has been emitted once';
like $log, qr/Inactivity timeout\./, 'right log message';
app->log->unsubscribe(message => $msg);

# Ping/pong (with negotiated compression)
my ($pong, $compressed, $extensions);
# Ping/pong
my $pong;
$ua->websocket(
'/echo' => sub {
my ($ua, $tx) = @_;
$tx->on(
frame => sub {
my ($tx, $frame) = @_;
$pong = $frame->[5] if $frame->[4] == 10;
$compressed = $tx->compressed;
$extensions = $tx->res->headers->sec_websocket_extensions;
$pong = $frame->[5] if $frame->[4] == 10;
Mojo::IOLoop->stop;
}
);
Expand All @@ -474,7 +472,5 @@ $ua->websocket(
);
Mojo::IOLoop->start;
is $pong, 'test', 'received pong with payload';
ok $compressed, 'WebSocket has compression';
is $extensions, 'permessage-deflate', 'right "Sec-WebSocket-Extensions" value';

done_testing();
22 changes: 3 additions & 19 deletions t/mojo/websocket_frames.t
Expand Up @@ -206,31 +206,15 @@ is $frame->[5], '', 'no payload';
isnt(Mojo::Transaction::WebSocket->new->build_frame(1, 0, 0, 0, 2, ''),
$bytes, 'frames are not equal');

# Compressed binary message roundtrip
$ws = Mojo::Transaction::WebSocket->new(compressed => 1);
# Binary message roundtrip
$ws = Mojo::Transaction::WebSocket->new;
$bytes = $ws->build_message({binary => 'just works'});
$frame = $ws->parse_frame(\($dummy = $bytes));
is $frame->[0], 1, 'fin flag is set';
is $frame->[1], 1, 'rsv1 flag is not set';
is $frame->[1], 0, 'rsv1 flag is not set';
is $frame->[2], 0, 'rsv2 flag is not set';
is $frame->[3], 0, 'rsv3 flag is not set';
is $frame->[4], 2, 'binary frame';
ok $frame->[5], 'has payload';
isnt(
Mojo::Transaction::WebSocket->new->build_message({binary => 'just works'}),
$bytes, 'messages are not equal');

# Compressed binary message roundtrip with context takeover
$ws = Mojo::Transaction::WebSocket->new(compressed => 1);
my $first = $ws->build_message({binary => 'just works'});
my $second = $ws->build_message({binary => 'just works'});
isnt $first, $second, 'messages are not equal';

# Compressed binary message roundtrip without context takeover
$ws
= Mojo::Transaction::WebSocket->new(compressed => 1, context_takeover => 0);
$first = $ws->build_message({binary => 'just works'});
$second = $ws->build_message({binary => 'just works'});
is $first, $second, 'messages are equal';

done_testing();
93 changes: 13 additions & 80 deletions t/mojolicious/websocket_lite_app.t
Expand Up @@ -55,21 +55,6 @@ websocket '/unicode' => sub {
);
};

websocket '/no_context_takeover' => sub {
my $self = shift;
$self->tx->context_takeover(0);
$self->res->headers->sec_websocket_extensions(
'permessage-deflate;client_no_context_takeover');
$self->on(binary => sub { shift->send({binary => shift}) });
};

websocket '/no_compression' => sub {
my $self = shift;
$self->tx->compressed(0);
$self->res->headers->remove('Sec-WebSocket-Extensions');
$self->on(binary => sub { shift->send({binary => shift}) });
};

websocket '/bytes' => sub {
my $self = shift;
$self->on(
Expand Down Expand Up @@ -135,12 +120,8 @@ is $t->tx->req->headers->dnt, 1, 'right "DNT" value';
is $t->tx->req->headers->sec_websocket_protocol, 'foo, bar, baz',
'right "Sec-WebSocket-Protocol" value';

# Bytes with compression (offer "client_no_context_takeover")
my $extensions = 'permessage-deflate;client_no_context_takeover';
$t->websocket_ok('/echo' => {'Sec-WebSocket-Extensions' => $extensions});
ok $t->tx->compressed, 'WebSocket has compression';
ok $t->tx->context_takeover, 'with context takeover';
$t->send_ok({binary => 'bytes!'})
# Bytes
$t->websocket_ok('/echo')->send_ok({binary => 'bytes!'})
->message_ok->message_is({binary => 'bytes!'})
->send_ok({binary => 'bytes!'})
->message_ok->message_isnt({text => 'bytes!'})->finish_ok;
Expand All @@ -150,42 +131,17 @@ $t->websocket_ok('/echo')->send_ok(0)->message_ok->message_is('echo: 0')
->send_ok(0)->message_ok->message_like({text => qr/0/})->finish_ok(1000)
->finished_ok(1000);

# 64bit binary message (extended limit and no compression)
my $tx = $t->ua->build_websocket_tx('/echo');
$tx->req->headers->remove('Sec-WebSocket-Extensions');
$t->request_ok($tx);
# 64bit binary message (extended limit)
$t->request_ok($t->ua->build_websocket_tx('/echo'));
is $t->tx->max_websocket_size, 262144, 'right size';
$t->tx->max_websocket_size(262145);
$t->send_ok({binary => 'a' x 262145})
->message_ok->message_is({binary => 'a' x 262145})
->finish_ok->finished_ok(1005);

# 64bit binary message (too large and no compression)
$tx = $t->ua->build_websocket_tx('/echo');
$tx->req->headers->remove('Sec-WebSocket-Extensions');
$t->request_ok($tx)->send_ok({binary => 'b' x 262145})->finished_ok(1009);

# Compressed message ("permessage-deflate")
$t->websocket_ok('/echo');
$t->send_ok({binary => 'a' x 50000})
->header_is('Sec-WebSocket-Extensions' => 'permessage-deflate');
is $t->tx->req->headers->sec_websocket_extensions, 'permessage-deflate',
'right "Sec-WebSocket-Extensions" value';
my $payload;
$t->tx->once(
frame => sub {
my ($tx, $frame) = @_;
$payload = $frame->[5];
}
);
$t->message_ok->message_is({binary => 'a' x 50000});
ok length $payload < 262145, 'message has been compressed';
$t->finish_ok->finished_ok(1005);

# Compressed message exceeding the limit when uncompressed
$t->websocket_ok('/echo')
->header_is('Sec-WebSocket-Extensions' => 'permessage-deflate')
->send_ok({binary => 'a' x 1000000})->finished_ok(1009);
# 64bit binary message (too large)
$t->websocket_ok('/echo')->send_ok({binary => 'b' x 262145})
->finished_ok(1009);

# Binary message in two 64bit frames without FIN bit (too large)
$t->websocket_ok('/echo')->send_ok([0, 0, 0, 0, 2, 'c' x 100000])
Expand Down Expand Up @@ -238,30 +194,9 @@ $t->websocket_ok('/unicode')->send_ok('hello again')
->send_ok('and one ☃ more time')
->message_ok->message_is('♥: and one ☃ more time')->finish_ok;

# Compression with forced "client_no_context_takeover"
$t->websocket_ok('/no_context_takeover');
ok $t->tx->compressed, 'WebSocket has compression';
ok !$t->tx->context_takeover, 'no context takeover';
$t->send_ok({binary => 'a' x 500})
->message_ok->message_is({binary => 'a' x 500})
->send_ok({binary => 'a' x 500})
->message_ok->message_is({binary => 'a' x 500})->finish_ok;

# Compression denied by the server
$t->websocket_ok('/no_compression');
is $t->tx->req->headers->sec_websocket_extensions, 'permessage-deflate',
'right "Sec-WebSocket-Extensions" value';
ok !$t->tx->compressed, 'WebSocket has no compression';
$t->send_ok({binary => 'a' x 500})
->message_ok->message_is({binary => 'a' x 500})->finish_ok;

# Binary frame and events (no compression)
# Binary frame and events
my $bytes = b("I ♥ Mojolicious")->encode('UTF-16LE')->to_string;
$tx = $t->ua->build_websocket_tx('/bytes');
$tx->req->headers->remove('Sec-WebSocket-Extensions');
$t->request_ok($tx)
->header_isnt('Sec-WebSocket-Extensions' => 'permessage-deflate');
ok !$t->tx->compressed, 'WebSocket has no compression';
$t->websocket_ok('/bytes');
my $binary;
$t->tx->on(
frame => sub {
Expand All @@ -279,12 +214,10 @@ ok !$binary, 'received text frame';
$t->finish_ok(1000 => 'Have a nice day!');
is_deeply $close, [1000, 'Have a nice day!'], 'right status and message';

# Binary roundtrips (no compression)
$tx = $t->ua->build_websocket_tx('/bytes');
$tx->req->headers->remove('Sec-WebSocket-Extensions');
$t->request_ok($tx)->send_ok({binary => $bytes})
->message_ok->message_is($bytes)->send_ok({binary => $bytes})
->message_ok->message_is($bytes)->finish_ok;
# Binary roundtrips
$t->request_ok($t->ua->build_websocket_tx('/bytes'))
->send_ok({binary => $bytes})->message_ok->message_is($bytes)
->send_ok({binary => $bytes})->message_ok->message_is($bytes)->finish_ok;

# Two responses
$t->websocket_ok('/once')->send_ok('hello')
Expand Down

0 comments on commit e98254f

Please sign in to comment.