Skip to content

Commit

Permalink
Support basic auth (incomplete)
Browse files Browse the repository at this point in the history
  • Loading branch information
4eUeP committed Nov 3, 2023
1 parent ba38388 commit 0dacd67
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 18 deletions.
6 changes: 5 additions & 1 deletion hs-grpc-server/HsGrpc/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ runServer ServerOptions{..} handlers = do
server <- newAsioServer
serverHost serverPort serverParallelism
serverSslOptions
serverAuthTokens
serverChannelArgs
serverInterceptors
runAsioGrpc server handlers serverOnStarted serverInternalChannelSize
Expand All @@ -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!"
Expand Down
2 changes: 2 additions & 0 deletions hs-grpc-server/HsGrpc/Server/FFI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 36 additions & 2 deletions hs-grpc-server/HsGrpc/Server/Types.hsc
Original file line number Diff line number Diff line change
Expand Up @@ -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 (..)
Expand Down Expand Up @@ -118,6 +123,7 @@ data ServerOptions = ServerOptions
, serverPort :: !Int
, serverParallelism :: !Int
, serverSslOptions :: !(Maybe SslServerCredentialsOptions)
, serverAuthTokens :: ![ByteString]
, serverOnStarted :: !(Maybe (IO ()))
, serverInterceptors :: ![ServerInterceptor]
, serverChannelArgs :: ![ChannelArg]
Expand All @@ -131,6 +137,7 @@ defaultServerOpts = ServerOptions
, serverPort = 50051
, serverParallelism = 0
, serverSslOptions = Nothing
, serverAuthTokens = []
, serverOnStarted = Nothing
, serverInterceptors = []
, serverChannelArgs = []
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
63 changes: 49 additions & 14 deletions hs-grpc-server/cbits/hs_grpc_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#include <grpc/support/log.h>
#include <grpcpp/server_builder.h>

#include "hsgrpc/auth.hpp"

#ifdef HSGRPC_ENABLE_ASAN
#include <sanitizer/lsan_interface.h>
#endif
Expand Down Expand Up @@ -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<std::string> 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<grpc::experimental::ServerInterceptorFactoryInterface>>
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) {
Expand All @@ -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<grpc::AuthMetadataProcessor>(
new hsgrpc::BasicAuthMetadataProcessor(auth_tokens));
channel_creds->SetAuthMetadataProcessor(processor);
}
builder.AddListeningPort(server_address, channel_creds);
}

Expand All @@ -550,18 +581,20 @@ CppAsioServer* new_asio_server(
}
}

// Custom interceptors
if (interceptors_size > 0) {
std::vector<
std::unique_ptr<grpc::experimental::ServerInterceptorFactoryInterface>>
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;
Expand Down Expand Up @@ -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
}

Expand Down
3 changes: 2 additions & 1 deletion hs-grpc-server/hs-grpc-server.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions hs-grpc-server/include/hs_grpc_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
120 changes: 120 additions & 0 deletions hs-grpc-server/include/hsgrpc/auth.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#ifndef HSGRPC_AUTH_HPP
#define HSGRPC_AUTH_HPP

#include <grpc/support/log.h>
#include <grpcpp/server.h>
#include <set>

// 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<std::string>& 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<std::string>& tokens_;
};

class InsecureBasicAuthMetadataInterceptorFactory
: public grpc::experimental::ServerInterceptorFactoryInterface {
public:
explicit InsecureBasicAuthMetadataInterceptorFactory(
std::set<std::string>& tokens)
: tokens_(tokens) {}

grpc::experimental::Interceptor*
CreateServerInterceptor(grpc::experimental::ServerRpcInfo* info) override {
return new InsecureBasicAuthMetadataInterceptor(info, tokens_);
}

private:
std::set<std::string> tokens_;
};

// ----------------------------------------------------------------------------

class BasicAuthMetadataProcessor : public grpc::AuthMetadataProcessor {
public:
explicit BasicAuthMetadataProcessor(std::set<std::string> 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<std::string> tokens_;
};

} // namespace hsgrpc

#endif // HSGRPC_AUTH_HPP

0 comments on commit 0dacd67

Please sign in to comment.