This commit is contained in:
Xueqian Wang 2026-04-17 11:40:28 +02:00 committed by GitHub
commit a2c501cf34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 184 additions and 5 deletions

View File

@ -289,6 +289,7 @@ func (p *OAuthProxy) setupServer(opts *options.Options) error {
BindAddress: opts.Server.BindAddress,
SecureBindAddress: opts.Server.SecureBindAddress,
TLS: opts.Server.TLS,
HTTP2: opts.Server.HTTP2,
}
// Option: AllowQuerySemicolons
@ -306,6 +307,7 @@ func (p *OAuthProxy) setupServer(opts *options.Options) error {
BindAddress: opts.MetricsServer.BindAddress,
SecureBindAddress: opts.MetricsServer.SecureBindAddress,
TLS: opts.MetricsServer.TLS,
HTTP2: opts.MetricsServer.HTTP2,
})
if err != nil {
return fmt.Errorf("could not build metrics server: %v", err)

View File

@ -482,6 +482,7 @@ type LegacyServer struct {
TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
TLSMinVersion string `flag:"tls-min-version" cfg:"tls_min_version"`
TLSCipherSuites []string `flag:"tls-cipher-suite" cfg:"tls_cipher_suites"`
ForceHTTP2 bool `flag:"force-http2" cfg:"force_http2"`
}
func legacyServerFlagset() *pflag.FlagSet {
@ -497,6 +498,7 @@ func legacyServerFlagset() *pflag.FlagSet {
flagSet.String("tls-key-file", "", "path to private key file")
flagSet.String("tls-min-version", "", "minimal TLS version for HTTPS clients (either \"TLS1.2\" or \"TLS1.3\")")
flagSet.StringSlice("tls-cipher-suite", []string{}, "restricts TLS cipher suites to those listed (e.g. TLS_RSA_WITH_RC4_128_SHA) (may be given multiple times)")
flagSet.Bool("force-http2", false, "enable HTTP/2 support (h2c for HTTP, h2 for HTTPS); required for gRPC proxying")
return flagSet
}
@ -653,6 +655,7 @@ func (l LegacyServer) convert() (Server, Server) {
appServer := Server{
BindAddress: l.HTTPAddress,
SecureBindAddress: l.HTTPSAddress,
HTTP2: l.ForceHTTP2,
}
if l.TLSKeyFile != "" || l.TLSCertFile != "" {
appServer.TLS = &TLS{

View File

@ -22,6 +22,12 @@ type Server struct {
// TLS contains the information for loading the certificate and key for the
// secure traffic and further configuration for the TLS server.
TLS *TLS `yaml:"tls,omitempty"`
// HTTP2 enables HTTP/2 support on the server.
// For the insecure (HTTP) server, this enables h2c (HTTP/2 Cleartext) support,
// which is required for gRPC proxying without TLS.
// For the secure (HTTPS) server, this adds "h2" to the TLS ALPN negotiation.
HTTP2 bool `yaml:"http2,omitempty"`
}
// TLS contains the information for loading a TLS certificate and key

View File

@ -12,6 +12,8 @@ import (
"strings"
"time"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"golang.org/x/sync/errgroup"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
@ -39,6 +41,11 @@ type Opts struct {
// TLS is the TLS configuration for the server.
TLS *options.TLS
// HTTP2 enables HTTP/2 support.
// For insecure (HTTP) servers, this enables h2c (HTTP/2 Cleartext).
// For secure (HTTPS) servers, this adds "h2" to TLS ALPN negotiation.
HTTP2 bool
// Let testing infrastructure circumvent parsing file descriptors
fdFiles []*os.File
}
@ -47,6 +54,7 @@ type Opts struct {
func NewServer(opts Opts) (Server, error) {
s := &server{
handler: opts.Handler,
http2: opts.HTTP2,
}
if len(opts.fdFiles) > 0 {
@ -70,6 +78,9 @@ type server struct {
listener net.Listener
tlsListener net.Listener
// http2 enables HTTP/2 support
http2 bool
// ensure activation.Files are called once
fdFiles []*os.File
}
@ -182,10 +193,15 @@ func (s *server) setupTLSListener(opts Opts) error {
return nil
}
nextProtos := []string{"http/1.1"}
if opts.HTTP2 {
nextProtos = []string{"h2", "http/1.1"}
}
config := &tls.Config{
MinVersion: tls.VersionTLS12, // default, override below
MaxVersion: tls.VersionTLS13,
NextProtos: []string{"http/1.1"},
NextProtos: nextProtos,
}
if opts.TLS == nil {
return errors.New("no TLS config provided")
@ -234,7 +250,7 @@ func (s *server) Start(ctx context.Context) error {
if s.listener != nil {
g.Go(func() error {
if err := s.startServer(groupCtx, s.listener); err != nil {
if err := s.startServer(groupCtx, s.listener, s.http2, false); err != nil {
return fmt.Errorf("error starting insecure server: %v", err)
}
return nil
@ -243,7 +259,7 @@ func (s *server) Start(ctx context.Context) error {
if s.tlsListener != nil {
g.Go(func() error {
if err := s.startServer(groupCtx, s.tlsListener); err != nil {
if err := s.startServer(groupCtx, s.tlsListener, s.http2, true); err != nil {
return fmt.Errorf("error starting secure server: %v", err)
}
return nil
@ -256,8 +272,27 @@ func (s *server) Start(ctx context.Context) error {
// startServer creates and starts a new server with the given listener.
// When the given context is cancelled the server will be shutdown.
// If any errors occur, only the first error will be returned.
func (s *server) startServer(ctx context.Context, listener net.Listener) error {
srv := &http.Server{Handler: s.handler, ReadHeaderTimeout: time.Minute}
// If enableHTTP2 is true and isTLS is false, the handler is wrapped
// with h2c to support HTTP/2 Cleartext (required for gRPC without TLS).
// If enableHTTP2 is true and isTLS is true, HTTP/2 is configured on the server.
func (s *server) startServer(ctx context.Context, listener net.Listener, enableHTTP2 bool, isTLS bool) error {
handler := s.handler
if enableHTTP2 && !isTLS {
// Wrap handler with h2c for HTTP/2 Cleartext support
h2s := &http2.Server{}
handler = h2c.NewHandler(handler, h2s)
}
srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Minute}
if enableHTTP2 && isTLS {
// Configure HTTP/2 for TLS server
if err := http2.ConfigureServer(srv, &http2.Server{}); err != nil {
return fmt.Errorf("error configuring HTTP/2: %v", err)
}
}
g, groupCtx := errgroup.WithContext(ctx)
g.Go(func() error {

View File

@ -2,6 +2,7 @@ package proxyhttp
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
@ -9,6 +10,8 @@ import (
"net/http"
"os"
"golang.org/x/net/http2"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@ -1329,6 +1332,136 @@ var _ = Describe("Server", func() {
})
})
Context("HTTP/2 support", func() {
var srv Server
var ctx context.Context
var cancel context.CancelFunc
BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())
})
AfterEach(func() {
cancel()
Eventually(Goroutines).ShouldNot(HaveLeaked())
})
Context("with h2c (HTTP/2 Cleartext) on an insecure server", func() {
var listenAddr string
BeforeEach(func() {
var err error
srv, err = NewServer(Opts{
Handler: handler,
BindAddress: "127.0.0.1:0",
HTTP2: true,
})
Expect(err).ToNot(HaveOccurred())
s, ok := srv.(*server)
Expect(ok).To(BeTrue())
Expect(s.http2).To(BeTrue())
listenAddr = s.listener.Addr().String()
})
It("Serves HTTP/2 cleartext requests", func() {
go func() {
defer GinkgoRecover()
Expect(srv.Start(ctx)).To(Succeed())
}()
// Use an HTTP/2 cleartext client
h2cTransport := &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
}
h2cClient := &http.Client{
Transport: h2cTransport,
}
resp, err := h2cClient.Get(fmt.Sprintf("http://%s/", listenAddr))
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
Expect(resp.ProtoMajor).To(Equal(2))
body, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(body)).To(Equal(hello))
// Close idle connections to prevent goroutine leaks
h2cTransport.CloseIdleConnections()
})
It("Still serves HTTP/1.1 requests", func() {
go func() {
defer GinkgoRecover()
Expect(srv.Start(ctx)).To(Succeed())
}()
resp, err := httpGet(ctx, fmt.Sprintf("http://%s/", listenAddr))
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
body, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(body)).To(Equal(hello))
})
})
Context("with HTTP/2 on a TLS server", func() {
var secureListenAddr string
BeforeEach(func() {
var err error
srv, err = NewServer(Opts{
Handler: handler,
SecureBindAddress: "127.0.0.1:0",
TLS: &options.TLS{
Key: &ipv4KeyDataSource,
Cert: &ipv4CertDataSource,
},
HTTP2: true,
})
Expect(err).ToNot(HaveOccurred())
s, ok := srv.(*server)
Expect(ok).To(BeTrue())
secureListenAddr = s.tlsListener.Addr().String()
})
It("Negotiates HTTP/2 via ALPN", func() {
go func() {
defer GinkgoRecover()
Expect(srv.Start(ctx)).To(Succeed())
}()
// Use an HTTP/2 TLS client
h2Transport := transport.Clone()
h2Transport.ForceAttemptHTTP2 = true
h2Client := &http.Client{Transport: h2Transport}
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/", secureListenAddr), nil)
Expect(err).ToNot(HaveOccurred())
resp, err := h2Client.Do(req)
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
Expect(resp.ProtoMajor).To(Equal(2))
body, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(body)).To(Equal(hello))
// Close idle connections to prevent goroutine leaks
h2Transport.CloseIdleConnections()
})
})
})
Context("getNetworkScheme", func() {
DescribeTable("should return the scheme", func(in, expected string) {
Expect(getNetworkScheme(in)).To(Equal(expected))