├── deploy ├── .gitignore ├── echod │ ├── templates │ │ ├── jwt-secret.yaml │ │ ├── svc.yaml │ │ ├── client-secret.yaml │ │ ├── server-secret.yaml │ │ └── deployment.yaml │ ├── Chart.yaml │ ├── values.yaml │ ├── .helmignore │ └── LICENSE └── README.md ├── .gitattributes ├── app ├── src │ ├── main │ │ ├── protobuf │ │ ├── scala │ │ │ ├── util │ │ │ │ ├── FileUtils.scala │ │ │ │ └── KeyUtils.scala │ │ │ ├── EchodApplication.scala │ │ │ ├── EchodModule.scala │ │ │ ├── services │ │ │ │ └── EchoService.scala │ │ │ ├── grpc │ │ │ │ ├── AccessTokenCallCredentials.scala │ │ │ │ ├── EchoClient.scala │ │ │ │ ├── EchoServer.scala │ │ │ │ └── UserContextServerInterceptor.scala │ │ │ └── models │ │ │ │ └── UserContext.scala │ │ └── resources │ │ │ ├── logback.xml │ │ │ └── application.conf │ └── test │ │ ├── resources │ │ ├── jwt │ │ │ ├── generate-test-jwt-signing-keys.sh │ │ │ ├── test-jwt-signing-key-public.pem │ │ │ └── test-jwt-signing-key-private.pem │ │ ├── ssl │ │ │ ├── generate-test-ssl-assets.sh │ │ │ ├── localhost-client-cert.pem │ │ │ ├── localhost-server-cert.pem │ │ │ ├── localhost-client-ca-cert.pem │ │ │ ├── localhost-server-ca-cert.pem │ │ │ ├── localhost-client-key.pem │ │ │ └── localhost-server-key.pem │ │ └── test.conf │ │ └── scala │ │ ├── BaseSpec.scala │ │ ├── EchodTestModule.scala │ │ ├── UserContextSpec.scala │ │ └── EchoServerSpec.scala ├── project │ ├── build.properties │ └── plugins.sbt ├── .scalafmt.conf ├── .gitignore ├── .editorconfig └── build.sbt ├── .editorconfig ├── gateway ├── .gitignore ├── Makefile ├── Dockerfile ├── install-gateway.sh └── src │ └── gateway │ └── main.go ├── TODO.md ├── protobuf ├── README.md └── src │ ├── echo.proto │ └── google │ ├── api │ ├── annotations.proto │ └── http.proto │ └── protobuf │ └── descriptor.proto ├── doc └── README.md ├── README.md ├── util ├── generate-jwt-signing-keys.sh └── generate-self-signed-ssl-assets.sh ├── Makefile └── LICENSE /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | protobuf/* linguist-vendored -------------------------------------------------------------------------------- /app/src/main/protobuf: -------------------------------------------------------------------------------- 1 | ../../../protobuf/src/ -------------------------------------------------------------------------------- /app/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.12 2 | -------------------------------------------------------------------------------- /app/.scalafmt.conf: -------------------------------------------------------------------------------- 1 | style = default 2 | maxColumn = 100 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [Makefile] 2 | indent_style = tab 3 | indent_size = 8 -------------------------------------------------------------------------------- /gateway/.gitignore: -------------------------------------------------------------------------------- 1 | src/github.com/ 2 | src/gateway/generated/ 3 | bin/ 4 | pkg/ 5 | .idea/ -------------------------------------------------------------------------------- /app/src/test/resources/jwt/generate-test-jwt-signing-keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ../../../../../util/generate-jwt-signing-keys.sh test-jwt-signing-key . 5 | 6 | -------------------------------------------------------------------------------- /app/src/test/resources/jwt/test-jwt-signing-key-public.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vyshane/grpc-scala-microservice-kit/HEAD/app/src/test/resources/jwt/test-jwt-signing-key-public.pem -------------------------------------------------------------------------------- /app/src/test/resources/jwt/test-jwt-signing-key-private.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vyshane/grpc-scala-microservice-kit/HEAD/app/src/test/resources/jwt/test-jwt-signing-key-private.pem -------------------------------------------------------------------------------- /deploy/echod/templates/jwt-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: jwt-verification 5 | labels: 6 | app: {{.Chart.Name}} 7 | data: 8 | jwt-verification-key: {{.Values.jwtVerificationKey}} -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * Add [Prometheus integration](https://github.com/prometheus/client_java) 4 | * Investigate [OpenTracing](https://github.com/opentracing/opentracing-java) integration 5 | * Write documentation 6 | * Makefile recipe to push to Helm repository? 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Java 2 | *.class 3 | 4 | # sbt 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # JetBrains/IntelliJ 16 | .idea/ 17 | .idea_modules/ 18 | 19 | # vim 20 | .*.sw[a-z] 21 | -------------------------------------------------------------------------------- /app/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | # Preserve trailing whitespace for Markdown files 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /deploy/echod/templates/svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{.Chart.Name}} 5 | labels: 6 | app: {{.Chart.Name}} 7 | spec: 8 | ports: 9 | - name: grpc 10 | port: 8443 11 | targetPort: grpc 12 | - name: json 13 | port: 9443 14 | targetPort: json 15 | selector: 16 | app: echod -------------------------------------------------------------------------------- /deploy/echod/templates/client-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{.Chart.Name}}-client 5 | labels: 6 | app: {{.Chart.Name}} 7 | data: 8 | {{.Chart.Name}}-client-cert: {{.Values.clientCert}} 9 | {{.Chart.Name}}-client-key: {{.Values.clientKey}} 10 | {{.Chart.Name}}-server-ca-cert: {{.Values.serverCaCert}} -------------------------------------------------------------------------------- /deploy/echod/templates/server-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{.Chart.Name}}-server 5 | labels: 6 | app: {{.Chart.Name}} 7 | data: 8 | {{.Chart.Name}}-server-cert: {{.Values.serverCert}} 9 | {{.Chart.Name}}-server-key: {{.Values.serverKey}} 10 | {{.Chart.Name}}-client-ca-cert: {{.Values.clientCaCert}} -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Deploying echod 2 | 3 | The [`echod/`](echod/) directory contains a [Helm chart](https://github.com/kubernetes/helm) for deploying echod to a [Kubernetes](http://kubernetes.io) cluster. 4 | 5 | To deploy echod to your current Kubernetes context, run the following command in the `deploy/` directory: 6 | 7 | ```text 8 | helm install echod 9 | ``` 10 | -------------------------------------------------------------------------------- /app/src/main/scala/util/FileUtils.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod.util 2 | 3 | import java.io.File 4 | 5 | trait FileUtils { 6 | def fileForAbsolutePath(path: String): File = new File(path) 7 | def fileForTestResourcePath(path: String): File = new File(pathForTestResourcePath(path)) 8 | def pathForTestResourcePath(path: String): String = getClass.getResource(path).getPath 9 | } 10 | -------------------------------------------------------------------------------- /app/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.4") 2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.4.0") 3 | addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.4.1") 4 | 5 | // gRPC and Protocol Buffers 6 | addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.1") 7 | libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.5.43" 8 | -------------------------------------------------------------------------------- /app/src/main/scala/EchodApplication.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod 2 | 3 | import org.slf4j.LoggerFactory 4 | 5 | /* 6 | * Main entry point for the application 7 | */ 8 | object EchodApplication extends EchodModule { 9 | def main(args: Array[String]): Unit = { 10 | LoggerFactory.getLogger(this.getClass).info("Starting gRPC server") 11 | echoServer.start().awaitTermination() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /deploy/echod/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: echod 2 | version: 1.0.0-SNAPSHOT 3 | description: A starter kit for building microservices using gRPC and Scala 4 | keywords: 5 | - Scala 6 | - gRPC 7 | home: https://github.com/vyshane/grpc-scala-microservice-kit 8 | sources: 9 | - https://github.com/vyshane/grpc-scala-microservice-kit 10 | maintainers: 11 | - name: Vy-Shane Xie 12 | email: shane@node.mu 13 | -------------------------------------------------------------------------------- /app/src/test/resources/ssl/generate-test-ssl-assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ../../../../../util/generate-self-signed-ssl-assets.sh localhost . 5 | 6 | mv localhost-key.pem localhost-server-key.pem 7 | mv localhost-cert.pem localhost-server-cert.pem 8 | mv localhost-ca-cert.pem localhost-server-ca-cert.pem 9 | 10 | ../../../../../util/generate-self-signed-ssl-assets.sh localhost-client . 11 | 12 | -------------------------------------------------------------------------------- /deploy/echod/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for echod. 2 | # This is a YAML-formatted file. 3 | # Declare name/value pairs to be passed into your templates. 4 | project: grpc-scala-microservices-kit 5 | imageRepository: vyshane 6 | version: 1.0.0-SNAPSHOT 7 | replicas: 1 8 | jwtVerificationSecret: jwt-verification 9 | corsAllowedOrigins: "*, http://localhost:3000, https://node.mu" # Development environment example 10 | -------------------------------------------------------------------------------- /gateway/Makefile: -------------------------------------------------------------------------------- 1 | APP=echod 2 | DOCKER_REPOSITORY=vyshane 3 | PROTO_FILE=echo.proto 4 | 5 | docker: clean 6 | mkdir -p src/gateway/generated/$(APP)/ 7 | cp ../protobuf/src/$(PROTO_FILE) src/gateway/generated/$(APP)/ 8 | docker build -t $(DOCKER_REPOSITORY)/$(APP)-gateway:1.0.0-SNAPSHOT . 9 | 10 | push: 11 | docker push $(DOCKER_REPOSITORY)/$(APP)-gateway:1.0.0-SNAPSHOT 12 | 13 | clean: 14 | rm -rf src/gateway/generated/ -------------------------------------------------------------------------------- /deploy/echod/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /protobuf/README.md: -------------------------------------------------------------------------------- 1 | # Protocol Buffers 2 | 3 | In practice, protobufs are likely to be from a shared project. Perhaps you want to import your protobufs into your project as a Git submodule and symlink specific files into the Scala app. 4 | 5 | Make sure that the relevant protobufs are also copied into the gateway. You can do that through the gateway's [Makefile](../gateway/Makefile). Symlinks won't work here because Dockerfiles don't support COPYing symlinked resources. -------------------------------------------------------------------------------- /app/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.out 5 | 6 | %d{YYYY-MM-DD HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/test/scala/BaseSpec.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod 2 | 3 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} 4 | import org.scalatest.concurrent.ScalaFutures 5 | import org.scalatest.time.{Millis, Seconds, Span} 6 | 7 | class BaseSpec 8 | extends WordSpec 9 | with EchodTestModule 10 | with Matchers 11 | with ScalaFutures 12 | with BeforeAndAfterAll { 13 | 14 | implicit override val patienceConfig = 15 | PatienceConfig(timeout = Span(5, Seconds), interval = Span(50, Millis)) 16 | } 17 | -------------------------------------------------------------------------------- /protobuf/src/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option java_package = "mu.node.echo"; 3 | option java_multiple_files = true; 4 | import "google/api/annotations.proto"; 5 | 6 | package echo; 7 | 8 | service EchoService { 9 | rpc Send (SendMessageRequest) returns (Message) { 10 | option (google.api.http) = { 11 | post: "/api/v1/echo" 12 | }; 13 | } 14 | } 15 | 16 | message SendMessageRequest { 17 | string content = 1; 18 | } 19 | 20 | message Message { 21 | string message_id = 1; 22 | string sender_id = 2; 23 | string content = 3; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## gRPC + Protocol Buffers 4 | 5 | * [What is gRPC?](http://www.grpc.io/docs/guides/index.html) 6 | * [What are Protocol Buffers?](https://developers.google.com/protocol-buffers/docs/overview) 7 | 8 | ## Why Use a Makefile? 9 | 10 | Microservices are often written by different teams using different technologies. A Makefile answers common questions such as: 11 | 12 | * How do I build this application? 13 | * How do I run tests? 14 | 15 | The Makefile can also be used as a self-documenting and stable interface for the CI system. 16 | 17 | -------------------------------------------------------------------------------- /app/src/test/scala/EchodTestModule.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import mu.node.echod.grpc.EchoServer 5 | import mu.node.echod.util.FileUtils 6 | 7 | /* 8 | * Test dependencies 9 | */ 10 | trait EchodTestModule extends EchodModule with FileUtils { 11 | override lazy val config = ConfigFactory.load("test") 12 | override lazy val jwtVerificationKey = loadX509PublicKey( 13 | pathForTestResourcePath(config.getString("jwt.signature-verification-key"))) 14 | override lazy val echoServer = 15 | EchoServer.build(config, echoService, userContextServerInterceptor, fileForTestResourcePath) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | server-port = 8443 2 | server-port = ${?SERVER_PORT} 3 | 4 | ssl { 5 | // Filesystem path of the SSL certificate used to encrypt connections to this service 6 | server-certificate = ${?SSL_SERVER_CERTIFICATE} 7 | // Filesystem path of the private key for the SSL certificate 8 | server-private-key = ${?SSL_SERVER_PRIVATE_KEY} 9 | // Filesystem path of the CA certificate for the client certificate that authorises connections to this service 10 | client-ca-certificate = ${?SSL_CLIENT_CA_CERTIFICATE} 11 | } 12 | 13 | jwt { 14 | // Filesystem path of the public key used to verify JSON Web Tokens 15 | signature-verification-key = ${?JWT_SIGNATURE_VERIFICATION_KEY} 16 | } 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/scala/EchodModule.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import mu.node.echod.util.KeyUtils 5 | import mu.node.echod.grpc.{EchoServer, UserContextServerInterceptor} 6 | import mu.node.echod.services.EchoService 7 | 8 | /* 9 | * Application dependencies 10 | */ 11 | trait EchodModule extends KeyUtils { 12 | lazy val config = ConfigFactory.load() 13 | lazy val echoService = new EchoService 14 | lazy val jwtVerificationKey = loadX509PublicKey( 15 | config.getString("jwt.signature-verification-key")) 16 | lazy val userContextServerInterceptor = new UserContextServerInterceptor(jwtVerificationKey) 17 | lazy val echoServer = EchoServer.build(config, echoService, userContextServerInterceptor) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/scala/services/EchoService.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod.services 2 | 3 | import java.util.UUID 4 | 5 | import io.grpc.Status 6 | import mu.node.echo.{EchoServiceGrpc, Message, SendMessageRequest} 7 | import mu.node.echod.grpc.UserContextServerInterceptor 8 | 9 | import scala.concurrent.Future 10 | 11 | class EchoService extends EchoServiceGrpc.EchoService { 12 | 13 | override def send(request: SendMessageRequest): Future[Message] = { 14 | Option(UserContextServerInterceptor.userContextKey.get) match { 15 | case Some(userContext) => 16 | Future.successful(Message(UUID.randomUUID().toString, userContext.userId, request.content)) 17 | case None => Future.failed(Status.UNAUTHENTICATED.asException()) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/test/resources/test.conf: -------------------------------------------------------------------------------- 1 | // Test configuration 2 | include "application" 3 | 4 | server-port = 9443 5 | 6 | ssl { 7 | // Paths are relative to the src/test/resources 8 | // Server-side 9 | server-certificate = "/ssl/localhost-server-cert.pem" 10 | server-private-key = "/ssl/localhost-server-key.pem" 11 | client-ca-certificate = "/ssl/localhost-client-ca-cert.pem" 12 | // Client-side 13 | client-certificate = "/ssl/localhost-client-cert.pem" 14 | client-private-key = "/ssl/localhost-client-key.pem" 15 | server-ca-certificate = "/ssl/localhost-server-ca-cert.pem" 16 | } 17 | 18 | jwt { 19 | // Paths are relative to the src/test/resources 20 | signature-verification-key = "/jwt/test-jwt-signing-key-public.pem" 21 | signing-key = "/jwt/test-jwt-signing-key-private.pem" 22 | } 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/scala/util/KeyUtils.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod.util 2 | 3 | import java.nio.file.{Files, Paths} 4 | import java.security.{KeyFactory, PrivateKey, PublicKey} 5 | import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} 6 | 7 | import pdi.jwt.JwtAlgorithm.RS256 8 | 9 | trait KeyUtils { 10 | 11 | val jwtDsa = RS256 12 | 13 | def loadPkcs8PrivateKey(path: String): PrivateKey = { 14 | val keyBytes = Files.readAllBytes(Paths.get(path)) 15 | val spec = new PKCS8EncodedKeySpec(keyBytes) 16 | KeyFactory.getInstance("RSA").generatePrivate(spec) 17 | } 18 | 19 | def loadX509PublicKey(path: String): PublicKey = { 20 | val keyBytes = Files.readAllBytes(Paths.get(path)) 21 | val spec = new X509EncodedKeySpec(keyBytes) 22 | KeyFactory.getInstance("RSA").generatePublic(spec) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.7 2 | MAINTAINER shane@node.mu 3 | ENV PROTOBUF_VERSION 3.1.0 4 | 5 | RUN set -x && \ 6 | apt-get -qq update && \ 7 | DEBIAN_FRONTEND=noninteractive apt-get -yq install unzip && \ 8 | apt-get clean && \ 9 | rm -rf /var/lib/apt/lists/* 10 | 11 | # Install protoc 12 | RUN mkdir -p /tmp/protobuf/ && \ 13 | wget -O /tmp/protobuf.zip \ 14 | https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip && \ 15 | unzip /tmp/protobuf.zip -d /tmp/protobuf/ && \ 16 | mv /tmp/protobuf/bin/protoc /usr/bin/ 17 | 18 | # Install the proto files that shipped with protoc 19 | RUN mkdir -p /go/src/gateway/generated/ && \ 20 | cp -r /tmp/protobuf/include/. /go/src/gateway/generated/echod/ 21 | 22 | COPY install-gateway.sh /go/install-gateway.sh 23 | COPY src/gateway/ /go/src/gateway/ 24 | 25 | WORKDIR /go/ 26 | RUN ./install-gateway.sh 27 | CMD ["gateway"] -------------------------------------------------------------------------------- /app/src/main/scala/grpc/AccessTokenCallCredentials.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod.grpc 2 | 3 | import java.util.concurrent.Executor 4 | 5 | import io.grpc.{Attributes, CallCredentials, Metadata, MethodDescriptor} 6 | 7 | class AccessTokenCallCredentials(accessToken: String) extends CallCredentials { 8 | 9 | override def applyRequestMetadata(method: MethodDescriptor[_, _], 10 | attributes: Attributes, 11 | appExecutor: Executor, 12 | applier: CallCredentials.MetadataApplier): Unit = { 13 | appExecutor.execute(new Runnable { 14 | override def run(): Unit = { 15 | val headers = new Metadata() 16 | val authorizationHeaderKey = 17 | Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER) 18 | headers.put(authorizationHeaderKey, "Bearer " + accessToken) 19 | applier.apply(headers) 20 | } 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /protobuf/src/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option java_multiple_files = true; 23 | option java_outer_classname = "AnnotationsProto"; 24 | option java_package = "com.google.api"; 25 | 26 | extend google.protobuf.MethodOptions { 27 | // See `HttpRule`. 28 | HttpRule http = 72295728; 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/scala/grpc/EchoClient.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod.grpc 2 | 3 | import java.io.File 4 | 5 | import com.typesafe.config.Config 6 | import io.grpc.netty.{GrpcSslContexts, NegotiationType, NettyChannelBuilder} 7 | import mu.node.echo.EchoServiceGrpc 8 | import mu.node.echod.util.FileUtils 9 | 10 | object EchoClient extends FileUtils { 11 | 12 | def buildServiceStub(config: Config, 13 | fileForConfiguredPath: (String) => File = fileForAbsolutePath) 14 | : EchoServiceGrpc.EchoServiceStub = { 15 | 16 | val sslContext = GrpcSslContexts 17 | .forClient() 18 | .keyManager(fileForConfiguredPath(config.getString("ssl.client-certificate")), 19 | fileForConfiguredPath(config.getString("ssl.client-private-key"))) 20 | .trustManager(fileForConfiguredPath(config.getString("ssl.server-ca-certificate"))) 21 | .build() 22 | 23 | val channel = NettyChannelBuilder 24 | .forAddress("localhost", config.getInt("server-port")) 25 | .negotiationType(NegotiationType.TLS) 26 | .sslContext(sslContext) 27 | .build() 28 | 29 | EchoServiceGrpc.stub(channel) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/test/resources/ssl/localhost-client-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC3zCCAcegAwIBAgIJALQpy5ubjZwfMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV 3 | BAMTCm92ZXJsYXAtY2EwHhcNMTYxMDA1MTMxMTA2WhcNNDQwMjIxMTMxMTA2WjAX 4 | MRUwEwYDVQQDDAxybWJwLTIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQDsmqtyKUaEW51w6E4ZHxZ9p9RKSYjfZEYLRjK2wEWhfkO4/ib8RlRN 6 | TjhPz1uy1VhujzRqe7B/jLZGCfreMhgiWbn6H0kHj44hsd45Aav7hRCyKB58AsuQ 7 | ZL64r8KdGrxJMBy7A23GdpzujdZLLFvfCfcs53F44q4V04y1CrGop/2zpUY00CRL 8 | GXbpzfaQwvybLoeddseKFbeeJF6+FywKTkWgLV+EDZuE2QfyJ7aI+VpOV3Miwa4X 9 | wlz/rqbrq1VSOurjNFSukljLn15j2P6tp6MsEpiDFxPXgPrw2sEcxvJiq9i3GSyt 10 | 8fwoF+lfvUQe4bKEs/SpJe3ru8fJMzOpAgMBAAGjMDAuMAkGA1UdEwQCMAAwCwYD 11 | VR0PBAQDAgXgMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQUFAAOC 12 | AQEAY7DMxBBwqAys1iO+v2Fmh6advMYGG+muFpnzIGS112QLYKwtdd/ukk9WSzOi 13 | uBr6u4WnZG5E0PVY/QK3u4qGdZYiRu5aeZLaLU1c/uqyMTRP+/axxW5PFjhE9a3V 14 | sDkasGfZWE/XhLfzzzPcCezTL3AAyv24Fo0n40vu82M14PR/+rvhPZrRm8io+l/h 15 | g3TAR4yfoo7560n/+1Dyp2YeEKzcHnObBVcD2XJ7bW1fti+5PoINq1hRdAYp1j3A 16 | tvnqHpp7A9+6yR4bjGNt+5Eeoon8zSoVhvpbI6Q8/YNhQjP1AyJHvonY5Ms3eHqI 17 | vBZGBjNLtl3hDQGygOX79qCr3g== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /app/src/test/resources/ssl/localhost-server-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC3zCCAcegAwIBAgIJAKzOV9l7h3kwMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV 3 | BAMTCm92ZXJsYXAtY2EwHhcNMTYxMDA1MTMxMTA2WhcNNDQwMjIxMTMxMTA2WjAX 4 | MRUwEwYDVQQDDAxybWJwLTIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQDEkWdkcIzsh28shY8DwRXA5/4YPWSItENPsJB5299HNmmk2OHQ++Dh 6 | HbaSKWNA+3FDbnj0YKLtOPvhlkcyklpbIvqaI+ZZvgue5E6Q5ILR5L4BagLe8z0g 7 | 8HRKrXd1hDBAnpIe2wrLxW20S8h+YOUNPS7k113VfV0yro1kMj98cgrbqqleYXmM 8 | zw6gnJKGxWlBTWZqkuEe9LBvfUdZaC6rni8uIyNxuyx/M9FtCAxsCTa1imvuFxyC 9 | LOtOYfbZEjGuyR5TchjtoosYQNarG2+8+HAwYKMEebmCbjyEcqNa+KeZQN3AtyFk 10 | BqBNj050ej2uMsYFuke4GkQbncaHT+bNAgMBAAGjMDAuMAkGA1UdEwQCMAAwCwYD 11 | VR0PBAQDAgXgMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQUFAAOC 12 | AQEAPUzqWS7XydgpbYreTEEN8T8Jytmzq+jtTUNtUGfcTreRbI1RKDydevlYPbm+ 13 | 136oNmDW1NoJ4ejOrlc3/kl5IAn/obZJdzTiFSn8ek6RZ+huyIkpw+79+gIlhBOz 14 | 1ukIwAI3VV1VDkVKtOasb1Xya5h+qkPHWZuqCoXLKPTBeM3qFBjX6fUeNv2BLZaQ 15 | CQsgXZh4d3QVDvQf/Keg8mwbj7laqY055tPF3GW4mlvh8t3314ay7JebZ6QOjRc5 16 | Lzs9O/yA7pGqW36nc7KOGboth/n+dJTTD4e2g4/me6SI5c7+WNYMoPDTMLBu/j3G 17 | EhqJvEGW0sESeFojg+v951oWIw== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /app/src/main/scala/models/UserContext.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod.models 2 | 3 | import java.security.{PrivateKey, PublicKey} 4 | import java.util.Calendar 5 | 6 | import mu.node.echod.util.KeyUtils 7 | import pdi.jwt.Jwt 8 | import play.api.libs.json.Json 9 | 10 | import scala.util.Try 11 | 12 | case class UserContext(userId: String) extends KeyUtils { 13 | 14 | def toJwt(expiryMillis: Long, jwtSigningKey: PrivateKey): String = { 15 | val json = 16 | s"""{ 17 | | "sub": "$userId", 18 | | "exp": $expiryMillis 19 | |} 20 | |""".stripMargin 21 | Jwt.encode(json, jwtSigningKey, jwtDsa) 22 | } 23 | } 24 | 25 | object UserContext extends KeyUtils { 26 | def fromJwt(jwt: String, jwtVerificationKey: PublicKey): Option[UserContext] = { 27 | Jwt 28 | .decode(jwt, jwtVerificationKey, Seq(jwtDsa)) 29 | .flatMap(payload => Try(Json.parse(payload))) 30 | .toOption 31 | .filter(json => (json \ "exp").asOpt[Long].exists(notExpired)) 32 | .flatMap(json => (json \ "sub").asOpt[String].map(UserContext(_))) 33 | } 34 | 35 | private def notExpired(expiryMillis: Long): Boolean = 36 | expiryMillis > Calendar.getInstance().getTimeInMillis 37 | } 38 | -------------------------------------------------------------------------------- /app/src/test/resources/ssl/localhost-client-ca-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDIzCCAgugAwIBAgIJALCZxp4byS91MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV 3 | BAMTCm92ZXJsYXAtY2EwHhcNMTYxMDA1MTMxMTA2WhcNNDQwMjIxMTMxMTA2WjAV 4 | MRMwEQYDVQQDEwpvdmVybGFwLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEAomneBxBcSFV7Qat1SbK7L1kB9X4dlAUR61fZ4FiuJ7ZGjKkcwSqEbnCS 6 | iVWlGxadNuWFedc87qWeb8feK5JO56PD2+09Xt2c3Jzs6Du7aekdEPRlGH+vXNZP 7 | jc2xSgDs/6l/ArTlLvB6718PB5WYMNIMhWR3y/TxNVxl4QwzGbc9VQ1e2KOB+zhv 8 | lCjcfhpLeJSV4ATZVs7/ORdpv5M4JJMAoDebpwtWqrQFPzrh8YwzRQ3R3lqqZRki 9 | 218ldxENIrwVVo3i5dilyqHoaou2Dwq9sUcRJiQmVPeex+yBwQ51YvJhdkGvV3Ls 10 | BeSd14UoLV7rVao/6XtzlrNCxjgeCwIDAQABo3YwdDAdBgNVHQ4EFgQUF8Qj8ub4 11 | CJIJjcN9URHIdqUQPIIwRQYDVR0jBD4wPIAUF8Qj8ub4CJIJjcN9URHIdqUQPIKh 12 | GaQXMBUxEzARBgNVBAMTCm92ZXJsYXAtY2GCCQCwmcaeG8kvdTAMBgNVHRMEBTAD 13 | AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBzXJLwOqgtVSRoSV4fZUhyRkQf7O5WYwQu 14 | 001LtRd0IBkdFx5ZjjxvcyeKeKd88285QWq1+EvqZFJcbI5ifYrJK6rips9fbMCi 15 | bv0lYPm5x7+Nz7MKNWZfeNz7ADz8EqYMKFBytSp24m+o3awm1siw05eOcvlfGVsu 16 | qzobNesHucke8CX8fdOHjgbIZNleLVGSUijyvnW1wEDPZCfJLq059HnWuI0B9Jkf 17 | luioYQJG049vxDV7mvFk/y+sVrRHFQWdpq71QYNO+SSEwEXED4pkQnlgO6BB+Usx 18 | qQqIfEnV2uVxMZunqTe2Ez/cjE9rrnz1gOk0CVRt6JrjPia+GGBH 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /app/src/test/resources/ssl/localhost-server-ca-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDIzCCAgugAwIBAgIJALtSJs5wyvL8MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV 3 | BAMTCm92ZXJsYXAtY2EwHhcNMTYxMDA1MTMxMTA1WhcNNDQwMjIxMTMxMTA1WjAV 4 | MRMwEQYDVQQDEwpvdmVybGFwLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEAuykSVb+8GWOOQJvEY9IuoBMaKGEagqNgUN396vzCXdkcINVnbsTdlAPf 6 | /hobxrkh3FMaF2rfLdcjZjW53NZACKumDkhgYi3iqO/bxlGItf6fQiK/z3qdMJWR 7 | GHVuEPYM4ulgxCJMfpK6JgfocMkzhKaNRlaoAcmCqZBBm8ooFzOgNkHMwba7leP6 8 | 33aiTSUKitDAPyia1VPjjuh/+aqj1YfJ6sjNJDzoDt9Yf0+3BZAu0NsonI8r59KP 9 | rebL29RZNwIQsOVXENYXtcPGFBMX3/fCcJp8SSvd8rjsFcyGl9lhkEfgkWMepEsn 10 | /o3Y3hRqcJI7NKmce3vWr5wCLb+3cQIDAQABo3YwdDAdBgNVHQ4EFgQUhJRb79yx 11 | wzVOl/M/yzjmDAqGZt0wRQYDVR0jBD4wPIAUhJRb79yxwzVOl/M/yzjmDAqGZt2h 12 | GaQXMBUxEzARBgNVBAMTCm92ZXJsYXAtY2GCCQC7UibOcMry/DAMBgNVHRMEBTAD 13 | AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAUzAuikmmreZz/9ZRs4adkk08LL6FliFjb 14 | cwxaFiPQSKvQqy+EFXRZcZoPQN6KVy+b+V711dQaepp+eMlAKGfxALp88H/FN/oH 15 | haxjFYlvyIIMNhcNazUCoIqYkTmeMl8MuZEwsBvNFXcNoBRjXp8/z21xO0hfF3H/ 16 | EMROtul0bLgchvlRIK+7y3bFHARaitXrrVnPsB+ICSUn5kvsuGF2bXvJvHHwrGW6 17 | XtlaER1+1fQ9y1514rZ0aCpLXdYfuzTL6m0fSXKpG/zRInX5NZNtNuOGHSrdOcFc 18 | baa6myco70eBKz81moQ8mpTNbUzIqdV8aDbaibb/gLjplMqB3+/l 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /gateway/install-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP=echod 3 | GOPATH=$(pwd):$GOPATH 4 | 5 | set -e 6 | 7 | go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway 8 | go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger 9 | go get -u github.com/golang/protobuf/protoc-gen-go 10 | 11 | function generate_stubs { 12 | local proto_file=$1 13 | # gRPC stub 14 | protoc -I. \ 15 | -I$GOPATH/src \ 16 | -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 17 | --go_out=Mgoogle/api/annotations.proto=github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api,plugins=grpc:. \ 18 | $proto_file 19 | # Reverse proxy 20 | protoc -I. \ 21 | -I$GOPATH/src \ 22 | -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 23 | --grpc-gateway_out=logtostderr=true:. \ 24 | $proto_file 25 | # Swagger definitions 26 | protoc -I. \ 27 | -I$GOPATH/src \ 28 | -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 29 | --swagger_out=logtostderr=true:. \ 30 | $proto_file 31 | } 32 | 33 | pushd src/gateway/generated/$APP/ 34 | generate_stubs "*.proto" 35 | popd 36 | 37 | pushd src/gateway/ 38 | go get -d -v 39 | go install -v 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC Scala Microservice Kit 2 | 3 | A starter kit for building [microservices](https://en.wikipedia.org/wiki/Microservices) using [gRPC](http://www.grpc.io) and [Scala](http://www.scala-lang.org). 4 | 5 | The gRPC server is [set up to use TLS](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#transport-security-tls) out of the box. [Mutual authentication](https://en.wikipedia.org/wiki/Transport_Layer_Security#Client-authenticated_TLS_handshake) is also implemented. 6 | 7 | User sessions are propagated as [JSON Web Tokens](https://jwt.io) through the `Authorization` HTTP header using the `Bearer` schema. JWTs are signed and verified using RS256. 8 | 9 | # Configuration 10 | 11 | The application can be [configured](app/src/main/resources/application.conf) through environment variables. 12 | 13 | [Utility scripts](util/) are provided to generate keys and SSL assets. 14 | 15 | # Building 16 | 17 | To build [Docker](https://www.docker.com/what-docker) images for the microservice: 18 | 19 | ```text 20 | make 21 | ``` 22 | 23 | To build the Docker images and push them to the registry: 24 | 25 | ```text 26 | make push 27 | ``` 28 | 29 | # Running Tests 30 | 31 | ```text 32 | make test 33 | ``` 34 | 35 | # Deployment 36 | 37 | A [Helm chart](deploy/echod/) is provided for deployment to a [Kubernetes](http://kubernetes.io) cluster. To run the deployment against your current Kubernetes context: 38 | 39 | ```text 40 | make deploy 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/scala/grpc/EchoServer.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod.grpc 2 | 3 | import java.io.File 4 | 5 | import com.typesafe.config.Config 6 | import io.grpc.ServerInterceptors 7 | import io.grpc.internal.ServerImpl 8 | import io.grpc.netty.{GrpcSslContexts, NettyServerBuilder} 9 | import io.netty.handler.ssl.ClientAuth 10 | import mu.node.echo.EchoServiceGrpc 11 | import mu.node.echo.EchoServiceGrpc.EchoService 12 | import mu.node.echod.util.FileUtils 13 | 14 | import scala.concurrent.ExecutionContext 15 | 16 | object EchoServer extends FileUtils { 17 | 18 | def build(config: Config, 19 | echoService: EchoService, 20 | userContextServerInterceptor: UserContextServerInterceptor, 21 | fileForConfiguredPath: (String) => File = fileForAbsolutePath): ServerImpl = { 22 | 23 | val sslContext = GrpcSslContexts 24 | .forServer(fileForConfiguredPath(config.getString("ssl.server-certificate")), 25 | fileForConfiguredPath(config.getString("ssl.server-private-key"))) 26 | .trustManager(fileForConfiguredPath(config.getString("ssl.client-ca-certificate"))) 27 | .clientAuth(ClientAuth.REQUIRE) 28 | .build() 29 | 30 | val echoGrpcService = EchoServiceGrpc.bindService(echoService, ExecutionContext.global) 31 | 32 | NettyServerBuilder 33 | .forPort(config.getInt("server-port")) 34 | .sslContext(sslContext) 35 | .addService(ServerInterceptors.intercept(echoGrpcService, userContextServerInterceptor)) 36 | .build() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /util/generate-jwt-signing-keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function print_usage { 5 | cat << EOF 6 | Usage: $0 keyname destination_directory 7 | 8 | Generate a private and public key pair for signing JSON Web Tokens using the RS256 algorithm 9 | 10 | The following files will be created: 11 | 12 | * keyname-private.pem 13 | * keyname-public.pem 14 | 15 | Requires OpenSSL 16 | EOF 17 | } 18 | 19 | function create_jwt_signing_key_pair { 20 | local keyname=$1 21 | local destination_directory=$2 22 | local working_directory="/tmp/${keyname}-workdir" 23 | 24 | mkdir -p $working_directory 25 | 26 | openssl genrsa \ 27 | -out ${working_directory}/${keyname}-private.rsa \ 28 | 2048 29 | 30 | openssl pkcs8 \ 31 | -topk8 \ 32 | -inform pem \ 33 | -in ${working_directory}/${keyname}-private.rsa \ 34 | -outform der \ 35 | -nocrypt \ 36 | -out ${working_directory}/${keyname}-private.pem 37 | 38 | openssl rsa \ 39 | -in ${working_directory}/${keyname}-private.rsa \ 40 | -pubout \ 41 | -outform der \ 42 | -out ${working_directory}/${keyname}-public.pem 43 | 44 | cp ${working_directory}/${keyname}-private.pem $destination_directory 45 | cp ${working_directory}/${keyname}-public.pem $destination_directory 46 | 47 | rm -rf $working_directory 48 | } 49 | 50 | if [ "$#" -ne 2 ]; then 51 | print_usage 52 | exit 1 53 | fi 54 | 55 | create_jwt_signing_key_pair $1 $2 56 | 57 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: app gateway 2 | 3 | push: pushapp pushgateway 4 | 5 | test: testapp testgateway 6 | 7 | clean: cleanapp cleangateway cleandeploy 8 | 9 | .PHONY: deploy 10 | deploy: 11 | @echo Creating SSL assets 12 | mkdir -p deploy/artifacts/ 13 | util/generate-self-signed-ssl-assets.sh echod deploy/artifacts/ 14 | util/generate-self-signed-ssl-assets.sh echod-client deploy/artifacts/ 15 | @echo Creating JWT signing assets 16 | util/generate-jwt-signing-keys.sh jwt-verification deploy/artifacts/ 17 | @echo Deploying to Kubernetes cluster via Helm 18 | @cd deploy; \ 19 | helm install echod \ 20 | --set serverCert=$(shell cat deploy/artifacts/echod-cert.pem | base64),serverKey=$(shell cat deploy/artifacts/echod-key.pem | base64),serverCaCert=$(shell cat deploy/artifacts/echod-ca-cert.pem | base64),clientCert=$(shell cat deploy/artifacts/echod-client-cert.pem | base64),clientKey=$(shell cat deploy/artifacts/echod-client-key.pem | base64),clientCaCert=$(shell cat deploy/artifacts/echod-client-ca-cert.pem | base64),jwtVerificationKey=$(shell cat deploy/artifacts/jwt-verification-public.pem | base64) 21 | 22 | cleandeploy: 23 | cd deploy; \ 24 | rm -rf artifacts/ 25 | 26 | .PHONY: app 27 | app: 28 | cd app; sbt docker:publishLocal 29 | 30 | pushapp: 31 | cd app; sbt docker:publish 32 | 33 | testapp: 34 | cd app; sbt clean coverage test 35 | 36 | cleanapp: 37 | cd app; sbt clean 38 | 39 | .PHONY: gateway 40 | gateway: 41 | cd gateway; make docker 42 | 43 | pushgateway: 44 | cd gateway; make push 45 | 46 | testgateway: 47 | cd gateway; # TODO: Run tests for gateway 48 | 49 | cleangateway: 50 | cd gateway; make clean 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/scala/grpc/UserContextServerInterceptor.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod.grpc 2 | 3 | import java.security.PublicKey 4 | 5 | import com.google.common.collect.Iterables 6 | import io.grpc._ 7 | import mu.node.echod.models.UserContext 8 | 9 | /* 10 | * Obtain the user context by reading from the JSON Web Token that is sent as an OAuth bearer 11 | * token with the HTTP request header. 12 | */ 13 | class UserContextServerInterceptor(jwtVerificationKey: PublicKey) extends ServerInterceptor { 14 | 15 | override def interceptCall[ReqT, RespT]( 16 | call: ServerCall[ReqT, RespT], 17 | headers: Metadata, 18 | next: ServerCallHandler[ReqT, RespT]): ServerCall.Listener[ReqT] = { 19 | readBearerToken(headers) flatMap { token => 20 | UserContext.fromJwt(token, jwtVerificationKey) 21 | } map { userContext => 22 | val withUserContext = Context 23 | .current() 24 | .withValue[UserContext](UserContextServerInterceptor.userContextKey, userContext) 25 | Contexts.interceptCall(withUserContext, call, headers, next) 26 | } getOrElse { 27 | next.startCall(call, headers) 28 | } 29 | } 30 | 31 | private def readBearerToken(headers: Metadata): Option[String] = { 32 | val authorizationHeaderKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER) 33 | try { 34 | Iterables 35 | .toArray(headers.getAll(authorizationHeaderKey), classOf[String]) 36 | .find(header => header.startsWith("Bearer ")) 37 | .map(header => header.replaceFirst("Bearer ", "")) 38 | } catch { 39 | case _: Exception => Option.empty 40 | } 41 | } 42 | } 43 | 44 | object UserContextServerInterceptor { 45 | val userContextKey: Context.Key[UserContext] = Context.key("user_context") 46 | } 47 | -------------------------------------------------------------------------------- /app/src/test/resources/ssl/localhost-client-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDsmqtyKUaEW51w 3 | 6E4ZHxZ9p9RKSYjfZEYLRjK2wEWhfkO4/ib8RlRNTjhPz1uy1VhujzRqe7B/jLZG 4 | CfreMhgiWbn6H0kHj44hsd45Aav7hRCyKB58AsuQZL64r8KdGrxJMBy7A23Gdpzu 5 | jdZLLFvfCfcs53F44q4V04y1CrGop/2zpUY00CRLGXbpzfaQwvybLoeddseKFbee 6 | JF6+FywKTkWgLV+EDZuE2QfyJ7aI+VpOV3Miwa4Xwlz/rqbrq1VSOurjNFSukljL 7 | n15j2P6tp6MsEpiDFxPXgPrw2sEcxvJiq9i3GSyt8fwoF+lfvUQe4bKEs/SpJe3r 8 | u8fJMzOpAgMBAAECggEAEHsRmCkh3VRYWiYEUqGkumn3UplerFjavCxOmgY4k3At 9 | HXSumIH5m0zohT6nX4SW3VSiTQyClc/iXmyRieqpXbMsTizhtGIef2BZX1UreqCh 10 | MhezT+YN8efilRDBGJplJR/x0GEDw4A9nCxLEf9sAtQ54GnasJXxlFmhAndVOq8e 11 | pQswicff2so3oNwOg10KawVLl17SladetgeDSrG1n8kUmxLOQU5JLNtkOmaArxrP 12 | TY9tt7hvVAioEanDS2uWlWmfsoftnIIJTZlZCizi3G+ZQgcD0BHJe9m1/zXluqOs 13 | cW4zA9/sL1ZFO0UhqhzSbjjrcvjxm5oN0aRUv9KigQKBgQD6wN8CquC29K5L4v4Q 14 | OuJS+EuBT55ZrCOjiDQ4UZOr7DKEorBJ4JsfxE2N0xVcnJ6I0vylOhX3SjqlEqT7 15 | pl3f6dk7OhBmjU7iEsubPF1s2m+ByIe8zUroz0qf+inpdUW+z02g6HnQ7ZabF4qs 16 | /zpFPJtZoVYdsJ2qns8oo+7duQKBgQDxjgKScYSPrTmDy43uG1s1C31udhY7CGZd 17 | lSsghiRCvVU2d34+ZsutisocwwFayVV3yn89cP1rEfJ8ofUlgyoEPZTvo1RDHvvq 18 | 8BWegWGaYxeCe3kIvJsDEXRUvkgIzi0w0mu1QFqaNWyQQLP9cV05XySM0r7EfYOk 19 | DbcLXoB9cQKBgBIYAr+VhvuMslsFeSHArf4grooZLar40eWF+Yaq1EYOmCKb/q6G 20 | B4uGRbZbKepx3rquxs5BX75lW8/3hXInMhTrMeKlMPPFdJC5nHmKJI+rP2qVBr5n 21 | 7eTYuGDM02NmM+8t1EMtI0UhL8HnM/mBvTmrwuX3z1f7G5VpOjeLhLv5AoGBALvD 22 | tq4c/X0tmtqe3OmsbB50mwDFXAxxQBkYfdifTQpv4BAhnIlnIIX6r5bh4miuvfgK 23 | +RNKhEK5RbOptUR+i6eWMvKAFFzEdfAuxa9bRQJcaobr8a5f5WLiK3pAGwB1O92K 24 | g1z19DeQtZ4AHakxeNTC50dr/gwAwNZCLvnwcJKRAoGAXRUv4nzZnAsApzQCLLb+ 25 | efGMa57dGXI6/XNfM0oOmnSD5JKC+cS2aQ9Wu6l2DczuZMrOWl0pDYGO8eGvxHYX 26 | 3kqgP9BOTPXTSpTbMii9KnCOXwXEazW+nuunL2JALW8eWVwQ0I7zWTsMEQLYI99N 27 | kB/6ciGuJSo5+Gk+2M0Er7s= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /app/src/test/resources/ssl/localhost-server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEkWdkcIzsh28s 3 | hY8DwRXA5/4YPWSItENPsJB5299HNmmk2OHQ++DhHbaSKWNA+3FDbnj0YKLtOPvh 4 | lkcyklpbIvqaI+ZZvgue5E6Q5ILR5L4BagLe8z0g8HRKrXd1hDBAnpIe2wrLxW20 5 | S8h+YOUNPS7k113VfV0yro1kMj98cgrbqqleYXmMzw6gnJKGxWlBTWZqkuEe9LBv 6 | fUdZaC6rni8uIyNxuyx/M9FtCAxsCTa1imvuFxyCLOtOYfbZEjGuyR5TchjtoosY 7 | QNarG2+8+HAwYKMEebmCbjyEcqNa+KeZQN3AtyFkBqBNj050ej2uMsYFuke4GkQb 8 | ncaHT+bNAgMBAAECggEBAJiUYVQ2hT0ol6RQe8WnQDKAId1A8mOmwUT/QfZFNJh7 9 | ZR+fdhGZImK7hLcJx1BunOmBvAO9a1qGTAY8d0GLFvX9PdceHLT2buDESTPxHJa+ 10 | XrqMRnQ3DPEflLVO1xZyKF5QeA3ZaxTnSBOWa6eloMZZAqiBgAWyjQBFO91Vn7AB 11 | HZF9zvMOc+PVM8Rq8VpBCPlU7znwZJzU5IU4LnMqewvNEQeDOliFsIxiPEPyq3Tt 12 | Yf0k5NXY5MRMH90WKcYX2Ye4IhqAMcz78Ka9qJfKd6l/ohjyyLZtRFUpXaYIdnWx 13 | NuskKwkicNr483mQ2M/BiGwt3HBLCLD5nlNFj8zUwEECgYEA5ZRoJlKMIyqTEVuH 14 | ZUDxbm7EoMU+Lij1I/U5vuKfr/M2fOmxGanI6Z7DskWAiquVR6bqMuqRmeLMX/T5 15 | J76tYppHo8PnlkYQDNJphXy/ydXQwxlGI/4GsxlKZRO7cfVro/kHFjajM3n7z09m 16 | jSiipFV1y11TP4u4+dw07Y/urZECgYEA2zByQvM1RjAv8aWmUhaeJIdPtSPw+wSQ 17 | dpI+kBI2WEWYxcZam719LGrCA8HoGbr5NKL4PQac3LsKWYNRjiZBw/778lO+zg2d 18 | PoEw3vMm+jMlla/cVVyFwvryzWnzmoqiwX03Hc/t57J8eXfCS3dR4P0Jm8MkVVqw 19 | 6klfyK9LN30CgYBKgMLbzOYVc3eOpnll7pFrGR9OXTQ0nq4PIUHAJKNV4kPIfb4v 20 | ad39krxHWi7A0bX8LrrKEz11Bxjz+vfwYfy6hv7Aso6xQcNrpc0AuN80jOLWrZkJ 21 | jSJ3dLmj26d08AQijmoyV0DXL7r8J/RL0ugVFHbJrFubv6gjzcaPYao/QQKBgQC4 22 | 9G9mSR1qJZ0/Nj1bRVSalEdRHYvp9NcGBq+eGJAc+lVIhfDNPB8UzNz8GLKGi7xf 23 | iykXLa5Nn9LC58l4vpV+Enp85+e4rcpDBPa9GsaIF4Keha/Ro+oHNoSNitsRS+y8 24 | grFwiZVBjt30DXc5AO1pgXuLISZWk9l3SQT3Ldu1bQKBgHDTqYyIuFxI5ZlAKe+q 25 | k2bZFgYXuFTOvUrLr3Qe32zB53LfRrCsuJIPtKNFiB9o1H6VwUgQM3zzNAb7Z1Im 26 | aatMcEPidcnggRk6cOdnfMReagmLqxEyUgR8S7Xl43BBP9gnHYAB2PggJDD2sdBh 27 | SSQe8qPQzeq+uyesx7HDcm9+ 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /app/src/test/scala/UserContextSpec.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod 2 | 3 | import java.util.Calendar 4 | 5 | import mu.node.echod.models.UserContext 6 | import mu.node.echod.util.KeyUtils 7 | import pdi.jwt.Jwt 8 | 9 | import scala.concurrent.duration._ 10 | 11 | class UserContextSpec extends BaseSpec with KeyUtils { 12 | val jwtSigningKey = loadPkcs8PrivateKey( 13 | pathForTestResourcePath(config.getString("jwt.signing-key"))) 14 | 15 | "The UserContext companion object" when { 16 | 17 | val userId = "8d5921be-8f85-11e6-ae22-56b6b6499611" 18 | val futureExpiry = Calendar.getInstance().getTimeInMillis + Duration(5, MINUTES).toMillis 19 | val validClaim = 20 | s"""|{ 21 | | "sub": "$userId", 22 | | "exp": $futureExpiry 23 | |}""".stripMargin 24 | 25 | "asked to create a UserContext from a valid, signed JWT" should { 26 | "return the UserContext" in { 27 | val validJwt = Jwt.encode(validClaim, jwtSigningKey, jwtDsa) 28 | UserContext.fromJwt(validJwt, jwtVerificationKey) shouldEqual Some(UserContext(userId)) 29 | } 30 | } 31 | 32 | "asked to create UserContext from an unsigned JWT" should { 33 | "return None" in { 34 | val unsignedJwt = Jwt.encode(validClaim) 35 | UserContext.fromJwt(unsignedJwt, jwtVerificationKey) shouldEqual None 36 | } 37 | } 38 | 39 | "asked to create UserContext from a JWT with an invalid claim" should { 40 | "return None" in { 41 | val invalidClaim = s"""{ "unknownField": "value" }""" 42 | val invalidJwt = Jwt.encode(invalidClaim, jwtSigningKey, jwtDsa) 43 | UserContext.fromJwt(invalidJwt, jwtVerificationKey) shouldEqual None 44 | } 45 | } 46 | 47 | "asked to create UserContext from a JWT with an invalid payload" should { 48 | "return None" in { 49 | val invalidPayload = "malformed JSON" 50 | val invalidJwt = Jwt.encode(invalidPayload, jwtSigningKey, jwtDsa) 51 | UserContext.fromJwt(invalidJwt, jwtVerificationKey) shouldEqual None 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/build.sbt: -------------------------------------------------------------------------------- 1 | /* 2 | * Project metadata 3 | */ 4 | name := "echod" 5 | version := "1.0.0-SNAPSHOT" 6 | description := "A starter kit for building microservices using gRPC and Scala" 7 | organization := "mu.node" 8 | organizationHomepage := Some(url("http://node.mu")) 9 | 10 | /* 11 | * Docker image build 12 | */ 13 | enablePlugins(JavaAppPackaging) 14 | enablePlugins(DockerPlugin) 15 | enablePlugins(AshScriptPlugin) 16 | import com.typesafe.sbt.packager.docker._ 17 | maintainer in Docker := "Vy-Shane Xie " 18 | dockerBaseImage := "openjdk:8-jre" 19 | dockerRepository := Some("vyshane") 20 | dockerUpdateLatest := true 21 | 22 | /* 23 | * Compiler 24 | */ 25 | scalaVersion := "2.11.8" 26 | scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8") 27 | 28 | /* 29 | * Dependencies 30 | */ 31 | libraryDependencies ++= Seq( 32 | // Configuration 33 | "com.typesafe" % "config" % "1.3.1", 34 | // Logging 35 | "ch.qos.logback" % "logback-classic" % "1.1.7", 36 | // Testing 37 | "org.scalatest" %% "scalatest" % "3.0.0" % "test", 38 | // Dependency injection 39 | // "com.softwaremill.macwire" %% "macros" % "2.2.4" % "provided", 40 | // JSON Web Tokens, JSON parsing 41 | "com.pauldijou" %% "jwt-core" % "0.9.0", 42 | "com.typesafe.play" %% "play-json" % "2.5.8" 43 | ) 44 | // gRPC and Protocol Buffers 45 | libraryDependencies ++= Seq( 46 | "io.grpc" % "grpc-netty" % "1.0.1", 47 | "io.grpc" % "grpc-stub" % "1.0.1", 48 | "com.trueaccord.scalapb" %% "scalapb-runtime-grpc" % "0.5.43", 49 | "io.netty" % "netty-tcnative-boringssl-static" % "1.1.33.Fork19", // SSL support 50 | "javassist" % "javassist" % "3.12.1.GA" // Improves Netty performance 51 | ) 52 | 53 | PB.targets in Compile := Seq( 54 | scalapb.gen(grpc = true, flatPackage = true) -> (sourceManaged in Compile).value 55 | ) 56 | 57 | /* 58 | * Code coverage via scoverage 59 | */ 60 | coverageMinimum := 90 61 | coverageFailOnMinimum := true 62 | coverageOutputCobertura := false 63 | coverageOutputHTML := true 64 | coverageOutputXML := false 65 | 66 | /* 67 | * Code formatting 68 | */ 69 | scalafmtConfig := Some(file(".scalafmt.conf")) 70 | -------------------------------------------------------------------------------- /gateway/src/gateway/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "github.com/golang/glog" 12 | "github.com/gorilla/handlers" 13 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 14 | "golang.org/x/net/context" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/credentials" 17 | 18 | echodGrpc "gateway/generated/echod" 19 | ) 20 | 21 | var ( 22 | clientCertPath = os.Getenv("SSL_CLIENT_CERTIFICATE") 23 | clientPrivateKeyPath = os.Getenv("SSL_CLIENT_PRIVATE_KEY") 24 | serverCaCertPath = os.Getenv("SSL_CA_CERTIFICATE") 25 | backend = os.Getenv("BACKEND_HOST") + ":" + os.Getenv("BACKEND_PORT") 26 | swaggerDir = "generated/echod" 27 | ) 28 | 29 | func run() error { 30 | ctx := context.Background() 31 | ctx, cancel := context.WithCancel(ctx) 32 | defer cancel() 33 | 34 | mux := runtime.NewServeMux() 35 | dialOptions, err := getDialOptions(serverCaCertPath, clientCertPath, clientPrivateKeyPath) 36 | if err != nil { 37 | return err 38 | } 39 | err = echodGrpc.RegisterEchoServiceHandlerFromEndpoint(ctx, mux, backend, dialOptions) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | allowedMethods := handlers.AllowedMethods([]string{"OPTIONS", "DELETE", "GET", "HEAD", "POST", "PUT"}) 45 | allowedOrigins := getAllowedOriginsFromConfig("CORS_ALLOWED_ORIGINS") 46 | allowedHeaders := handlers.AllowedHeaders([]string{"Authorization", "Origin", "Content-Type"}) 47 | 48 | http.ListenAndServe(":"+os.Getenv("GATEWAY_PORT"), handlers.CORS(allowedMethods, allowedOrigins, allowedHeaders)(mux)) 49 | return nil 50 | } 51 | 52 | func main() { 53 | defer glog.Flush() 54 | if err := run(); err != nil { 55 | glog.Error(err) 56 | } 57 | } 58 | 59 | func getDialOptions(serverCaCertPath string, clientCertPath string, clientPrivateKeyPath string) ([]grpc.DialOption, error) { 60 | clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientPrivateKeyPath) 61 | if err != nil { 62 | return nil, err 63 | } 64 | serverCaCert, err := ioutil.ReadFile(serverCaCertPath) 65 | if err != nil { 66 | return nil, err 67 | } 68 | caCertPool := x509.NewCertPool() 69 | caCertPool.AppendCertsFromPEM(serverCaCert) 70 | transportCredentials := credentials.NewTLS(&tls.Config{ 71 | Certificates: []tls.Certificate{clientCert}, 72 | RootCAs: caCertPool, 73 | }) 74 | return []grpc.DialOption{grpc.WithTransportCredentials(transportCredentials)}, err 75 | } 76 | 77 | func getAllowedOriginsFromConfig(env string) handlers.CORSOption { 78 | configuredOrigins := strings.Split(os.Getenv(env), ",") 79 | for key, url := range configuredOrigins { 80 | configuredOrigins[key] = strings.TrimSpace(url) 81 | } 82 | return handlers.AllowedOrigins(configuredOrigins) 83 | } 84 | -------------------------------------------------------------------------------- /deploy/echod/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{.Chart.Name}} 5 | spec: 6 | replicas: {{.Values.replicas}} 7 | template: 8 | metadata: 9 | labels: 10 | app: {{.Chart.Name}} 11 | project: {{.Values.project}} 12 | spec: 13 | containers: 14 | # App 15 | - name: {{.Chart.Name}} 16 | image: {{.Values.imageRepository}}/{{.Chart.Name}}:{{.Values.version}} 17 | volumeMounts: 18 | - name: {{.Chart.Name}}-server-secret 19 | mountPath: /etc/secrets/{{.Chart.Name}}-server 20 | readOnly: true 21 | - name: jwt-verification-secret 22 | mountPath: /etc/secrets/jwt-verification 23 | readOnly: true 24 | ports: 25 | - name: grpc 26 | containerPort: 8443 27 | env: 28 | - name: SSL_SERVER_CERTIFICATE 29 | value: /etc/secrets/{{.Chart.Name}}-server/{{.Chart.Name}}-server-cert 30 | - name: SSL_SERVER_PRIVATE_KEY 31 | value: /etc/secrets/{{.Chart.Name}}-server/{{.Chart.Name}}-server-key 32 | - name: SSL_CLIENT_CA_CERTIFICATE 33 | value: /etc/secrets/{{.Chart.Name}}-server/{{.Chart.Name}}-client-ca-cert 34 | - name: JWT_SIGNATURE_VERIFICATION_KEY 35 | value: /etc/secrets/jwt-verification/jwt-verification-key 36 | # Gateway 37 | - name: {{.Chart.Name}}-gateway 38 | image: {{.Values.imageRepository}}/{{.Chart.Name}}-gateway:{{.Values.version}} 39 | volumeMounts: 40 | - name: {{.Chart.Name}}-client-secret 41 | mountPath: /etc/secrets/{{.Chart.Name}}-client 42 | readOnly: true 43 | ports: 44 | - name: json 45 | containerPort: 9443 46 | env: 47 | - name: GATEWAY_PORT 48 | value: "9443" 49 | - name: CORS_ALLOWED_ORIGINS 50 | value: {{.Values.corsAllowedOrigins | quote}} 51 | - name: BACKEND_HOST 52 | value: localhost 53 | - name: BACKEND_PORT 54 | value: $({{.Chart.Name | upper}}_SERVICE_PORT_GRPC) 55 | - name: SSL_CA_CERTIFICATE 56 | value: /etc/secrets/{{.Chart.Name}}-client/{{.Chart.Name}}-server-ca-cert 57 | - name: SSL_CLIENT_CERTIFICATE 58 | value: /etc/secrets/{{.Chart.Name}}-client/{{.Chart.Name}}-client-cert 59 | - name: SSL_CLIENT_PRIVATE_KEY 60 | value: /etc/secrets/{{.Chart.Name}}-client/{{.Chart.Name}}-client-key 61 | volumes: 62 | - name: {{.Chart.Name}}-server-secret 63 | secret: 64 | secretName: {{.Chart.Name}}-server 65 | - name: {{.Chart.Name}}-client-secret 66 | secret: 67 | secretName: {{.Chart.Name}}-client 68 | - name: jwt-verification-secret 69 | secret: 70 | secretName: {{.Values.jwtVerificationSecret}} -------------------------------------------------------------------------------- /app/src/test/scala/EchoServerSpec.scala: -------------------------------------------------------------------------------- 1 | package mu.node.echod 2 | 3 | import java.util.Calendar 4 | 5 | import mu.node.echo.SendMessageRequest 6 | import grpc.{AccessTokenCallCredentials, EchoClient} 7 | import mu.node.echod.models.UserContext 8 | import org.scalatest.BeforeAndAfterAll 9 | import scala.concurrent.duration._ 10 | 11 | class EchoServerSpec extends BaseSpec with BeforeAndAfterAll { 12 | 13 | val jwtSigningKey = loadPkcs8PrivateKey( 14 | pathForTestResourcePath(config.getString("jwt.signing-key"))) 15 | val echoServiceStub = EchoClient.buildServiceStub(config, fileForTestResourcePath) 16 | 17 | override def beforeAll(): Unit = { 18 | echoServer.start() 19 | } 20 | 21 | override def afterAll(): Unit = { 22 | echoServer.shutdown() 23 | } 24 | 25 | "The echod gRPC server" when { 26 | 27 | "sent a valid, authenticated SendMessageRequest" should { 28 | "reply back with the Message" in { 29 | val userId = "8d5921be-8f85-11e6-ae22-56b6b6499611" 30 | val futureExpiry = Calendar.getInstance().getTimeInMillis + Duration(5, MINUTES).toMillis 31 | val jwt = UserContext(userId).toJwt(futureExpiry, jwtSigningKey) 32 | val sendMessage = echoServiceStub 33 | .withCallCredentials(new AccessTokenCallCredentials(jwt)) 34 | .send(SendMessageRequest("hello")) 35 | whenReady(sendMessage) { reply => 36 | reply.messageId.nonEmpty shouldBe true 37 | reply.senderId shouldEqual userId 38 | reply.content shouldEqual "hello" 39 | } 40 | } 41 | } 42 | 43 | "sent an unauthenticated SendMessageRequest" should { 44 | "return an exception indicating that the call was unauthenticated" in { 45 | val sendMessage = echoServiceStub.send(SendMessageRequest("test")) 46 | whenReady(sendMessage.failed) { ex => 47 | ex shouldBe a[Exception] 48 | ex.getMessage shouldEqual "UNAUTHENTICATED" 49 | } 50 | } 51 | } 52 | 53 | "sent a SendMessageRequest with an expired access token" should { 54 | "reply back with the Message" in { 55 | val userId = "8d5921be-8f85-11e6-ae22-56b6b6499611" 56 | val lapsedExpiry = Calendar.getInstance().getTimeInMillis - Duration(5, MINUTES).toMillis 57 | val jwt = UserContext(userId).toJwt(lapsedExpiry, jwtSigningKey) 58 | val sendMessage = echoServiceStub 59 | .withCallCredentials(new AccessTokenCallCredentials(jwt)) 60 | .send(SendMessageRequest("hello")) 61 | whenReady(sendMessage.failed) { ex => 62 | ex shouldBe a[Exception] 63 | ex.getMessage shouldEqual "UNAUTHENTICATED" 64 | } 65 | } 66 | } 67 | 68 | "sent a SendMessageRequest with an invalid access token" should { 69 | "return an exception indicating that the call was unauthenticated" in { 70 | val sendMessage = echoServiceStub 71 | .withCallCredentials(new AccessTokenCallCredentials("bad jwt")) 72 | .send(SendMessageRequest("hello")) 73 | whenReady(sendMessage.failed) { ex => 74 | ex shouldBe a[Exception] 75 | ex.getMessage shouldEqual "UNAUTHENTICATED" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /util/generate-self-signed-ssl-assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function print_usage { 5 | cat << EOF 6 | Usage: $0 hostname destination_directory 7 | 8 | Generate self-signed SSL certificate assets for hostname, placing the files in the destination_directory. 9 | 10 | The following files will be created: 11 | 12 | * hostname-key.pem (private key in PKCS #8 format) 13 | * hostname-cert.pem 14 | * hostname-ca-cert.pem 15 | 16 | Requires OpenSSL. 17 | EOF 18 | } 19 | 20 | function create_self_signed_certificate_assets { 21 | local hostname=$1 22 | local destination_directory=$2 23 | local working_directory="/tmp/${hostname}-ssl-workdir" 24 | export RANDFILE=${working_directory}/.rnd 25 | 26 | mkdir -p $working_directory 27 | 28 | openssl genrsa \ 29 | -out ${working_directory}/${hostname}-ca-key.rsa \ 30 | 2048 31 | 32 | openssl req \ 33 | -x509 \ 34 | -new \ 35 | -nodes \ 36 | -key ${working_directory}/${hostname}-ca-key.rsa \ 37 | -days 10000 \ 38 | -out ${working_directory}/${hostname}-ca-cert.pem \ 39 | -subj "/CN=node-mu-ca" 40 | 41 | cat > $working_directory/openssl.cnf <= 128 are escaped. 187 | // TODO(kenton): Base-64 encode? 188 | optional string default_value = 7; 189 | 190 | // If set, gives the index of a oneof in the containing type's oneof_decl 191 | // list. This field is a member of that oneof. 192 | optional int32 oneof_index = 9; 193 | 194 | // JSON name of this field. The value is set by protocol compiler. If the 195 | // user has set a "json_name" option on this field, that option's value 196 | // will be used. Otherwise, it's deduced from the field's name by converting 197 | // it to camelCase. 198 | optional string json_name = 10; 199 | 200 | optional FieldOptions options = 8; 201 | } 202 | 203 | // Describes a oneof. 204 | message OneofDescriptorProto { 205 | optional string name = 1; 206 | optional OneofOptions options = 2; 207 | } 208 | 209 | // Describes an enum type. 210 | message EnumDescriptorProto { 211 | optional string name = 1; 212 | 213 | repeated EnumValueDescriptorProto value = 2; 214 | 215 | optional EnumOptions options = 3; 216 | } 217 | 218 | // Describes a value within an enum. 219 | message EnumValueDescriptorProto { 220 | optional string name = 1; 221 | optional int32 number = 2; 222 | 223 | optional EnumValueOptions options = 3; 224 | } 225 | 226 | // Describes a service. 227 | message ServiceDescriptorProto { 228 | optional string name = 1; 229 | repeated MethodDescriptorProto method = 2; 230 | 231 | optional ServiceOptions options = 3; 232 | } 233 | 234 | // Describes a method of a service. 235 | message MethodDescriptorProto { 236 | optional string name = 1; 237 | 238 | // Input and output type names. These are resolved in the same way as 239 | // FieldDescriptorProto.type_name, but must refer to a message type. 240 | optional string input_type = 2; 241 | optional string output_type = 3; 242 | 243 | optional MethodOptions options = 4; 244 | 245 | // Identifies if client streams multiple client messages 246 | optional bool client_streaming = 5 [default=false]; 247 | // Identifies if server streams multiple server messages 248 | optional bool server_streaming = 6 [default=false]; 249 | } 250 | 251 | 252 | // =================================================================== 253 | // Options 254 | 255 | // Each of the definitions above may have "options" attached. These are 256 | // just annotations which may cause code to be generated slightly differently 257 | // or may contain hints for code that manipulates protocol messages. 258 | // 259 | // Clients may define custom options as extensions of the *Options messages. 260 | // These extensions may not yet be known at parsing time, so the parser cannot 261 | // store the values in them. Instead it stores them in a field in the *Options 262 | // message called uninterpreted_option. This field must have the same name 263 | // across all *Options messages. We then use this field to populate the 264 | // extensions when we build a descriptor, at which point all protos have been 265 | // parsed and so all extensions are known. 266 | // 267 | // Extension numbers for custom options may be chosen as follows: 268 | // * For options which will only be used within a single application or 269 | // organization, or for experimental options, use field numbers 50000 270 | // through 99999. It is up to you to ensure that you do not use the 271 | // same number for multiple options. 272 | // * For options which will be published and used publicly by multiple 273 | // independent entities, e-mail protobuf-global-extension-registry@google.com 274 | // to reserve extension numbers. Simply provide your project name (e.g. 275 | // Objective-C plugin) and your project website (if available) -- there's no 276 | // need to explain how you intend to use them. Usually you only need one 277 | // extension number. You can declare multiple options with only one extension 278 | // number by putting them in a sub-message. See the Custom Options section of 279 | // the docs for examples: 280 | // https://developers.google.com/protocol-buffers/docs/proto#options 281 | // If this turns out to be popular, a web service will be set up 282 | // to automatically assign option numbers. 283 | 284 | 285 | message FileOptions { 286 | 287 | // Sets the Java package where classes generated from this .proto will be 288 | // placed. By default, the proto package is used, but this is often 289 | // inappropriate because proto packages do not normally start with backwards 290 | // domain names. 291 | optional string java_package = 1; 292 | 293 | 294 | // If set, all the classes from the .proto file are wrapped in a single 295 | // outer class with the given name. This applies to both Proto1 296 | // (equivalent to the old "--one_java_file" option) and Proto2 (where 297 | // a .proto always translates to a single class, but you may want to 298 | // explicitly choose the class name). 299 | optional string java_outer_classname = 8; 300 | 301 | // If set true, then the Java code generator will generate a separate .java 302 | // file for each top-level message, enum, and service defined in the .proto 303 | // file. Thus, these types will *not* be nested inside the outer class 304 | // named by java_outer_classname. However, the outer class will still be 305 | // generated to contain the file's getDescriptor() method as well as any 306 | // top-level extensions defined in the file. 307 | optional bool java_multiple_files = 10 [default=false]; 308 | 309 | // If set true, then the Java code generator will generate equals() and 310 | // hashCode() methods for all messages defined in the .proto file. 311 | // This increases generated code size, potentially substantially for large 312 | // protos, which may harm a memory-constrained application. 313 | // - In the full runtime this is a speed optimization, as the 314 | // AbstractMessage base class includes reflection-based implementations of 315 | // these methods. 316 | // - In the lite runtime, setting this option changes the semantics of 317 | // equals() and hashCode() to more closely match those of the full runtime; 318 | // the generated methods compute their results based on field values rather 319 | // than object identity. (Implementations should not assume that hashcodes 320 | // will be consistent across runtimes or versions of the protocol compiler.) 321 | optional bool java_generate_equals_and_hash = 20 [default=false]; 322 | 323 | // If set true, then the Java2 code generator will generate code that 324 | // throws an exception whenever an attempt is made to assign a non-UTF-8 325 | // byte sequence to a string field. 326 | // Message reflection will do the same. 327 | // However, an extension field still accepts non-UTF-8 byte sequences. 328 | // This option has no effect on when used with the lite runtime. 329 | optional bool java_string_check_utf8 = 27 [default=false]; 330 | 331 | 332 | // Generated classes can be optimized for speed or code size. 333 | enum OptimizeMode { 334 | SPEED = 1; // Generate complete code for parsing, serialization, 335 | // etc. 336 | CODE_SIZE = 2; // Use ReflectionOps to implement these methods. 337 | LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. 338 | } 339 | optional OptimizeMode optimize_for = 9 [default=SPEED]; 340 | 341 | // Sets the Go package where structs generated from this .proto will be 342 | // placed. If omitted, the Go package will be derived from the following: 343 | // - The basename of the package import path, if provided. 344 | // - Otherwise, the package statement in the .proto file, if present. 345 | // - Otherwise, the basename of the .proto file, without extension. 346 | optional string go_package = 11; 347 | 348 | 349 | 350 | // Should generic services be generated in each language? "Generic" services 351 | // are not specific to any particular RPC system. They are generated by the 352 | // main code generators in each language (without additional plugins). 353 | // Generic services were the only kind of service generation supported by 354 | // early versions of google.protobuf. 355 | // 356 | // Generic services are now considered deprecated in favor of using plugins 357 | // that generate code specific to your particular RPC system. Therefore, 358 | // these default to false. Old code which depends on generic services should 359 | // explicitly set them to true. 360 | optional bool cc_generic_services = 16 [default=false]; 361 | optional bool java_generic_services = 17 [default=false]; 362 | optional bool py_generic_services = 18 [default=false]; 363 | 364 | // Is this file deprecated? 365 | // Depending on the target platform, this can emit Deprecated annotations 366 | // for everything in the file, or it will be completely ignored; in the very 367 | // least, this is a formalization for deprecating files. 368 | optional bool deprecated = 23 [default=false]; 369 | 370 | // Enables the use of arenas for the proto messages in this file. This applies 371 | // only to generated classes for C++. 372 | optional bool cc_enable_arenas = 31 [default=false]; 373 | 374 | 375 | // Sets the objective c class prefix which is prepended to all objective c 376 | // generated classes from this .proto. There is no default. 377 | optional string objc_class_prefix = 36; 378 | 379 | // Namespace for generated classes; defaults to the package. 380 | optional string csharp_namespace = 37; 381 | 382 | // The parser stores options it doesn't recognize here. See above. 383 | repeated UninterpretedOption uninterpreted_option = 999; 384 | 385 | // Clients can define custom options in extensions of this message. See above. 386 | extensions 1000 to max; 387 | 388 | reserved 38; 389 | } 390 | 391 | message MessageOptions { 392 | // Set true to use the old proto1 MessageSet wire format for extensions. 393 | // This is provided for backwards-compatibility with the MessageSet wire 394 | // format. You should not use this for any other reason: It's less 395 | // efficient, has fewer features, and is more complicated. 396 | // 397 | // The message must be defined exactly as follows: 398 | // message Foo { 399 | // option message_set_wire_format = true; 400 | // extensions 4 to max; 401 | // } 402 | // Note that the message cannot have any defined fields; MessageSets only 403 | // have extensions. 404 | // 405 | // All extensions of your type must be singular messages; e.g. they cannot 406 | // be int32s, enums, or repeated messages. 407 | // 408 | // Because this is an option, the above two restrictions are not enforced by 409 | // the protocol compiler. 410 | optional bool message_set_wire_format = 1 [default=false]; 411 | 412 | // Disables the generation of the standard "descriptor()" accessor, which can 413 | // conflict with a field of the same name. This is meant to make migration 414 | // from proto1 easier; new code should avoid fields named "descriptor". 415 | optional bool no_standard_descriptor_accessor = 2 [default=false]; 416 | 417 | // Is this message deprecated? 418 | // Depending on the target platform, this can emit Deprecated annotations 419 | // for the message, or it will be completely ignored; in the very least, 420 | // this is a formalization for deprecating messages. 421 | optional bool deprecated = 3 [default=false]; 422 | 423 | // Whether the message is an automatically generated map entry type for the 424 | // maps field. 425 | // 426 | // For maps fields: 427 | // map map_field = 1; 428 | // The parsed descriptor looks like: 429 | // message MapFieldEntry { 430 | // option map_entry = true; 431 | // optional KeyType key = 1; 432 | // optional ValueType value = 2; 433 | // } 434 | // repeated MapFieldEntry map_field = 1; 435 | // 436 | // Implementations may choose not to generate the map_entry=true message, but 437 | // use a native map in the target language to hold the keys and values. 438 | // The reflection APIs in such implementions still need to work as 439 | // if the field is a repeated message field. 440 | // 441 | // NOTE: Do not set the option in .proto files. Always use the maps syntax 442 | // instead. The option should only be implicitly set by the proto compiler 443 | // parser. 444 | optional bool map_entry = 7; 445 | 446 | // The parser stores options it doesn't recognize here. See above. 447 | repeated UninterpretedOption uninterpreted_option = 999; 448 | 449 | // Clients can define custom options in extensions of this message. See above. 450 | extensions 1000 to max; 451 | } 452 | 453 | message FieldOptions { 454 | // The ctype option instructs the C++ code generator to use a different 455 | // representation of the field than it normally would. See the specific 456 | // options below. This option is not yet implemented in the open source 457 | // release -- sorry, we'll try to include it in a future version! 458 | optional CType ctype = 1 [default = STRING]; 459 | enum CType { 460 | // Default mode. 461 | STRING = 0; 462 | 463 | CORD = 1; 464 | 465 | STRING_PIECE = 2; 466 | } 467 | // The packed option can be enabled for repeated primitive fields to enable 468 | // a more efficient representation on the wire. Rather than repeatedly 469 | // writing the tag and type for each element, the entire array is encoded as 470 | // a single length-delimited blob. In proto3, only explicit setting it to 471 | // false will avoid using packed encoding. 472 | optional bool packed = 2; 473 | 474 | 475 | // The jstype option determines the JavaScript type used for values of the 476 | // field. The option is permitted only for 64 bit integral and fixed types 477 | // (int64, uint64, sint64, fixed64, sfixed64). By default these types are 478 | // represented as JavaScript strings. This avoids loss of precision that can 479 | // happen when a large value is converted to a floating point JavaScript 480 | // numbers. Specifying JS_NUMBER for the jstype causes the generated 481 | // JavaScript code to use the JavaScript "number" type instead of strings. 482 | // This option is an enum to permit additional types to be added, 483 | // e.g. goog.math.Integer. 484 | optional JSType jstype = 6 [default = JS_NORMAL]; 485 | enum JSType { 486 | // Use the default type. 487 | JS_NORMAL = 0; 488 | 489 | // Use JavaScript strings. 490 | JS_STRING = 1; 491 | 492 | // Use JavaScript numbers. 493 | JS_NUMBER = 2; 494 | } 495 | 496 | // Should this field be parsed lazily? Lazy applies only to message-type 497 | // fields. It means that when the outer message is initially parsed, the 498 | // inner message's contents will not be parsed but instead stored in encoded 499 | // form. The inner message will actually be parsed when it is first accessed. 500 | // 501 | // This is only a hint. Implementations are free to choose whether to use 502 | // eager or lazy parsing regardless of the value of this option. However, 503 | // setting this option true suggests that the protocol author believes that 504 | // using lazy parsing on this field is worth the additional bookkeeping 505 | // overhead typically needed to implement it. 506 | // 507 | // This option does not affect the public interface of any generated code; 508 | // all method signatures remain the same. Furthermore, thread-safety of the 509 | // interface is not affected by this option; const methods remain safe to 510 | // call from multiple threads concurrently, while non-const methods continue 511 | // to require exclusive access. 512 | // 513 | // 514 | // Note that implementations may choose not to check required fields within 515 | // a lazy sub-message. That is, calling IsInitialized() on the outher message 516 | // may return true even if the inner message has missing required fields. 517 | // This is necessary because otherwise the inner message would have to be 518 | // parsed in order to perform the check, defeating the purpose of lazy 519 | // parsing. An implementation which chooses not to check required fields 520 | // must be consistent about it. That is, for any particular sub-message, the 521 | // implementation must either *always* check its required fields, or *never* 522 | // check its required fields, regardless of whether or not the message has 523 | // been parsed. 524 | optional bool lazy = 5 [default=false]; 525 | 526 | // Is this field deprecated? 527 | // Depending on the target platform, this can emit Deprecated annotations 528 | // for accessors, or it will be completely ignored; in the very least, this 529 | // is a formalization for deprecating fields. 530 | optional bool deprecated = 3 [default=false]; 531 | 532 | // For Google-internal migration only. Do not use. 533 | optional bool weak = 10 [default=false]; 534 | 535 | 536 | // The parser stores options it doesn't recognize here. See above. 537 | repeated UninterpretedOption uninterpreted_option = 999; 538 | 539 | // Clients can define custom options in extensions of this message. See above. 540 | extensions 1000 to max; 541 | } 542 | 543 | message OneofOptions { 544 | // The parser stores options it doesn't recognize here. See above. 545 | repeated UninterpretedOption uninterpreted_option = 999; 546 | 547 | // Clients can define custom options in extensions of this message. See above. 548 | extensions 1000 to max; 549 | } 550 | 551 | message EnumOptions { 552 | 553 | // Set this option to true to allow mapping different tag names to the same 554 | // value. 555 | optional bool allow_alias = 2; 556 | 557 | // Is this enum deprecated? 558 | // Depending on the target platform, this can emit Deprecated annotations 559 | // for the enum, or it will be completely ignored; in the very least, this 560 | // is a formalization for deprecating enums. 561 | optional bool deprecated = 3 [default=false]; 562 | 563 | // The parser stores options it doesn't recognize here. See above. 564 | repeated UninterpretedOption uninterpreted_option = 999; 565 | 566 | // Clients can define custom options in extensions of this message. See above. 567 | extensions 1000 to max; 568 | } 569 | 570 | message EnumValueOptions { 571 | // Is this enum value deprecated? 572 | // Depending on the target platform, this can emit Deprecated annotations 573 | // for the enum value, or it will be completely ignored; in the very least, 574 | // this is a formalization for deprecating enum values. 575 | optional bool deprecated = 1 [default=false]; 576 | 577 | // The parser stores options it doesn't recognize here. See above. 578 | repeated UninterpretedOption uninterpreted_option = 999; 579 | 580 | // Clients can define custom options in extensions of this message. See above. 581 | extensions 1000 to max; 582 | } 583 | 584 | message ServiceOptions { 585 | 586 | // Note: Field numbers 1 through 32 are reserved for Google's internal RPC 587 | // framework. We apologize for hoarding these numbers to ourselves, but 588 | // we were already using them long before we decided to release Protocol 589 | // Buffers. 590 | 591 | // Is this service deprecated? 592 | // Depending on the target platform, this can emit Deprecated annotations 593 | // for the service, or it will be completely ignored; in the very least, 594 | // this is a formalization for deprecating services. 595 | optional bool deprecated = 33 [default=false]; 596 | 597 | // The parser stores options it doesn't recognize here. See above. 598 | repeated UninterpretedOption uninterpreted_option = 999; 599 | 600 | // Clients can define custom options in extensions of this message. See above. 601 | extensions 1000 to max; 602 | } 603 | 604 | message MethodOptions { 605 | 606 | // Note: Field numbers 1 through 32 are reserved for Google's internal RPC 607 | // framework. We apologize for hoarding these numbers to ourselves, but 608 | // we were already using them long before we decided to release Protocol 609 | // Buffers. 610 | 611 | // Is this method deprecated? 612 | // Depending on the target platform, this can emit Deprecated annotations 613 | // for the method, or it will be completely ignored; in the very least, 614 | // this is a formalization for deprecating methods. 615 | optional bool deprecated = 33 [default=false]; 616 | 617 | // The parser stores options it doesn't recognize here. See above. 618 | repeated UninterpretedOption uninterpreted_option = 999; 619 | 620 | // Clients can define custom options in extensions of this message. See above. 621 | extensions 1000 to max; 622 | } 623 | 624 | 625 | // A message representing a option the parser does not recognize. This only 626 | // appears in options protos created by the compiler::Parser class. 627 | // DescriptorPool resolves these when building Descriptor objects. Therefore, 628 | // options protos in descriptor objects (e.g. returned by Descriptor::options(), 629 | // or produced by Descriptor::CopyTo()) will never have UninterpretedOptions 630 | // in them. 631 | message UninterpretedOption { 632 | // The name of the uninterpreted option. Each string represents a segment in 633 | // a dot-separated name. is_extension is true iff a segment represents an 634 | // extension (denoted with parentheses in options specs in .proto files). 635 | // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents 636 | // "foo.(bar.baz).qux". 637 | message NamePart { 638 | required string name_part = 1; 639 | required bool is_extension = 2; 640 | } 641 | repeated NamePart name = 2; 642 | 643 | // The value of the uninterpreted option, in whatever type the tokenizer 644 | // identified it as during parsing. Exactly one of these should be set. 645 | optional string identifier_value = 3; 646 | optional uint64 positive_int_value = 4; 647 | optional int64 negative_int_value = 5; 648 | optional double double_value = 6; 649 | optional bytes string_value = 7; 650 | optional string aggregate_value = 8; 651 | } 652 | 653 | // =================================================================== 654 | // Optional source code info 655 | 656 | // Encapsulates information about the original source file from which a 657 | // FileDescriptorProto was generated. 658 | message SourceCodeInfo { 659 | // A Location identifies a piece of source code in a .proto file which 660 | // corresponds to a particular definition. This information is intended 661 | // to be useful to IDEs, code indexers, documentation generators, and similar 662 | // tools. 663 | // 664 | // For example, say we have a file like: 665 | // message Foo { 666 | // optional string foo = 1; 667 | // } 668 | // Let's look at just the field definition: 669 | // optional string foo = 1; 670 | // ^ ^^ ^^ ^ ^^^ 671 | // a bc de f ghi 672 | // We have the following locations: 673 | // span path represents 674 | // [a,i) [ 4, 0, 2, 0 ] The whole field definition. 675 | // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). 676 | // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). 677 | // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). 678 | // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). 679 | // 680 | // Notes: 681 | // - A location may refer to a repeated field itself (i.e. not to any 682 | // particular index within it). This is used whenever a set of elements are 683 | // logically enclosed in a single code segment. For example, an entire 684 | // extend block (possibly containing multiple extension definitions) will 685 | // have an outer location whose path refers to the "extensions" repeated 686 | // field without an index. 687 | // - Multiple locations may have the same path. This happens when a single 688 | // logical declaration is spread out across multiple places. The most 689 | // obvious example is the "extend" block again -- there may be multiple 690 | // extend blocks in the same scope, each of which will have the same path. 691 | // - A location's span is not always a subset of its parent's span. For 692 | // example, the "extendee" of an extension declaration appears at the 693 | // beginning of the "extend" block and is shared by all extensions within 694 | // the block. 695 | // - Just because a location's span is a subset of some other location's span 696 | // does not mean that it is a descendent. For example, a "group" defines 697 | // both a type and a field in a single declaration. Thus, the locations 698 | // corresponding to the type and field and their components will overlap. 699 | // - Code which tries to interpret locations should probably be designed to 700 | // ignore those that it doesn't understand, as more types of locations could 701 | // be recorded in the future. 702 | repeated Location location = 1; 703 | message Location { 704 | // Identifies which part of the FileDescriptorProto was defined at this 705 | // location. 706 | // 707 | // Each element is a field number or an index. They form a path from 708 | // the root FileDescriptorProto to the place where the definition. For 709 | // example, this path: 710 | // [ 4, 3, 2, 7, 1 ] 711 | // refers to: 712 | // file.message_type(3) // 4, 3 713 | // .field(7) // 2, 7 714 | // .name() // 1 715 | // This is because FileDescriptorProto.message_type has field number 4: 716 | // repeated DescriptorProto message_type = 4; 717 | // and DescriptorProto.field has field number 2: 718 | // repeated FieldDescriptorProto field = 2; 719 | // and FieldDescriptorProto.name has field number 1: 720 | // optional string name = 1; 721 | // 722 | // Thus, the above path gives the location of a field name. If we removed 723 | // the last element: 724 | // [ 4, 3, 2, 7 ] 725 | // this path refers to the whole field declaration (from the beginning 726 | // of the label to the terminating semicolon). 727 | repeated int32 path = 1 [packed=true]; 728 | 729 | // Always has exactly three or four elements: start line, start column, 730 | // end line (optional, otherwise assumed same as start line), end column. 731 | // These are packed into a single field for efficiency. Note that line 732 | // and column numbers are zero-based -- typically you will want to add 733 | // 1 to each before displaying to a user. 734 | repeated int32 span = 2 [packed=true]; 735 | 736 | // If this SourceCodeInfo represents a complete declaration, these are any 737 | // comments appearing before and after the declaration which appear to be 738 | // attached to the declaration. 739 | // 740 | // A series of line comments appearing on consecutive lines, with no other 741 | // tokens appearing on those lines, will be treated as a single comment. 742 | // 743 | // leading_detached_comments will keep paragraphs of comments that appear 744 | // before (but not connected to) the current element. Each paragraph, 745 | // separated by empty lines, will be one comment element in the repeated 746 | // field. 747 | // 748 | // Only the comment content is provided; comment markers (e.g. //) are 749 | // stripped out. For block comments, leading whitespace and an asterisk 750 | // will be stripped from the beginning of each line other than the first. 751 | // Newlines are included in the output. 752 | // 753 | // Examples: 754 | // 755 | // optional int32 foo = 1; // Comment attached to foo. 756 | // // Comment attached to bar. 757 | // optional int32 bar = 2; 758 | // 759 | // optional string baz = 3; 760 | // // Comment attached to baz. 761 | // // Another line attached to baz. 762 | // 763 | // // Comment attached to qux. 764 | // // 765 | // // Another line attached to qux. 766 | // optional double qux = 4; 767 | // 768 | // // Detached comment for corge. This is not leading or trailing comments 769 | // // to qux or corge because there are blank lines separating it from 770 | // // both. 771 | // 772 | // // Detached comment for corge paragraph 2. 773 | // 774 | // optional string corge = 5; 775 | // /* Block comment attached 776 | // * to corge. Leading asterisks 777 | // * will be removed. */ 778 | // /* Block comment attached to 779 | // * grault. */ 780 | // optional int32 grault = 6; 781 | // 782 | // // ignored detached comments. 783 | optional string leading_comments = 3; 784 | optional string trailing_comments = 4; 785 | repeated string leading_detached_comments = 6; 786 | } 787 | } 788 | 789 | // Describes the relationship between generated code and its original source 790 | // file. A GeneratedCodeInfo message is associated with only one generated 791 | // source file, but may contain references to different source .proto files. 792 | message GeneratedCodeInfo { 793 | // An Annotation connects some span of text in generated code to an element 794 | // of its generating .proto file. 795 | repeated Annotation annotation = 1; 796 | message Annotation { 797 | // Identifies the element in the original source .proto file. This field 798 | // is formatted the same as SourceCodeInfo.Location.path. 799 | repeated int32 path = 1 [packed=true]; 800 | 801 | // Identifies the filesystem path to the original source .proto. 802 | optional string source_file = 2; 803 | 804 | // Identifies the starting offset in bytes in the generated code 805 | // that relates to the identified object. 806 | optional int32 begin = 3; 807 | 808 | // Identifies the ending offset in bytes in the generated code that 809 | // relates to the identified offset. The end offset should be one past 810 | // the last relevant byte (so the length of the text = end - begin). 811 | optional int32 end = 4; 812 | } 813 | } 814 | --------------------------------------------------------------------------------