diff --git a/sender/Dockerfile b/sender/Dockerfile index 3c8ce43..b152cb4 100644 --- a/sender/Dockerfile +++ b/sender/Dockerfile @@ -14,6 +14,54 @@ RUN apk add \ s6-overlay \ vim +# Dependencies for REST API +RUN apk add \ + gcc \ + libc-dev \ + make \ + perl-app-cpanminus \ + perl-clone \ + perl-config-any \ + perl-data-optlist \ + perl-dev \ + perl-exporter-tiny \ + perl-extutils-config \ + perl-extutils-helpers \ + perl-extutils-installpaths \ + perl-file-sharedir \ + perl-file-sharedir-install \ + perl-file-slurp \ + perl-file-which \ + perl-hash-merge-simple \ + perl-hash-multivalue \ + perl-http-date \ + perl-http-headers-fast \ + perl-import-into \ + perl-json-maybexs \ + perl-module-build \ + perl-module-build-tiny \ + perl-module-implementation \ + perl-module-runtime \ + perl-moo \ + perl-params-util \ + perl-params-validate \ + perl-path-tiny \ + perl-plack \ + perl-readonly \ + perl-ref-util \ + perl-role-tiny \ + perl-safe-isa \ + perl-sub-exporter \ + perl-sub-install \ + perl-sub-quote \ + perl-template-toolkit \ + perl-type-tiny \ + perl-yaml + +RUN cpanm -n -v \ + Dancer2 \ + Module::Pluggable::Object + RUN newaliases RUN install -m 0700 -o opendkim -g opendkim -d /run/opendkim @@ -23,6 +71,8 @@ COPY etc/s6-overlay /etc/s6-overlay COPY etc/postfix /etc/postfix COPY etc/opendkim /etc/opendkim +COPY web-api /src/api + ENTRYPOINT ["/init"] # Ne pas positionner USER, ou sinon les services ne démarreront pas de manière diff --git a/sender/etc/opendkim/opendkim.conf b/sender/etc/opendkim/opendkim.conf index ca65dff..0bae792 100644 --- a/sender/etc/opendkim/opendkim.conf +++ b/sender/etc/opendkim/opendkim.conf @@ -17,6 +17,6 @@ SendReports yes ## Il vaut donc mieux paramétrer une SigningTable (qui liste les expéditeurs ## pour lesquels on signe) et une KeyTable (qui liste les emplacements des ## clefs privées). -# -# SigningTable file:/etc/opendkim/signing_table -# KeyTable file:/etc/opendkim/key_table + +SigningTable file:/etc/opendkim/signing_table +KeyTable file:/etc/opendkim/key_table diff --git a/sender/etc/s6-overlay/s6-rc.d/api/run b/sender/etc/s6-overlay/s6-rc.d/api/run new file mode 100644 index 0000000..2d9058d --- /dev/null +++ b/sender/etc/s6-overlay/s6-rc.d/api/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +/usr/bin/env perl /src/api/bin/app.psgi \ No newline at end of file diff --git a/sender/etc/s6-overlay/s6-rc.d/api/type b/sender/etc/s6-overlay/s6-rc.d/api/type new file mode 100644 index 0000000..1780f9f --- /dev/null +++ b/sender/etc/s6-overlay/s6-rc.d/api/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/sender/etc/s6-overlay/s6-rc.d/user/contents.d/api b/sender/etc/s6-overlay/s6-rc.d/user/contents.d/api new file mode 100644 index 0000000..e69de29 diff --git a/sender/web-api/.dancer b/sender/web-api/.dancer new file mode 100644 index 0000000..e69de29 diff --git a/sender/web-api/MANIFEST b/sender/web-api/MANIFEST new file mode 100644 index 0000000..aada075 --- /dev/null +++ b/sender/web-api/MANIFEST @@ -0,0 +1,24 @@ +MANIFEST +MANIFEST.SKIP +.dancer +Makefile.PL +config.yml +cpanfile +views/index.tt +views/layouts/main.tt +lib/Email/SpoofingDemo/API/DNS.pm +t/002_index_route.t +t/001_base.t +environments/production.yml +environments/development.yml +bin/app.psgi +public/500.html +public/dispatch.cgi +public/dispatch.fcgi +public/favicon.ico +public/404.html +public/javascripts/jquery.js +public/css/error.css +public/css/style.css +public/images/perldancer-bg.jpg +public/images/perldancer.jpg diff --git a/sender/web-api/MANIFEST.SKIP b/sender/web-api/MANIFEST.SKIP new file mode 100644 index 0000000..bbfb365 --- /dev/null +++ b/sender/web-api/MANIFEST.SKIP @@ -0,0 +1,17 @@ +^\.git\/ +maint +^tags$ +.last_cover_stats +Makefile$ +^blib +^pm_to_blib +^.*.bak +^.*.old +^t.*sessions +^cover_db +^.*\.log +^.*\.swp$ +MYMETA.* +^.gitignore +^.svn\/ +^Email-SpoofingDemo-API-DNS- diff --git a/sender/web-api/Makefile.PL b/sender/web-api/Makefile.PL new file mode 100644 index 0000000..6077c4f --- /dev/null +++ b/sender/web-api/Makefile.PL @@ -0,0 +1,26 @@ +use strict; +use warnings; +use ExtUtils::MakeMaker; + +# Normalize version strings like 6.30_02 to 6.3002, +# so that we can do numerical comparisons on it. +my $eumm_version = $ExtUtils::MakeMaker::VERSION; +$eumm_version =~ s/_//; + +WriteMakefile( + NAME => 'Email::SpoofingDemo::API::DNS', + AUTHOR => q{Marc van der Wal }, + VERSION_FROM => 'lib/Email/SpoofingDemo/API/Sender.pm', + ABSTRACT => 'Email spoofing demo: REST API for Sender', + ($eumm_version >= 6.3001 + ? ('LICENSE'=> 'all-rights-reserved') + : ()), + PL_FILES => {}, + PREREQ_PM => { + 'Test::More' => 0, + 'YAML' => 0, + 'Dancer2' => 0.300000, + }, + dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, + clean => { FILES => 'Email-SpoofingDemo-API-Sender-*' }, +); diff --git a/sender/web-api/bin/app.psgi b/sender/web-api/bin/app.psgi new file mode 100644 index 0000000..d0cb56f --- /dev/null +++ b/sender/web-api/bin/app.psgi @@ -0,0 +1,9 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/../lib"; + +use Email::SpoofingDemo::API::Sender; +Email::SpoofingDemo::API::Sender->to_app; diff --git a/sender/web-api/config.yml b/sender/web-api/config.yml new file mode 100644 index 0000000..42e24b6 --- /dev/null +++ b/sender/web-api/config.yml @@ -0,0 +1,4 @@ + +appname: "Email::SpoofingDemo::API::Sender" +charset: "UTF-8" +serializer: JSON diff --git a/sender/web-api/cpanfile b/sender/web-api/cpanfile new file mode 100644 index 0000000..28436b4 --- /dev/null +++ b/sender/web-api/cpanfile @@ -0,0 +1,11 @@ +requires "Dancer2" => "0.300000"; + +recommends "YAML" => "0"; +recommends "URL::Encode::XS" => "0"; +recommends "CGI::Deurl::XS" => "0"; +recommends "HTTP::Parser::XS" => "0"; + +on "test" => sub { + requires "Test::More" => "0"; + requires "HTTP::Request::Common" => "0"; +}; diff --git a/sender/web-api/environments/development.yml b/sender/web-api/environments/development.yml new file mode 100644 index 0000000..0887b4b --- /dev/null +++ b/sender/web-api/environments/development.yml @@ -0,0 +1,14 @@ + +logger: "console" +log: "core" + +# should Dancer2 consider warnings as critical errors? +warnings: 1 + +# should Dancer2 show a stacktrace when an 5xx error is caught? +# if set to yes, public/500.html will be ignored and either +# views/500.tt, 'error_template' template, or a default error template will be used. +show_errors: 1 + +# print the banner +startup_info: 1 diff --git a/sender/web-api/environments/production.yml b/sender/web-api/environments/production.yml new file mode 100644 index 0000000..41b436f --- /dev/null +++ b/sender/web-api/environments/production.yml @@ -0,0 +1,16 @@ +# configuration file for production environment + +# only log warning and error messsages +log: "warning" + +# log message to a file in logs/ +logger: "file" + +# don't consider warnings critical +warnings: 0 + +# hide errors +show_errors: 0 + +# disable server tokens in production environments +no_server_tokens: 1 diff --git a/sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm b/sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm new file mode 100644 index 0000000..a873dbc --- /dev/null +++ b/sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm @@ -0,0 +1,74 @@ +package Email::SpoofingDemo::API::Sender; + +use Dancer2; + +use Email::SpoofingDemo::DKIM qw(read_signing_table read_key_table + write_signing_table write_key_table + generate_dkim_key); + +our $VERSION = '0.1'; + +my $signing_table = "/etc/opendkim/signing_table"; +my $key_table = "/etc/opendkim/key_table"; +my $key_dir = "/etc/opendkim/keys"; + +get '/' => sub { return "Welcome"; }; + +get '/installed-keys' => sub { + my $signing_table = read_signing_table($signing_table); + my $key_table = read_key_table($key_table); + + my @result; + + for my $domain (sort keys %$key_table) { + push @result, { + domain => $domain, + available_keys => $key_table->{$domain}, + current_key => $signing_table->{$domain} + }; + } + + return \@result; +}; + +post '/generate-dkim-key' => sub { + my $domain = body_parameters->get('domain'); + my $selector = body_parameters->get('selector'); + my $key_size = body_parameters->get('key_size'); + + # Generate key + my $txt_data = generate_dkim_key($domain, $selector, $key_size, + $key_table, $key_dir, $signing_table); + + my $txt_record = sprintf("%-30s. TXT %s", + qq{$selector._domainkey.$domain}, + $txt_data); + + return { + txt_record => $txt_record + }; +}; + +post '/send-email/confirmation_email' => sub { + system("/home/expediteur/scripts/send_confirmation_email.sh"); + my $status = ($? >> 8); + if ($status != 0) { + status(500); + return "E-mail script exited with status $status"; + } +}; + +post '/send-email/newsletter' => sub { + system("/home/expediteur/scripts/send_newsletter.sh"); + my $status = ($? >> 8); + if ($status != 0) { + status(500); + return "E-mail script exited with status $status"; + } +}; + +any qr{.*} => sub { status 'not_found'; return "Invalid route" }; + +dance; + +true; diff --git a/sender/web-api/lib/Email/SpoofingDemo/DKIM.pm b/sender/web-api/lib/Email/SpoofingDemo/DKIM.pm new file mode 100644 index 0000000..281cdf0 --- /dev/null +++ b/sender/web-api/lib/Email/SpoofingDemo/DKIM.pm @@ -0,0 +1,157 @@ +package Email::SpoofingDemo::DKIM; + +use strict; +use warnings; +use v5.10; +use utf8; + +use Exporter 'import'; + +our @EXPORT_OK = qw(read_signing_table read_key_table + write_signing_table write_key_table + generate_dkim_key); + +sub generate_dkim_key { + my ($domain, $selector, $key_size, $key_table_name, $key_dir, $signing_table_name) = @_; + + die if $domain =~ /\.\./; + + my $key_domain_dir = "$key_dir/$domain"; + + # Generate the key + system("mkdir", "-p", $key_domain_dir); + system("opendkim-genkey", + "-D", $key_domain_dir, + "-d", $domain, + "-s", $selector, + "-b", $key_size); + system("chown", "-R", "opendkim", $key_domain_dir); + + # Read in the public key + my $public_key_file = "$key_domain_dir/$selector.txt"; + open(my $fh, '<', $public_key_file) or die "$key_domain_dir: $!"; + my $data = eval { + local $/ = undef; + my $raw_record = <$fh>; + my ($owner, $class, $type, $data) = split(" ", $raw_record, 4); + $data =~ s/\s*;.*$//; + return $data; + }; + close($fh); + + # Update key table + my $key_table = read_key_table($key_table_name); + push @{$key_table->{$domain}}, $selector; + write_key_table($key_table_name, $key_dir, $key_table); + + # Update signing table if it’s the first key for the domain + my $signing_table = read_signing_table($signing_table_name); + if (not exists $signing_table->{$domain}) { + $signing_table->{$domain} = $selector; + write_signing_table($signing_table_name, $signing_table); + } + + # Done! + reload_opendkim(); + + return $data; +} + +sub read_signing_table { + my ($filename) = @_; + + my %sign_table; + + open(my $fh, '<', $filename) or die "$filename: $!"; + while (<$fh>) { + chomp; + s/#.*$//; + next if /^\s*$/; + + my ($domain_or_email, $key_id) = split(" ", $_, 2); + my $domain = ($domain_or_email =~ s/^.*@//r); + my $selector = ($key_id =~ s/\._domainkey.$domain$//r); + + $sign_table{$domain} = $selector; + } + close($fh); + + return \%sign_table; +} + +sub write_signing_table { + my ($filename, $contents) = @_; + + open(my $fh, '>', $filename) or die "$filename: $!"; + binmode($fh, ':utf8'); + print $fh <<'EOF'; +## +## FORMAT DE LA TABLE +## +## +## +## L’adresse mail peut être un wildcard (ex. *@expediteur.example). +## + +EOF + for my $domain (sort keys %$contents) { + my $selector = $contents->{$domain}; + my $key_id = "$selector._domainkey.$domain"; + printf $fh "%-30s %s\n", $domain, $key_id; + } + close($fh); +} + +sub read_key_table { + my ($filename) = @_; + + # We only care about the list of keys that exist for a given domain. + # The rest of the data can be deduced from that mapping. + + my %key_table; + + open(my $fh, '<', $filename) or die "$filename: $!\n"; + while (<$fh>) { + chomp; + s/#.*$//; + next if /^\s*$/; + + my ($key_id, $key_spec) = split(" ", $_, 2); + my ($domain, $selector, $key_location) = split(":", $key_spec, 3); + + push @{$key_table{$domain}}, $selector; + } + + return \%key_table; +} + +sub write_key_table { + my ($filename, $key_dir, $contents) = @_; + + open(my $fh, '>', $filename) or die "$filename: $!\n"; + binmode($fh, ':utf8'); + print $fh < :: +## + +EOF + for my $domain (sort keys %$contents) { + for my $selector (@{$contents->{$domain}}) { + my $key_id = "$selector._domainkey.$domain"; + my $key_file = "$key_dir/$domain/$selector.private"; + + printf $fh "%-30s %s:%s:%s\n", $key_id, $domain, $selector, $key_file; + } + } + close($fh); +} + +sub reload_opendkim { + system(qw(killall -USR1 opendkim)); + return (($? >> 8) == 0); +} + +1; diff --git a/sender/web-api/public/dispatch.cgi b/sender/web-api/public/dispatch.cgi new file mode 100644 index 0000000..706ba0c --- /dev/null +++ b/sender/web-api/public/dispatch.cgi @@ -0,0 +1,16 @@ +#!/usr/bin/env perl +BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';} +use Dancer2; +use FindBin '$RealBin'; +use Plack::Runner; + +# For some reason Apache SetEnv directives don't propagate +# correctly to the dispatchers, so forcing PSGI and env here +# is safer. +set apphandler => 'PSGI'; +set environment => 'production'; + +my $psgi = path($RealBin, '..', 'bin', 'app.psgi'); +die "Unable to read startup script: $psgi" unless -r $psgi; + +Plack::Runner->run($psgi); diff --git a/sender/web-api/public/dispatch.fcgi b/sender/web-api/public/dispatch.fcgi new file mode 100644 index 0000000..ad42deb --- /dev/null +++ b/sender/web-api/public/dispatch.fcgi @@ -0,0 +1,18 @@ +#!/usr/bin/env perl +BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';} +use Dancer2; +use FindBin '$RealBin'; +use Plack::Handler::FCGI; + +# For some reason Apache SetEnv directives don't propagate +# correctly to the dispatchers, so forcing PSGI and env here +# is safer. +set apphandler => 'PSGI'; +set environment => 'production'; + +my $psgi = path($RealBin, '..', 'bin', 'app.psgi'); +my $app = do($psgi); +die "Unable to read startup script: $@" if $@; +my $server = Plack::Handler::FCGI->new(nproc => 5, detach => 1); + +$server->run($app);