kopia lustrzana https://github.com/alecmuffett/eotk
402 wiersze
11 KiB
Perl
Executable File
402 wiersze
11 KiB
Perl
Executable File
#!/usr/bin/perl -w
|
|
# eotk (c) 2017 Alec Muffett
|
|
|
|
use Data::Dumper;
|
|
|
|
die "$0: needs EOTK_HOME environment variable to be set\n"
|
|
unless (defined($ENV{EOTK_HOME}));
|
|
|
|
# state
|
|
|
|
my %projects = ();
|
|
my $unset_variable = "<-UNSET-VARIABLE->";
|
|
my $here = $ENV{EOTK_HOME};
|
|
|
|
chdir($here) or die "chdir: $here: $!\n";
|
|
|
|
##################################################################
|
|
|
|
sub SetEnv {
|
|
my ($var, $val, $why) = @_;
|
|
die "bad varname: $var\n" if ($var !~ /^[A-Za-z_]\w+/);
|
|
$var =~ tr/a-z/A-Z/;
|
|
$ENV{$var} = $val;
|
|
warn (($why ? "($why) " : "") . "set $var to $val\n");
|
|
}
|
|
|
|
sub RunOrDie {
|
|
warn "RunOrDie @_\n";
|
|
(system(@_) == 0) or die "system: @_: $!\n";
|
|
}
|
|
|
|
sub GoAndRun {
|
|
warn "GoAndRun @_\n";
|
|
my ($where, @cmd) = @_;
|
|
chdir($where) or die "chdir: $where: $!\n";
|
|
&RunOrDie(@cmd);
|
|
chdir($here) or die "chdir: $here: $!\n";
|
|
}
|
|
|
|
sub Pipeify {
|
|
my ($cmd, @hashreflist) = @_; # there WILL be shell syntax in $cmd
|
|
warn "Pipeify $cmd ...\n";
|
|
|
|
my $first = $hashreflist[0];
|
|
|
|
if (!defined($first)) {
|
|
warn "Pipeify note: empty template\n";
|
|
}
|
|
|
|
my @vars = sort keys %{$first}; # get column names, assume uniformity
|
|
|
|
open(PIPE, "|$cmd") or die "popen: $cmd: $!\n";
|
|
warn "head @vars\n";
|
|
print PIPE join(" ", @vars), "\n"; # send the column names, even if "empty"
|
|
|
|
foreach my $hashref (@hashreflist) { # send the rows
|
|
my @row = ();
|
|
foreach my $var (@vars) {
|
|
my $val = ${$hashref}{$var};
|
|
die "Pipeify: empty var $var\n" unless ($val ne "");
|
|
die "Pipeify: whitespace var $var: $val\n" if ($val =~ /\s/);
|
|
push(@row, $val);
|
|
}
|
|
warn "body @row\n";
|
|
print PIPE join(" ", @row), "\n";
|
|
}
|
|
close(PIPE) or die "pclose: $cmd: $!\n";
|
|
}
|
|
|
|
sub MakeDir {
|
|
warn "MakeDir @_\n";
|
|
my $path = shift;
|
|
die "MakeDir: empty path?\n" unless ($path);
|
|
if (! -d $path) {
|
|
&RunOrDie("mkdir", "-p", $path);
|
|
}
|
|
chmod(0700, $path) or die "chmod: $path: $!\n";
|
|
}
|
|
|
|
sub CopyFile {
|
|
warn "CopyFile: @_\n";
|
|
my ($from, $to) = @_;
|
|
&RunOrDie("cp", $from, $to);
|
|
chmod(0600, $to) or die "chmod: $to: $!\n";
|
|
}
|
|
|
|
sub Dotify { # dotify a regexp
|
|
warn "Dotify @_\n";
|
|
my $arg = shift;
|
|
$arg =~ s!\.!\\.!g;
|
|
return $arg;
|
|
}
|
|
|
|
##################################################################
|
|
|
|
# $projects{$project}{ROWS} = [ {}, {}, ... ] # see $row
|
|
# $projects{$project}{SUBDOMAINS} = {} # keys-only
|
|
# $projects{$project}{FIRST_ONION} = ""
|
|
# $projects{$project}{TYPE} = ""
|
|
# $projects{$project}{IS_SOFTMAP} = 0/1
|
|
|
|
# each $row = {
|
|
# DNS_DOMAIN
|
|
# DNS_DOMAIN_RE
|
|
# ONION_ADDRESS
|
|
# ONION_ADDRESS_RE
|
|
# KEYFILE
|
|
# }
|
|
|
|
# also, calling the templater will dynamically set:
|
|
# PROJECT
|
|
# PROJECT_DIR
|
|
|
|
sub DoMap {
|
|
warn "DoMap @_\n";
|
|
my ($what, $from, $to, @subdomains) = @_;
|
|
my $foo;
|
|
|
|
my $project = $ENV{PROJECT}; # the instantaneous value of `project` is stored
|
|
|
|
if (!defined($projects{$project})) {
|
|
$projects{$project} = {};
|
|
}
|
|
|
|
if (!defined($projects{$project}{TYPE})) {
|
|
$projects{$project}{TYPE} = $what;
|
|
$projects{$project}{IS_SOFTMAP} = ($what eq "softmap" ? 1 : 0);
|
|
}
|
|
|
|
$foo = $projects{$project}{TYPE};
|
|
if ($foo ne $what) {
|
|
die "DoMap: you cannot add $what ($from/$to) to an existing $foo project\n";
|
|
}
|
|
|
|
my $keyfile = $unset_variable;
|
|
my $onion;
|
|
|
|
if ($what eq "hardmap") {
|
|
$keyfile = $from;
|
|
die "map: $keyfile: no such file\n" unless -f ($keyfile);
|
|
$onion = $keyfile;
|
|
$onion =~ s!^.*/!!;
|
|
$onion =~ s!\.(key|pem)!!;
|
|
$onion =~ s!\.(onion)!!; # cleanup dups
|
|
}
|
|
elsif ($what eq "softmap") {
|
|
$onion = $from;
|
|
$onion =~ s!\.(onion)!!; # cleanup dups
|
|
die "map: $onion: bad onion address\n" unless ($onion =~ /^[a-z2-7]{16}$/);
|
|
}
|
|
else {
|
|
die "wtf?\n";
|
|
}
|
|
$onion = "$onion.onion"; # restore trailing .onion
|
|
|
|
warn "mapping $what($keyfile) from=$onion to=$to san=(@subdomains)\n";
|
|
|
|
if (!defined($projects{$project}{FIRST_ONION})) {
|
|
$projects{$project}{FIRST_ONION} = $onion;
|
|
}
|
|
|
|
# populate the subdomains
|
|
$projects{$project}{SUBDOMAINS}{$onion} = 1;
|
|
foreach my $sd (@subdomains) {
|
|
$projects{$project}{SUBDOMAINS}{"$sd.$onion"} = 1;
|
|
}
|
|
|
|
# create the row
|
|
my %row = ();
|
|
$row{DNS_DOMAIN} = $to;
|
|
$row{DNS_DOMAIN_RE} = &Dotify($to);
|
|
$row{DNS_DOMAIN_RE2} = &Dotify(&Dotify($to));
|
|
$row{ONION_ADDRESS} = $onion;
|
|
$row{ONION_ADDRESS_RE} = &Dotify($onion);
|
|
$row{ONION_ADDRESS_RE2} = &Dotify(&Dotify($onion));
|
|
$row{KEYFILE} = $keyfile;
|
|
|
|
warn Dumper(\%row);
|
|
|
|
# push a reference to the row
|
|
push(@{$projects{$project}{ROWS}}, \%row);
|
|
}
|
|
|
|
sub DoProject {
|
|
warn "DoProject @_\n";
|
|
my $project = shift;
|
|
|
|
# set the ephemerals
|
|
&SetEnv("project", $project, "defaulting");
|
|
&SetEnv("project_dir", "$ENV{PROJECTS_HOME}/$ENV{PROJECT}.d", "defaulting");
|
|
&SetEnv("log_dir", "$ENV{PROJECT_DIR}/log.d", "defaulting");
|
|
&SetEnv("ssl_dir", "$ENV{PROJECT_DIR}/ssl.d", "defaulting");
|
|
&SetEnv("is_softmap", $projects{$project}{IS_SOFTMAP}, "inheriting");
|
|
# tor_dir is a very special ephemeral, just for softmap, more later
|
|
|
|
# make the directories
|
|
&MakeDir($ENV{PROJECT_DIR});
|
|
&MakeDir($ENV{SSL_DIR});
|
|
&MakeDir($ENV{LOG_DIR});
|
|
|
|
# set the CommonName for the project cert; this is the first onion encountered:
|
|
my $cert_prefix;
|
|
|
|
if (defined($ENV{CERT_COMMON_NAME})) {
|
|
$cert_prefix = $ENV{CERT_COMMON_NAME};
|
|
}
|
|
else {
|
|
if ($ENV{IS_SOFTMAP}) {
|
|
$cert_prefix = "$project.local";
|
|
}
|
|
else {
|
|
$cert_prefix = $projects{$project}{FIRST_ONION};
|
|
}
|
|
}
|
|
die "empty cert_prefix in project $project\n" unless (defined($cert_prefix));
|
|
&SetEnv("cert_prefix", $cert_prefix);
|
|
|
|
# clean up the SAN list; purge the CommonName for deduplication
|
|
delete($projects{$project}{SUBDOMAINS}{$cert_prefix});
|
|
my @sanlist = sort keys %{$projects{$project}{SUBDOMAINS}};
|
|
|
|
# debugging
|
|
warn "commit $ENV{PROJECT} san $cert_prefix @sanlist\n";
|
|
|
|
# cert generation
|
|
$cert = "$ENV{SSL_DIR}/$cert_prefix.cert";
|
|
if (-f $cert) {
|
|
warn "$cert exists!";
|
|
}
|
|
else {
|
|
warn "making cert for $cert_prefix\n";
|
|
&GoAndRun(
|
|
$ENV{SSL_DIR},
|
|
$ENV{SSL_TOOL},
|
|
$cert_prefix, # must be first argument
|
|
@sanlist
|
|
);
|
|
}
|
|
|
|
# nginx config: feed the rows to the template
|
|
my $cmd = "$ENV{TEMPLATE_TOOL} $ENV{NGINX_TEMPLATE} >$ENV{PROJECT_DIR}/nginx.conf";
|
|
&Pipeify($cmd, @{$projects{$project}{ROWS}});
|
|
|
|
# tor directory generation; we may nuke LOG_DIR in the process
|
|
if ($ENV{IS_SOFTMAP}) {
|
|
# for each enumerated worker
|
|
foreach my $i (1..$ENV{SOFTMAP_TOR_WORKERS}) {
|
|
# make worker directory
|
|
my $hs_dir = "$ENV{PROJECT_DIR}/$ENV{TOR_WORKER_PREFIX}-$i.d";
|
|
&MakeDir($hs_dir);
|
|
|
|
# no keys to install, no poisoning to deal with
|
|
|
|
# tor config using TOR_DIR to drop everything in there
|
|
&SetEnv("tor_dir", $hs_dir, "softmap");
|
|
my $cmd = "$ENV{TEMPLATE_TOOL} $ENV{TOR_TEMPLATE} >$hs_dir/tor.conf";
|
|
&Pipeify($cmd, @{$projects{$project}{ROWS}});
|
|
}
|
|
}
|
|
else {
|
|
# setup tor hs directories and keys
|
|
foreach my $row (@{$projects{$project}{ROWS}}) {
|
|
# what dns?
|
|
my $dns = ${$row}{DNS_DOMAIN};
|
|
|
|
# which onion?
|
|
my $onion = ${$row}{ONION_ADDRESS};
|
|
|
|
# make hs directory
|
|
my $hs_dir = "$ENV{PROJECT_DIR}/$onion.d";
|
|
&MakeDir($hs_dir);
|
|
|
|
# install keyfile
|
|
my $keyfile = ${$row}{KEYFILE};
|
|
&CopyFile($keyfile, "$hs_dir/private_key");
|
|
|
|
# bypass key-poisoning obscurity hassle
|
|
$poison = "$hs_dir/onion_service_non_anonymous";
|
|
if ($ENV{TOR_SINGLE_ONION}) {
|
|
&CopyFile("/dev/null", $poison) if (! -f $poison);
|
|
}
|
|
else {
|
|
unlink($poison) if (-f $poison);
|
|
}
|
|
}
|
|
|
|
# tor config
|
|
my $cmd = "$ENV{TEMPLATE_TOOL} $ENV{TOR_TEMPLATE} >$ENV{PROJECT_DIR}/tor.conf";
|
|
&Pipeify($cmd, @{$projects{$project}{ROWS}});
|
|
}
|
|
|
|
# various scripts
|
|
foreach my $script (split(" ", $ENV{SCRIPT_NAMES})) {
|
|
my $path = "$ENV{PROJECT_DIR}/$script";
|
|
my $cmd = "$ENV{TEMPLATE_TOOL} templates.d/$script.txt >$path";
|
|
&Pipeify($cmd, @{$projects{$project}{ROWS}});
|
|
chmod(0700, $path) or die "chmod: $path: $!\n";
|
|
}
|
|
|
|
# tell the user
|
|
foreach my $row (@{$projects{$project}{ROWS}}) {
|
|
print "mapping ${$row}{ONION_ADDRESS} to ${$row}{DNS_DOMAIN} project $project\n";
|
|
}
|
|
}
|
|
|
|
|
|
##################################################################
|
|
|
|
# where stuff lives
|
|
|
|
&SetEnv("projects_home", "$here/projects.d");
|
|
&SetEnv("project", "default");
|
|
|
|
&SetEnv("nginx_template", "$here/templates.d/nginx.conf.txt");
|
|
&SetEnv("tor_template", "$here/templates.d/tor.conf.txt");
|
|
|
|
&SetEnv("ssl_tool", "$here/lib.d/make-selfsigned-wildcard-ssl-cert.sh");
|
|
&SetEnv("template_tool", "$here/lib.d/expand-template.pl");
|
|
|
|
# in-template settings
|
|
|
|
&SetEnv("nginx_hello_onion", 1);
|
|
&SetEnv("nginx_resolver", "8.8.8.8");
|
|
&SetEnv("nginx_rlim", 1024);
|
|
&SetEnv("nginx_timeout", 30);
|
|
&SetEnv("nginx_workers", 5);
|
|
|
|
&SetEnv("tor_intros_per_daemon", 3);
|
|
&SetEnv("tor_single_onion", 1);
|
|
&SetEnv("tor_worker_prefix", "hs");
|
|
|
|
&SetEnv("softmap_tor_workers", 4);
|
|
&SetEnv("softmap_nginx_workers", 5 * 4); # nginx_workers * softmap_tor_workers
|
|
|
|
&SetEnv("header_csp_suppress", 1); # eventually header_csp_rewrite?
|
|
&SetEnv("header_hpkp_suppress", 1);
|
|
&SetEnv("header_hsts_suppress", 1);
|
|
|
|
&SetEnv("SCRIPT_NAMES", "bounce.sh debugoff.sh debugon.sh harvest.sh maps.sh nxreload.sh start.sh status.sh stop.sh syntax.sh");
|
|
&SetEnv("SCRIPT_PAUSE", 5);
|
|
|
|
# dynamic settings: overridable / may be given a global setting
|
|
|
|
&SetEnv("CERT_PREFIX", $unset_variable);
|
|
&SetEnv("LOG_DIR", $unset_variable);
|
|
&SetEnv("SSL_DIR", $unset_variable);
|
|
|
|
# nb: see DoMap() for non-overridable, project-local settings
|
|
|
|
# fetch the config
|
|
|
|
if ($#ARGV < 0) {
|
|
$config = "onions.conf"; # in $here
|
|
}
|
|
else {
|
|
$config = $ARGV[0];
|
|
}
|
|
|
|
die "$config: no such file / missing configuration: $config\n" unless (-f $config);
|
|
|
|
open(CONFIG, $config) or die "$config: $!\n";
|
|
chomp(@config = grep(!/^\s*(#.*)?$/, <CONFIG>));
|
|
close(CONFIG);
|
|
|
|
# run it
|
|
|
|
foreach (@config) {
|
|
my ($cmd, @args) = split;
|
|
|
|
if ($cmd eq "set") {
|
|
my $var = shift(@args);
|
|
my $val = "@args";
|
|
&SetEnv($var, $val);
|
|
}
|
|
elsif ($cmd eq "hardmap") {
|
|
&DoMap($cmd, @args);
|
|
}
|
|
elsif ($cmd eq "softmap") {
|
|
&DoMap($cmd, @args);
|
|
}
|
|
else {
|
|
die "error: what does '$cmd @args' mean?";
|
|
}
|
|
}
|
|
|
|
# what did we get?
|
|
warn "dumping final state\n";
|
|
warn Dumper(\%projects);
|
|
|
|
# create a home
|
|
&MakeDir($ENV{PROJECTS_HOME});
|
|
|
|
# lay it out
|
|
foreach my $project (sort keys %projects) {
|
|
&DoProject($project);
|
|
}
|
|
|
|
# done
|
|
exit 0;
|
|
|
|
##################################################################
|