summaryrefslogtreecommitdiff
path: root/modules/caddyhttp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r--modules/caddyhttp/caddyhttp.go10
-rw-r--r--modules/caddyhttp/replacer.go104
-rw-r--r--modules/caddyhttp/replacer_test.go80
-rw-r--r--modules/caddyhttp/server.go2
4 files changed, 179 insertions, 17 deletions
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index 30c2f79..135afef 100644
--- a/modules/caddyhttp/caddyhttp.go
+++ b/modules/caddyhttp/caddyhttp.go
@@ -71,6 +71,16 @@ func init() {
// `{http.request.remote.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme
+// `{http.request.tls.version}` | The TLS version name
+// `{http.request.tls.cipher_suite}` | The TLS cipher suite
+// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
+// `{http.request.tls.proto}` | The negotiated next protocol
+// `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server
+// `{http.request.tls.server_name}` | The server name requested by the client, if any
+// `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate
+// `{http.request.tls.client.issuer}` | The issuer DN of the client certificate
+// `{http.request.tls.client.serial}` | The serial number of the client certificate
+// `{http.request.tls.client.subject}` | The subject DN of the client certificate
// `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go
index cea820d..c9c7522 100644
--- a/modules/caddyhttp/replacer.go
+++ b/modules/caddyhttp/replacer.go
@@ -15,6 +15,9 @@
package caddyhttp
import (
+ "crypto/sha256"
+ "crypto/tls"
+ "crypto/x509"
"fmt"
"net"
"net/http"
@@ -24,14 +27,15 @@ import (
"strings"
"github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/modules/caddytls"
)
func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) {
httpVars := func(key string) (string, bool) {
if req != nil {
// query string parameters
- if strings.HasPrefix(key, queryReplPrefix) {
- vals := req.URL.Query()[key[len(queryReplPrefix):]]
+ if strings.HasPrefix(key, reqURIQueryReplPrefix) {
+ vals := req.URL.Query()[key[len(reqURIQueryReplPrefix):]]
// always return true, since the query param might
// be present only in some requests
return strings.Join(vals, ","), true
@@ -47,8 +51,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
}
// cookies
- if strings.HasPrefix(key, cookieReplPrefix) {
- name := key[len(cookieReplPrefix):]
+ if strings.HasPrefix(key, reqCookieReplPrefix) {
+ name := key[len(reqCookieReplPrefix):]
for _, cookie := range req.Cookies() {
if strings.EqualFold(name, cookie.Name) {
// always return true, since the cookie might
@@ -58,6 +62,11 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
}
}
+ // http.request.tls.
+ if strings.HasPrefix(key, reqTLSReplPrefix) {
+ return getReqTLSReplacement(req, key)
+ }
+
switch key {
case "http.request.method":
return req.Method, true
@@ -129,8 +138,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
}
// hostname labels
- if strings.HasPrefix(key, hostLabelReplPrefix) {
- idxStr := key[len(hostLabelReplPrefix):]
+ if strings.HasPrefix(key, reqHostLabelsReplPrefix) {
+ idxStr := key[len(reqHostLabelsReplPrefix):]
idx, err := strconv.Atoi(idxStr)
if err != nil {
return "", false
@@ -150,8 +159,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
}
// path parts
- if strings.HasPrefix(key, pathPartsReplPrefix) {
- idxStr := key[len(pathPartsReplPrefix):]
+ if strings.HasPrefix(key, reqURIPathReplPrefix) {
+ idxStr := key[len(reqURIPathReplPrefix):]
idx, err := strconv.Atoi(idxStr)
if err != nil {
return "", false
@@ -208,12 +217,77 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
repl.Map(httpVars)
}
+func getReqTLSReplacement(req *http.Request, key string) (string, bool) {
+ if req == nil || req.TLS == nil {
+ return "", false
+ }
+
+ if len(key) < len(reqTLSReplPrefix) {
+ return "", false
+ }
+
+ field := strings.ToLower(key[len(reqTLSReplPrefix):])
+
+ if strings.HasPrefix(field, "client.") {
+ cert := getTLSPeerCert(req.TLS)
+ if cert == nil {
+ return "", false
+ }
+
+ switch field {
+ case "client.fingerprint":
+ return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true
+ case "client.issuer":
+ return cert.Issuer.String(), true
+ case "client.serial":
+ return fmt.Sprintf("%x", cert.SerialNumber), true
+ case "client.subject":
+ return cert.Subject.String(), true
+ default:
+ return "", false
+ }
+ }
+
+ switch field {
+ case "version":
+ return caddytls.ProtocolName(req.TLS.Version), true
+ case "cipher_suite":
+ return tls.CipherSuiteName(req.TLS.CipherSuite), true
+ case "resumed":
+ if req.TLS.DidResume {
+ return "true", true
+ }
+ return "false", true
+ case "proto":
+ return req.TLS.NegotiatedProtocol, true
+ case "proto_mutual":
+ if req.TLS.NegotiatedProtocolIsMutual {
+ return "true", true
+ }
+ return "false", true
+ case "server_name":
+ return req.TLS.ServerName, true
+ default:
+ return "", false
+ }
+}
+
+// getTLSPeerCert retrieves the first peer certificate from a TLS session.
+// Returns nil if no peer cert is in use.
+func getTLSPeerCert(cs *tls.ConnectionState) *x509.Certificate {
+ if len(cs.PeerCertificates) == 0 {
+ return nil
+ }
+ return cs.PeerCertificates[0]
+}
+
const (
- queryReplPrefix = "http.request.uri.query."
- reqHeaderReplPrefix = "http.request.header."
- cookieReplPrefix = "http.request.cookie."
- hostLabelReplPrefix = "http.request.host.labels."
- pathPartsReplPrefix = "http.request.uri.path."
- varsReplPrefix = "http.vars."
- respHeaderReplPrefix = "http.response.header."
+ reqCookieReplPrefix = "http.request.cookie."
+ reqHeaderReplPrefix = "http.request.header."
+ reqHostLabelsReplPrefix = "http.request.host.labels."
+ reqTLSReplPrefix = "http.request.tls."
+ reqURIPathReplPrefix = "http.request.uri.path."
+ reqURIQueryReplPrefix = "http.request.uri.query."
+ respHeaderReplPrefix = "http.response.header."
+ varsReplPrefix = "http.vars."
)
diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go
index b355c7f..ea9fa65 100644
--- a/modules/caddyhttp/replacer_test.go
+++ b/modules/caddyhttp/replacer_test.go
@@ -16,6 +16,9 @@ package caddyhttp
import (
"context"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
"net/http"
"net/http/httptest"
"testing"
@@ -30,6 +33,41 @@ func TestHTTPVarReplacement(t *testing.T) {
req = req.WithContext(ctx)
req.Host = "example.com:80"
req.RemoteAddr = "localhost:1234"
+
+ clientCert := []byte(`-----BEGIN CERTIFICATE-----
+MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
+eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
+A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
+iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
+z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
+fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
+BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
+AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
+eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
+3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
+9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
+-----END CERTIFICATE-----`)
+
+ block, _ := pem.Decode(clientCert)
+ if block == nil {
+ t.Fatalf("failed to decode PEM certificate")
+ }
+
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ t.Fatalf("failed to decode PEM certificate: %v", err)
+ }
+
+ req.TLS = &tls.ConnectionState{
+ Version: tls.VersionTLS13,
+ HandshakeComplete: true,
+ ServerName: "foo.com",
+ CipherSuite: tls.TLS_AES_256_GCM_SHA384,
+ PeerCertificates: []*x509.Certificate{cert},
+ NegotiatedProtocol: "h2",
+ NegotiatedProtocolIsMutual: true,
+ }
+
res := httptest.NewRecorder()
addHTTPVarsToReplacer(repl, req, res)
@@ -39,7 +77,7 @@ func TestHTTPVarReplacement(t *testing.T) {
}{
{
input: "{http.request.scheme}",
- expect: "http",
+ expect: "https",
},
{
input: "{http.request.host}",
@@ -69,6 +107,46 @@ func TestHTTPVarReplacement(t *testing.T) {
input: "{http.request.host.labels.1}",
expect: "example",
},
+ {
+ input: "{http.request.tls.cipher_suite}",
+ expect: "TLS_AES_256_GCM_SHA384",
+ },
+ {
+ input: "{http.request.tls.proto}",
+ expect: "h2",
+ },
+ {
+ input: "{http.request.tls.proto_mutual}",
+ expect: "true",
+ },
+ {
+ input: "{http.request.tls.resumed}",
+ expect: "false",
+ },
+ {
+ input: "{http.request.tls.server_name}",
+ expect: "foo.com",
+ },
+ {
+ input: "{http.request.tls.version}",
+ expect: "tls1.3",
+ },
+ {
+ input: "{http.request.tls.client.fingerprint}",
+ expect: "9f57b7b497cceacc5459b76ac1c3afedbc12b300e728071f55f84168ff0f7702",
+ },
+ {
+ input: "{http.request.tls.client.issuer}",
+ expect: "CN=Caddy Test CA",
+ },
+ {
+ input: "{http.request.tls.client.serial}",
+ expect: "2",
+ },
+ {
+ input: "{http.request.tls.client.subject}",
+ expect: "CN=client.localdomain",
+ },
} {
actual := repl.ReplaceAll(tc.input, "<empty>")
if actual != tc.expect {
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index 124331d..580449b 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -173,7 +173,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
log("handled request",
- zap.String("common_log", repl.ReplaceAll(commonLogFormat, "-")),
+ zap.String("common_log", repl.ReplaceAll(commonLogFormat, commonLogEmptyValue)),
zap.Duration("latency", latency),
zap.Int("size", wrec.Size()),
zap.Int("status", wrec.Status()),