|
@@ -1,6 +1,7 @@
|
|
|
#include "server.hpp"
|
|
#include "server.hpp"
|
|
|
#include "errors.hpp"
|
|
#include "errors.hpp"
|
|
|
#include "json_http.hpp"
|
|
#include "json_http.hpp"
|
|
|
|
|
+#include "rate_limiter.hpp"
|
|
|
#include <chrono>
|
|
#include <chrono>
|
|
|
#include <fstream>
|
|
#include <fstream>
|
|
|
#include <sstream>
|
|
#include <sstream>
|
|
@@ -38,10 +39,31 @@ void requireProjectAccess(const ApiKey& k, const std::string& project) {
|
|
|
throw ApiError(ErrCode::Forbidden, "forbidden", "key not granted project '" + project + "'");
|
|
throw ApiError(ErrCode::Forbidden, "forbidden", "key not granted project '" + project + "'");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+void requireCapability(ServerDeps& d, const ApiKey& k, const httplib::Request& req,
|
|
|
|
|
+ const std::string& project, const std::string& collection, KeyOp op) {
|
|
|
|
|
+ requireProjectAccess(k, project);
|
|
|
|
|
+ if (!k.scope) return; // legacy full-access key
|
|
|
|
|
+ const KeyScope& sc = *k.scope;
|
|
|
|
|
+ const KeyScopeRule* rule = sc.findRule(collection, op);
|
|
|
|
|
+ if (!rule)
|
|
|
|
|
+ throw ApiError(ErrCode::Forbidden, "forbidden", "key scope does not permit this operation");
|
|
|
|
|
+ std::string origin;
|
|
|
|
|
+ if (auto it = req.headers.find("Origin"); it != req.headers.end()) origin = it->second;
|
|
|
|
|
+ if (!sc.originAllowed(origin))
|
|
|
|
|
+ throw ApiError(ErrCode::Forbidden, "forbidden", "origin not allowed for this key");
|
|
|
|
|
+ if (!rateLimiter().allow(k.id, sc.rateLimitPerMin))
|
|
|
|
|
+ throw ApiError(ErrCode::TooManyRequests, "rate_limited", "rate limit exceeded");
|
|
|
|
|
+ // Phase 2 (CAPTCHA) inserts the rule->requireHumanToken check here.
|
|
|
|
|
+ (void)d;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
void ApiServer::registerRoutes() {
|
|
void ApiServer::registerRoutes() {
|
|
|
svr_.set_exception_handler([](const httplib::Request&, httplib::Response& res, std::exception_ptr ep) {
|
|
svr_.set_exception_handler([](const httplib::Request&, httplib::Response& res, std::exception_ptr ep) {
|
|
|
try { std::rethrow_exception(ep); }
|
|
try { std::rethrow_exception(ep); }
|
|
|
- catch (const ApiError& e) { sendJson(res, httpStatus(e.code), errorBody(e.slug, e.what())); }
|
|
|
|
|
|
|
+ catch (const ApiError& e) {
|
|
|
|
|
+ if (e.code == ErrCode::TooManyRequests) res.set_header("Retry-After", "60");
|
|
|
|
|
+ sendJson(res, httpStatus(e.code), errorBody(e.slug, e.what()));
|
|
|
|
|
+ }
|
|
|
catch (const std::exception& e) { sendJson(res, 500, errorBody("internal", e.what())); }
|
|
catch (const std::exception& e) { sendJson(res, 500, errorBody("internal", e.what())); }
|
|
|
});
|
|
});
|
|
|
|
|
|