From 2fe120376838c7e8ee7ff227b2161dce1f5fd977 Mon Sep 17 00:00:00 2001 From: Ryan Michela Date: Mon, 20 Nov 2017 22:53:56 -0800 Subject: [PATCH 1/7] Added x-forwarded-client-cert server interceptor --- grpc-contrib/pom.xml | 16 ++ .../com/salesforce/grpc/contrib/Xfcc.g4 | 53 ++++++ .../interceptor/XfccServerInterceptor.java | 156 ++++++++++++++++++ .../XfccServerInterceptorTest.java | 140 ++++++++++++++++ pom.xml | 7 + 5 files changed, 372 insertions(+) create mode 100644 grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/Xfcc.g4 create mode 100644 grpc-contrib/src/main/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptor.java create mode 100644 grpc-contrib/src/test/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptorTest.java diff --git a/grpc-contrib/pom.xml b/grpc-contrib/pom.xml index d1857b60..2686b671 100644 --- a/grpc-contrib/pom.xml +++ b/grpc-contrib/pom.xml @@ -35,6 +35,10 @@ io.grpc grpc-context + + org.antlr + antlr4-runtime + com.google.code.gson gson @@ -99,6 +103,18 @@ + + org.antlr + antlr4-maven-plugin + ${antlr.version} + + + + antlr4 + + + + diff --git a/grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/Xfcc.g4 b/grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/Xfcc.g4 new file mode 100644 index 00000000..8d380adc --- /dev/null +++ b/grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/Xfcc.g4 @@ -0,0 +1,53 @@ +grammar Xfcc; + +/* + * Parser Rules for x-forwarded-client-cert HTTP header. + * https://www.envoyproxy.io/docs/envoy/latest/configuration/http_conn_man/headers.html#config-http-conn-man-headers-x-forwarded-client-cert + */ + +header : (element COMMA)* element ; +element : (kvp SEMOCOLON)* kvp ; +kvp : key value ; +key : BY | HASH | SAN | SUBJECT ; +value : TEXT | QUOTED_TEXT ; + + +/* + * Lexer Rules + */ + +fragment A : ('A'|'a') ; +fragment B : ('B'|'b') ; +fragment C : ('C'|'c') ; +fragment E : ('E'|'e') ; +fragment H : ('H'|'h') ; +fragment J : ('J'|'j') ; +fragment N : ('N'|'n') ; +fragment S : ('S'|'s') ; +fragment T : ('T'|'t') ; +fragment U : ('U'|'u') ; +fragment Y : ('Y'|'y') ; + +// Case insensitive keys +BY : B Y ; +HASH : H A S H ; +SAN : S A N ; +SUBJECT : S U B J E C T ; + +// Un-quoted text runs up to the next seperator +TEXT + : EQUALS ~[;=,]+ + {setText(getText().substring(1));} + ; + +// Quoted text runs up to the first unescaped closing quote +QUOTED_TEXT + : EQUALS QUOTE (~('"') | BACKSLASH QUOTE)* QUOTE + {setText(getText().substring(2, getText().length()-1).replace("\\\"", "\""));} + ; + +SEMOCOLON : ';' ; +COMMA : ',' ; +EQUALS : '=' ; +QUOTE : '"' ; +BACKSLASH : '\\' ; \ No newline at end of file diff --git a/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptor.java b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptor.java new file mode 100644 index 00000000..c4397cd3 --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptor.java @@ -0,0 +1,156 @@ +/* + * 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.interceptor; + +import com.google.common.annotations.VisibleForTesting; +import com.salesforce.grpc.contrib.XfccBaseListener; +import com.salesforce.grpc.contrib.XfccLexer; +import com.salesforce.grpc.contrib.XfccListener; +import com.salesforce.grpc.contrib.XfccParser; +import io.grpc.*; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTreeWalker; + +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, 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 class XfccServerInterceptor implements ServerInterceptor { + /** + * 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 static final Metadata.Key XFCC_METADATA_KEY = Metadata.Key.of("x-forwarded-client-cert", Metadata.ASCII_STRING_MARSHALLER); + + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + try { + Iterable values = headers.getAll(XFCC_METADATA_KEY); + if (values != null) { + List xfccs = new ArrayList<>(); + for (String value : values) { + xfccs.addAll(parse(value)); + } + + Context xfccContext = Context.current().withValue(XFCC_CONTEXT_KEY, xfccs); + return Contexts.interceptCall(xfccContext, call, headers, next); + } else { + return next.startCall(call, headers); + } + } catch (NoClassDefFoundError err) { + throw new RuntimeException("Likely missing optional dependency on org.antlr:antlr4-runtime:4.7", err); + } + } + + @VisibleForTesting + static List parse(String header) { + List clientCerts = new ArrayList<>(); + + XfccLexer lexer = new XfccLexer(CharStreams.fromString(header)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + XfccParser parser = new XfccParser(tokens); + + XfccParser.HeaderContext headerContext = parser.header(); + ParseTreeWalker walker = new ParseTreeWalker(); + + XfccListener listener = new XfccBaseListener() { + private XForwardedClientCert clientCert; + + @Override + public void enterElement(XfccParser.ElementContext ctx) { + clientCert = new XForwardedClientCert(); + } + + @Override + public void enterKvp(XfccParser.KvpContext ctx) { + XfccParser.KeyContext key = ctx.key(); + XfccParser.ValueContext value = ctx.value(); + + if (key.BY() != null) { + clientCert.by = value.getText(); + } + if (key.HASH() != null) { + clientCert.hash = value.getText(); + } + if (key.SAN() != null) { + clientCert.san = value.getText(); + } + if (key.SUBJECT() != null) { + clientCert.subject = value.getText(); + } + } + + @Override + public void exitElement(XfccParser.ElementContext ctx) { + clientCerts.add(clientCert); + clientCert = null; + } + }; + + walker.walk(listener, headerContext); + return clientCerts; + } + + /** + * 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 static class XForwardedClientCert { + private String by; + private String hash; + private String san; + private String subject; + + /** + * @return The Subject Alternative Name (SAN) of the current proxy’s certificate. + */ + public String getBy() { + return by; + } + + /** + * @return The SHA 256 diguest of the current client certificate. + */ + public String getHash() { + return hash; + } + + /** + * @return The SAN field (URI type) of the current client certificate. + */ + public String getSan() { + return san; + } + + /** + * @return The Subject field of the current client certificate. + */ + public String getSubject() { + return subject; + } + + @Override + public String toString() { + return "XForwardedClientCert{" + + "by='" + by + '\'' + + ", hash='" + hash + '\'' + + ", san='" + san + '\'' + + ", subject='" + subject + '\'' + + '}'; + } + } +} diff --git a/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptorTest.java b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptorTest.java new file mode 100644 index 00000000..93d0c4cb --- /dev/null +++ b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptorTest.java @@ -0,0 +1,140 @@ +/* + * 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.interceptor; + +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 parseSimpleHeaderWorks() { + String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + + "SAN=http://testclient.lyft.com"; + List certs = XfccServerInterceptor.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).getSan()).isEqualTo("http://testclient.lyft.com"); + assertThat(certs.get(0).getSubject()).isNull(); + } + @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 = XfccServerInterceptor.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 = XfccServerInterceptor.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 = XfccServerInterceptor.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 endToEndTest() { + AtomicReference> certs = new AtomicReference<>(); + + GreeterGrpc.GreeterImplBase svc = new GreeterGrpc.GreeterImplBase() { + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + certs.set(XfccServerInterceptor.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(XfccServerInterceptor.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"); + } +} diff --git a/pom.xml b/pom.xml index 412e47e8..89a55f88 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,7 @@ 2.7 0.9.4 4.2.0.RELEASE + 4.7 4.12 @@ -153,6 +154,12 @@ spring-webmvc ${spring.version} + + org.antlr + antlr4-runtime + ${antlr.version} + true + org.slf4j From 0ce21ba0b5941fdfc765d8d4616fd37eb4fc904d Mon Sep 17 00:00:00 2001 From: Ryan Michela Date: Tue, 21 Nov 2017 11:38:16 -0800 Subject: [PATCH 2/7] Refactor XFCC to use a stand-alone marshaller --- .../grpc/contrib/{ => xfcc}/Xfcc.g4 | 0 .../interceptor/XfccServerInterceptor.java | 156 ------------------ .../contrib/xfcc/XForwardedClientCert.java | 97 +++++++++++ .../grpc/contrib/xfcc/XfccMarshaller.java | 84 ++++++++++ .../contrib/xfcc/XfccServerInterceptor.java | 46 ++++++ .../grpc/contrib/xfcc/XfccMarshallerTest.java | 119 +++++++++++++ .../XfccServerInterceptorTest.java | 49 +----- 7 files changed, 349 insertions(+), 202 deletions(-) rename grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/{ => xfcc}/Xfcc.g4 (100%) delete mode 100644 grpc-contrib/src/main/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptor.java create mode 100644 grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XForwardedClientCert.java create mode 100644 grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccMarshaller.java create mode 100644 grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptor.java create mode 100644 grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccMarshallerTest.java rename grpc-contrib/src/test/java/com/salesforce/grpc/contrib/{interceptor => xfcc}/XfccServerInterceptorTest.java (60%) diff --git a/grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/Xfcc.g4 b/grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/xfcc/Xfcc.g4 similarity index 100% rename from grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/Xfcc.g4 rename to grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/xfcc/Xfcc.g4 diff --git a/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptor.java b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptor.java deleted file mode 100644 index c4397cd3..00000000 --- a/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptor.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.interceptor; - -import com.google.common.annotations.VisibleForTesting; -import com.salesforce.grpc.contrib.XfccBaseListener; -import com.salesforce.grpc.contrib.XfccLexer; -import com.salesforce.grpc.contrib.XfccListener; -import com.salesforce.grpc.contrib.XfccParser; -import io.grpc.*; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.tree.ParseTreeWalker; - -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, 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 class XfccServerInterceptor implements ServerInterceptor { - /** - * 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 static final Metadata.Key XFCC_METADATA_KEY = Metadata.Key.of("x-forwarded-client-cert", Metadata.ASCII_STRING_MARSHALLER); - - @Override - public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { - try { - Iterable values = headers.getAll(XFCC_METADATA_KEY); - if (values != null) { - List xfccs = new ArrayList<>(); - for (String value : values) { - xfccs.addAll(parse(value)); - } - - Context xfccContext = Context.current().withValue(XFCC_CONTEXT_KEY, xfccs); - return Contexts.interceptCall(xfccContext, call, headers, next); - } else { - return next.startCall(call, headers); - } - } catch (NoClassDefFoundError err) { - throw new RuntimeException("Likely missing optional dependency on org.antlr:antlr4-runtime:4.7", err); - } - } - - @VisibleForTesting - static List parse(String header) { - List clientCerts = new ArrayList<>(); - - XfccLexer lexer = new XfccLexer(CharStreams.fromString(header)); - CommonTokenStream tokens = new CommonTokenStream(lexer); - XfccParser parser = new XfccParser(tokens); - - XfccParser.HeaderContext headerContext = parser.header(); - ParseTreeWalker walker = new ParseTreeWalker(); - - XfccListener listener = new XfccBaseListener() { - private XForwardedClientCert clientCert; - - @Override - public void enterElement(XfccParser.ElementContext ctx) { - clientCert = new XForwardedClientCert(); - } - - @Override - public void enterKvp(XfccParser.KvpContext ctx) { - XfccParser.KeyContext key = ctx.key(); - XfccParser.ValueContext value = ctx.value(); - - if (key.BY() != null) { - clientCert.by = value.getText(); - } - if (key.HASH() != null) { - clientCert.hash = value.getText(); - } - if (key.SAN() != null) { - clientCert.san = value.getText(); - } - if (key.SUBJECT() != null) { - clientCert.subject = value.getText(); - } - } - - @Override - public void exitElement(XfccParser.ElementContext ctx) { - clientCerts.add(clientCert); - clientCert = null; - } - }; - - walker.walk(listener, headerContext); - return clientCerts; - } - - /** - * 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 static class XForwardedClientCert { - private String by; - private String hash; - private String san; - private String subject; - - /** - * @return The Subject Alternative Name (SAN) of the current proxy’s certificate. - */ - public String getBy() { - return by; - } - - /** - * @return The SHA 256 diguest of the current client certificate. - */ - public String getHash() { - return hash; - } - - /** - * @return The SAN field (URI type) of the current client certificate. - */ - public String getSan() { - return san; - } - - /** - * @return The Subject field of the current client certificate. - */ - public String getSubject() { - return subject; - } - - @Override - public String toString() { - return "XForwardedClientCert{" + - "by='" + by + '\'' + - ", hash='" + hash + '\'' + - ", san='" + san + '\'' + - ", subject='" + subject + '\'' + - '}'; - } - } -} 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..243c4b20 --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XForwardedClientCert.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 java.util.ArrayList; +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 class XForwardedClientCert { + private String by = ""; + private String hash = ""; + private String san = ""; + private String subject = ""; + + void setBy(String by) { + this.by = by; + } + + void setHash(String hash) { + this.hash = hash; + } + + void setSan(String san) { + this.san = san; + } + + void setSubject(String subject) { + this.subject = subject; + } + + /** + * @return The Subject Alternative Name (SAN) of the current proxy’s certificate. + */ + public String getBy() { + return by; + } + + /** + * @return The SHA 256 diguest of the current client certificate. + */ + public String getHash() { + return hash; + } + + /** + * @return The SAN field (URI type) of the current client certificate. + */ + public String getSan() { + return san; + } + + /** + * @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 (!san.isEmpty()) { + kvp.add("SAN=" + enquote(san)); + } + 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..7e781d0f --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccMarshaller.java @@ -0,0 +1,84 @@ +/* + * 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 org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTreeWalker; + +import java.util.ArrayList; +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 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) { + try { + List clientCerts = new ArrayList<>(); + + XfccLexer lexer = new XfccLexer(CharStreams.fromString(serialized)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + XfccParser parser = new XfccParser(tokens); + + XfccParser.HeaderContext headerContext = parser.header(); + ParseTreeWalker walker = new ParseTreeWalker(); + + XfccListener listener = new XfccBaseListener() { + private XForwardedClientCert clientCert; + + @Override + public void enterElement(XfccParser.ElementContext ctx) { + clientCert = new XForwardedClientCert(); + } + + @Override + public void enterKvp(XfccParser.KvpContext ctx) { + XfccParser.KeyContext key = ctx.key(); + XfccParser.ValueContext value = ctx.value(); + + if (key.BY() != null) { + clientCert.setBy(value.getText()); + } + if (key.HASH() != null) { + clientCert.setHash(value.getText()); + } + if (key.SAN() != null) { + clientCert.setSan(value.getText()); + } + if (key.SUBJECT() != null) { + clientCert.setSubject(value.getText()); + } + } + + @Override + public void exitElement(XfccParser.ElementContext ctx) { + clientCerts.add(clientCert); + clientCert = null; + } + }; + + walker.walk(listener, headerContext); + return clientCerts; + } catch (NoClassDefFoundError err) { + throw new RuntimeException("Likely missing optional dependency on org.antlr:antlr4-runtime:4.7", err); + } + } +} 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..33775acd --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptor.java @@ -0,0 +1,46 @@ +/* + * 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, 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 class XfccServerInterceptor implements ServerInterceptor { + /** + * 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 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(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..e3ed4c87 --- /dev/null +++ b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccMarshallerTest.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 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;" + + "SAN=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).getSan()).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;SAN=http://testclient.lyft.com," + + "By=http://backend.lyft.com;Hash=9ba61d6425303443c0748a02dd8de688468ed33be74eee6556d90c0149c1309e;SAN=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\";SAN=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\";SAN=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;" + + "SAN=http://testclient.lyft.com"; + + List certs = marshaller.parseAsciiString(header); + String serialized = marshaller.toAsciiString(certs); + + assertThat(serialized).isEqualTo(header); + } + + @Test + public void roundTripCompoundTest() { + XfccMarshaller marshaller = new XfccMarshaller(); + + 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 = 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;" + + "SAN=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;" + + "SAN=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/interceptor/XfccServerInterceptorTest.java b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptorTest.java similarity index 60% rename from grpc-contrib/src/test/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptorTest.java rename to grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptorTest.java index 93d0c4cb..d7ff3de3 100644 --- a/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/interceptor/XfccServerInterceptorTest.java +++ b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccServerInterceptorTest.java @@ -5,7 +5,7 @@ * 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.interceptor; +package com.salesforce.grpc.contrib.xfcc; import com.salesforce.grpc.contrib.GreeterGrpc; import com.salesforce.grpc.contrib.HelloRequest; @@ -26,52 +26,9 @@ public class XfccServerInterceptorTest { @Rule public final GrpcServerRule serverRule = new GrpcServerRule().directExecutor(); - @Test - public void parseSimpleHeaderWorks() { - String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + - "SAN=http://testclient.lyft.com"; - List certs = XfccServerInterceptor.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).getSan()).isEqualTo("http://testclient.lyft.com"); - assertThat(certs.get(0).getSubject()).isNull(); - } - @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 = XfccServerInterceptor.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 = XfccServerInterceptor.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 = XfccServerInterceptor.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 endToEndTest() { - AtomicReference> certs = new AtomicReference<>(); + AtomicReference> certs = new AtomicReference<>(); GreeterGrpc.GreeterImplBase svc = new GreeterGrpc.GreeterImplBase() { @Override @@ -103,7 +60,7 @@ public void sayHello(HelloRequest request, StreamObserver respons @Test public void endToEndTestMultiple() { - AtomicReference> certs = new AtomicReference<>(); + AtomicReference> certs = new AtomicReference<>(); GreeterGrpc.GreeterImplBase svc = new GreeterGrpc.GreeterImplBase() { @Override From f7e76e0cd5938d3e696e0bc86d8e3eb21f130bc7 Mon Sep 17 00:00:00 2001 From: Ryan Michela Date: Tue, 21 Nov 2017 13:12:10 -0800 Subject: [PATCH 3/7] Remove Antlr dependency with manual parsing --- grpc-contrib/pom.xml | 16 --- .../com/salesforce/grpc/contrib/xfcc/Xfcc.g4 | 53 -------- .../grpc/contrib/xfcc/XfccMarshaller.java | 54 +------- .../grpc/contrib/xfcc/XfccParser.java | 115 ++++++++++++++++++ .../grpc/contrib/xfcc/XfccParserTest.java | 68 +++++++++++ pom.xml | 7 -- 6 files changed, 184 insertions(+), 129 deletions(-) delete mode 100644 grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/xfcc/Xfcc.g4 create mode 100644 grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccParser.java create mode 100644 grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccParserTest.java diff --git a/grpc-contrib/pom.xml b/grpc-contrib/pom.xml index 2686b671..d1857b60 100644 --- a/grpc-contrib/pom.xml +++ b/grpc-contrib/pom.xml @@ -35,10 +35,6 @@ io.grpc grpc-context - - org.antlr - antlr4-runtime - com.google.code.gson gson @@ -103,18 +99,6 @@ - - org.antlr - antlr4-maven-plugin - ${antlr.version} - - - - antlr4 - - - - diff --git a/grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/xfcc/Xfcc.g4 b/grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/xfcc/Xfcc.g4 deleted file mode 100644 index 8d380adc..00000000 --- a/grpc-contrib/src/main/antlr4/com/salesforce/grpc/contrib/xfcc/Xfcc.g4 +++ /dev/null @@ -1,53 +0,0 @@ -grammar Xfcc; - -/* - * Parser Rules for x-forwarded-client-cert HTTP header. - * https://www.envoyproxy.io/docs/envoy/latest/configuration/http_conn_man/headers.html#config-http-conn-man-headers-x-forwarded-client-cert - */ - -header : (element COMMA)* element ; -element : (kvp SEMOCOLON)* kvp ; -kvp : key value ; -key : BY | HASH | SAN | SUBJECT ; -value : TEXT | QUOTED_TEXT ; - - -/* - * Lexer Rules - */ - -fragment A : ('A'|'a') ; -fragment B : ('B'|'b') ; -fragment C : ('C'|'c') ; -fragment E : ('E'|'e') ; -fragment H : ('H'|'h') ; -fragment J : ('J'|'j') ; -fragment N : ('N'|'n') ; -fragment S : ('S'|'s') ; -fragment T : ('T'|'t') ; -fragment U : ('U'|'u') ; -fragment Y : ('Y'|'y') ; - -// Case insensitive keys -BY : B Y ; -HASH : H A S H ; -SAN : S A N ; -SUBJECT : S U B J E C T ; - -// Un-quoted text runs up to the next seperator -TEXT - : EQUALS ~[;=,]+ - {setText(getText().substring(1));} - ; - -// Quoted text runs up to the first unescaped closing quote -QUOTED_TEXT - : EQUALS QUOTE (~('"') | BACKSLASH QUOTE)* QUOTE - {setText(getText().substring(2, getText().length()-1).replace("\\\"", "\""));} - ; - -SEMOCOLON : ';' ; -COMMA : ',' ; -EQUALS : '=' ; -QUOTE : '"' ; -BACKSLASH : '\\' ; \ No newline at end of file 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 index 7e781d0f..c21ae475 100644 --- 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 @@ -8,11 +8,7 @@ package com.salesforce.grpc.contrib.xfcc; import io.grpc.Metadata; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.tree.ParseTreeWalker; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -31,54 +27,6 @@ public String toAsciiString(List value) { @Override public List parseAsciiString(String serialized) { - try { - List clientCerts = new ArrayList<>(); - - XfccLexer lexer = new XfccLexer(CharStreams.fromString(serialized)); - CommonTokenStream tokens = new CommonTokenStream(lexer); - XfccParser parser = new XfccParser(tokens); - - XfccParser.HeaderContext headerContext = parser.header(); - ParseTreeWalker walker = new ParseTreeWalker(); - - XfccListener listener = new XfccBaseListener() { - private XForwardedClientCert clientCert; - - @Override - public void enterElement(XfccParser.ElementContext ctx) { - clientCert = new XForwardedClientCert(); - } - - @Override - public void enterKvp(XfccParser.KvpContext ctx) { - XfccParser.KeyContext key = ctx.key(); - XfccParser.ValueContext value = ctx.value(); - - if (key.BY() != null) { - clientCert.setBy(value.getText()); - } - if (key.HASH() != null) { - clientCert.setHash(value.getText()); - } - if (key.SAN() != null) { - clientCert.setSan(value.getText()); - } - if (key.SUBJECT() != null) { - clientCert.setSubject(value.getText()); - } - } - - @Override - public void exitElement(XfccParser.ElementContext ctx) { - clientCerts.add(clientCert); - clientCert = null; - } - }; - - walker.walk(listener, headerContext); - return clientCerts; - } catch (NoClassDefFoundError err) { - throw new RuntimeException("Likely missing optional dependency on org.antlr:antlr4-runtime:4.7", err); - } + 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..309252fe --- /dev/null +++ b/grpc-contrib/src/main/java/com/salesforce/grpc/contrib/xfcc/XfccParser.java @@ -0,0 +1,115 @@ +/* + * 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))); + } + if (l.get(0).toLowerCase().equals("san")) { + cert.setSan(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/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..e1621399 --- /dev/null +++ b/grpc-contrib/src/test/java/com/salesforce/grpc/contrib/xfcc/XfccParserTest.java @@ -0,0 +1,68 @@ +/* + * 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 parseSimpleHeaderWorks() { + 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).getSan()).isEqualTo("http://testclient.lyft.com"); + assertThat(certs.get(0).getSubject()).isEmpty(); + } + @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/pom.xml b/pom.xml index 89a55f88..412e47e8 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,6 @@ 2.7 0.9.4 4.2.0.RELEASE - 4.7 4.12 @@ -154,12 +153,6 @@ spring-webmvc ${spring.version} - - org.antlr - antlr4-runtime - ${antlr.version} - true - org.slf4j From 10fa8cc11c8ac38d914667b4b68776c5812976b6 Mon Sep 17 00:00:00 2001 From: Ryan Michela Date: Tue, 21 Nov 2017 14:59:54 -0800 Subject: [PATCH 4/7] Move XFCC_CONTEXT_KEY to XForwardedClientCert class --- .../salesforce/grpc/contrib/xfcc/XForwardedClientCert.java | 7 +++++++ .../grpc/contrib/xfcc/XfccServerInterceptor.java | 7 +------ .../grpc/contrib/xfcc/XfccServerInterceptorTest.java | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) 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 index 243c4b20..915c9941 100644 --- 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 @@ -7,6 +7,8 @@ package com.salesforce.grpc.contrib.xfcc; +import io.grpc.Context; + import java.util.ArrayList; import java.util.List; @@ -15,6 +17,11 @@ * clients or proxies that a request has flowed through, on its way from the client to the server. */ public 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 san = ""; 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 index 33775acd..71e5e364 100644 --- 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 @@ -21,11 +21,6 @@ * @see Linkerd XFCC Header */ public class XfccServerInterceptor implements ServerInterceptor { - /** - * 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 static final Metadata.Key> XFCC_METADATA_KEY = Metadata.Key.of("x-forwarded-client-cert", new XfccMarshaller()); @Override @@ -37,7 +32,7 @@ public ServerCall.Listener interceptCall(ServerCall responseObserver) { - certs.set(XfccServerInterceptor.XFCC_CONTEXT_KEY.get()); + certs.set(XForwardedClientCert.XFCC_CONTEXT_KEY.get()); responseObserver.onNext(HelloResponse.newBuilder().setMessage("Hello " + request.getName()).build()); responseObserver.onCompleted(); @@ -65,7 +65,7 @@ public void endToEndTestMultiple() { GreeterGrpc.GreeterImplBase svc = new GreeterGrpc.GreeterImplBase() { @Override public void sayHello(HelloRequest request, StreamObserver responseObserver) { - certs.set(XfccServerInterceptor.XFCC_CONTEXT_KEY.get()); + certs.set(XForwardedClientCert.XFCC_CONTEXT_KEY.get()); responseObserver.onNext(HelloResponse.newBuilder().setMessage("Hello " + request.getName()).build()); responseObserver.onCompleted(); From 9dbf83a085cfe2e81acb2ffc71ee8abb3db8b0f5 Mon Sep 17 00:00:00 2001 From: Ryan Michela Date: Fri, 18 May 2018 13:22:37 -0700 Subject: [PATCH 5/7] Bring up to date with latest XFCC implementation --- .../contrib/xfcc/XForwardedClientCert.java | 32 +++++++++---- .../grpc/contrib/xfcc/XfccParser.java | 8 +++- .../contrib/xfcc/XfccServerInterceptor.java | 2 +- .../grpc/contrib/xfcc/XfccMarshallerTest.java | 2 +- .../grpc/contrib/xfcc/XfccParserTest.java | 46 ++++++++++++++++++- 5 files changed, 75 insertions(+), 15 deletions(-) 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 index 915c9941..31c04960 100644 --- 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 @@ -10,6 +10,8 @@ import io.grpc.Context; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; /** @@ -24,7 +26,8 @@ public class XForwardedClientCert { private String by = ""; private String hash = ""; - private String san = ""; + private String sanUri = ""; + private List sanDns = new ArrayList<>(); private String subject = ""; void setBy(String by) { @@ -35,14 +38,18 @@ void setHash(String hash) { this.hash = hash; } - void setSan(String san) { - this.san = san; + 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. */ @@ -51,17 +58,24 @@ public String getBy() { } /** - * @return The SHA 256 diguest of the current client certificate. + * @return The SHA 256 digest of the current client certificate. */ public String getHash() { return hash; } /** - * @return The SAN field (URI type) of the current client certificate. + * @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 String getSan() { - return san; + public Collection getSanDns() { + return Collections.unmodifiableCollection(sanDns); } /** @@ -80,8 +94,8 @@ public String toString() { if (!hash.isEmpty()) { kvp.add("Hash=" + enquote(hash)); } - if (!san.isEmpty()) { - kvp.add("SAN=" + enquote(san)); + if (!sanUri.isEmpty()) { + kvp.add("URI=" + enquote(sanUri)); } if (!subject.isEmpty()) { kvp.add("Subject=" + enquote(subject)); 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 index 309252fe..8137987c 100644 --- 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 @@ -37,8 +37,12 @@ static List parse(String header) { if (l.get(0).toLowerCase().equals("hash")) { cert.setHash(dequote(l.get(1))); } - if (l.get(0).toLowerCase().equals("san")) { - cert.setSan(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))); 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 index 71e5e364..badabf7c 100644 --- 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 @@ -14,7 +14,7 @@ /** * {@code XfccServerInterceptor} parses the {@code x-forwarded-client-cert} (XFCC) header populated by TLS-terminating - * reverse proxies. For example, Istio and Linkerd. If present, the parsed XFCC header is appended to the + * 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 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 index e3ed4c87..3b510d7e 100644 --- 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 @@ -25,7 +25,7 @@ public void parseSimpleHeaderWorks() { 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).getSan()).isEqualTo("http://testclient.lyft.com"); + assertThat(certs.get(0).getSanUri()).isEqualTo("http://testclient.lyft.com"); assertThat(certs.get(0).getSubject()).isEmpty(); } @Test 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 index e1621399..408273c6 100644 --- 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 @@ -16,7 +16,7 @@ public class XfccParserTest { @Test - public void parseSimpleHeaderWorks() { + public void parseLegacySanHeaderWorks() { String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + "SAN=http://testclient.lyft.com"; List certs = XfccParser.parse(header); @@ -24,9 +24,51 @@ public void parseSimpleHeaderWorks() { 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).getSan()).isEqualTo("http://testclient.lyft.com"); + 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," + From ae199dcdad7461680fc8024f0349a0653bde5828 Mon Sep 17 00:00:00 2001 From: Ryan Michela Date: Fri, 18 May 2018 14:03:23 -0700 Subject: [PATCH 6/7] XFCC round trip fixes --- .../contrib/xfcc/XForwardedClientCert.java | 3 +++ .../grpc/contrib/xfcc/XfccMarshallerTest.java | 25 +++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) 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 index 31c04960..5474e788 100644 --- 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 @@ -97,6 +97,9 @@ public String toString() { 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)); } 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 index 3b510d7e..0ca0381a 100644 --- 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 @@ -19,7 +19,7 @@ public void parseSimpleHeaderWorks() { XfccMarshaller marshaller = new XfccMarshaller(); String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + - "SAN=http://testclient.lyft.com"; + "URI=http://testclient.lyft.com"; List certs = marshaller.parseAsciiString(header); assertThat(certs.size()).isEqualTo(1); @@ -32,8 +32,8 @@ public void parseSimpleHeaderWorks() { public void parseCompoundHeaderWorks() { XfccMarshaller marshaller = new XfccMarshaller(); - 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"; + 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); @@ -46,7 +46,7 @@ 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\";SAN=http://testclient.lyft.com"; + "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); @@ -58,7 +58,7 @@ 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\";SAN=http://testclient.lyft.com"; + "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); @@ -70,7 +70,7 @@ public void roundTripSimpleTest() { XfccMarshaller marshaller = new XfccMarshaller(); String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + - "SAN=http://testclient.lyft.com"; + "URI=http://testclient.lyft.com"; List certs = marshaller.parseAsciiString(header); String serialized = marshaller.toAsciiString(certs); @@ -78,12 +78,17 @@ public void roundTripSimpleTest() { 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;SAN=http://testclient.lyft.com," + - "By=http://backend.lyft.com;Hash=9ba61d6425303443c0748a02dd8de688468ed33be74eee6556d90c0149c1309e;SAN=http://frontend.lyft.com"; + 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); @@ -96,7 +101,7 @@ public void roundTripQuotedTest() { XfccMarshaller marshaller = new XfccMarshaller(); String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + - "SAN=http://testclient.lyft.com;Subject=\"/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client\""; + "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); @@ -109,7 +114,7 @@ public void roundTripEscapedQuotedTest() { XfccMarshaller marshaller = new XfccMarshaller(); String header = "By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;" + - "SAN=http://testclient.lyft.com;Subject=\"/C=US/ST=CA/L=\\\"San Francisco\\\"/OU=Lyft/CN=Test Client\""; + "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); From 0be24262b03893731ceb57bc06743f08219df720 Mon Sep 17 00:00:00 2001 From: Ryan Michela Date: Fri, 18 May 2018 14:10:49 -0700 Subject: [PATCH 7/7] Make classes final --- .../com/salesforce/grpc/contrib/xfcc/XForwardedClientCert.java | 2 +- .../java/com/salesforce/grpc/contrib/xfcc/XfccMarshaller.java | 2 +- .../com/salesforce/grpc/contrib/xfcc/XfccServerInterceptor.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index 5474e788..cfd8efa2 100644 --- 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 @@ -18,7 +18,7 @@ * 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 class XForwardedClientCert { +public final class XForwardedClientCert { /** * The metadata key used to access any present {@link XForwardedClientCert} objects. */ 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 index c21ae475..a4d4fce2 100644 --- 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 @@ -19,7 +19,7 @@ * @see Envoy XFCC Header * @see Linkerd XFCC Header */ -public class XfccMarshaller implements Metadata.AsciiMarshaller> { +public final class XfccMarshaller implements Metadata.AsciiMarshaller> { @Override public String toAsciiString(List value) { return value.stream().map(XForwardedClientCert::toString).collect(Collectors.joining(",")); 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 index badabf7c..72139a67 100644 --- 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 @@ -20,7 +20,7 @@ * @see Envoy XFCC Header * @see Linkerd XFCC Header */ -public class XfccServerInterceptor implements ServerInterceptor { +public final class XfccServerInterceptor implements ServerInterceptor { private static final Metadata.Key> XFCC_METADATA_KEY = Metadata.Key.of("x-forwarded-client-cert", new XfccMarshaller()); @Override