gRPC C++ SSL上でストリーミング通信をする

#はじめに
linuxでのgRPCのインストール方法をまとめておきます。
今回はdockerコンテナでlinux環境を用意します。

今回コードの説明などは省きます。
参考文献を参照してください。

#インストール
`Dockerfile`で書きます。
cmakeやgrpcをインストールします。

```Dockerfile:Dockerfile
FROM ubuntu:18.04

ENV CMAKE_INSTALL_DIR /local
ENV PATH $PATH:/grpc/cmake/build
ENV PATH $PATH:/grpc/cmake/build/third_party/protobuf
ENV PATH $PATH:$CMAKE_INSTALL_DIR/bin

RUN apt-get update && \
    apt-get install -y \
            git \
            wget \
            clang \
            ca-certificates \
            build-essential \
            libssl-dev \
            make \
            autoconf \
            automake \
            pkg-config \
            libtool \
            golang \
            curl && \
    # Install cmake
    wget -q -O cmake-linux.sh https://github.com/Kitware/CMake/releases/download/v3.17.0/cmake-3.17.0-Linux-x86_64.sh && \
    mkdir -p $CMAKE_INSTALL_DIR && \
    sh cmake-linux.sh -- --skip-license --prefix=$CMAKE_INSTALL_DIR && \
    rm cmake-linux.sh && \
    # Install grpc
    cd / && git clone -b v1.28.1 https://github.com/grpc/grpc && \
    cd /grpc && \
    git submodule update --init && \
    mkdir -p cmake/build && \
    cd cmake/build && \
    cmake ../.. && \
    make && \
    make install && \
    ldconfig  

WORKDIR /grpc-communication
```

`docker-compose`ファイルも用意しておきます。

```docker-compose:docker-compose.yml
version: '3.3'
services:
  dev:
    build: .
    volumes:
      - type: bind
        source: .
        target: /grpc_communication
```

# コンテナに入る

```bash:bash
$ docker-compose build
$ docker-compose run --rm dev
```

`Dockerfile`のあるホストマシンのディレクトリをコンテナにマウントさせているので、ホスト上でファイルの編集を行えます。

# gRPC関連の作業
## `.proto`ファイルの作成

```proto:chat.proto
syntax = "proto3";

package chat;

service Chat {
    rpc HogeChat(stream Note) returns (stream Note) {} 
}

message Note {
    string message = 1;
}
```

## `.proto`ファイルを用いていろいろ作成

```bash:bash
$ protoc -I ./ --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ./chat.proto
$ protoc -I ./ --cpp_out=. ./chat.proto
```

`chat.grpc.pb.h`、`chat.grpc.pb.cc`、`chat.pb.h`、`chat.pb.cc`ファイルができます。
今後使います。

## serverの実装

```cpp:chat_server.cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>

#include <grpc/grpc.h>
#include <grpcpp/server.h>
#include <grpcpp/server_builder.h>
#include <grpcpp/server_context.h>
#include <grpcpp/security/server_credentials.h>
#include "chat.grpc.pb.h"

using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::ServerReaderWriter;
using grpc::Status;
using chat::Note;
using chat::Chat;

Note MakeNote(const std::string& message) {
  Note n;
  n.set_message(message);
  return n;
}

void readFile(std::string filename, std::string &content) {
    std::ifstream file(filename, std::ios::in);
    while(file.is_open()) {
        std::stringstream ss;
        ss << file.rdbuf ();
        file.close();
        content = ss.str ();
    }
}

// 証明書を読む
std::string readFile(std::string filename) {
    std::string file_content;
    std::string str_line;
    std::ifstream file(filename, std::ios::in);
    while(getline(file, str_line)) {
        file_content += str_line;
    }
    file.close();
    return file_content;
}

class ChatImpl final : public Chat::Service {
    public:
        Status HogeChat(ServerContext* context,
                    ServerReaderWriter<Note, Note>* stream) override {
            Note note;
            std::vector<Note> notes{
                MakeNote("Hey, Client!"),
                MakeNote("Hey, Apple!"),
                MakeNote("Hey, Banana!"),
                MakeNote("Hey, Melon!")};
            while (stream->Read(&note)) {
                std::unique_lock<std::mutex> lock(mu_);
                std::cout << "Got message " << note.message() << std::endl;
            }
            for (const Note& n : notes) {
                std::cout << "Sending message " << n.message() << std::endl;
                stream->Write(n);
            }

            return Status::OK;     
        }
    private:
        std::mutex mu_;
};

void RunServer() {
    std::string server_address("0.0.0.0:50051");
    ChatImpl service;

    ServerBuilder builder;

    // SSLの設定
    std::string ca_crt_content;
    std::string server_crt_content;
    std::string server_key_content;
    readFile("ca.crt", ca_crt_content);
    readFile("server.crt", server_crt_content);
    readFile("server.key", server_key_content);
    grpc::SslServerCredentialsOptions sslOpts;
    grpc::SslServerCredentialsOptions::PemKeyCertPair keycert = { server_key_content, server_crt_content };
    grpc::SslServerCredentialsOptions sslOps;
	sslOps.pem_root_certs = ca_crt_content;
    sslOps.pem_key_cert_pairs.push_back(keycert);

    builder.AddListeningPort(server_address, grpc::SslServerCredentials(sslOps));
    builder.RegisterService(&service);
    std::unique_ptr<Server> server(builder.BuildAndStart());
    std::cout << "Server listening on " << server_address << std::endl;
    server->Wait();
}

int main(int argc, char** argv) {
  RunServer();

  return 0;
}
```

