github.com/haraldrudell/parl@v0.4.176/phttp/https_test.go (about)

     1  /*
     2  © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package phttp
     7  
     8  import (
     9  	"crypto"
    10  	"crypto/x509"
    11  	"fmt"
    12  	"io/fs"
    13  	"net"
    14  	"net/http"
    15  	"net/netip"
    16  	"os"
    17  	"path/filepath"
    18  	"strconv"
    19  	"testing"
    20  
    21  	"github.com/haraldrudell/parl"
    22  	"github.com/haraldrudell/parl/parlca"
    23  	"github.com/haraldrudell/parl/perrors"
    24  	"github.com/haraldrudell/parl/pnet"
    25  )
    26  
    27  func TestNewHttps(t *testing.T) {
    28  	var ip = "127.0.0.1"
    29  	var nearSocket = netip.MustParseAddrPort(ip + ":0")
    30  	var network = pnet.NetworkTCP
    31  	var addrExp = ip + ":" + strconv.Itoa(int(HttpsPort))
    32  
    33  	var certDER parl.CertificateDer
    34  	var signer crypto.Signer
    35  	var https = NewHttps(nearSocket, network, certDER, signer)
    36  	if https.Network != pnet.NetworkTCP {
    37  		t.Errorf("New bad network %s exp %s", https.Network, network)
    38  	}
    39  	if https.Server.Addr != addrExp {
    40  		t.Errorf("bad Addr %q exp %q", https.Server.Addr, addrExp)
    41  	}
    42  }
    43  
    44  func TestHttpsListen(t *testing.T) {
    45  	var IPv4loopback = net.IPv4(127, 0, 0, 1)
    46  	var ipS = IPv4loopback.String()
    47  	var dir = t.TempDir()
    48  	var nearSocket = netip.MustParseAddrPort(ipS + ":0")
    49  	var network = pnet.NetworkDefault
    50  	// https://
    51  	var protocol = "https://"
    52  	// "/" matches everything
    53  	var URIPattern = "/"
    54  	// ca common name, will default to {host}ca
    55  	var canonicalName = ""
    56  	// /usr/local/opt/openssl/bin/openssl x509 -in ca.der -inform der -noout -text
    57  	var caCertFilename = filepath.Join(dir, "ca.der")
    58  	var ownerRW = fs.FileMode(0o600)
    59  	// /usr/local/opt/openssl/bin/openssl pkey -in key.der -inform der -text -noout
    60  	var keyFilename = filepath.Join(dir, "key.der")
    61  	// /usr/local/opt/openssl/bin/openssl x509 -in cert.der -inform der -noout -text
    62  	var certFilename = filepath.Join(dir, "cert.der")
    63  	// var commonName = "" // server certificate, will default to {host}
    64  	// var subject = "subject"
    65  
    66  	var err error
    67  	var caCert parl.CertificateAuthority
    68  	var caX509 *x509.Certificate
    69  	var serverKey parl.PrivateKey
    70  	// private key for running the server
    71  	var serverSigner parl.PrivateKey
    72  	// public key for creating server certificate
    73  	var serverPublic crypto.PublicKey
    74  	var keyDER parl.PrivateKeyDer
    75  	var template x509.Certificate
    76  	var certDER parl.CertificateDer
    77  	var handler *sHandler
    78  	var goResult = parl.NewGoResult()
    79  	var near, respS string
    80  	var resp *http.Response
    81  	var statusCode int
    82  
    83  	t.Log("Creating self-signed certificate authority")
    84  	if caCert, err = parlca.NewSelfSigned(canonicalName, x509.RSA); err != nil {
    85  		t.Fatalf("parlca.NewSelfSigned %s %s", x509.RSA, perrors.Short(err))
    86  	}
    87  	writeFile(caCertFilename, caCert.DER(), ownerRW, t.Logf)
    88  	caX509, err = caCert.Check()
    89  	if err != nil {
    90  		t.Fatalf("caCert,Check: %s", perrors.Short(err))
    91  	}
    92  
    93  	t.Log("Creating server private key")
    94  	serverKey, err = parlca.NewEd25519()
    95  	if err != nil {
    96  		t.Fatal(perrors.Errorf("server parlca.NewEd25519: '%w'", err))
    97  	}
    98  	serverSigner = serverKey             // private key for running the server
    99  	serverPublic = serverSigner.Public() // public key for creating server certificate
   100  	if keyDER, err = serverKey.DER(); err != nil {
   101  		t.Fatal(err)
   102  	}
   103  	writeFile(keyFilename, keyDER, ownerRW, t.Logf)
   104  
   105  	t.Log("Creating server certificate")
   106  	template = x509.Certificate{
   107  		IPAddresses: []net.IP{IPv4loopback, net.IPv6loopback},
   108  	}
   109  	parlca.EnsureServer(&template)
   110  	if certDER, err = caCert.Sign(&template, serverPublic); err != nil {
   111  		t.Errorf("signing server certificate: %+v", err)
   112  		return
   113  	}
   114  	writeFile(certFilename, certDER, ownerRW, t.Logf)
   115  
   116  	// Listen() TLS()
   117  	var https *Https = NewHttps(nearSocket, network, certDER, serverSigner)
   118  
   119  	handler = newShandler()
   120  	https.HandleFunc(URIPattern, handler.Handle)
   121  	defer https.Shutdown()
   122  
   123  	t.Log("invoking Listen")
   124  	go errChListener(https.Listen(), goResult)
   125  
   126  	t.Log("waiting for ListenAwaitable")
   127  	<-https.ListenAwaitable.Ch()
   128  	if !https.Near.IsValid() {
   129  		t.Fatalf("FATAL: https.Near invalid")
   130  	}
   131  	near = https.Near.String()
   132  	t.Logf("Near: %s", near)
   133  
   134  	t.Log("issuing http.GET")
   135  	resp, err = pnet.Get(protocol+near, pnet.NewTLSConfig(caX509), nil)
   136  	// macOS does accept self-signed certificate
   137  	//resp, err = http.Get(protocol + near)
   138  	if resp != nil {
   139  		statusCode = resp.StatusCode
   140  		respS = fmt.Sprintf("status code: %d", statusCode)
   141  	} else {
   142  		respS = "resp nil"
   143  	}
   144  
   145  	t.Logf("%s err: %s", respS, perrors.Short(err))
   146  	if err != nil {
   147  		t.Errorf("http.Get err %s", perrors.Short(err))
   148  	}
   149  	// status code should be 204
   150  	if resp != nil {
   151  		if resp.StatusCode != http.StatusNoContent {
   152  			t.Errorf("http.Get status code %d exp %d", resp.StatusCode, http.StatusNoContent)
   153  		}
   154  		if e := resp.Body.Close(); e != nil {
   155  			panic(e)
   156  		}
   157  	}
   158  
   159  	// handle count should be 1
   160  	if c := int(handler.Rqs.Load()); c != 1 {
   161  		t.Errorf("bad handle count: %d exp 1", c)
   162  	}
   163  
   164  	t.Logf("Shutting down server")
   165  	https.Shutdown()
   166  
   167  	// wait for error reader to exit
   168  	goResult.ReceiveError(nil)
   169  
   170  	if !https.ErrCh.IsClosed() {
   171  		t.Error("ErrCh not closed")
   172  	}
   173  	if !https.EndListenAwaitable.IsClosed() {
   174  		t.Error("EndListenAwaitable not closed")
   175  	}
   176  }
   177  
   178  // writeFile writes byts to file panic on error
   179  func writeFile(filename string, byts []byte, mode fs.FileMode, logf parl.PrintfFunc) {
   180  	logf("Writing: %s", filename)
   181  	if err := os.WriteFile(filename, byts, mode); err != nil {
   182  		panic(perrors.Errorf("os.WriteFile: '%w'", err))
   183  	}
   184  }