// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package acmetest provides types for testing acme and autocert packages.
//
// TODO: Consider moving this to x/crypto/acme/internal/acmetest for acme tests as well.
package acmetest

import (
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"math/big"
	"net"
	"net/http"
	"net/http/httptest"
	"path"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"golang.org/x/crypto/acme"
)

// CAServer is a simple test server which implements ACME spec bits needed for testing.
type CAServer struct {
	rootKey      crypto.Signer
	rootCert     []byte // DER encoding
	rootTemplate *x509.Certificate

	t              *testing.T
	server         *httptest.Server
	issuer         pkix.Name
	challengeTypes []string
	url            string
	roots          *x509.CertPool
	eabRequired    bool

	mu             sync.Mutex
	certCount      int                           // number of issued certs
	acctRegistered bool                          // set once an account has been registered
	domainAddr     map[string]string             // domain name to addr:port resolution
	domainGetCert  map[string]getCertificateFunc // domain name to GetCertificate function
	domainHandler  map[string]http.Handler       // domain name to Handle function
	validAuthz     map[string]*authorization     // valid authz, keyed by domain name
	authorizations []*authorization              // all authz, index is used as ID
	orders         []*order                      // index is used as order ID
	errors         []error                       // encountered client errors
}

type getCertificateFunc func(hello *tls.ClientHelloInfo) (*tls.Certificate, error)

// NewCAServer creates a new ACME test server. The returned CAServer issues
// certs signed with the CA roots available in the Roots field.
func NewCAServer(t *testing.T) *CAServer {
	ca := &CAServer{t: t,
		challengeTypes: []string{"fake-01", "tls-alpn-01", "http-01"},
		domainAddr:     make(map[string]string),
		domainGetCert:  make(map[string]getCertificateFunc),
		domainHandler:  make(map[string]http.Handler),
		validAuthz:     make(map[string]*authorization),
	}

	ca.server = httptest.NewUnstartedServer(http.HandlerFunc(ca.handle))

	r, err := rand.Int(rand.Reader, big.NewInt(1000000))
	if err != nil {
		panic(fmt.Sprintf("rand.Int: %v", err))
	}
	ca.issuer = pkix.Name{
		Organization: []string{"Test Acme Co"},
		CommonName:   "Root CA " + r.String(),
	}

	return ca
}

func (ca *CAServer) generateRoot() {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err))
	}
	tmpl := &x509.Certificate{
		SerialNumber:          big.NewInt(1),
		Subject:               ca.issuer,
		NotBefore:             time.Now(),
		NotAfter:              time.Now().Add(365 * 24 * time.Hour),
		KeyUsage:              x509.KeyUsageCertSign,
		BasicConstraintsValid: true,
		IsCA:                  true,
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		panic(fmt.Sprintf("x509.CreateCertificate: %v", err))
	}
	cert, err := x509.ParseCertificate(der)
	if err != nil {
		panic(fmt.Sprintf("x509.ParseCertificate: %v", err))
	}
	ca.roots = x509.NewCertPool()
	ca.roots.AddCert(cert)
	ca.rootKey = key
	ca.rootCert = der
	ca.rootTemplate = tmpl
}

// IssuerName sets the name of the issuing CA.
func (ca *CAServer) IssuerName(name pkix.Name) *CAServer {
	if ca.url != "" {
		panic("IssuerName must be called before Start")
	}
	ca.issuer = name
	return ca
}

// ChallengeTypes sets the supported challenge types.
func (ca *CAServer) ChallengeTypes(types ...string) *CAServer {
	if ca.url != "" {
		panic("ChallengeTypes must be called before Start")
	}
	ca.challengeTypes = types
	return ca
}

// URL returns the server address, after Start has been called.
func (ca *CAServer) URL() string {
	if ca.url == "" {
		panic("URL called before Start")
	}
	return ca.url
}

// Roots returns a pool containing the CA root.
func (ca *CAServer) Roots() *x509.CertPool {
	if ca.url == "" {
		panic("Roots called before Start")
	}
	return ca.roots
}

