diff --git a/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XForwardedClientCert.java b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XForwardedClientCert.java new file mode 100644 index 00000000..cfd8efa2 --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XForwardedClientCert.java @@ -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> XFCC_CONTEXT_KEY = Context.key("x-forwarded-client-cert"); + + private String by = ""; + private String hash = ""; + private String sanUri = ""; + private List 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 getSanDns() { + return Collections.unmodifiableCollection(sanDns); + } + + /** + * @return The Subject field of the current client certificate. + */ + public String getSubject() { + return subject; + } + + @Override + public String toString() { + List 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; + } +} diff --git a/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccMarshaller.java b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccMarshaller.java new file mode 100644 index 00000000..a4d4fce2 --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccMarshaller.java @@ -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 Envoy XFCC Header + * @see Linkerd XFCC Header + */ +public final class XfccMarshaller implements Metadata.AsciiMarshaller> { + @Override + public String toAsciiString(List value) { + return value.stream().map(XForwardedClientCert::toString).collect(Collectors.joining(",")); + } + + @Override + public List parseAsciiString(String serialized) { + return XfccParser.parse(serialized); + } +} diff --git a/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccParser.java b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccParser.java new file mode 100644 index 00000000..8137987c --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccParser.java @@ -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 Envoy XFCC Header + */ +final class XfccParser { + private XfccParser() { } + + /** + * Given a header string, parse and return a collection of {@link XForwardedClientCert} objects. + */ + static List parse(String header) { + List certs = new ArrayList<>(); + + for (String element : quoteAwareSplit(header, ',')) { + XForwardedClientCert cert = new XForwardedClientCert(); + List kvps = quoteAwareSplit(element, ';'); + for (String kvp : kvps) { + List 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 quoteAwareSplit(String str, char delim) { + boolean inQuotes = false; + boolean inEscape = false; + + List 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; + } +} diff --git a/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptor.java b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptor.java new file mode 100644 index 00000000..72139a67 --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptor.java @@ -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 Envoy XFCC Header + * @see Linkerd XFCC Header + */ +public final class XfccServerInterceptor implements ServerInterceptor { + private static final Metadata.Key> XFCC_METADATA_KEY = Metadata.Key.of("x-forwarded-client-cert", new XfccMarshaller()); + + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + Iterable> values = headers.getAll(XFCC_METADATA_KEY); + if (values != null) { + List xfccs = new ArrayList<>(); + for (List 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); + } + } +} diff --git a/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccMarshallerTest.java b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccMarshallerTest.java new file mode 100644 index 00000000..0ca0381a --- /dev/null +++ b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccMarshallerTest.java @@ -0,0 +1,124 @@ +/* + * 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 org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class XfccMarshallerTest { + @Test + public void parseSimpleHeaderWorks() { + XfccMarshaller marshaller = new XfccMarshaller(); + + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "URI=http://testclient.lyft.com"; + List certs = marshaller.parseAsciiString(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getBy()).isEqualTo("http://frontend.lyft.com"); + assertThat(certs.get(0).getHash()).isEqualTo("468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"); + assertThat(certs.get(0).getSanUri()).isEqualTo("http://testclient.lyft.com"); + assertThat(certs.get(0).getSubject()).isEmpty(); + } + @Test + public void parseCompoundHeaderWorks() { + XfccMarshaller marshaller = new XfccMarshaller(); + + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;URI=http://testclient.lyft.com," + + "By=http://backend.lyft.com;Hash=9ba61d6425303443c0748a02dd8de688468ed33be74eee6556d90c0149c1309e;URI=http://frontend.lyft.com"; + List certs = marshaller.parseAsciiString(header); + + assertThat(certs.size()).isEqualTo(2); + assertThat(certs.get(0).getBy()).isEqualTo("http://frontend.lyft.com"); + assertThat(certs.get(1).getBy()).isEqualTo("http://backend.lyft.com"); + } + + @Test + public void quotedHeaderWorks() { + XfccMarshaller marshaller = new XfccMarshaller(); + + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "Subject=\"/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client\";URI=http://testclient.lyft.com"; + List certs = marshaller.parseAsciiString(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getSubject()).isEqualTo("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client"); + } + + @Test + public void escapedQuotedHeaderWorks() { + XfccMarshaller marshaller = new XfccMarshaller(); + + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "Subject=\"/C=US/ST=CA/L=\\\"San Francisco\\\"/OU=Lyft/CN=Test Client\";URI=http://testclient.lyft.com"; + List certs = marshaller.parseAsciiString(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getSubject()).isEqualTo("/C=US/ST=CA/L=\"San Francisco\"/OU=Lyft/CN=Test Client"); + } + + @Test + public void roundTripSimpleTest() { + XfccMarshaller marshaller = new XfccMarshaller(); + + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "URI=http://testclient.lyft.com"; + + List certs = marshaller.parseAsciiString(header); + String serialized = marshaller.toAsciiString(certs); + + assertThat(serialized).isEqualTo(header); + } + + @Test + public void roundTripUriAndDnsTest() { + + } + + @Test + public void roundTripCompoundTest() { + XfccMarshaller marshaller = new XfccMarshaller(); + + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;URI=http://testclient.lyft.com," + + "By=http://backend.lyft.com;Hash=9ba61d6425303443c0748a02dd8de688468ed33be74eee6556d90c0149c1309e;URI=http://frontend.lyft.com"; + + List certs = marshaller.parseAsciiString(header); + String serialized = marshaller.toAsciiString(certs); + + assertThat(serialized).isEqualTo(header); + } + + @Test + public void roundTripQuotedTest() { + XfccMarshaller marshaller = new XfccMarshaller(); + + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "URI=http://testclient.lyft.com;Subject=\"/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client\""; + + List certs = marshaller.parseAsciiString(header); + String serialized = marshaller.toAsciiString(certs); + + assertThat(serialized).isEqualTo(header); + } + + @Test + public void roundTripEscapedQuotedTest() { + XfccMarshaller marshaller = new XfccMarshaller(); + + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "URI=http://testclient.lyft.com;Subject=\"/C=US/ST=CA/L=\\\"San Francisco\\\"/OU=Lyft/CN=Test Client\""; + + List certs = marshaller.parseAsciiString(header); + String serialized = marshaller.toAsciiString(certs); + + assertThat(serialized).isEqualTo(header); + } +} diff --git a/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccParserTest.java b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccParserTest.java new file mode 100644 index 00000000..408273c6 --- /dev/null +++ b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccParserTest.java @@ -0,0 +1,110 @@ +/* + * 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 org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class XfccParserTest { + @Test + public void parseLegacySanHeaderWorks() { + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "SAN=http://testclient.lyft.com"; + List certs = XfccParser.parse(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getBy()).isEqualTo("http://frontend.lyft.com"); + assertThat(certs.get(0).getHash()).isEqualTo("468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"); + assertThat(certs.get(0).getSanUri()).isEqualTo("http://testclient.lyft.com"); + assertThat(certs.get(0).getSubject()).isEmpty(); + assertThat(certs.get(0).getSanDns()).isEmpty(); + } + + @Test + public void parseSimpleHeaderWorks() { + String header = "Hash=ebb216c5155a5fd8c8f082a07362b3c7b1a8ee2f98c20f6142b49fe5c2db90bd;DNS=test-tls-in;DNS=second-san;" + + "DNS=third-san;Subject=\"OU=0:test-tls-in,CN=localhost\""; + List certs = XfccParser.parse(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getBy()).isEmpty(); + assertThat(certs.get(0).getHash()).isEqualTo("ebb216c5155a5fd8c8f082a07362b3c7b1a8ee2f98c20f6142b49fe5c2db90bd"); + assertThat(certs.get(0).getSanUri()).isEmpty(); + assertThat(certs.get(0).getSubject()).isEqualTo("OU=0:test-tls-in,CN=localhost"); + assertThat(certs.get(0).getSanDns()).containsExactly("test-tls-in", "second-san", "third-san"); + } + + @Test + public void parseUriSanHeaderWorks() { + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject=\"/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client\";URI=http://testclient.lyft.com"; + List certs = XfccParser.parse(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getBy()).isEqualTo("http://frontend.lyft.com"); + assertThat(certs.get(0).getHash()).isEqualTo("468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"); + assertThat(certs.get(0).getSanUri()).isEqualTo("http://testclient.lyft.com"); + assertThat(certs.get(0).getSubject()).isEqualTo("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client"); + assertThat(certs.get(0).getSanDns()).isEmpty(); + } + + @Test + public void parseUriAndDnsSanHeaderWorks() { + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject=\"/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client\";URI=http://testclient.lyft.com;DNS=lyft.com;DNS=www.lyft.com"; + List certs = XfccParser.parse(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getBy()).isEqualTo("http://frontend.lyft.com"); + assertThat(certs.get(0).getHash()).isEqualTo("468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"); + assertThat(certs.get(0).getSanUri()).isEqualTo("http://testclient.lyft.com"); + assertThat(certs.get(0).getSubject()).isEqualTo("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client"); + assertThat(certs.get(0).getSanDns()).containsExactly("lyft.com", "www.lyft.com"); + } + + @Test + public void parseCompoundHeaderWorks() { + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;SAN=http://testclient.lyft.com," + + "By=http://backend.lyft.com;Hash=9ba61d6425303443c0748a02dd8de688468ed33be74eee6556d90c0149c1309e;SAN=http://frontend.lyft.com"; + List certs = XfccParser.parse(header); + + assertThat(certs.size()).isEqualTo(2); + assertThat(certs.get(0).getBy()).isEqualTo("http://frontend.lyft.com"); + assertThat(certs.get(1).getBy()).isEqualTo("http://backend.lyft.com"); + } + + @Test + public void quotedHeaderWorks() { + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "Subject=\"/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client\";SAN=http://testclient.lyft.com"; + List certs = XfccParser.parse(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getSubject()).isEqualTo("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client"); + } + + @Test + public void escapedQuotedHeaderWorks() { + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "Subject=\"/C=US/ST=CA/L=\\\"San Francisco\\\"/OU=Lyft/CN=Test Client\";SAN=http://testclient.lyft.com"; + List certs = XfccParser.parse(header); + + assertThat(certs.size()).isEqualTo(1); + assertThat(certs.get(0).getSubject()).isEqualTo("/C=US/ST=CA/L=\"San Francisco\"/OU=Lyft/CN=Test Client"); + } + + @Test + public void mismatchedQuotesThrows() { + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "Subject=\"/C=US/ST=CA/L=\\\"San Francisco\"/OU=Lyft/CN=Test Client\";SAN=http://testclient.lyft.com"; + + assertThatThrownBy(() -> XfccParser.parse(header)).isInstanceOf(RuntimeException.class); + } +} diff --git a/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptorTest.java b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptorTest.java new file mode 100644 index 00000000..5d4fbbd3 --- /dev/null +++ b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptorTest.java @@ -0,0 +1,97 @@ +/* + * 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 com.salesforce.grpc.contrib.GreeterGrpc; +import com.salesforce.grpc.contrib.HelloRequest; +import com.salesforce.grpc.contrib.HelloResponse; +import io.grpc.Metadata; +import io.grpc.ServerInterceptors; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcServerRule; +import org.junit.Rule; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +public class XfccServerInterceptorTest { + @Rule public final GrpcServerRule serverRule = new GrpcServerRule().directExecutor(); + + @Test + public void endToEndTest() { + AtomicReference> certs = new AtomicReference<>(); + + GreeterGrpc.GreeterImplBase svc = new GreeterGrpc.GreeterImplBase() { + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + certs.set(XForwardedClientCert.XFCC_CONTEXT_KEY.get()); + + responseObserver.onNext(HelloResponse.newBuilder().setMessage("Hello " + request.getName()).build()); + responseObserver.onCompleted(); + } + }; + + serverRule.getServiceRegistry().addService(ServerInterceptors.intercept(svc, new XfccServerInterceptor())); + + String xfcc = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;SAN=http://testclient.lyft.com," + + "By=http://backend.lyft.com;Hash=9ba61d6425303443c0748a02dd8de688468ed33be74eee6556d90c0149c1309e;SAN=http://frontend.lyft.com"; + + Metadata xfccHeader = new Metadata(); + xfccHeader.put(Metadata.Key.of("x-forwarded-client-cert", Metadata.ASCII_STRING_MARSHALLER), xfcc); + + GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(serverRule.getChannel()) + .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(xfccHeader)); + + stub.sayHello(HelloRequest.newBuilder().setName("World").build()); + + assertThat(certs.get().size()).isEqualTo(2); + assertThat(certs.get().get(0).getBy()).isEqualTo("http://frontend.lyft.com"); + assertThat(certs.get().get(1).getBy()).isEqualTo("http://backend.lyft.com"); + } + + @Test + public void endToEndTestMultiple() { + AtomicReference> certs = new AtomicReference<>(); + + GreeterGrpc.GreeterImplBase svc = new GreeterGrpc.GreeterImplBase() { + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + certs.set(XForwardedClientCert.XFCC_CONTEXT_KEY.get()); + + responseObserver.onNext(HelloResponse.newBuilder().setMessage("Hello " + request.getName()).build()); + responseObserver.onCompleted(); + } + }; + + serverRule.getServiceRegistry().addService(ServerInterceptors.intercept(svc, new XfccServerInterceptor())); + + String xfcc = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;SAN=http://testclient.lyft.com," + + "By=http://backend.lyft.com;Hash=9ba61d6425303443c0748a02dd8de688468ed33be74eee6556d90c0149c1309e;SAN=http://frontend.lyft.com"; + String xfcc2 = "By=http://middle.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "SAN=http://testclient.lyft.com"; + + Metadata xfccHeader = new Metadata(); + Metadata.Key key = Metadata.Key.of("x-forwarded-client-cert", Metadata.ASCII_STRING_MARSHALLER); + xfccHeader.put(key, xfcc); + xfccHeader.put(key, xfcc2); + + GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(serverRule.getChannel()) + .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(xfccHeader)); + + stub.sayHello(HelloRequest.newBuilder().setName("World").build()); + + assertThat(certs.get().size()).isEqualTo(3); + assertThat(certs.get().get(0).getBy()).isEqualTo("http://frontend.lyft.com"); + assertThat(certs.get().get(1).getBy()).isEqualTo("http://backend.lyft.com"); + assertThat(certs.get().get(2).getBy()).isEqualTo("http://middle.lyft.com"); + } +}