1 2 /** Simple client for ACME protocol 3 * 4 * This software provides a simple and minimalistic ACME client. It 5 * provides no fancy options, but sticks with the most common settings. 6 */ 7 module acme.client; 8 9 import std.conv; 10 import std.datetime; 11 import std.json; 12 import std.net.curl; 13 import std.stdio; 14 import std.string; 15 import std.typecons; 16 17 import acme; 18 19 /** By default, we always use the staging and test server. */ 20 bool useStagingServer = true; 21 22 /* ------------------------------------------------------------------------ */ 23 24 /** Url to official v2 API of letsencrypt */ 25 enum directoryUrlProd = "https://acme-v02.api.letsencrypt.org/directory"; 26 /** Url to official v2 staging API of letsencrypt */ 27 enum directoryUrlStaging = "https://acme-staging-v02.api.letsencrypt.org/directory"; 28 29 /* ------------------------------------------------------------------------ */ 30 31 /** This structure stores the resource url of the ACME server 32 */ 33 struct AcmeResources 34 { 35 string nonce; /// The Nonce for the next JWS transfer 36 37 string directoryUrl; /// Initial config url to directory resource 38 JSONValue directoryJson; /// JSON string returned returned from directoryURL 39 40 string newNOnceUrl; /// Url to newNonce resource 41 string newAccountUrl; /// Url to newAccount resource 42 string newOrderUrl; /// Url to newOrder resource 43 string newAuthZUrl; /// Url to newAuthz resource 44 string revokeCrtUrl; /// Url to revokeCert resource 45 string keyChangeUrl; /// Url to keyChange resource 46 47 string metaJson; /// Metadata as JSON string (undecoded) 48 49 string accountUrl; /// Account Url for a JWK. 50 // FIXME Are these members obsolete or from V1 API? 51 //string newCertUrl; 52 //string newRegUrl; 53 54 /** Init the Resource with a ACME directory URL */ 55 void initClient(string initstr = directoryUrlStaging) { 56 directoryUrl = initstr; 57 } 58 /** Decode a ACME directory JSON */ 59 void decodeDirectoryJson(const(char[]) directory) 60 { 61 directoryJson = parseJSON(directory); 62 alias json = directoryJson; 63 if ("keyChange" in json) this.keyChangeUrl = json["keyChange"].str; 64 if ("newAccount" in json) this.newAccountUrl = json["newAccount"].str; 65 if ("newNonce" in json) this.newNOnceUrl = json["newNonce"].str; 66 if ("newOrder" in json) this.newOrderUrl = json["newOrder"].str; 67 if ("revokeCert" in json) this.revokeCrtUrl = json["revokeCert"].str; 68 69 if ("newAuthz" in json) this.newAuthZUrl = json["newAuthz"].str; 70 /* Are the following fields obsolete or v1? */ 71 //if ("newCert" in json) this.newCertUrl = json["newCert"].str; 72 //if ("newReg" in json) this.newRegUrl = json["newReg"].str; 73 74 if ("meta" in json) this.metaJson = json["meta"].toJSON; 75 } 76 void getResources() 77 { 78 try 79 { 80 auto conn = HTTP(this.directoryUrl); 81 conn.setUserAgent = "acme-lw-d/" ~ acmeClientVersion ~ " " ~ HTTP.defaultUserAgent(); 82 conn.method = HTTP.Method.get; 83 debug { conn.verifyPeer = false; } 84 char[] directory = std.net.curl.get(this.directoryUrl, conn); 85 decodeDirectoryJson(directory); 86 } 87 catch (Exception e) 88 { 89 string msg = "Unable to initialize resource url from " ~ this.directoryUrl ~ ": " ~ e.msg; 90 throw new AcmeException(msg, __FILE__, __LINE__, e ); 91 } 92 } 93 } 94 95 unittest 96 { 97 string dirTestData = q"({ 98 "Ca1Xc_O0Nwk": "https:\/\/community.letsencrypt.org\/t\/adding-random-entries-to-the-directory\/33417", 99 "keyChange": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/key-change", 100 "meta": { 101 "caaIdentities": [ 102 "letsencrypt.org" 103 ], 104 "termsOfService": "https:\/\/letsencrypt.org\/documents\/LE-SA-v1.2-November-15-2017.pdf", 105 "website": "https:\/\/letsencrypt.org\/docs\/staging-environment\/" 106 }, 107 "newAccount": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/new-acct", 108 "newAuthz": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/new-authz", 109 "newNonce": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/new-nonce", 110 "newOrder": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/new-order", 111 "revokeCert": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/revoke-cert" 112 })"; 113 void testcode(string url, bool dofullasserts = false ) 114 { 115 AcmeResources test; 116 if (url is null) { 117 test.initClient(); 118 test.decodeDirectoryJson(dirTestData); 119 } else { 120 test.initClient(url); 121 test.directoryUrl = url; 122 test.getResources(); 123 } 124 writeln("Received directory data :\n", test.directoryJson.toPrettyString); 125 assert( test.directoryUrl !is null, "Shouldn't be null"); 126 127 assert( test.keyChangeUrl !is null, "Shouldn't be null"); 128 assert( test.newAccountUrl !is null, "Shouldn't be null"); 129 assert( test.newNOnceUrl !is null, "Shouldn't be null"); 130 assert( test.newOrderUrl !is null, "Shouldn't be null"); 131 assert( test.revokeCrtUrl !is null, "Shouldn't be null"); 132 assert( test.metaJson !is null, "Shouldn't be null"); 133 if (dofullasserts) { 134 assert( test.newAuthZUrl !is null, "Shouldn't be null"); 135 } 136 } 137 writeln("**** Testing AcmeResources : Decode test vector"); 138 testcode(null, true); 139 writeln("**** Testing AcmeResources : Use staging server : ", directoryUrlStaging); 140 testcode(directoryUrlStaging); 141 writeln("**** Testing AcmeResources : Use production server : ", directoryUrlProd); 142 testcode(directoryUrlProd); 143 } 144 145 /* ------------------------------------------------------------------------ */ 146 147 148 /** An openssl certificate */ 149 struct Certificate 150 { 151 /** The full CA chain with cert */ 152 string fullchain; 153 /** The private key to sign requests */ 154 string privkey; 155 156 // Note that neither of the 'Expiry' calls below require 'privkey' 157 // to be set; they only rely on 'fullchain'. 158 159 /** 160 Returns the number of seconds since 1970, i.e., epoch time. 161 162 Due to openssl quirkiness there might be a little drift 163 from a strictly accurate result, but it should be close 164 enough for the purpose of determining whether the certificate 165 needs to be renewed. 166 */ 167 DateTime getExpiry() const 168 { 169 import acme.openssl_glues : ASN1_TIME, stubSSL_ASN1_TIME_diff; 170 static const(DateTime) extractor(const ASN1_TIME * t) 171 { 172 import acme.openssl_glues; 173 import core.stdc.time; 174 time_t unixTime = ASN1_GetTimeT(t); 175 auto stdTime = unixTimeToStdTime(unixTime); 176 auto st = SysTime(stdTime); 177 auto dt = cast(DateTime)st; 178 return dt; 179 } 180 DateTime rc = extractExpiryData!(DateTime, extractor)(this.fullchain); 181 return rc; 182 } 183 184 /** Returns the 'Not After' result that openssl would display if 185 running the following command. 186 187 openssl x509 -noout -in fullchain.pem -text 188 189 For example: 190 191 May 6 21:15:03 2018 GMT 192 */ 193 string getExpiryDisplay() const 194 { 195 import acme.openssl_glues : ASN1_TIME, stubSSL_ASN1_TIME_print, BIO, stubSSL_BIO_free; 196 string extractor(const ASN1_TIME * t) 197 { 198 BIO* b = stubSSL_ASN1_TIME_print(t); 199 scope(exit) stubSSL_BIO_free(b); 200 return b.toVector.to!string; 201 } 202 return extractExpiryData!(string, extractor)(this.fullchain); 203 } 204 } 205 206 /* ------------------------------------------------------------------------ */ 207 208 /** A simple ACME v2 client 209 * 210 * This class implements the ACME v2 protocol to obtain signed SSL 211 * certificates. 212 */ 213 class AcmeClient 214 { 215 private: 216 import acme.openssl_glues : EVP_PKEY; 217 EVP_PKEY* privateKey_; /// Copy of private key as ASC PEM 218 219 JSONValue jwkData_; /// JWK object as JSONValue tree 220 string jwkString_; /// JWK as plain JSON string 221 ubyte[] jwkSHAHash_; /// The SHA256 hash value of jwkString_ 222 string jwkThumbprint_; /// Base64 url-safe string of jwkSHAHash_ 223 224 bool beVerbose_; /// Be verbose 225 226 void myLog(alias fun = writeln, T...)(T args) 227 { 228 if (beVerbose_) { 229 fun(args); 230 stdout.flush; 231 } 232 } 233 234 /** Create and send a JWS request with payload to a ACME enabled CA server 235 * 236 * Still unfinished template to build the JWS object. This code must be 237 * refactored later. 238 * 239 * Params: 240 * T = return type 241 * useKID = useKID 242 * url = Url to post to 243 * payload = data to send 244 * status = pointer to StatusLine 245 * rheaders = Pointer to ResponseHeaders received from server, or null 246 * 247 * See: https://tools.ietf.org/html/rfc7515 248 */ 249 T sendRequest(T, bool useKID = true) 250 (string url, string payload, HTTP.StatusLine* status = null, string[string]* rheaders = null) 251 if ( is(T : string) || is(T : char[]) || is(T : ubyte[])) 252 { 253 string nonce = this.acmeRes.nonce; 254 assert(nonce !is null && !nonce.empty, "Invalid Nonce value."); 255 myLog("Using NOnce: ", nonce); 256 257 /* Create protection data */ 258 JSONValue jvReqHeader; 259 jvReqHeader["alg"] = "RS256"; 260 static if (useKID) 261 jvReqHeader["kid"] = acmeRes.accountUrl; 262 else 263 jvReqHeader["jwk"] = jwkData_; 264 jvReqHeader["nonce"] = nonce; 265 jvReqHeader["url"] = url; 266 char[] protectd = jvReqHeader.toJSON.dup; 267 myLog(protectd); 268 protectd = base64EncodeUrlSafe(protectd); 269 270 const char[] payld = base64EncodeUrlSafe(payload); 271 272 auto signData = protectd ~ "." ~ payld; 273 myLog!writefln("Data to sign: %s", signData); 274 char[] signature = signDataWithSHA256(signData, privateKey_); 275 myLog!writefln("Signature: %s", signature); 276 277 JSONValue jvBody; 278 jvBody["protected"] = protectd; 279 jvBody["payload"] = payld; 280 jvBody["signature"] = signature; 281 char[] body_ = jvBody.toJSON.dup; 282 myLog!writefln("Body: %s", jvBody.toPrettyString); 283 284 auto response = doPost(url, body_, status, rheaders, &(acmeRes.nonce)); 285 if (rheaders) { 286 myLog( "ResponseHeaders: "); 287 foreach( v ; (*rheaders).byKey) { 288 myLog(" ", v, " : ", (*rheaders)[v]); 289 //~ if (v.toLower == "replay-nonce") { 290 //~ acmeRes.nonce = (*rheaders)[v]; 291 //~ myLog("Setting new NOnce: ", acmeRes.nonce); 292 //~ } 293 } 294 } 295 myLog( "Response: ", response); 296 297 return to!T(response); 298 } 299 300 public: 301 AcmeResources acmeRes; /// The Urls to the ACME resources 302 303 304 /** Instanciate a AcmeClient using a private key for signing 305 * 306 * Param: 307 * accountPrivateKey = The signingKey is the Acme account private 308 * key used to sign requests to the acme CA, in pem format. 309 * Throws: an instance of AcmeException on fatal or unexpected errors. 310 */ 311 this(string accountPrivateKey, bool beVerbose = false) 312 { 313 import acme.openssl_glues : RSA, BIGNUM, stubSSL_RSA_Get0_key; 314 beVerbose_ = beVerbose; 315 316 acmeRes.initClient( useStagingServer ? directoryUrlStaging : directoryUrlProd); 317 SSL_OpenLibrary(); 318 319 /* Create the private key */ 320 RSA* rsa; 321 privateKey_ = SSL_x509_read_pkey_memory( cast(const(char[]))accountPrivateKey, &rsa); 322 323 BIGNUM* n; 324 BIGNUM* e; 325 BIGNUM* d; 326 stubSSL_RSA_Get0_key(rsa, &n, &e, &d); 327 328 // https://tools.ietf.org/html/rfc7638 329 // JSON Web Key (JWK) Thumbprint 330 JSONValue jvJWK; 331 jvJWK["e"] = getBigNumberBytes(e).base64EncodeUrlSafe; 332 jvJWK["kty"] = "RSA"; 333 jvJWK["n"] = getBigNumberBytes(n).base64EncodeUrlSafe; 334 jwkData_ = jvJWK; 335 jwkString_ = jvJWK.toJSON; 336 jwkSHAHash_ = sha256Encode( jwkString_ ); 337 jwkThumbprint_ = jwkSHAHash_.base64EncodeUrlSafe.idup; 338 //myLog("JWK:\n", jvJWK.toPrettyString); 339 myLog("JWK:\n", jvJWK.toJSON); 340 myLog("SHA of JWK:\n", jwkSHAHash_); 341 myLog("Thumbprint of JWK:\n", jwkThumbprint_); 342 } 343 ~this () { 344 SSL_CloseLibrary(); 345 } 346 347 /** Call once after instantiating AcmeClient to setup parameters 348 * 349 * This function will fetch the directory from the ACME CA server and 350 * extracts the Urls. 351 * 352 * Also fetches the initial NOnce value for the next JWS transfer. 353 */ 354 void setupClient() 355 { 356 acmeRes.getResources(); 357 // Get initial Nonce 358 this.getNonce(); 359 360 } 361 362 /** Get a fresh and new Nonce from server 363 * 364 * To start the communication with JWS, an initial Nonce value must be 365 * fetched from the server. 366 * 367 * The Nonce returned is internally store in the AcmeResource structure. 368 * 369 * Note: 370 * Use the Nonce of a JWS response header to update the Nonce for the 371 * next transfer! So, only a single call to this function is needed to 372 * setup the initial transfer. 373 * 374 * Returns: 375 * a fresh and new Nonce value. 376 */ 377 string getNonce() 378 { 379 /* Get a NOnce number from server */ 380 auto nonce = getResponseHeader(acmeRes.newNOnceUrl, "Replay-Nonce"); 381 acmeRes.nonce = nonce; 382 return nonce; 383 } 384 385 /** Create a new account and bind a key pair to it. 386 * 387 * Before we can do anything, we need to register an account and 388 * bind a RSA/EC keypair to it, which is used for signatures in 389 * JWS and to create the CSR. 390 * 391 * Params: 392 * contacts = list of contacts for the account 393 * tosAgreed = set this to true, when user ack on commandline. otherwise 394 * the default is false, and the CA server might refuse to 395 * operate in this case. 396 * onlyReturnExisting = do not create a new account, but only reuse an 397 * existing one. Defaults to false. When set to true, an 398 * account is never created, but only existing accounts are 399 * returned. Useful to lookup the accountUrl of an key. 400 * 401 * Note: tosAgreed must be queried from user, e.g. by setting a commandline 402 * option. This is required by the RFC8555. 403 * Note: Usually there is no need to set useExisting to false. If set to 404 * true, an existing account for a JWK is returned or new one 405 * is created and returned. 406 */ 407 bool createNewAccount(string[] contacts, bool tosAgreed = false, bool onlyReturnExisting = false) 408 { 409 bool rc; 410 /* Create newAccount payload */ 411 JSONValue jvPayload; 412 jvPayload["termsOfServiceAgreed"] = tosAgreed; 413 /* Do not create new account? Used for lookups of accountURL */ 414 if (onlyReturnExisting) 415 jvPayload["onlyReturnExisting"] = true; 416 const JSONValue jvContact = contacts; 417 jvPayload["contact"] = jvContact; 418 419 string payload = jvPayload.toJSON; 420 421 string[string] rheaders; 422 import std.net.curl : HTTP; 423 HTTP.StatusLine statusLine; 424 string response = sendRequest!(string,false)(acmeRes.newAccountUrl, payload, &statusLine, &rheaders); 425 if (statusLine.code / 100 == 2) 426 { 427 acmeRes.accountUrl = rheaders["location"]; 428 writeln("Account Location : ", acmeRes.accountUrl); 429 430 auto json = parseJSON(response); 431 if ("createdAt" in json) 432 writeln("Account Creation : ", json["createdAt"]); 433 // ... 434 rc = true; 435 } 436 else { 437 stderr.writeln("Got http error: ", statusLine); 438 stderr.writeln("Got response:\n", response); 439 // FIXME handle different error types... 440 } 441 return rc; 442 } 443 444 /** Authorization setup callback 445 * 446 * The implementation of this function allows Let's Encrypt to 447 * verify that the requestor has control of the domain name. 448 * 449 * The callback may be called once for each domain name in the 450 * 'issueCertificate' call. The callback should do whatever is 451 * needed so that a GET on the 'url' returns the 'keyAuthorization', 452 * (which is what the Acme protocol calls the expected response.) 453 * 454 * Note that this function may not be called in cases where 455 * Let's Encrypt already believes the caller has control 456 * of the domain name. 457 */ 458 alias Callback = 459 int function ( 460 string domainName, 461 string url, 462 string keyAuthorization); 463 464 /** Issue a certificate for domainNames 465 * 466 * The client begins the certificate issuance process by sending a POST 467 * request to the server's newOrder resource. The body of the POST is a 468 * JWS object whose JSON payload is a subset of the order object defined 469 * in Section 7.1.3, containing the fields that describe the certificate 470 * to be issued. 471 * 472 * Params: 473 * domainKeyData = the private PEM-encoded key 474 * domainNames = list of domains 475 * callback = pointer to function to setup expected response 476 * on given URL 477 * Returns: A Certificate object or null. 478 * Throws: an instance of AcmeException on fatal or unexpected errors. 479 */ 480 Certificate issueCertificate(string domainKeyData, string[] domainNames, Callback callback) 481 { 482 if (domainNames.empty) 483 throw new AcmeException("There must be at least one domain name in a certificate"); 484 485 /* Pass any challenges we need to pass to make the CA believe we're entitled to a certificate. */ 486 JSONValue[] jvIdentifiers; 487 jvIdentifiers.length = domainNames.length; 488 foreach (i, domain ; domainNames) 489 { 490 jvIdentifiers[i]["type"] = "dns"; 491 jvIdentifiers[i]["value"] = domain; 492 } 493 JSONValue jvIdentifiersArray; 494 jvIdentifiersArray.array = jvIdentifiers; 495 496 JSONValue jvPayload; 497 // ISSUE: https://community.letsencrypt.org/t/notbefore-and-notafter-are-not-supported/54712 498 version (boulderHasBeforeAfter) { 499 jvPayload["notBefore"] = "2016-01-01T00:04:00+04:00"; // FIXME - use DateTime.to...() 500 jvPayload["notAfter"] = "2020-01-01T00:04:00+04:00"; // FIXME - use DateTime.to...() 501 } 502 jvPayload["identifiers"] = jvIdentifiersArray; 503 504 string payload = jvPayload.toJSON; 505 myLog("Payload : ", jvPayload.toPrettyString); 506 HTTP.StatusLine statusLine; 507 string response = sendRequest!string(acmeRes.newOrderUrl, payload, &statusLine); 508 509 if (statusLine.code / 100 != 2) { 510 stderr.writeln("Got http error: ", statusLine); 511 stderr.writeln("Got response:\n", response); 512 throw new AcmeException("Issue Request failed."); 513 } 514 auto json = parseJSON(response); 515 myLog(json.toPrettyString); 516 517 /* If you pass a challenge, that's good for 300 days. The cert is only good for 90. 518 * This means for a while you can re-issue without passing another challenge, so we 519 * check to see if we need to validate again. 520 * 521 * Note that this introduces a race since it possible for the status to not be valid 522 * by the time the certificate is requested. The assumption is that client retries 523 * will deal with this. 524 */ 525 static if (!is(JSONType)) { 526 pragma(msg,"Has no public JSONType - skip test"); 527 enum bool typeTestOk = true; 528 } else { 529 bool typeTestOk = json["status"].type == JSONType..string; 530 } 531 if ( ("status" in json) && 532 (typeTestOk) && 533 (json["status"].str != "valid") ) 534 { 535 if ("authorizations" in json) { 536 auto authorizations = json["authorizations"]; 537 foreach ( i, authorizationUrl ; authorizations.array) 538 { 539 string authurl = authorizationUrl.str; 540 string response2 = sendRequest!string(authurl, "", &statusLine); 541 if (statusLine.code / 100 != 2) { 542 stderr.writeln("Got http error: ", statusLine); 543 stderr.writeln("Got response:\n", response2); 544 stdout.flush; 545 throw new AcmeException("Auth Request failed."); 546 //return cast(Certificate)null; 547 } 548 auto json2 = parseJSON(response2); 549 myLog(json2.toPrettyString); 550 551 if ("challenges" in json2) 552 { 553 auto domain = json2["identifier"]["value"].str; 554 auto challenges = json2["challenges"]; 555 foreach (j, challenge; challenges.array) 556 { 557 if ( ("type" in challenge) && 558 (challenge["type"].str == "http-01") ) 559 { 560 const string token = challenge["token"].str; 561 string url = "http://" ~ domain ~ "/.well-known/acme-challenge/" ~ token; 562 string keyAuthorization = token ~ "." ~ jwkThumbprint_.to!string; 563 const auto rc = callback(domain, url, keyAuthorization); 564 if (rc != 0) 565 throw new AcmeException("challange setup script failed."); 566 verifyChallengePassed(authorizationUrl.str, challenge); 567 break; 568 } 569 } 570 } 571 } 572 } 573 } else { 574 writefln("Send payload: \n%s", jvPayload.toPrettyString); 575 writefln("Got failure response:\n%s", json.toPrettyString); 576 throw new AcmeException(json.toPrettyString); 577 } 578 579 // Issue the certificate 580 // auto r = makeCertificateSigningRequest(domainNames); 581 // string csr = r.csr; 582 // string privateKey = r.pkey; 583 const char[] privateKey = domainKeyData /* openSSL_CreatePrivateKey() */; 584 const char[] csr = openSSL_CreateCertificateSignRequest(privateKey, domainNames); 585 586 myLog("CSR:\n", csr); 587 588 /* Send CSRs and get the intermediate certs */ 589 string[string] rheaders; 590 591 JSONValue ncrs; 592 ncrs["csr"] = csr; 593 594 auto finalizeUrl = json["finalize"].str; 595 auto finalizePayLoad = ncrs.toJSON; 596 auto finalizeResponseStr = sendRequest!(char[])(finalizeUrl, finalizePayLoad, &statusLine, &rheaders); 597 if (statusLine.code / 100 != 2) { 598 stderr.writeln("Got http error: ", statusLine); 599 stderr.writeln("Got response:\n", finalizeResponseStr); 600 stdout.flush; 601 throw new AcmeException("Verification for passed challange failed."); 602 } 603 auto finalizeResponseJV = parseJSON(finalizeResponseStr); 604 myLog(finalizeResponseJV.toPrettyString); 605 606 /* Download the certificate (via POST-as-GET) */ 607 auto certificateUrl = finalizeResponseJV["certificate"].str; 608 auto crtpem = sendRequest!(char[])(certificateUrl, "", &statusLine, &rheaders); 609 myLog(crtpem); 610 611 /* Create a container object */ 612 Certificate cert; 613 cert.fullchain = crtpem.to!string; 614 cert.privkey = privateKey.idup; 615 return cert; 616 } 617 618 /** Acknowledge to CA server that a Auth is setup for check. 619 * 620 * Params: 621 * authorizationUrl = url to a auth job 622 * challenge = the current challange for reference 623 * 624 * Throws if the challenge isn't accepted (or on timeout) 625 */ 626 void verifyChallengePassed(string authorizationUrl, JSONValue challenge) 627 { 628 string verificationUri = challenge["url"].str; 629 630 import std.net.curl : HTTP; 631 HTTP.StatusLine statusLine; 632 string response = sendRequest!string(verificationUri, q"({})", &statusLine ); 633 if (statusLine.code / 100 != 2) { 634 stderr.writeln("Got http error: ", statusLine); 635 stderr.writeln("Got response:\n", response); 636 stdout.flush; 637 throw new AcmeException("Verification for passed challange failed."); 638 //return cast(Certificate)null; 639 } 640 641 // Poll waiting for the CA to verify the challenge 642 int counter = 0; 643 enum loopDelay = 2; 644 enum maxRetryCount = 60; 645 writefln("Waiting for valid http-01 challange. (%d times a %d seconds)", maxRetryCount, loopDelay); 646 do 647 { 648 // sleep for a second 649 import core.thread : Thread; 650 Thread.sleep(dur!"seconds"(loopDelay)); 651 652 // get response from verification URL 653 response = sendRequest!string(authorizationUrl, "", &statusLine); 654 if (statusLine.code / 100 != 2) { 655 stderr.writeln("Got http error: ", statusLine); 656 stderr.writeln("Got response:\n", response); 657 stdout.flush; 658 throw new AcmeException("Verification for passed challange failed."); 659 } 660 else { 661 myLog(response); 662 auto json = parseJSON(response); 663 myLog(json.toPrettyString); 664 if (json["status"].str == "valid") 665 { 666 writeln("Challange valid. Continue."); 667 return; 668 } 669 else 670 myLog!writefln("Waiting for http-01 challange to be valid (%d/%d)", counter, maxRetryCount); 671 } 672 } while (counter++ < maxRetryCount); 673 674 throw new AcmeException("Failure / timeout verifying challenge passed"); 675 } 676 }