// ExternalAccountRequired makes an EAB JWS required for account registration.
func (ca *CAServer) ExternalAccountRequired() *CAServer {
	if ca.url != "" {
		panic("ExternalAccountRequired must be called before Start")
	}
	ca.eabRequired = true
	return ca
}

// Start starts serving requests. The server address becomes available in the
// URL field.
func (ca *CAServer) Start() *CAServer {
	if ca.url == "" {
		ca.generateRoot()
		ca.server.Start()
		ca.t.Cleanup(ca.server.Close)
		ca.url = ca.server.URL
	}
	return ca
}

func (ca *CAServer) serverURL(format string, arg ...interface{}) string {
	return ca.server.URL + fmt.Sprintf(format, arg...)
}

func (ca *CAServer) addr(domain string) (string, bool) {
	ca.mu.Lock()
	defer ca.mu.Unlock()
	addr, ok := ca.domainAddr[domain]
	return addr, ok
}

func (ca *CAServer) getCert(domain string) (getCertificateFunc, bool) {
	ca.mu.Lock()
	defer ca.mu.Unlock()
	f, ok := ca.domainGetCert[domain]
	return f, ok
}

func (ca *CAServer) getHandler(domain string) (http.Handler, bool) {
	ca.mu.Lock()
	defer ca.mu.Unlock()
	h, ok := ca.domainHandler[domain]
	return h, ok
}

func (ca *CAServer) httpErrorf(w http.ResponseWriter, code int, format string, a ...interface{}) {
	s := fmt.Sprintf(format, a...)
	ca.t.Errorf(format, a...)
	http.Error(w, s, code)
}

// Resolve adds a domain to address resolution for the ca to dial to
// when validating challenges for the domain authorization.
func (ca *CAServer) Resolve(domain, addr string) {
	ca.mu.Lock()
	defer ca.mu.Unlock()
	ca.domainAddr[domain] = addr
}

// ResolveGetCertificate redirects TLS connections for domain to f when
// validating challenges for the domain authorization.
func (ca *CAServer) ResolveGetCertificate(domain string, f getCertificateFunc) {
	ca.mu.Lock()
	defer ca.mu.Unlock()
	ca.domainGetCert[domain] = f
}

// ResolveHandler redirects HTTP requests for domain to f when
// validating challenges for the domain authorization.
func (ca *CAServer) ResolveHandler(domain string, h http.Handler) {
	ca.mu.Lock()
	defer ca.mu.Unlock()
	ca.domainHandler[domain] = h
}

type discovery struct {
	NewNonce   string `json:"newNonce"`
	NewAccount string `json:"newAccount"`
	NewOrder   string `json:"newOrder"`
	NewAuthz   string `json:"newAuthz"`

	Meta discoveryMeta `json:"meta,omitempty"`
}

type discoveryMeta struct {
	Terms                   string `json:"termsOfService,omitempty"`
	ExternalAccountRequired bool   `json:"externalAccountRequired,omitempty"`
}

type challenge struct {
	URI   string `json:"uri"`
	Type  string `json:"type"`
	Token string `json:"token"`
}

type authorization struct {
	Status     string      `json:"status"`
	Challenges []challenge `json:"challenges"`

	domain string
	id     int
}

type order struct {
	Status      string   `json:"status"`
	AuthzURLs   []string `json:"authorizations"`
	FinalizeURL string   `json:"finalize"`    // CSR submit URL
	CertURL     string   `json:"certificate"` // already issued cert

	leaf []byte // issued cert in DER format
}

