PAGI-Server
view release on metacpan or search on metacpan
t/52-mandatory-validation.t view on Meta::CPAN
PeerAddr => '127.0.0.1', PeerPort => $sse_port, Proto => 'tcp', Timeout => 5,
);
ok($sock, 'raw socket connected') or last;
$sock->blocking(0);
my $read_until = sub {
my ($stop) = @_;
my $buf = '';
my $deadline = time + 5;
while (time < $deadline) {
my $chunk;
my $n = sysread($sock, $chunk, 4096);
if (defined $n && $n > 0) { $buf .= $chunk }
elsif (defined $n && $n == 0) { last } # EOF
last if $buf =~ $stop;
$loop->loop_once(0.02);
}
return $buf;
};
print $sock "GET /sse-newline-event HTTP/1.1\r\nHost: 127.0.0.1:$sse_port\r\n"
. "Accept: text/event-stream\r\n\r\n";
my $stream = $read_until->(qr/\r\n0\r\n\r\n/);
like($stream, qr/must not contain newline/, 'SSE exchange completed on the raw socket');
like($stream, qr/\r\n0\r\n\r\n/, 'SSE stream ended with the chunked terminator');
print $sock "GET /plain HTTP/1.1\r\nHost: 127.0.0.1:$sse_port\r\n\r\n";
like($read_until->(qr/PLAIN-OK/), qr/^HTTP\/1\.1 200.*PLAIN-OK/s,
'ordinary request served on the SAME socket after the SSE stream');
close $sock;
}
$sse_server->shutdown->get;
$loop->remove($sse_client);
# --- WebSocket: shape and mis-sequencing errors on the send path ---
my $ws_app = async sub {
my ($scope, $receive, $send) = @_;
if ($scope->{type} eq 'websocket') {
my $e = await $receive->(); # websocket.connect
my $err1 = do { local $@; eval { await $send->({ type => 'websocket.send', bytes => 'a', text => 'b' }) }; $@ };
($WsErrs::SHAPE = $err1 // 'NO-ERROR') =~ s/\n/ /g;
my $err2 = do { local $@; eval { await $send->({ type => 'websocket.keepalive', interval => 1 }) }; $@ };
($WsErrs::SEQ = $err2 // 'NO-ERROR') =~ s/\n/ /g;
await $send->({ type => 'websocket.accept' });
await $send->({ type => 'websocket.close' });
return;
}
die "unsupported scope $scope->{type}";
};
my $ws_server = PAGI::Server->new(app => $ws_app, host => '127.0.0.1', port => 0, quiet => 1, validate_events => 0);
$loop->add($ws_server);
$ws_server->listen->get;
my $ws_port = $ws_server->port;
# Raw handshake boilerplate lifted from t/04-websocket.t: the app under test
# reads/writes application-level events, so a raw socket lets us drive the
# handshake without a WebSocket client library getting in the way. Pumps the
# loop until the server closes the connection, so that whatever the app
# coroutine recorded in its package vars is settled by the time we assert.
use IO::Socket::INET;
my $ws_handshake_and_drain = sub {
my ($port) = @_;
my $sock = IO::Socket::INET->new(
PeerAddr => '127.0.0.1',
PeerPort => $port,
Proto => 'tcp',
Timeout => 5,
);
return unless $sock;
my $key = 'dGhlIHNhbXBsZSBub25jZQ==';
print $sock "GET / HTTP/1.1\r\n";
print $sock "Host: 127.0.0.1:$port\r\n";
print $sock "Upgrade: websocket\r\n";
print $sock "Connection: Upgrade\r\n";
print $sock "Sec-WebSocket-Key: $key\r\n";
print $sock "Sec-WebSocket-Version: 13\r\n";
print $sock "\r\n";
$sock->blocking(0);
my $deadline = time + 5;
while (time < $deadline) {
my $buf;
my $n = sysread($sock, $buf, 4096);
last if defined($n) && $n == 0; # EOF: server closed
$loop->loop_once(0.1);
}
close $sock;
return 1;
};
SKIP: {
skip "Cannot connect", 2 unless $ws_handshake_and_drain->($ws_port);
like( $WsErrs::SHAPE // '', qr/exactly one of bytes\/text/,
'websocket.send with both bytes and text fails shape validation' );
like( $WsErrs::SEQ // '', qr/cannot send 'websocket\.keepalive'/,
'websocket.keepalive before accept fails sequencing' );
}
$ws_server->shutdown->get;
# --- WebSocket: sends after a terminal state raise, not swallowed ---
# Same class of bug as the SSE carve-out above: websocket.close and a
# completed denial response both flip the connection's {closed} flag as a
# side effect (see _handle_disconnect_and_close), independent of a genuine
# client disconnect. A post-close/post-denial-complete send must still raise
# through advance_websocket, not silently no-op on the transport-closed check.
my $ws_after_close_app = async sub {
my ($scope, $receive, $send) = @_;
if ($scope->{type} eq 'websocket') {
my $e = await $receive->(); # websocket.connect
await $send->({ type => 'websocket.accept' });
# App-initiated close with no client close received: the transport
# isn't gone for real, only the app's own logical close.
await $send->({ type => 'websocket.close' });
my $err = do { local $@; eval { await $send->({ type => 'websocket.send', text => 'late' }) }; $@ };
( run in 1.959 second using v1.01-cache-2.11-cpan-364913b4093 )