github.com/letsencrypt/boulder@v0.20251208.0/test/chall-test-srv-client/client.go (about) 1 package challtestsrvclient 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "encoding/base32" 7 "encoding/base64" 8 "encoding/json" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 "strings" 14 ) 15 16 // Client is an HTTP client for https://github.com/letsencrypt/challtestsrv's 17 // management interface (test/chall-test-srv). 18 type Client struct { 19 baseURL string 20 } 21 22 // NewClient creates a new Client using the provided baseURL, or defaults to 23 // http://10.77.77.77:8055 if none is provided. 24 func NewClient(baseURL string) *Client { 25 if baseURL == "" { 26 baseURL = "http://10.77.77.77:8055" 27 } 28 return &Client{baseURL: baseURL} 29 } 30 31 const ( 32 setIPv4 = "set-default-ipv4" 33 setIPv6 = "set-default-ipv6" 34 delHistory = "clear-request-history" 35 getHTTPHistory = "http-request-history" 36 getDNSHistory = "dns-request-history" 37 getALPNHistory = "tlsalpn01-request-history" 38 addA = "add-a" 39 delA = "clear-a" 40 addAAAA = "add-aaaa" 41 delAAAA = "clear-aaaa" 42 addCAA = "add-caa" 43 delCAA = "clear-caa" 44 addRedirect = "add-redirect" 45 delRedirect = "del-redirect" 46 addHTTP = "add-http01" 47 delHTTP = "del-http01" 48 addTXT = "set-txt" 49 delTXT = "clear-txt" 50 addALPN = "add-tlsalpn01" 51 delALPN = "del-tlsalpn01" 52 addServfail = "set-servfail" 53 delServfail = "clear-servfail" 54 ) 55 56 func (c *Client) postURL(path string, body any) ([]byte, error) { 57 endpoint, err := url.JoinPath(c.baseURL, path) 58 if err != nil { 59 return nil, fmt.Errorf("joining URL %q with path %q: %w", c.baseURL, path, err) 60 } 61 62 payload, err := json.Marshal(body) 63 if err != nil { 64 return nil, fmt.Errorf("marshalling payload for %s: %w", endpoint, err) 65 } 66 67 resp, err := http.Post(endpoint, "application/json", bytes.NewBuffer(payload)) 68 if err != nil { 69 return nil, fmt.Errorf("sending POST to %s: %w", endpoint, err) 70 } 71 defer resp.Body.Close() 72 73 if resp.StatusCode != http.StatusOK { 74 return nil, fmt.Errorf("unexpected status code %d from %s", resp.StatusCode, endpoint) 75 } 76 respBytes, err := io.ReadAll(resp.Body) 77 if err != nil { 78 return nil, fmt.Errorf("reading response from %s: %w", endpoint, err) 79 } 80 return respBytes, nil 81 } 82 83 // SetDefaultIPv4 sets the challenge server's default IPv4 address used to 84 // respond to A queries when there are no specific mock A addresses for the 85 // hostname being queried. Provide an empty string as the default address to 86 // disable answering A queries except for hosts that have mock A addresses 87 // added. Any failure returns an error that includes both the relevant operation 88 // and the payload. 89 func (c *Client) SetDefaultIPv4(addr string) ([]byte, error) { 90 payload := map[string]string{"ip": addr} 91 resp, err := c.postURL(setIPv4, payload) 92 if err != nil { 93 return nil, fmt.Errorf( 94 "while setting default IPv4 to %q (payload: %v): %w", 95 addr, payload, err, 96 ) 97 } 98 return resp, nil 99 } 100 101 // SetDefaultIPv6 sets the challenge server's default IPv6 address used to 102 // respond to AAAA queries when there are no specific mock AAAA addresses for 103 // the hostname being queried. Provide an empty string as the default address to 104 // disable answering AAAA queries except for hosts that have mock AAAA addresses 105 // added. Any failure returns an error that includes both the relevant operation 106 // and the payload. 107 func (c *Client) SetDefaultIPv6(addr string) ([]byte, error) { 108 payload := map[string]string{"ip": addr} 109 resp, err := c.postURL(setIPv6, payload) 110 if err != nil { 111 return nil, fmt.Errorf( 112 "while setting default IPv6 to %q (payload: %v): %w", 113 addr, payload, err, 114 ) 115 } 116 return resp, nil 117 } 118 119 // AddARecord adds a mock A response to the challenge server's DNS interface for 120 // the given host and IPv4 addresses. Any failure returns an error that includes 121 // both the relevant operation and the payload. 122 func (c *Client) AddARecord(host string, addresses []string) ([]byte, error) { 123 payload := map[string]any{ 124 "host": host, 125 "addresses": addresses, 126 } 127 resp, err := c.postURL(addA, payload) 128 if err != nil { 129 return nil, fmt.Errorf( 130 "while adding A record for host %q (payload: %v): %w", 131 host, payload, err, 132 ) 133 } 134 return resp, nil 135 } 136 137 // RemoveARecord removes a mock A response from the challenge server's DNS 138 // interface for the given host. Any failure returns an error that includes both 139 // the relevant operation and the payload. 140 func (c *Client) RemoveARecord(host string) ([]byte, error) { 141 payload := map[string]string{"host": host} 142 resp, err := c.postURL(delA, payload) 143 if err != nil { 144 return nil, fmt.Errorf( 145 "while removing A record for host %q (payload: %v): %w", 146 host, payload, err, 147 ) 148 } 149 return resp, nil 150 } 151 152 // AddAAAARecord adds a mock AAAA response to the challenge server's DNS 153 // interface for the given host and IPv6 addresses. Any failure returns an error 154 // that includes both the relevant operation and the payload. 155 func (c *Client) AddAAAARecord(host string, addresses []string) ([]byte, error) { 156 payload := map[string]any{ 157 "host": host, 158 "addresses": addresses, 159 } 160 resp, err := c.postURL(addAAAA, payload) 161 if err != nil { 162 return nil, fmt.Errorf( 163 "while adding AAAA record for host %q (payload: %v): %w", 164 host, payload, err, 165 ) 166 } 167 return resp, nil 168 } 169 170 // RemoveAAAARecord removes mock AAAA response from the challenge server's DNS 171 // interface for the given host. Any failure returns an error that includes both 172 // the relevant operation and the payload. 173 func (c *Client) RemoveAAAARecord(host string) ([]byte, error) { 174 payload := map[string]string{"host": host} 175 resp, err := c.postURL(delAAAA, payload) 176 if err != nil { 177 return nil, fmt.Errorf( 178 "while removing AAAA record for host %q (payload: %v): %w", 179 host, payload, err, 180 ) 181 } 182 return resp, nil 183 } 184 185 // AddCAAIssue adds a mock CAA response to the challenge server's DNS interface. 186 // The mock CAA response will contain one policy with an "issue" tag specifying 187 // the provided value. Any failure returns an error that includes both the 188 // relevant operation and the payload. 189 func (c *Client) AddCAAIssue(host, value string) ([]byte, error) { 190 payload := map[string]any{ 191 "host": host, 192 "policies": []map[string]string{ 193 {"tag": "issue", "value": value}, 194 }, 195 } 196 resp, err := c.postURL(addCAA, payload) 197 if err != nil { 198 return nil, fmt.Errorf( 199 "while adding CAA issue for host %q, val %q (payload: %v): %w", 200 host, value, payload, err, 201 ) 202 } 203 return resp, nil 204 } 205 206 // RemoveCAAIssue removes a mock CAA response from the challenge server's DNS 207 // interface for the given host. Any failure returns an error that includes both 208 // the relevant operation and the payload. 209 func (c *Client) RemoveCAAIssue(host string) ([]byte, error) { 210 payload := map[string]string{"host": host} 211 resp, err := c.postURL(delCAA, payload) 212 if err != nil { 213 return nil, fmt.Errorf( 214 "while removing CAA issue for host %q (payload: %v): %w", 215 host, payload, err, 216 ) 217 } 218 return resp, nil 219 } 220 221 // HTTPRequest is a single HTTP request in the request history. 222 type HTTPRequest struct { 223 URL string `json:"URL"` 224 Host string `json:"Host"` 225 HTTPS bool `json:"HTTPS"` 226 ServerName string `json:"ServerName"` 227 UserAgent string `json:"UserAgent"` 228 } 229 230 // HTTPRequestHistory fetches the challenge server's HTTP request history for 231 // the given host. 232 func (c *Client) HTTPRequestHistory(host string) ([]HTTPRequest, error) { 233 payload := map[string]string{"host": host} 234 raw, err := c.postURL(getHTTPHistory, payload) 235 if err != nil { 236 return nil, fmt.Errorf( 237 "while fetching HTTP request history for host %q (payload: %v): %w", 238 host, payload, err, 239 ) 240 } 241 var data []HTTPRequest 242 err = json.Unmarshal(raw, &data) 243 if err != nil { 244 return nil, fmt.Errorf("unmarshalling HTTP request history: %w", err) 245 } 246 return data, nil 247 } 248 249 func (c *Client) clearRequestHistory(host, typ string) ([]byte, error) { 250 return c.postURL(delHistory, map[string]string{"host": host, "type": typ}) 251 } 252 253 // ClearHTTPRequestHistory clears the challenge server's HTTP request history 254 // for the given host. Any failure returns an error that includes both the 255 // relevant operation and the payload. 256 func (c *Client) ClearHTTPRequestHistory(host string) ([]byte, error) { 257 resp, err := c.clearRequestHistory(host, "http") 258 if err != nil { 259 return nil, fmt.Errorf( 260 "while clearing HTTP request history for host %q: %w", host, err, 261 ) 262 } 263 return resp, nil 264 } 265 266 // AddHTTPRedirect adds a redirect to the challenge server's HTTP interfaces for 267 // HTTP requests to the given path directing the client to the targetURL. 268 // Redirects are not served for HTTPS requests. Any failure returns an error 269 // that includes both the relevant operation and the payload. 270 func (c *Client) AddHTTPRedirect(path, targetURL string) ([]byte, error) { 271 payload := map[string]string{"path": path, "targetURL": targetURL} 272 resp, err := c.postURL(addRedirect, payload) 273 if err != nil { 274 return nil, fmt.Errorf( 275 "while adding HTTP redirect for path %q -> %q (payload: %v): %w", 276 path, targetURL, payload, err, 277 ) 278 } 279 return resp, nil 280 } 281 282 // RemoveHTTPRedirect removes a redirect from the challenge server's HTTP 283 // interfaces for the given path. Any failure returns an error that includes 284 // both the relevant operation and the payload. 285 func (c *Client) RemoveHTTPRedirect(path string) ([]byte, error) { 286 payload := map[string]string{"path": path} 287 resp, err := c.postURL(delRedirect, payload) 288 if err != nil { 289 return nil, fmt.Errorf( 290 "while removing HTTP redirect for path %q (payload: %v): %w", 291 path, payload, err, 292 ) 293 } 294 return resp, nil 295 } 296 297 // AddHTTP01Response adds an ACME HTTP-01 challenge response for the provided 298 // token under the /.well-known/acme-challenge/ path of the challenge test 299 // server's HTTP interfaces. The given keyauth will be returned as the HTTP 300 // response body for requests to the challenge token. Any failure returns an 301 // error that includes both the relevant operation and the payload. 302 func (c *Client) AddHTTP01Response(token, keyauth string) ([]byte, error) { 303 payload := map[string]string{"token": token, "content": keyauth} 304 resp, err := c.postURL(addHTTP, payload) 305 if err != nil { 306 return nil, fmt.Errorf( 307 "while adding HTTP-01 challenge response for token %q (payload: %v): %w", 308 token, payload, err, 309 ) 310 } 311 return resp, nil 312 } 313 314 // RemoveHTTP01Response removes an ACME HTTP-01 challenge response for the 315 // provided token from the challenge test server. Any failure returns an error 316 // that includes both the relevant operation and the payload. 317 func (c *Client) RemoveHTTP01Response(token string) ([]byte, error) { 318 payload := map[string]string{"token": token} 319 resp, err := c.postURL(delHTTP, payload) 320 if err != nil { 321 return nil, fmt.Errorf( 322 "while removing HTTP-01 challenge response for token %q (payload: %v): %w", 323 token, payload, err, 324 ) 325 } 326 return resp, nil 327 } 328 329 // AddServfailResponse configures the challenge test server to return SERVFAIL 330 // for all queries made for the provided host. This will override any other 331 // mocks for the host until removed with remove_servfail_response. Any failure 332 // returns an error that includes both the relevant operation and the payload. 333 func (c *Client) AddServfailResponse(host string) ([]byte, error) { 334 payload := map[string]string{"host": host} 335 resp, err := c.postURL(addServfail, payload) 336 if err != nil { 337 return nil, fmt.Errorf( 338 "while adding SERVFAIL response for host %q (payload: %v): %w", 339 host, payload, err, 340 ) 341 } 342 return resp, nil 343 } 344 345 // RemoveServfailResponse undoes the work of AddServfailResponse, removing the 346 // SERVFAIL configuration for the given host. Any failure returns an error that 347 // includes both the relevant operation and the payload. 348 func (c *Client) RemoveServfailResponse(host string) ([]byte, error) { 349 payload := map[string]string{"host": host} 350 resp, err := c.postURL(delServfail, payload) 351 if err != nil { 352 return nil, fmt.Errorf( 353 "while removing SERVFAIL response for host %q (payload: %v): %w", 354 host, payload, err, 355 ) 356 } 357 return resp, nil 358 } 359 360 // AddDNS01Response adds an ACME DNS-01 challenge response for the provided host 361 // to the challenge test server's DNS interfaces. The value is hashed and 362 // base64-encoded using RawURLEncoding, and served for TXT queries to 363 // _acme-challenge.<host>. Any failure returns an error that includes both the 364 // relevant operation and the payload. 365 func (c *Client) AddDNS01Response(host, value string) ([]byte, error) { 366 host = "_acme-challenge." + host 367 if !strings.HasSuffix(host, ".") { 368 host += "." 369 } 370 h := sha256.Sum256([]byte(value)) 371 value = base64.RawURLEncoding.EncodeToString(h[:]) 372 payload := map[string]string{"host": host, "value": value} 373 resp, err := c.postURL(addTXT, payload) 374 if err != nil { 375 return nil, fmt.Errorf( 376 "while adding DNS-01 response for host %q, val %q (payload: %v): %w", 377 host, value, payload, err, 378 ) 379 } 380 return resp, nil 381 } 382 383 // RemoveDNS01Response removes an ACME DNS-01 challenge response for the 384 // provided host from the challenge test server's DNS interfaces. Any failure 385 // returns an error that includes both the relevant operation and the payload. 386 func (c *Client) RemoveDNS01Response(host string) ([]byte, error) { 387 if !strings.HasPrefix(host, "_acme-challenge.") { 388 host = "_acme-challenge." + host 389 } 390 if !strings.HasSuffix(host, ".") { 391 host += "." 392 } 393 payload := map[string]string{"host": host} 394 resp, err := c.postURL(delTXT, payload) 395 if err != nil { 396 return nil, fmt.Errorf( 397 "while removing DNS-01 response for host %q (payload: %v): %w", 398 host, payload, err, 399 ) 400 } 401 return resp, nil 402 } 403 404 // AddDNSAccount01Response adds an ACME DNS-ACCOUNT-01 challenge response for the 405 // provided host to the challenge test server's DNS interfaces. The TXT record 406 // name is constructed using the accountURL, and the TXT record value is the 407 // base64url encoded SHA-256 hash of the provided value. Any failure returns an 408 // error that includes the relevant operation and the payload. 409 func (c *Client) AddDNSAccount01Response(accountURL, host, value string) ([]byte, error) { 410 if accountURL == "" { 411 return nil, fmt.Errorf("accountURL cannot be empty") 412 } 413 if host == "" { 414 return nil, fmt.Errorf("host cannot be empty") 415 } 416 label, err := calculateDNSAccount01Label(accountURL) 417 if err != nil { 418 return nil, fmt.Errorf("error calculating DNS label: %v", err) 419 } 420 host = fmt.Sprintf("%s._acme-challenge.%s", label, host) 421 if !strings.HasSuffix(host, ".") { 422 host += "." 423 } 424 h := sha256.Sum256([]byte(value)) 425 value = base64.RawURLEncoding.EncodeToString(h[:]) 426 payload := map[string]string{"host": host, "value": value} 427 resp, err := c.postURL(addTXT, payload) 428 if err != nil { 429 return nil, fmt.Errorf( 430 "while adding DNS-ACCOUNT-01 response for host %q, val %q (payload: %v): %w", 431 host, value, payload, err, 432 ) 433 } 434 return resp, nil 435 } 436 437 // RemoveDNSAccount01Response removes an ACME DNS-ACCOUNT-01 challenge 438 // response for the provided host and accountURL combination from the 439 // challenge test server's DNS interfaces. The TXT record name is 440 // constructed using the accountURL. Any failure returns an error 441 // that includes both the relevant operation and the payload. 442 func (c *Client) RemoveDNSAccount01Response(accountURL, host string) ([]byte, error) { 443 if accountURL == "" { 444 return nil, fmt.Errorf("accountURL cannot be empty") 445 } 446 if host == "" { 447 return nil, fmt.Errorf("host cannot be empty") 448 } 449 label, err := calculateDNSAccount01Label(accountURL) 450 if err != nil { 451 return nil, fmt.Errorf("error calculating DNS label: %v", err) 452 } 453 host = fmt.Sprintf("%s._acme-challenge.%s", label, host) 454 if !strings.HasSuffix(host, ".") { 455 host += "." 456 } 457 payload := map[string]string{"host": host} 458 resp, err := c.postURL(delTXT, payload) 459 if err != nil { 460 return nil, fmt.Errorf( 461 "while removing DNS-ACCOUNT-01 response for host %q (payload: %v): %w", 462 host, payload, err, 463 ) 464 } 465 return resp, nil 466 } 467 468 func calculateDNSAccount01Label(accountURL string) (string, error) { 469 if accountURL == "" { 470 return "", fmt.Errorf("account URL cannot be empty") 471 } 472 473 h := sha256.Sum256([]byte(accountURL)) 474 label := fmt.Sprintf("_%s", strings.ToLower(base32.StdEncoding.EncodeToString(h[:10]))) 475 return label, nil 476 } 477 478 // DNSRequest is a single DNS request in the request history. 479 type DNSRequest struct { 480 Question struct { 481 Name string `json:"Name"` 482 Qtype uint16 `json:"Qtype"` 483 Qclass uint16 `json:"Qclass"` 484 } `json:"Question"` 485 UserAgent string `json:"UserAgent"` 486 } 487 488 // DNSRequestHistory returns the history of DNS requests made to the challenge 489 // test server's DNS interfaces for the given host. Any failure returns an error 490 // that includes both the relevant operation and the payload. 491 func (c *Client) DNSRequestHistory(host string) ([]DNSRequest, error) { 492 payload := map[string]string{"host": host} 493 raw, err := c.postURL(getDNSHistory, payload) 494 if err != nil { 495 return nil, fmt.Errorf( 496 "while fetching DNS request history for host %q (payload: %v): %w", 497 host, payload, err, 498 ) 499 } 500 var data []DNSRequest 501 err = json.Unmarshal(raw, &data) 502 if err != nil { 503 return nil, fmt.Errorf("unmarshalling DNS request history: %w", err) 504 } 505 return data, nil 506 } 507 508 // ClearDNSRequestHistory clears the history of DNS requests made to the 509 // challenge test server's DNS interfaces for the given host. Any failure 510 // returns an error that includes both the relevant operation and the payload. 511 func (c *Client) ClearDNSRequestHistory(host string) ([]byte, error) { 512 resp, err := c.clearRequestHistory(host, "dns") 513 if err != nil { 514 return nil, fmt.Errorf( 515 "while clearing DNS request history for host %q: %w", host, err, 516 ) 517 } 518 return resp, nil 519 } 520 521 // TLSALPN01Request is a single TLS-ALPN-01 request in the request history. 522 type TLSALPN01Request struct { 523 ServerName string `json:"ServerName"` 524 SupportedProtos []string `json:"SupportedProtos"` 525 } 526 527 // AddTLSALPN01Response adds an ACME TLS-ALPN-01 challenge response certificate 528 // to the challenge test server's TLS-ALPN-01 interface for the given host. The 529 // provided key authorization value will be embedded in the response certificate 530 // served to clients that initiate a TLS-ALPN-01 challenge validation with the 531 // challenge test server for the provided host. Any failure returns an error 532 // that includes both the relevant operation and the payload. 533 func (c *Client) AddTLSALPN01Response(host, value string) ([]byte, error) { 534 payload := map[string]string{"host": host, "content": value} 535 resp, err := c.postURL(addALPN, payload) 536 if err != nil { 537 return nil, fmt.Errorf( 538 "while adding TLS-ALPN-01 response for host %q, val %q (payload: %v): %w", 539 host, value, payload, err, 540 ) 541 } 542 return resp, nil 543 } 544 545 // RemoveTLSALPN01Response removes an ACME TLS-ALPN-01 challenge response 546 // certificate from the challenge test server's TLS-ALPN-01 interface for the 547 // given host. Any failure returns an error that includes both the relevant 548 // operation and the payload. 549 func (c *Client) RemoveTLSALPN01Response(host string) ([]byte, error) { 550 payload := map[string]string{"host": host} 551 resp, err := c.postURL(delALPN, payload) 552 if err != nil { 553 return nil, fmt.Errorf( 554 "while removing TLS-ALPN-01 response for host %q (payload: %v): %w", 555 host, payload, err, 556 ) 557 } 558 return resp, nil 559 } 560 561 // TLSALPN01RequestHistory returns the history of TLS-ALPN-01 requests made to 562 // the challenge test server's TLS-ALPN-01 interface for the given host. Any 563 // failure returns an error that includes both the relevant operation and the 564 // payload. 565 func (c *Client) TLSALPN01RequestHistory(host string) ([]TLSALPN01Request, error) { 566 payload := map[string]string{"host": host} 567 raw, err := c.postURL(getALPNHistory, payload) 568 if err != nil { 569 return nil, fmt.Errorf( 570 "while fetching TLS-ALPN-01 request history for host %q (payload: %v): %w", 571 host, payload, err, 572 ) 573 } 574 var data []TLSALPN01Request 575 err = json.Unmarshal(raw, &data) 576 if err != nil { 577 return nil, fmt.Errorf("unmarshalling TLS-ALPN-01 request history: %w", err) 578 } 579 return data, nil 580 } 581 582 // ClearTLSALPN01RequestHistory clears the history of TLS-ALPN-01 requests made 583 // to the challenge test server's TLS-ALPN-01 interface for the given host. Any 584 // failure returns an error that includes both the relevant operation and the 585 // payload. 586 func (c *Client) ClearTLSALPN01RequestHistory(host string) ([]byte, error) { 587 resp, err := c.clearRequestHistory(host, "tlsalpn") 588 if err != nil { 589 return nil, fmt.Errorf( 590 "while clearing TLS-ALPN-01 request history for host %q: %w", host, err, 591 ) 592 } 593 return resp, nil 594 }