Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions acme/api/handler.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package api

import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"time"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"net/url"
"time"

"github.com/go-chi/chi/v5"

Expand Down Expand Up @@ -120,17 +121,44 @@ func Route(r api.Router) {
}

func route(r api.Router, middleware func(next nextHTTP) nextHTTP) {
commonMiddleware := func(next nextHTTP) nextHTTP {
handler := func(w http.ResponseWriter, r *http.Request) {
// Linker middleware gets the provisioner and current url from the
// request and sets them in the context.
linker := acme.MustLinkerFromContext(r.Context())
linker.Middleware(http.HandlerFunc(checkPrerequisites(next))).ServeHTTP(w, r)
}
if middleware != nil {
handler = middleware(handler)
}
return handler
// providerClient injecte un client ACME configuré selon le provisioner courant (proxy/DNS)
providerClient := func(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Le provisioner est fixé par linker.Middleware en amont
if acmeProv, err := acmeProvisionerFromContext(ctx); err == nil && acmeProv != nil {
var opts []acme.ClientOption
if acmeProv.ProxyURL != "" {
opts = append(opts, acme.WithProxyURL(acmeProv.ProxyURL))
}
if acmeProv.DisableProxy {
// Désactive totalement le proxy (ignore les variables d'environnement)
opts = append(opts, acme.WithProxyFunc(func(req *http.Request) (*url.URL, error) { return nil, nil }))
}
if acmeProv.DNS != "" {
opts = append(opts, acme.WithDNS(acmeProv.DNS))
}
if len(opts) > 0 {
c := acme.NewClient(opts...)
ctx = acme.NewClientContext(ctx, c)
}
}
next(w, r.WithContext(ctx))
}
}

commonMiddleware := func(next nextHTTP) nextHTTP {
handler := func(w http.ResponseWriter, r *http.Request) {
// Linker middleware gets the provisioner and current url from the
// request and sets them in the context.
linker := acme.MustLinkerFromContext(r.Context())
// Après que linker.Middleware ait résolu le provisioner, injecter un client ACME spécifique au provider
linker.Middleware(http.HandlerFunc(providerClient(checkPrerequisites(next)))).ServeHTTP(w, r)
}
if middleware != nil {
handler = middleware(handler)
}
return handler
}
validatingMiddleware := func(next nextHTTP) nextHTTP {
return commonMiddleware(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next))))))
Expand Down
128 changes: 103 additions & 25 deletions acme/client.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package acme

import (
"context"
"crypto/tls"
"net"
"net/http"
"time"
"context"
"crypto/tls"
"net"
"net/http"
"net/url"
"time"
)

// Client is the interface used to verify ACME challenges.
Expand Down Expand Up @@ -45,35 +46,112 @@ func MustClientFromContext(ctx context.Context) Client {
}

type client struct {
http *http.Client
dialer *net.Dialer
http *http.Client
dialer *net.Dialer
// resolver is used for DNS lookups; defaults to net.DefaultResolver
resolver *net.Resolver
}

// NewClient returns an implementation of Client for verifying ACME challenges.
func NewClient() Client {
return &client{
http: &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
//nolint:gosec // used on tls-alpn-01 challenge
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
},
},
},
dialer: &net.Dialer{
Timeout: 30 * time.Second,
},
}
// ClientOption configures the ACME client.
type ClientOption func(*client)

// WithProxyURL configures the HTTP(S) proxy to use for ACME HTTP requests.
// Example: WithProxyURL("http://proxy.local:3128") or WithProxyURL("socks5://...").
func WithProxyURL(proxyURL string) ClientOption {
return func(c *client) {
if tr, ok := c.http.Transport.(*http.Transport); ok {
if u, err := url.Parse(proxyURL); err == nil {
tr.Proxy = http.ProxyURL(u)
}
}
}
}

// WithProxyFunc sets a custom proxy selection function, overriding environment variables.
func WithProxyFunc(fn func(*http.Request) (*url.URL, error)) ClientOption {
return func(c *client) {
if tr, ok := c.http.Transport.(*http.Transport); ok {
tr.Proxy = fn
}
}
}

