use AnyEvent::Handle;
use AnyEvent::Socket;
+use HTTP::Headers;
use HTTP::Request;
-use HTTP::Response;
+use HTTP::Parser::XS qw/parse_http_response HEADERS_AS_HASHREF/;
use Plack::Request;
use URI;
=head1 DESCRIPTION
-This is a subclass of L<Plack::App::Proxy> that adds support for proxying
-WebSocket connections. It has no extra dependencies or 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.
-
-This subclass necessarily requires extra L<PSGI> server features in order to
-work. The server must support C<psgi.streaming> and C<psgix.io>. It is also
-highly recommended to choose a C<psgi.nonblocking> server, though that isn't
-strictly required; performance may suffer greatly without it. L<Twiggy> is an
-excellent choice for this application.
+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 CAVEATS
-Some servers (e.g. L<Starman>) ignore the C<Connection> HTTP response header
-and use their own values, but WebSocket clients expect the value of that
-header to be C<Upgrade>. This module cannot work on such servers. Your best
-bet is to use a non-blocking server like L<Twiggy> that doesn't mess with the
-C<Connection> header.
+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>.
=cut
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
- # detect the websocket handshake or just proxy as usual
- lc($req->header('Upgrade') || "") eq 'websocket' or return $self->SUPER::call($env);
+ # detect a protocol upgrade handshake or just proxy as usual
+ my $upgrade = $req->header('Upgrade') or return $self->SUPER::call($env);
$env->{'psgi.streaming'} or die "Plack server support for psgi.streaming is required";
my $client_fh = $env->{'psgix.io'} or die "Plack server support for the psgix.io extension is required";
my $client = AnyEvent::Handle->new(fh => $client_fh);
my $server = AnyEvent::Handle->new(fh => $server_fh);
- # forward request from the client, modifying the host and origin
- my $headers = $req->headers->clone;
- my $host = $uri->host_port;
- $headers->header(Host => $host, Origin => "http://$host");
- my $hs = HTTP::Request->new('GET', $uri->path, $headers);
+ # forward request from the client
+ my $headers = $self->build_headers_from_env($env, $req, $uri);
+ $headers->{Upgrade} = $upgrade;
+ $headers->{Connection} = 'Upgrade';
+ my $hs = HTTP::Request->new('GET', $uri->path, HTTP::Headers->new(%$headers));
$hs->protocol($req->protocol);
$server->push_write($hs->as_string);
my $hdl = shift;
my $buf = delete $hdl->{rbuf};
- if ($writer) {
- $writer->write($buf);
- return;
- }
-
- if (($buffer .= $buf) =~ s/^(.+\r?\n\r?\n)//s) {
- my $http = HTTP::Response->parse($1);
- my @headers;
- $http->headers->remove_header('Status');
- $http->headers->scan(sub { push @headers, @_ });
- $writer = $res->([$http->code, [@headers]]);
- $writer->write($buffer) if $buffer;
- $buffer = undef;
- }
+ return $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;
+
+ $headers = [$self->response_headers(HTTP::Headers->new(%$headers))] unless $status == 101;
+ $writer = $res->([$status, $headers]);
+ $writer->write(substr($buffer, $ret));
+ $buffer = undef;
});
# shut down the sockets and exit the loop if an error occurs
$server->on_error(sub {
$server->destroy;
# get the client handle's attention
- $writer->close;
+ $client->push_shutdown;
});
};
$cv->recv if $cv;
};
-};
+}
+
+=method 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.
+
+=cut
+
+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 mutiple 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;
+}
1;