<commit_before>#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use IO::File;
use JSON::MaybeXS;
use Log::Log4perl;
use Log::Log4perl::Level;
use Module::Load;
use Encode 'decode';
use Digest::SHA 'sha256';
use MIME::Base64 'encode_base64url';
use Crypt::LE ':errors', ':keys';
use utf8;

my $VERSION = '0.28a';

exit main();

sub main {
    Log::Log4perl->easy_init({ utf8  => 1 });
    my $opt = { logger => Log::Log4perl->get_logger(), e => encode_args() };
    binmode(STDOUT, ":encoding(UTF-8)");
    if (my $rv = work($opt)) {
        $opt->{logger}->error($rv->{'msg'}) if $rv->{'msg'};
        return defined $rv->{'code'} ? $rv->{'code'} : 255;
    }
    return 0;
}

sub work {
    my $opt = shift;
    my $rv = parse_options($opt);
    return $rv if $rv;

    my $le = Crypt::LE->new(autodir => 0, server => $opt->{'server'}, live => $opt->{'live'}, debug => $opt->{'debug'}, logger => $opt->{'logger'});

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

    if ($opt->{'update-contacts'}) {
        # Register.
        my $reg = _register($le, $opt);
        return $reg if $reg;
        my @contacts = (lc($opt->{'update-contacts'}) eq 'none') ? () : grep { $_ } split /\s*\,\s*/, $opt->{'update-contacts'};
        my @rejected = ();
        foreach (@contacts) {
            /^(\w+:)?(.+)$/;
            # NB: tel is not supported by LE at the moment.
            my ($prefix, $data) = (lc($1||''), $2);
            push @rejected, $_ unless ($data=~/^[^\@]+\@[^\.]+\.[^\.]+/ and (!$prefix or ($prefix eq 'mailto:')));
        }
        return _error("Unknown format for the contacts: " . join(", ", @rejected)) if @rejected;
        return _error("Could not update contact details: " . $le->error_details) if $le->update_contacts(\@contacts);
        $opt->{'logger'}->info("Contact details have been updated.");
        return;
    }

    if ($opt->{'revoke'}) {
        my $crt = _read($opt->{'crt'});
        return _error("Could not read the certificate file.") unless $crt;
        # Take the first certificate in file, disregard the issuer's one.
        $crt=~s/^(.*?-+\s*END CERTIFICATE\s*-+).*/$1/s;
        my $rv = $le->revoke_certificate(\$crt);
        if ($rv == OK) {
            $opt->{'logger'}->info("Certificate has been revoked.");
        } elsif ($rv == ALREADY_DONE) {
            $opt->{'logger'}->info("Certificate has been ALREADY revoked.");
        } else {
            return _error("Problem with revoking certificate: " . $le->error_details);
        }
        return;
    }

    if ($opt->{'domains'}) {
        if ($opt->{'e'}) {
            $opt->{'logger'}->warn("Could not encode arguments, support for internationalized domain names may not be available.");
        } else {
            my @domains = grep { $_ } split /\s*\,\s*/, $opt->{'domains'};
            $opt->{'domains'} = join ",", map { _puny($_) } @domains;
        }
    }
    if (-r $opt->{'csr'}) {
        $opt->{'logger'}->info("Loading a CSR from $opt->{'csr'}");
        $le->load_csr($opt->{'csr'}, $opt->{'domains'}) == OK or return _error("Could not load a CSR: " . $le->error_details);
        return _error("For multi-webroot path usage, the amount of paths given should match the amount of domain names listed.") if _path_mismatch($le, $opt);
        # Load existing CSR key if specified, even if we have CSR (for example for PFX export).
        if ($opt->{'csr-key'} and -e $opt->{'csr-key'}) {
            return _error("Could not load existing CSR key from $opt->{'csr-key'} - " . $le->error_details) if $le->load_csr_key($opt->{'csr-key'});
        }
    } else {
        return _error("For multi-webroot path usage, the amount of paths given should match the amount of domain names listed.") if _path_mismatch($le, $opt);
        $opt->{'logger'}->info("Generating a new CSR for domains $opt->{'domains'}");
        if (-e $opt->{'csr-key'}) {
             # Allow using pre-existing key when generating CSR
             return _error("Could not load existing CSR key from $opt->{'csr-key'} - " . $le->error_details) if $le->load_csr_key($opt->{'csr-key'});
             $opt->{'logger'}->info("New CSR will be based on '$opt->{'csr-key'}' key");
        } else {
             $opt->{'logger'}->info("New CSR will be based on a generated key");
        }
        my ($type, $attr) = $opt->{'curve'} ? (KEY_ECC, $opt->{'curve'}) : (KEY_RSA, $opt->{'legacy'} ? 2048 : 4096);
        $le->generate_csr($opt->{'domains'}, $type, $attr) == OK or return _error("Could not generate a CSR: " . $le->error_details);
        $opt->{'logger'}->info("Saving a new CSR into $opt->{'csr'}");
        return "Failed to save a CSR" if _write($opt->{'csr'}, $le->csr);
        unless (-e $opt->{'csr-key'}) {
            $opt->{'logger'}->info("Saving a new CSR key into $opt->{'csr-key'}");
            return _error("Failed to save a CSR key") if _write($opt->{'csr-key'}, $le->csr_key);
        }
    }

    return if $opt->{'generate-only'};

    if ($opt->{'renew'}) {
        if ($opt->{'crt'} and -r $opt->{'crt'}) {
            $opt->{'logger'}->info("Checking certificate for expiration (local file).");
            $opt->{'expires'} = $le->check_expiration($opt->{'crt'});
            $opt->{'logger'}->warn("Problem checking existing certificate file.") unless (defined $opt->{'expires'});
        }
        unless (defined $opt->{'expires'}) {
            $opt->{'logger'}->info("Checking certificate for expiration (website connection).");
            foreach my $domain (@{$le->domains}) {
                $opt->{'logger'}->info("Checking $domain");
                $opt->{'expires'} = $le->check_expiration("https://$domain/");
                last if (defined $opt->{'expires'});
            }
        }
        return _error("Could not get the certificate expiration value, cannot renew.") unless (defined $opt->{'expires'});
        if ($opt->{'expires'} > $opt->{'renew'}) {
            $opt->{'logger'}->info("Too early for renewal, certificate expires in $opt->{'expires'} days.");
            return;
        }
        $opt->{'logger'}->info("Expiration threshold set at $opt->{'renew'} days, the certificate " . ($opt->{'expires'} < 0 ? "has already expired" : "expires in $opt->{'expires'} days") . " - will be renewing.");
    }
    
    if ($opt->{'email'}) {
        return _error($le->error_details) if $le->set_account_email($opt->{'email'});
    }

    # Register.
    my $reg = _register($le, $opt);
    return $reg if $reg;

    # We might not need to re-verify, verification holds for a while.
    my $new_crt_status = $le->request_certificate();
    unless ($new_crt_status) {
        $opt->{'logger'}->info("Received domain certificate, no validation required at this time.");
    } else {
        # If it's not an auth problem, but blacklisted domains for example - stop.
        return _error("Error requesting certificate: " . $le->error_details) if $new_crt_status != AUTH_ERROR;
        # Add multi-webroot option to parameters passed if it is set.
        $opt->{'handle-params'}->{'multiroot'} = $opt->{'multiroot'} if $opt->{'multiroot'};
        # Handle DNS internally along with HTTP
        my ($challenge_handler, $verification_handler) = ($opt->{'handler'}, $opt->{'handler'});
        if (!$opt->{'handler'}) {
            if ($opt->{'handle-as'}) {
                return _error("Only 'http' and 'dns' can be handled internally, use external modules for other verification types.") unless $opt->{'handle-as'}=~/^(http|dns)$/i;
                if (lc($1) eq 'dns') {
                    ($challenge_handler, $verification_handler) = (\&process_challenge_dns, \&process_verification_dns);
                }
            }
        }
        return _error($le->error_details) if $le->request_challenge();
        return _error($le->error_details) if $le->accept_challenge($challenge_handler || \&process_challenge, $opt->{'handle-params'}, $opt->{'handle-as'});
        return _error($le->error_details) if $le->verify_challenge($verification_handler || \&process_verification, $opt->{'handle-params'}, $opt->{'handle-as'});
    }
    unless ($le->certificate) {
        $opt->{'logger'}->info("Requesting domain certificate.");
        return _error($le->error_details) if $le->request_certificate();
    }
    $opt->{'logger'}->info("Requesting issuer's certificate.");
    if ($le->request_issuer_certificate()) {
        $opt->{'logger'}->error("Could not download an issuer's certificate, try to download manually from " . $le->issuer_url);
        $opt->{'logger'}->warn("Will be saving the domain certificate alone, not the full chain.");
        return _error("Failed to save the domain certificate file") if _write($opt->{'crt'}, $le->certificate);
    } else {
        unless ($opt->{'legacy'}) {
            $opt->{'logger'}->info("Saving the full certificate chain to $opt->{'crt'}.");
            return _error("Failed to save the domain certificate file") if _write($opt->{'crt'}, $le->certificate . "\n" . $le->issuer . "\n");
        } else {
            $opt->{'logger'}->info("Saving the domain certificate to $opt->{'crt'}.");
            return _error("Failed to save the domain certificate file") if _write($opt->{'crt'}, $le->certificate);
            $opt->{'crt'}=~s/\.[^\.]+$//;
            $opt->{'crt'}.='.ca';
            $opt->{'logger'}->info("Saving the issuer's certificate to $opt->{'crt'}.");
            $opt->{'logger'}->error("Failed to save the issuer's certificate, try to download manually from " . $le->issuer_url) if _write($opt->{'crt'}, $le->issuer);
        }
    }
    if ($opt->{'export-pfx'}) {
        # Note: At this point the certificate is already issued, but with pfx export option active we will return an error if export has failed, to avoid triggering
        # the 'success' batch processing IIS users might have set up on issuance and export.
        if ($le->issuer) {
            my $target_pfx = $opt->{'crt'};
            $target_pfx=~s/\.[^\.]*$//;
            $opt->{'logger'}->info("Exporting certificate to $target_pfx.pfx.");
            return _error("Error exporting pfx: " . $le->error_details) if $le->export_pfx("$target_pfx.pfx", $opt->{'export-pfx'}, $le->certificate, $le->csr_key, $le->issuer);
        } else {
            return _error("Issuer's certificate is not available, skipping pfx export to avoid creating an invalid pfx.");
        }
    }
    if ($opt->{'complete-handler'}) {
        # Add multi-webroot option to parameters passed if it is set.
        $opt->{'complete-params'}->{'multiroot'} = $opt->{'multiroot'} if $opt->{'multiroot'};
        my $data = {
            # Note, certificate here is just a domain certificate, issuer is passed separately - so handler could merge those or use them separately as well.
            certificate => $le->certificate, certificate_file => $opt->{'crt'}, key_file => $opt->{'csr-key'}, issuer => $le->issuer, 
            domains => $le->domains, logger => $opt->{'logger'},
        };
        my $rv;
        eval {
            $rv = $opt->{'complete-handler'}->complete($data, $opt->{'complete-params'});
        };
        if ($@ or !$rv) {
            return _error("Completion handler " . ($@ ? "thrown an error: $@" : "did not return a true value"));
        }
    }
    $opt->{'logger'}->info("===> NOTE: You have been using the test server for this certificate. To issue a valid trusted certificate add --live option.") unless $opt->{'live'};
    $opt->{'logger'}->info("The job is done, enjoy your certificate! For feedback and bug reports contact us at [ https://ZeroSSL.com | https://Do-Know.com ]\n");
    return { code => $opt->{'issue-code'}||0 };
}

