File Coverage

blib/lib/Test/HTTP/AnyEvent/Server.pm
Criterion Covered Total %
statement 173 186 93.0
branch 29 34 85.2
condition 3 8 37.5
subroutine 29 31 93.5
pod 2 4 50.0
total 236 263 89.7


line stmt bran cond sub pod time code
1             package Test::HTTP::AnyEvent::Server;
2             # ABSTRACT: the async counterpart to Test::HTTP::Server
3              
4              
5 7     7   557130 use feature qw(state switch);
  7         16  
  7         823  
6 7     7   44 use strict;
  7         11  
  7         286  
7 7     7   42 use utf8;
  7         15  
  7         80  
8 7     7   177 use warnings qw(all);
  7         14  
  7         464  
9              
10 7     7   3752 use AnyEvent;
  7         14316  
  7         200  
11 7     7   7158 use AnyEvent::Handle;
  7         74995  
  7         242  
12 7     7   8503 use AnyEvent::Log;
  7         100354  
  7         353  
13 7     7   9524 use AnyEvent::Socket;
  7         134595  
  7         1378  
14 7     7   97 use AnyEvent::Util;
  7         17  
  7         668  
15 7     7   8522 use HTTP::Headers;
  7         71010  
  7         311  
16 7     7   7252 use HTTP::Request;
  7         126374  
  7         305  
17 7     7   6969 use HTTP::Response;
  7         47406  
  7         389  
18 7     7   70 use HTTP::Status;
  7         15  
  7         2875  
19 7     7   7280 use Moo;
  7         142393  
  7         84  
20 7     7   29287 use MooX::Types::MooseLike::Base qw(:all);
  7         64861  
  7         3840  
21 7     7   84 use POSIX;
  7         13  
  7         75  
22              
23 7     7   34733 no if ($] >= 5.017010), warnings => q(experimental);
  7         77  
  7         51  
