#!/usr/bin/env perl
BEGIN {
  $ENV{SSLMAKER_DEBUG} //= (grep {/^--silent/} @ARGV) ? 0 : 1;
}

package App::sslmaker::script;
use Getopt::App -complete;

use if -e 'lib/App/sslmaker.pm', qw(lib lib);
use App::sslmaker;
use File::Spec::Functions qw(catdir);
use Path::Tiny;

my @options = (
  'ext=s@    # Add extensions such as "subjectAltName=DNS:example.com" (default)',
  'subject=s # /C=US/ST=Texas/L=Dallas/O=Company/OU=Department/CN=example.com/emailAddress=user@domain',
  'bits=s    # SSL key bit size',
  'days=s    # Number of days the cert should be valid',
  'home=s    # sslmaker working directory',
  'root=s    # Path to root key.pem. Required for making intermediate key+cert',
  'silent    # Only output data on failure',
  'h|help    # Show this help text',
);

my $wrapper = sub {
  my ($sslmaker, $method, @args) = @_;

  eval { return $ENV{OPENSSL_CONF} ? $sslmaker->$method(@args) : $sslmaker->with_config($method, @args); } or do {
    $@ =~ s/\s+at\s+\S+\s+line.*/\n/s unless $ENV{TRACE};
    print STDERR $@;
    exit($? || $!);
  };
};

sub getopt_subcommands {
  my ($self) = @_;

  no warnings qw(once);
  return undef if $Getopt::App::SUBCOMMAND;

  return [
    [generate     => run(@options, $self->can('subcommand_generate')),       'Generate a client certificate'],
    [sign         => run(@options, $self->can('subcommand_sign')),           'Sign a certificate'],
    [revoke       => run(@options, $self->can('subcommand_revoke')),         'Revoke a certificate'],
    [intermediate => run(@options, $self->can('subcommand_intermediate')),   'Generate intermediate CA'],
    [root         => run(@options, $self->can('subcommand_root')),           'Generate root CA'],
    [dhparam      => run(@options, $self->can('subcommand_dhparam')),        'Create a dhparam file'],
    [nginx        => run(@options, $self->can('subcommand_nginx')),          'Print example nginx config'],
    [man          => run(@options, sub { exec perldoc => 'App::sslmaker' }), 'Show the sslmaker manual'],
  ];
}

sub home {
  return $_[0]->{home} if ref $_[0]->{home};    # Built

  my ($self) = @_;
  my $home = $self->{home} || $ENV{SSLMAKER_HOME};
  return $self->{home} = Path::Tiny->new($home)->absolute if $home;
  -w "/etc/$_" && return ($self->{home} = Path::Tiny->new("/etc/$_/sslmaker")->absolute) for qw(pki ssl);
  die "Cannot detect default --home. Maybe you have to run as root?\n";
}

sub root {
  return $_[0]->{root} if ref $_[0]->{root};    # Built

  my ($self) = @_;
  return $self->{root} = Path::Tiny->new($self->{root} || $self->home->child(qw(root ca.key.pem)))->absolute;
}

sub subcommand_revoke {
  my ($self, $cert) = @_;
  die "Usage: sslmaker revoke /path/to/file.cert\n" unless $cert;

  my $sslmaker   = App::sslmaker->new;
  my $passphrase = $self->home->child(qw(private passphrase));

  $sslmaker->$wrapper(
    revoke_cert => {
      cert       => $self->home->child(qw(certs ca.cert.pem)),
      key        => $self->home->child(qw(private ca.key.pem)),
      passphrase => -e $passphrase ? $passphrase : undef,
      revoke     => $cert,
    }
  );

  return 0;
}

sub subcommand_root {
  my ($self) = @_;
  die "--subject is required\n" unless $self->{subject};

  my $args = {
    bits       => $self->{bits} || 8192,
    cert       => $self->_root_file('cert'),
    days       => $self->{days} || 365 * 30,
    home       => $self->root->parent,
    key        => $self->_root_file('key'),
    passphrase => $self->root->parent->child(qw(passphrase)),
  };

  my $sslmaker = App::sslmaker->new;
  $sslmaker->_d('# Root CA settings');
  $sslmaker->_d(sprintf '- %-12s %s', "$_:", "$args->{$_}") for sort keys %$args;
  $sslmaker->subject($self->{subject});
  $sslmaker->make_directories({home => $args->{home}, templates => 1});
  $self->run_maybe($args->{key},  sub { $sslmaker->$wrapper(make_key  => $args) });
  $self->run_maybe($args->{cert}, sub { $sslmaker->$wrapper(make_cert => $args) });
}

sub subcommand_dhparam {
  my ($self, $path, $bits) = @_;
  $path ||= $self->home->child('dhparam.pem');
  $self->run_maybe($path, sub { App::sslmaker::openssl(qw(dhparam -out) => $path, $bits || 2048) });
}

