github.com/openshift-online/ocm-sdk-go@v0.1.473/testing/servers.go (about)

     1  /*
     2  Copyright (c) 2019 Red Hat, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8    http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package testing
    18  
    19  import (
    20  	"crypto/rand"
    21  	"crypto/rsa"
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"crypto/x509/pkix"
    25  	"encoding/pem"
    26  	"log"
    27  	"math/big"
    28  	"net"
    29  	"net/http"
    30  	"net/url"
    31  	"os"
    32  	"path/filepath"
    33  	"time"
    34  
    35  	jsonpatch "github.com/evanphx/json-patch/v5"
    36  	"github.com/onsi/gomega/ghttp"
    37  	"golang.org/x/net/http2"
    38  	"golang.org/x/net/http2/h2c"
    39  
    40  	. "github.com/onsi/ginkgo/v2/dsl/core" // nolint
    41  	. "github.com/onsi/gomega"             // nolint
    42  )
    43  
    44  // MakeTCPServer creates a test server that listens in a TCP socket and configured so that it
    45  // sends log messages to the Ginkgo writer.
    46  func MakeTCPServer() *ghttp.Server {
    47  	server := ghttp.NewUnstartedServer()
    48  	server.Writer = GinkgoWriter
    49  	server.HTTPTestServer.Config.ErrorLog = log.New(GinkgoWriter, "", log.LstdFlags)
    50  	server.HTTPTestServer.EnableHTTP2 = true
    51  	server.HTTPTestServer.Start()
    52  	return server
    53  }
    54  
    55  // MakeUnixServer creates a test server that listens in a Unix socket and configured so that it
    56  // sends log messages to the Ginkgo writer. It returns the created server and name of a temporary
    57  // file containing the Unix socket. This file will be in a temporary directory, and the caller is
    58  // resposible for removing the directory once it is no longer needed.
    59  func MakeUnixServer() (server *ghttp.Server, socket string) {
    60  	// Create a temporary directory for the Unix sockets:
    61  	sockets, err := os.MkdirTemp("", "sockets")
    62  	Expect(err).ToNot(HaveOccurred())
    63  	socket = filepath.Join(sockets, "server.socket")
    64  
    65  	// Create the listener:
    66  	listener, err := net.Listen("unix", socket)
    67  	Expect(err).ToNot(HaveOccurred())
    68  
    69  	// Create and configure the server:
    70  	server = ghttp.NewUnstartedServer()
    71  	server.Writer = GinkgoWriter
    72  	server.HTTPTestServer.Config.ErrorLog = log.New(GinkgoWriter, "", log.LstdFlags)
    73  	server.HTTPTestServer.EnableHTTP2 = true
    74  	server.HTTPTestServer.Listener = listener
    75  	server.HTTPTestServer.Start()
    76  
    77  	return
    78  }
    79  
    80  // MakeTCPTLSServer creates a test server configured so that it sends log messages to the Ginkgo
    81  // writer. It returns the created server and the name of a temporary file that contains the CA
    82  // certificate that the client should trust in order to connect to the server. It is the
    83  // responsibility of the caller to delete this temporary file when it is no longer needed.
    84  func MakeTCPTLSServer() (server *ghttp.Server, ca string) {
    85  	// Create and configure the server:
    86  	server = ghttp.NewUnstartedServer()
    87  	server.Writer = GinkgoWriter
    88  	server.HTTPTestServer.Config.ErrorLog = log.New(GinkgoWriter, "", log.LstdFlags)
    89  	server.HTTPTestServer.EnableHTTP2 = true
    90  	server.HTTPTestServer.StartTLS()
    91  
    92  	// Fetch the CA certificate:
    93  	address, err := url.Parse(server.URL())
    94  	Expect(err).ToNot(HaveOccurred())
    95  	ca = fetchCACertificate("tcp", address.Host)
    96  
    97  	return
    98  }
    99  
   100  // MakeUnixTLSServer creates a test server that listens in a Unix socket and configured so that it
   101  // sends log messages to the Ginkgo writer. It returns the created server, the name of a temporary
   102  // file that contains the CA certificate that the client should trust in order to connect to the
   103  // server and the name of a directory containing the Unix sockets. This file will be in a temporary
   104  // directory. It is the responsibility of the caller to remove these temporary directories and
   105  // files.
   106  func MakeUnixTLSServer() (server *ghttp.Server, ca, socket string) {
   107  	// Create a temporary directory for the Unix sockets:
   108  	sockets, err := os.MkdirTemp("", "sockets")
   109  	Expect(err).ToNot(HaveOccurred())
   110  	socket = filepath.Join(sockets, "server.socket")
   111  
   112  	// Create the listener:
   113  	listener, err := net.Listen("unix", socket)
   114  	Expect(err).ToNot(HaveOccurred())
   115  
   116  	// Create and configure the server:
   117  	server = ghttp.NewUnstartedServer()
   118  	server.Writer = GinkgoWriter
   119  	server.HTTPTestServer.Config.ErrorLog = log.New(GinkgoWriter, "", log.LstdFlags)
   120  	server.HTTPTestServer.EnableHTTP2 = true
   121  	server.HTTPTestServer.Listener = listener
   122  	server.HTTPTestServer.StartTLS()
   123  
   124  	// Fetch the CA certificate:
   125  	ca = fetchCACertificate("unix", socket)
   126  
   127  	return
   128  }
   129  
   130  // MakeTCPH2CServer creates a test server that supports HTTP/2 without TLS, configured so that it
   131  // sends log messages to the Ginkgo writer.
   132  func MakeTCPH2CServer() *ghttp.Server {
   133  	// Create the server that supports HTTP/2 without TLS:
   134  	h2s := &http2.Server{}
   135  
   136  	// Create the regular server:
   137  	server := ghttp.NewUnstartedServer()
   138  	server.Writer = GinkgoWriter
   139  	server.HTTPTestServer.Config.ErrorLog = log.New(GinkgoWriter, "", log.LstdFlags)
   140  	server.HTTPTestServer.EnableHTTP2 = true
   141  
   142  	// Wrap the handler of the regular server with the handler that detects HTTP/2 requests
   143  	// without TLS and delegates them to the HTTP/2 server that supports that:
   144  	server.HTTPTestServer.Config.Handler = h2c.NewHandler(server.HTTPTestServer.Config.Handler, h2s)
   145  
   146  	// Start the server:
   147  	server.Start()
   148  
   149  	return server
   150  }
   151  
   152  // MakeUnixH2cServer creates a test server that listens in a Unix socket and supports HTTP/2 without
   153  // TLS, configured so that it sends log messages to the Ginkgo writer. It returns the created server
   154  // and name of a temporary file containing the Unix socket. This file will be in a temporary
   155  // directory, and the caller is resposible for removing the directory once it is no longer needed.
   156  func MakeUnixH2CServer() (server *ghttp.Server, socket string) {
   157  	// Create a temporary directory for the Unix sockets:
   158  	sockets, err := os.MkdirTemp("", "sockets")
   159  	Expect(err).ToNot(HaveOccurred())
   160  	socket = filepath.Join(sockets, "server.socket")
   161  
   162  	// Create the listener:
   163  	listener, err := net.Listen("unix", socket)
   164  	Expect(err).ToNot(HaveOccurred())
   165  
   166  	// Create the server that supports HTTP/2 without TLS:
   167  	h2s := &http2.Server{}
   168  
   169  	// Create the regular server:
   170  	server = ghttp.NewUnstartedServer()
   171  	server.Writer = GinkgoWriter
   172  	server.HTTPTestServer.Config.ErrorLog = log.New(GinkgoWriter, "", log.LstdFlags)
   173  	server.HTTPTestServer.EnableHTTP2 = true
   174  	server.HTTPTestServer.Listener = listener
   175  
   176  	// Wrap the handler of the regular server with the handler that detects HTTP/2 requests
   177  	// without TLS and delegates them to the HTTP/2 server that supports that:
   178  	server.HTTPTestServer.Config.Handler = h2c.NewHandler(server.HTTPTestServer.Config.Handler, h2s)
   179  
   180  	// Start the server:
   181  	server.Start()
   182  
   183  	return
   184  }
   185  
   186  // fetchCACertificates connects to the given network address and extracts the CA certificate from
   187  // the TLS handshake. It returns the path of a temporary file containing that CA certificate encoded
   188  // in PEM format. It is the responsibility of the caller to delete that file when it is no longer
   189  // needed.
   190  func fetchCACertificate(network, address string) string {
   191  	// Connect to the server and do the TLS handshake to obtain the certificate chain:
   192  	conn, err := tls.Dial(network, address, &tls.Config{
   193  		InsecureSkipVerify: true, // nolint
   194  	})
   195  	Expect(err).ToNot(HaveOccurred())
   196  	defer func() {
   197  		err = conn.Close()
   198  		Expect(err).ToNot(HaveOccurred())
   199  	}()
   200  	err = conn.Handshake()
   201  	Expect(err).ToNot(HaveOccurred())
   202  	certs := conn.ConnectionState().PeerCertificates
   203  	Expect(certs).ToNot(BeNil())
   204  	Expect(len(certs)).To(BeNumerically(">=", 1))
   205  	cert := certs[len(certs)-1]
   206  	Expect(cert).ToNot(BeNil())
   207  
   208  	// Serialize the CA certificate:
   209  	Expect(cert.Raw).ToNot(BeNil())
   210  	block := &pem.Block{
   211  		Type:  "CERTIFICATE",
   212  		Bytes: cert.Raw,
   213  	}
   214  	buffer := pem.EncodeToMemory(block)
   215  	Expect(buffer).ToNot(BeNil())
   216  
   217  	// Store the CA certificate in a temporary file:
   218  	file, err := os.CreateTemp("", "*.test.ca")
   219  	Expect(err).ToNot(HaveOccurred())
   220  	_, err = file.Write(buffer)
   221  	Expect(err).ToNot(HaveOccurred())
   222  	err = file.Close()
   223  	Expect(err).ToNot(HaveOccurred())
   224  
   225  	// Return the path of the temporary file:
   226  	return file.Name()
   227  }
   228  
   229  // RespondeWithContent responds with the given status code, content type and body.
   230  func RespondWithContent(status int, contentType, body string) http.HandlerFunc {
   231  	return ghttp.RespondWith(
   232  		status,
   233  		body,
   234  		http.Header{
   235  			"Content-Type": []string{
   236  				contentType,
   237  			},
   238  		},
   239  	)
   240  }
   241  
   242  // RespondWithJSON responds with the given status code and JSON body.
   243  func RespondWithJSON(status int, body string) http.HandlerFunc {
   244  	return RespondWithContent(status, "application/json", body)
   245  }
   246  
   247  // RespondWithJSONTemplate responds with the given status code and with a JSON body that is
   248  // generated from the given template and arguments. See the EvaluateTemplate function for details
   249  // on how the template and the arguments are combined.
   250  func RespondWithJSONTemplate(status int, source string, args ...interface{}) http.HandlerFunc {
   251  	return RespondWithJSON(status, EvaluateTemplate(source, args...))
   252  }
   253  
   254  // RespondWithPatchedJSON responds with the given status code and the result of
   255  // patching the given JSON with the given patch.
   256  func RespondWithPatchedJSON(status int, body string, patch string) http.HandlerFunc {
   257  	patchObject, err := jsonpatch.DecodePatch([]byte(patch))
   258  	Expect(err).ToNot(HaveOccurred())
   259  	patchResult, err := patchObject.Apply([]byte(body))
   260  	Expect(err).ToNot(HaveOccurred())
   261  	return RespondWithJSON(status, string(patchResult))
   262  }
   263  
   264  // RespondWithCookie responds to the request adding a cookie with the given name and value.
   265  func RespondWithCookie(name, value string) http.HandlerFunc {
   266  	return func(w http.ResponseWriter, r *http.Request) {
   267  		http.SetCookie(w, &http.Cookie{
   268  			Name:  name,
   269  			Value: value,
   270  		})
   271  	}
   272  }
   273  
   274  // VerifyCookie checks that the request contains a cookie with the given name and value.
   275  func VerifyCookie(name, value string) http.HandlerFunc {
   276  	return func(w http.ResponseWriter, r *http.Request) {
   277  		cookie, err := r.Cookie(name)
   278  		Expect(err).ToNot(HaveOccurred())
   279  		Expect(cookie).ToNot(BeNil())
   280  		Expect(cookie.Value).To(Equal(value))
   281  	}
   282  }
   283  
   284  // LocalhostCertificate returns a self signed TLS certificate valid for the name `localhost` DNS
   285  // name, for the `127.0.0.1` IPv4 address and for the `::1` IPv6 address.
   286  //
   287  // A similar certificate can be generated with the following command:
   288  //
   289  //	openssl req \
   290  //	-x509 \
   291  //	-newkey rsa:4096 \
   292  //	-nodes \
   293  //	-keyout tls.key \
   294  //	-out tls.crt \
   295  //	-subj '/CN=localhost' \
   296  //	-addext 'subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1' \
   297  //	-days 1
   298  func LocalhostCertificate() tls.Certificate {
   299  	if localhostCertificate == nil {
   300  		key, err := rsa.GenerateKey(rand.Reader, 4096)
   301  		Expect(err).ToNot(HaveOccurred())
   302  		now := time.Now()
   303  		spec := x509.Certificate{
   304  			SerialNumber: big.NewInt(0),
   305  			Subject: pkix.Name{
   306  				CommonName: "localhost",
   307  			},
   308  			DNSNames: []string{
   309  				"localhost",
   310  			},
   311  			IPAddresses: []net.IP{
   312  				net.ParseIP("127.0.0.1"),
   313  				net.ParseIP("::1"),
   314  			},
   315  			NotBefore: now,
   316  			NotAfter:  now.Add(24 * time.Hour),
   317  			KeyUsage:  x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
   318  			ExtKeyUsage: []x509.ExtKeyUsage{
   319  				x509.ExtKeyUsageServerAuth,
   320  			},
   321  		}
   322  		data, err := x509.CreateCertificate(rand.Reader, &spec, &spec, &key.PublicKey, key)
   323  		Expect(err).ToNot(HaveOccurred())
   324  		localhostCertificate = &tls.Certificate{
   325  			Certificate: [][]byte{data},
   326  			PrivateKey:  key,
   327  		}
   328  	}
   329  	return *localhostCertificate
   330  }
   331  
   332  // localhostCertificate contains the TLS certificate returned by the LocalhostCertificate function.
   333  var localhostCertificate *tls.Certificate