feat: new HTTP-01 and TLS-ALPN-01 servers constructors (#2801)

This commit is contained in:
Ludovic Fernandez 2026-01-19 19:11:24 +01:00 committed by GitHub
commit 2f2f587b03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 134 additions and 40 deletions

View file

@ -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)

View file

@ -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.
//

20
challenge/network.go Normal file
View file

@ -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
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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{

View file

@ -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