Skip to content

Commit 37fd806

Browse files
aseigosleipnirpolvalente
authored
Access mode detection (transcode vs grpcweb vs grpc) (#412)
* Detect preflight requests These typically come from grpcweb clients doing OPTIONS checks for CORS * Detect client types: grpc, grpcweb, web This replaces the `http_transcode` member of the `Server.Stream` struct as transcoding is correlated with being a web client (*not* a grpcweb client, however!) Knowing the type of client also allows for detection of when to send different types of headers, for e.g. CORS * Add a CORS header injection Interceptor It an option `:allow` option to define which origins are allowed. This may be a static string or a function which will be passed the current `req` and `stream` structs to choose what to allow. * Reintroduce the boolean http_transcode member of the Stream struct NOTE: will merge this with commit c39c834 post-review/discussion. I am leaving it as a separate commit for now for ease of continued discussion on the current PR. * web can be detected in extract_subtype * change access_type to access_mode, :web to :http_transcoding this more accurately and directly maps to the grpc terminology, and is probably clearer. * default opts to an empty list, whitespace fix * add tests for the CORS header interceptor * Assign to a temporary variable and assign that to Stream.http_transcode * provide access to request headers in the Stream object * only set cors when the sec-fetch-mode header indicates it * another test: if there is NO sec-fetch-mode header * Make access-control-allow-headers configurable, and optional Only send access-control-allow-headers when the client requests it with access-control-request-headers and allow the same configurability as access-control-allow-origin provides * align variable names with http header names * Add a CORS example to the README * format * Verbage. Co-authored-by: Paulo Valente <16843419+polvalente@users.noreply.github.com> * remove default for allow_origin in CORS interceptor * raise on failed allow_origin type, and test for that * allow_origin, not allow_header * test: add missing compile-time test * chore: format readme * chore: fix Note bold * docs: cleanup readme linking * docs: proper module linking in exdoc * fix: do not use Keyword.validate due to Elixir 1.12 support --------- Co-authored-by: Adriano Santos <solid.sistemas@gmail.com> Co-authored-by: Paulo Valente <16843419+polvalente@users.noreply.github.com>
1 parent 9a9d73a commit 37fd806

File tree

7 files changed

+552
-50
lines changed

7 files changed

+552
-50
lines changed

README.md

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ An Elixir implementation of [gRPC](http://www.grpc.io/).
1515
- [Usage](#usage)
1616
- [Simple RPC](#simple-rpc)
1717
- [HTTP Transcoding](#http-transcoding)
18+
- [CORS](#cors)
1819
- [Start Application](#start-application)
1920
- [Features](#features)
2021
- [Benchmark](#benchmark)
@@ -24,13 +25,13 @@ An Elixir implementation of [gRPC](http://www.grpc.io/).
2425

2526
The package can be installed as:
2627

27-
```elixir
28-
def deps do
29-
[
30-
{:grpc, "~> 0.9"}
31-
]
32-
end
33-
```
28+
```elixir
29+
def deps do
30+
[
31+
{:grpc, "~> 0.9"}
32+
]
33+
end
34+
```
3435

3536
## Usage
3637

@@ -96,7 +97,7 @@ end
9697

9798
We will use this module [in the gRPC server startup section](#start-application).
9899

99-
**__Note:__** For other types of RPC call like streams see [here](interop/lib/interop/server.ex).
100+
**Note:** For other types of RPC call like streams see [here](interop/lib/interop/server.ex).
100101

101102
### **HTTP Transcoding**
102103

@@ -152,6 +153,7 @@ mix protobuf.generate \
152153
```
153154

154155
3. Enable http_transcode option in your Server module
156+
155157
```elixir
156158
defmodule Helloworld.Greeter.Server do
157159
use GRPC.Server,
@@ -167,6 +169,23 @@ end
167169

168170
See full application code in [helloworld_transcoding](examples/helloworld_transcoding) example.
169171

172+
### **CORS**
173+
174+
When accessing gRPC from a browser via HTTP transcoding or gRPC-Web, CORS headers may be required for the browser to allow access to the gRPC endpoint. Adding CORS headers can be done by using `GRPC.Server.Interceptors.CORS` as an interceptor in your `GRPC.Endpoint` module, configuring it as decribed in the module documentation:
175+
176+
Example:
177+
178+
```elixir
179+
# Define your endpoint
180+
defmodule Helloworld.Endpoint do
181+
use GRPC.Endpoint
182+
183+
intercept GRPC.Server.Interceptors.Logger
184+
intercept GRPC.Server.Interceptors.CORS, allow_origin: "mydomain.io"
185+
run Helloworld.Greeter.Server
186+
end
187+
```
188+
170189
### **Start Application**
171190

172191
1. Start gRPC Server in your supervisor tree or Application module:
@@ -231,7 +250,7 @@ The accepted options for configuration are the ones listed on [Mint.HTTP.connect
231250
- [HTTP Transcoding](https://cloud.google.com/endpoints/docs/grpc/transcoding)
232251
- [TLS Authentication](https://grpc.io/docs/guides/auth/#supported-auth-mechanisms)
233252
- [Error handling](https://grpc.io/docs/guides/error/)
234-
- Interceptors (See [`GRPC.Endpoint`](https://github.com/elixir-grpc/grpc/blob/master/lib/grpc/endpoint.ex))
253+
- [Interceptors](`GRPC.Endpoint`)
235254
- [Connection Backoff](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md)
236255
- Data compression
237256
- [gRPC Reflection](https://github.com/elixir-grpc/grpc-reflection)

lib/grpc/server.ex

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ defmodule GRPC.Server do
152152
path = "/#{service_name}/#{name}"
153153
grpc_type = GRPC.Service.grpc_type(rpc)
154154

155-
def __call_rpc__(unquote(path), :post, stream) do
155+
def __call_rpc__(unquote(path), http_method, stream)
156+
when http_method == :post or http_method == :options do
156157
GRPC.Server.call(
157158
unquote(service_mod),
158159
%{
@@ -178,8 +179,7 @@ defmodule GRPC.Server do
178179
| service_name: unquote(service_name),
179180
method_name: unquote(to_string(name)),
180181
grpc_type: unquote(grpc_type),
181-
http_method: unquote(http_method),
182-
http_transcode: unquote(http_transcode)
182+
http_method: unquote(http_method)
183183
},
184184
unquote(Macro.escape(put_elem(rpc, 0, func_name))),
185185
unquote(func_name)
@@ -252,7 +252,7 @@ defmodule GRPC.Server do
252252
codec: codec,
253253
adapter: adapter,
254254
payload: payload,
255-
http_transcode: true
255+
access_mode: :http_transcoding
256256
} = stream,
257257
func_name
258258
) do
@@ -271,6 +271,10 @@ defmodule GRPC.Server do
271271
end
272272
end
273273

274+
defp do_handle_request(false, res_stream, %{is_preflight?: true} = stream, func_name) do
275+
call_with_interceptors(res_stream, func_name, stream, [])
276+
end
277+
274278
defp do_handle_request(
275279
false,
276280
res_stream,
@@ -339,7 +343,8 @@ defmodule GRPC.Server do
339343
) do
340344
GRPC.Telemetry.server_span(server, endpoint, func_name, stream, fn ->
341345
last = fn r, s ->
342-
reply = apply(server, func_name, [r, s])
346+
# no response is rquired for preflight requests
347+
reply = if stream.is_preflight?, do: [], else: apply(server, func_name, [r, s])
343348

344349
if res_stream do
345350
{:ok, stream}

lib/grpc/server/adapters/cowboy/handler.ex

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
2828
@type stream_state :: %{
2929
pid: server_rpc_pid :: pid,
3030
handling_timer: timeout_timer_ref :: reference,
31-
pending_reader: nil | pending_reader
31+
pending_reader: nil | pending_reader,
32+
access_mode: GRPC.Server.Stream.access_mode()
3233
}
3334
@type init_result ::
3435
{:cowboy_loop, :cowboy_req.req(), stream_state} | {:ok, :cowboy_req.req(), init_state}
@@ -56,10 +57,12 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
5657
|> String.downcase()
5758
|> String.to_existing_atom()
5859

59-
with {:ok, sub_type, content_type} <- find_content_type_subtype(req),
60+
with {:ok, access_mode, sub_type, content_type} <- find_content_type_subtype(req),
6061
{:ok, codec} <- find_codec(sub_type, content_type, server),
6162
{:ok, compressor} <- find_compressor(req, server) do
6263
stream_pid = self()
64+
http_transcode = access_mode == :http_transcoding
65+
request_headers = :cowboy_req.headers(req)
6366

6467
stream = %GRPC.Server.Stream{
6568
server: server,
@@ -69,16 +72,19 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
6972
local: opts[:local],
7073
codec: codec,
7174
http_method: http_method,
75+
http_request_headers: request_headers,
76+
http_transcode: http_transcode,
7277
compressor: compressor,
73-
http_transcode: transcode?(req)
78+
is_preflight?: preflight?(req),
79+
access_mode: access_mode
7480
}
7581

7682
server_rpc_pid = :proc_lib.spawn_link(__MODULE__, :call_rpc, [server, route, stream])
7783
Process.flag(:trap_exit, true)
7884

7985
req = :cowboy_req.set_resp_headers(HTTP2.server_headers(stream), req)
8086

81-
timeout = :cowboy_req.header("grpc-timeout", req)
87+
timeout = Map.get(request_headers, "grpc-timeout")
8288

8389
timer_ref =
8490
if is_binary(timeout) do
@@ -89,7 +95,16 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
8995
)
9096
end
9197

92-
{:cowboy_loop, req, %{pid: server_rpc_pid, handling_timer: timer_ref, pending_reader: nil}}
98+
{
99+
:cowboy_loop,
100+
req,
101+
%{
102+
pid: server_rpc_pid,
103+
handling_timer: timer_ref,
104+
pending_reader: nil,
105+
access_mode: access_mode
106+
}
107+
}
93108
else
94109
{:error, error} ->
95110
Logger.error(fn -> inspect(error) end)
@@ -121,12 +136,9 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
121136
content_type
122137
end
123138

124-
find_subtype(content_type)
125-
end
126-
127-
defp find_subtype(content_type) do
128-
{:ok, subtype} = extract_subtype(content_type)
129-
{:ok, subtype, content_type}
139+
{:ok, access_mode, subtype} = extract_subtype(content_type)
140+
access_mode = resolve_access_mode(req, access_mode, subtype)
141+
{:ok, access_mode, subtype, content_type}
130142
end
131143

132144
defp find_compressor(req, server) do
@@ -600,38 +612,43 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
600612
end
601613
end
602614

603-
defp extract_subtype("application/json"), do: {:ok, "json"}
604-
defp extract_subtype("application/grpc"), do: {:ok, "proto"}
605-
defp extract_subtype("application/grpc+"), do: {:ok, "proto"}
606-
defp extract_subtype("application/grpc;"), do: {:ok, "proto"}
607-
defp extract_subtype(<<"application/grpc+", rest::binary>>), do: {:ok, rest}
608-
defp extract_subtype(<<"application/grpc;", rest::binary>>), do: {:ok, rest}
615+
defp extract_subtype("application/json"), do: {:ok, :http_transcoding, "json"}
616+
defp extract_subtype("application/grpc"), do: {:ok, :grpc, "proto"}
617+
defp extract_subtype("application/grpc+"), do: {:ok, :grpc, "proto"}
618+
defp extract_subtype("application/grpc;"), do: {:ok, :grpc, "proto"}
619+
defp extract_subtype(<<"application/grpc+", rest::binary>>), do: {:ok, :grpc, rest}
620+
defp extract_subtype(<<"application/grpc;", rest::binary>>), do: {:ok, :grpc, rest}
609621

610-
defp extract_subtype("application/grpc-web"), do: {:ok, "proto"}
611-
defp extract_subtype("application/grpc-web+"), do: {:ok, "proto"}
612-
defp extract_subtype("application/grpc-web;"), do: {:ok, "proto"}
613-
defp extract_subtype("application/grpc-web-text"), do: {:ok, "text"}
614-
defp extract_subtype("application/grpc-web+" <> rest), do: {:ok, rest}
615-
defp extract_subtype("application/grpc-web-text+" <> rest), do: {:ok, rest}
622+
defp extract_subtype("application/grpc-web"), do: {:ok, :grpcweb, "proto"}
623+
defp extract_subtype("application/grpc-web+"), do: {:ok, :grpcweb, "proto"}
624+
defp extract_subtype("application/grpc-web;"), do: {:ok, :grpcweb, "proto"}
625+
defp extract_subtype("application/grpc-web-text"), do: {:ok, :grpcweb, "text"}
626+
defp extract_subtype("application/grpc-web+" <> rest), do: {:ok, :grpcweb, rest}
627+
defp extract_subtype("application/grpc-web-text+" <> rest), do: {:ok, :grpcweb, rest}
616628

617629
defp extract_subtype(type) do
618630
Logger.warning("Got unknown content-type #{type}, please create an issue.")
619-
{:ok, "proto"}
631+
{:ok, :grpc, "proto"}
620632
end
621633

622-
defp transcode?(%{version: "HTTP/1.1"}), do: true
634+
defp resolve_access_mode(%{version: "HTTP/1.1"}, _detected_access_mode, _type_subtype),
635+
do: :http_transcoding
623636

624-
defp transcode?(req) do
625-
case find_content_type_subtype(req) do
626-
{:ok, "json", _} -> true
627-
_ -> false
628-
end
629-
end
637+
defp resolve_access_mode(%{method: "OPTIONS"}, _detected_access_mode, _type_subtype),
638+
do: :grpcweb
639+
640+
defp resolve_access_mode(_req, detected_access_mode, _type_subtype), do: detected_access_mode
641+
642+
defp preflight?(%{method: "OPTIONS"}), do: true
643+
defp preflight?(_), do: false
630644

631645
defp send_error(req, error, state, reason) do
632646
trailers = HTTP2.server_trailers(error.status, error.message)
633647

634-
status = if transcode?(req), do: GRPC.Status.http_code(error.status), else: 200
648+
status =
649+
if state.access_mode == :http_transcoding,
650+
do: GRPC.Status.http_code(error.status),
651+
else: 200
635652

636653
if pid = Map.get(state, :pid) do
637654
exit_handler(pid, reason)

0 commit comments

Comments
 (0)