sub subcommand_generate {
  my ($self, $cn) = @_;
  die "Usage: sslmaker generate <CN>\n" unless $cn;

  my $args = {
    bits    => $self->{bits},
    csr     => "$cn.csr.pem",
    days    => $self->{days},
    ext     => $self->{ext} || ["subjectAltName=DNS:$cn"],
    home    => $self->home,
    key     => "$cn.key.pem",
    subject => "/CN=$cn",
  };

  my $sslmaker          = App::sslmaker->new;
  my $intermediate_cert = $self->home->child(qw(certs ca.cert.pem));
  $sslmaker->subject(-r $intermediate_cert ? ($intermediate_cert, $self->{subject}) : ($self->{subject}));
  $self->run_maybe($args->{key}, sub { $sslmaker->make_key($args) });
  $self->run_maybe($args->{csr}, sub { $sslmaker->make_csr($args) });
  $sslmaker->_d("# It is safe to send $args->{csr} to SSL admin for signing.");
}

sub subcommand_intermediate {
  my ($self) = @_;
  $self->{bits} ||= 8192;

  my $home = $self->home;
  my $args = {
    bits       => $self->{bits},
    ca_cert    => $self->_root_file('cert'),
    ca_key     => $self->_root_file('key'),
    cert       => $home->child(qw(certs ca.cert.pem)),
    csr        => $home->child(qw(certs ca.csr.pem)),
    days       => $self->{days} || 365 * 28,
    extensions => 'v3_ca',
    home       => $home,
    key        => $home->child(qw(private ca.key.pem)),
    passphrase => $home->child(qw(private passphrase)),
  };

  my $sslmaker = App::sslmaker->new;
  $sslmaker->_d('# Intermediate CA settings');
  $sslmaker->_d(sprintf '- %-12s %s', "$_:", "$args->{$_}") for sort keys %$args;
  $sslmaker->_d('');
  $sslmaker->subject($args->{ca_cert}, $self->{subject});
  $sslmaker->make_directories({home => $home, templates => 1});
  $self->run_maybe($args->{key}, sub { $sslmaker->$wrapper(make_key => $args) });
  $self->run_maybe($args->{csr}, sub { $sslmaker->$wrapper(make_csr => $args) });

  $args->{home}       = $self->root->parent;
  $args->{passphrase} = $self->root->parent->child(qw(passphrase));
  $self->run_maybe($args->{cert}, sub { $sslmaker->$wrapper(sign_csr => $args) });

  $args->{chain_cert} = $home->child(qw(certs ca-chain.cert.pem));
  $sslmaker->_cat(@$args{qw( cert ca_cert chain_cert )});
  $sslmaker->_d("# Generated $args->{chain_cert} from CA and intermediate certificate");

  $sslmaker->openssl(
    verify => -CAfile => @$args{qw( ca_cert cert )},
    sub {
      my ($sslmaker, $output) = @_;
      die $output if $output =~ /error/;
    }
  );

  return 0;
}

sub subcommand_nginx {
  my ($self, $domain) = @_;
  die "Usage: sslmaker nginx <domain>\n" unless $domain;

  print +App::sslmaker->_render_template(
    'nginx.config',
    {
      domain  => $domain,
      key     => "/etc/nginx/ssl/$domain.key.pem",
      cert    => "/etc/nginx/ssl/$domain.cert.pem",
      ca_cert => $self->home->child(qw(certs ca-chain.cert.pem)),
    },
  );

  return 0;
}

sub subcommand_sign {
  my ($self, $csr, $cert) = @_;
  my $home = $self->home;
  die "Usage: sslmaker sign <csr> [cert]\n" unless $csr;

  $cert ||= do { local $_ = $csr; s!(\.csr)?\.pem$!\.cert.pem!; $_ };
  my $sslmaker = App::sslmaker->new;
  $sslmaker->$wrapper(
    sign_csr => {
      home       => $home,
      ca_cert    => $home->child(qw(certs ca.cert.pem)),
      ca_key     => $home->child(qw(private ca.key.pem)),
      cert       => $cert,
      csr        => $csr,
      days       => $self->{days},
      extensions => 'usr_cert',
      passphrase => $home->child(qw(private passphrase)),
    }
  );

  $sslmaker->_d("# Generated $cert");
  $sslmaker->_d("# Run this command for more details: openssl x509 -in $cert -noout -text");
}

sub run_maybe {
  my ($self, $file, $cb) = @_;
  return App::sslmaker->_d("! File $file exists.") if -e $file;
  $self->$cb;
  App::sslmaker->_d("# Generated $_[1]");
}

sub _root_file {
  my ($self, $ext) = @_;
  my $base = $self->root->basename;
  $base =~ s!\b(cert|key)\b!$ext!;
  return $self->root->parent->child($base);
}

run(
  @options,
  sub {
    my ($self, $action, @args) = @_;
    return print extract_usage if $self->{h};
    return print extract_usage unless $action;
  },
);
