Repository: elixir-grpc/grpc
Pull request #156 (2020-06-01 03:48:04)

feature: introduce get cert
First of all, thanks for this code! Awesome work.

This is similar to #119, but for [`:cowboy_req.cert/1`](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_req.cert/)

The motivation behind came because I'm implementing a IoT system and wanted to use Mutual TLS to identify the device, using the Common Name for that.

For that, I needed access to the client certificate so I could fetch the CN and finally uniquely identify the device, but gRPC didn't gave me access to the client certificate, so I ended up implementing this PR.

I still need to test that it returns the client certificate, but that would require me generating client keys and stuff, and it is already pretty late here, so I'll leave for another day.

Wanted to create the PR to check if you are interested in this.

Cheers


Patch:
diff --git a/lib/grpc/adapter/cowboy.ex b/lib/grpc/adapter/cowboy.ex
index 0ebb3a6..31bc099 100644
--- a/lib/grpc/adapter/cowboy.ex
+++ b/lib/grpc/adapter/cowboy.ex
@@ -11,6 +11,7 @@ defmodule GRPC.Adapter.Cowboy do
   alias GRPC.Adapter.Cowboy.Handler, as: Handler
 
   @default_num_acceptors 20
+  @default_max_connections 16384
 
   # Only used in starting a server manually using `GRPC.Server.start(servers)`
   @spec start(atom, GRPC.Server.servers_map(), non_neg_integer, keyword) ::
@@ -143,6 +144,10 @@ defmodule GRPC.Adapter.Cowboy do
     Handler.get_peer(pid)
   end
 
+  def get_cert(%{pid: pid}) do
+    Handler.get_cert(pid)
+  end
+
   def set_compressor(%{pid: pid}, compressor) do
     Handler.set_compressor(pid, compressor)
   end
@@ -154,6 +159,8 @@ defmodule GRPC.Adapter.Cowboy do
       ])
 
     idle_timeout = Keyword.get(opts, :idle_timeout, :infinity)
+    num_acceptors = Keyword.get(opts, :num_acceptors, @default_num_acceptors)
+    max_connections = Keyword.get(opts, :max_connections, @default_max_connections)
 
     # https://ninenines.eu/docs/en/cowboy/2.7/manual/cowboy_http2/
     opts =
@@ -175,7 +182,11 @@ defmodule GRPC.Adapter.Cowboy do
 
     [
       servers_name(endpoint, servers),
-      %{num_acceptors: @default_num_acceptors, socket_opts: socket_opts(port, opts)},
+      %{
+        num_acceptors: num_acceptors,
+        max_connections: max_connections,
+        socket_opts: socket_opts(port, opts)
+      },
       opts
     ]
   end
diff --git a/lib/grpc/adapter/cowboy/handler.ex b/lib/grpc/adapter/cowboy/handler.ex
index 437775d..370d016 100644
--- a/lib/grpc/adapter/cowboy/handler.ex
+++ b/lib/grpc/adapter/cowboy/handler.ex
@@ -150,6 +150,10 @@ defmodule GRPC.Adapter.Cowboy.Handler do
     sync_call(pid, :get_peer)
   end
 
+  def get_cert(pid) do
+    sync_call(pid, :get_cert)
+  end
+
   defp sync_call(pid, key) do
     ref = make_ref()
     send(pid, {key, ref, self()})
@@ -219,6 +223,12 @@ defmodule GRPC.Adapter.Cowboy.Handler do
     {:ok, req, state}
   end
 
+  def info({:get_cert, ref, pid}, req, state) do
+    peer = :cowboy_req.cert(req)
+    send(pid, {ref, peer})
+    {:ok, req, state}
+  end
+
   def info({:stream_body, data, opts, is_fin}, req, state) do
     # If compressor exists, compress is true by default
     compressor =
diff --git a/lib/grpc/server.ex b/lib/grpc/server.ex
index 842fe00..f30933e 100644
--- a/lib/grpc/server.ex
+++ b/lib/grpc/server.ex
@@ -41,7 +41,7 @@ defmodule GRPC.Server do
   @type rpc :: (GRPC.Server.rpc_req(), Stream.t() -> rpc_return)
 
   defmacro __using__(opts) do
-    quote bind_quoted: [opts: opts] do
+    quote bind_quoted: [opts: opts], location: :keep do
       service_mod = opts[:service]
       service_name = service_mod.__meta__(:name)
       codecs = opts[:codecs] || [GRPC.Codec.Proto]
diff --git a/lib/grpc/transport/http2.ex b/lib/grpc/transport/http2.ex
index a5cbf24..dfee977 100644
--- a/lib/grpc/transport/http2.ex
+++ b/lib/grpc/transport/http2.ex
@@ -53,20 +53,17 @@ defmodule GRPC.Transport.HTTP2 do
   def extract_metadata(headers) do
     headers
     |> Enum.filter(fn {k, _} -> is_metadata(k) end)
-    |> Enum.map(&decode_metadata/1)
-    |> Enum.into(%{})
+    |> Enum.into(%{}, &decode_metadata/1)
   end
 
   def decode_headers(headers) do
-    headers
-    |> Enum.map(fn {k, v} ->
+    Enum.into(headers, %{}, fn {k, v} ->
       if is_metadata(k) do
         decode_metadata({k, v})
       else
         {k, v}
       end
     end)
-    |> Enum.into(%{})
   end
 
   def encode_metadata(metadata) do


Test patch:
diff --git a/test/grpc/integration/server_test.exs b/test/grpc/integration/server_test.exs
index 3960bf1..c80f814 100644
--- a/test/grpc/integration/server_test.exs
+++ b/test/grpc/integration/server_test.exs
@@ -23,6 +23,16 @@ defmodule GRPC.Integration.ServerTest do
       Helloworld.HelloReply.new(message: "Hello, #{name}")
     end
 
+    def say_hello(%{name: "get cert"}, stream) do
+      case stream.adapter.get_cert(stream.payload) do
+        :undefined ->
+          Helloworld.HelloReply.new(message: "Hello, unauthenticated")
+
+        _ ->
+          Helloworld.HelloReply.new(message: "Hello, authenticated")
+      end
+    end
+
     def say_hello(req, _stream) do
       Helloworld.HelloReply.new(message: "Hello, #{req.name}")
     end
@@ -194,4 +204,14 @@ defmodule GRPC.Integration.ServerTest do
       assert reply.message == "Hello, 127.0.0.1"
     end)
   end
+
+  test "get cert returns correct client certificate when not present" do
+    run_server([HelloServer], fn port ->
+      {:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
+
+      req = Helloworld.HelloRequest.new(name: "get cert")
+      {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req)
+      assert reply.message == "Hello, unauthenticated"
+    end)
+  end
 end