PAGI-Server
view release on metacpan or search on metacpan
t/http2/07-websocket.t view on Meta::CPAN
};
# ============================================================
# Extended CONNECT â app receives websocket scope
# ============================================================
subtest 'Extended CONNECT creates websocket scope' => sub {
my @scopes;
my $app = async sub {
my ($scope, $receive, $send) = @_;
push @scopes, $scope;
if ($scope->{type} eq 'websocket') {
# Accept the WebSocket connection
await $send->({
type => 'websocket.accept',
});
# Receive connect event
my $event = await $receive->();
# Wait for disconnect
while ($event->{type} ne 'websocket.disconnect') {
$event = await $receive->();
}
}
};
my ($conn, $stream, $client_sock, $server) = create_h2_connection(app => $app);
my %response_headers;
my $response_data = '';
my $client = create_client(
on_header => sub {
my ($sid, $name, $value) = @_;
$response_headers{$name} = $value;
return 0;
},
on_data_chunk_recv => sub {
my ($sid, $data) = @_;
$response_data .= $data;
return 0;
},
);
complete_h2_handshake($client, $client_sock);
# Send Extended CONNECT for WebSocket (RFC 8441)
$client->submit_request(
method => 'CONNECT',
path => '/ws/chat',
scheme => 'https',
authority => 'localhost',
headers => [
[':protocol', 'websocket'],
['sec-websocket-version', '13'],
['sec-websocket-protocol', 'chat, superchat'],
['origin', 'https://localhost'],
],
body => sub { return undef }, # streaming: keep open
);
$client_sock->syswrite($client->mem_send);
exchange_frames($client, $client_sock);
ok(scalar @scopes >= 1, 'App was called');
if (@scopes) {
my $scope = $scopes[0];
is($scope->{type}, 'websocket', 'scope type is websocket');
is($scope->{http_version}, '2', 'http_version is 2');
is($scope->{path}, '/ws/chat', 'path is /ws/chat');
is($scope->{scheme}, 'ws', 'scheme is ws (WebSocket, no TLS in test)');
ok(ref $scope->{headers} eq 'ARRAY', 'headers is array');
ok(ref $scope->{subprotocols} eq 'ARRAY', 'subprotocols is array');
is(scalar @{$scope->{subprotocols}}, 2, 'Two subprotocols');
is($scope->{subprotocols}[0], 'chat', 'First subprotocol');
is($scope->{subprotocols}[1], 'superchat', 'Second subprotocol');
}
# Server should have responded with 200 (not 101)
is($response_headers{':status'}, '200', 'Server responded with 200');
$stream->close_now;
$loop->remove($server);
};
# ============================================================
# Bidirectional text message exchange
# ============================================================
subtest 'Bidirectional text message exchange' => sub {
my @received_messages;
my $app = async sub {
my ($scope, $receive, $send) = @_;
if ($scope->{type} eq 'websocket') {
await $send->({ type => 'websocket.accept' });
# Read connect event
my $event = await $receive->();
next unless $event->{type} eq 'websocket.connect';
# Echo loop
while (1) {
$event = await $receive->();
if ($event->{type} eq 'websocket.receive') {
push @received_messages, $event;
# Echo back
await $send->({
type => 'websocket.send',
text => "echo: $event->{text}",
});
}
elsif ($event->{type} eq 'websocket.disconnect') {
last;
}
}
}
};
my ($conn, $stream_io, $client_sock, $server) = create_h2_connection(app => $app);
my %response_headers;
my $ws_data = ''; # Raw DATA frames received on the WS stream
my $client = create_client(
on_header => sub {
my ($sid, $name, $value) = @_;
$response_headers{$name} = $value;
return 0;
},
on_data_chunk_recv => sub {
my ($sid, $data) = @_;
$ws_data .= $data;
return 0;
},
);
complete_h2_handshake($client, $client_sock);
# Send Extended CONNECT
my $ws_stream_id = $client->submit_request(
method => 'CONNECT',
path => '/ws/echo',
scheme => 'https',
authority => 'localhost',
headers => [
[':protocol', 'websocket'],
['sec-websocket-version', '13'],
],
body => sub { return undef }, # streaming: keep open
);
$client_sock->syswrite($client->mem_send);
# Wait for 200 response
exchange_frames($client, $client_sock);
is($response_headers{':status'}, '200', 'Got 200 for WebSocket accept');
# Send a WebSocket text frame via HTTP/2 DATA
my $ws_frame = Protocol::WebSocket::Frame->new(
buffer => 'Hello WebSocket',
type => 'text',
masked => 1,
);
my $frame_bytes = $ws_frame->to_bytes;
send_stream_data($client, $client_sock, $ws_stream_id, $frame_bytes);
# Read the echo response
$ws_data = '';
exchange_frames($client, $client_sock);
# Parse the server's WebSocket frame from the DATA
ok(length($ws_data) > 0, 'Received data from server');
if (length($ws_data) > 0) {
my $response_frame = Protocol::WebSocket::Frame->new;
$response_frame->append($ws_data);
my $text = $response_frame->next_bytes;
is($text, 'echo: Hello WebSocket', 'Got echoed message');
}
ok(scalar @received_messages >= 1, 'Server received message');
is($received_messages[0]{text}, 'Hello WebSocket', 'Server got correct text');
$stream_io->close_now;
$loop->remove($server);
};
# ============================================================
# Close handshake
# ============================================================
subtest 'WebSocket close handshake over HTTP/2' => sub {
my $disconnect_event;
my $app = async sub {
my ($scope, $receive, $send) = @_;
if ($scope->{type} eq 'websocket') {
await $send->({ type => 'websocket.accept' });
my $event = await $receive->(); # websocket.connect
# Wait for disconnect
while (1) {
$event = await $receive->();
if ($event->{type} eq 'websocket.disconnect') {
$disconnect_event = $event;
last;
}
}
}
};
my ($conn, $stream_io, $client_sock, $server) = create_h2_connection(app => $app);
my %response_headers;
my $ws_data = '';
my $stream_closed = 0;
my $client = create_client(
on_header => sub {
my ($sid, $name, $value) = @_;
$response_headers{$name} = $value;
return 0;
},
on_data_chunk_recv => sub {
my ($sid, $data) = @_;
$ws_data .= $data;
return 0;
},
on_stream_close => sub {
$stream_closed = 1;
return 0;
},
);
complete_h2_handshake($client, $client_sock);
# Open WebSocket
my $ws_stream_id = $client->submit_request(
method => 'CONNECT',
path => '/ws/close-test',
scheme => 'https',
authority => 'localhost',
headers => [
[':protocol', 'websocket'],
['sec-websocket-version', '13'],
],
body => sub { return undef }, # streaming: keep open
);
$client_sock->syswrite($client->mem_send);
exchange_frames($client, $client_sock);
is($response_headers{':status'}, '200', 'WebSocket accepted');
# Send close frame (code=1000, reason="normal closure")
my $close_frame = Protocol::WebSocket::Frame->new(
type => 'close',
buffer => pack('n', 1000) . 'normal closure',
masked => 1,
);
send_stream_data($client, $client_sock, $ws_stream_id, $close_frame->to_bytes);
exchange_frames($client, $client_sock);
# Server should have received the disconnect event
ok(defined $disconnect_event, 'Server got disconnect event');
if ($disconnect_event) {
is($disconnect_event->{code}, 1000, 'Close code is 1000');
is($disconnect_event->{reason}, 'normal closure', 'Close reason matches');
}
# Server should have sent close frame back
if (length($ws_data) > 0) {
my $frame = Protocol::WebSocket::Frame->new;
$frame->append($ws_data);
my $bytes = $frame->next_bytes;
# Close frame echo
ok(defined $bytes, 'Server sent close frame response');
}
$stream_io->close_now;
$loop->remove($server);
};
done_testing;
( run in 0.631 second using v1.01-cache-2.11-cpan-995e09ba956 )