24              
25             #$AnyEvent::Log::FILTER->level('debug');
26              
27             our $VERSION = '0.013'; # VERSION
28              
29             my %pool;
30              
31              
32             has address => (is => 'ro', isa => Str, default => sub { '127.0.0.1' }, writer => 'set_address');
33              
34              
35             has port => (is => 'ro', isa => Int, writer => 'set_port');
36              
37              
38             has maxconn => (is => 'ro', isa => Int, default => sub { 10 });
39              
40              
41             has timeout => (is => 'ro', isa => Int, default => sub { 60 });
42              
43              
44             has disable_proxy => (is => 'ro', isa => Bool, default => sub { 1 });
45              
46              
47             has https => (is => 'ro', isa => HashRef);
48              
49              
50             has custom_handler => (is => 'ro', isa => CodeRef);
51              
52              
53             has forked => (is => 'ro', isa => Bool, default => sub { 0 });
54              
55              
56             has forked_pid => (is => 'ro', isa => Int, writer => 'set_forked_pid');
57              
58              
59             has server => (is => 'ro', isa => Ref, writer => 'set_server');
60              
61              
62             sub BUILD {
63 5     5 0 237 my ($self) = @_;
64              
65             ## no critic (RequireLocalizedPunctuationVars)
66 5 100       126 @ENV{qw(no_proxy http_proxy https_proxy ftp_proxy all_proxy)} = (q(localhost,127.0.0.1), (q()) x 4)
67             if $self->disable_proxy;
68              
69 5 100       41 unless ($self->forked) {
70             $self->set_server(
71             $self->start_server(sub {
72 4     4   863 my (undef, $address, $port) = @_;
73 4         24 $self->set_address($address);
74 4         2311 $self->set_port($port);
75 4         2276 AE::log info =>
76             'bound to ' . $self->uri;
77             })
78 4         42 );
79             } else {
80 1         9 my ($rh, $wh) = portable_pipe;
81              
82 1         1529 given (fork) {
83 1         60 when (undef) {
84 0         0 AE::log fatal =>
85             "couldn't fork(): $!";
86 1         27 } when (0) {
87             # child
88 0         0 close $rh;
89              
90             my $h = AnyEvent::Handle->new(
91             fh => $wh,
92             on_error => sub {
93 0     0   0 AE::log fatal =>
94             "couldn't syswrite() to pipe: $!";
95             },
96 0         0 );
97              
98             $self->set_server(
99             $self->start_server(sub {
100 0     0   0 my (undef, $address, $port) = @_;
101             # have to postpone so the address/port gets actually bound
102 0         0 AE::postpone { $h->push_write(join("\t", $address, $port)) };
  0         0  
103             })
104 0         0 );
105              
106 0         0 AE::cv->wait;
107 0         0 POSIX::_exit(0);
108 0         0 exit 1;
109 1         14 } default {
110             # parent
111 1         18 my $pid = $_;
112 1         49 close $wh;
113              
114 1         1 my $buf;
115 1         19007 my $len = sysread $rh, $buf, 65536;
116 1 50 33     82 AE::log fatal =>
117             "couldn't sysread() from pipe: $!"
118             if not defined $len or not $len;
119              
120 1         14 my ($address, $port) = split m{\t}x, $buf;
121 1         83 $self->set_address($address);
122 1         1327 $self->set_port($port);
123 1         716 $self->set_forked_pid($pid);
124 1         683 AE::log info =>
125             "forked as $pid and bound to " . $self->uri;
126             }
127             }
128             }
129              
130 5         33262 return;
131             }
132              
133             sub DEMOLISH {
134 1     1 0 16500 my ($self) = @_;
135              
136 1 50       18 if ($self->forked) {
137 1         7 my $pid = $self->forked_pid;
138 1         62 kill 9 => $pid;
139 1         12 AE::log info =>
140             "killed $pid";
141             }
142              
143 1         122 return;
144             }
145              
146              
147             sub uri {
148 16     16 1 1662 my ($self) = @_;
149 16 100       376 return sprintf(
150             '%s://%s:%d/',
151             ($self->https ? 'https' : 'http'),
152             $self->address,
153             $self->port,
154             );
155             }
156              
157              
158             sub start_server {
159 4     4 1 8 my ($self, $cb) = @_;
160              
161             return tcp_server(
162             $self->address => $self->port,
163             sub {
164 12     12   40138 my ($fh, $host, $port) = @_;
165 12 50       88 if (scalar keys %pool > $self->maxconn) {
166 0         0 AE::log error =>
167             "deny connection from $host:$port (too many connections)\n";
168 0         0 return;
169             } else {
170 12         70 AE::log warn =>
171             "new connection from $host:$port\n";
172             }
173              
174 12 100       834 my $h = AnyEvent::Handle->new(
175             fh => $fh,
176             on_eof => \&_cleanup,
177             on_error => \&_cleanup,
178             timeout => $self->timeout,
179             ($self->https ? (tls_ctx => $self->https) : ()),
180             );
181              
182 12 100       1063 $h->push_read(tls_autostart => 'accept') if $self->https;
183              
184 12         242 $pool{fileno($fh)} = $h;
185 12         81 AE::log debug =>
186             sprintf "%d connection(s) in pool\n", scalar keys %pool;
187              
188 12         760 $self->_start($h);
189 4         64 } => $cb
190             );
191             }
192              
193              
194             sub _start {
195 13     13   23 my ($self, $my_handle) = @_;
196             return $my_handle->push_read(regex => qr{(\015?\012){2}}x, sub {
197 13     13   71869 my ($h, $data) = @_;
198 13         82 my ($req, $hdr) = split m{\015?\012}x, $data, 2;
199 13         60 $req =~ s/\s+$//sx;
200 13         71 AE::log debug => "request: [$req]\n";
201 13 100       906 if ($hdr =~ m{\bContent-length:\s*(\d+)\b}isx) {
202 2         8 AE::log debug => "expecting content\n";
203             $h->push_read(chunk => int($1), sub {
204 2         99 my ($_h, $_data) = @_;
205 2         9 $self->_reply($_h, $req, $hdr, $_data);
206 2         115 });
207             } else {
208 11         72 $self->_reply($h, $req, $hdr);
209             }
210 13         143 });
211             }
212              
213              
214             sub _cleanup {
215 12     12   31 my ($h) = @_;
216 12         48 AE::log debug => "closing connection\n";
217 12         752 my $r = eval {
218             ## no critic (ProhibitNoWarnings)
219 7     7   19349 no warnings;
  7         17  
  7         11209  
220              
221 12         43 my $id = fileno($h->{fh});
222 12         37 delete $pool{$id};
223 12         307 shutdown $h->{fh}, 2;
224              
225 12         35 return 1;
226             };
227 12 50 33     83 AE::log warn => "shutdown() aborted\n"
228             if not defined $r or $@;
229 12         68 $h->destroy;
230 12         729 return;
231             }
232              
233              
234             sub _reply {
235 13     13   37 my ($self, $h, $req, $hdr, $content) = @_;
236 13         29 state $timer = {};
237              
238 13         71 my $res = HTTP::Response->new(
239             &HTTP::Status::RC_OK ,=> undef,
240             HTTP::Headers->new(
241             Connection => 'close',
242             Content_Type => 'text/plain',
243 13   50     243 Server => __PACKAGE__ . "/@{[ $Test::HTTP::AnyEvent::Server::VERSION // 0 ]} AnyEvent/$AE::VERSION Perl/$] ($^O)",
244             )
245             );
246 13         3864 $res->date(time);
247 13         25793 $res->protocol('HTTP/1.0');
248              
249 13 100       265 if ($req =~ m{^(GET|POST)\s+(.+)\s+(HTTP/1\.[01])$}ix) {
    100          
250 11         67 my ($method, $uri, $protocol) = ($1, $2, $3);
251 11         66 AE::log debug => "sending response to $method ($protocol)\n";
252 11 100       764 AE::log debug => "simulating connection to $1\n"
253             if $uri =~ s{^(https?://[^/]+)}{}ix;
254 11         80 for ($uri) {
255 11         26 when (m{^/repeat/(\d+)/(.+)}x) {
256 1         10 $res->content($2 x $1);
257 10         38 } when (m{^/echo/head$}x) {
258 4         1114 $res->content(
259             join(
260             "\015\012",
261             qq($method $uri $protocol),
262             $hdr,
263             )
264             );
265 6         15 } when (m{^/echo/body$}x) {
266 1         6 $res->content($content);
267 5         12 } when (m{^/delay/(\d+)$}x) {
268 1         18 $res->content(sprintf(qq(issued %s\n), scalar gmtime));
269             $timer->{$h} = AE::timer $1, 0, sub {
270 1     1   2973268 delete $timer->{$h};
271 1         18 AE::log debug => "delayed response\n";
272 1         143 $h->push_write($res->as_string("\015\012"));
273 1         408 _cleanup($h);
274 1         36 };
275 1         9 return;
276 4         9 } default {
277 4         6 my $found;
278 4 100       24 if ($self->custom_handler) {
279 16         96 $res->request(HTTP::Request->new(
280             $method,
281             $uri,
282             [
283             map {
284 3         33 m{^\s*([^:\s]+)\s*:\s*(.*)$}sx
285             } split m{\015?\012}x, $hdr
286             ],
287             $content,
288             ));
289 3         7389 $found = eval { $self->custom_handler->($res) };
  3         19  
290 3 100       2551 if ($@) {
291 1         10 AE::log error => "custom_handler died: $@";
292 1         88 $res->code(&HTTP::Status::RC_INTERNAL_SERVER_ERROR);
293 1         25 $res->content($@);
294 1         27 $found = 1;
295             }
296             }
297 4 100       27 unless ($found) {
298 2         14 $res->code(&HTTP::Status::RC_NOT_FOUND);
299 2         27 $res->content('Not Found');
300             }
301             }
302             }
303             } elsif ($req =~ m{^CONNECT\s+([\w\.\-]+):(\d+)\s+(HTTP/1\.[01])$}ix) {
304 1         9 my ($peer_host, $peer_port, $protocol) = ($1, $2, $3);
305 1         12 AE::log debug => "simulating connection to $peer_host:$peer_port ($protocol)\n";
306 1         91 $res->message('Connection established');
307 1         16 $h->push_write($res->as_string("\015\012"));
308 1 50       325 if ($self->https) {
309 1         6 AE::log debug => 'attempting to use TLS';
310 1         68 $h->push_read(tls_autostart => 'accept');
311             }
312 1         47 $self->_start($h);
313 1         46 return;
314             } else {
315 1         4 AE::log error => "bad request\n";
316 1         57 $res->code(&HTTP::Status::RC_BAD_REQUEST);
317 1         13 $res->content('Bad Request');
318             }
319              
320 11         6751 $h->push_write($res->as_string("\015\012"));
321 11         2857 _cleanup($h);
322 11         280 return;
323             }
324              
325              
326             1;
327              
328             __END__