#!/usr/bin/env perl
use strict;
use warnings;
use lib '../lib';
use Getopt::Long;
use IO::File;
use Crypt::LE ':errors';

sub info { print shift, "\n" }
sub bail { &info; exit 255 }

my ($account_key, $csr, $csr_key, $domains, $challenge_path, $crt, $live, $generate_missing, $generate_only, $verified, $debug, $help);

GetOptions ("key=s" => \$account_key, "csr=s" => \$csr, "csr-key=s" => \$csr_key, "domains=s" => \$domains, "path=s" => \$challenge_path,
            "crt=s" => \$crt, "generate-missing" => \$generate_missing, "generate-only" => \$generate_only, 
	    "live" => \$live, "debug" => \$debug, "verified" => \$verified, "help" => \$help);

my $usage = <<EOF;

Usage example: le.pl --key account.key --csr domain.csr --csr-key domain.key --crt domain.crt --domains "www.domain.ext,domain.ext" --generate-missing

Note: by default a staging server is used, which does not provide trusted certificates. This is to aviod hitting a rate limits on Let's Encrypt
      live server. To generate an actual certificate, always add --live option.

Available parameters:

 key <file>                 - Your account key file.
 csr <file>                 - Your CSR file.
 csr-key <file>             - Key for your CSR (only mandatory if CSR is missing and to be generated).
 domains <list>             - Domains as comma-separated list (only mandatory if CSR is missing).
 path <absolute path>       - Path to local .well-known/acme-challenge/ to drop required challenge files into (optional).
 generate-missing           - Generate missing files (key, csr and csr-key).
 generate-only              - Generate a new key and/or CSR if they are missing and then exit.
 crt <file>                 - Name for the domain certificate file.
 verified                   - Skip challenge verification steps (request/accept/verify). Use only if domains have been already verified recently.
 live                       - Connect to a live server instead of staging.
 debug                      - Print out debug messages.
 help                       - This screen.

EOF

info "\nCrypt::LE client started.\n";

bail $usage if $help;

unless (($account_key and (-r $account_key or $generate_missing)) and
        ($csr and (-r $csr or ($csr_key and $generate_missing)))) {
    info "Incorrect parameters - need both an account key and CSR either loaded or generated.";
    bail $usage;
}

if (!$generate_missing and ! -r $csr and (!$domains or $domains=~/^\s*$/)) {
    bail "Domain list should be provided to generate a CSR.";
}

unless ($crt) {
    bail "You would probably want to save the domain certificate somewhere - please specify a filename";
}

if ($challenge_path) {
    bail "Path to save challenge files into should be a writable directory" unless (-d $challenge_path and -w _);
}

# Begin work

my $le = Crypt::LE->new(debug => $debug, live => $live);

if (-r $account_key) {
    info "Loading an account key from $account_key";
    $le->load_account_key($account_key) == OK or bail "Could not load an account key: " . $le->error_details;
} else {
    info "Generating a new account key";
    $le->generate_account_key == OK or bail "Could not generate an account key: " . $le->error_details;
    info "Saving generated account key into $account_key";
    bail "Failed to save an account key file" if _write($account_key, $le->account_key);
}

if (-r $csr) {
    info "Loading a CSR from $csr";
    $le->load_csr($csr, $domains) == OK or bail "Could not load a CSR: " . $le->error_details;
} else {
    info "Generating a new CSR for domains '$domains'";
    $le->generate_csr($domains) == OK or bail "Could not generate a CSR: " . $le->error_details;
    info "Saving a new CSR into $csr";
    bail "Failed to save a CSR" if _write($csr, $le->csr);
    info "Saving a new CSR key into $csr_key";
    bail "Failed to save a CSR key" if _write($csr_key, $le->csr_key);
}

exit if $generate_only;

bail $le->error_details if $le->register;
$le->accept_tos(); # No need to check explicitly for tos_changed() - Crypt::LE will handle that for you.
unless ($verified) {
    bail $le->error_details if $le->request_challenge();
    bail $le->error_details if $le->accept_challenge(\&process_challenge);
    bail $le->error_details if $le->verify_challenge;
}
info "Requesting domain certificate";
bail $le->error_details if $le->request_certificate();
info "Requesting issuer's certificate";
if ($le->request_issuer_certificate()) {
    info "Could not download an issuer's certificate, try to download manually from " . $le->issuer_url;
    info "Will be saving the domain certificate alone, not the full chain (Qualys SSL test score will be capped to 'B' at best)";
    bail "Failed to save the domain certificate file" if _write($crt, $le->certificate);
} else {
    info "Saving the full certificate chain (With proper server configuration and HSTS you can get an 'A+' on Qualys SSL test)";
    bail "Failed to save the domain certificate file" if _write($crt, $le->certificate . "\n" . $le->issuer);
}

info "The job is done, enjoy your certificate! For feedback and bug reports contact me at https://do-know.com";

sub _write {
    my ($file, $content) = @_;
    return 1 unless ($file and $content);
    my $fh = IO::File->new($file, 'w');
    return 1 unless defined $fh;
    $fh->binmode;
    print $fh $content;
    $fh->close;
    return 0;
}

sub process_challenge {
    my $challenge = shift;
    my $text = "$challenge->{token}.$challenge->{fingerprint}";
    if ($challenge_path) {
        my $file = "$challenge_path/$challenge->{token}";
	if (_write($file, $text)) {
	   info "Failed to save a challenge file '$file' for domain '$challenge->{domain}'";
           return 0;
	} else {
           info "Successfully saved a challenge file '$file' for domain '$challenge->{domain}'";
           return 1;
        }
    }
    print <<EOF;
Challenge for $challenge->{domain} requires:
A file '$challenge->{token}' in '/.well-known/acme-challenge/' with the text: $text
When done, press <Enter>
EOF
 	<STDIN>;
	return 1;
};