sub parse_options {
    my $opt = shift;
    my $args = @ARGV;

    GetOptions ($opt, 'key=s', 'csr=s', 'csr-key=s', 'domains=s', 'path=s', 'crt=s', 'email=s', 'curve=s', 'server=s', 'renew=i', 'issue-code=i',
        'handle-with=s', 'handle-as=s', 'handle-params=s', 'complete-with=s', 'complete-params=s', 'log-config=s', 'update-contacts=s', 'export-pfx=s',
        'generate-missing', 'generate-only', 'revoke', 'legacy', 'unlink', 'live', 'quiet', 'debug', 'help') || return _error("Use --help to see the usage examples.");

    usage_and_exit($opt) unless ($args and !$opt->{'help'});
    my $rv = reconfigure_log($opt);
    return $rv if $rv;

    $opt->{'logger'}->info("[ ZeroSSL Crypt::LE client v$VERSION started. ]");

    if ($opt->{'server'}) {
        return _error("Unsupported protocol for the custom server URL: $1.") if ($opt->{'server'}=~s~^(.*?)://~~ and uc($1) ne 'HTTPS');
        my $server = $opt->{'server'}; # For logging.
        $opt->{'logger'}->warn("Remember to URL-escape special characters if you are using server URL with basic auth credentials.") if $server=~s~[^@/]*@~~;
        $opt->{'logger'}->info("Custom server URL 'https://$server' is used.");
        $opt->{'logger'}->warn("Note: 'live' option is ignored.") if $opt->{'live'};
    }

    return _error("Incorrect parameters - need account key file name specified.") unless $opt->{'key'};
    if (-e $opt->{'key'}) {
        return _error("Account key file is not readable.") unless (-r $opt->{'key'});
    } else {
        return _error("Account key file is missing and the option to generate missing files is not used.") unless $opt->{'generate-missing'};
    }

    unless ($opt->{'crt'} or $opt->{'generate-only'} or $opt->{'update-contacts'}) {
        return _error("Please specify a file name for the certificate.");
    }

    if ($opt->{'export-pfx'}) {
        if ($opt->{'crt'} and $opt->{'crt'}=~/\.pfx$/i) {
            return _error("Please ensure that the extension of the certificate filename is different from '.pfx' to be able to additionally export the certificate in pfx form.");
        }
        unless ($opt->{'csr-key'} and (-r $opt->{'csr-key'} or ($opt->{'generate-missing'} and ! -e $opt->{'csr'}))) {
            return _error("Need either existing csr-key specified or having CSR file generated (via 'generate-missing') for PFX export to work");
        }
    }

    if ($opt->{'revoke'}) {
        return _error("Need a certificate file for revoke to work.") unless ($opt->{'crt'} and -r $opt->{'crt'});
        return _error("Need an account key - revoke assumes you had a registered account when got the certificate.") unless (-r $opt->{'key'});
    } elsif (!$opt->{'update-contacts'}) {
        return _error("Incorrect parameters - need CSR file name specified.") unless $opt->{'csr'};
        if (-e $opt->{'csr'}) {
            return _error("CSR file is not readable.") unless (-r $opt->{'csr'});
        } else {
            return _error("CSR file is missing and the option to generate missing files is not used.") unless $opt->{'generate-missing'};
            return _error("CSR file is missing and CSR-key file name is not specified.") unless $opt->{'csr-key'};
            return _error("Domain list should be provided to generate a CSR.") unless ($opt->{'domains'} and $opt->{'domains'}!~/^[\s\,]*$/);
        }

        if ($opt->{'path'}) {
            my @non_writable = ();
            foreach my $path (grep { $_ } split /\s*,\s*/, $opt->{'path'}) {
                push @non_writable, $path unless (-d $path and -w _);
            }
            return _error("Path to save challenge files into should be a writable directory for: " . join(', ', @non_writable)) if @non_writable;
        } elsif ($opt->{'unlink'}) {
            return _error("Unlink option will have no effect without --path.");
        }

        $opt->{'handle-as'} = $opt->{'handle-as'} ? lc($opt->{'handle-as'}) : 'http';

        if ($opt->{'handle-with'}) {
            eval {
                load $opt->{'handle-with'};
                $opt->{'handler'} = $opt->{'handle-with'}->new();
            };
            return _error("Cannot use the module to handle challenges with.") if $@;
            my $method = 'handle_challenge_' . $opt->{'handle-as'};
            return _error("Module to handle challenges does not seem to support the challenge type of $opt->{'handle-as'}.") unless $opt->{'handler'}->can($method);
            my $rv = _load_params($opt, 'handle-params');
            return $rv if $rv;
        } else {
            $opt->{'handle-params'} = { path => $opt->{'path'}, unlink => $opt->{'unlink'} };
        }

        if ($opt->{'complete-with'}) {
            eval {
                load $opt->{'complete-with'};
                $opt->{'complete-handler'} = $opt->{'complete-with'}->new();
            };
            return _error("Cannot use the module to complete processing with.") if $@;
            return _error("Module to complete processing with does not seem to support the required 'complete' method.") unless $opt->{'complete-handler'}->can('complete');
            my $rv = _load_params($opt, 'complete-params');
            return $rv if $rv;
        } else {
            $opt->{'complete-params'} = { path => $opt->{'path'}, unlink => $opt->{'unlink'} };
        }
    }
    return;
}

sub encode_args {
    my @ARGVmod = ();
    my @vals = ();
    # Account for cmd-shell parameters splitting.
    foreach my $param (@ARGV) {
        if ($param=~/^-/) {
            if (@vals) {
                push @ARGVmod, join(" ", @vals);
                @vals = ();
            }
            if ($param=~/^(.+?)\s*=\s*(.*)$/) {
                push @ARGVmod, $1;
                push @vals, $2 if $2;
            } else {
                push @ARGVmod, $param;
            }
        } else {
            push @vals, $param;
        }
    }<commit_msg>v0.28a<commit_after>#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use IO::File;
use JSON::MaybeXS;
use Log::Log4perl;
use Log::Log4perl::Level;
use Module::Load;
use Encode 'decode';
use Digest::SHA 'sha256';
use MIME::Base64 'encode_base64url';
use Crypt::LE ':errors', ':keys';
use utf8;

my $VERSION = '0.28a';

exit main();

sub main {
    Log::Log4perl->easy_init({ utf8  => 1 });
    my $opt = { logger => Log::Log4perl->get_logger(), e => encode_args() };
    binmode(STDOUT, ":encoding(UTF-8)");
    if (my $rv = work($opt)) {
        $opt->{logger}->error($rv->{'msg'}) if $rv->{'msg'};
        return defined $rv->{'code'} ? $rv->{'code'} : 255;
    }
    return 0;
}

sub work {
    my $opt = shift;
    my $rv = parse_options($opt);
    return $rv if $rv;

    my $le = Crypt::LE->new(autodir => 0, server => $opt->{'server'}, live => $opt->{'live'}, debug => $opt->{'debug'}, logger => $opt->{'logger'});

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

    if ($opt->{'update-contacts'}) {
        # Register.
        my $reg = _register($le, $opt);
        return $reg if $reg;
        my @contacts = (lc($opt->{'update-contacts'}) eq 'none') ? () : grep { $_ } split /\s*\,\s*/, $opt->{'update-contacts'};
        my @rejected = ();
        foreach (@contacts) {
            /^(\w+:)?(.+)$/;
            # NB: tel is not supported by LE at the moment.
            my ($prefix, $data) = (lc($1||''), $2);
            push @rejected, $_ unless ($data=~/^[^\@]+\@[^\.]+\.[^\.]+/ and (!$prefix or ($prefix eq 'mailto:')));
        }
        return _error("Unknown format for the contacts: " . join(", ", @rejected)) if @rejected;
        return _error("Could not update contact details: " . $le->error_details) if $le->update_contacts(\@contacts);
        $opt->{'logger'}->info("Contact details have been updated.");
        return;
    }

    if ($opt->{'revoke'}) {
        my $crt = _read($opt->{'crt'});
        return _error("Could not read the certificate file.") unless $crt;
        # Take the first certificate in file, disregard the issuer's one.
        $crt=~s/^(.*?-+\s*END CERTIFICATE\s*-+).*/$1/s;
        return _error("Could not load the resource directory: " . $le->error_details) if $le->directory;
        my $rv = $le->revoke_certificate(\$crt);
        if ($rv == OK) {
            $opt->{'logger'}->info("Certificate has been revoked.");
        } elsif ($rv == ALREADY_DONE) {
            $opt->{'logger'}->info("Certificate has been ALREADY revoked.");
        } else {
            return _error("Problem with revoking certificate: " . $le->error_details);
        }
        return;
    }

    if ($opt->{'domains'}) {
        if ($opt->{'e'}) {
            $opt->{'logger'}->warn("Could not encode arguments, support for internationalized domain names may not be available.");
        } else {
            my @domains = grep { $_ } split /\s*\,\s*/, $opt->{'domains'};
            $opt->{'domains'} = join ",", map { _puny($_) } @domains;
        }
    }
    if (-r $opt->{'csr'}) {
        $opt->{'logger'}->info("Loading a CSR from $opt->{'csr'}");
        $le->load_csr($opt->{'csr'}, $opt->{'domains'}) == OK or return _error("Could not load a CSR: " . $le->error_details);
        return _error("For multi-webroot path usage, the amount of paths given should match the amount of domain names listed.") if _path_mismatch($le, $opt);
        # Load existing CSR key if specified, even if we have CSR (for example for PFX export).
        if ($opt->{'csr-key'} and -e $opt->{'csr-key'}) {
            return _error("Could not load existing CSR key from $opt->{'csr-key'} - " . $le->error_details) if $le->load_csr_key($opt->{'csr-key'});
        }
    } else {
        return _error("For multi-webroot path usage, the amount of paths given should match the amount of domain names listed.") if _path_mismatch($le, $opt);
        $opt->{'logger'}->info("Generating a new CSR for domains $opt->{'domains'}");
        if (-e $opt->{'csr-key'}) {
             # Allow using pre-existing key when generating CSR
             return _error("Could not load existing CSR key from $opt->{'csr-key'} - " . $le->error_details) if $le->load_csr_key($opt->{'csr-key'});
             $opt->{'logger'}->info("New CSR will be based on '$opt->{'csr-key'}' key");
        } else {
             $opt->{'logger'}->info("New CSR will be based on a generated key");
        }
        my ($type, $attr) = $opt->{'curve'} ? (KEY_ECC, $opt->{'curve'}) : (KEY_RSA, $opt->{'legacy'} ? 2048 : 4096);
        $le->generate_csr($opt->{'domains'}, $type, $attr) == OK or return _error("Could not generate a CSR: " . $le->error_details);
        $opt->{'logger'}->info("Saving a new CSR into $opt->{'csr'}");
        return "Failed to save a CSR" if _write($opt->{'csr'}, $le->csr);
        unless (-e $opt->{'csr-key'}) {
            $opt->{'logger'}->info("Saving a new CSR key into $opt->{'csr-key'}");
            return _error("Failed to save a CSR key") if _write($opt->{'csr-key'}, $le->csr_key);
        }
    }

    return if $opt->{'generate-only'};

    if ($opt->{'renew'}) {
        if ($opt->{'crt'} and -r $opt->{'crt'}) {
            $opt->{'logger'}->info("Checking certificate for expiration (local file).");
            $opt->{'expires'} = $le->check_expiration($opt->{'crt'});
            $opt->{'logger'}->warn("Problem checking existing certificate file.") unless (defined $opt->{'expires'});
        }
        unless (defined $opt->{'expires'}) {
            $opt->{'logger'}->info("Checking certificate for expiration (website connection).");
            foreach my $domain (@{$le->domains}) {
                $opt->{'logger'}->info("Checking $domain");
                $opt->{'expires'} = $le->check_expiration("https://$domain/");
                last if (defined $opt->{'expires'});
            }
        }
        return _error("Could not get the certificate expiration value, cannot renew.") unless (defined $opt->{'expires'});
        if ($opt->{'expires'} > $opt->{'renew'}) {
            $opt->{'logger'}->info("Too early for renewal, certificate expires in $opt->{'expires'} days.");
            return;
        }
        $opt->{'logger'}->info("Expiration threshold set at $opt->{'renew'} days, the certificate " . ($opt->{'expires'} < 0 ? "has already expired" : "expires in $opt->{'expires'} days") . " - will be renewing.");
    }
    
    if ($opt->{'email'}) {
        return _error($le->error_details) if $le->set_account_email($opt->{'email'});
    }

    # Register.
    my $reg = _register($le, $opt);
    return $reg if $reg;

    # We might not need to re-verify, verification holds for a while.
    my $new_crt_status = $le->request_certificate();
    unless ($new_crt_status) {
        $opt->{'logger'}->info("Received domain certificate, no validation required at this time.");
    } else {
        # If it's not an auth problem, but blacklisted domains for example - stop.
        return _error("Error requesting certificate: " . $le->error_details) if $new_crt_status != AUTH_ERROR;
        # Add multi-webroot option to parameters passed if it is set.
        $opt->{'handle-params'}->{'multiroot'} = $opt->{'multiroot'} if $opt->{'multiroot'};
        # Handle DNS internally along with HTTP
        my ($challenge_handler, $verification_handler) = ($opt->{'handler'}, $opt->{'handler'});
        if (!$opt->{'handler'}) {
            if ($opt->{'handle-as'}) {
                return _error("Only 'http' and 'dns' can be handled internally, use external modules for other verification types.") unless $opt->{'handle-as'}=~/^(http|dns)$/i;
                if (lc($1) eq 'dns') {
                    ($challenge_handler, $verification_handler) = (\&process_challenge_dns, \&process_verification_dns);
                }
            }
        }
        return _error($le->error_details) if $le->request_challenge();
        return _error($le->error_details) if $le->accept_challenge($challenge_handler || \&process_challenge, $opt->{'handle-params'}, $opt->{'handle-as'});
        return _error($le->error_details) if $le->verify_challenge($verification_handler || \&process_verification, $opt->{'handle-params'}, $opt->{'handle-as'});
    }
    unless ($le->certificate) {
        $opt->{'logger'}->info("Requesting domain certificate.");
        return _error($le->error_details) if $le->request_certificate();
    }
    $opt->{'logger'}->info("Requesting issuer's certificate.");
    if ($le->request_issuer_certificate()) {
        $opt->{'logger'}->error("Could not download an issuer's certificate, try to download manually from " . $le->issuer_url);
        $opt->{'logger'}->warn("Will be saving the domain certificate alone, not the full chain.");
        return _error("Failed to save the domain certificate file") if _write($opt->{'crt'}, $le->certificate);
    } else {
        unless ($opt->{'legacy'}) {
            $opt->{'logger'}->info("Saving the full certificate chain to $opt->{'crt'}.");
            return _error("Failed to save the domain certificate file") if _write($opt->{'crt'}, $le->certificate . "\n" . $le->issuer . "\n");
        } else {
            $opt->{'logger'}->info("Saving the domain certificate to $opt->{'crt'}.");
            return _error("Failed to save the domain certificate file") if _write($opt->{'crt'}, $le->certificate);
            $opt->{'crt'}=~s/\.[^\.]+$//;
            $opt->{'crt'}.='.ca';
            $opt->{'logger'}->info("Saving the issuer's certificate to $opt->{'crt'}.");
            $opt->{'logger'}->error("Failed to save the issuer's certificate, try to download manually from " . $le->issuer_url) if _write($opt->{'crt'}, $le->issuer);
        }
    }
    if ($opt->{'export-pfx'}) {
        # Note: At this point the certificate is already issued, but with pfx export option active we will return an error if export has failed, to avoid triggering
        # the 'success' batch processing IIS users might have set up on issuance and export.
        if ($le->issuer) {
            my $target_pfx = $opt->{'crt'};
            $target_pfx=~s/\.[^\.]*$//;
            $opt->{'logger'}->info("Exporting certificate to $target_pfx.pfx.");
            return _error("Error exporting pfx: " . $le->error_details) if $le->export_pfx("$target_pfx.pfx", $opt->{'export-pfx'}, $le->certificate, $le->csr_key, $le->issuer);
        } else {
            return _error("Issuer's certificate is not available, skipping pfx export to avoid creating an invalid pfx.");
        }
    }
    if ($opt->{'complete-handler'}) {
        # Add multi-webroot option to parameters passed if it is set.
        $opt->{'complete-params'}->{'multiroot'} = $opt->{'multiroot'} if $opt->{'multiroot'};
        my $data = {
            # Note, certificate here is just a domain certificate, issuer is passed separately - so handler could merge those or use them separately as well.
            certificate => $le->certificate, certificate_file => $opt->{'crt'}, key_file => $opt->{'csr-key'}, issuer => $le->issuer, 
            domains => $le->domains, logger => $opt->{'logger'},
        };
        my $rv;
        eval {
            $rv = $opt->{'complete-handler'}->complete($data, $opt->{'complete-params'});
        };
        if ($@ or !$rv) {
            return _error("Completion handler " . ($@ ? "thrown an error: $@" : "did not return a true value"));
        }
    }
    $opt->{'logger'}->info("===> NOTE: You have been using the test server for this certificate. To issue a valid trusted certificate add --live option.") unless $opt->{'live'};
    $opt->{'logger'}->info("The job is done, enjoy your certificate! For feedback and bug reports contact us at [ https://ZeroSSL.com | https://Do-Know.com ]\n");
    return { code => $opt->{'issue-code'}||0 };
}

sub parse_options {
    my $opt = shift;
    my $args = @ARGV;

    GetOptions ($opt, 'key=s', 'csr=s', 'csr-key=s', 'domains=s', 'path=s', 'crt=s', 'email=s', 'curve=s', 'server=s', 'renew=i', 'issue-code=i',
        'handle-with=s', 'handle-as=s', 'handle-params=s', 'complete-with=s', 'complete-params=s', 'log-config=s', 'update-contacts=s', 'export-pfx=s',
        'generate-missing', 'generate-only', 'revoke', 'legacy', 'unlink', 'live', 'quiet', 'debug:i@', 'help') || return _error("Use --help to see the usage examples.");

    usage_and_exit($opt) unless ($args and !$opt->{'help'});
    my $rv = reconfigure_log($opt);
    return $rv if $rv;

    $opt->{'logger'}->info("[ ZeroSSL Crypt::LE client v$VERSION started. ]");

    if ($opt->{'server'}) {
        return _error("Unsupported protocol for the custom server URL: $1.") if ($opt->{'server'}=~s~^(.*?)://~~ and uc($1) ne 'HTTPS');
        my $server = $opt->{'server'}; # For logging.
        $opt->{'logger'}->warn("Remember to URL-escape special characters if you are using server URL with basic auth credentials.") if $server=~s~[^@/]*@~~;
        $opt->{'logger'}->info("Custom server URL 'https://$server' is used.");
        $opt->{'logger'}->warn("Note: 'live' option is ignored.") if $opt->{'live'};
    }

    return _error("Incorrect parameters - need account key file name specified.") unless $opt->{'key'};
    if (-e $opt->{'key'}) {
        return _error("Account key file is not readable.") unless (-r $opt->{'key'});
    } else {
        return _error("Account key file is missing and the option to generate missing files is not used.") unless $opt->{'generate-missing'};
    }

    unless ($opt->{'crt'} or $opt->{'generate-only'} or $opt->{'update-contacts'}) {
        return _error("Please specify a file name for the certificate.");
    }

    if ($opt->{'export-pfx'}) {
        if ($opt->{'crt'} and $opt->{'crt'}=~/\.pfx$/i) {
            return _error("Please ensure that the extension of the certificate filename is different from '.pfx' to be able to additionally export the certificate in pfx form.");
        }
        unless ($opt->{'csr-key'} and (-r $opt->{'csr-key'} or ($opt->{'generate-missing'} and ! -e $opt->{'csr'}))) {
            return _error("Need either existing csr-key specified or having CSR file generated (via 'generate-missing') for PFX export to work");
        }
    }

    if ($opt->{'revoke'}) {
        return _error("Need a certificate file for revoke to work.") unless ($opt->{'crt'} and -r $opt->{'crt'});
        return _error("Need an account key - revoke assumes you had a registered account when got the certificate.") unless (-r $opt->{'key'});
    } elsif (!$opt->{'update-contacts'}) {
        return _error("Incorrect parameters - need CSR file name specified.") unless $opt->{'csr'};
        if (-e $opt->{'csr'}) {
            return _error("CSR file is not readable.") unless (-r $opt->{'csr'});
        } else {
            return _error("CSR file is missing and the option to generate missing files is not used.") unless $opt->{'generate-missing'};
            return _error("CSR file is missing and CSR-key file name is not specified.") unless $opt->{'csr-key'};
            return _error("Domain list should be provided to generate a CSR.") unless ($opt->{'domains'} and $opt->{'domains'}!~/^[\s\,]*$/);
        }

        if ($opt->{'path'}) {
            my @non_writable = ();
            foreach my $path (grep { $_ } split /\s*,\s*/, $opt->{'path'}) {
                push @non_writable, $path unless (-d $path and -w _);
            }
            return _error("Path to save challenge files into should be a writable directory for: " . join(', ', @non_writable)) if @non_writable;
        } elsif ($opt->{'unlink'}) {
            return _error("Unlink option will have no effect without --path.");
        }

        $opt->{'handle-as'} = $opt->{'handle-as'} ? lc($opt->{'handle-as'}) : 'http';

        if ($opt->{'handle-with'}) {
            eval {
                load $opt->{'handle-with'};
                $opt->{'handler'} = $opt->{'handle-with'}->new();
            };
            return _error("Cannot use the module to handle challenges with.") if $@;
            my $method = 'handle_challenge_' . $opt->{'handle-as'};
            return _error("Module to handle challenges does not seem to support the challenge type of $opt->{'handle-as'}.") unless $opt->{'handler'}->can($method);
            my $rv = _load_params($opt, 'handle-params');
            return $rv if $rv;
        } else {
            $opt->{'handle-params'} = { path => $opt->{'path'}, unlink => $opt->{'unlink'} };
        }

        if ($opt->{'complete-with'}) {
            eval {
                load $opt->{'complete-with'};
                $opt->{'complete-handler'} = $opt->{'complete-with'}->new();
            };
            return _error("Cannot use the module to complete processing with.") if $@;
            return _error("Module to complete processing with does not seem to support the required 'complete' method.") unless $opt->{'complete-handler'}->can('complete');
            my $rv = _load_params($opt, 'complete-params');
            return $rv if $rv;
        } else {
            $opt->{'complete-params'} = { path => $opt->{'path'}, unlink => $opt->{'unlink'} };
        }
    }
    $opt->{'debug'} = scalar @{$opt->{'debug'}} if $opt->{'debug'};
    return;
}

sub encode_args {
    my @ARGVmod = ();
    my @vals = ();
    # Account for cmd-shell parameters splitting.
    foreach my $param (@ARGV) {
        if ($param=~/^-/) {
            if (@vals) {
                push @ARGVmod, join(" ", @vals);
                @vals = ();
            }
            if ($param=~/^(.+?)\s*=\s*(.*)$/) {
                push @ARGVmod, $1;
                push @vals, $2 if $2;
            } else {
                push @ARGVmod, $param;
            }
        } else {
            push @vals, $param;
        }
    }
{'fraction_non_alphanumeric': 0.1979318100776257, 'fraction_numerical': 0.0023863729873549663, 'mean_word_length': 2.619737887212073, 'pattern_counts': {'":': 2, '<': 5, '<?xml version=': 0, '>': 626, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7267113', 'n_tokens_mistral': 11471, 'n_tokens_neox': 11158, 'n_words': 3153}