## clientの実装

```cpp:chat_client.cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <thread>

#include <grpc/grpc.h>
#include <grpcpp/channel.h>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
#include "chat.grpc.pb.h"

using grpc::Channel;
using grpc::ClientContext;
using grpc::ClientReaderWriter;
using grpc::Status;
using chat::Note;
using chat::Chat;

Note MakeNote(const std::string& message) {
  Note n;
  n.set_message(message);
  return n;
}

// 証明書を読む
void readFile(std::string filename, std::string &content) {
    std::ifstream file(filename, std::ios::in);
    while(file.is_open()) {
        std::stringstream ss;
        ss << file.rdbuf ();
        file.close();
        content = ss.str ();
    }
}

class ChatClient {
    public:
        ChatClient(std::shared_ptr<Channel> channel): stub_(Chat::NewStub(channel)) {}

    void HogeChat() {
        ClientContext context;

        std::shared_ptr<ClientReaderWriter<Note, Note> > stream(
            stub_->HogeChat(&context));

        std::thread writer([stream]() {
            std::vector<Note> notes{ 
                MakeNote("Hello, Server!"),
                MakeNote("Hello, Apple!"),
                MakeNote("Hello, Banana!"),
                MakeNote("Hello, Melon!")};
            for (const Note& note : notes) {
                std::cout << "Sending message " << note.message() << std::endl;
                stream->Write(note);
            }
            stream->WritesDone();
        });

        Note server_note;
        while (stream->Read(&server_note)) {
            std::cout << "Got message " << server_note.message() << std::endl;
        }
        writer.join();
        Status status = stream->Finish();
        if (!status.ok()) {
            std::cout << "RouteChat rpc failed." << std::endl;
        }
    }

 private:
  std::unique_ptr<Chat::Stub> stub_;
};

int main(int argc, char** argv) {
    // sslの設定
    grpc::SslCredentialsOptions sslOpts;
    std::string ca_crt_content;
    std::string client_crt_content;
    std::string client_key_content;
    readFile("ca.crt", ca_crt_content);
    readFile("client.crt", client_crt_content);
    readFile("client.key", client_key_content);
    sslOpts.pem_cert_chain = client_crt_content;
    sslOpts.pem_private_key = client_key_content;
    sslOpts.pem_root_certs = ca_crt_content;
    auto channel_creds = grpc::SslCredentials(sslOpts);

    ChatClient chat(grpc::CreateChannel("localhost:50051", channel_creds));
    std::cout << "-------------- Chat --------------" << std::endl;
    chat.HogeChat();
    return 0;
}
```

## 証明書、秘密鍵等の作成
ここはテストなのでかなり適当にしています。
ちゃんとする人はちゃんとしましょう。


