Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/capi_frontend/server_settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ struct ServerSettingsImpl {
std::string allowedOrigins{"*"};
std::string allowedMethods{"*"};
std::string allowedHeaders{"*"};
std::string apiKey;
#ifdef MTR_ENABLED
std::string tracePath;
#endif
Expand Down
7 changes: 6 additions & 1 deletion src/cli_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ void CLIParser::parse(int argc, char** argv) {
("allowed_headers",
"Comma separated list of headers that are allowed to access the API. Default: *.",
cxxopts::value<std::string>()->default_value("*"),
"ALLOWED_HEADERS");
"ALLOWED_HEADERS")
("api_key",
"API key for authentication for generative endpoints. If not set, authentication is disabled.",
cxxopts::value<std::string>()->default_value(""),
"API_KEY");

options->add_options("multi model")
("config_path",
Expand Down Expand Up @@ -493,6 +497,7 @@ void CLIParser::prepareServer(ServerSettingsImpl& serverSettings) {
serverSettings.allowedOrigins = result->operator[]("allowed_origins").as<std::string>();
serverSettings.allowedMethods = result->operator[]("allowed_methods").as<std::string>();
serverSettings.allowedHeaders = result->operator[]("allowed_headers").as<std::string>();
serverSettings.apiKey = result->operator[]("api_key").as<std::string>();
}

void CLIParser::prepareModel(ModelsSettingsImpl& modelsSettings, HFSettingsImpl& hfSettings) {
Expand Down
1 change: 1 addition & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -361,5 +361,6 @@ const std::string& Config::allowedOrigins() const { return this->serverSettings.
const std::string& Config::allowedMethods() const { return this->serverSettings.allowedMethods; }
const std::string& Config::allowedHeaders() const { return this->serverSettings.allowedHeaders; }
const std::string Config::cacheDir() const { return this->serverSettings.cacheDir; }
const std::string& Config::apiKey() const { return this->serverSettings.apiKey; }

} // namespace ovms
1 change: 1 addition & 0 deletions src/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ class Config {
const std::string& allowedOrigins() const;
const std::string& allowedMethods() const;
const std::string& allowedHeaders() const;
const std::string& apiKey() const;

/**
* @brief Model cache directory
Expand Down
25 changes: 22 additions & 3 deletions src/http_rest_api_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const std::string HttpRestApiHandler::v3_RegexExp =
const std::string HttpRestApiHandler::metricsRegexExp = R"((.?)\/metrics(\?(.*))?)";

HttpRestApiHandler::HttpRestApiHandler(ovms::Server& ovmsServer, int timeout_in_ms) :
api_key(ovmsServer.getAPIKey()),
predictionRegex(predictionRegexExp),
modelstatusRegex(modelstatusRegexExp),
configReloadRegex(configReloadRegexExp),
Expand All @@ -140,6 +141,7 @@ HttpRestApiHandler::HttpRestApiHandler(ovms::Server& ovmsServer, int timeout_in_
metricsRegex(metricsRegexExp),
timeout_in_ms(timeout_in_ms),
ovmsServer(ovmsServer),


kfsGrpcImpl(dynamic_cast<const GRPCServerModule*>(this->ovmsServer.getModule(GRPC_SERVER_MODULE_NAME))->getKFSGrpcImpl()),
grpcGetModelMetadataImpl(dynamic_cast<const GRPCServerModule*>(this->ovmsServer.getModule(GRPC_SERVER_MODULE_NAME))->getTFSModelMetadataImpl()),
Expand Down Expand Up @@ -221,7 +223,7 @@ void HttpRestApiHandler::registerAll() {
});
registerHandler(V3, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser) -> Status {
OVMS_PROFILE_FUNCTION();
return processV3(uri, request_components, response, request_body, std::move(serverReaderWriter), std::move(multiPartParser));
return processV3(uri, request_components, response, request_body, std::move(serverReaderWriter), std::move(multiPartParser), api_key);
});
registerHandler(Metrics, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser) -> Status {
return processMetrics(request_components, response, request_body);
Expand Down Expand Up @@ -668,14 +670,31 @@ Status HttpRestApiHandler::processListModelsRequest(std::string& response) {
return StatusCode::OK;
}

Status HttpRestApiHandler::processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser) {
Status HttpRestApiHandler::processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser, const std::string& api_key) {
#if (MEDIAPIPE_DISABLE == 0)
OVMS_PROFILE_FUNCTION();

HttpPayload request;
std::string modelName;
bool streamFieldVal = false;

// convert headers to lowercase because http headers are case insensitive
for (const auto& [key, value] : request_components.headers) {
std::string lowercaseKey = key;
std::transform(lowercaseKey.begin(), lowercaseKey.end(), lowercaseKey.begin(),
[](unsigned char c){ return std::tolower(c); });
}
if (!api_key.empty()) {
if (request_components.headers.count("authorization")) {
if (request_components.headers.at("authorization") != "Bearer " + api_key) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (request_components.headers.at("authorization") != "Bearer " + api_key) {
if (request_components.headers.at("authorization") != "Bearer " + apiKey) {

Is exact match safe here? No risk of some additional whitespaces?

Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header lookup uses exact case 'authorization' but HTTP headers are case-insensitive. This will fail if the client sends 'Authorization' (capitalized). Use case-insensitive header lookup or convert the headers map to lowercase keys.

Copilot uses AI. Check for mistakes.
SPDLOG_DEBUG("Unauthorized request - invalid API key {} instead of {}", request_components.headers.at("authorization"), api_key);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SPDLOG_DEBUG("Unauthorized request - invalid API key {} instead of {}", request_components.headers.at("authorization"), api_key);
SPDLOG_DEBUG("Unauthorized request - invalid API key {} instead of {}", request_components.headers.at("authorization"), apiKey);

return StatusCode::UNAUTHORIZED;
}
}
else {
SPDLOG_DEBUG("Unauthorized request - missing API key");
return StatusCode::UNAUTHORIZED;
}
}
auto status = createV3HttpPayload(uri, request_components, response, request_body, serverReaderWriter, std::move(multiPartParser), request, modelName, streamFieldVal);
if (!status.ok()) {
SPDLOG_DEBUG("Failed to create V3 payload: {}", status.string());
Expand Down
3 changes: 2 additions & 1 deletion src/http_rest_api_handler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,10 @@ class HttpRestApiHandler {
Status processServerLiveKFSRequest(const HttpRequestComponents& request_components, std::string& response, const std::string& request_body);
Status processServerMetadataKFSRequest(const HttpRequestComponents& request_components, std::string& response, const std::string& request_body);

Status processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser);
Status processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser, const std::string& api_key);
Status processListModelsRequest(std::string& response);
Status processRetrieveModelRequest(const std::string& name, std::string& response);
const std::string api_key;

private:
const std::regex predictionRegex;
Expand Down
5 changes: 5 additions & 0 deletions src/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ Status Server::startModules(ovms::Config& config) {
// that's why we delay starting the servable until the very end while we need to create it before
// GRPC & REST
Status status;
apiKey = config.apiKey();
bool inserted = false;
auto it = modules.end();
if (config.getServerSettings().serverMode == UNKNOWN_MODE) {
Expand Down Expand Up @@ -371,6 +372,10 @@ void Server::ensureModuleShutdown(const std::string& name) {
it->second->shutdown();
}

std::string Server::getAPIKey() const {
return apiKey;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}


class ModulesShutdownGuard {
Server& server;

Expand Down
3 changes: 3 additions & 0 deletions src/server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ class Server {
virtual ~Server();
Status startModules(ovms::Config& config);
void shutdownModules();
std::string getAPIKey() const;
std::string setAPIKey(const std::string& newApiKey);
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setAPIKey method is declared but never implemented. Either implement this method or remove the declaration if API key modification after initialization is not intended.

Suggested change
std::string setAPIKey(const std::string& newApiKey);
std::string setAPIKey(const std::string& newApiKey) {
std::unique_lock<std::shared_mutex> lock(modulesMtx);
std::string oldApiKey = apiKey;
apiKey = newApiKey;
return oldApiKey;
}

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpServerModule has access to ovms::config in HTTPServerModule::start(ovms::Config)
Either extend ovms::createAndStartDrogonHttpServer() with additional parameter or wrap
apiKey together with restBindAddress,restPort, workersCount in helper struct nested inside server settings (HTTPServerConfig?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bump


private:
void ensureModuleShutdown(const std::string& name);
std::string apiKey;
};
} // namespace ovms
1 change: 1 addition & 0 deletions src/status.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const std::unordered_map<StatusCode, std::string> Status::statusMessageMap = {
{StatusCode::UNKNOWN_REQUEST_COMPONENTS_TYPE, "Request components type not recognized"},
{StatusCode::FAILED_TO_PARSE_MULTIPART_CONTENT_TYPE, "Request of multipart type but failed to parse"},
{StatusCode::FAILED_TO_DEDUCE_MODEL_NAME_FROM_URI, "Failed to deduce model name from all possible ways"},
{StatusCode::UNAUTHORIZED, "Unauthorized request due to invalid api-key"},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalid or missing

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{StatusCode::UNAUTHORIZED, "Unauthorized request due to invalid api-key"},
{StatusCode::UNAUTHORIZED, "Unauthorized request due to invalid or missing api-key"},


// Rest parser failure
{StatusCode::REST_BODY_IS_NOT_AN_OBJECT, "Request body should be JSON object"},
Expand Down
1 change: 1 addition & 0 deletions src/status.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ enum class StatusCode {
UNKNOWN_REQUEST_COMPONENTS_TYPE, /*!< Components type not recognized */
FAILED_TO_PARSE_MULTIPART_CONTENT_TYPE, /*!< Request of multipart type but failed to parse */
FAILED_TO_DEDUCE_MODEL_NAME_FROM_URI, /*!< Failed to deduce model name from all possible ways */
UNAUTHORIZED, /*!< Unauthorized request due to invalid api-key*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalid or missing

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
UNAUTHORIZED, /*!< Unauthorized request due to invalid api-key*/
UNAUTHORIZED, /*!< Unauthorized request due to invalid or missing api-key*/


// REST Parse
REST_BODY_IS_NOT_AN_OBJECT, /*!< REST body should be JSON object */
Expand Down