diff --git a/http/server.go b/http/server.go deleted file mode 100644 index 758e51b..0000000 --- a/http/server.go +++ /dev/null @@ -1,113 +0,0 @@ -package http - -import ( - "context" - "crypto/tls" - "errors" - "net" - "net/http" - "time" - - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" -) - -// SrvOptFunc represents a server option function. -type SrvOptFunc func(*http.Server) - -// WithTLSConfig sets the serve tls config. -func WithTLSConfig(cfg *tls.Config) SrvOptFunc { - return func(srv *http.Server) { - srv.TLSConfig = cfg - } -} - -// WithReadTimeout sets the server read timeout. -func WithReadTimeout(d time.Duration) SrvOptFunc { - return func(srv *http.Server) { - srv.ReadTimeout = d - } -} - -// WithWriteTimeout sets the server write timeout. -func WithWriteTimeout(d time.Duration) SrvOptFunc { - return func(srv *http.Server) { - srv.WriteTimeout = d - } -} - -// WithH2C allows the server to handle h2c connections. -func WithH2C() SrvOptFunc { - return func(srv *http.Server) { - h2s := &http2.Server{ - IdleTimeout: 120 * time.Second, - } - srv.Handler = h2c.NewHandler(srv.Handler, h2s) - } -} - -var testHookServerServe func(net.Listener) - -// Server is a convenience wrapper around the standard -// library HTTP server. -// -// Deprecated: Use server.GenericServer instead of this type. This type is not -// needed and will be removed in a future release. -type Server struct { - srv *http.Server -} - -// NewServer returns a server with the base context ctx. -func NewServer(ctx context.Context, addr string, h http.Handler, opts ...SrvOptFunc) *Server { - srv := &http.Server{ - BaseContext: func(ln net.Listener) context.Context { - if testHookServerServe != nil { - testHookServerServe(ln) - } - return ctx - }, - Addr: addr, - Handler: h, - ReadHeaderTimeout: time.Second, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 120 * time.Second, - } - - for _, opt := range opts { - opt(srv) - } - - return &Server{ - srv: srv, - } -} - -// Serve starts the server in a non-blocking way. -func (s *Server) Serve(errFn func(error)) { - go func() { - if s.srv.TLSConfig != nil { - if err := s.srv.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { - errFn(err) - } - return - } - - if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - errFn(err) - } - }() -} - -// Shutdown attempts to close all server connections. -func (s *Server) Shutdown(timeout time.Duration) error { - stopCtx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - return s.srv.Shutdown(stopCtx) -} - -// Close closes the server. -func (s *Server) Close() error { - return s.srv.Close() -} diff --git a/http/server/server.go b/http/server/server.go index 90361ee..576cdb0 100644 --- a/http/server/server.go +++ b/http/server/server.go @@ -8,6 +8,7 @@ import ( "log" "net" "net/http" + "slices" "sync" "time" @@ -15,8 +16,6 @@ import ( lctx "github.com/hamba/logger/v2/ctx" "github.com/hamba/pkg/v2/http/healthz" "github.com/hamba/statter/v2" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" ) var testHookServerServe func(net.Listener) @@ -128,20 +127,25 @@ func (s *GenericServer[T]) runServer( if err != nil { return nil, nil, err } + var tlsCfg *tls.Config if s.TLSConfig != nil { - ln = tls.NewListener(ln, s.TLSConfig) + tlsCfg = s.TLSConfig.Clone() + if !slices.Contains(tlsCfg.NextProtos, "h2") { + tlsCfg.NextProtos = append([]string{"h2"}, tlsCfg.NextProtos...) + } + ln = tls.NewListener(ln, tlsCfg) } if testHookServerServe != nil { testHookServerServe(ln) } - // If there is no TLS, setup h2c. + protos := &http.Protocols{} + protos.SetHTTP1(true) + protos.SetHTTP2(true) if s.TLSConfig == nil { - h2s := &http2.Server{ - IdleTimeout: s.IdleTimeout, - } - h = h2c.NewHandler(h, h2s) + // If there is no TLS, setup unencrypted HTTP/2 (h2c). + protos.SetUnencryptedHTTP2(true) } srv := &http.Server{ @@ -150,12 +154,13 @@ func (s *GenericServer[T]) runServer( return ctx }, Handler: h, - TLSConfig: s.TLSConfig, + TLSConfig: tlsCfg, ReadHeaderTimeout: withDefault(s.ReadHeaderTimeout, time.Second), ReadTimeout: withDefault(s.ReadTimeout, 10*time.Second), WriteTimeout: withDefault(s.WriteTimeout, 10*time.Second), IdleTimeout: withDefault(s.IdleTimeout, 120*time.Second), ErrorLog: log.New(s.Log.Writer(logger.Error), "", 0), + Protocols: protos, } serverShutdownCh := make(chan struct{}) @@ -197,7 +202,7 @@ func (s *GenericServer[T]) shutdownTimeout() time.Duration { func withDefault[T comparable](val, def T) T { var defT T if val == defT { - return val + return def } - return def + return val } diff --git a/http/server/server_test.go b/http/server/server_test.go index bb2dc95..e090542 100644 --- a/http/server/server_test.go +++ b/http/server/server_test.go @@ -3,6 +3,7 @@ package server import ( "context" "crypto/tls" + "crypto/x509" "io" "net" "net/http" @@ -108,7 +109,7 @@ func TestGenericServer_RunWithTLS(t *testing.T) { go func() { defer close(shutdownCh) - err := srv.Run(ctx) + err = srv.Run(ctx) assert.NoError(t, err) }() @@ -386,11 +387,20 @@ func TestGenericServer_RunHandlesUnexpectedListenerClose(t *testing.T) { func requireDoRequest(t *testing.T, path string) (int, string) { t.Helper() + certPool := x509.NewCertPool() + ok := certPool.AppendCertsFromPEM(localhostCert) + require.True(t, ok, "failed to append cert to pool") + + protos := &http.Protocols{} + protos.SetHTTP2(true) + protos.SetUnencryptedHTTP2(true) + client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, + RootCAs: certPool, }, + Protocols: protos, }, } diff --git a/http/server_test.go b/http/server_test.go deleted file mode 100644 index 3738ead..0000000 --- a/http/server_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package http - -import ( - "context" - "crypto/tls" - "io" - "net" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/net/http2" -) - -func TestServer(t *testing.T) { - lnCh := make(chan net.Listener, 1) - setTestHookServerServe(func(ln net.Listener) { - lnCh <- ln - }) - t.Cleanup(func() { setTestHookServerServe(nil) }) - - var handlerCalled bool - h := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { - handlerCalled = true - }) - - srv := NewServer(context.Background(), "localhost:0", h) - srv.Serve(func(err error) { - require.NoError(t, err) - }) - t.Cleanup(func() { - _ = srv.Close() - }) - - var ln net.Listener - select { - case <-time.After(30 * time.Second): - require.Fail(t, "Timed out waiting for server listener") - case ln = <-lnCh: - } - - url := "http://" + ln.Addr().String() + "/" - statusCode := requireDoRequest(t, url) - - err := srv.Shutdown(time.Second) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, statusCode) - assert.True(t, handlerCalled) -} - -func TestServer_WithTLSConfig(t *testing.T) { - lnCh := make(chan net.Listener, 1) - setTestHookServerServe(func(ln net.Listener) { - lnCh <- ln - }) - t.Cleanup(func() { setTestHookServerServe(nil) }) - - var handlerCalled bool - h := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { - handlerCalled = true - }) - - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - require.NoError(t, err) - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - } - - srv := NewServer(context.Background(), "localhost:0", h, WithTLSConfig(tlsConfig)) - srv.Serve(func(err error) { - require.NoError(t, err) - }) - t.Cleanup(func() { - _ = srv.Close() - }) - - var ln net.Listener - select { - case <-time.After(30 * time.Second): - require.Fail(t, "Timed out waiting for server listener") - case ln = <-lnCh: - } - - url := "https://" + ln.Addr().String() + "/" - statusCode := requireDoRequest(t, url) - - err = srv.Shutdown(time.Second) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, statusCode) - assert.True(t, handlerCalled) -} - -func TestServer_WithH2C(t *testing.T) { - lnCh := make(chan net.Listener, 1) - setTestHookServerServe(func(ln net.Listener) { - lnCh <- ln - }) - t.Cleanup(func() { setTestHookServerServe(nil) }) - - var handlerCalled bool - h := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { - assert.True(t, req.ProtoAtLeast(2, 0)) - - handlerCalled = true - }) - - srv := NewServer(context.Background(), "localhost:0", h, WithH2C()) - srv.Serve(func(err error) { - require.NoError(t, err) - }) - t.Cleanup(func() { - _ = srv.Close() - }) - - var ln net.Listener - select { - case <-time.After(30 * time.Second): - require.Fail(t, "Timed out waiting for server listener") - case ln = <-lnCh: - } - - c := &http.Client{ - Transport: &http2.Transport{ - AllowHTTP: true, - DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, network, addr) - }, - }, - } - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+ln.Addr().String()+"/", nil) - require.NoError(t, err) - - res, err := c.Do(req) - require.NoError(t, err) - - t.Cleanup(func() { - _, _ = io.Copy(io.Discard, res.Body) - _ = res.Body.Close() - }) - - err = srv.Shutdown(time.Second) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.True(t, handlerCalled) -} - -func requireDoRequest(t *testing.T, path string) int { - t.Helper() - - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - }, - } - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil) - require.NoError(t, err) - - resp, err := client.Do(req) - require.NoError(t, err) - defer func() { - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - }() - - return resp.StatusCode -} - -func setTestHookServerServe(fn func(net.Listener)) { - testHookServerServe = fn -} - -var ( - localhostCert = []byte(`-----BEGIN CERTIFICATE----- -MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS -MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw -MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r -bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U -aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P -YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk -POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu -h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE -AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud -DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv -bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI -5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv -cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2 -+tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B -grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK -5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/ -WkBKOclmOV2xlTVuPw== ------END CERTIFICATE-----`) - - localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi -4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS -gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW -URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX -AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy -VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK -x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk -lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL -dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89 -EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq -XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki -6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O -3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s -uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ -Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ -w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo -+bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP -OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA -brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv -m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y -LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN -/3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN -s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ -Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0 -xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/ -ZboOWVe3icTy64BT3OQhmg== ------END RSA PRIVATE KEY-----`) -) diff --git a/ptr/ptr.go b/ptr/ptr.go deleted file mode 100644 index d1582f2..0000000 --- a/ptr/ptr.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package ptr implements functions to take the pointer of values. -package ptr - -// Of returns a pointer to v. -// -// Deprecated: Use the built-in new operator instead of this function. This function is not needed -// and will be removed in a future release. -func Of[T any](v T) *T { - return &v -} diff --git a/ptr/ptr_test.go b/ptr/ptr_test.go deleted file mode 100644 index f863488..0000000 --- a/ptr/ptr_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package ptr_test - -import ( - "testing" - - "github.com/hamba/pkg/v2/ptr" - "github.com/stretchr/testify/assert" -) - -func TestOf(t *testing.T) { - want := true - - got := ptr.Of(want) - - assert.Exactly(t, &want, got) -}