package main import ( "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "math/big" "net" "net/http" "os" "path" "strconv" "strings" "testing" "time" "github.com/beme/abide" "github.com/rogpeppe/go-internal/testscript" ) func TestMain(m *testing.M) { testscript.Main(m, map[string]func(){ "karma": main, }) err := abide.Cleanup() if err != nil { fmt.Printf("abide.Cleanup() error: %v\n", err) } } func TestScripts(t *testing.T) { testscript.Run(t, testscript.Params{ Dir: "tests/testscript", UpdateScripts: os.Getenv("UPDATE_SNAPSHOTS") == "1", Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ "http": httpServer, "cert": tlsCert, }, Setup: func(env *testscript.Env) error { env.Values["mocks"] = &httpMocks{responses: map[string][]httpMock{}} return nil }, }) } func httpServer(ts *testscript.TestScript, _ bool, args []string) { mocks := ts.Value("mocks").(*httpMocks) if len(args) == 0 { ts.Fatalf("! http command requires arguments") } cmd := args[0] ts.Logf("test-script http command: %s", strings.Join(args, " ")) switch cmd { // http response name /200 200 OK case "response": if len(args) < 5 { ts.Fatalf("! http response command requires '$NAME $PATH $CODE $BODY' args, got [%s]", strings.Join(args, " ")) } name := args[1] path := args[2] code, err := strconv.Atoi(args[3]) ts.Check(err) body := strings.Join(args[4:], " ") mocks.add(name, httpMock{pattern: path, handler: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(code) _, err := w.Write([]byte(body)) ts.Check(err) }}) // http response name /200 200 OK case "slow-response": if len(args) < 6 { ts.Fatalf("! http response command requires '$NAME $PATH $DELAY $CODE $BODY' args, got [%s]", strings.Join(args, " ")) } name := args[1] path := args[2] delay, err := time.ParseDuration(args[3]) ts.Check(err) code, err := strconv.Atoi(args[4]) ts.Check(err) body := strings.Join(args[5:], " ") mocks.add(name, httpMock{pattern: path, handler: func(w http.ResponseWriter, _ *http.Request) { time.Sleep(delay) w.WriteHeader(code) _, err := w.Write([]byte(body)) ts.Check(err) }}) // http redirect name /foo/src /dst case "redirect": if len(args) != 4 { ts.Fatalf("! http redirect command requires '$NAME $SRCPATH $DSTPATH' args, got [%s]", strings.Join(args, " ")) } name := args[1] srcpath := args[2] dstpath := args[3] mocks.add(name, httpMock{pattern: srcpath, handler: func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Location", dstpath) w.WriteHeader(http.StatusFound) }}) // http start name 127.0.0.1:7088 [TLS cert] [TLS key] case "start": if len(args) < 3 { ts.Fatalf("! http start command requires '$NAME $LISTEN [$TLS_CERT $TLS_KEY]' args, got [%s]", strings.Join(args, " ")) } name := args[1] listen := args[2] var isTLS bool var tlsCert, tlsKey string if len(args) == 5 { isTLS = true tlsCert = args[3] tlsKey = args[4] } mux := http.NewServeMux() mux.HandleFunc("/__ping__", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("pong")) }) for n, mockList := range mocks.responses { if n == name { for _, mock := range mockList { mux.HandleFunc(mock.pattern, mock.handler) } break } } listener, err := net.Listen("tcp", listen) ts.Check(err) server := &http.Server{Addr: listen, Handler: mux} sChan := make(chan struct{}, 1) sCtx, sCancel := context.WithTimeout(context.Background(), time.Minute*5) ts.Defer(func() { ts.Logf("running deferred http server shutdown") defer sCancel() sChan <- struct{}{} ts.Logf("http server %s shutting down", name) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = server.Shutdown(ctx) }) go func() { select { case <-sCtx.Done(): ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = server.Shutdown(ctx) // nolint: contextcheck ts.Fatalf("http server %s running for too long, forcing a shutdown: %s", name, sCtx.Err()) case <-sChan: return } }() go func() { if isTLS { err = server.ServeTLS(listener, tlsCert, tlsKey) } else { err = server.Serve(listener) } if err != nil && !errors.Is(err, http.ErrServerClosed) { ts.Fatalf("http server failed to start: %s", err) } }() tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client := &http.Client{Transport: tr} try := 0 for { try++ if try > 30 { ts.Fatalf("HTTP server didn't pass health checks after %d check(s)", try) } ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) uri := fmt.Sprintf("http://%s/__ping__", listen) if isTLS { uri = fmt.Sprintf("https://%s/__ping__", listen) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) ts.Check(err) resp, err := client.Do(req) if err == nil && resp.StatusCode == http.StatusOK { ts.Logf("http server ready after %d check(s)", try) cancel() return } cancel() time.Sleep(time.Second) } default: ts.Fatalf("! unknown http command: %v", args) } } type httpMock struct { handler func(http.ResponseWriter, *http.Request) pattern string } type httpMocks struct { responses map[string][]httpMock } func (m *httpMocks) add(name string, mock httpMock) { if _, ok := m.responses[name]; !ok { m.responses[name] = []httpMock{} } m.responses[name] = append(m.responses[name], mock) } func tlsCert(ts *testscript.TestScript, _ bool, args []string) { if len(args) < 2 { ts.Fatalf("! cert command requires '$DIRNAME $NAME' args, got [%s]", strings.Join(args, " ")) } dirname := args[0] name := args[1] ts.Logf("test-script cert command: %s", strings.Join(args, " ")) ca := &x509.Certificate{ SerialNumber: big.NewInt(2019), Subject: pkix.Name{ Organization: []string{"Company, INC."}, Country: []string{"US"}, Province: []string{""}, Locality: []string{"San Francisco"}, StreetAddress: []string{""}, PostalCode: []string{""}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(10, 0, 0), IsCA: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, } caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { ts.Fatalf("failed to generate CA private key: %s", err) } caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) if err != nil { ts.Fatalf("failed to generate CA cert: %s", err) } writeCert(ts, dirname, name+"-ca.pem", &pem.Block{ Type: "CERTIFICATE", Bytes: caBytes, }) writeCert(ts, dirname, name+"-ca.key", &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), }) cert := &x509.Certificate{ SerialNumber: big.NewInt(1658), Subject: pkix.Name{ Organization: []string{""}, Country: []string{""}, Province: []string{""}, Locality: []string{""}, StreetAddress: []string{""}, PostalCode: []string{""}, }, IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 1), SubjectKeyId: []byte{1, 2, 3, 4, 6}, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature, } certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { ts.Fatalf("failed to generate cert private key: %s", err) } certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) if err != nil { ts.Fatalf("failed to generate cert: %s", err) } writeCert(ts, dirname, name+".pem", &pem.Block{ Type: "CERTIFICATE", Bytes: certBytes, }) writeCert(ts, dirname, name+".key", &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), }) } func writeCert(ts *testscript.TestScript, dirname, filename string, block *pem.Block) { fullpath := path.Join(dirname, filename) f, err := os.Create(fullpath) if err != nil { ts.Fatalf("failed to write %s: %s", fullpath, err) } if err = pem.Encode(f, block); err != nil { ts.Fatalf("failed to encode %s: %s", fullpath, err) } if err = f.Close(); err != nil { ts.Fatalf("failed to close %s: %s", fullpath, err) } }