Test-HTTP-AnyEvent-Server
view release on metacpan or search on metacpan
lib/Test/HTTP/AnyEvent/Server.pm view on Meta::CPAN
sub BUILD {
my ($self) = @_;
## no critic (RequireLocalizedPunctuationVars)
@ENV{qw(no_proxy http_proxy https_proxy ftp_proxy all_proxy)} = (q(localhost,127.0.0.1), (q()) x 4)
if $self->disable_proxy;
unless ($self->forked) {
$self->set_server(
$self->start_server(sub {
my (undef, $address, $port) = @_;
$self->set_address($address);
$self->set_port($port);
AE::log info =>
'bound to ' . $self->uri;
})
);
} else {
my ($rh, $wh) = portable_pipe;
given (fork) {
when (undef) {
AE::log fatal =>
"couldn't fork(): $!";
} when (0) {
# child
close $rh;
my $h = AnyEvent::Handle->new(
fh => $wh,
on_error => sub {
AE::log fatal =>
"couldn't syswrite() to pipe: $!";
},
);
$self->set_server(
$self->start_server(sub {
my (undef, $address, $port) = @_;
# have to postpone so the address/port gets actually bound
AE::postpone { $h->push_write(join("\t", $address, $port)) };
})
);
AE::cv->wait;
POSIX::_exit(0);
exit 1;
} default {
# parent
my $pid = $_;
close $wh;
my $buf;
my $len = sysread $rh, $buf, 65536;
AE::log fatal =>
"couldn't sysread() from pipe: $!"
if not defined $len or not $len;
my ($address, $port) = split m{\t}x, $buf;
$self->set_address($address);
$self->set_port($port);
$self->set_forked_pid($pid);
AE::log info =>
"forked as $pid and bound to " . $self->uri;
}
}
}
return;
}
sub DEMOLISH {
my ($self) = @_;
if ($self->forked) {
my $pid = $self->forked_pid;
kill 9 => $pid;
AE::log info =>
"killed $pid";
}
return;
}
sub uri {
my ($self) = @_;
return sprintf(
'%s://%s:%d/',
($self->https ? 'https' : 'http'),
$self->address,
$self->port,
);
}
sub start_server {
my ($self, $cb) = @_;
return tcp_server(
$self->address => $self->port,
sub {
my ($fh, $host, $port) = @_;
if (scalar keys %pool > $self->maxconn) {
AE::log error =>
"deny connection from $host:$port (too many connections)\n";
return;
} else {
AE::log warn =>
"new connection from $host:$port\n";
}
my $h = AnyEvent::Handle->new(
fh => $fh,
on_eof => \&_cleanup,
on_error => \&_cleanup,
timeout => $self->timeout,
($self->https ? (tls_ctx => $self->https) : ()),
);
$h->push_read(tls_autostart => 'accept') if $self->https;
$pool{fileno($fh)} = $h;
AE::log debug =>
sprintf "%d connection(s) in pool\n", scalar keys %pool;
$self->_start($h);
} => $cb
);
}
sub _start {
my ($self, $my_handle) = @_;
return $my_handle->push_read(regex => qr{(\015?\012){2}}x, sub {
my ($h, $data) = @_;
my ($req, $hdr) = split m{\015?\012}x, $data, 2;
$req =~ s/\s+$//sx;
AE::log debug => "request: [$req]\n";
if ($hdr =~ m{\bContent-length:\s*(\d+)\b}isx) {
AE::log debug => "expecting content\n";
$h->push_read(chunk => int($1), sub {
my ($_h, $_data) = @_;
$self->_reply($_h, $req, $hdr, $_data);
});
} else {
$self->_reply($h, $req, $hdr);
}
});
}
sub _cleanup {
my ($h) = @_;
AE::log debug => "closing connection\n";
my $r = eval {
## no critic (ProhibitNoWarnings)
no warnings;
my $id = fileno($h->{fh});
delete $pool{$id};
shutdown $h->{fh}, 2;
return 1;
};
AE::log warn => "shutdown() aborted\n"
if not defined $r or $@;
$h->destroy;
return;
}
sub _reply {
my ($self, $h, $req, $hdr, $content) = @_;
state $timer = {};
my $res = HTTP::Response->new(
&HTTP::Status::RC_OK ,=> undef,
HTTP::Headers->new(
Connection => 'close',
Content_Type => 'text/plain',
Server => __PACKAGE__ . "/@{[ $Test::HTTP::AnyEvent::Server::VERSION // 0 ]} AnyEvent/$AE::VERSION Perl/$] ($^O)",
)
);
$res->date(time);
$res->protocol('HTTP/1.0');
if ($req =~ m{^(GET|POST)\s+(.+)\s+(HTTP/1\.[01])$}ix) {
my ($method, $uri, $protocol) = ($1, $2, $3);
AE::log debug => "sending response to $method ($protocol)\n";
AE::log debug => "simulating connection to $1\n"
if $uri =~ s{^(https?://[^/]+)}{}ix;
for ($uri) {
when (m{^/repeat/(\d+)/(.+)}x) {
$res->content($2 x $1);
} when (m{^/echo/head$}x) {
$res->content(
join(
"\015\012",
qq($method $uri $protocol),
$hdr,
)
);
} when (m{^/echo/body$}x) {
$res->content($content);
} when (m{^/delay/(\d+)$}x) {
$res->content(sprintf(qq(issued %s\n), scalar gmtime));
$timer->{$h} = AE::timer $1, 0, sub {
delete $timer->{$h};
AE::log debug => "delayed response\n";
$h->push_write($res->as_string("\015\012"));
_cleanup($h);
};
return;
} default {
my $found;
if ($self->custom_handler) {
$res->request(HTTP::Request->new(
$method,
$uri,
[
map {
m{^\s*([^:\s]+)\s*:\s*(.*)$}sx
} split m{\015?\012}x, $hdr
],
$content,
));
$found = eval { $self->custom_handler->($res) };
if ($@) {
AE::log error => "custom_handler died: $@";
$res->code(&HTTP::Status::RC_INTERNAL_SERVER_ERROR);
$res->content($@);
$found = 1;
}
}
unless ($found) {
$res->code(&HTTP::Status::RC_NOT_FOUND);
$res->content('Not Found');
}
}
}
} elsif ($req =~ m{^CONNECT\s+([\w\.\-]+):(\d+)\s+(HTTP/1\.[01])$}ix) {
my ($peer_host, $peer_port, $protocol) = ($1, $2, $3);
AE::log debug => "simulating connection to $peer_host:$peer_port ($protocol)\n";
$res->message('Connection established');
$h->push_write($res->as_string("\015\012"));
if ($self->https) {
AE::log debug => 'attempting to use TLS';
$h->push_read(tls_autostart => 'accept');
}
$self->_start($h);
return;
} else {
AE::log error => "bad request\n";
$res->code(&HTTP::Status::RC_BAD_REQUEST);
$res->content('Bad Request');
}
$h->push_write($res->as_string("\015\012"));
_cleanup($h);
return;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test::HTTP::AnyEvent::Server - the async counterpart to Test::HTTP::Server
=head1 VERSION
version 0.013
=head1 SYNOPSIS
#!/usr/bin/env perl
use common::sense;
( run in 0.531 second using v1.01-cache-2.11-cpan-5511b514fd6 )