-module(livery_grpc_reflection). -moduledoc """ The v1 gRPC server reflection service (`grpc.reflection.v1`). Lets tools like grpcurl and Postman discover services and message schemas at runtime, without a local `.proto`. Enable it by starting the server with `reflection => true` (see `livery_grpc:start_server/1`), which mounts this service and feeds it the descriptor set built from every registered service. `ServerReflectionInfo` is bidirectional: the client streams requests and this service streams one response each, answering `list_services`, `file_by_filename`, and `file_containing_symbol`. Extensions are a proto2 feature and report empty/not-found. The served file descriptors come from gpb's `descriptor/0` (the `descriptor` build option), which returns a `FileDescriptorSet`; this module splits it into the per-file `FileDescriptorProto` bytes the reflection protocol expects. """. -export([service/0, build/1]). -export([server_reflection_info/2]). -export_type([data/0]). %% Reflection lookup tables, built once at server start. -type data() :: #{ services := [binary()], files := #{binary() => [binary()]}, symbols := #{binary() => [binary()]} }. %% gRPC status codes used in error responses. -define(NOT_FOUND, 5). -define(UNIMPLEMENTED, 12). %%==================================================================== %% Registration and data %%==================================================================== -doc "The service spec to mount (done for you by `reflection => true`).". -spec service() -> livery_grpc_service:registration(). service() -> #{proto => reflection_pb, service => 'ServerReflection', handler => ?MODULE}. -doc """ Build the reflection lookup tables from the server's registrations: the exposed service names, and, keyed by file name and by symbol, the `FileDescriptorProto` bytes that define them. """. -spec build([livery_grpc_service:registration()]) -> data(). build(Registrations) -> Services = [ livery_grpc_service:service_full_name(P, S) || #{proto := P, service := S} <- Registrations ], Protos = lists:usort([P || #{proto := P} <- Registrations]), {Files, Symbols} = lists:foldl(fun index_proto/2, {#{}, #{}}, Protos), #{services => Services, files => Files, symbols => Symbols}. %% Index one proto module: map each of its file names and each symbol it %% defines (services, messages, enums) to the module's descriptor files. -spec index_proto(module(), {map(), map()}) -> {map(), map()}. index_proto(Proto, {Files, Symbols}) -> Fdps = file_descriptors(Proto), Files1 = lists:foldl(fun(Fdp, Acc) -> Acc#{fdp_name(Fdp) => Fdps} end, Files, Fdps), Symbols1 = lists:foldl(fun(Sym, Acc) -> Acc#{Sym => Fdps} end, Symbols, symbols(Proto)), {Files1, Symbols1}. %% The FileDescriptorProto bytes for a proto module (the file plus any %% dependencies), extracted from gpb's FileDescriptorSet. -spec file_descriptors(module()) -> [binary()]. file_descriptors(Proto) -> field1_values(Proto:descriptor()). -spec symbols(module()) -> [binary()]. symbols(Proto) -> Package = livery_grpc_service:package(Proto), Names = Proto:get_service_names() ++ Proto:get_msg_names() ++ Proto:get_enum_names(), [livery_grpc_service:qualify(Package, atom_to_binary(N, utf8)) || N <- Names]. %%==================================================================== %% Bidirectional handler %%==================================================================== -doc "The `ServerReflectionInfo` bidirectional RPC.". -spec server_reflection_info(livery_grpc_stream:stream(), livery_grpc_server:ctx()) -> ok | {error, term()}. server_reflection_info(Stream, Ctx) -> Data = maps:get(reflection, Ctx, empty_data()), loop(Stream, Data). -spec loop(livery_grpc_stream:stream(), data()) -> ok | {error, term()}. loop(Stream, Data) -> case livery_grpc_stream:recv(Stream) of {ok, Request, Stream1} -> case livery_grpc_stream:send(Stream1, respond(Request, Data)) of ok -> loop(Stream1, Data); {error, _} = E -> E end; {eof, _Stream1} -> ok; {error, Reason, _Stream1} -> {error, {internal, format(Reason)}} end. -spec respond(map(), data()) -> map(). respond(#{message_request := {list_services, _}} = Request, Data) -> Services = [#{name => N} || N <- maps:get(services, Data)], reply(Request, {list_services_response, #{service => Services}}); respond(#{message_request := {file_containing_symbol, Symbol}} = Request, Data) -> by_key(Request, Symbol, maps:get(symbols, Data)); respond(#{message_request := {file_by_filename, Name}} = Request, Data) -> by_key(Request, Name, maps:get(files, Data)); respond(#{message_request := {all_extension_numbers_of_type, Type}} = Request, _Data) -> reply( Request, {all_extension_numbers_response, #{base_type_name => Type, extension_number => []}} ); respond(#{message_request := {file_containing_extension, _}} = Request, _Data) -> error_reply(Request, ?NOT_FOUND, <<"extensions are not supported">>); respond(Request, _Data) -> error_reply(Request, ?UNIMPLEMENTED, <<"unsupported reflection request">>). -spec by_key(map(), binary(), #{binary() => [binary()]}) -> map(). by_key(Request, Key, Table) -> case maps:find(Key, Table) of {ok, Fdps} -> reply(Request, {file_descriptor_response, #{file_descriptor_proto => Fdps}}); error -> error_reply(Request, ?NOT_FOUND, <<"symbol not found: ", Key/binary>>) end. -spec reply(map(), tuple()) -> map(). reply(Request, MessageResponse) -> #{ valid_host => maps:get(host, Request, <<>>), original_request => Request, message_response => MessageResponse }. -spec error_reply(map(), integer(), binary()) -> map(). error_reply(Request, Code, Message) -> reply(Request, {error_response, #{error_code => Code, error_message => Message}}). -spec empty_data() -> data(). empty_data() -> #{services => [], files => #{}, symbols => #{}}. %%==================================================================== %% Minimal protobuf descriptor parsing %%==================================================================== %% The FileDescriptorProto `name` field (field 1) as a binary. -spec fdp_name(binary()) -> binary(). fdp_name(Fdp) -> case field1_values(Fdp) of [Name | _] -> Name; [] -> <<>> end. %% Every length-delimited field-1 value at the top level of a protobuf %% message. For a FileDescriptorSet that is each FileDescriptorProto; for a %% FileDescriptorProto field 1 is its name. -spec field1_values(binary()) -> [binary()]. field1_values(Bin) -> [V || {1, V} <- scan(Bin, [])]. %% Scan top-level fields, keeping length-delimited values as `{Field, Bin}` %% and skipping everything else. -spec scan(binary(), [{non_neg_integer(), binary()}]) -> [{non_neg_integer(), binary()}]. scan(<<>>, Acc) -> lists:reverse(Acc); scan(Bin, Acc) -> {Tag, Rest} = varint(Bin), Field = Tag bsr 3, case Tag band 7 of 2 -> {Len, Rest1} = varint(Rest), <> = Rest1, scan(Rest2, [{Field, Value} | Acc]); 0 -> {_V, Rest1} = varint(Rest), scan(Rest1, Acc); 1 -> <<_:8/binary, Rest1/binary>> = Rest, scan(Rest1, Acc); 5 -> <<_:4/binary, Rest1/binary>> = Rest, scan(Rest1, Acc) end. -spec varint(binary()) -> {non_neg_integer(), binary()}. varint(Bin) -> varint(Bin, 0, 0). -spec varint(binary(), non_neg_integer(), non_neg_integer()) -> {non_neg_integer(), binary()}. varint(<<1:1, Group:7, Rest/binary>>, Shift, Acc) -> varint(Rest, Shift + 7, Acc bor (Group bsl Shift)); varint(<<0:1, Group:7, Rest/binary>>, Shift, Acc) -> {Acc bor (Group bsl Shift), Rest}. -spec format(term()) -> binary(). format(Reason) -> iolist_to_binary(io_lib:format("~p", [Reason])).