Skip to content

Commit b3cc0ab

Browse files
authored
Merge pull request #64 from salesforce/feature/XFCC
Added x-forwarded-client-cert server interceptor
2 parents cfa5f0a + 0be2426 commit b3cc0ab

File tree

7 files changed

+644
-0
lines changed

7 files changed

+644
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright (c) 2017, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
package com.salesforce.grpc.contrib.xfcc;
9+
10+
import io.grpc.Context;
11+
12+
import java.util.ArrayList;
13+
import java.util.Collection;
14+
import java.util.Collections;
15+
import java.util.List;
16+
17+
/**
18+
* x-forwarded-client-cert (XFCC) is a proxy header which indicates certificate information of part or all of the
19+
* clients or proxies that a request has flowed through, on its way from the client to the server.
20+
*/
21+
public final class XForwardedClientCert {
22+
/**
23+
* The metadata key used to access any present {@link XForwardedClientCert} objects.
24+
*/
25+
public static final Context.Key<List<XForwardedClientCert>> XFCC_CONTEXT_KEY = Context.key("x-forwarded-client-cert");
26+
27+
private String by = "";
28+
private String hash = "";
29+
private String sanUri = "";
30+
private List<String> sanDns = new ArrayList<>();
31+
private String subject = "";
32+
33+
void setBy(String by) {
34+
this.by = by;
35+
}
36+
37+
void setHash(String hash) {
38+
this.hash = hash;
39+
}
40+
41+
void setSanUri(String sanUri) {
42+
this.sanUri = sanUri;
43+
}
44+
45+
void setSubject(String subject) {
46+
this.subject = subject;
47+
}
48+
49+
void addSanDns(String sanDns) {
50+
this.sanDns.add(sanDns);
51+
}
52+
53+
/**
54+
* @return The Subject Alternative Name (SAN) of the current proxy’s certificate.
55+
*/
56+
public String getBy() {
57+
return by;
58+
}
59+
60+
/**
61+
* @return The SHA 256 digest of the current client certificate.
62+
*/
63+
public String getHash() {
64+
return hash;
65+
}
66+
67+
/**
68+
* @return The URI type Subject Alternative Name field of the current client certificate.
69+
*/
70+
public String getSanUri() {
71+
return sanUri;
72+
}
73+
74+
/**
75+
* @return The DNS type Subject Alternative Name field(s) of the current client certificate.
76+
*/
77+
public Collection<String> getSanDns() {
78+
return Collections.unmodifiableCollection(sanDns);
79+
}
80+
81+
/**
82+
* @return The Subject field of the current client certificate.
83+
*/
84+
public String getSubject() {
85+
return subject;
86+
}
87+
88+
@Override
89+
public String toString() {
90+
List<String> kvp = new ArrayList<>();
91+
if (!by.isEmpty()) {
92+
kvp.add("By=" + enquote(by));
93+
}
94+
if (!hash.isEmpty()) {
95+
kvp.add("Hash=" + enquote(hash));
96+
}
97+
if (!sanUri.isEmpty()) {
98+
kvp.add("URI=" + enquote(sanUri));
99+
}
100+
for (String dns : sanDns) {
101+
kvp.add("DNS=" + enquote(dns));
102+
}
103+
if (!subject.isEmpty()) {
104+
kvp.add("Subject=" + enquote(subject));
105+
}
106+
107+
return String.join(";", kvp);
108+
}
109+
110+
private String enquote(String value) {
111+
// Escape inner quotes with \"
112+
value = value.replace("\"", "\\\"");
113+
114+
// Wrap in quotes if ,;= is present
115+
if (value.contains(",") || value.contains(";") || value.contains("=")) {
116+
value = "\"" + value + "\"";
117+
}
118+
119+
return value;
120+
}
121+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2017, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
package com.salesforce.grpc.contrib.xfcc;
9+
10+
import io.grpc.Metadata;
11+
12+
import java.util.List;
13+
import java.util.stream.Collectors;
14+
15+
/**
16+
* {@code XfccMarshaller} parses the {@code x-forwarded-client-cert} (XFCC) header populated by TLS-terminating
17+
* reverse proxies. For example, Istio and Linkerd.
18+
*
19+
* @see <a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/http_conn_man/headers.html#config-http-conn-man-headers-x-forwarded-client-cert">Envoy XFCC Header</a>
20+
* @see <a href="https://github.com/linkerd/linkerd/issues/1153">Linkerd XFCC Header</a>
21+
*/
22+
public final class XfccMarshaller implements Metadata.AsciiMarshaller<List<XForwardedClientCert>> {
23+
@Override
24+
public String toAsciiString(List<XForwardedClientCert> value) {
25+
return value.stream().map(XForwardedClientCert::toString).collect(Collectors.joining(","));
26+
}
27+
28+
@Override
29+
public List<XForwardedClientCert> parseAsciiString(String serialized) {
30+
return XfccParser.parse(serialized);
31+
}
32+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright (c) 2017, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
package com.salesforce.grpc.contrib.xfcc;
9+
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
13+
/**
14+
* {@code XfccParser} parses the {@code x-forwarded-client-cert} (XFCC) header populated by TLS-terminating
15+
* reverse proxies.
16+
*
17+
* @see <a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/http_conn_man/headers.html#config-http-conn-man-headers-x-forwarded-client-cert">Envoy XFCC Header</a>
18+
*/
19+
final class XfccParser {
20+
private XfccParser() { }
21+
22+
/**
23+
* Given a header string, parse and return a collection of {@link XForwardedClientCert} objects.
24+
*/
25+
static List<XForwardedClientCert> parse(String header) {
26+
List<XForwardedClientCert> certs = new ArrayList<>();
27+
28+
for (String element : quoteAwareSplit(header, ',')) {
29+
XForwardedClientCert cert = new XForwardedClientCert();
30+
List<String> kvps = quoteAwareSplit(element, ';');
31+
for (String kvp : kvps) {
32+
List<String> l = quoteAwareSplit(kvp, '=');
33+
34+
if (l.get(0).toLowerCase().equals("by")) {
35+
cert.setBy(dequote(l.get(1)));
36+
}
37+
if (l.get(0).toLowerCase().equals("hash")) {
38+
cert.setHash(dequote(l.get(1)));
39+
}
40+
// Use "SAN:" instead of "URI:" for backward compatibility with previous mesh proxy releases.
41+
if (l.get(0).toLowerCase().equals("san") || l.get(0).toLowerCase().equals("uri")) {
42+
cert.setSanUri(dequote(l.get(1)));
43+
}
44+
if (l.get(0).toLowerCase().equals("dns")) {
45+
cert.addSanDns(dequote(l.get(1)));
46+
}
47+
if (l.get(0).toLowerCase().equals("subject")) {
48+
cert.setSubject(dequote(l.get(1)));
49+
}
50+
}
51+
certs.add(cert);
52+
}
53+
54+
return certs;
55+
}
56+
57+
// Break str into individual elements, splitting on delim (not in quotes)
58+
private static List<String> quoteAwareSplit(String str, char delim) {
59+
boolean inQuotes = false;
60+
boolean inEscape = false;
61+
62+
List<String> elements = new ArrayList<>();
63+
StringBuilder buffer = new StringBuilder();
64+
for (char c : str.toCharArray()) {
65+
if (c == delim && !inQuotes) {
66+
elements.add(buffer.toString());
67+
buffer = new StringBuilder();
68+
inEscape = false;
69+
continue;
70+
}
71+
72+
if (c == '"') {
73+
if (inQuotes) {
74+
if (!inEscape) {
75+
inQuotes = false;
76+
}
77+
} else {
78+
inQuotes = true;
79+
80+
}
81+
inEscape = false;
82+
buffer.append(c);
83+
continue;
84+
}
85+
86+
if (c == '\\') {
87+
if (!inEscape) {
88+
inEscape = true;
89+
buffer.append(c);
90+
continue;
91+
}
92+
}
93+
94+
// all other characters
95+
inEscape = false;
96+
buffer.append(c);
97+
}
98+
99+
if (inQuotes) {
100+
throw new RuntimeException("Quoted string not closed");
101+
}
102+
103+
elements.add(buffer.toString());
104+
105+
return elements;
106+
}
107+
108+
// Remove leading and tailing unescaped quotes, remove escaping from escaped internal quotes
109+
private static String dequote(String str) {
110+
str = str.replace("\\\"", "\"");
111+
if (str.startsWith("\"")) {
112+
str = str.substring(1);
113+
}
114+
if (str.endsWith("\"")) {
115+
str = str.substring(0, str.length() - 1);
116+
}
117+
return str;
118+
}
119+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2017, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
package com.salesforce.grpc.contrib.xfcc;
9+
10+
import io.grpc.*;
11+
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
15+
/**
16+
* {@code XfccServerInterceptor} parses the {@code x-forwarded-client-cert} (XFCC) header populated by TLS-terminating
17+
* reverse proxies. For example: Envoy, Istio, and Linkerd. If present, the parsed XFCC header is appended to the
18+
* gRPC {@code Context}.
19+
*
20+
* @see <a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/http_conn_man/headers.html#config-http-conn-man-headers-x-forwarded-client-cert">Envoy XFCC Header</a>
21+
* @see <a href="https://github.com/linkerd/linkerd/issues/1153">Linkerd XFCC Header</a>
22+
*/
23+
public final class XfccServerInterceptor implements ServerInterceptor {
24+
private static final Metadata.Key<List<XForwardedClientCert>> XFCC_METADATA_KEY = Metadata.Key.of("x-forwarded-client-cert", new XfccMarshaller());
25+
26+
@Override
27+
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
28+
Iterable<List<XForwardedClientCert>> values = headers.getAll(XFCC_METADATA_KEY);
29+
if (values != null) {
30+
List<XForwardedClientCert> xfccs = new ArrayList<>();
31+
for (List<XForwardedClientCert> value : values) {
32+
xfccs.addAll(value);
33+
}
34+
35+
Context xfccContext = Context.current().withValue(XForwardedClientCert.XFCC_CONTEXT_KEY, xfccs);
36+
return Contexts.interceptCall(xfccContext, call, headers, next);
37+
} else {
38+
return next.startCall(call, headers);
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)