1 package Plack
::App
::Proxy
::WebSocket
;
2 # ABSTRACT: proxy HTTP and WebSocket connections
4 use warnings FATAL
=> 'all';
11 use HTTP
::Parser
::XS qw
/parse_http_response HEADERS_AS_ARRAYREF/;
15 use parent
'Plack::App::Proxy';
19 use Plack::App::Proxy::WebSocket;
23 mount "/socket.io" => Plack::App::Proxy::WebSocket->new(
24 remote => "http://localhost:9000/socket.io",
25 preserve_host_header => 1,
31 This is a subclass of L<Plack::App::Proxy> that adds support for proxying
32 WebSocket connections. It works by looking for the C<Upgrade> header,
33 forwarding the handshake to the remote back-end, and then buffering
34 full-duplex between the client and the remote. Regular HTTP requests are
35 handled by L<Plack::App::Proxy> as usual, though there are a few differences
36 related to the generation of headers for the back-end request; see
37 L</build_headers_from_env> for details.
39 This module has no configuration options beyond what L<Plack::App::Proxy>
40 requires or provides, so it may be an easy drop-in replacement. Read the
41 documentation of that module for advanced usage not covered here. Also note
42 that extra L<PSGI> server features are required in order for the WebSocket
43 proxying to work. The server must support C<psgi.streaming> and C<psgix.io>.
44 It is also highly recommended that you choose a C<psgi.nonblocking> server,
45 though that isn't strictly required. L<Twiggy> is one good choice for this
48 This module is B<EXPERIMENTAL>. I use it in development and it works
49 swimmingly for me, but it is completely untested in production scenarios.
53 L<Starman> ignores the C<Connection> HTTP response header from applications
54 and chooses its own value (C<Close> or C<Keep-Alive>), but WebSocket clients
55 expect the value of that header to be C<Upgrade>. Therefore, WebSocket
56 proxying does not work on L<Starman>. Your best bet is to use a server that
57 doesn't mess with the C<Connection> header, like L<Twiggy>.
62 my ($self, $env) = @_;
63 my $req = Plack
::Request-
>new($env);
65 # detect a protocol upgrade handshake or just proxy as usual
66 my $upgrade = $req->header('Upgrade') or return $self->SUPER::call
($env);
68 $env->{'psgi.streaming'} or die "Plack server support for psgi.streaming is required";
69 my $client_fh = $env->{'psgix.io'} or die "Plack server support for the psgix.io extension is required";
71 my $url = $self->build_url_from_env($env) or return [502, [], ["Bad Gateway"]];
72 my $uri = URI-
>new($url);
77 # set up an event loop if the server is blocking
79 unless ($env->{'psgi.nonblocking'}) {
80 $env->{'psgi.errors'}->print("Plack server support for psgi.nonblocking is highly recommended.\n");
84 tcp_connect
$uri->host, $uri->port, sub {
85 my $server_fh = shift;
87 # return 502 if connection to server fails
89 $res->([502, [], ["Bad Gateway"]]);
94 my $client = AnyEvent
::Handle-
>new(fh
=> $client_fh);
95 my $server = AnyEvent
::Handle-
>new(fh
=> $server_fh);
97 # forward request from the client
98 my $headers = $self->build_headers_from_env($env, $req, $uri);
99 $headers->{Upgrade
} = $upgrade;
100 $headers->{Connection
} = 'Upgrade';
101 my $hs = HTTP
::Request-
>new('GET', $uri->path, HTTP
::Headers-
>new(%$headers));
102 $hs->protocol($req->protocol);
103 $server->push_write($hs->as_string);
108 # buffer the exchange between the client and server
109 $client->on_read(sub {
111 my $buf = delete $hdl->{rbuf
};
112 $server->push_write($buf);
114 $server->on_read(sub {
116 my $buf = delete $hdl->{rbuf
};
118 return $writer->write($buf) if $writer;
121 my ($ret, $http_version, $status, $message, $headers) =
122 parse_http_response
($buffer, HEADERS_AS_ARRAYREF
);
123 $server->push_shutdown if $ret == -2;
126 $headers = [$self->response_headers($headers)] unless $status == 101;
127 $writer = $res->([$status, $headers]);
128 $writer->write(substr($buffer, $ret));
132 # shut down the sockets and exit the loop if an error occurs
133 $client->on_error(sub {
135 $server->push_shutdown;
137 $writer->close if $writer;
139 $server->on_error(sub {
141 # get the client handle's attention
142 $client->push_shutdown;
150 =method build_headers_from_env
152 Supplement the headers-building logic from L
<Plack
::App
::Proxy
> to maintain
153 the complete list of proxies
in C
<X-Forwarded-For
> and to set the following
154 headers
if they are
not already set
: C
<X-Forwarded-Proto
> to the value of
155 C
<psgi
.url_scheme
>, C
<X-Real-IP
> to the value of C
<REMOTE_ADDR
>, and C
<Host
>
156 to the host
and port number of a URI
(if given).
158 This
is called internally
.
162 sub build_headers_from_env
{
163 my ($self, $env, $req, $uri) = @_;
165 my $headers = $self->SUPER::build_headers_from_env
($env, $req);
167 # if x-forwarded-for already existed, append the remote address; the super
168 # method fails to maintain a list of mutiple proxies
169 if (my $forwarded_for = $env->{HTTP_X_FORWARDED_FOR
}) {
170 $headers->{'X-Forwarded-For'} = "$forwarded_for, $env->{REMOTE_ADDR}";
173 # the super method depends on the user agent to add the host header if it
174 # is missing, so set the host if it needs to be set
175 if ($uri && !$headers->{'Host'}) {
176 $headers->{'Host'} = $uri->host_port;
179 $headers->{'X-Forwarded-Proto'} ||= $env->{'psgi.url_scheme'};
180 $headers->{'X-Real-IP'} ||= $env->{REMOTE_ADDR
};