Skip to content
Draft
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
16 changes: 16 additions & 0 deletions kv_cache_manager/service/http_service/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ package(default_visibility = [
"//stub_source:__subpackages__",
])

cc_library(
name = "auth",
srcs = [
"auth/auth_util.cc",
"auth/static_bearer_token_verifier.cc",
],
hdrs = [
"auth/auth_util.h",
"auth/static_bearer_token_verifier.h",
"auth/token_verifier.h",
],
copts = ["-std=c++20"],
include_prefix = "service/http_service",
)

cc_library(
name = "http_service",
srcs = [
Expand All @@ -23,6 +38,7 @@ cc_library(
],
include_prefix = "service/http_service",
deps = [
":auth",
"//kv_cache_manager/common:logger",
"//kv_cache_manager/common:request_context",
"//kv_cache_manager/config",
Expand Down
36 changes: 36 additions & 0 deletions kv_cache_manager/service/http_service/auth/auth_util.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "kv_cache_manager/service/http_service/auth/auth_util.h"

namespace kv_cache_manager {

bool AuthUtil::ConstantTimeEquals(std::string_view a, std::string_view b) {
if (a.size() != b.size()) {
return false;
}
unsigned char diff = 0;
for (std::size_t i = 0; i < a.size(); ++i) {
diff |= static_cast<unsigned char>(a[i]) ^ static_cast<unsigned char>(b[i]);
}
return diff == 0;
}

bool AuthUtil::ICaseEqualsAscii(std::string_view a, std::string_view b) {
if (a.size() != b.size()) {
return false;
}
for (std::size_t i = 0; i < a.size(); ++i) {
unsigned char ca = static_cast<unsigned char>(a[i]);
unsigned char cb = static_cast<unsigned char>(b[i]);
if (ca >= 'A' && ca <= 'Z') {
ca = static_cast<unsigned char>(ca + ('a' - 'A'));
}
if (cb >= 'A' && cb <= 'Z') {
cb = static_cast<unsigned char>(cb + ('a' - 'A'));
}
if (ca != cb) {
return false;
}
}
return true;
}

} // namespace kv_cache_manager
22 changes: 22 additions & 0 deletions kv_cache_manager/service/http_service/auth/auth_util.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <string_view>

namespace kv_cache_manager {

class AuthUtil {
public:
// length-revealing constant-time equality compare; returns true
// iff a and b have the same length and the same bytes. the
// comparison cost is O(min(len(a), len(b))) regardless of where
// the first mismatch occurs, defeating naive timing oracles on
// the matching prefix. callers should keep secrets at a
// bounded length to avoid leaking length itself
Comment thread
oldsharp marked this conversation as resolved.
Outdated
static bool ConstantTimeEquals(std::string_view a, std::string_view b);

// case-insensitive ASCII equality, used to match the scheme
// name "Bearer" per RFC 7235 §2.1 (scheme is case-insensitive)
static bool ICaseEqualsAscii(std::string_view a, std::string_view b);
};

} // namespace kv_cache_manager
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include "kv_cache_manager/service/http_service/auth/static_bearer_token_verifier.h"

#include <utility>

#include "kv_cache_manager/service/http_service/auth/auth_util.h"

namespace kv_cache_manager {

namespace {

// trim ASCII SP/HTAB at both ends
std::string_view TrimOWS(std::string_view sv) {
while (!sv.empty() && (sv.front() == ' ' || sv.front() == '\t')) {
sv.remove_prefix(1);
}
while (!sv.empty() && (sv.back() == ' ' || sv.back() == '\t')) {
sv.remove_suffix(1);
}
return sv;
}

} // namespace

StaticBearerTokenVerifier::StaticBearerTokenVerifier(std::vector<std::string> accepted_tokens, std::string realm)
: tokens_(std::move(accepted_tokens)), realm_(std::move(realm)) {}

AuthOutcome StaticBearerTokenVerifier::Verify(std::string_view authz_header) const {
// RFC 7235 §4.2: an absent Authorization header means "no
// credentials supplied"
auto h = TrimOWS(authz_header);
if (h.empty()) {
return AuthOutcome::kMissingCredentials;
}

// scheme (RFC 7235 §2.1): scheme is case-insensitive, followed
// by 1*SP and a token68
constexpr std::string_view kScheme = "Bearer";
if (h.size() < kScheme.size() + 1) {
return AuthOutcome::kInvalidRequest;
}
if (!AuthUtil::ICaseEqualsAscii(h.substr(0, kScheme.size()), kScheme)) {
return AuthOutcome::kInvalidRequest;
}
char sep = h[kScheme.size()];
if (sep != ' ' && sep != '\t') {
// adjacent token without separator (e.g. "BearerXYZ") is
// not a valid Bearer credential
return AuthOutcome::kInvalidRequest;
}

auto rest = h.substr(kScheme.size());
// skip 1*SP (also tolerate HTAB; some clients use it)
std::size_t i = 0;
while (i < rest.size() && (rest[i] == ' ' || rest[i] == '\t')) {
++i;
}
if (i == 0 || i == rest.size()) {
Comment thread
oldsharp marked this conversation as resolved.
return AuthOutcome::kInvalidRequest;
}
auto token = rest.substr(i);
// token68 has no internal whitespace; reject if any
if (token.find_first_of(" \t") != std::string_view::npos) {
return AuthOutcome::kInvalidRequest;
}

for (const auto &accepted : tokens_) {
if (AuthUtil::ConstantTimeEquals(token, accepted)) {
return AuthOutcome::kOk;
}
}
return AuthOutcome::kInvalidToken;
}

} // namespace kv_cache_manager
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include <string>
#include <string_view>
#include <vector>

#include "kv_cache_manager/service/http_service/auth/token_verifier.h"

namespace kv_cache_manager {

// verifies HTTP Authorization headers carrying a Bearer token (RFC
// 6750) against a fixed list of accepted tokens. multiple tokens
// allow zero-downtime rotation: deploy with both old and new in the
// list, switch clients, then remove the old one
class StaticBearerTokenVerifier : public TokenVerifier {
public:
explicit StaticBearerTokenVerifier(std::vector<std::string> accepted_tokens, std::string realm = "kvcm");
Comment thread
oldsharp marked this conversation as resolved.

AuthOutcome Verify(std::string_view authz_header) const override;
std::string Realm() const override { return realm_; }

private:
std::vector<std::string> tokens_;
std::string realm_;
};

} // namespace kv_cache_manager
29 changes: 29 additions & 0 deletions kv_cache_manager/service/http_service/auth/token_verifier.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include <string>
#include <string_view>

namespace kv_cache_manager {

// outcome of an authorization attempt; mapped to RFC 6750 §3.1
// WWW-Authenticate `error` parameter values
enum class AuthOutcome {
kOk,
kMissingCredentials, // no Authorization header present
kInvalidRequest, // header malformed or scheme not Bearer
kInvalidToken, // scheme is Bearer but token not accepted
};

class TokenVerifier {
public:
virtual ~TokenVerifier() = default;

// verify the raw value of an HTTP Authorization header
// (may be empty if the client sent no header)
virtual AuthOutcome Verify(std::string_view authz_header) const = 0;

// realm advertised in the WWW-Authenticate response header
virtual std::string Realm() const { return "kvcm"; }
};

} // namespace kv_cache_manager
61 changes: 59 additions & 2 deletions kv_cache_manager/service/http_service/coro_http_service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ bool CoroHttpService::Start(int32_t port, size_t thread_num) {
server_ = std::make_unique<coro_http::coro_http_server>(thread_num, static_cast<unsigned short>(port), "0.0.0.0");

// 注册所有 GET/POST handler
// chain order: logger(auth(handler)) — so 401 responses produced
// by the auth middleware still go through the request/response
// logger for audit purposes
for (const auto &[path, handler] : get_handlers_) {
server_->set_http_handler<coro_http::GET>(path, WrapWithLogger(path, handler));
server_->set_http_handler<coro_http::GET>(path, WrapWithLogger(path, WrapWithAuth(path, handler)));
}
for (const auto &[path, handler] : post_handlers_) {
server_->set_http_handler<coro_http::POST>(path, WrapWithLogger(path, handler));
server_->set_http_handler<coro_http::POST>(path, WrapWithLogger(path, WrapWithAuth(path, handler)));
}

auto ec = server_->async_start().get(); // 注意这里用 get()
Expand Down Expand Up @@ -101,4 +104,58 @@ CoroHttpService::HandlerType CoroHttpService::WrapWithLogger(const std::string &
};
}

void CoroHttpService::SetTokenVerifier(std::shared_ptr<TokenVerifier> verifier) {
token_verifier_ = std::move(verifier);
}

CoroHttpService::HandlerType CoroHttpService::WrapWithAuth(const std::string &api, HandlerType handler) {
// when no verifier is configured the service runs in open mode;
// return the handler unchanged so there is zero per-request cost
if (!token_verifier_) {
return handler;
}
auto verifier = token_verifier_;
return [api, handler, verifier](coro_http::coro_http_request &req,
coro_http::coro_http_response &res) -> async_simple::coro::Lazy<void> {
auto authz = req.get_header_value("Authorization");
Comment thread
oldsharp marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HTTP header field names are case-insensitive (RFC 9110 §5.1), but req.get_header_value("Authorization") looks up by an exact-case key. If the underlying coro_http request stores headers as transmitted on the wire (some clients/proxies send authorization lowercased; HTTP/2 mandates lowercase field names), this lookup will silently miss and return empty, which the verifier then maps to kMissingCredentials — i.e. a perfectly well-formed HTTP/2 request with a valid Bearer token would be rejected as "no credentials". Worth verifying the lookup is case-insensitive (and adding a unit/integration test for authorization: Bearer … lowercased) or normalising the lookup explicitly.


🤖 Generated by Qoder


🤖 Generated by Qoder

auto outcome = verifier->Verify(authz);
if (outcome == AuthOutcome::kOk) {
co_await handler(req, res);
co_return;
}

// RFC 6750 §3: respond with 401 and a WWW-Authenticate
// challenge advertising the Bearer scheme. the optional
// `error` parameter distinguishes malformed requests from
// bad tokens; absent credentials get the bare challenge
std::string www_auth = "Bearer realm=\"" + verifier->Realm() + "\"";
const char *err = nullptr;
switch (outcome) {
case AuthOutcome::kInvalidRequest:
err = "invalid_request";
break;
case AuthOutcome::kInvalidToken:
err = "invalid_token";
break;
case AuthOutcome::kMissingCredentials:
case AuthOutcome::kOk:
break;
}
if (err != nullptr) {
www_auth += ", error=\"";
www_auth += err;
www_auth += "\"";
}
res.add_header("WWW-Authenticate", www_auth);
res.add_header("Content-Type", "application/json");
res.set_status_and_content(coro_http::status_type::unauthorized, std::string(R"({"error":"unauthorized"})"));

KVCM_LOG_WARN("[AUTH] denied api=%s outcome=%d ip=%s",
Comment thread
oldsharp marked this conversation as resolved.
api.c_str(),
static_cast<int>(outcome),
GetHttpClientIp(req.get_conn()).c_str());
co_return;
};
}

} // namespace kv_cache_manager
8 changes: 8 additions & 0 deletions kv_cache_manager/service/http_service/coro_http_service.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <unordered_map>

#include "google/protobuf/message.h"
#include "kv_cache_manager/service/http_service/auth/token_verifier.h"
#include "kv_cache_manager/service/util/proto_message_json_util.h"
#include "ylt/coro_http/coro_http_server.hpp"

Expand All @@ -30,12 +31,18 @@ class CoroHttpService {
bool Start(int32_t port, size_t thread_num = std::thread::hardware_concurrency());
void Stop();

// attach a TokenVerifier; when set, every registered handler is
// guarded with Bearer-auth middleware before it runs. must be
// called before Start()
void SetTokenVerifier(std::shared_ptr<TokenVerifier> verifier);

static std::string GetHttpClientIp(const coro_http::coro_http_connection *http_conn);

protected:
void RegisterGetHandler(const std::string &api, HandlerType handler);
void RegisterPostHandler(const std::string &api, HandlerType handler);
HandlerType WrapWithLogger(const std::string &api, HandlerType handler);
HandlerType WrapWithAuth(const std::string &api, HandlerType handler);

template <typename ServiceType, typename PbRequestMessage, typename PbResponseMessage>
HandlerType GetHandler(
Expand All @@ -46,6 +53,7 @@ class CoroHttpService {
std::unordered_map<std::string, HandlerType> get_handlers_{};
std::unordered_map<std::string, HandlerType> post_handlers_{};
std::unique_ptr<coro_http::coro_http_server> server_{};
std::shared_ptr<TokenVerifier> token_verifier_{};
};

template <typename ServiceType, typename PbRequestMessage, typename PbResponseMessage>
Expand Down
25 changes: 25 additions & 0 deletions kv_cache_manager/service/http_service/test/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package(default_visibility = ["//visibility:private"])

cc_test(
name = "AuthUtilTest",
srcs = [
"auth_util_test.cc",
],
copts = ["-std=c++20"],
deps = [
"//kv_cache_manager/common:unittest",
"//kv_cache_manager/service/http_service:auth",
],
)

cc_test(
name = "StaticBearerTokenVerifierTest",
srcs = [
"static_bearer_token_verifier_test.cc",
],
copts = ["-std=c++20"],
deps = [
"//kv_cache_manager/common:unittest",
"//kv_cache_manager/service/http_service:auth",
],
)
36 changes: 36 additions & 0 deletions kv_cache_manager/service/http_service/test/auth_util_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "kv_cache_manager/common/unittest.h"
#include "kv_cache_manager/service/http_service/auth/auth_util.h"

using namespace kv_cache_manager;

class AuthUtilTest : public TESTBASE {};

TEST_F(AuthUtilTest, ConstantTimeEqualsBasic) {
ASSERT_TRUE(AuthUtil::ConstantTimeEquals("", ""));
ASSERT_TRUE(AuthUtil::ConstantTimeEquals("abc", "abc"));
ASSERT_FALSE(AuthUtil::ConstantTimeEquals("abc", "abd"));
}

TEST_F(AuthUtilTest, ConstantTimeEqualsLengthMismatch) {
ASSERT_FALSE(AuthUtil::ConstantTimeEquals("abc", "abcd"));
ASSERT_FALSE(AuthUtil::ConstantTimeEquals("abcd", "abc"));
ASSERT_FALSE(AuthUtil::ConstantTimeEquals("", "x"));
}

TEST_F(AuthUtilTest, ConstantTimeEqualsBinarySafe) {
std::string a("ab\0cd", 5);
std::string b("ab\0cd", 5);
std::string c("ab\0ce", 5);
ASSERT_TRUE(AuthUtil::ConstantTimeEquals(a, b));
ASSERT_FALSE(AuthUtil::ConstantTimeEquals(a, c));
}

TEST_F(AuthUtilTest, ICaseEqualsAscii) {
ASSERT_TRUE(AuthUtil::ICaseEqualsAscii("Bearer", "bearer"));
ASSERT_TRUE(AuthUtil::ICaseEqualsAscii("BEARER", "bearer"));
ASSERT_TRUE(AuthUtil::ICaseEqualsAscii("BeArEr", "bEaReR"));
ASSERT_FALSE(AuthUtil::ICaseEqualsAscii("Bearer", "Basic"));
ASSERT_FALSE(AuthUtil::ICaseEqualsAscii("Bearer", "Bearers"));
ASSERT_FALSE(AuthUtil::ICaseEqualsAscii("", "x"));
ASSERT_TRUE(AuthUtil::ICaseEqualsAscii("", ""));
}
Loading
Loading