Class ::letsencrypt::Client

::letsencrypt::Client[i] create ... \
           [ -API (default "staging") ] \
           [ -background:boolean (default "false") ] \
           [ -certPemFile certPemFile ] \
           [ -certPrivKey certPrivKey ] \
           [ -domain domain ] \
           [ -domains (default "") ] \
           [ -exponent exponent ] \
           [ -jwk jwk ] \
           [ -key_type (default "rsa") ] \
           [ -log (default "") ] \
           [ -modulus modulus ] \
           [ -nonce nonce ] \
           [ -replyHeaders replyHeaders ] \
           [ -replyText replyText ] \
           [ -sans sans ] \
           [ -sslpath (default "") ] \
           [ -thumbprint64 thumbprint64 ]

state and configuration variables
Defined in /usr/local/ns/tcl/letsencrypt/letsencrypt-procs.tcl

Class Relations

  • class: ::nx::Class[i]
  • superclass: ::nx::Object[i]
::nx::Class create ::letsencrypt::Client \
     -superclass ::nx::Object

Methods (to be applied on instances)

  • getCertificate (scripted, public)

     <instance of letsencrypt::Client[i]> getCertificate

    This method does all the steps required to obtain a certificate, such as - selecting the API (production or staging), - registering a new account if necessary, - create public and private key for the account, - issuing a certificate request, - obtaining the certificate, and - installing the certificate. If called interactivaly, the progress is logged to the console, otherwise just into the system log.

    Partial Call Graph (max 5 caller/called nodes):
    %3

    Testcases:
    No testcase defined.
    ns_log notice "letsencrypt client: domains <${:domains}> background ${:background}"
    
    if {${:domains} eq ""} {
        #
        # Are values for the domains specified in the
        # NaviServer configuration file?
        #
        set :domains [ns_config ns/server/[ns_info server]/module/letsencrypt domains]
        #ns_log notice "letsencrypt client: domains from NaviServer configuration file: <${:domains}>"
    
    }
    
    if {${:domains} eq "" && [ns_conn isconnected]} {
        #
        # Still no values. Try to get it from the query parameters
        #
        #ns_log notice "letsencrypt client: we need a queryget"
        set :domains [ns_queryget domains ""]
        ns_log notice "letsencrypt client: domains from query <${:domains}> background ${:background}"
        #
        # If the domain names were already submitted in the form
        # (or via query parameters), we have all data we
        # need.
    }
    
    ns_log notice "letsencrypt client: domains <${:domains}> background ${:background}"
    if {${:domains} eq ""} {
        #
        # If we have still no values, provide the user with a
        # form to fill-in the data and to continue from there.
        # But this works only, when we are not called in the
        # background.
        #
        if {${:background}} {
            error "letsencrypt: either provide '-domains ...' or run from a connection thread"
        }
        ns_log notice "letsencrypt: have to return domainForm"
        :domainForm
        return
    }
    
    set :domain    [lindex ${:domains} 0]
    set :sans      [lrange ${:domains} 1 end]
    
    set config {
        staging    {https://acme-staging-v02.api.letsencrypt.org/directory}
        production {https://acme-v02.api.letsencrypt.org/directory}
    }
    
    #
    # Make sure, the sslpath exists
    #
    file mkdir ${:sslpath}
    set :accoutKeyFile ${:sslpath}/letsencrypt-${:API}-account.key
    ns_log notice "letsencrypt client: call start of report"
    
    #
    # Start output
    #
    :startOfReport
    
    ns_log notice "letsencrypt: getAPIurls"
    
    #
    # Always get first the API URLs
    #
    :getAPIurls $config
    
    #
    # Create or reuse an account
    #
    if {[file exists ${:accoutKeyFile}]} {
        #
        # We have already registered in the past successfully at
        # Let's Encrypt and signed the agreement.
        #
        :log "Reuse existing account registration at Let's Encrypt (${:accoutKeyFile})<br>"
    
        :parseAccountKey
        :getNonce
    
        set payload [subst {{"termsOfServiceAgreed": true, "onlyReturnExisting": true, "contact": \["mailto:webmaster@${:domain}"\]}}]
        set status [:send_signed_request [:URL newAccount] $payload]
    
        if {$status eq "400"} {
            :abortMsg $status "authorization for existing account failed"
            return
        } else {
            set :kid [ns_set iget ${:replyHeaders} "location"]
            :log "<pre>registration headers contained kid ${:kid}\n</pre>"
        }
    
    } else {
    
        set status [:registerNewAccount]
        if {$status >= 400} {
            :abortMsg $status "Registration"
            return
        }
    
        set status [:signAgreement]
        if {$status >= 400} {
            :abortMsg $status "Agreement"
            return
        }
    }
    
    #
    # Create a new order for the domains
    #
    file delete -force [ns_server pagedir]/.well-known
    file mkdir [ns_server pagedir]/.well-known
    
    :log "Creating new order...<br>"
    set ids {}
    foreach domain ${:domains} {
        lappend ids [subst {{"type""dns""value""$domain"}}]
    }
    set payload [subst {{"identifiers": \[[join $ids ,]\]}}]
    :log "... payload: <pre>$payload</pre>"
    
    set httpStatus [:send_signed_request [:URL newOrder] $payload]
    if {$httpStatus >= 400} {
        :abortMsg $httpStatus "Order failed"
        return
    }
    set orderDict [json::json2dict ${:replyText}]
    set authorizations [dict get $orderDict authorizations]
    set identifiers [dict get $orderDict identifiers]
    set orderFinalizeURL [dict get $orderDict finalize]
    
    #:log "<pre>authorizations:\n$authorizations\norderFinalizeURL:$orderFinalizeURL</pre>"
    
    if {[llength $authorizations] != [llength ${:domains}]} {
        :abortMsg $httpStatus "number of domains ([llength ${:domains}]) differs from number of authorizations ([llength $authorizations])"
        return
    }
    
    foreach domain ${:domains} auth_url $authorizations id $identifiers {
        set status [:authorizeDomain $auth_url [dict get $id value]]
        if {$status in {invalid}} {
            :log [ns_trim -delimiter | [subst {
                |Validation of domain $domain failed (final status $status).
                |<p>Please issue a corrected certificate check.
            }]]
            return
        }
    }
    
    file delete -force [ns_server pagedir]/.well-known
    
    #
    # Get certificate
    #
    set status [:certificateRequest $orderFinalizeURL]
    if {$status >= 400} {
        :abortMsg $status "Certificate request"
        return
    }
    
    #
    # Install certificate and update configuration file
    #
    :certificateInstall
    :updateConfiguration
    
    if {${:API} eq "production"} {
        #
        # Everything was updated, We can trigger the reload
        # operation by sending SIGHUP to the nsd process
        #
        ns_kill [pid] 1
        :log [ns_trim -delimiter | [subst {
            |<br>The new certificate is installed and was
            |reloaded via SIGHUP. For old versions of
            |NaviServer, restart your NaviServer instance and
            |check results on
            |<a href="https://${:domain}">https://${:domain}</a>.
            |<p> }]]
    
        :log "<p>Certificate were reloaded by sending SIGHUP to nsd"
    } else {
        :log "<p><strong>Warning:</strong> no automated reloading"  "when using the 'staging' environment<p>"
    }