Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support known tls placeholders replacing even for non tls request #5154

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 103 additions & 34 deletions modules/caddyhttp/replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,20 +311,21 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
}

func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
if req == nil || req.TLS == nil {
return nil, false
}

if len(key) < len(reqTLSReplPrefix) {
return nil, false
}

var isTLSReq bool = req != nil && req.TLS != nil

field := strings.ToLower(key[len(reqTLSReplPrefix):])

if strings.HasPrefix(field, "client.") {
cert := getTLSPeerCert(req.TLS)
if cert == nil {
return nil, false
var cert *x509.Certificate
if isTLSReq {
cert = getTLSPeerCert(req.TLS)
if cert == nil {
return nil, false
}
}

// subject alternate names (SANs)
Expand All @@ -335,16 +336,32 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
switch {
case strings.HasPrefix(field, "dns_names"):
fieldName = "dns_names"
fieldValue = cert.DNSNames
if isTLSReq {
fieldValue = cert.DNSNames
} else {
return nil, true
}
nikolaevn marked this conversation as resolved.
Show resolved Hide resolved
case strings.HasPrefix(field, "emails"):
fieldName = "emails"
fieldValue = cert.EmailAddresses
if isTLSReq {
fieldValue = cert.EmailAddresses
} else {
return nil, true
}
case strings.HasPrefix(field, "ips"):
fieldName = "ips"
fieldValue = cert.IPAddresses
if isTLSReq {
fieldValue = cert.IPAddresses
} else {
return nil, true
}
case strings.HasPrefix(field, "uris"):
fieldName = "uris"
fieldValue = cert.URIs
if isTLSReq {
fieldValue = cert.URIs
} else {
return nil, true
}
default:
return nil, false
}
Expand Down Expand Up @@ -387,49 +404,101 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {

switch field {
case "client.fingerprint":
return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true
case "client.public_key", "client.public_key_sha256":
if cert.PublicKey == nil {
return nil, true
if isTLSReq {
return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true
} else {
return "", true
}
pubKeyBytes, err := marshalPublicKey(cert.PublicKey)
if err != nil {
case "client.public_key", "client.public_key_sha256":
if isTLSReq {
if cert.PublicKey == nil {
return nil, true
}
pubKeyBytes, err := marshalPublicKey(cert.PublicKey)
if err != nil {
return nil, true
}
if strings.HasSuffix(field, "_sha256") {
return fmt.Sprintf("%x", sha256.Sum256(pubKeyBytes)), true
}
return fmt.Sprintf("%x", pubKeyBytes), true
} else {
return nil, true
}
if strings.HasSuffix(field, "_sha256") {
return fmt.Sprintf("%x", sha256.Sum256(pubKeyBytes)), true
}
return fmt.Sprintf("%x", pubKeyBytes), true
case "client.issuer":
return cert.Issuer, true
if isTLSReq {
return cert.Issuer, true
} else {
return "", true
}
nikolaevn marked this conversation as resolved.
Show resolved Hide resolved
case "client.serial":
return cert.SerialNumber, true
if isTLSReq {
return cert.SerialNumber, true
} else {
return "", true
}
case "client.subject":
return cert.Subject, true
if isTLSReq {
return cert.Subject, true
} else {
return "", true
}
case "client.certificate_pem":
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
return pem.EncodeToMemory(&block), true
if isTLSReq {
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
return pem.EncodeToMemory(&block), true
} else {
return "", true
}
case "client.certificate_der_base64":
return base64.StdEncoding.EncodeToString(cert.Raw), true
if isTLSReq {
return base64.StdEncoding.EncodeToString(cert.Raw), true
} else {
return "", true
}
default:
return nil, false
}
}

switch field {
case "version":
return caddytls.ProtocolName(req.TLS.Version), true
if isTLSReq {
return caddytls.ProtocolName(req.TLS.Version), true
} else {
return "", true
}
case "cipher_suite":
return tls.CipherSuiteName(req.TLS.CipherSuite), true
if isTLSReq {
return tls.CipherSuiteName(req.TLS.CipherSuite), true
} else {
return "", true
}
case "resumed":
return req.TLS.DidResume, true
if isTLSReq {
return req.TLS.DidResume, true
} else {
return false, true
}
case "proto":
return req.TLS.NegotiatedProtocol, true
if isTLSReq {
return req.TLS.NegotiatedProtocol, true
} else {
return "", true
}
case "proto_mutual":
// req.TLS.NegotiatedProtocolIsMutual is deprecated - it's always true.
return true, true
if isTLSReq {
// req.TLS.NegotiatedProtocolIsMutual is deprecated - it's always true.
return true, true
} else {
return false, true
}
case "server_name":
return req.TLS.ServerName, true
if isTLSReq {
return req.TLS.ServerName, true
} else {
return "", true
}
}
return nil, false
}
Expand Down
157 changes: 157 additions & 0 deletions modules/caddyhttp/replacer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,160 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
}
}
}

