API REST pour l’émetteur de mails

On peut générer des clefs DKIM mais on ne peut pas encore choisir quel
sélecteur utiliser pour signer les mails sortants.

Une fois DKIM activé pour un domaine, on ne peut pas non plus le
désactiver.
This commit is contained in:
Marc van der Wal 2023-10-25 15:50:23 +02:00
parent 31f08bb329
commit 30cf2e5a9f
18 changed files with 442 additions and 3 deletions

View File

@ -14,6 +14,54 @@ RUN apk add \
s6-overlay \ s6-overlay \
vim 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 newaliases
RUN install -m 0700 -o opendkim -g opendkim -d /run/opendkim 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/postfix /etc/postfix
COPY etc/opendkim /etc/opendkim COPY etc/opendkim /etc/opendkim
COPY web-api /src/api
ENTRYPOINT ["/init"] ENTRYPOINT ["/init"]
# Ne pas positionner USER, ou sinon les services ne démarreront pas de manière # Ne pas positionner USER, ou sinon les services ne démarreront pas de manière

View File

@ -17,6 +17,6 @@ SendReports yes
## Il vaut donc mieux paramétrer une SigningTable (qui liste les expéditeurs ## 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 ## pour lesquels on signe) et une KeyTable (qui liste les emplacements des
## clefs privées). ## clefs privées).
#
# SigningTable file:/etc/opendkim/signing_table SigningTable file:/etc/opendkim/signing_table
# KeyTable file:/etc/opendkim/key_table KeyTable file:/etc/opendkim/key_table

View File

@ -0,0 +1,2 @@
#!/bin/execlineb -P
/usr/bin/env perl /src/api/bin/app.psgi

View File

@ -0,0 +1 @@
longrun

0
sender/web-api/.dancer Normal file
View File

24
sender/web-api/MANIFEST Normal file
View File

@ -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

View File

@ -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-

View File

@ -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 <marc.vanderwal@afnic.fr>},
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-*' },
);

View File

@ -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;

View File

@ -0,0 +1,4 @@
appname: "Email::SpoofingDemo::API::Sender"
charset: "UTF-8"
serializer: JSON

11
sender/web-api/cpanfile Normal file
View File

@ -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";
};

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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 its 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
##
## <domaine ou adresse mail> <identifiant>
##
## Ladresse 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;
##
## FORMAT DE LA TABLE
##
## <identifiant> <domaine>:<sélecteur>:<fichier>
##
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;

View File

@ -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);

View File

@ -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);