func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) {
	ca.t.Logf("%s %s", r.Method, r.URL)
	w.Header().Set("Replay-Nonce", "nonce")
	// TODO: Verify nonce header for all POST requests.

	switch {
	default:
		ca.httpErrorf(w, http.StatusBadRequest, "unrecognized r.URL.Path: %s", r.URL.Path)

	// Discovery request.
	case r.URL.Path == "/":
		resp := &discovery{
			NewNonce:   ca.serverURL("/new-nonce"),
			NewAccount: ca.serverURL("/new-account"),
			NewOrder:   ca.serverURL("/new-order"),
			Meta: discoveryMeta{
				Terms:                   ca.serverURL("/terms"),
				ExternalAccountRequired: ca.eabRequired,
			},
		}
		if err := json.NewEncoder(w).Encode(resp); err != nil {
			panic(fmt.Sprintf("discovery response: %v", err))
		}

	// Nonce requests.
	case r.URL.Path == "/new-nonce":
		// Nonce values are always set. Nothing else to do.
		return

	// Client key registration request.
	case r.URL.Path == "/new-account":
		ca.mu.Lock()
		defer ca.mu.Unlock()
		if ca.acctRegistered {
			ca.httpErrorf(w, http.StatusServiceUnavailable, "multiple accounts are not implemented")
			return
		}
		ca.acctRegistered = true

		var req struct {
			ExternalAccountBinding json.RawMessage
		}

		if err := decodePayload(&req, r.Body); err != nil {
			ca.httpErrorf(w, http.StatusBadRequest, "%v", err)
			return
		}

		if ca.eabRequired && len(req.ExternalAccountBinding) == 0 {
			ca.httpErrorf(w, http.StatusBadRequest, "registration failed: no JWS for EAB")
			return
		}

		// TODO: Check the user account key against a ca.accountKeys?
		w.Header().Set("Location", ca.serverURL("/accounts/1"))
		w.WriteHeader(http.StatusCreated)
		w.Write([]byte("{}"))

	// New order request.
	case r.URL.Path == "/new-order":
		var req struct {
			Identifiers []struct{ Value string }
		}
		if err := decodePayload(&req, r.Body); err != nil {
			ca.httpErrorf(w, http.StatusBadRequest, "%v", err)
			return
		}
		ca.mu.Lock()
		defer ca.mu.Unlock()
		o := &order{Status: acme.StatusPending}
		for _, id := range req.Identifiers {
			z := ca.authz(id.Value)
			o.AuthzURLs = append(o.AuthzURLs, ca.serverURL("/authz/%d", z.id))
		}
		orderID := len(ca.orders)
		ca.orders = append(ca.orders, o)
		w.Header().Set("Location", ca.serverURL("/orders/%d", orderID))
		w.WriteHeader(http.StatusCreated)
		if err := json.NewEncoder(w).Encode(o); err != nil {
			panic(err)
		}

	// Existing order status requests.
	case strings.HasPrefix(r.URL.Path, "/orders/"):
		ca.mu.Lock()
		defer ca.mu.Unlock()
		o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/orders/"))
		if err != nil {
			ca.httpErrorf(w, http.StatusBadRequest, "%v", err)
			return
		}
		if err := json.NewEncoder(w).Encode(o); err != nil {
			panic(err)
		}

	// Accept challenge requests.
	case strings.HasPrefix(r.URL.Path, "/challenge/"):
		parts := strings.Split(r.URL.Path, "/")
		typ, id := parts[len(parts)-2], parts[len(parts)-1]
		ca.mu.Lock()
		supported := false
		for _, suppTyp := range ca.challengeTypes {
			if suppTyp == typ {
				supported = true
			}
		}
		a, err := ca.storedAuthz(id)
		ca.mu.Unlock()
		if !supported {
			ca.httpErrorf(w, http.StatusBadRequest, "unsupported challenge: %v", typ)
			return
		}
		if err != nil {
			ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: %v", err)
			return
		}
		ca.validateChallenge(a, typ)
		w.Write([]byte("{}"))

	// Get authorization status requests.
	case strings.HasPrefix(r.URL.Path, "/authz/"):
		var req struct{ Status string }
		decodePayload(&req, r.Body)
		deactivate := req.Status == "deactivated"
		ca.mu.Lock()
		defer ca.mu.Unlock()
		authz, err := ca.storedAuthz(strings.TrimPrefix(r.URL.Path, "/authz/"))
		if err != nil {
			ca.httpErrorf(w, http.StatusNotFound, "%v", err)
			return
		}
		if deactivate {
			// Note we don't invalidate authorized orders as we should.
			authz.Status = "deactivated"
			ca.t.Logf("authz %d is now %s", authz.id, authz.Status)
			ca.updatePendingOrders()
		}
		if err := json.NewEncoder(w).Encode(authz); err != nil {
			panic(fmt.Sprintf("encoding authz %d: %v", authz.id, err))
		}

	// Certificate issuance request.
	case strings.HasPrefix(r.URL.Path, "/new-cert/"):
		ca.mu.Lock()
		defer ca.mu.Unlock()
		orderID := strings.TrimPrefix(r.URL.Path, "/new-cert/")
		o, err := ca.storedOrder(orderID)
		if err != nil {
			ca.httpErrorf(w, http.StatusBadRequest, "%v", err)
			return
		}
		if o.Status != acme.StatusReady {
			ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status)
			return
		}
		// Validate CSR request.
		var req struct {
			CSR string `json:"csr"`
		}
		decodePayload(&req, r.Body)
		b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
		csr, err := x509.ParseCertificateRequest(b)
		if err != nil {
			ca.httpErrorf(w, http.StatusBadRequest, "%v", err)
			return
		}
		// Issue the certificate.
		der, err := ca.leafCert(csr)
		if err != nil {
			ca.httpErrorf(w, http.StatusBadRequest, "new-cert response: ca.leafCert: %v", err)
			return
		}
		o.leaf = der
		o.CertURL = ca.serverURL("/issued-cert/%s", orderID)
		o.Status = acme.StatusValid
		if err := json.NewEncoder(w).Encode(o); err != nil {
			panic(err)
		}

	// Already issued cert download requests.
	case strings.HasPrefix(r.URL.Path, "/issued-cert/"):
		ca.mu.Lock()
		defer ca.mu.Unlock()
		o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/issued-cert/"))
		if err != nil {
			ca.httpErrorf(w, http.StatusBadRequest, "%v", err)
			return
		}
		if o.Status != acme.StatusValid {
			ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status)
			return
		}
		w.Header().Set("Content-Type", "application/pem-certificate-chain")
		pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: o.leaf})
		pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: ca.rootCert})
	}
}

