github.com/haraldrudell/parl@v0.4.176/phttp/https.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/tls"
    11  	"io/fs"
    12  	"net"
    13  	"net/http"
    14  	"net/netip"
    15  
    16  	"github.com/haraldrudell/parl"
    17  	"github.com/haraldrudell/parl/perrors"
    18  	"github.com/haraldrudell/parl/pnet"
    19  	"github.com/haraldrudell/parl/pstrings"
    20  )
    21  
    22  const (
    23  	// default port for https: 443, or address for localhost IPv4 or IPv6 port 443
    24  	HttpsPort uint16 = 443
    25  )
    26  
    27  // Http is an http server instance
    28  //   - based on [http.Server]
    29  //   - has listener thread
    30  //   - all errors sent on channel
    31  //   - idempotent deferrable panic-free shutdown
    32  //   - awaitable, observable
    33  type Https struct {
    34  	Http
    35  	Cert    parl.CertificateDer
    36  	Private crypto.Signer
    37  }
    38  
    39  // NewHttp creates http server for default “localhost:443”
    40  //   - if nearSocket.Addr is invalid, all interfaces for IPv6 if allowed, IPv4 otherwise is used
    41  //   - if nearSocket.Port is zero:
    42  //   - — if network is NetworkDefault: ephemeral port
    43  //   - — otherwise port 443 “:https” is used
    44  //   - for NetworkDefault, NetworkTCP is used
    45  //   - panic for bad Network
    46  //
    47  // Usage:
    48  //
    49  //	var s = NewHttps(netip.AdddrPort{}, pnet.NetworkTCP)
    50  //	s.HandleFunc("/", myHandler)
    51  //	defer s.Shutdown()
    52  //	for err := range s.Listen() {
    53  func NewHttps(nearSocket netip.AddrPort, network pnet.Network, certDER parl.CertificateDer, signer crypto.Signer) (hp *Https) {
    54  	if nearSocket.Port() == 0 && network != pnet.NetworkDefault {
    55  		nearSocket = netip.AddrPortFrom(nearSocket.Addr(), HttpsPort)
    56  	}
    57  	return &Https{
    58  		Http:    *NewHttp(nearSocket, network),
    59  		Cert:    certDER,
    60  		Private: signer,
    61  	}
    62  }
    63  
    64  // Listen initiates listening and returns the error channel
    65  //   - can only be invoked once or panic
    66  //   - errCh closes on server shutdown
    67  //   - non-blocking, all errors are sent on the error channel
    68  func (s *Https) Listen() (errCh <-chan error) {
    69  	if !s.NoListen.CompareAndSwap(false, true) {
    70  		panic(perrors.NewPF("multiple invocations"))
    71  	}
    72  	errCh = s.ErrCh.Ch()
    73  	// listen is deferred so just launch the thread
    74  	go s.httpsListenerThread()
    75  	return
    76  }
    77  
    78  const (
    79  	httpsAddr = ":https"
    80  	http11    = "http/1.1"
    81  )
    82  
    83  // httpsListenerThread is gorouitn starting listen and
    84  // waiting for server to terminate
    85  func (s *Https) httpsListenerThread() {
    86  	defer s.EndListenAwaitable.Close()
    87  	var err error
    88  	defer parl.Recover(func() parl.DA { return parl.A() }, &err, s.SendErr)
    89  
    90  	// get near tls socket listener
    91  	// *tls.listener — need this or it’s file certificates
    92  	var tlsListener net.Listener
    93  	if tlsListener, err = s.TLS(); err != nil {
    94  		return
    95  	}
    96  	defer s.maybeClose(&tlsListener, &err)
    97  
    98  	// set Near socket address
    99  	if s.Near, err = pnet.AddrPortFromAddr(tlsListener.Addr()); err != nil {
   100  		return
   101  	}
   102  	s.ListenAwaitable.Close()
   103  
   104  	// blocks here until Shutdown or Close
   105  	err = s.Server.Serve(tlsListener)
   106  	tlsListener = nil
   107  
   108  	// on regular close, http.ErrServerClosed
   109  	if err == http.ErrServerClosed {
   110  		err = nil // ignore error
   111  		return    // successful return
   112  	}
   113  	err = perrors.Errorf("srv.ServeTLS: ‘%w’", err)
   114  }
   115  
   116  func (s *Https) TLS() (tlsListener net.Listener, err error) {
   117  	var httpServer = &s.Server
   118  
   119  	// make ServeTLS execute srv.setupHTTP2_ServeTLS go 1.16.6
   120  	badPath := "%"
   121  	if err = httpServer.ServeTLS(nil, badPath, badPath); err == nil {
   122  		err = perrors.New("missing error from srv.ServeTLS")
   123  		return
   124  	}
   125  
   126  	// ignore the error if expected badPath
   127  	if pathError, ok := err.(*fs.PathError); ok {
   128  		if pathError.Path == badPath {
   129  			err = nil
   130  		}
   131  	}
   132  
   133  	// failure on other errors
   134  	if err != nil {
   135  		err = perrors.Errorf("srv.ServeTLS: '%w'", err)
   136  		return
   137  	}
   138  
   139  	// get *net.TCPListener from srv.ListenAndServeTLS
   140  
   141  	// tcp listener from pnet
   142  	var listener net.Listener
   143  	if listener, err = pnet.Listen(s.Network, s.Server.Addr, &s.Cancel); err != nil {
   144  		return
   145  	}
   146  	var _ = net.Listen
   147  
   148  	// create a TLS listener from srv.ServeTLS
   149  	var tlsConfig *tls.Config
   150  	if httpServer.TLSConfig == nil {
   151  		tlsConfig = &tls.Config{}
   152  	} else {
   153  		tlsConfig = httpServer.TLSConfig.Clone()
   154  	}
   155  	if !pstrings.StrSliceContains(tlsConfig.NextProtos, http11) {
   156  		tlsConfig.NextProtos = append(tlsConfig.NextProtos, http11)
   157  	}
   158  	tlsConfig.Certificates = make([]tls.Certificate, 1)
   159  	tlsCertificate := &tlsConfig.Certificates[0]
   160  	tlsCertificate.Certificate = append(tlsCertificate.Certificate, s.Cert) // certificate not from file system
   161  	tlsCertificate.PrivateKey = s.Private                                   // private key not from file system
   162  	tlsListener = tls.NewListener(listener, tlsConfig)
   163  
   164  	return
   165  }
   166  
   167  // closes listener if non-nil
   168  func (s *Https) maybeClose(listenerp *net.Listener, errp *error) {
   169  	var listener = *listenerp
   170  	if listener == nil {
   171  		return
   172  	}
   173  	parl.Close(listener, errp)
   174  }