]> Dogcows Code - chaz/p5-Plack-App-Proxy-WebSocket/blob - lib/Plack/App/Proxy/WebSocket.pm
fixup documentation
[chaz/p5-Plack-App-Proxy-WebSocket] / lib / Plack / App / Proxy / WebSocket.pm
1 package Plack::App::Proxy::WebSocket;
2 # ABSTRACT: proxy HTTP and WebSocket connections
3
4 use warnings FATAL => 'all';
5 use strict;
6
7 use AnyEvent::Handle;
8 use AnyEvent::Socket;
9 use HTTP::Headers;
10 use HTTP::Request;
11 use HTTP::Parser::XS qw/parse_http_response HEADERS_AS_ARRAYREF/;
12 use Plack::Request;
13 use URI;
14
15 use parent 'Plack::App::Proxy';
16
17 =head1 SYNOPSIS
18
19 use Plack::App::Proxy::WebSocket;
20 use Plack::Builder;
21
22 builder {
23 mount "/socket.io" => Plack::App::Proxy::WebSocket->new(
24 remote => "http://localhost:9000/socket.io",
25 preserve_host_header => 1,
26 )->to_app;
27 };
28
29 =head1 DESCRIPTION
30
31 This is a subclass of L<Plack::App::Proxy> that adds support for transparent
32 (i.e. reverse) proxying WebSocket connections. If your proxy is a forward
33 proxy that is to be explicitly configured in the system or browser, you may be
34 able to use L<Plack::Middleware::Proxy::Connect> instead.
35
36 This module works by looking for the C<Upgrade: WebSocket> header, completing
37 the handshake with the remote, and then buffering full-duplex between the
38 client and the remote. Regular requests are handled by L<Plack::App::Proxy>
39 as usual, though there are a few differences related to the generation of
40 headers for the back-end request; see L</build_headers_from_env> for details.
41
42 This module has no configuration options beyond what L<Plack::App::Proxy>
43 requires or provides, so it may be an easy drop-in replacement. Read the
44 documentation of that module for advanced usage not covered here. Also, you
45 must use a L<PSGI> server that supports C<psgi.streaming> and C<psgix.io>.
46 For performance reasons, you should also use a C<psgi.nonblocking> server
47 (like L<Twiggy>) and the L<Plack::App::Proxy::Backend::AnyEvent::HTTP> user
48 agent back-end (which is the default, so no extra configuration is needed).
49
50 This module is B<EXPERIMENTAL>. I use it in development and it works
51 swimmingly for me, but it is completely untested in production scenarios.
52
53 =head1 CAVEATS
54
55 L<Starman> ignores the C<Connection> HTTP response header from applications
56 and chooses its own value (C<Close> or C<Keep-Alive>), but WebSocket clients
57 expect the value of that header to be C<Upgrade>. Therefore, WebSocket
58 proxying does not work on L<Starman>. Your best bet is to use a server that
59 doesn't mess with the C<Connection> header, like L<Twiggy>.
60
61 =cut
62
63 sub call {
64 my ($self, $env) = @_;
65 my $req = Plack::Request->new($env);
66
67 # detect a protocol upgrade handshake or just proxy as usual
68 my $upgrade = $req->header('Upgrade') or return $self->SUPER::call($env);
69
70 $env->{'psgi.streaming'} or die "Plack server support for psgi.streaming is required";
71 my $client_fh = $env->{'psgix.io'} or die "Plack server support for the psgix.io extension is required";
72
73 my $url = $self->build_url_from_env($env) or return [502, [], ["Bad Gateway"]];
74 my $uri = URI->new($url);
75
76 sub {
77 my $res = shift;
78
79 # set up an event loop if the server is blocking
80 my $cv;
81 unless ($env->{'psgi.nonblocking'}) {
82 $env->{'psgi.errors'}->print("Plack server support for psgi.nonblocking is highly recommended.\n");
83 $cv = AE::cv;
84 }
85
86 tcp_connect $uri->host, $uri->port, sub {
87 my $server_fh = shift;
88
89 # return 502 if connection to server fails
90 unless ($server_fh) {
91 $res->([502, [], ["Bad Gateway"]]);
92 $cv->send if $cv;
93 return;
94 }
95
96 my $client = AnyEvent::Handle->new(fh => $client_fh);
97 my $server = AnyEvent::Handle->new(fh => $server_fh);
98
99 # forward request from the client
100 my $headers = $self->build_headers_from_env($env, $req, $uri);
101 $headers->{Upgrade} = $upgrade;
102 $headers->{Connection} = 'Upgrade';
103 my $hs = HTTP::Request->new('GET', $uri->path, HTTP::Headers->new(%$headers));
104 $hs->protocol($req->protocol);
105 $server->push_write($hs->as_string);
106
107 my $buffer = "";
108 my $writer;
109
110 # buffer the exchange between the client and server
111 $client->on_read(sub {
112 my $hdl = shift;
113 my $buf = delete $hdl->{rbuf};
114 $server->push_write($buf);
115 });
116 $server->on_read(sub {
117 my $hdl = shift;
118 my $buf = delete $hdl->{rbuf};
119
120 return $writer->write($buf) if $writer;
121 $buffer .= $buf;
122
123 my ($ret, $http_version, $status, $message, $headers) =
124 parse_http_response($buffer, HEADERS_AS_ARRAYREF);
125 $server->push_shutdown if $ret == -2;
126 return if $ret < 0;
127
128 $headers = [$self->response_headers($headers)] unless $status == 101;
129 $writer = $res->([$status, $headers]);
130 $writer->write(substr($buffer, $ret));
131 $buffer = undef;
132 });
133
134 # shut down the sockets and exit the loop if an error occurs
135 $client->on_error(sub {
136 $client->destroy;
137 $server->push_shutdown;
138 $cv->send if $cv;
139 $writer->close if $writer;
140 });
141 $server->on_error(sub {
142 $server->destroy;
143 # get the client handle's attention
144 $client->push_shutdown;
145 });
146 };
147
148 $cv->recv if $cv;
149 };
150 }
151
152 =method build_headers_from_env
153
154 Supplement the headers-building logic from L<Plack::App::Proxy> to maintain
155 the complete list of proxies in C<X-Forwarded-For> and to set the following
156 headers if they are not already set: C<X-Forwarded-Proto> to the value of
157 C<psgi.url_scheme>, C<X-Real-IP> to the value of C<REMOTE_ADDR>, and C<Host>
158 to the host and port number of a URI (if given).
159
160 This is called internally.
161
162 =cut
163
164 sub build_headers_from_env {
165 my ($self, $env, $req, $uri) = @_;
166
167 my $headers = $self->SUPER::build_headers_from_env($env, $req);
168
169 # if x-forwarded-for already existed, append the remote address; the super
170 # method fails to maintain a list of mutiple proxies
171 if (my $forwarded_for = $env->{HTTP_X_FORWARDED_FOR}) {
172 $headers->{'X-Forwarded-For'} = "$forwarded_for, $env->{REMOTE_ADDR}";
173 }
174
175 # the super method depends on the user agent to add the host header if it
176 # is missing, so set the host if it needs to be set
177 if ($uri && !$headers->{'Host'}) {
178 $headers->{'Host'} = $uri->host_port;
179 }
180
181 $headers->{'X-Forwarded-Proto'} ||= $env->{'psgi.url_scheme'};
182 $headers->{'X-Real-IP'} ||= $env->{REMOTE_ADDR};
183
184 $headers;
185 }
186
187 1;
This page took 0.042215 seconds and 4 git commands to generate.