├── 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 |
--------------------------------------------------------------------------------