Skip to content

Commit 38528bb

Browse files
u038472mcarbonneaux
authored andcommitted
add proxy support in acme providers
add dns resolver support in acme providers
1 parent 930e8fc commit 38528bb

File tree

3 files changed

+167
-51
lines changed

3 files changed

+167
-51
lines changed

acme/api/handler.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package api
22

33
import (
4-
"context"
5-
"crypto/x509"
6-
"encoding/json"
7-
"encoding/pem"
8-
"fmt"
9-
"net/http"
10-
"time"
4+
"context"
5+
"crypto/x509"
6+
"encoding/json"
7+
"encoding/pem"
8+
"fmt"
9+
"net/http"
10+
"net/url"
11+
"time"
1112

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

@@ -120,17 +121,44 @@ func Route(r api.Router) {
120121
}
121122

122123
func route(r api.Router, middleware func(next nextHTTP) nextHTTP) {
123-
commonMiddleware := func(next nextHTTP) nextHTTP {
124-
handler := func(w http.ResponseWriter, r *http.Request) {
125-
// Linker middleware gets the provisioner and current url from the
126-
// request and sets them in the context.
127-
linker := acme.MustLinkerFromContext(r.Context())
128-
linker.Middleware(http.HandlerFunc(checkPrerequisites(next))).ServeHTTP(w, r)
129-
}
130-
if middleware != nil {
131-
handler = middleware(handler)
132-
}
133-
return handler
124+
// providerClient injecte un client ACME configuré selon le provisioner courant (proxy/DNS)
125+
providerClient := func(next nextHTTP) nextHTTP {
126+
return func(w http.ResponseWriter, r *http.Request) {
127+
ctx := r.Context()
128+
// Le provisioner est fixé par linker.Middleware en amont
129+
if acmeProv, err := acmeProvisionerFromContext(ctx); err == nil && acmeProv != nil {
130+
var opts []acme.ClientOption
131+
if acmeProv.ProxyURL != "" {
132+
opts = append(opts, acme.WithProxyURL(acmeProv.ProxyURL))
133+
}
134+
if acmeProv.DisableProxy {
135+
// Désactive totalement le proxy (ignore les variables d'environnement)
136+
opts = append(opts, acme.WithProxyFunc(func(req *http.Request) (*url.URL, error) { return nil, nil }))
137+
}
138+
if acmeProv.DNS != "" {
139+
opts = append(opts, acme.WithDNS(acmeProv.DNS))
140+
}
141+
if len(opts) > 0 {
142+
c := acme.NewClient(opts...)
143+
ctx = acme.NewClientContext(ctx, c)
144+
}
145+
}
146+
next(w, r.WithContext(ctx))
147+
}
148+
}
149+
150+
commonMiddleware := func(next nextHTTP) nextHTTP {
151+
handler := func(w http.ResponseWriter, r *http.Request) {
152+
// Linker middleware gets the provisioner and current url from the
153+
// request and sets them in the context.
154+
linker := acme.MustLinkerFromContext(r.Context())
155+
// Après que linker.Middleware ait résolu le provisioner, injecter un client ACME spécifique au provider
156+
linker.Middleware(http.HandlerFunc(providerClient(checkPrerequisites(next)))).ServeHTTP(w, r)
157+
}
158+
if middleware != nil {
159+
handler = middleware(handler)
160+
}
161+
return handler
134162
}
135163
validatingMiddleware := func(next nextHTTP) nextHTTP {
136164
return commonMiddleware(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next))))))

acme/client.go

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package acme
22

33
import (
4-
"context"
5-
"crypto/tls"
6-
"net"
7-
"net/http"
8-
"time"
4+
"context"
5+
"crypto/tls"
6+
"net"
7+
"net/http"
8+
"net/url"
9+
"time"
910
)
1011

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

4748
type client struct {
48-
http *http.Client
49-
dialer *net.Dialer
49+
http *http.Client
50+
dialer *net.Dialer
51+
// resolver is used for DNS lookups; defaults to net.DefaultResolver
52+
resolver *net.Resolver
5053
}
5154

