From 0dacd6703749e87d37835af0a8e29a7e13576d5f Mon Sep 17 00:00:00 2001 From: mu <59917266+4eUeP@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:02:32 +0800 Subject: [PATCH] Support basic auth (incomplete) --- hs-grpc-server/HsGrpc/Server.hs | 6 +- hs-grpc-server/HsGrpc/Server/FFI.hs | 2 + hs-grpc-server/HsGrpc/Server/Types.hsc | 38 +++++++- hs-grpc-server/cbits/hs_grpc_server.cpp | 63 ++++++++++--- hs-grpc-server/hs-grpc-server.cabal | 3 +- hs-grpc-server/include/hs_grpc_server.h | 11 +++ hs-grpc-server/include/hsgrpc/auth.hpp | 120 ++++++++++++++++++++++++ 7 files changed, 225 insertions(+), 18 deletions(-) create mode 100644 hs-grpc-server/include/hsgrpc/auth.hpp diff --git a/hs-grpc-server/HsGrpc/Server.hs b/hs-grpc-server/HsGrpc/Server.hs index fd39595..756f7c4 100644 --- a/hs-grpc-server/HsGrpc/Server.hs +++ b/hs-grpc-server/HsGrpc/Server.hs @@ -97,6 +97,7 @@ runServer ServerOptions{..} handlers = do server <- newAsioServer serverHost serverPort serverParallelism serverSslOptions + serverAuthTokens serverChannelArgs serverInterceptors runAsioGrpc server handlers serverOnStarted serverInternalChannelSize @@ -110,17 +111,20 @@ newAsioServer -> Int -- ^ port -> Int -- ^ parallelism -> Maybe SslServerCredentialsOptions + -> [ByteString] -- ^ auth tokens -> [ChannelArg] -> [ServerInterceptor] -> IO AsioServer -newAsioServer host port parallelism m_sslOpts chanArgs interceptors = do +newAsioServer host port parallelism m_sslOpts tokens chanArgs interceptors = do ptr <- HF.withShortByteString host $ \host' host_len -> HF.withMaybePtr m_sslOpts withSslServerCredentialsOptions $ \sslOpts' -> + withAuthTokens (basicAuthTokens tokens) $ \authTokens' -> withChannelArgs chanArgs $ \chanArgs' chanArgs_size -> HF.withPrimList (map toCItcptFact interceptors) $ \intcept' intcept_size -> new_asio_server host' host_len port parallelism sslOpts' + authTokens' chanArgs' chanArgs_size intcept' intcept_size if ptr == nullPtr then Ex.throwIO $ ServerException "newGrpcServer failed!" diff --git a/hs-grpc-server/HsGrpc/Server/FFI.hs b/hs-grpc-server/HsGrpc/Server/FFI.hs index 5e87f8d..ae4c89b 100644 --- a/hs-grpc-server/HsGrpc/Server/FFI.hs +++ b/hs-grpc-server/HsGrpc/Server/FFI.hs @@ -24,6 +24,8 @@ foreign import ccall unsafe "new_asio_server" -- ^ parallelism -> Ptr SslServerCredentialsOptions -- ^ tls options + -> Ptr AuthTokens + -- ^ auth tokens -> Ptr ChannelArg -> Int -- ^ Grpc Channel arguments -> Ptr (Ptr CServerInterceptorFactory) -> Int diff --git a/hs-grpc-server/HsGrpc/Server/Types.hsc b/hs-grpc-server/HsGrpc/Server/Types.hsc index 13ec460..d0ef5fe 100644 --- a/hs-grpc-server/HsGrpc/Server/Types.hsc +++ b/hs-grpc-server/HsGrpc/Server/Types.hsc @@ -52,6 +52,11 @@ module HsGrpc.Server.Types , pattern GrpcSslRequestAndRequireClientCertificateButDontVerify , pattern GrpcSslRequestAndRequireClientCertificateAndVerify + -- * AuthTokens + , AuthTokens + , basicAuthTokens + , withAuthTokens -- XXX: this function should be in a Internal module + -- * Interceptors , CServerInterceptorFactory , ServerInterceptor (..) @@ -118,6 +123,7 @@ data ServerOptions = ServerOptions , serverPort :: !Int , serverParallelism :: !Int , serverSslOptions :: !(Maybe SslServerCredentialsOptions) + , serverAuthTokens :: ![ByteString] , serverOnStarted :: !(Maybe (IO ())) , serverInterceptors :: ![ServerInterceptor] , serverChannelArgs :: ![ChannelArg] @@ -131,6 +137,7 @@ defaultServerOpts = ServerOptions , serverPort = 50051 , serverParallelism = 0 , serverSslOptions = Nothing + , serverAuthTokens = [] , serverOnStarted = Nothing , serverInterceptors = [] , serverChannelArgs = [] @@ -418,6 +425,34 @@ newtype GrpcSslClientCertificateRequestType = GrpcSslClientCertificateRequestTyp , GrpcSslRequestAndRequireClientCertificateAndVerify #-} +------------------------------------------------------------------------------- +-- Auth tokens + +newtype AuthTokens = AuthTokens { unAuthTokens :: [ByteString] } + deriving (Show, Eq) + +-- > bash$ echo -n "user:passwd" | base64 +-- > dXNlcjpwYXNzd2Q= +-- +-- > basicAuthTokens ["dXNlcjpwYXNzd2Q="] +basicAuthTokens :: [ByteString] -> AuthTokens +basicAuthTokens = AuthTokens . map ("Basic " <>) + +instance Storable AuthTokens where + sizeOf _ = (#size hsgrpc::hs_auth_tokens_t) + alignment _ = (#alignment hsgrpc::hs_auth_tokens_t) + peek _ptr = error "Unimplemented" + poke _ _ = error "Unimplemented, use withAuthTokens instead" + +withAuthTokens :: AuthTokens -> (Ptr AuthTokens -> IO a) -> IO a +withAuthTokens tokens f = + allocaBytesAligned (sizeOf tokens) (alignment tokens) $ \ptr -> + HF.withByteStringList (unAuthTokens tokens) $ \ds ls l -> do + (#poke hsgrpc::hs_auth_tokens_t, datas) ptr ds + (#poke hsgrpc::hs_auth_tokens_t, sizes) ptr ls + (#poke hsgrpc::hs_auth_tokens_t, len) ptr l + f ptr + ------------------------------------------------------------------------------- -- Status @@ -492,8 +527,7 @@ data ChanArgValue | ChanArgValueString ShortByteString deriving (Show, Eq) -newtype ChannelArg = ChannelArg - { unChannelArg :: (ShortByteString, ChanArgValue) } +newtype ChannelArg = ChannelArg (ShortByteString, ChanArgValue) deriving (Eq) instance Show ChannelArg where diff --git a/hs-grpc-server/cbits/hs_grpc_server.cpp b/hs-grpc-server/cbits/hs_grpc_server.cpp index 208e4a6..163c09a 100644 --- a/hs-grpc-server/cbits/hs_grpc_server.cpp +++ b/hs-grpc-server/cbits/hs_grpc_server.cpp @@ -15,6 +15,8 @@ #include #include +#include "hsgrpc/auth.hpp" + #ifdef HSGRPC_ENABLE_ASAN #include #endif @@ -483,29 +485,53 @@ CppAsioServer* new_asio_server( const char* host, HsInt host_len, HsInt port, HsInt parallelism, // ssl options hsgrpc::hs_ssl_server_credentials_options_t* ssl_server_opts, + // auth tokens + hsgrpc::hs_auth_tokens_t* auth_tokens_, // grpc channel args hsgrpc::hs_grpc_channel_arg_t* grpc_chan_args, HsInt grpc_chan_args_size, - // interceptors + // custom interceptors grpc::experimental::ServerInterceptorFactoryInterface** interceptor_facts, HsInt interceptors_size) { - const auto total_conc = std::thread::hardware_concurrency(); - if (parallelism <= 0 || parallelism > total_conc) { - parallelism = total_conc; - } + // arg: server address std::string server_address(std::string(host, host_len) + ":" + std::to_string(port)); + // arg: auth tokens + std::set auth_tokens; + if (auth_tokens_) { + for (auto i = 0; i < auth_tokens_->len; ++i) { + auth_tokens.emplace(auth_tokens_->datas[i], auth_tokens_->sizes[i]); + } + } CppAsioServer* server_data = new CppAsioServer; - server_data->server_threads_.reserve(parallelism); - grpc::ServerBuilder builder; + std::vector< + std::unique_ptr> + interceptors; + // Set concurrency + const auto total_conc = std::thread::hardware_concurrency(); + if (parallelism <= 0 || parallelism > total_conc) { + parallelism = total_conc; + } + server_data->server_threads_.reserve(parallelism); for (size_t i = 0; i < parallelism; ++i) { server_data->grpc_contexts_.emplace_front(builder.AddCompletionQueue()); } + // Set server credentials if (!ssl_server_opts) { builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); + // Really insecure! Only for debugging usage + if (!auth_tokens.empty()) { + gpr_log(GPR_ERROR, + "!!! Using token with insecure server is meaningless !!!"); + interceptors.push_back( + std::unique_ptr< + grpc::experimental::ServerInterceptorFactoryInterface>( + new hsgrpc::InsecureBasicAuthMetadataInterceptorFactory( + auth_tokens))); + } } else { grpc::SslServerCredentialsOptions ssl_opts_; if (ssl_server_opts->pem_root_certs_data) { @@ -525,6 +551,11 @@ CppAsioServer* new_asio_server( ssl_server_opts->client_certificate_request; auto channel_creds = grpc::SslServerCredentials(ssl_opts_); + if (!auth_tokens.empty()) { + auto processor = std::shared_ptr( + new hsgrpc::BasicAuthMetadataProcessor(auth_tokens)); + channel_creds->SetAuthMetadataProcessor(processor); + } builder.AddListeningPort(server_address, channel_creds); } @@ -550,18 +581,20 @@ CppAsioServer* new_asio_server( } } + // Custom interceptors if (interceptors_size > 0) { - std::vector< - std::unique_ptr> - creators; for (HsInt i = 0; i < interceptors_size; ++i) { - creators.push_back(std::unique_ptr< - grpc::experimental::ServerInterceptorFactoryInterface>( - interceptor_facts[i])); + interceptors.push_back( + std::unique_ptr< + grpc::experimental::ServerInterceptorFactoryInterface>( + interceptor_facts[i])); } - builder.experimental().SetInterceptorCreators(std::move(creators)); } + // Build and start + if (!interceptors.empty()) { + builder.experimental().SetInterceptorCreators(std::move(interceptors)); + } server_data->server_ = builder.BuildAndStart(); if (server_data->server_) { return server_data; @@ -626,7 +659,9 @@ void delete_asio_server(CppAsioServer* server) { // active exception" can occur. delete server; #ifdef HSGRPC_ENABLE_ASAN + fprintf(stderr, "do_leak_check...\n"); __lsan_do_leak_check(); + fprintf(stderr, "do_leak_check done.\n"); #endif } diff --git a/hs-grpc-server/hs-grpc-server.cabal b/hs-grpc-server/hs-grpc-server.cabal index 89f78e5..94459fd 100644 --- a/hs-grpc-server/hs-grpc-server.cabal +++ b/hs-grpc-server/hs-grpc-server.cabal @@ -21,7 +21,8 @@ extra-source-files: external/asio/asio/include/**/*.ipp external/asio-grpc/src/**/*.hpp external/asio-grpc/src/**/*.ipp - include/*.h + include/**/*.h + include/**/*.hpp README.md source-repository head diff --git a/hs-grpc-server/include/hs_grpc_server.h b/hs-grpc-server/include/hs_grpc_server.h index 8d0e68f..9d5c598 100644 --- a/hs-grpc-server/include/hs_grpc_server.h +++ b/hs-grpc-server/include/hs_grpc_server.h @@ -65,6 +65,17 @@ struct hs_ssl_server_credentials_options_t { grpc_ssl_client_certificate_request_type client_certificate_request; }; +// Basic auth tokens +// +// TODO: maybe we can support more types of tokens in the future +// +// enum class TokenType : uint8_t { Basic }; +struct hs_auth_tokens_t { + const char** datas; + HsInt* sizes; + HsInt len; +}; + enum class GrpcChannelArgValType : uint8_t { Int, String }; struct hs_grpc_channel_arg_t { diff --git a/hs-grpc-server/include/hsgrpc/auth.hpp b/hs-grpc-server/include/hsgrpc/auth.hpp new file mode 100644 index 0000000..a004f78 --- /dev/null +++ b/hs-grpc-server/include/hsgrpc/auth.hpp @@ -0,0 +1,120 @@ +#ifndef HSGRPC_AUTH_HPP +#define HSGRPC_AUTH_HPP + +#include +#include +#include + +// TODO: hash tokens + +namespace hsgrpc { + +// Really insecure! Only for debugging usage +// +// https://datatracker.ietf.org/doc/html/rfc7617 +class InsecureBasicAuthMetadataInterceptor + : public grpc::experimental::Interceptor { +public: + explicit InsecureBasicAuthMetadataInterceptor( + grpc::experimental::ServerRpcInfo* info, std::set& tokens) + : info_(info), tokens_(tokens) {} + + void + Intercept(grpc::experimental::InterceptorBatchMethods* methods) override { + bool exit = false; + if (methods->QueryInterceptionHookPoint( + grpc::experimental::InterceptionHookPoints:: + POST_RECV_INITIAL_METADATA)) { + // Here metadata is a multimap, however there should be only one + // authorization key. + // + // TODO: support comma-separated value + // + // `authorization: Bearer foo, Basic bar` + // + // https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2 + auto& metadata = info_->server_context()->client_metadata(); + if (auto authorization = metadata.find("authorization"); + authorization != metadata.end()) { + auto auth_val = authorization->second; + auto token = + tokens_.find(std::string(auth_val.data(), auth_val.length())); + exit = token == tokens_.end(); + } else { + exit = true; + } + } + if (exit) { + // [?] Since this is in the POST_RECV_INITIAL_METADATA hook, there should + // not be a race condition between TryCancel and the handler + // + // https://grpc.github.io/grpc/cpp/classgrpc_1_1_server_context_base.html#a88d3a0c3d53e39f38654ce8fba968301 + info_->server_context()->TryCancel(); + } + methods->Proceed(); + } + +private: + grpc::experimental::ServerRpcInfo* info_; + // e.g. "Basic dGVzdDoxMjMK" + std::set& tokens_; +}; + +class InsecureBasicAuthMetadataInterceptorFactory + : public grpc::experimental::ServerInterceptorFactoryInterface { +public: + explicit InsecureBasicAuthMetadataInterceptorFactory( + std::set& tokens) + : tokens_(tokens) {} + + grpc::experimental::Interceptor* + CreateServerInterceptor(grpc::experimental::ServerRpcInfo* info) override { + return new InsecureBasicAuthMetadataInterceptor(info, tokens_); + } + +private: + std::set tokens_; +}; + +// ---------------------------------------------------------------------------- + +class BasicAuthMetadataProcessor : public grpc::AuthMetadataProcessor { +public: + explicit BasicAuthMetadataProcessor(std::set tokens) + : tokens_(tokens) {} + + bool IsBlocking() const override { return false; } + + grpc::Status Process(const InputMetadata& auth_metadata, + grpc::AuthContext* context, + OutputMetadata* consumed_auth_metadata, + OutputMetadata* response_metadata) override { + auto auth_md = auth_metadata.find("authorization"); + if (auth_md != auth_metadata.end()) { + auto auth_md_value = + std::string(auth_md->second.data(), auth_md->second.length()); + auto token = tokens_.find(auth_md_value); + if (token != tokens_.end()) { + // context->AddProperty("novel identity", *token); + // context->SetPeerIdentityPropertyName("novel identity"); + consumed_auth_metadata->insert(std::make_pair( + std::string(auth_md->first.data(), auth_md->first.length()), + std::string(auth_md->second.data(), auth_md->second.length()))); + return grpc::Status::OK; + } else { + return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, + std::string("Invalid token: ") + auth_md_value); + } + } else { + return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, + "No auth metadata found"); + } + } + +private: + std::set tokens_; +}; + +} // namespace hsgrpc + +#endif // HSGRPC_AUTH_HPP