From 09908bab4d9b198e81cb2c978d0998abafdfe2ee Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 8 Feb 2024 01:23:02 +0100 Subject: [PATCH] feat: new HTTP-01 and TLS-ALPN-01 servers constructors --- challenge/http01/http_challenge.go | 4 -- challenge/http01/http_challenge_server.go | 53 +++++++++++++++---- challenge/network.go | 20 +++++++ challenge/tlsalpn01/tls_alpn_challenge.go | 4 -- .../tlsalpn01/tls_alpn_challenge_server.go | 53 +++++++++++++------ .../tlsalpn01/tls_alpn_challenge_test.go | 6 +-- cmd/setup_challenges.go | 34 ++++++++++-- 7 files changed, 134 insertions(+), 40 deletions(-) create mode 100644 challenge/network.go diff --git a/challenge/http01/http_challenge.go b/challenge/http01/http_challenge.go index 83e494a9f..57424490c 100644 --- a/challenge/http01/http_challenge.go +++ b/challenge/http01/http_challenge.go @@ -52,10 +52,6 @@ func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Prov return chlg } -func (c *Challenge) SetProvider(provider challenge.Provider) { - c.provider = provider -} - func (c *Challenge) Solve(ctx context.Context, authz acme.Authorization) error { domain := challenge.GetTargetedDomain(authz) log.Info("acme: Trying to solve HTTP-01.", "domain", domain) diff --git a/challenge/http01/http_challenge_server.go b/challenge/http01/http_challenge_server.go index 2c589125a..7960171ed 100644 --- a/challenge/http01/http_challenge_server.go +++ b/challenge/http01/http_challenge_server.go @@ -9,15 +9,25 @@ import ( "os" "strings" + "github.com/go-acme/lego/v5/challenge" "github.com/go-acme/lego/v5/log" ) +var _ challenge.Provider = (*ProviderServer)(nil) + +type Options struct { + Network string + NetworkStack challenge.NetworkStack + Address string + SocketMode fs.FileMode +} + // ProviderServer implements ChallengeProvider for `http-01` challenge. // It may be instantiated without using the NewProviderServer function if // you want only to use the default values. type ProviderServer struct { - address string network string // must be valid argument to net.Listen + address string socketMode fs.FileMode @@ -26,19 +36,42 @@ type ProviderServer struct { listener net.Listener } +// NewProviderServerWithOptions creates a new ProviderServer. +func NewProviderServerWithOptions(opts Options) *ProviderServer { + if opts.Network == "" { + opts.Network = "tcp" + } + + return &ProviderServer{ + network: opts.NetworkStack.Network(opts.Network), + address: opts.Address, + socketMode: opts.SocketMode, + matcher: &hostMatcher{}, + } +} + // NewProviderServer creates a new ProviderServer on the selected interface and port. -// Setting iface and / or port to an empty string will make the server fall back to +// Setting host and / or port to an empty string will make the server fall back to // the "any" interface and port 80 respectively. -func NewProviderServer(iface, port string) *ProviderServer { +func NewProviderServer(host, port string) *ProviderServer { if port == "" { + // Fallback to port 80 if the port was not provided. port = "80" } - return &ProviderServer{network: "tcp", address: net.JoinHostPort(iface, port), matcher: &hostMatcher{}} + return NewProviderServerWithOptions(Options{ + Network: "tcp", + Address: net.JoinHostPort(host, port), + }) } -func NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer { - return &ProviderServer{network: "unix", address: socketPath, socketMode: mode, matcher: &hostMatcher{}} +// NewUnixProviderServer creates a new ProviderServer. +func NewUnixProviderServer(socketPath string, socketMode fs.FileMode) *ProviderServer { + return NewProviderServerWithOptions(Options{ + Network: "unix", + Address: socketPath, + SocketMode: socketMode, + }) } // Present starts a web server and makes the token available at `ChallengePath(token)` for web requests. @@ -63,10 +96,6 @@ func (s *ProviderServer) Present(domain, token, keyAuth string) error { return nil } -func (s *ProviderServer) GetAddress() string { - return s.address -} - // CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`. func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error { if s.listener == nil { @@ -80,6 +109,10 @@ func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error { return nil } +func (s *ProviderServer) GetAddress() string { + return s.address +} + // SetProxyHeader changes the validation of incoming requests. // By default, s matches the "Host" header value to the domain name. // diff --git a/challenge/network.go b/challenge/network.go new file mode 100644 index 000000000..7556267e5 --- /dev/null +++ b/challenge/network.go @@ -0,0 +1,20 @@ +package challenge + +type NetworkStack int + +const ( + dualStack NetworkStack = iota + ipv4only + ipv6only +) + +func (s NetworkStack) Network(proto string) string { + switch s { + case ipv4only: + return proto + "4" + case ipv6only: + return proto + "6" + default: + return proto + } +} diff --git a/challenge/tlsalpn01/tls_alpn_challenge.go b/challenge/tlsalpn01/tls_alpn_challenge.go index 2469b5898..7fa133f2e 100644 --- a/challenge/tlsalpn01/tls_alpn_challenge.go +++ b/challenge/tlsalpn01/tls_alpn_challenge.go @@ -57,10 +57,6 @@ func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Prov return chlg } -func (c *Challenge) SetProvider(provider challenge.Provider) { - c.provider = provider -} - // Solve manages the provider to validate and solve the challenge. func (c *Challenge) Solve(ctx context.Context, authz acme.Authorization) error { domain := authz.Identifier.Value diff --git a/challenge/tlsalpn01/tls_alpn_challenge_server.go b/challenge/tlsalpn01/tls_alpn_challenge_server.go index b242e8ca8..4fdf2c810 100644 --- a/challenge/tlsalpn01/tls_alpn_challenge_server.go +++ b/challenge/tlsalpn01/tls_alpn_challenge_server.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "github.com/go-acme/lego/v5/challenge" "github.com/go-acme/lego/v5/log" ) @@ -20,34 +21,52 @@ const ( defaultTLSPort = "443" ) +var _ challenge.Provider = (*ProviderServer)(nil) + +type Options struct { + Network string + NetworkStack challenge.NetworkStack + Host string + Port string +} + // ProviderServer implements ChallengeProvider for `TLS-ALPN-01` challenge. // It may be instantiated without using the NewProviderServer // if you want only to use the default values. type ProviderServer struct { - iface string - port string + network string + address string + listener net.Listener } -// NewProviderServer creates a new ProviderServer on the selected interface and port. -// Setting iface and / or port to an empty string will make the server fall back to -// the "any" interface and port 443 respectively. -func NewProviderServer(iface, port string) *ProviderServer { - return &ProviderServer{iface: iface, port: port} +// NewProviderServerWithOptions creates a new ProviderServer. +func NewProviderServerWithOptions(opts Options) *ProviderServer { + if opts.Port == "" { + // Fallback to port 443 if the port was not provided. + opts.Port = defaultTLSPort + } + + if opts.Network == "" { + opts.Network = "tcp" + } + + return &ProviderServer{ + network: opts.NetworkStack.Network(opts.Network), + address: net.JoinHostPort(opts.Host, opts.Port), + } } -func (s *ProviderServer) GetAddress() string { - return net.JoinHostPort(s.iface, s.port) +// NewProviderServer creates a new ProviderServer on the selected interface and port. +// Setting host and / or port to an empty string will make the server fall back to +// the "any" interface and port 443 respectively. +func NewProviderServer(host, port string) *ProviderServer { + return NewProviderServerWithOptions(Options{Host: host, Port: port}) } // Present generates a certificate with an SHA-256 digest of the keyAuth provided // as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN spec. func (s *ProviderServer) Present(domain, token, keyAuth string) error { - if s.port == "" { - // Fallback to port 443 if the port was not provided. - s.port = defaultTLSPort - } - // Generate the challenge certificate using the provided keyAuth and domain. cert, err := ChallengeCert(domain, keyAuth) if err != nil { @@ -65,7 +84,7 @@ func (s *ProviderServer) Present(domain, token, keyAuth string) error { tlsConf.NextProtos = []string{ACMETLS1Protocol} // Create the listener with the created tls.Config. - s.listener, err = tls.Listen("tcp", s.GetAddress(), tlsConf) + s.listener, err = tls.Listen(s.network, s.GetAddress(), tlsConf) if err != nil { return fmt.Errorf("could not start HTTPS server for challenge: %w", err) } @@ -94,3 +113,7 @@ func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error { return nil } + +func (s *ProviderServer) GetAddress() string { + return s.address +} diff --git a/challenge/tlsalpn01/tls_alpn_challenge_test.go b/challenge/tlsalpn01/tls_alpn_challenge_test.go index 936603ff4..37770be99 100644 --- a/challenge/tlsalpn01/tls_alpn_challenge_test.go +++ b/challenge/tlsalpn01/tls_alpn_challenge_test.go @@ -76,7 +76,7 @@ func TestChallenge(t *testing.T) { solver := NewChallenge( core, mockValidate, - &ProviderServer{port: port}, + NewProviderServerWithOptions(Options{Host: domain, Port: port}), ) authz := acme.Authorization{ @@ -105,7 +105,7 @@ func TestChallengeInvalidPort(t *testing.T) { solver := NewChallenge( core, func(_ context.Context, _ *api.Core, _ string, _ acme.Challenge) error { return nil }, - &ProviderServer{port: "123456"}, + NewProviderServerWithOptions(Options{Host: "127.0.0.1", Port: "123456"}), ) authz := acme.Authorization{ @@ -183,7 +183,7 @@ func TestChallengeIPaddress(t *testing.T) { solver := NewChallenge( core, mockValidate, - &ProviderServer{port: port}, + NewProviderServerWithOptions(Options{Host: domain, Port: port}), ) authz := acme.Authorization{ diff --git a/cmd/setup_challenges.go b/cmd/setup_challenges.go index 6f7fce708..a82a2cf48 100644 --- a/cmd/setup_challenges.go +++ b/cmd/setup_challenges.go @@ -57,6 +57,7 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { } return ps + case ctx.IsSet(flgHTTPMemcachedHost): ps, err := memcached.NewMemcachedProvider(ctx.StringSlice(flgHTTPMemcachedHost)) if err != nil { @@ -65,6 +66,7 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { } return ps + case ctx.IsSet(flgHTTPS3Bucket): ps, err := s3.NewHTTPProvider(ctx.String(flgHTTPS3Bucket)) if err != nil { @@ -73,8 +75,10 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { } return ps + case ctx.IsSet(flgHTTPPort): iface := ctx.String(flgHTTPPort) + if !strings.Contains(iface, ":") { log.Fatal( fmt.Sprintf("The --%s switch only accepts interface:port or :port for its argument.", flgHTTPPort), @@ -87,19 +91,31 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { log.Fatal("Could not split host and port.", "iface", iface, "error", err) } - srv := http01.NewProviderServer(host, port) + srv := http01.NewProviderServerWithOptions(http01.Options{ + // TODO(ldez): set network stack + Network: "tcp", + Address: net.JoinHostPort(host, port), + }) + if header := ctx.String(flgHTTPProxyHeader); header != "" { srv.SetProxyHeader(header) } return srv + case ctx.Bool(flgHTTP): - srv := http01.NewProviderServer("", "") + srv := http01.NewProviderServerWithOptions(http01.Options{ + // TODO(ldez): set network stack + Network: "tcp", + Address: net.JoinHostPort("", ":80"), + }) + if header := ctx.String(flgHTTPProxyHeader); header != "" { srv.SetProxyHeader(header) } return srv + default: log.Fatal("Invalid HTTP challenge options.") return nil @@ -119,9 +135,19 @@ func setupTLSProvider(ctx *cli.Context) challenge.Provider { log.Fatal("Could not split host and port.", "iface", iface, "error", err) } - return tlsalpn01.NewProviderServer(host, port) + return tlsalpn01.NewProviderServerWithOptions(tlsalpn01.Options{ + // TODO(ldez): set network stack + Network: "tcp", + Host: host, + Port: port, + }) + case ctx.Bool(flgTLS): - return tlsalpn01.NewProviderServer("", "") + return tlsalpn01.NewProviderServerWithOptions(tlsalpn01.Options{ + // TODO(ldez): set network stack + Network: "tcp", + }) + default: log.Fatal("Invalid HTTP challenge options.") return nil