|
1 | 1 | package acme |
2 | 2 |
|
3 | 3 | 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" |
9 | 10 | ) |
10 | 11 |
|
11 | 12 | // Client is the interface used to verify ACME challenges. |
@@ -45,35 +46,112 @@ func MustClientFromContext(ctx context.Context) Client { |
45 | 46 | } |
46 | 47 |
|
47 | 48 | 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 |
50 | 53 | } |
51 | 54 |
|
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...) |
69 | 141 | } |
70 | 142 |
|
71 | 143 | func (c *client) Get(url string) (*http.Response, error) { |
72 | 144 | return c.http.Get(url) |
73 | 145 | } |
74 | 146 |
|
75 | 147 | 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) |
77 | 155 | } |
78 | 156 |
|
79 | 157 | func (c *client) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) { |
|
0 commit comments