// storedOrder retrieves a previously created order at index i.
// It requires ca.mu to be locked.
func (ca *CAServer) storedOrder(i string) (*order, error) {
	idx, err := strconv.Atoi(i)
	if err != nil {
		return nil, fmt.Errorf("storedOrder: %v", err)
	}
	if idx < 0 {
		return nil, fmt.Errorf("storedOrder: invalid order index %d", idx)
	}
	if idx > len(ca.orders)-1 {
		return nil, fmt.Errorf("storedOrder: no such order %d", idx)
	}

	ca.updatePendingOrders()
	return ca.orders[idx], nil
}

// storedAuthz retrieves a previously created authz at index i.
// It requires ca.mu to be locked.
func (ca *CAServer) storedAuthz(i string) (*authorization, error) {
	idx, err := strconv.Atoi(i)
	if err != nil {
		return nil, fmt.Errorf("storedAuthz: %v", err)
	}
	if idx < 0 {
		return nil, fmt.Errorf("storedAuthz: invalid authz index %d", idx)
	}
	if idx > len(ca.authorizations)-1 {
		return nil, fmt.Errorf("storedAuthz: no such authz %d", idx)
	}
	return ca.authorizations[idx], nil
}

// authz returns an existing valid authorization for the identifier or creates a
// new one. It requires ca.mu to be locked.
func (ca *CAServer) authz(identifier string) *authorization {
	authz, ok := ca.validAuthz[identifier]
	if !ok {
		authzId := len(ca.authorizations)
		authz = &authorization{
			id:     authzId,
			domain: identifier,
			Status: acme.StatusPending,
		}
		for _, typ := range ca.challengeTypes {
			authz.Challenges = append(authz.Challenges, challenge{
				Type:  typ,
				URI:   ca.serverURL("/challenge/%s/%d", typ, authzId),
				Token: challengeToken(authz.domain, typ, authzId),
			})
		}
		ca.authorizations = append(ca.authorizations, authz)
	}
	return authz
}

