Establish CSP plugin
Change-Id: Iffb988f3c6a022ab20e64a1dcbd96f9cc6f96cb4
diff --git a/Changes b/Changes
index 0e58320..66c56ae 100755
--- a/Changes
+++ b/Changes
@@ -6,6 +6,7 @@
while keeping the api_path.
- Added advice in Readme regarding scripts in
Windows Powershell (lerepp).
+ - Establish CSP plugin.
0.40 2020-12-17
- Modernize ES and fix in-loops.
diff --git a/lib/Kalamar/Plugin/CSP.pm b/lib/Kalamar/Plugin/CSP.pm
new file mode 100644
index 0000000..440642d
--- /dev/null
+++ b/lib/Kalamar/Plugin/CSP.pm
@@ -0,0 +1,76 @@
+package Kalamar::Plugin::CSP;
+use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::Base -strict;
+use Mojo::Util qw!quote trim!;
+use List::Util qw'uniq';
+
+sub register {
+ my ($plugin, $app, $param) = @_;
+
+ $param ||= {};
+
+ # Load parameter from Config file
+ if (my $config_param = $app->config('CSP')) {
+ $param = { %$param, %$config_param };
+ };
+
+ # Initialize directives
+ my %directives = ();
+ foreach (keys %$param) {
+ $directives{$_} = ref $param->{$_} eq 'ARRAY' ? $param->{$_} : [$param->{$_}];
+ };
+
+ # Generate csp based on directives
+ my $csp = \( generate(%directives) );
+
+ # Add csp header
+ $app->hook(
+ before_dispatch => sub {
+ my $c = shift;
+ if ($$csp) {
+ $c->res->headers->header('Content-Security-Policy' => $$csp);
+ };
+ }
+ );
+
+ # Add csp directives
+ $app->helper(
+ 'csp.add' => sub {
+ my ($c, $dir, $url) = @_;
+
+ if ($c->tx->{req}) {
+
+ # Add template to content block
+ # @{$c->stash->{'csp.' . $name} ...
+ $c->app->log->warn('Calling csp.add from controller not yet supported');
+ return;
+ }
+
+ # Probably called from app
+
+ # Add to static directives
+ push(@{$directives{$dir} //= []}, $url);
+ $csp = \(generate(%directives));
+ }
+ );
+};
+
+
+# Quote elements that need to be single quoted
+sub opt_quote {
+ my $s = trim(shift);
+ if ($s =~ /^(?:(?:self|none|unsafe-(?:inline|eval|hashes))$|nonce-|sha(?:256|384|512)-)/) {
+ return qq!'$s'!;
+ };
+ return $s;
+};
+
+
+# Generate CSP string
+sub generate {
+ return '' unless @_;
+ my %d = @_;
+ return join(';', map { $_ . ' ' . join(' ', map { opt_quote($_) } uniq @{$d{$_}}) } sort keys %d) . ';';
+};
+
+1;
diff --git a/t/plugin/csp.t b/t/plugin/csp.t
new file mode 100644
index 0000000..6e15a61
--- /dev/null
+++ b/t/plugin/csp.t
@@ -0,0 +1,84 @@
+use Mojolicious::Lite;
+use Test::Mojo;
+use Test::More;
+
+my $t = Test::Mojo->new;
+
+plugin 'Kalamar::Plugin::CSP' => {
+ 'style-src' => ['self','unsafe-inline'],
+ 'script-src' => '*',
+ 'img-src' => ['self', 'data:']
+};
+
+get '/' => sub {
+ shift->render(text => 'hello world');
+};
+
+my $csp = 'Content-Security-Policy';
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->content_is('hello world')
+ ->header_is($csp, "img-src 'self' data:;script-src *;style-src 'self' 'unsafe-inline';")
+ ;
+
+$t->app->csp->add('img-src' => 'stats.ids-mannheim.de');
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->content_is('hello world')
+ ->header_is($csp, "img-src 'self' data: stats.ids-mannheim.de;script-src *;style-src 'self' 'unsafe-inline';")
+ ;
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->content_is('hello world')
+ ->header_is($csp, "img-src 'self' data: stats.ids-mannheim.de;script-src *;style-src 'self' 'unsafe-inline';")
+ ;
+
+$t->app->csp->add('img-src' => 'stats.ids-mannheim.de');
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->content_is('hello world')
+ ->header_is($csp, "img-src 'self' data: stats.ids-mannheim.de;script-src *;style-src 'self' 'unsafe-inline';")
+ ;
+
+$t->app->csp->add('script-src' => '*');
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->content_is('hello world')
+ ->header_is($csp, "img-src 'self' data: stats.ids-mannheim.de;script-src *;style-src 'self' 'unsafe-inline';")
+ ;
+
+
+# New
+$t = Test::Mojo->new;
+$t->app->config(
+ CSP => {
+ 'style-src' => ['self','unsafe-inline'],
+ 'img-src' => ['self', 'data:']
+ }
+);
+
+$t->app->plugin('Kalamar::Plugin::CSP' => {
+ 'script-src' => '*',
+ 'img-src' => 'self'
+});
+
+$t->app->routes->get('/n')->to(
+ cb => sub {
+ shift->render(text => 'hello world');
+ }
+);
+
+$t->get_ok('/n')
+ ->status_is(200)
+ ->content_is('hello world')
+ ->header_is($csp, "img-src 'self' data:;script-src *;style-src 'self' 'unsafe-inline';")
+ ;
+
+
+done_testing;
+__END__