```bash:bash
# Generate valid CA
$ openssl genrsa -passout pass:1234 -des3 -out ca.key 4096
$ openssl req -passin pass:1234 -new -x509 -days 365 -key ca.key -out ca.crt -subj  "/C=JP/ST=Tokyo/L=Tokyo/O=Test/OU=Test/CN=Root CA"

# Generate valid Server Key/Cert
$ openssl genrsa -passout pass:1234 -des3 -out server.key 4096
$ openssl req -passin pass:1234 -new -key server.key -out server.csr -subj  "/C=JP/ST=Tokyo/L=Tokyo/O=Test/OU=Server/CN=localhost"
$ openssl x509 -req -passin pass:1234 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

# Remove passphrase from the Server Key
$ openssl rsa -passin pass:1234 -in server.key -out server.key

# Generate valid Client Key/Cert
$ openssl genrsa -passout pass:1234 -des3 -out client.key 4096
$ openssl req -passin pass:1234 -new -key client.key -out client.csr -subj  "/C=JP/ST=Tokyo/L=Tokyo/O=Test/OU=Client/CN=localhost"
$ openssl x509 -passin pass:1234 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

# Remove passphrase from Client Key
$ openssl rsa -passin pass:1234 -in client.key -out client.key
```

# 実行する
まず、ビルドします。
`Makefile`を用意しました。

```Makefile:Makefile
HOST_SYSTEM = $(shell uname | cut -f 1 -d_)
SYSTEM ?= $(HOST_SYSTEM)
CXX = g++
CPPFLAGS += `pkg-config --cflags protobuf grpc`
CXXFLAGS += -std=c++11
ifeq ($(SYSTEM),Darwin)
LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
					 -pthread\
           -lgrpc++_reflection\
           -ldl
else
LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
           -pthread\
           -Wl,--no-as-needed -lgrpc++_reflection -Wl,--as-needed\
           -ldl
endif
PROTOC = protoc
GRPC_CPP_PLUGIN = grpc_cpp_plugin
GRPC_CPP_PLUGIN_PATH ?= `which $(GRPC_CPP_PLUGIN)`

PROTOS_PATH = ./

vpath %.proto $(PROTOS_PATH)

all: chat_client chat_server

chat_client: chat.pb.o chat.grpc.pb.o chat_client.o 
	$(CXX) $^ $(LDFLAGS) -o $@

chat_server: chat.pb.o chat.grpc.pb.o chat_server.o 
	$(CXX) $^ $(LDFLAGS) -o $@

%.grpc.pb.cc: %.proto
	$(PROTOC) -I $(PROTOS_PATH) --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN_PATH) $<

%.pb.cc: %.proto
	$(PROTOC) -I $(PROTOS_PATH) --cpp_out=. $<

clean:
	rm -f *.o *.pb.cc *.pb.h chat_client chat_server
```

ではビルドします。

```bash:bash
$ make
```

次に、実行してみましょう。

```
$ ./chat_server
```

```
$ ./chat_client
```

```
$ ./chat_server
Server listening on 0.0.0.0:50051
Got message Hello, Server!
Got message Hello, Apple!
Got message Hello, Banana!
Got message Hello, Melon!
Sending message Hey, Client!
Sending message Hey, Apple!
Sending message Hey, Banana!
Sending message Hey, Melon!
```

```
$ ./chat_client
-------------- Chat --------------
Sending message Hello, Server!
Sending message Hello, Apple!
Sending message Hello, Banana!
Sending message Hello, Melon!
Got message Hey, Client!
Got message Hey, Apple!
Got message Hey, Banana!
Got message Hey, Melon!
```
# エラー対処
証明書を作成している段階でエラーが発生しました。

```
random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/root/.rnd
```
以下の方法で対処しました。
適切な対処方法なのかどうかは未調査です。

`/etc/ssl/openssl.cnf` の以下の行を削除

```
RANDFILE              = $ENV::HOME/.rnd
```
# 終わりに
クライアント、サーバそれぞれが複数のメッセージを受信、送信することができました。

ちなみに、tcpdumpを使ってパケットキャプチャを行うと実際に通信が暗号化されていることが確認できます。
以下のコマンドを実行した後で、`./chat_server`、`./chat_client`を実行します。

```
$ apt-get install tcpdump
$ tcpdump -i lo -X dst port 50051 or src port 50051
```

# 参考文献
- A basic tutorial introduction to gRPC in C++.
    - https://grpc.io/docs/languages/cpp/basics/
- gRPC Authentication
    - https://grpc.io/docs/guides/auth/
- gRPCのSSL上での通信関連のissue
    - https://github.com/grpc/grpc/issues/9593