// leafCert issues a new certificate.
// It requires ca.mu to be locked.
func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) {
	ca.certCount++ // next leaf cert serial number
	leaf := &x509.Certificate{
		SerialNumber:          big.NewInt(int64(ca.certCount)),
		Subject:               pkix.Name{Organization: []string{"Test Acme Co"}},
		NotBefore:             time.Now(),
		NotAfter:              time.Now().Add(90 * 24 * time.Hour),
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		DNSNames:              csr.DNSNames,
		BasicConstraintsValid: true,
	}
	if len(csr.DNSNames) == 0 {
		leaf.DNSNames = []string{csr.Subject.CommonName}
	}
	return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey)
}

// LeafCert issues a leaf certificate.
func (ca *CAServer) LeafCert(name, keyType string, notBefore, notAfter time.Time) *tls.Certificate {
	if ca.url == "" {
		panic("LeafCert called before Start")
	}

	ca.mu.Lock()
	defer ca.mu.Unlock()
	var pk crypto.Signer
	switch keyType {
	case "RSA":
		var err error
		pk, err = rsa.GenerateKey(rand.Reader, 1024)
		if err != nil {
			ca.t.Fatal(err)
		}
	case "ECDSA":
		var err error
		pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
		if err != nil {
			ca.t.Fatal(err)
		}
	default:
		panic("LeafCert: unknown key type")
	}
	ca.certCount++ // next leaf cert serial number
	leaf := &x509.Certificate{
		SerialNumber:          big.NewInt(int64(ca.certCount)),
		Subject:               pkix.Name{Organization: []string{"Test Acme Co"}},
		NotBefore:             notBefore,
		NotAfter:              notAfter,
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		DNSNames:              []string{name},
		BasicConstraintsValid: true,
	}
	der, err := x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, pk.Public(), ca.rootKey)
	if err != nil {
		ca.t.Fatal(err)
	}
	return &tls.Certificate{
		Certificate: [][]byte{der},
		PrivateKey:  pk,
	}
}

func (ca *CAServer) validateChallenge(authz *authorization, typ string) {
	var err error
	switch typ {
	case "tls-alpn-01":
		err = ca.verifyALPNChallenge(authz)
	case "http-01":
		err = ca.verifyHTTPChallenge(authz)
	default:
		panic(fmt.Sprintf("validation of %q is not implemented", typ))
	}
	ca.mu.Lock()
	defer ca.mu.Unlock()
	if err != nil {
		authz.Status = "invalid"
	} else {
		authz.Status = "valid"
		ca.validAuthz[authz.domain] = authz
	}
	ca.t.Logf("validated %q for %q, err: %v", typ, authz.domain, err)
	ca.t.Logf("authz %d is now %s", authz.id, authz.Status)

	ca.updatePendingOrders()
}

func (ca *CAServer) updatePendingOrders() {
	// Update all pending orders.
	// An order becomes "ready" if all authorizations are "valid".
	// An order becomes "invalid" if any authorization is "invalid".
	// Status changes: https://tools.ietf.org/html/rfc8555#section-7.1.6
	for i, o := range ca.orders {
		if o.Status != acme.StatusPending {
			continue
		}

		countValid, countInvalid := ca.validateAuthzURLs(o.AuthzURLs, i)
		if countInvalid > 0 {
			o.Status = acme.StatusInvalid
			ca.t.Logf("order %d is now invalid", i)
			continue
		}
		if countValid == len(o.AuthzURLs) {
			o.Status = acme.StatusReady
			o.FinalizeURL = ca.serverURL("/new-cert/%d", i)
			ca.t.Logf("order %d is now ready", i)
		}
	}
}

func (ca *CAServer) validateAuthzURLs(urls []string, orderNum int) (countValid, countInvalid int) {
	for _, zurl := range urls {
		z, err := ca.storedAuthz(path.Base(zurl))
		if err != nil {
			ca.t.Logf("no authz %q for order %d", zurl, orderNum)
			continue
		}
		if z.Status == acme.StatusInvalid {
			countInvalid++
		}
		if z.Status == acme.StatusValid {
			countValid++
		}
	}
	return countValid, countInvalid
}

