cro-http-server
Cro::HTTP::Server
The Cro::HTTP::Server
class is the most convenient way to host a HTTP or
HTTPS service in Cro. Instances of Cro::HTTP::Server
do the Cro::Service
role, and as such have the start
and stop
methods. The only required
configuration for a Cro::HTTP::Server
is an application, which is any
Cro::Transform
that consumes a Cro::HTTP::Request
and produces a
Cro::HTTP::Response
. This will often be produced using Cro::HTTP::Router
.
A minimal setup
The following example demonstrates how Cro::HTTP::Server
can be used to host
a simple "Hello, world" HTTP application, configuring it to listen on port 8888
of localhost. A signal handler is added to allow for clean shutdown of the
server upon Ctrl + C.
use Cro::HTTP::Router;
use Cro::HTTP::Server;
my $application = route {
get -> {
content 'text/html', '<strong>Hello, world!</strong>';
}
}
my Cro::Service $hello-service = Cro::HTTP::Server.new(
:host('localhost'), :port(8888), :$application
);
$hello-service.start;
react whenever signal(SIGINT) {
$hello-service.stop;
exit;
}
Configuring HTTPS
By default, Cro::HTTP::Server
will operates as a HTTP server. To set it up
as a HTTPS server instead, use the tls
named parameter. It expects to receive
a hash of arguments providing the TLS configuration, which it will in turn pass
to the constructor of Cro::TLS::Listener
. Typically, certificate-file
and
private-key-file
, specifying the locations of files containing a certificate
and private key respectively, should be passed.
my %tls = private-key-file => 'server.pem',
certificate-file => 'cert.pem';
my Cro::Service $hello-service = Cro::HTTP::Server.new(
:host('localhost'), :port(8888), :%tls, :$application
);
Request body parsers and response body serializers
Additional request body parsers (implementations of Cro::BodyParser
)
and response body serializers (implementations of Cro::BodySerializer
)
can be added at the server level. Alternatively, the set of default set of
body parsers and serializers can be replaced entirely.
To add extra body parsers to the set of defaults, pass a list of them to
add-body-parsers
.
my Cro::Service $hello-service = Cro::HTTP::Server.new(
:host('localhost'), :port(8888), :$application,
add-body-parsers => [
YAMLBodyParser,
XMLBodyParser
]
);
These will take precedence over (that is, tested for applicability ahead of)
the default set of body parsers. To replace those entirely, pass a list of the
body parsers to use as the body-parsers
named parameter:
my Cro::Service $hello-service = Cro::HTTP::Server.new(
:host('localhost'), :port(8888), :$application,
body-parsers => [
# Don't parse any kind of body except a JSON one; anything else
# will throw an exception when `.body` is called.
Cro::HTTP::BodyParser::JSON
]
);
If both body-parsers
and add-body-parsers
is used, then both will be used,
with those in add-body-parsers
again having higher precedence.
If the class that represents a body parser or body serializer does not have attributes, passing type object is equivalent to passing an instance.
add-body-parsers => [
YAMLBodyParser.new
]
# as well as
add-body-parsers => [
YAMLBodyParser
]
A similar scheme applies for body serializers. Use add-body-serializers
to
add extra ones to the defaults:
my Cro::Service $hello-service = Cro::HTTP::Server.new(
:host('localhost'), :port(8888), :$application,
add-body-serializers => [
YAMLBodySerializer,
XMLBodySerializer
]
);
Or replace the set of body serializers entirely by passing body-serializers
:
my Cro::Service $hello-service = Cro::HTTP::Server.new(
:host('localhost'), :port(8888), :$application,
body-serializers => [
# The body can only ever be something that can be JSON serialized.
Cro::HTTP::BodySerializer::JSON
]
);
If both add-body-serializers
and body-serializers
are passed, they both
will be used, with those in add-body-serializers
taking precedence.
Middleware
HTTP middleware is implemented as a Cro::Transform
. There are four places
that HTTP middleware can be inserted. There are, in order of processing:
before-parse - operates on the raw bytes coming over the network prior to the
Cro::HTTP::RequestParser
seeing them. The transform should consume aCro::TCP::Message
and produce aCro::TCP::Message
. It is relatively unusual to need to insert middleware at this stage, though it could be useful for getting rate limiting in early before the effort to even parse a request has been expended, for example.before - operates on requests after they have been parsed, but before they reach the application. Consumes a
Cro::HTTP::Request
and produces aCro::HTTP::Request
. This is a common place to put middleware that does authentication, authorization, session handling, CSRF protection, and so forth.after - operates on responses produced by the application. Consumes a
Cro::HTTP::Response
and produces aCro::HTTP::Response
. This is a common place to put middleware that does things like logging and inserting headers to increase security (likeX-Frame-Options
,Strict-Transport-Security
,Content-Security-Policy
, and so forth).after-serialize - operates on the bytes sent back over the network in response to a request. Consumes a
Cro::TCP::Message
and produces aCro::TCP::Message
. It is unusual to need to insert middleware at this stage.
The names of these places are named parameters that can be passed to the
Cro::HTTP::Server
constructor. Either a single Cro::Transform
or an
Iterable
(for example, List
) of Cro::Transform
s may be passed.
For example, the following piece of middleware:
class StrictTransportSecurity does Cro::Transform {
has Duration:D $.max-age is required;
method consumes() { Cro::HTTP::Response }
method produces() { Cro::HTTP::Response }
method transformer(Supply $pipeline --> Supply) {
supply {
whenever $pipeline -> $response {
$response.append-header:
'Strict-Transport-Security',
"max-age=$!max-age";
emit $response;
}
}
}
}
Could be applied as follows:
my Cro::Service $hello-service = Cro::HTTP::Server.new(
:host('localhost'), :port(8888), :$application,
after => StrictTransportSecurity.new(Duration.new(30 * 24 * 60 * 60))
);
HTTP versions
The :http
option can be passed to control which versions of HTTP should be
supported. It can be passed a single item or list. Valid options are 1.1
(which will implicitly handle HTTP/1.0 too) and 2
.
:http<1.1> # HTTP/1.1 only
:http<2> # HTTP/2 only
:http<1.1 2> # HTTP/1.1 and HTTP/2 (TLS only; selected by ALPN)
The default is :http<1.1>
for a HTTP server. For a HTTPS server, if the TLS
library (IO::Socket::Async::SSL
) detects that ALPN support is available then
it will default to :http<1.1 2>
; otherwise it will default to :http<1.1>
.
If :http<1.1 2>
is specified and ALPN support is not available, then an
exception will be thrown. ALPN is the only supported mechanism for selecting
between HTTP/1.1 and HTTP/2, thus the requirement on it for this configuration
(and why there is no option for both versions with HTTP).
Allowed request methods
By default, the following HTTP request methods are allowed:
GET
HEAD
POST
PUT
DELETE
PATCH
CONNECT
OPTIONS
A request using a request method other than these shall be refused. To change
set of allowed request methods, pass a list using the allowed-methods
named
constructor parameter:
my $service = Cro::HTTP::Server.new(
:host('localhost'), :port(8888), :$application,
:allowed-methods<GET HEAD PUT POST DELETE LINK UNLINK>
);