blob: cfcd4042a28e3eeb6bcce14e8e00913200520a09 [file] [log] [blame] [edit]
#include "proxy.h"
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "redfish_query_engine/file/uds.h"
#include "grpc/grpc.h"
#include "grpcpp/impl/service_type.h"
#include "grpcpp/security/auth_context.h"
#include "grpcpp/security/authorization_policy_provider.h"
#include "grpcpp/server_builder.h"
#include "grpcpp/support/status.h"
#include "metrics.h"
#include "proxy_config.pb.h"
#include "redfish_plugin.h"
#include "request_response.h"
#include "resource_authz.h"
#include "utils/status_macros.h"
#include "voyager/deferrable_priority_queue.hpp"
#include "voyager/priority_queue.hpp"
constexpr absl::string_view kUdsUrlBase = "unix://";
// Channel arguments to be added to the gRPC server builder.
// These are the default values in G3 go/keepalive-ping-grpc-core.
constexpr uint32_t kKeepAliveTimeMs = 10 * 60 * 1000 /*10 min*/;
constexpr uint32_t kKeepAliveTimeoutMs = 20 * 1000 /*20 sec*/;
constexpr uint32_t kKeepAlivePermitWithoutCalls = 1;
constexpr uint32_t kHttp2MaxPingWithoutData = 2;
constexpr uint32_t kHttp2MinRecvPingIntervalWithoutDataMs =
10 * 1000 /*10 sec*/;
constexpr uint32_t kHttp2MaxPingStrikes = 2;
namespace milotic {
// Only intended for use with numeric types whose default value is 0
// Returns override_value if config_value is 0
template <typename T>
T GetConfigValue(T config_value, T override_value) {
if (config_value == 0) return override_value;
return config_value;
}
Proxy::Proxy(std::vector<Host> hosts,
absl::Span<std::unique_ptr<RedfishPlugin>> handlers,
voyager::DeferrablePriorityQueue* queue, Resources resources,
milotic_grpc_proxy::AuthorizationPolicy authorization_policy,
std::unique_ptr<PermissionChecker> permission_checker,
std::string name)
: hosts_(std::move(hosts)),
queue_(queue),
resources_(std::move(resources)),
authorization_policy_(std::move(authorization_policy)),
permission_checker_(std::move(permission_checker)) {
CHECK(!hosts_.empty()) << "No hosts provided";
name_ = name.empty() ? absl::StrCat("@", hosts_[0].GetHostname())
: std::move(name);
handlers_.reserve(handlers.size());
for (std::unique_ptr<RedfishPlugin>& handler : handlers) {
absl::Status init_status = handler->Initialize(this);
if (!init_status.ok()) {
LOG(FATAL) << name_ << " - Handler init failed for " << handler->Name()
<< ": " << init_status.message();
}
LOG(INFO) << name_ << " - Handler init succeeded: " << handler->Name();
handlers_.push_back(std::move(handler));
}
handler_initialized_ = true;
}
Proxy::~Proxy() {
if (server_ != nullptr) {
server_->Shutdown();
server_->Wait();
}
}
RedfishPlugin::RequestAction Proxy::GetRequestAction(
RedfishPlugin::RequestVerb request_verb, const ProxyRequest& request,
const std::vector<std::pair<RedfishPlugin::RequestVerb, absl::string_view>>&
paths_to_handle) {
std::string request_path = std::string(request.GetPath());
RedfishPlugin::RequestAction action_if_no_match =
RedfishPlugin::RequestAction::kNext;
for (const auto& [verb, path] : paths_to_handle) {
if (request_path == path) {
action_if_no_match = RedfishPlugin::RequestAction::kDrop;
// Only handle requests for the correct verb
if (request_verb == verb) return RedfishPlugin::RequestAction::kHandle;
}
}
return action_if_no_match;
}
std::unique_ptr<ProxyRequest> Proxy::CreateRequest(
absl::string_view redfish_id) const {
auto request =
std::make_unique<ProxyRequest>(hosts_, std::string(redfish_id));
// TODO(jainrish): Header should be added by the caller based on the host
// being used.
// NO_CDC: The size of hosts_ is safety-checked in the constructor.
request->AddHeader("Host", hosts_[0].GetHostHeader());
return request;
}
absl::StatusOr<RedfishPlugin*> Proxy::GetRequestHandler(
RedfishPlugin::RequestVerb verb, ProxyRequest& request) {
if (!handler_initialized_)
return absl::UnavailableError("Handlers not initialized");
for (std::unique_ptr<RedfishPlugin>& handler : handlers_) {
RedfishPlugin::RequestAction action =
handler->PreprocessRequest(verb, request);
if (action == RedfishPlugin::RequestAction::kDrop) {
return absl::UnavailableError("Request dropped");
}
if (action == RedfishPlugin::RequestAction::kHandle) {
return handler.get();
}
if (action == RedfishPlugin::RequestAction::kNext) {
// do nothing
} else {
return absl::UnknownError("Bad plugin action");
}
}
return absl::UnimplementedError("Request not handled.");
}
absl::StatusOr<ProxyResponse> Proxy::DispatchRequest(
RedfishPlugin::RequestVerb verb,
std::unique_ptr<ProxyRequest> http_request) {
ASSIGN_OR_RETURN(RedfishPlugin * handler,
GetRequestHandler(verb, *http_request));
return handler->HandleRequest(verb, std::move(http_request));
}
absl::Status Proxy::Subscribe(std::unique_ptr<ProxyRequest> http_request,
RedfishPlugin::EventHandler* event_handler) {
ASSIGN_OR_RETURN(
RedfishPlugin * handler,
GetRequestHandler(RedfishPlugin::RequestVerb::kSubscribe, *http_request));
return handler->Subscribe(std::move(http_request), event_handler);
}
absl::StatusOr<std::unique_ptr<RedfishPlugin::EventHandler>>
Proxy::BidirectionalStream(std::unique_ptr<ProxyRequest> http_request,
RedfishPlugin::EventHandler* event_handler) {
ASSIGN_OR_RETURN(
RedfishPlugin * handler,
GetRequestHandler(RedfishPlugin::RequestVerb::kBidirectionalStream,
*http_request));
return handler->BidirectionalStream(std::move(http_request), event_handler);
}
absl::StatusOr<std::unique_ptr<Proxy::RequestJob>>
Proxy::DispatchRequestToQueue(RedfishPlugin::RequestVerb verb,
std::unique_ptr<ProxyRequest> http_request,
Proxy::RequestPriority priority) {
if (queue_ == nullptr) {
return absl::UnavailableError("Queue not initialized");
}
auto job = std::make_unique<RequestJob>(*this, verb, std::move(http_request));
absl::Status result = queue_->Enqueue(*job, RequestJob::Run, priority);
if (!result.ok()) {
return result;
}
return job;
}
absl::Status Proxy::DispatchRequestToQueue(
std::unique_ptr<RequestJob> request_job, Proxy::RequestPriority priority,
std::optional<absl::Time> after) {
if (queue_ == nullptr) {
return absl::UnavailableError("Queue not initialized");
}
if (after) {
return queue_->EnqueueAfter(*after, std::move(request_job), RequestJob::Run,
priority);
} else {
return queue_->Enqueue(std::move(request_job), RequestJob::Run, priority);
}
}
absl::Status Proxy::ConfigGrpcAndStart(
const milotic_grpc_proxy::GrpcConfiguration& config,
const std::function<bool(const std::string&)>& is_safe_uds_root,
const GrpcCredentials& credentials,
const std::shared_ptr<
grpc::experimental::AuthorizationPolicyProviderInterface>& provider) {
using ecclesia::DomainSocketOwners;
using ecclesia::DomainSocketPermissions;
using ecclesia::SetUpUnixDomainSocket;
grpc::ServerBuilder builder;
if (provider != nullptr) {
builder.experimental().SetAuthorizationPolicyProvider(provider);
}
if (config.has_net_endpoint()) {
if (credentials.net_creds == nullptr) {
return absl::FailedPreconditionError(
absl::StrCat(name_, " - No network credentials provided"));
}
std::string net_endpoint = absl::StrCat(config.net_endpoint().host(), ":",
config.net_endpoint().port());
LOG(INFO) << name_ << " - Adding net endpoint " << net_endpoint;
builder.AddListeningPort(net_endpoint, credentials.net_creds);
}
if (config.has_uds_endpoint()) {
if (credentials.uds_creds == nullptr) {
return absl::FailedPreconditionError(
absl::StrCat(name_, " - No UDS credentials provided"));
}
DomainSocketOwners owners;
DomainSocketPermissions permissions =
config.uds_endpoint().permission() ==
milotic_grpc_proxy::Permission::PERM_USER_ONLY
? DomainSocketPermissions::kUserOnly
: DomainSocketPermissions::kUserAndGroup;
if (config.uds_endpoint().has_user()) {
owners.uid = config.uds_endpoint().user().id();
}
if (config.uds_endpoint().has_group()) {
owners.gid = config.uds_endpoint().group().id();
}
if (!SetUpUnixDomainSocket(config.uds_endpoint().path(), permissions,
owners, is_safe_uds_root)) {
return absl::UnavailableError(
absl::StrCat(name_, " - Failed to set up socket at ",
config.uds_endpoint().path()));
}
LOG(INFO) << "Created socket file at " << config.uds_endpoint().path();
std::string server_uds_path(
absl::StrCat(kUdsUrlBase, config.uds_endpoint().path()));
LOG(INFO) << name_ << " - Adding UDS endpoint " << server_uds_path;
builder.AddListeningPort(server_uds_path, credentials.uds_creds);
}
for (std::unique_ptr<grpc::Service>& service : services_) {
builder.RegisterService(service.get());
}
// Adding explicit ChannelArguments to prevent gRPC from using the default
// values in OpenSource version.
builder.AddChannelArgument(
GRPC_ARG_KEEPALIVE_TIME_MS,
GetConfigValue(config.channel_arguments().keepalive_time_ms(),
kKeepAliveTimeMs));
builder.AddChannelArgument(
GRPC_ARG_KEEPALIVE_TIMEOUT_MS,
GetConfigValue(config.channel_arguments().keepalive_timeout_ms(),
kKeepAliveTimeoutMs));
builder.AddChannelArgument(
GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS,
GetConfigValue(
config.channel_arguments().keepalive_permit_without_calls(),
kKeepAlivePermitWithoutCalls));
builder.AddChannelArgument(
GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA,
GetConfigValue(config.channel_arguments().http2_max_pings_without_data(),
kHttp2MaxPingWithoutData));
builder.AddChannelArgument(
GRPC_ARG_HTTP2_MIN_RECV_PING_INTERVAL_WITHOUT_DATA_MS,
GetConfigValue(config.channel_arguments()
.http2_min_recv_ping_interval_without_data_ms(),
kHttp2MinRecvPingIntervalWithoutDataMs));
builder.AddChannelArgument(
GRPC_ARG_HTTP2_MAX_PING_STRIKES,
GetConfigValue(config.channel_arguments().http2_max_ping_strikes(),
kHttp2MaxPingStrikes));
server_ = builder.BuildAndStart();
if (server_ == nullptr) {
return absl::InternalError(
absl::StrCat(name_, " - Failed to create server"));
}
LOG(INFO) << "Proxy " << name_ << " started.";
return absl::OkStatus();
}
absl::Status Proxy::RequestJob::RunImpl() {
response_ = proxy_.DispatchRequest(verb_, std::move(request_));
Handler handler;
{
absl::MutexLock lock(handler_mutex_);
has_run_ = true;
handler = std::move(handler_);
}
if (handler) {
handler(response_);
}
return response_.status();
}
void Proxy::RequestJob::Handle(Handler handler) {
if (absl::MutexLock lock(handler_mutex_); !has_run_) {
handler_ = std::move(handler);
return;
}
handler(response_);
}
absl::StatusOr<std::unique_ptr<ProxyRequest>> Proxy::CreateAuthorizedRequest(
RedfishPlugin::RequestVerb verb, absl::string_view redfish_id,
const grpc::AuthContext& auth_context) const {
AuthorizationContext context{.grpc_context = auth_context,
.permission_checker = permission_checker_.get()};
if (!authorization_policy_.keep_query_params()) {
absl::string_view::size_type pos = redfish_id.find('?');
if (pos != absl::string_view::npos) {
redfish_id.remove_suffix(redfish_id.length() - pos);
CommonMetrics::Get().query_parameters_removed.Increment({});
}
}
absl::Status status =
AuthorizeRequest(authorization_policy_, verb, redfish_id, context);
if (!status.ok()) {
LOG_EVERY_N_SEC(ERROR, 1) << name_ << " - Unauthorized request: " << status;
return status;
}
return CreateRequest(redfish_id);
}
grpc::Status ConvertStatus(const absl::Status& status) {
if (status.ok()) return grpc::Status::OK;
if (status.code() == absl::StatusCode::kInvalidArgument) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
std::string(status.message()));
}
if (status.code() == absl::StatusCode::kNotFound) {
return grpc::Status(grpc::StatusCode::NOT_FOUND,
std::string(status.message()));
}
if (status.code() == absl::StatusCode::kPermissionDenied) {
return grpc::Status(grpc::StatusCode::PERMISSION_DENIED,
std::string(status.message()));
}
return grpc::Status(grpc::StatusCode::INTERNAL,
std::string(status.message()));
}
absl::Status Proxy::AddService(const google::protobuf::Any& config) {
std::unique_ptr<::grpc::Service> service = CreateService(config);
if (service == nullptr) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to create service with config: ", config));
}
services_.push_back(std::move(service));
return absl::OkStatus();
}
std::unique_ptr<grpc::Service> Proxy::CreateService(
const google::protobuf::Any& config) {
for (const auto& creator : service_creators()) {
std::unique_ptr<grpc::Service> service = creator(*this, config);
if (service != nullptr) {
return service;
}
}
return nullptr;
}
} // namespace milotic