// WithResolver sets a custom DNS resolver to be used for both TXT lookups and dialing.
func WithResolver(r *net.Resolver) ClientOption {
return func(c *client) {
c.resolver = r
c.dialer.Resolver = r
}
}

// WithDNS configures the client to use a specific DNS server for all lookups and dialing.
// The address should be in host:port form, e.g. "8.8.8.8:53".
func WithDNS(addr string) ClientOption {
return func(c *client) {
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := &net.Dialer{Timeout: 5 * time.Second}
return d.DialContext(ctx, network, addr)
},
}
c.resolver = r
c.dialer.Resolver = r
}
}

// NewClientWithOptions returns an implementation of Client for verifying ACME challenges.
// It accepts optional ClientOptions to override proxy and DNS resolver behavior.
func NewClientWithOptions(opts ...ClientOption) Client {
d := &net.Dialer{Timeout: 30 * time.Second}
// Default transport uses environment proxy and our dialer so that custom resolver applies to HTTP too.
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: d.DialContext,
TLSClientConfig: &tls.Config{
//nolint:gosec // used on tls-alpn-01 challenge
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
},
}
c := &client{
http: &http.Client{
Timeout: 30 * time.Second,
Transport: tr,
},
dialer: d,
resolver: net.DefaultResolver,
}

// Apply options
for _, opt := range opts {
opt(c)
}
// Ensure transport dialer is bound (in case options replaced dialer.resolver)
if tr2, ok := c.http.Transport.(*http.Transport); ok {
tr2.DialContext = c.dialer.DialContext
}
return c
}

// NewClient returns an implementation of Client with default settings
// (proxy from environment and system DNS resolver). For custom configuration
// use NewClientWithOptions.
func NewClient(opts ...ClientOption) Client { // keep signature source-compatible for callers without options
return NewClientWithOptions(opts...)
}

func (c *client) Get(url string) (*http.Response, error) {
return c.http.Get(url)
}

func (c *client) LookupTxt(name string) ([]string, error) {
return net.LookupTXT(name)
// Prefer custom resolver with a bounded timeout
if c.resolver != nil {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return c.resolver.LookupTXT(ctx, name)
}
return net.LookupTXT(name)
}

func (c *client) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) {
Expand Down
26 changes: 18 additions & 8 deletions authority/provisioner/acme.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,24 @@ func (f ACMEAttestationFormat) Validate() error {
// ACME is the acme provisioner type, an entity that can authorize the ACME
// provisioning flow.
type ACME struct {
*base
ID string `json:"-"`
Type string `json:"type"`
Name string `json:"name"`
ForceCN bool `json:"forceCN,omitempty"`
// TermsOfService contains a URL pointing to the ACME server's
// terms of service. Defaults to empty.
TermsOfService string `json:"termsOfService,omitempty"`
*base
ID string `json:"-"`
Type string `json:"type"`
Name string `json:"name"`
ForceCN bool `json:"forceCN,omitempty"`
// ProxyURL enables configuring a custom HTTP(S) proxy for outbound
// ACME validation requests performed by the server (e.g., http-01 fetches).
// If empty, the default proxy from the environment is used.
ProxyURL string `json:"proxyURL,omitempty"`
// DisableProxy disables usage of any proxy (including environment variables)
// for outbound ACME validation requests.
DisableProxy bool `json:"disableProxy,omitempty"`
// DNS allows forcing a specific DNS resolver in the form "host:port"
// (e.g., "8.8.8.8:53") for DNS queries executed during ACME challenges.
DNS string `json:"dns,omitempty"`
// TermsOfService contains a URL pointing to the ACME server's
// terms of service. Defaults to empty.
TermsOfService string `json:"termsOfService,omitempty"`
// Website contains an URL pointing to more information about
// the ACME server. Defaults to empty.
Website string `json:"website,omitempty"`
Expand Down