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:
		
							parent
							
								
									31f08bb329
								
							
						
					
					
						commit
						30cf2e5a9f
					
				| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
							
								
								
									
										2
									
								
								sender/etc/s6-overlay/s6-rc.d/api/run
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								sender/etc/s6-overlay/s6-rc.d/api/run
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| #!/bin/execlineb -P | ||||
| /usr/bin/env perl /src/api/bin/app.psgi | ||||
							
								
								
									
										1
									
								
								sender/etc/s6-overlay/s6-rc.d/api/type
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								sender/etc/s6-overlay/s6-rc.d/api/type
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| longrun | ||||
							
								
								
									
										0
									
								
								sender/etc/s6-overlay/s6-rc.d/user/contents.d/api
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sender/etc/s6-overlay/s6-rc.d/user/contents.d/api
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								sender/web-api/.dancer
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sender/web-api/.dancer
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								sender/web-api/MANIFEST
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								sender/web-api/MANIFEST
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										17
									
								
								sender/web-api/MANIFEST.SKIP
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								sender/web-api/MANIFEST.SKIP
									
									
									
									
									
										Normal 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- | ||||
							
								
								
									
										26
									
								
								sender/web-api/Makefile.PL
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								sender/web-api/Makefile.PL
									
									
									
									
									
										Normal 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-*' }, | ||||
| ); | ||||
							
								
								
									
										9
									
								
								sender/web-api/bin/app.psgi
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								sender/web-api/bin/app.psgi
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										4
									
								
								sender/web-api/config.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								sender/web-api/config.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| 
 | ||||
| appname: "Email::SpoofingDemo::API::Sender" | ||||
| charset: "UTF-8" | ||||
| serializer: JSON | ||||
							
								
								
									
										11
									
								
								sender/web-api/cpanfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								sender/web-api/cpanfile
									
									
									
									
									
										Normal 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"; | ||||
| }; | ||||
							
								
								
									
										14
									
								
								sender/web-api/environments/development.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								sender/web-api/environments/development.yml
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										16
									
								
								sender/web-api/environments/production.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								sender/web-api/environments/production.yml
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										74
									
								
								sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										157
									
								
								sender/web-api/lib/Email/SpoofingDemo/DKIM.pm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								sender/web-api/lib/Email/SpoofingDemo/DKIM.pm
									
									
									
									
									
										Normal 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 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 | ||||
| ## | ||||
| ## <domaine ou adresse mail>    <identifiant> | ||||
| ## | ||||
| ## 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; | ||||
| ## | ||||
| ## 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; | ||||
							
								
								
									
										16
									
								
								sender/web-api/public/dispatch.cgi
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								sender/web-api/public/dispatch.cgi
									
									
									
									
									
										Normal 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); | ||||
							
								
								
									
										18
									
								
								sender/web-api/public/dispatch.fcgi
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								sender/web-api/public/dispatch.fcgi
									
									
									
									
									
										Normal 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); | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Marc van der Wal
						Marc van der Wal