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 import acme.openssl_glues; 152 153 /** The full CA chain with cert */ 154 string fullchain; 155 /** The private key to sign requests */ 156 string privkey; 157 158 // Note that neither of the 'Expiry' calls below require 'privkey' 159 // to be set; they only rely on 'fullchain'. 160 161 /** 162 Returns the number of seconds since 1970, i.e., epoch time. 163 164 Due to openssl quirkiness there might be a little drift 165 from a strictly accurate result, but it should be close 166 enough for the purpose of determining whether the certificate 167 needs to be renewed. 168 */ 169 DateTime getExpiry() const 170 { 171 static const(DateTime) extractor(const ASN1_TIME * t) 172 { 173 // See this link for issues in converting from ASN1_TIME to epoch time. 174 // https://stackoverflow.com/questions/10975542/asn1-time-to-time-t-conversion 175 int days, seconds; 176 if (!C_ASN1_TIME_diff(&days, &seconds, cast(ASN1_TIME*)null, cast(ASN1_TIME*)t)) 177 { 178 throw new AcmeException("Can't get time diff."); 179 } 180 // Hackery here, since the call to time(0) will not necessarily match 181 // the equivilent call openssl just made in the 'diff' call above. 182 // Nonetheless, it'll be close at worst. 183 auto dt = DateTime( Date(0) ); 184 dt += dur!"seconds"(seconds + days * 3600 * 24); 185 return dt; 186 } 187 DateTime rc = extractExpiryData!(DateTime, extractor)(this.fullchain); 188 return rc; 189 } 190 191 /** Returns the 'Not After' result that openssl would display if 192 running the following command. 193 194 openssl x509 -noout -in fullchain.pem -text 195 196 For example: 197 198 May 6 21:15:03 2018 GMT 199 */ 200 string getExpiryDisplay() const 201 { 202 string extractor(const ASN1_TIME * t) 203 { 204 BIO* b = C_ASN1_TIME_print(t); 205 scope(exit) C_BIO_free(b); 206 return b.toVector.to!string; 207 } 208 return extractExpiryData!(string, extractor)(this.fullchain); 209 } 210 } 211 212 /* ------------------------------------------------------------------------ */ 213 214 /** A simple ACME v2 client 215 * 216 * This class implements the ACME v2 protocol to obtain signed SSL 217 * certificates. 218 */ 219 class AcmeClient 220 { 221 private: 222 import acme.openssl_glues; 223 EVP_PKEY* privateKey_; /// Copy of private key as ASC PEM 224 225 JSONValue jwkData_; /// JWK object as JSONValue tree 226 string jwkString_; /// JWK as plain JSON string 227 ubyte[] jwkSHAHash_; /// The SHA256 hash value of jwkString_ 228 string jwkThumbprint_; /// Base64 url-safe string of jwkSHAHash_ 229 230 bool beVerbose_; /// Be verbose 231 232 void myLog(alias fun = writeln, T...)(T args) 233 { 234 if (beVerbose_) { 235 fun(args); 236 stdout.flush; 237 } 238 } 239 240 /** Create and send a JWS request with payload to a ACME enabled CA server 241 * 242 * Still unfinished template to build the JWS object. This code must be 243 * refactored later. 244 * 245 * Params: 246 * T = return type 247 * useKID = useKID 248 * url = Url to post to 249 * payload = data to send 250 * status = pointer to StatusLine 251 * rheaders = Pointer to ResponseHeaders received from server, or null 252 * 253 * See: https://tools.ietf.org/html/rfc7515 254 */ 255 T sendRequest(T, bool useKID = true) 256 (string url, string payload, HTTP.StatusLine* status = null, string[string]* rheaders = null) 257 if ( is(T : string) || is(T : char[]) || is(T : ubyte[])) 258 { 259 string nonce = this.acmeRes.nonce; 260 assert(nonce !is null && !nonce.empty, "Invalid Nonce value."); 261 myLog("Using NOnce: ", nonce); 262 263 /* Create protection data */ 264 JSONValue jvReqHeader; 265 jvReqHeader["alg"] = "RS256"; 266 static if (useKID) 267 jvReqHeader["kid"] = acmeRes.accountUrl; 268 else 269 jvReqHeader["jwk"] = jwkData_; 270 jvReqHeader["nonce"] = nonce; 271 jvReqHeader["url"] = url; 272 char[] protectd = jvReqHeader.toJSON.dup; 273 myLog(protectd); 274 protectd = base64EncodeUrlSafe(protectd); 275 276 const char[] payld = base64EncodeUrlSafe(payload); 277 278 auto signData = protectd ~ "." ~ payld; 279 myLog!writefln("Data to sign: %s", signData); 280 char[] signature = signDataWithSHA256(signData, privateKey_); 281 myLog!writefln("Signature: %s", signature); 282 283 JSONValue jvBody; 284 jvBody["protected"] = protectd; 285 jvBody["payload"] = payld; 286 jvBody["signature"] = signature; 287 char[] body_ = jvBody.toJSON.dup; 288 myLog!writefln("Body: %s", jvBody.toPrettyString); 289 290 auto response = doPost(url, body_, status, rheaders, &(acmeRes.nonce)); 291 if (rheaders) { 292 myLog( "ResponseHeaders: "); 293 foreach( v ; (*rheaders).byKey) { 294 myLog(" ", v, " : ", (*rheaders)[v]); 295 //~ if (v.toLower == "replay-nonce") { 296 //~ acmeRes.nonce = (*rheaders)[v]; 297 //~ myLog("Setting new NOnce: ", acmeRes.nonce); 298 //~ } 299 } 300 } 301 myLog( "Response: ", response); 302 303 return to!T(response); 304 } 305 306 public: 307 AcmeResources acmeRes; /// The Urls to the ACME resources 308 309 310 /** Instanciate a AcmeClient using a private key for signing 311 * 312 * Param: 313 * accountPrivateKey = The signingKey is the Acme account private 314 * key used to sign requests to the acme CA, in pem format. 315 * Throws: an instance of AcmeException on fatal or unexpected errors. 316 */ 317 this(string accountPrivateKey, bool beVerbose = false) 318 { 319 import acme.openssl_glues; 320 beVerbose_ = beVerbose; 321 322 acmeRes.initClient( useStagingServer ? directoryUrlStaging : directoryUrlProd); 323 SSL_OpenLibrary(); 324 325 /* Create the private key */ 326 RSA* rsa; 327 privateKey_ = SSL_x509_read_pkey_memory( cast(const(char[]))accountPrivateKey, &rsa); 328 329 BIGNUM* n; 330 BIGNUM* e; 331 BIGNUM* d; 332 C_RSA_Get0_key(rsa, &n, &e, &d); 333 334 // https://tools.ietf.org/html/rfc7638 335 // JSON Web Key (JWK) Thumbprint 336 JSONValue jvJWK; 337 jvJWK["e"] = getBigNumberBytes(e).base64EncodeUrlSafe; 338 jvJWK["kty"] = "RSA"; 339 jvJWK["n"] = getBigNumberBytes(n).base64EncodeUrlSafe; 340 jwkData_ = jvJWK; 341 jwkString_ = jvJWK.toJSON; 342 jwkSHAHash_ = sha256Encode( jwkString_ ); 343 jwkThumbprint_ = jwkSHAHash_.base64EncodeUrlSafe.idup; 344 //myLog("JWK:\n", jvJWK.toPrettyString); 345 myLog("JWK:\n", jvJWK.toJSON); 346 myLog("SHA of JWK:\n", jwkSHAHash_); 347 myLog("Thumbprint of JWK:\n", jwkThumbprint_); 348 } 349 ~this () { 350 SSL_CloseLibrary(); 351 } 352 353 /** Call once after instantiating AcmeClient to setup parameters 354 * 355 * This function will fetch the directory from the ACME CA server and 356 * extracts the Urls. 357 * 358 * Also fetches the initial NOnce value for the next JWS transfer. 359 */ 360 void setupClient() 361 { 362 acmeRes.getResources(); 363 // Get initial Nonce 364 this.getNonce(); 365 366 } 367 368 /** Get a fresh and new Nonce from server 369 * 370 * To start the communication with JWS, an initial Nonce value must be 371 * fetched from the server. 372 * 373 * The Nonce returned is internally store in the AcmeResource structure. 374 * 375 * Note: 376 * Use the Nonce of a JWS response header to update the Nonce for the 377 * next transfer! So, only a single call to this function is needed to 378 * setup the initial transfer. 379 * 380 * Returns: 381 * a fresh and new Nonce value. 382 */ 383 string getNonce() 384 { 385 /* Get a NOnce number from server */ 386 auto nonce = getResponseHeader(acmeRes.newNOnceUrl, "Replay-Nonce"); 387 acmeRes.nonce = nonce; 388 return nonce; 389 } 390 391 /** Create a new account and bind a key pair to it. 392 * 393 * Before we can do anything, we need to register an account and 394 * bind a RSA/EC keypair to it, which is used for signatures in 395 * JWS and to create the CSR. 396 * 397 * Params: 398 * contacts = list of contacts for the account 399 * tosAgreed = set this to true, when user ack on commandline. otherwise 400 * the default is false, and the CA server might refuse to 401 * operate in this case. 402 * onlyReturnExisting = do not create a new account, but only reuse an 403 * existing one. Defaults to false. When set to true, an 404 * account is never created, but only existing accounts are 405 * returned. Useful to lookup the accountUrl of an key. 406 * 407 * Note: tosAgreed must be queried from user, e.g. by setting a commandline 408 * option. This is required by the RFC8555. 409 * Note: Usually there is no need to set useExisting to false. If set to 410 * true, an existing account for a JWK is returned or new one 411 * is created and returned. 412 */ 413 bool createNewAccount(string[] contacts, bool tosAgreed = false, bool onlyReturnExisting = false) 414 { 415 bool rc; 416 /* Create newAccount payload */ 417 JSONValue jvPayload; 418 jvPayload["termsOfServiceAgreed"] = tosAgreed; 419 /* Do not create new account? Used for lookups of accountURL */ 420 if (onlyReturnExisting) 421 jvPayload["onlyReturnExisting"] = true; 422 const JSONValue jvContact = contacts; 423 jvPayload["contact"] = jvContact; 424 425 string payload = jvPayload.toJSON; 426 427 string[string] rheaders; 428 import std.net.curl : HTTP; 429 HTTP.StatusLine statusLine; 430 string response = sendRequest!(string,false)(acmeRes.newAccountUrl, payload, &statusLine, &rheaders); 431 if (statusLine.code / 100 == 2) 432 { 433 acmeRes.accountUrl = rheaders["location"]; 434 writeln("Account Location : ", acmeRes.accountUrl); 435 436 auto json = parseJSON(response); 437 if ("createdAt" in json) 438 writeln("Account Creation : ", json["createdAt"]); 439 // ... 440 rc = true; 441 } 442 else { 443 stderr.writeln("Got http error: ", statusLine); 444 stderr.writeln("Got response:\n", response); 445 // FIXME handle different error types... 446 } 447 return rc; 448 } 449 450 /** Authorization setup callback 451 * 452 * The implementation of this function allows Let's Encrypt to 453 * verify that the requestor has control of the domain name. 454 * 455 * The callback may be called once for each domain name in the 456 * 'issueCertificate' call. The callback should do whatever is 457 * needed so that a GET on the 'url' returns the 'keyAuthorization', 458 * (which is what the Acme protocol calls the expected response.) 459 * 460 * Note that this function may not be called in cases where 461 * Let's Encrypt already believes the caller has control 462 * of the domain name. 463 */ 464 alias Callback = 465 int function ( 466 string domainName, 467 string url, 468 string keyAuthorization); 469 470 /** Issue a certificate for domainNames 471 * 472 * The client begins the certificate issuance process by sending a POST 473 * request to the server's newOrder resource. The body of the POST is a 474 * JWS object whose JSON payload is a subset of the order object defined 475 * in Section 7.1.3, containing the fields that describe the certificate 476 * to be issued. 477 * 478 * Params: 479 * domainKeyData = the private PEM-encoded key 480 * domainNames = list of domains 481 * callback = pointer to function to setup expected response 482 * on given URL 483 * Returns: A Certificate object or null. 484 * Throws: an instance of AcmeException on fatal or unexpected errors. 485 */ 486 Certificate issueCertificate(string domainKeyData, string[] domainNames, Callback callback) 487 { 488 if (domainNames.empty) 489 throw new AcmeException("There must be at least one domain name in a certificate"); 490 491 /* Pass any challenges we need to pass to make the CA believe we're entitled to a certificate. */ 492 JSONValue[] jvIdentifiers; 493 jvIdentifiers.length = domainNames.length; 494 foreach (i, domain ; domainNames) 495 { 496 jvIdentifiers[i]["type"] = "dns"; 497 jvIdentifiers[i]["value"] = domain; 498 } 499 JSONValue jvIdentifiersArray; 500 jvIdentifiersArray.array = jvIdentifiers; 501 502 JSONValue jvPayload; 503 // ISSUE: https://community.letsencrypt.org/t/notbefore-and-notafter-are-not-supported/54712 504 version (boulderHasBeforeAfter) { 505 jvPayload["notBefore"] = "2016-01-01T00:04:00+04:00"; // FIXME - use DateTime.to...() 506 jvPayload["notAfter"] = "2020-01-01T00:04:00+04:00"; // FIXME - use DateTime.to...() 507 } 508 jvPayload["identifiers"] = jvIdentifiersArray; 509 510 string payload = jvPayload.toJSON; 511 myLog("Payload : ", jvPayload.toPrettyString); 512 HTTP.StatusLine statusLine; 513 string response = sendRequest!string(acmeRes.newOrderUrl, payload, &statusLine); 514 515 if (statusLine.code / 100 != 2) { 516 stderr.writeln("Got http error: ", statusLine); 517 stderr.writeln("Got response:\n", response); 518 throw new AcmeException("Issue Request failed."); 519 } 520 auto json = parseJSON(response); 521 myLog(json.toPrettyString); 522 523 /* If you pass a challenge, that's good for 300 days. The cert is only good for 90. 524 * This means for a while you can re-issue without passing another challenge, so we 525 * check to see if we need to validate again. 526 * 527 * Note that this introduces a race since it possible for the status to not be valid 528 * by the time the certificate is requested. The assumption is that client retries 529 * will deal with this. 530 */ 531 if ( ("status" in json) && 532 (json["status"].type == JSONType..string) && 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 }