The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

package MyTest;
use 5.012;
XS::Loader::load();
{
sub log_format {
my ($self, $msg, $level, $module, $file, $line, $func, $formatter) = @_;
$file = substr($file, rindex($file, '/'));
$module = substr($module, rindex($module, '::')+2);
my $code = "$module$file:$line";
my $res = sprintf '%-32s %s', $code, $msg;
say $res;
}
}
if ($ENV{LOGGER}) {
XLog::set_logger(Protocol::WebSocket::Logger->new);
XLog::set_level(XLog::WARNING);
XLog::set_level(XLog::DEBUG, "Protocol::WebSocket");
}
sub accept_packet {
my @data = (
"GET /?encoding=text HTTP/1.1\r\n",
"Host: dev.crazypanda.ru:4680\r\n",
"Connection: Upgrade\r\n",
"Pragma: no-cache\r\n",
"Cache-Control: no-cache\r\n",
"Upgrade: websocket\r\n",
"Origin: http://www.websocket.org\r\n",
"Sec-WebSocket-Version: 13\r\n",
"User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\r\n",
"Accept-Encoding: gzip, deflate, sdch\r\n",
"Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n",
"Cookie: _ga=GA1.2.1700804447.1456741171\r\n",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n",
"Sec-WebSocket-Protocol: chat\r\n",
"\r\n",
);
return wantarray ? @data : join('', @data);
}
sub accept_parsed {
return methods(
headers => {
'pragma' => 'no-cache',
'sec-websocket-protocol' => 'chat',
'upgrade' => 'websocket',
'accept-encoding' => 'gzip, deflate, sdch',
'origin' => 'http://www.websocket.org',
'cache-control' => 'no-cache',
'connection' => 'Upgrade',
'cookie' => '_ga=GA1.2.1700804447.1456741171',
'sec-websocket-key' => 'dGhlIHNhbXBsZSBub25jZQ==',
'host' => 'dev.crazypanda.ru:4680',
'sec-websocket-version' => '13',
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36',
'accept-language' => 'ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4'
},
uri => methods(to_string => '/?encoding=text'),
ws_key => 'dGhlIHNhbXBsZSBub25jZQ==',
ws_protocol => 'chat',
ws_version => 13,
);
}
sub get_established_server {
my $p = Protocol::WebSocket::Fast::ServerParser->new(shift);
_establish_server($p);
return $p;
}
sub _establish_server {
my $p = shift;
$p->accept(scalar accept_packet()) or die "should not happen";
$p->accept_response;
die "should not happen" unless $p->established;
}
sub reset {
my $p = shift;
$p->reset;
die "should not happen" if $p->established;
$p->isa("Protocol::WebSocket::Fast::ServerParser") ? _establish_server($p) : _establish_client($p);
}
sub get_established_client {
my $p = Protocol::WebSocket::Fast::ClientParser->new(shift);
_establish_client($p);
return $p;
}
sub _establish_client {
my $p = shift;
$p->no_deflate;
my $cstr = $p->connect_request({uri => "ws://jopa.ru"});
my $sp = new Protocol::WebSocket::Fast::ServerParser;
$sp->accept($cstr) or die "should not happen";
$sp->accepted or die "should not happen";
my $rstr = $sp->accept_response;
$p->connect($rstr) or die "should not happen";
$p->established or die "should not happen";
return $p;
}
sub gen_frame {
my $params = shift;
my $first = 0;
my $second = 0;
foreach my $p (qw/fin rsv1 rsv2 rsv3/) {
$first |= ($params->{$p} ? 1 : 0);
$first <<= 1;
}
$first <<= 3;
$first |= ($params->{opcode} & 15);
$first = pack("C", $first);
$second |= ($params->{mask} ? 1 : 0);
$second <<= 7;
my $data = $params->{data} // '';
if ($params->{close_code} && !ref $params->{close_code}) {
$data = pack("S>", $params->{close_code}).$data;
}
my $dlen = length($data);
my $extlen = '';
if ($dlen < 126) {
$second |= $dlen;
}
elsif ($dlen < 65536) {
$second |= 126;
$extlen = pack "S>", $dlen;
}
else {
$second |= 127;
$extlen = pack "Q>", $dlen;
}
$second = pack("C", $second);
my $mask = $params->{mask} || '';
my $payload;
if ($mask) {
$mask = (length($mask) == 4) ? $mask : pack("L>", int rand(2**32-1));
$payload = crypt_xor($data, $mask);
} else {
$payload = $data;
}
my $frame = $first.$second.$extlen.$mask.$payload;
return $frame;
}
sub gen_message {
my $params = shift;
my $nframes = $params->{nframes} || 1;
my $payload = $params->{data} // '';
my $opcode = $params->{opcode} // OPCODE_TEXT;
my @bin;
my $frames_left = $nframes;
while ($frames_left) {
my $curlen = (length($payload) / $frames_left--);
my $chunk = substr($payload, 0, $curlen, '');
push @bin, gen_frame({
opcode => $opcode,
data => $chunk,
fin => !length($payload),
mask => $params->{mask},
});
$opcode = OPCODE_CONTINUE;
}
return wantarray ? @bin : join('', @bin);
}
sub is_bin {
my ($got, $expected, $name) = @_;
return if our $leak_test;
state $has_binary = eval { require Test::BinaryData; Test::BinaryData->import(); 1 };
$has_binary ? is_binary($got, $expected, $name) : is($got, $expected, $name);
}
sub crypt_xor {
my ($data, $mask) = @_;
my @key = unpack("C*", $mask);
my $result = pack("C*", map { my $c = shift @key; push @key, $c; $_ ^ $c } unpack("C*", $data));
return $result;
}
sub test_frame {
my ($p, $frame_data, $error, $suggested_close_code) = @_;
my $bin = gen_frame($frame_data);
my $check_data = {};
$check_data->{opcode} = $frame_data->{opcode};
$check_data->{is_control} = ($check_data->{opcode} >= OPCODE_CLOSE);
$check_data->{final} = $frame_data->{fin} || '';
$check_data->{payload_length} = length($frame_data->{data}//'');
$check_data->{payload} = $frame_data->{data};
$check_data->{close_code} = $frame_data->{close_code} if exists $frame_data->{close_code};
subtest 'whole buffer' => sub {
my @frames = $p->get_frames($bin);
my ($frame) = @frames;
ok(scalar(@frames) == 1 && $frame, "one frame returned");
if ($error) {
is($frame->error, $error, "frame parsing error: $error");
is $p->suggested_close_code, $suggested_close_code, "suggested close code: $suggested_close_code" if $suggested_close_code;
&reset($p);
} else {
is($frame->error, undef, "no errors");
cmp_deeply($frame, methods(%$check_data), "frame properties ok");
}
};
&reset($p) if $check_data->{opcode} == OPCODE_CLOSE;
subtest 'buffer by char' => sub {
my $it;
while (length($bin) && !$it) { $it = $p->get_frames(substr($bin, 0, 1, '')); }
my $frame = $it->next;
ok($frame && !$bin && !$it->next, "got frame on last char") unless $error;
if ($error) {
is($frame->error, $error, "frame parsing error: $error");
is $p->suggested_close_code, $suggested_close_code, "suggested close code: $suggested_close_code" if $suggested_close_code;
&reset($p);
} else {
is($frame->error, undef, "no errors");
cmp_deeply($frame, methods(%$check_data), "frame properties ok");
}
}
}
sub test_message {
my ($p, $message_data, $error) = @_;
my $opcode = $message_data->{opcode} // OPCODE_TEXT;
my $nframes = $message_data->{nframes} //= 1;
my $check_data = {};
$check_data->{opcode} = $opcode;
$check_data->{is_control} = ($opcode >= OPCODE_CLOSE);
$check_data->{payload_length} = length($message_data->{data}//'');
$check_data->{payload} = $message_data->{data};
$check_data->{close_code} = $message_data->{close_code} if exists $message_data->{close_code};
$check_data->{frame_count} = $nframes;
my $bin;
if ($opcode >= OPCODE_CLOSE) {
$bin = gen_frame($message_data);
} else {
$bin = gen_message($message_data);
}
subtest 'whole buffer' => sub {
my @messages = $p->get_messages($bin);
my ($message) = @messages;
ok(scalar(@messages) == 1 && $message, "one message returned");
if ($error) {
is($message->error, $error, "message parsing error: $error");
&reset($p);
} else {
is($message->error, undef, "no errors");
cmp_deeply($message, methods(%$check_data), "message properties ok");
}
};
&reset($p) if $check_data->{opcode} == OPCODE_CLOSE;
my $message;
subtest 'buffer by char' => sub {
my $it;
while (length($bin) && !$it) { $it = $p->get_messages(substr($bin, 0, 1, '')); }
$message = $it->next;
ok($message && !$bin && !$it->next, "got message on last char") unless $error;
if ($error) {
is($message->error, $error, "message parsing error: $error");
&reset($p);
} else {
is($message->error, undef, "no errors");
cmp_deeply($message, methods(%$check_data), "message properties ok");
}
};
return $message;
}
1;