Lance Wicks
@lancew
https://slides.com/lancewicks/yapceu2018
Building a web app
with Perl 6 and Cro
use Cro::HTTP::Test;
use Routes;
test-service routes(), {
test get('/'),
status => 200,
content-type => 'text/html',
body => /example1/;
}
done-testing;
use Cro::HTTP::Router;
sub routes() is export {
route {
get -> {
content 'text/html', "<h1> example1 </h1>";
}
}
}
Simple GET route
Simple GET route
prove6 -I=lib -v t/example1.t
ok 1 - Status is acceptable
ok 2 - Content type is acceptable
ok 3 - Body is acceptable
1..3
ok 1 - GET /
1..1
t/example1.t .. ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs
Result: PASS
use Cro::HTTP::Test;
use Routes;
test-service routes(), {
test get('/hello/world'),
status => 200,
content-type => 'text/html',
body => /Hello world/;
}
done-testing;
use Cro::HTTP::Router;
sub routes() is export {
route {
get -> 'hello', $name {
content 'text/html', "<h1> Hello $name </h1>";
}
}
}
GET route with params
prove6 -I=lib -v t/example2.t
ok 1 - Status is acceptable
ok 2 - Content type is acceptable
ok 3 - Body is acceptable
1..3
ok 1 - GET /hello/world
1..1
t/example2.t .. ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs
Result: PASS
GET route with params
use Cro::HTTP::Test;
use Routes;
test-service routes(), {
test-given '/add', {
test post(json => { :x(37), :y(5) }),
status => 200,
json => { :result(42) };
}
}
done-testing;
use Cro::HTTP::Router;
sub routes() is export {
route {
post -> 'add' {
request-body 'application/json' => -> (:$x!, :$y!) {
content 'application/json', { :result($x + $y) };
}
}
}
}
POST route
$ prove6 -v -I=lib t/example3.t
ok 1 - Status is acceptable
ok 2 - Content type is recognized as a JSON one
ok 3 - Body is acceptable
1..3
ok 1 - POST /add
1..1
t/example3.t .. ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs
Result: PASS
$ curl --header "Content-Type: application/json" \
--request POST \
--data '{"x":"2","y":"5"}' \
http://localhost:20000/add
{"result": 7}
POST route
use Cro::HTTP::Log::File;
use Cro::HTTP::Server;
use Routes;
my Cro::Service $http = Cro::HTTP::Server.new(
http => <1.1>,
host => %*ENV<EXAMPLE2_HOST>,
port => %*ENV<EXAMPLE2_PORT>,
application => routes(),
after => [
Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR)
]
);
$http.start;
myjudo.net
# Cro
sub routes() is export {
route {
get -> LoggedIn $user, 'user', $user_name {
my %user_data = $mj.get_user_data( user_name => $user.username );
my $t = Template::Mojo.from-file('views/user/home.tm');
content 'text/html', $t.render( {
user_data => $(%user_data),
} );
};
}
# Bailador
prefix '/user' => sub {
get "/:user" => sub ($user){
my $session = session();
redirect '/' unless $session<user>:exists && $session<user> eq $user;
my %user_data = $mj.get_user_data( user_name => $session<user> );
redirect '/' unless %user_data;
template 'user/home.tt', {
user_data => $(%user_data),
};
}
}
Docker
FROM croservices/cro-http:0.7.5
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN zef install --deps-only . && perl6 -c -Ilib service.p6
ENV EXAMPLE1_HOST="0.0.0.0" EXAMPLE1_PORT="10000"
EXPOSE 10000
CMD perl6 -Ilib service.p6
myjudo.net
Docker
FROM alpine:3.8
RUN apk add --no-cache curl gcc git libressl-dev linux-headers make musl-dev perl sqlite-libs
# Install Perl 6
RUN curl -L https://github.com/rakudo/rakudo/archive/2018.06.tar.gz | tar xzf - \
&& cd rakudo-2018.06 \
&& CFLAGS=-flto ./Configure.pl --backend=moar --gen-moar --moar-option=--ar=gcc-ar --prefix=/usr \
&& make -j`nproc` install
# Install zef
RUN git clone https://github.com/ugexe/zef.git \
&& cd zef \
&& perl6 -Ilib bin/zef install --/test .
WORKDIR /app
COPY META6.json .
RUN /usr/share/perl6/site/bin/zef install --deps-only --/test .
# Avoid having "binaries" in the final image
RUN rm -r /usr/share/perl6/site/bin
FROM scratch
COPY --from=0 /bin/sh /bin/
COPY --from=0 /lib/ld-musl-x86_64.so.1 \
/lib/libz.* /lib/
COPY --from=0 /usr/bin/moar \
/usr/bin/perl6 /usr/bin/
COPY --from=0 /usr/lib/libmoar.so \
/usr/lib/libcrypto.* \
/usr/lib/libsqlite3.* \
/usr/lib/libssl.* /usr/lib/
COPY --from=0 /usr/share/nqp /usr/share/nqp
COPY --from=0 /usr/share/perl6 /usr/share/perl6
WORKDIR /app
COPY . /app
CMD ["perl6", "-Ilib", "service.p6"]
cro stub
$ cro stub http example2 example2
Stubbing a HTTP Service 'example2' in 'example2'...
First, please provide a little more information.
Secure (HTTPS) (yes/no) [no]:
Support HTTP/1.1 (yes/no) [yes]:
Support HTTP/2.0 (yes/no) [no]:
Support Web Sockets (yes/no) [no]:
---
$ cro stub web example2 example2
Couldn't find template 'web'. Available templates:
zeromq-worksink, http, zeromq-worker, react-redux-spa
cro run
$ cro run
▶ Starting example2 (example2)
🔌 Endpoint HTTP will be at http://localhost:20000/
♻ Restarting example2 (example2)
📓 example2 Listening at http://localhost:20000
⚠ example2 [ERROR] 404 /favicon.ico - ::1
📓 example2 [OK] 200 / - ::1
📓 example2 Shutting down...
♻ Restarting example2 (example2)
📓 example2 Listening at http://localhost:20000
cro trace
$ cro trace
▶ Starting example2 (example2)
🔌 Endpoint HTTP will be at http://localhost:20000/
📓 example2 Listening at http://localhost:20000
👓 example2 ⚡ EMIT [HTTP(20000)] Cro::TCP::Listener
Cro::TCP::ServerConnection.new(replier => Cro::TCP::Replier.new)
👓 example2 ⚡ EMIT [HTTP(20000)] Cro::TCP::Listener
Cro::TCP::ServerConnection.new(replier => Cro::TCP::Replier.new)
👓 example2 ⚡ EMIT [HTTP(20000)] Cro::TCP::ServerConnection
TCP Message
47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a GET / HTTP/1.1..
48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a Host: localhost:
32 30 30 30 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f 20000..Connectio
6e 3a 20 6b 65 65 70 2d 61 6c 69 76 65 0d 0a 43 n: keep-alive..C
61 63 68 65 2d 43 6f 6e 74 72 6f 6c 3a 20 6d 61 ache-Control: ma
78 2d 61 67 65 3d 30 0d 0a 55 70 67 72 61 64 65 x-age=0..Upgrade
2d 49 6e 73 65 63 75 72 65 2d 52 65 71 75 65 73 -Insecure-Reques
74 73 3a 20 31 0d 0a 55 73 65 72 2d 41 67 65 6e ts: 1..User-Agen
74 3a 20 4d 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28 t: Mozilla/5.0 (
58 31 31 3b 20 4c 69 6e 75 78 20 78 38 36 5f 36 X11; Linux x86_6
34 29 20 41 70 70 6c 65 57 65 62 4b 69 74 2f 35 4) AppleWebKit/5
33 37 2e 33 36 20 28 4b 48 54 4d 4c 2c 20 6c 69 37.36 (KHTML, li
6b 65 20 47 65 63 6b 6f 29 20 43 68 72 6f 6d 65 ke Gecko) Chrome
2f 37 30 2e 30 2e 33 35 31 34 2e 30 20 53 61 66 /70.0.3514.0 Saf
61 72 69 2f 35 33 37 2e 33 36 0d 0a 44 4e 54 3a ari/537.36..DNT:
20 31 0d 0a 41 63 63 65 70 74 3a 20 74 65 78 74 1..Accept: text
2f 68 74 6d 6c 2c 61 70 70 6c 69 63 61 74 69 6f /html,applicatio
6e 2f 78 68 74 6d 6c 2b 78 6d 6c 2c 61 70 70 6c n/xhtml+xml,appl
69 63 61 74 69 6f 6e 2f 78 6d 6c 3b 71 3d 30 2e ication/xml;q=0.
39 2c 69 6d 61 67 65 2f 77 65 62 70 2c 69 6d 61 9,image/webp,ima
67 65 2f 61 70 6e 67 2c 2a 2f 2a 3b 71 3d 30 2e ge/apng,*/*;q=0.
38 0d 0a 41 63 63 65 70 74 2d 45 6e 63 6f 64 69 8..Accept-Encodi
6e 67 3a 20 67 7a 69 70 2c 20 64 65 66 6c 61 74 ng: gzip, deflat
65 2c 20 62 72 0d 0a 41 63 63 65 70 74 2d 4c 61 e, br..Accept-La
6e 67 75 61 67 65 3a 20 65 6e 2d 55 53 2c 65 6e nguage: en-US,en
3b 71 3d 30 2e 39 0d 0a 0d 0a ;q=0.9....
cro web
$ cro web
Cro web interface running at http://localhost:10203/
myjudo.net
SSL
my $https = Cro::HTTP::Server.new(
:1443port,
:host<0.0.0.0>,
http => <1.1>,
before => [
Cro::HTTP::Session::InMemory[UserSession].new;
],
tls => %(
private-key-file => %*ENV<MYJUDO_TLS_KEY>,
certificate-file => %*ENV<MYJUDO_TLS_CERT>,
ciphers => 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256',
),
application => routes(),
after => [
Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR),
]
);
$https.start;
myjudo.net
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; includeSubDomains; preload";
emit $response;
}
}
}
}
my $https = Cro::HTTP::Server.new(
...
application => routes(),
after => [
Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR),
# set max age to be one year and one day, 366 days
StrictTransportSecurity.new(max-age => Duration.new(366 * 24 * 60 * 60)),
]
);
$https.start;
https://slides.com/lancewicks/yapceu2018
https://slides.com/lancewicks/yapceu2018
Lance Wicks
@lancew