@marcel91200 / caddy Mirror · read-only / commits / b0a491ae
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 14, 05:15 UTC

Expose TLS placeholders (#2982)

* caddytls: Add CipherSuiteName and ProtocolName functions The cipher_suites.go file is derived from a commit to the Go master branch that's slated for Go 1.14. Once Go 1.14 is released, this file can be removed. * caddyhttp: Use commonLogEmptyValue in common_log replacer * caddyhttp: Add TLS placeholders * caddytls: update unsupportedProtocols Don't export unsupportedProtocols and update its godoc to mention that it's used for logging only. * caddyhttp: simplify getRegTLSReplacement signature getRegTLSReplacement should receive a string instead of a pointer. * caddyhttp: Remove http.request.tls.client.cert replacer The previous behavior of printing the raw certificate bytes was ported from Caddy 1, but the usefulness of that approach is suspect. Remove the client cert replacer from v2 until a use case is presented. * caddyhttp: Use tls.CipherSuiteName from Go 1.14 Remove ported version of CipherSuiteName in the process.

Cameron Moore committed Feb 26, 2020 at 02:22 UTC b0a491aec808652dfd65910ee192ab88c07ac99d
6 files changed +214 -17
caddyconfig/httpcaddyfile/httptype.go
+7
@@ -88,6 +88,13 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
88 "{remote}", "{http.request.remote}",
89 "{scheme}", "{http.request.scheme}",
90 "{uri}", "{http.request.uri}",
91 +
92 + "{tls_cipher}", "{http.request.tls.cipher_suite}",
93 + "{tls_version}", "{http.request.tls.version}",
94 + "{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
95 + "{tls_client_issuer}", "{http.request.tls.client.issuer}",
96 + "{tls_client_serial}", "{http.request.tls.client.serial}",
97 + "{tls_client_subject}", "{http.request.tls.client.subject}",
98 )
99 for _, segment := range sb.block.Segments {
100 for i := 0; i < len(segment); i++ {
modules/caddyhttp/caddyhttp.go
+10
@@ -71,6 +71,16 @@ func init() {
71 // `{http.request.remote.port}` | The port part of the remote client's address
72 // `{http.request.remote}` | The address of the remote client
73 // `{http.request.scheme}` | The request scheme
74 +// `{http.request.tls.version}` | The TLS version name
75 +// `{http.request.tls.cipher_suite}` | The TLS cipher suite
76 +// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
77 +// `{http.request.tls.proto}` | The negotiated next protocol
78 +// `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server
79 +// `{http.request.tls.server_name}` | The server name requested by the client, if any
80 +// `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate
81 +// `{http.request.tls.client.issuer}` | The issuer DN of the client certificate
82 +// `{http.request.tls.client.serial}` | The serial number of the client certificate
83 +// `{http.request.tls.client.subject}` | The subject DN of the client certificate
84 // `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
85 // `{http.request.uri.path.dir}` | The directory, excluding leaf filename
86 // `{http.request.uri.path.file}` | The filename of the path, excluding directory
modules/caddyhttp/replacer.go
+89 -15
@@ -15,6 +15,9 @@
15 package caddyhttp
16
17 import (
18 + "crypto/sha256"
19 + "crypto/tls"
20 + "crypto/x509"
21 "fmt"
22 "net"
23 "net/http"
@@ -24,14 +27,15 @@ import (
27 "strings"
28
29 "github.com/caddyserver/caddy/v2"
30 + "github.com/caddyserver/caddy/v2/modules/caddytls"
31 )
32
33 func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) {
34 httpVars := func(key string) (string, bool) {
35 if req != nil {
36 // query string parameters
33 - if strings.HasPrefix(key, queryReplPrefix) {
34 - vals := req.URL.Query()[key[len(queryReplPrefix):]]
37 + if strings.HasPrefix(key, reqURIQueryReplPrefix) {
38 + vals := req.URL.Query()[key[len(reqURIQueryReplPrefix):]]
39 // always return true, since the query param might
40 // be present only in some requests
41 return strings.Join(vals, ","), true
@@ -47,8 +51,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
51 }
52
53 // cookies
50 - if strings.HasPrefix(key, cookieReplPrefix) {
51 - name := key[len(cookieReplPrefix):]
54 + if strings.HasPrefix(key, reqCookieReplPrefix) {
55 + name := key[len(reqCookieReplPrefix):]
56 for _, cookie := range req.Cookies() {
57 if strings.EqualFold(name, cookie.Name) {
58 // always return true, since the cookie might
@@ -58,6 +62,11 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
62 }
63 }
64
65 + // http.request.tls.
66 + if strings.HasPrefix(key, reqTLSReplPrefix) {
67 + return getReqTLSReplacement(req, key)
68 + }
69 +
70 switch key {
71 case "http.request.method":
72 return req.Method, true
@@ -129,8 +138,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
138 }
139
140 // hostname labels
132 - if strings.HasPrefix(key, hostLabelReplPrefix) {
133 - idxStr := key[len(hostLabelReplPrefix):]
141 + if strings.HasPrefix(key, reqHostLabelsReplPrefix) {
142 + idxStr := key[len(reqHostLabelsReplPrefix):]
143 idx, err := strconv.Atoi(idxStr)
144 if err != nil {
145 return "", false
@@ -150,8 +159,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
159 }
160
161 // path parts
153 - if strings.HasPrefix(key, pathPartsReplPrefix) {
154 - idxStr := key[len(pathPartsReplPrefix):]
162 + if strings.HasPrefix(key, reqURIPathReplPrefix) {
163 + idxStr := key[len(reqURIPathReplPrefix):]
164 idx, err := strconv.Atoi(idxStr)
165 if err != nil {
166 return "", false
@@ -208,12 +217,77 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
217 repl.Map(httpVars)
218 }
219
220 +func getReqTLSReplacement(req *http.Request, key string) (string, bool) {
221 + if req == nil || req.TLS == nil {
222 + return "", false
223 + }
224 +
225 + if len(key) < len(reqTLSReplPrefix) {
226 + return "", false
227 + }
228 +
229 + field := strings.ToLower(key[len(reqTLSReplPrefix):])
230 +
231 + if strings.HasPrefix(field, "client.") {
232 + cert := getTLSPeerCert(req.TLS)
233 + if cert == nil {
234 + return "", false
235 + }
236 +
237 + switch field {
238 + case "client.fingerprint":
239 + return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true
240 + case "client.issuer":
241 + return cert.Issuer.String(), true
242 + case "client.serial":
243 + return fmt.Sprintf("%x", cert.SerialNumber), true
244 + case "client.subject":
245 + return cert.Subject.String(), true
246 + default:
247 + return "", false
248 + }
249 + }
250 +
251 + switch field {
252 + case "version":
253 + return caddytls.ProtocolName(req.TLS.Version), true
254 + case "cipher_suite":
255 + return tls.CipherSuiteName(req.TLS.CipherSuite), true
256 + case "resumed":
257 + if req.TLS.DidResume {
258 + return "true", true
259 + }
260 + return "false", true
261 + case "proto":
262 + return req.TLS.NegotiatedProtocol, true
263 + case "proto_mutual":
264 + if req.TLS.NegotiatedProtocolIsMutual {
265 + return "true", true
266 + }
267 + return "false", true
268 + case "server_name":
269 + return req.TLS.ServerName, true
270 + default:
271 + return "", false
272 + }
273 +}
274 +
275 +// getTLSPeerCert retrieves the first peer certificate from a TLS session.
276 +// Returns nil if no peer cert is in use.
277 +func getTLSPeerCert(cs *tls.ConnectionState) *x509.Certificate {
278 + if len(cs.PeerCertificates) == 0 {
279 + return nil
280 + }
281 + return cs.PeerCertificates[0]
282 +}
283 +
284 const (
212 - queryReplPrefix = "http.request.uri.query."
213 - reqHeaderReplPrefix = "http.request.header."
214 - cookieReplPrefix = "http.request.cookie."
215 - hostLabelReplPrefix = "http.request.host.labels."
216 - pathPartsReplPrefix = "http.request.uri.path."
217 - varsReplPrefix = "http.vars."
218 - respHeaderReplPrefix = "http.response.header."
285 + reqCookieReplPrefix = "http.request.cookie."
286 + reqHeaderReplPrefix = "http.request.header."
287 + reqHostLabelsReplPrefix = "http.request.host.labels."
288 + reqTLSReplPrefix = "http.request.tls."
289 + reqURIPathReplPrefix = "http.request.uri.path."
290 + reqURIQueryReplPrefix = "http.request.uri.query."
291 + respHeaderReplPrefix = "http.response.header."
292 + varsReplPrefix = "http.vars."
293 )
modules/caddyhttp/replacer_test.go
+79 -1
@@ -16,6 +16,9 @@ package caddyhttp
16
17 import (
18 "context"
19 + "crypto/tls"
20 + "crypto/x509"
21 + "encoding/pem"
22 "net/http"
23 "net/http/httptest"
24 "testing"
@@ -30,6 +33,41 @@ func TestHTTPVarReplacement(t *testing.T) {
33 req = req.WithContext(ctx)
34 req.Host = "example.com:80"
35 req.RemoteAddr = "localhost:1234"
36 +
37 + clientCert := []byte(`-----BEGIN CERTIFICATE-----
38 +MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
39 +eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
40 +A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
41 +iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
42 +z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
43 +fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
44 +BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
45 +AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
46 +eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
47 +3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
48 +9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
49 +-----END CERTIFICATE-----`)
50 +
51 + block, _ := pem.Decode(clientCert)
52 + if block == nil {
53 + t.Fatalf("failed to decode PEM certificate")
54 + }
55 +
56 + cert, err := x509.ParseCertificate(block.Bytes)
57 + if err != nil {
58 + t.Fatalf("failed to decode PEM certificate: %v", err)
59 + }
60 +
61 + req.TLS = &tls.ConnectionState{
62 + Version: tls.VersionTLS13,
63 + HandshakeComplete: true,
64 + ServerName: "foo.com",
65 + CipherSuite: tls.TLS_AES_256_GCM_SHA384,
66 + PeerCertificates: []*x509.Certificate{cert},
67 + NegotiatedProtocol: "h2",
68 + NegotiatedProtocolIsMutual: true,
69 + }
70 +
71 res := httptest.NewRecorder()
72 addHTTPVarsToReplacer(repl, req, res)
73
@@ -39,7 +77,7 @@ func TestHTTPVarReplacement(t *testing.T) {
77 }{
78 {
79 input: "{http.request.scheme}",
42 - expect: "http",
80 + expect: "https",
81 },
82 {
83 input: "{http.request.host}",
@@ -69,6 +107,46 @@ func TestHTTPVarReplacement(t *testing.T) {
107 input: "{http.request.host.labels.1}",
108 expect: "example",
109 },
110 + {
111 + input: "{http.request.tls.cipher_suite}",
112 + expect: "TLS_AES_256_GCM_SHA384",
113 + },
114 + {
115 + input: "{http.request.tls.proto}",
116 + expect: "h2",
117 + },
118 + {
119 + input: "{http.request.tls.proto_mutual}",
120 + expect: "true",
121 + },
122 + {
123 + input: "{http.request.tls.resumed}",
124 + expect: "false",
125 + },
126 + {
127 + input: "{http.request.tls.server_name}",
128 + expect: "foo.com",
129 + },
130 + {
131 + input: "{http.request.tls.version}",
132 + expect: "tls1.3",
133 + },
134 + {
135 + input: "{http.request.tls.client.fingerprint}",
136 + expect: "9f57b7b497cceacc5459b76ac1c3afedbc12b300e728071f55f84168ff0f7702",
137 + },
138 + {
139 + input: "{http.request.tls.client.issuer}",
140 + expect: "CN=Caddy Test CA",
141 + },
142 + {
143 + input: "{http.request.tls.client.serial}",
144 + expect: "2",
145 + },
146 + {
147 + input: "{http.request.tls.client.subject}",
148 + expect: "CN=client.localdomain",
149 + },
150 } {
151 actual := repl.ReplaceAll(tc.input, "<empty>")
152 if actual != tc.expect {
modules/caddyhttp/server.go
+1 -1
@@ -173,7 +173,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
173 }
174
175 log("handled request",
176 - zap.String("common_log", repl.ReplaceAll(commonLogFormat, "-")),
176 + zap.String("common_log", repl.ReplaceAll(commonLogFormat, commonLogEmptyValue)),
177 zap.Duration("latency", latency),
178 zap.Int("size", wrec.Size()),
179 zap.Int("status", wrec.Status()),
modules/caddytls/values.go
+28
@@ -17,6 +17,7 @@ package caddytls
17 import (
18 "crypto/tls"
19 "crypto/x509"
20 + "fmt"
21
22 "github.com/go-acme/lego/v3/certcrypto"
23 "github.com/klauspost/cpuid"
@@ -127,9 +128,36 @@ var SupportedProtocols = map[string]uint16{
128 "tls1.3": tls.VersionTLS13,
129 }
130
131 +// unsupportedProtocols is a map of unsupported protocols.
132 +// Used for logging only, not enforcement.
133 +var unsupportedProtocols = map[string]uint16{
134 + "ssl3.0": tls.VersionSSL30,
135 + "tls1.0": tls.VersionTLS10,
136 + "tls1.1": tls.VersionTLS11,
137 +}
138 +
139 // publicKeyAlgorithms is the map of supported public key algorithms.
140 var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{
141 "rsa": x509.RSA,
142 "dsa": x509.DSA,
143 "ecdsa": x509.ECDSA,
144 }
145 +
146 +// ProtocolName returns the standard name for the passed protocol version ID
147 +// (e.g. "TLS1.3") or a fallback representation of the ID value if the version
148 +// is not supported.
149 +func ProtocolName(id uint16) string {
150 + for k, v := range SupportedProtocols {
151 + if v == id {
152 + return k
153 + }
154 + }
155 +
156 + for k, v := range unsupportedProtocols {
157 + if v == id {
158 + return k
159 + }
160 + }
161 +
162 + return fmt.Sprintf("0x%04x", id)
163 +}