func TestHTTPVarReplacement_NonTLSRequest(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz", nil)
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
req.Host = "example.com:80"
req.RemoteAddr = "192.168.159.32:1234"

res := httptest.NewRecorder()
addHTTPVarsToReplacer(repl, req, res)

for i, tc := range []struct {
get string
expect string
}{
{
get: "http.request.scheme",
expect: "http",
},
{
get: "http.request.method",
expect: http.MethodGet,
},
{
get: "http.request.host",
expect: "example.com",
},
{
get: "http.request.port",
expect: "80",
},
{
get: "http.request.hostport",
expect: "example.com:80",
},
{
get: "http.request.remote.host",
expect: "192.168.159.32",
},
{
get: "http.request.remote.host/24",
expect: "192.168.159.0/24",
},
{
get: "http.request.remote.host/24,32",
expect: "192.168.159.0/24",
},
{
get: "http.request.remote.host/999",
expect: "",
},
{
get: "http.request.remote.port",
expect: "1234",
},
{
get: "http.request.host.labels.0",
expect: "com",
},
{
get: "http.request.host.labels.1",
expect: "example",
},
{
get: "http.request.host.labels.2",
expect: "",
},
{
get: "http.request.uri.path.file",
expect: "bar.tar.gz",
},
{
get: "http.request.uri.path.file.base",
expect: "bar.tar",
},
{
// not ideal, but also most correct, given that files can have dots (example: index.<SHA>.html) TODO: maybe this isn't right..
get: "http.request.uri.path.file.ext",
expect: ".gz",
},

{
get: "http.request.tls.cipher_suite",
expect: "",
},
{
get: "http.request.tls.proto",
expect: "",
},
{
get: "http.request.tls.proto_mutual",
expect: "false",
},
{
get: "http.request.tls.resumed",
expect: "false",
},
{
get: "http.request.tls.server_name",
expect: "",
},
{
get: "http.request.tls.version",
expect: "",
},
{
get: "http.request.tls.client.fingerprint",
expect: "",
},
{
get: "http.request.tls.client.issuer",
expect: "",
},
{
get: "http.request.tls.client.serial",
expect: "",
},
{
get: "http.request.tls.client.subject",
expect: "",
},
{
get: "http.request.tls.client.san.dns_names",
expect: "",
},
{
get: "http.request.tls.client.san.dns_names.0",
expect: "",
},
{
get: "http.request.tls.client.san.dns_names.1",
expect: "",
},
{
get: "http.request.tls.client.san.ips",
expect: "",
},
{
get: "http.request.tls.client.san.ips.0",
expect: "",
},
{
get: "http.request.tls.client.certificate_pem",
expect: "",
},
} {
actual, got := repl.GetString(tc.get)
if !got {
t.Errorf("Test %d: Expected to recognize the placeholder name, but didn't", i)
}
if actual != tc.expect {
t.Errorf("Test %d: Expected %s to be '%s' but got '%s'",
i, tc.get, tc.expect, actual)
}
}
}