52-
// NewClient returns an implementation of Client for verifying ACME challenges.
53-
func NewClient() Client {
54-
return &client{
55-
http: &http.Client{
56-
Timeout: 30 * time.Second,
57-
Transport: &http.Transport{
58-
Proxy: http.ProxyFromEnvironment,
59-
TLSClientConfig: &tls.Config{
60-
//nolint:gosec // used on tls-alpn-01 challenge
61-
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
62-
},
63-
},
64-
},
65-
dialer: &net.Dialer{
66-
Timeout: 30 * time.Second,
67-
},
68-
}
55+
// ClientOption configures the ACME client.
56+
type ClientOption func(*client)
57+
58+
// WithProxyURL configures the HTTP(S) proxy to use for ACME HTTP requests.
59+
// Example: WithProxyURL("http://proxy.local:3128") or WithProxyURL("socks5://...").
60+
func WithProxyURL(proxyURL string) ClientOption {
61+
return func(c *client) {
62+
if tr, ok := c.http.Transport.(*http.Transport); ok {
63+
if u, err := url.Parse(proxyURL); err == nil {
64+
tr.Proxy = http.ProxyURL(u)
65+
}
66+
}
67+
}
68+
}
69+
70+
// WithProxyFunc sets a custom proxy selection function, overriding environment variables.
71+
func WithProxyFunc(fn func(*http.Request) (*url.URL, error)) ClientOption {
72+
return func(c *client) {
73+
if tr, ok := c.http.Transport.(*http.Transport); ok {
74+
tr.Proxy = fn
75+
}
76+
}
77+
}
78+
79+
// WithResolver sets a custom DNS resolver to be used for both TXT lookups and dialing.
80+
func WithResolver(r *net.Resolver) ClientOption {
81+
return func(c *client) {
82+
c.resolver = r
83+
c.dialer.Resolver = r
84+
}
85+
}
86+
87+
// WithDNS configures the client to use a specific DNS server for all lookups and dialing.
88+
// The address should be in host:port form, e.g. "8.8.8.8:53".
89+
func WithDNS(addr string) ClientOption {
90+
return func(c *client) {
91+
r := &net.Resolver{
92+
PreferGo: true,
93+
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
94+
d := &net.Dialer{Timeout: 5 * time.Second}
95+
return d.DialContext(ctx, network, addr)
96+
},
97+
}
98+
c.resolver = r
99+
c.dialer.Resolver = r
100+
}
101+
}
102+
103+
// NewClientWithOptions returns an implementation of Client for verifying ACME challenges.
104+
// It accepts optional ClientOptions to override proxy and DNS resolver behavior.
105+
func NewClientWithOptions(opts ...ClientOption) Client {
106+
d := &net.Dialer{Timeout: 30 * time.Second}
107+
// Default transport uses environment proxy and our dialer so that custom resolver applies to HTTP too.
108+
tr := &http.Transport{
109+
Proxy: http.ProxyFromEnvironment,
110+
DialContext: d.DialContext,
111+
TLSClientConfig: &tls.Config{
112+
//nolint:gosec // used on tls-alpn-01 challenge
113+
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
114+
},
115+
}
116+
c := &client{
117+
http: &http.Client{
118+
Timeout: 30 * time.Second,
119+
Transport: tr,
120+
},
121+
dialer: d,
122+
resolver: net.DefaultResolver,
123+
}
124+
125+
// Apply options
126+
for _, opt := range opts {
127+
opt(c)
128+
}
129+
// Ensure transport dialer is bound (in case options replaced dialer.resolver)
130+
if tr2, ok := c.http.Transport.(*http.Transport); ok {
131+
tr2.DialContext = c.dialer.DialContext
132+
}
133+
return c
134+
}
135+
136+
// NewClient returns an implementation of Client with default settings
137+
// (proxy from environment and system DNS resolver). For custom configuration
138+
// use NewClientWithOptions.
139+
func NewClient(opts ...ClientOption) Client { // keep signature source-compatible for callers without options
140+
return NewClientWithOptions(opts...)
69141
}
70142

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

75147
func (c *client) LookupTxt(name string) ([]string, error) {
76-
return net.LookupTXT(name)
148+
// Prefer custom resolver with a bounded timeout
149+
if c.resolver != nil {
150+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
151+
defer cancel()
152+
return c.resolver.LookupTXT(ctx, name)
153+
}
154+
return net.LookupTXT(name)
77155
}
78156

79157
func (c *client) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) {

authority/provisioner/acme.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,24 @@ func (f ACMEAttestationFormat) Validate() error {
8484
// ACME is the acme provisioner type, an entity that can authorize the ACME
8585
// provisioning flow.
8686
type ACME struct {
87-
*base
88-
ID string `json:"-"`
89-
Type string `json:"type"`
90-
Name string `json:"name"`
91-
ForceCN bool `json:"forceCN,omitempty"`
92-
// TermsOfService contains a URL pointing to the ACME server's
93-
// terms of service. Defaults to empty.
94-
TermsOfService string `json:"termsOfService,omitempty"`
87+
*base
88+
ID string `json:"-"`
89+
Type string `json:"type"`
90+
Name string `json:"name"`
91+
ForceCN bool `json:"forceCN,omitempty"`
92+
// ProxyURL enables configuring a custom HTTP(S) proxy for outbound
93+
// ACME validation requests performed by the server (e.g., http-01 fetches).
94+
// If empty, the default proxy from the environment is used.
95+
ProxyURL string `json:"proxyURL,omitempty"`
96+
// DisableProxy disables usage of any proxy (including environment variables)
97+
// for outbound ACME validation requests.
98+
DisableProxy bool `json:"disableProxy,omitempty"`
99+
// DNS allows forcing a specific DNS resolver in the form "host:port"
100+
// (e.g., "8.8.8.8:53") for DNS queries executed during ACME challenges.
101+
DNS string `json:"dns,omitempty"`
102+
// TermsOfService contains a URL pointing to the ACME server's
103+
// terms of service. Defaults to empty.
104+
TermsOfService string `json:"termsOfService,omitempty"`
95105
// Website contains an URL pointing to more information about
96106
// the ACME server. Defaults to empty.
97107
Website string `json:"website,omitempty"`

0 commit comments

Comments
 (0)