func (ca *CAServer) verifyALPNChallenge(a *authorization) error {
	const acmeALPNProto = "acme-tls/1"

	addr, haveAddr := ca.addr(a.domain)
	getCert, haveGetCert := ca.getCert(a.domain)
	if !haveAddr && !haveGetCert {
		return fmt.Errorf("no resolution information for %q", a.domain)
	}
	if haveAddr && haveGetCert {
		return fmt.Errorf("overlapping resolution information for %q", a.domain)
	}

	var crt *x509.Certificate
	switch {
	case haveAddr:
		conn, err := tls.Dial("tcp", addr, &tls.Config{
			ServerName:         a.domain,
			InsecureSkipVerify: true,
			NextProtos:         []string{acmeALPNProto},
			MinVersion:         tls.VersionTLS12,
		})
		if err != nil {
			return err
		}
		if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto {
			return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto)
		}
		if n := len(conn.ConnectionState().PeerCertificates); n != 1 {
			return fmt.Errorf("len(PeerCertificates) = %d; want 1", n)
		}
		crt = conn.ConnectionState().PeerCertificates[0]
	case haveGetCert:
		hello := &tls.ClientHelloInfo{
			ServerName: a.domain,
			// TODO: support selecting ECDSA.
			CipherSuites:      []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
			SupportedProtos:   []string{acme.ALPNProto},
			SupportedVersions: []uint16{tls.VersionTLS12},
		}
		c, err := getCert(hello)
		if err != nil {
			return err
		}
		crt, err = x509.ParseCertificate(c.Certificate[0])
		if err != nil {
			return err
		}
	}

	if err := crt.VerifyHostname(a.domain); err != nil {
		return fmt.Errorf("verifyALPNChallenge: VerifyHostname: %v", err)
	}
	// See RFC 8737, Section 6.1.
	oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
	for _, x := range crt.Extensions {
		if x.Id.Equal(oid) {
			// TODO: check the token.
			return nil
		}
	}
	return fmt.Errorf("verifyTokenCert: no id-pe-acmeIdentifier extension found")
}

func (ca *CAServer) verifyHTTPChallenge(a *authorization) error {
	addr, haveAddr := ca.addr(a.domain)
	handler, haveHandler := ca.getHandler(a.domain)
	if !haveAddr && !haveHandler {
		return fmt.Errorf("no resolution information for %q", a.domain)
	}
	if haveAddr && haveHandler {
		return fmt.Errorf("overlapping resolution information for %q", a.domain)
	}

	token := challengeToken(a.domain, "http-01", a.id)
	path := "/.well-known/acme-challenge/" + token

	var body string
	switch {
	case haveAddr:
		t := &http.Transport{
			DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
				return (&net.Dialer{}).DialContext(ctx, network, addr)
			},
		}
		req, err := http.NewRequest("GET", "http://"+a.domain+path, nil)
		if err != nil {
			return err
		}
		res, err := t.RoundTrip(req)
		if err != nil {
			return err
		}
		if res.StatusCode != http.StatusOK {
			return fmt.Errorf("http token: w.Code = %d; want %d", res.StatusCode, http.StatusOK)
		}
		b, err := io.ReadAll(res.Body)
		if err != nil {
			return err
		}
		body = string(b)
	case haveHandler:
		r := httptest.NewRequest("GET", path, nil)
		r.Host = a.domain
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			return fmt.Errorf("http token: w.Code = %d; want %d", w.Code, http.StatusOK)
		}
		body = w.Body.String()
	}

	if !strings.HasPrefix(body, token) {
		return fmt.Errorf("http token value = %q; want 'token-http-01.' prefix", body)
	}
	return nil
}

func decodePayload(v interface{}, r io.Reader) error {
	var req struct{ Payload string }
	if err := json.NewDecoder(r).Decode(&req); err != nil {
		return err
	}
	payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
	if err != nil {
		return err
	}
	return json.Unmarshal(payload, v)
}

func challengeToken(domain, challType string, authzID int) string {
	return fmt.Sprintf("token-%s-%s-%d", domain, challType, authzID)
}
