@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 213 lines 5.76 KB
Raw
1 package caddytls
2
3 import (
4 "context"
5 "crypto/tls"
6 "fmt"
7 "io"
8 "net"
9 "net/http"
10 "net/url"
11 "strings"
12
13 "github.com/caddyserver/certmagic"
14 "github.com/tailscale/tscert"
15 "go.uber.org/zap"
16 "go.uber.org/zap/zapcore"
17
18 "github.com/caddyserver/caddy/v2"
19 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
20 )
21
22 func init() {
23 caddy.RegisterModule(Tailscale{})
24 caddy.RegisterModule(HTTPCertGetter{})
25 }
26
27 // Tailscale is a module that can get certificates from the local Tailscale process.
28 type Tailscale struct {
29 logger *zap.Logger
30 }
31
32 // CaddyModule returns the Caddy module information.
33 func (Tailscale) CaddyModule() caddy.ModuleInfo {
34 return caddy.ModuleInfo{
35 ID: "tls.get_certificate.tailscale",
36 New: func() caddy.Module { return new(Tailscale) },
37 }
38 }
39
40 func (ts *Tailscale) Provision(ctx caddy.Context) error {
41 ts.logger = ctx.Logger()
42 return nil
43 }
44
45 func (ts Tailscale) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
46 canGetCert, err := ts.canHazCertificate(ctx, hello)
47 if err == nil && !canGetCert {
48 return nil, nil // pass-thru: Tailscale can't offer a cert for this name
49 }
50 if err != nil {
51 if c := ts.logger.Check(zapcore.WarnLevel, "could not get status; will try to get certificate anyway"); c != nil {
52 c.Write(zap.Error(err))
53 }
54 }
55 return tscert.GetCertificateWithContext(ctx, hello)
56 }
57
58 // canHazCertificate returns true if Tailscale reports it can get a certificate for the given ClientHello.
59 func (ts Tailscale) canHazCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (bool, error) {
60 if !strings.HasSuffix(strings.ToLower(hello.ServerName), tailscaleDomainAliasEnding) {
61 return false, nil
62 }
63 status, err := tscert.GetStatus(ctx)
64 if err != nil {
65 return false, err
66 }
67 for _, domain := range status.CertDomains {
68 if certmagic.MatchWildcard(hello.ServerName, domain) {
69 return true, nil
70 }
71 }
72 return false, nil
73 }
74
75 // UnmarshalCaddyfile deserializes Caddyfile tokens into ts.
76 //
77 // ... tailscale
78 func (Tailscale) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
79 d.Next() // consume cert manager name
80 if d.NextArg() {
81 return d.ArgErr()
82 }
83 return nil
84 }
85
86 // tailscaleDomainAliasEnding is the ending for all Tailscale custom domains.
87 const tailscaleDomainAliasEnding = ".ts.net"
88
89 // HTTPCertGetter can get a certificate via HTTP(S) request.
90 type HTTPCertGetter struct {
91 // The URL from which to download the certificate. Required.
92 //
93 // The URL will be augmented with query string parameters taken
94 // from the TLS handshake:
95 //
96 // - server_name: The SNI value
97 // - signature_schemes: Comma-separated list of hex IDs of signatures
98 // - cipher_suites: Comma-separated list of hex IDs of cipher suites
99 //
100 // To be valid, the response must be HTTP 200 with a PEM body
101 // consisting of blocks for the certificate chain and the private
102 // key.
103 //
104 // To indicate that this manager is not managing a certificate for
105 // the described handshake, the endpoint should return HTTP 204
106 // (No Content). Error statuses will indicate that the manager is
107 // capable of providing a certificate but was unable to.
108 URL string `json:"url,omitempty"`
109
110 ctx context.Context
111 }
112
113 // CaddyModule returns the Caddy module information.
114 func (hcg HTTPCertGetter) CaddyModule() caddy.ModuleInfo {
115 return caddy.ModuleInfo{
116 ID: "tls.get_certificate.http",
117 New: func() caddy.Module { return new(HTTPCertGetter) },
118 }
119 }
120
121 func (hcg *HTTPCertGetter) Provision(ctx caddy.Context) error {
122 hcg.ctx = ctx
123 if hcg.URL == "" {
124 return fmt.Errorf("URL is required")
125 }
126 return nil
127 }
128
129 func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
130 sigs := make([]string, len(hello.SignatureSchemes))
131 for i, sig := range hello.SignatureSchemes {
132 sigs[i] = fmt.Sprintf("%x", uint16(sig)) // you won't believe what %x uses if the val is a Stringer
133 }
134 suites := make([]string, len(hello.CipherSuites))
135 for i, cs := range hello.CipherSuites {
136 suites[i] = fmt.Sprintf("%x", cs)
137 }
138
139 parsed, err := url.Parse(hcg.URL)
140 if err != nil {
141 return nil, err
142 }
143 qs := parsed.Query()
144 qs.Set("server_name", hello.ServerName)
145 qs.Set("signature_schemes", strings.Join(sigs, ","))
146 qs.Set("cipher_suites", strings.Join(suites, ","))
147 localIP, _, err := net.SplitHostPort(hello.Conn.LocalAddr().String())
148 if err == nil && localIP != "" {
149 qs.Set("local_ip", localIP)
150 }
151 parsed.RawQuery = qs.Encode()
152
153 req, err := http.NewRequestWithContext(hcg.ctx, http.MethodGet, parsed.String(), nil)
154 if err != nil {
155 return nil, err
156 }
157
158 resp, err := http.DefaultClient.Do(req) //nolint:gosec // SSRF false positive... request URI comes from config
159 if err != nil {
160 return nil, err
161 }
162 defer resp.Body.Close()
163 if resp.StatusCode == http.StatusNoContent {
164 // endpoint is not managing certs for this handshake
165 return nil, nil
166 }
167 if resp.StatusCode != http.StatusOK {
168 return nil, fmt.Errorf("got HTTP %d", resp.StatusCode)
169 }
170
171 bodyBytes, err := io.ReadAll(resp.Body)
172 if err != nil {
173 return nil, fmt.Errorf("error reading response body: %v", err)
174 }
175
176 cert, err := tlsCertFromCertAndKeyPEMBundle(bodyBytes)
177 if err != nil {
178 return nil, err
179 }
180
181 return &cert, nil
182 }
183
184 // UnmarshalCaddyfile deserializes Caddyfile tokens into ts.
185 //
186 // ... http <url>
187 func (hcg *HTTPCertGetter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
188 d.Next() // consume cert manager name
189
190 if !d.NextArg() {
191 return d.ArgErr()
192 }
193 hcg.URL = d.Val()
194
195 if d.NextArg() {
196 return d.ArgErr()
197 }
198 if d.NextBlock(0) {
199 return d.Err("block not allowed here")
200 }
201 return nil
202 }
203
204 // Interface guards
205 var (
206 _ certmagic.Manager = (*Tailscale)(nil)
207 _ caddy.Provisioner = (*Tailscale)(nil)
208 _ caddyfile.Unmarshaler = (*Tailscale)(nil)
209
210 _ certmagic.Manager = (*HTTPCertGetter)(nil)
211 _ caddy.Provisioner = (*HTTPCertGetter)(nil)
212 _ caddyfile.Unmarshaler = (*HTTPCertGetter)(nil)
213 )