Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (c) 2017, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

package com.salesforce.grpc.contrib.xfcc;

import io.grpc.Context;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
* x-forwarded-client-cert (XFCC) is a proxy header which indicates certificate information of part or all of the
* clients or proxies that a request has flowed through, on its way from the client to the server.
*/
public final class XForwardedClientCert {
/**
* The metadata key used to access any present {@link XForwardedClientCert} objects.
*/
public static final Context.Key<List<XForwardedClientCert>> XFCC_CONTEXT_KEY = Context.key("x-forwarded-client-cert");

private String by = "";
private String hash = "";
private String sanUri = "";
private List<String> sanDns = new ArrayList<>();
private String subject = "";

void setBy(String by) {
this.by = by;
}

void setHash(String hash) {
this.hash = hash;
}

void setSanUri(String sanUri) {
this.sanUri = sanUri;
}

void setSubject(String subject) {
this.subject = subject;
}

void addSanDns(String sanDns) {
this.sanDns.add(sanDns);
}

/**
* @return The Subject Alternative Name (SAN) of the current proxy’s certificate.
*/
public String getBy() {
return by;
}

/**
* @return The SHA 256 digest of the current client certificate.
*/
public String getHash() {
return hash;
}

/**
* @return The URI type Subject Alternative Name field of the current client certificate.
*/
public String getSanUri() {
return sanUri;
}

/**
* @return The DNS type Subject Alternative Name field(s) of the current client certificate.
*/
public Collection<String> getSanDns() {
return Collections.unmodifiableCollection(sanDns);
}

/**
* @return The Subject field of the current client certificate.
*/
public String getSubject() {
return subject;
}

@Override
public String toString() {
List<String> kvp = new ArrayList<>();
if (!by.isEmpty()) {
kvp.add("By=" + enquote(by));
}
if (!hash.isEmpty()) {
kvp.add("Hash=" + enquote(hash));
}
if (!sanUri.isEmpty()) {
kvp.add("URI=" + enquote(sanUri));
}
for (String dns : sanDns) {
kvp.add("DNS=" + enquote(dns));
}
if (!subject.isEmpty()) {
kvp.add("Subject=" + enquote(subject));
}

return String.join(";", kvp);
}

private String enquote(String value) {
// Escape inner quotes with \"
value = value.replace("\"", "\\\"");

// Wrap in quotes if ,;= is present
if (value.contains(",") || value.contains(";") || value.contains("=")) {
value = "\"" + value + "\"";
}

return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2017, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

package com.salesforce.grpc.contrib.xfcc;

import io.grpc.Metadata;

import java.util.List;
import java.util.stream.Collectors;

/**
* {@code XfccMarshaller} parses the {@code x-forwarded-client-cert} (XFCC) header populated by TLS-terminating
* reverse proxies. For example, Istio and Linkerd.
*
* @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>
* @see <a href="https://github.com/linkerd/linkerd/issues/1153">Linkerd XFCC Header</a>
*/
public final class XfccMarshaller implements Metadata.AsciiMarshaller<List<XForwardedClientCert>> {
@Override
public String toAsciiString(List<XForwardedClientCert> value) {
return value.stream().map(XForwardedClientCert::toString).collect(Collectors.joining(","));
}

@Override
public List<XForwardedClientCert> parseAsciiString(String serialized) {
return XfccParser.parse(serialized);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2017, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

package com.salesforce.grpc.contrib.xfcc;

import java.util.ArrayList;
import java.util.List;

/**
* {@code XfccParser} parses the {@code x-forwarded-client-cert} (XFCC) header populated by TLS-terminating
* reverse proxies.
*
* @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>
*/
final class XfccParser {
private XfccParser() { }

/**
* Given a header string, parse and return a collection of {@link XForwardedClientCert} objects.
*/
static List<XForwardedClientCert> parse(String header) {
List<XForwardedClientCert> certs = new ArrayList<>();

for (String element : quoteAwareSplit(header, ',')) {
XForwardedClientCert cert = new XForwardedClientCert();
List<String> kvps = quoteAwareSplit(element, ';');
for (String kvp : kvps) {
List<String> l = quoteAwareSplit(kvp, '=');

if (l.get(0).toLowerCase().equals("by")) {
cert.setBy(dequote(l.get(1)));
}
if (l.get(0).toLowerCase().equals("hash")) {
cert.setHash(dequote(l.get(1)));
}
// Use "SAN:" instead of "URI:" for backward compatibility with previous mesh proxy releases.
if (l.get(0).toLowerCase().equals("san") || l.get(0).toLowerCase().equals("uri")) {
cert.setSanUri(dequote(l.get(1)));
}
if (l.get(0).toLowerCase().equals("dns")) {
cert.addSanDns(dequote(l.get(1)));
}
if (l.get(0).toLowerCase().equals("subject")) {
cert.setSubject(dequote(l.get(1)));
}
}
certs.add(cert);
}

return certs;
}

// Break str into individual elements, splitting on delim (not in quotes)
private static List<String> quoteAwareSplit(String str, char delim) {
boolean inQuotes = false;
boolean inEscape = false;

List<String> elements = new ArrayList<>();
StringBuilder buffer = new StringBuilder();
for (char c : str.toCharArray()) {
if (c == delim && !inQuotes) {
elements.add(buffer.toString());
buffer = new StringBuilder();
inEscape = false;
continue;
}

if (c == '"') {
if (inQuotes) {
if (!inEscape) {
inQuotes = false;
}
} else {
inQuotes = true;

}
inEscape = false;
buffer.append(c);
continue;
}

if (c == '\\') {
if (!inEscape) {
inEscape = true;
buffer.append(c);
continue;
}
}

// all other characters
inEscape = false;
buffer.append(c);
}

if (inQuotes) {
throw new RuntimeException("Quoted string not closed");
}

elements.add(buffer.toString());

return elements;
}

// Remove leading and tailing unescaped quotes, remove escaping from escaped internal quotes
private static String dequote(String str) {
str = str.replace("\\\"", "\"");
if (str.startsWith("\"")) {
str = str.substring(1);
}
if (str.endsWith("\"")) {
str = str.substring(0, str.length() - 1);
}
return str;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2017, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

package com.salesforce.grpc.contrib.xfcc;

import io.grpc.*;

import java.util.ArrayList;
import java.util.List;

/**
* {@code XfccServerInterceptor} parses the {@code x-forwarded-client-cert} (XFCC) header populated by TLS-terminating
* reverse proxies. For example: Envoy, Istio, and Linkerd. If present, the parsed XFCC header is appended to the
* gRPC {@code Context}.
*
* @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>
* @see <a href="https://github.com/linkerd/linkerd/issues/1153">Linkerd XFCC Header</a>
*/
public final class XfccServerInterceptor implements ServerInterceptor {
private static final Metadata.Key<List<XForwardedClientCert>> XFCC_METADATA_KEY = Metadata.Key.of("x-forwarded-client-cert", new XfccMarshaller());

@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
Iterable<List<XForwardedClientCert>> values = headers.getAll(XFCC_METADATA_KEY);
if (values != null) {
List<XForwardedClientCert> xfccs = new ArrayList<>();
for (List<XForwardedClientCert> value : values) {
xfccs.addAll(value);
}

Context xfccContext = Context.current().withValue(XForwardedClientCert.XFCC_CONTEXT_KEY, xfccs);
return Contexts.interceptCall(xfccContext, call, headers, next);
} else {
return next.startCall(call, headers);
}
}
}
Loading