github.com/letsencrypt/boulder@v0.20251208.0/test/integration/observer_test.go (about)

     1  //go:build integration
     2  
     3  package integration
     4  
     5  import (
     6  	"bufio"
     7  	"context"
     8  	"crypto/ecdsa"
     9  	"crypto/elliptic"
    10  	"crypto/rand"
    11  	"crypto/x509"
    12  	"encoding/pem"
    13  	"fmt"
    14  	"net/http"
    15  	"os"
    16  	"os/exec"
    17  	"path"
    18  	"path/filepath"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/eggsampler/acme/v3"
    24  )
    25  
    26  func streamOutput(t *testing.T, c *exec.Cmd) (<-chan string, func()) {
    27  	t.Helper()
    28  	outChan := make(chan string)
    29  
    30  	stdout, err := c.StdoutPipe()
    31  	if err != nil {
    32  		t.Fatalf("getting stdout handle: %s", err)
    33  	}
    34  
    35  	outScanner := bufio.NewScanner(stdout)
    36  	go func() {
    37  		for outScanner.Scan() {
    38  			outChan <- outScanner.Text()
    39  		}
    40  	}()
    41  
    42  	stderr, err := c.StderrPipe()
    43  	if err != nil {
    44  		t.Fatalf("getting stderr handle: %s", err)
    45  	}
    46  
    47  	errScanner := bufio.NewScanner(stderr)
    48  	go func() {
    49  		for errScanner.Scan() {
    50  			outChan <- errScanner.Text()
    51  		}
    52  	}()
    53  
    54  	err = c.Start()
    55  	if err != nil {
    56  		t.Fatalf("starting cmd: %s", err)
    57  	}
    58  
    59  	return outChan, func() {
    60  		c.Cancel()
    61  		c.Wait()
    62  	}
    63  }
    64  
    65  func TestTLSProbe(t *testing.T) {
    66  	t.Parallel()
    67  
    68  	// We can't use random_domain(), because the observer needs to be able to
    69  	// resolve this hostname within the docker-compose environment.
    70  	hostname := "integration.trust"
    71  	tempdir := t.TempDir()
    72  
    73  	// Create the certificate that the prober will inspect.
    74  	client, err := makeClient()
    75  	if err != nil {
    76  		t.Fatalf("creating test acme client: %s", err)
    77  	}
    78  
    79  	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    80  	if err != nil {
    81  		t.Fatalf("generating test key: %s", err)
    82  	}
    83  
    84  	res, err := authAndIssue(client, key, []acme.Identifier{{Type: "dns", Value: hostname}}, true, "")
    85  	if err != nil {
    86  		t.Fatalf("issuing test cert: %s", err)
    87  	}
    88  
    89  	// Set up the HTTP server that the prober will be pointed at.
    90  	certFile, err := os.Create(path.Join(tempdir, "fullchain.pem"))
    91  	if err != nil {
    92  		t.Fatalf("creating cert file: %s", err)
    93  	}
    94  
    95  	err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: res.certs[0].Raw})
    96  	if err != nil {
    97  		t.Fatalf("writing test cert to file: %s", err)
    98  	}
    99  
   100  	err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: res.certs[1].Raw})
   101  	if err != nil {
   102  		t.Fatalf("writing test issuer cert to file: %s", err)
   103  	}
   104  
   105  	err = certFile.Close()
   106  	if err != nil {
   107  		t.Errorf("closing cert file: %s", err)
   108  	}
   109  
   110  	keyFile, err := os.Create(path.Join(tempdir, "privkey.pem"))
   111  	if err != nil {
   112  		t.Fatalf("creating key file: %s", err)
   113  	}
   114  
   115  	keyDER, err := x509.MarshalECPrivateKey(key)
   116  	if err != nil {
   117  		t.Fatalf("marshalling test key: %s", err)
   118  	}
   119  
   120  	err = pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
   121  	if err != nil {
   122  		t.Fatalf("writing test key to file: %s", err)
   123  	}
   124  
   125  	err = keyFile.Close()
   126  	if err != nil {
   127  		t.Errorf("closing key file: %s", err)
   128  	}
   129  
   130  	go http.ListenAndServeTLS(":8675", certFile.Name(), keyFile.Name(), http.DefaultServeMux)
   131  
   132  	// Kick off the prober, pointed at the server presenting our test cert.
   133  	configFile, err := os.Create(path.Join(tempdir, "observer.yml"))
   134  	if err != nil {
   135  		t.Fatalf("creating config file: %s", err)
   136  	}
   137  
   138  	_, err = configFile.WriteString(fmt.Sprintf(`---
   139  buckets: [.001, .002, .005, .01, .02, .05, .1, .2, .5, 1, 2, 5, 10]
   140  syslog:
   141    stdoutlevel: 6
   142    sysloglevel: 0
   143  monitors:
   144    -
   145      period: 1s
   146      kind: TLS
   147      settings:
   148        response: valid
   149        hostname: "%s:8675"`, hostname))
   150  	if err != nil {
   151  		t.Fatalf("writing test config: %s", err)
   152  	}
   153  
   154  	binPath, err := filepath.Abs("bin/boulder")
   155  	if err != nil {
   156  		t.Fatalf("computing boulder binary path: %s", err)
   157  	}
   158  
   159  	c := exec.CommandContext(context.Background(), binPath, "boulder-observer", "-config", configFile.Name(), "-debug-addr", ":8024")
   160  	output, cancel := streamOutput(t, c)
   161  	defer cancel()
   162  
   163  	timeout := time.NewTimer(5 * time.Second)
   164  
   165  	for {
   166  		select {
   167  		case <-timeout.C:
   168  			t.Fatalf("timed out before getting desired log line from boulder-observer")
   169  		case line := <-output:
   170  			t.Log(line)
   171  			if strings.Contains(line, "name=[integration.trust:8675]") && strings.Contains(line, "success=[true]") {
   172  				return
   173  			}
   174  		}
   175  	}
   176  }