%3 ::nx::Object ::nx::Object __default_accessor __default_method_call_protection __object_configureparameter __resolve_method_path contains copy delete object method delete object property delete object variable destroy_on_cleanup info info info lookup parameters info lookup slots info lookup syntax info lookup variables info object method args info object method body info object method callprotection info object method debug info object method definition info object method definitionhandle info object method deprecated info object method exists info object method handle info object method origin info object method parameters info object method registrationhandle info object method returns info object method submethods info object method syntax info object method type info object slots info object variables info variable definition info variable name info variable parameter move object alias object forward object method object property object variable private protected public qn require namespace require object method require private object method require protected object method require public object method serialize ::letsencrypt::Client ::letsencrypt::Client URL abortMsg authorizeDomain backup certificateInstall certificateRequest decnum_to_bytes domainForm getAPIurls getCertificate getNonce init log parseAccountKey printHeaders readFile registerNewAccount requireKeyFile send_signed_request signAgreement startOfReport updateConfiguration writeFile ::letsencrypt::Client->::nx::Object

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>"
    }