Plack-App-Proxy-WebSocket

 view release on metacpan or  search on metacpan

lib/Plack/App/Proxy/WebSocket.pm  view on Meta::CPAN


            my $buffer = "";
            my $writer;

            # buffer the exchange between the client and server
            $client->on_read(sub {
                my $hdl = shift;
                my $buf = delete $hdl->{rbuf};
                $server->push_write($buf);
            });
            $server->on_read(sub {
                my $hdl = shift;
                my $buf = delete $hdl->{rbuf};

                return eval { $writer->write($buf) } if $writer;
                $buffer .= $buf;

                my ($ret, $http_version, $status, $message, $headers) =
                    parse_http_response($buffer, HEADERS_AS_HASHREF);
                $server->push_shutdown if $ret == -2;
                return if $ret < 0;

                if ($status == 101) {
                    $headers = [$self->switching_response_headers(HTTP::Headers->new(%$headers))];
                }
                else {
                    $headers = [$self->response_headers(HTTP::Headers->new(%$headers))];
                }
                $writer = $res->([$status, $headers]);
                eval { $writer->write(substr($buffer, $ret)) };
                $buffer = undef;
            });

            # shut down the sockets and exit the loop if an error occurs
            $client->on_error(sub {
                $client->destroy;
                $server->push_shutdown;
                $cv->send if $cv;
                $writer->close if $writer;
            });
            $server->on_error(sub {
                $server->destroy;
                # get the client handle's attention
                $client->push_shutdown;
            });
        };

        $cv->recv if $cv;
    };
}


sub build_headers_from_env {
    my ($self, $env, $req, $uri) = @_;

    my $headers = $self->SUPER::build_headers_from_env($env, $req);

    # if x-forwarded-for already existed, append the remote address; the super
    # method fails to maintain a list of multiple proxies
    if (my $forwarded_for = $env->{HTTP_X_FORWARDED_FOR}) {
        $headers->{'X-Forwarded-For'} = "$forwarded_for, $env->{REMOTE_ADDR}";
    }

    # the super method depends on the user agent to add the host header if it
    # is missing, so set the host if it needs to be set
    if ($uri && !$headers->{'Host'}) {
        $headers->{'Host'} = $uri->host_port;
    }

    $headers->{'X-Forwarded-Proto'} ||= $env->{'psgi.url_scheme'};
    $headers->{'X-Real-IP'} ||= $env->{REMOTE_ADDR};

    $headers;
}


sub switching_response_headers {
    my ($self, $headers) = @_;

    # Save Connection and related headers
    my @connection_tokens = $headers->header('Connection');
    my @other_headers = map { $_ => $headers->header($_) } @connection_tokens;

    $self->filter_headers( $headers );

    # Remove PSGI forbidden headers
    $headers->remove_header('Status');

    # Add Connection and other headers listed in Connection back in
    $headers->push_header('Connection' => \@connection_tokens, @other_headers);

    my @headers;
    $headers->scan( sub { push @headers, @_ } );

    return @headers;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Plack::App::Proxy::WebSocket - proxy HTTP and WebSocket connections

=head1 VERSION

version 0.04

=head1 SYNOPSIS

    use Plack::App::Proxy::WebSocket;
    use Plack::Builder;

    builder {
        mount "/socket.io" => Plack::App::Proxy::WebSocket->new(
            remote               => "http://localhost:9000/socket.io",
            preserve_host_header => 1,
        )->to_app;
    };

=head1 DESCRIPTION

This is a subclass of L<Plack::App::Proxy> that adds support for transparent
(i.e. reverse) proxying WebSocket connections.  If your proxy is a forward
proxy that is to be explicitly configured in the system or browser, you may be
able to use L<Plack::Middleware::Proxy::Connect> instead.

This module works by looking for the C<Upgrade: WebSocket> header, completing
the handshake with the remote, and then buffering full-duplex between the
client and the remote.  Regular requests are handled by L<Plack::App::Proxy>
as usual, though there are a few differences related to the generation of
headers for the back-end request; see L</build_headers_from_env> for details.

This module has no configuration options beyond what L<Plack::App::Proxy>
requires or provides, so it may be an easy drop-in replacement.  Read the
documentation of that module for advanced usage not covered here.  Also, you
must use a L<PSGI> server that supports C<psgi.streaming> and C<psgix.io>.
For performance reasons, you should also use a C<psgi.nonblocking> server
(like L<Twiggy>) and the L<Plack::App::Proxy::Backend::AnyEvent::HTTP> user
agent back-end (which is the default, so no extra configuration is needed).

This module is B<EXPERIMENTAL>.  I use it in development and it works
swimmingly for me, but it is completely untested in production scenarios.

=head1 METHODS

=head2 build_headers_from_env

Supplement the headers-building logic from L<Plack::App::Proxy> to maintain
the complete list of proxies in C<X-Forwarded-For> and to set the following
headers if they are not already set: C<X-Forwarded-Proto> to the value of
C<psgi.url_scheme>, C<X-Real-IP> to the value of C<REMOTE_ADDR>, and C<Host>
to the host and port number of a URI (if given).

This is called internally.

=head2 switching_response_headers

Like C<response_headers> from L<Plack::App::Proxy> but doesn't filter the
"Connection" header nor the headers listed by the "Connection" header.

=head1 CAVEATS

L<Starman> ignores the C<Connection> HTTP response header from applications
and chooses its own value (C<Close> or C<Keep-Alive>), but WebSocket clients
expect the value of that header to be C<Upgrade>.  Therefore, WebSocket
proxying does not work on L<Starman>.  Your best bet is to use a server that
doesn't mess with the C<Connection> header, like L<Twiggy>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website
L<https://github.com/chazmcgarvey/p5-Plack-App-Proxy-WebSocket/issues>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 AUTHOR

Charles McGarvey <chazmcgarvey@brokenzipper.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2018 by Charles McGarvey.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut



( run in 1.209 second using v1.01-cache-2.11-cpan-39bf76dae61 )