Bug fixing
This commit is contained in:
10
README.md
10
README.md
@@ -84,17 +84,17 @@ The server can be configured by creating a JSON configuration file at `~/.config
|
|||||||
"port": 8080,
|
"port": 8080,
|
||||||
"storage_path": "/path/to/storage",
|
"storage_path": "/path/to/storage",
|
||||||
"write_tokens": ["your-secret-token"],
|
"write_tokens": ["your-secret-token"],
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Optionally, you can modify the CORS configuration. Defaults:
|
|
||||||
```json
|
|
||||||
"cors": {
|
"cors": {
|
||||||
"allowed_origins": ["*"],
|
"allowed_origins": ["*"],
|
||||||
"allowed_methods": ["GET", "PUT", "POST", "DELETE", "OPTIONS"],
|
"allowed_methods": ["GET", "PUT", "POST", "DELETE", "OPTIONS"],
|
||||||
"allowed_headers": ["Authorization", "Content-Type"],
|
"allowed_headers": ["Authorization", "Content-Type"],
|
||||||
"allow_credentials": false
|
"allow_credentials": false
|
||||||
|
},
|
||||||
|
"rate_limiting": {
|
||||||
|
"auth_rate_limit": 5, // Maximum number of auth attempts
|
||||||
|
"auth_window_seconds": 10 // Time window in seconds (10 seconds)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
@@ -59,6 +59,17 @@ bool load_config(const std::string& config_path, ServerConfig& config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse rate limiting configuration
|
||||||
|
if (j.contains("rate_limiting")) {
|
||||||
|
const auto& rate_limit = j["rate_limiting"];
|
||||||
|
if (rate_limit.contains("auth_rate_limit")) {
|
||||||
|
config.auth_rate_limit = rate_limit["auth_rate_limit"].get<int>();
|
||||||
|
}
|
||||||
|
if (rate_limit.contains("auth_window_seconds")) {
|
||||||
|
config.auth_window_seconds = rate_limit["auth_window_seconds"].get<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "Error parsing config file: " << e.what() << std::endl;
|
std::cerr << "Error parsing config file: " << e.what() << std::endl;
|
||||||
|
@@ -17,6 +17,9 @@ struct ServerConfig {
|
|||||||
std::vector<std::string> allowed_methods = {"GET", "PUT", "POST", "DELETE", "OPTIONS"};
|
std::vector<std::string> allowed_methods = {"GET", "PUT", "POST", "DELETE", "OPTIONS"};
|
||||||
std::vector<std::string> allowed_headers = {"Authorization", "Content-Type"};
|
std::vector<std::string> allowed_headers = {"Authorization", "Content-Type"};
|
||||||
bool allow_credentials = false;
|
bool allow_credentials = false;
|
||||||
|
// Rate limiting configuration
|
||||||
|
int auth_rate_limit = 5; // Maximum number of auth attempts
|
||||||
|
int auth_window_seconds = 10; // Time window in seconds (10 seconds)
|
||||||
};
|
};
|
||||||
|
|
||||||
bool load_config(const std::string& config_path, ServerConfig& config);
|
bool load_config(const std::string& config_path, ServerConfig& config);
|
||||||
|
62
src/rate_limiter.hpp
Normal file
62
src/rate_limiter.hpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#ifndef RATE_LIMITER_HPP
|
||||||
|
#define RATE_LIMITER_HPP
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
namespace simple_object_storage {
|
||||||
|
|
||||||
|
class RateLimiter {
|
||||||
|
public:
|
||||||
|
RateLimiter(int max_requests, std::chrono::seconds window)
|
||||||
|
: max_requests_(max_requests), window_(window) {}
|
||||||
|
|
||||||
|
bool is_allowed(const std::string& key) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
auto& bucket = buckets_[key];
|
||||||
|
|
||||||
|
// Clean up old requests
|
||||||
|
while (!bucket.empty() && (now - bucket.front()) > window_) {
|
||||||
|
bucket.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're under the limit
|
||||||
|
if (bucket.size() < max_requests_) {
|
||||||
|
bucket.push(now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method: check if over the limit WITHOUT incrementing
|
||||||
|
bool is_over_limit(const std::string& key) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
auto& bucket = buckets_[key];
|
||||||
|
while (!bucket.empty() && (now - bucket.front()) > window_) {
|
||||||
|
bucket.pop();
|
||||||
|
}
|
||||||
|
return bucket.size() >= max_requests_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset(const std::string& key) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
buckets_.erase(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
int max_requests_;
|
||||||
|
std::chrono::seconds window_;
|
||||||
|
std::unordered_map<std::string, std::queue<std::chrono::steady_clock::time_point>> buckets_;
|
||||||
|
std::mutex mutex_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace simple_object_storage
|
||||||
|
|
||||||
|
#endif
|
@@ -32,38 +32,50 @@ bool Server::init_db() {
|
|||||||
|
|
||||||
bool Server::validate_write_request(const httplib::Request &req, httplib::Response &res, const std::vector<std::string> &required_params, std::map<std::string, std::string> ¶ms)
|
bool Server::validate_write_request(const httplib::Request &req, httplib::Response &res, const std::vector<std::string> &required_params, std::map<std::string, std::string> ¶ms)
|
||||||
{
|
{
|
||||||
|
std::string client_ip = req.remote_addr;
|
||||||
|
|
||||||
|
// Check if the client is already over the limit (do NOT increment)
|
||||||
|
if (auth_rate_limiter_->is_over_limit(client_ip)) {
|
||||||
|
res.status = 429;
|
||||||
|
nlohmann::json response = {{"result", "error"}, {"error", "Too many authentication attempts. Please try again later."}};
|
||||||
|
res.set_content(response.dump(), "application/json");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Get token from Authorization header
|
// Get token from Authorization header
|
||||||
std::string token;
|
std::string token;
|
||||||
if (req.has_header("Authorization")) {
|
if (req.has_header("Authorization")) {
|
||||||
const auto& auth_header = req.get_header_value("Authorization");
|
const auto& auth_header = req.get_header_value("Authorization");
|
||||||
// Check if it's a Bearer token
|
|
||||||
if (auth_header.substr(0, 7) == "Bearer ") {
|
if (auth_header.substr(0, 7) == "Bearer ") {
|
||||||
token = auth_header.substr(7);
|
token = auth_header.substr(7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.empty()) {
|
if (token.empty()) {
|
||||||
|
// Only count failed attempt (increment the limiter)
|
||||||
|
auth_rate_limiter_->is_allowed(client_ip); // This will increment the count
|
||||||
res.status = 401;
|
res.status = 401;
|
||||||
nlohmann::json response = {{"result", "error"}, {"error", "Missing or invalid Authorization header"}};
|
nlohmann::json response = {{"result", "error"}, {"error", "Missing or invalid Authorization header"}};
|
||||||
res.set_content(response.dump(), "application/json");
|
res.set_content(response.dump(), "application/json");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if token is valid
|
|
||||||
bool write_token_valid = std::find(config_.write_tokens.begin(), config_.write_tokens.end(), token) != config_.write_tokens.end();
|
bool write_token_valid = std::find(config_.write_tokens.begin(), config_.write_tokens.end(), token) != config_.write_tokens.end();
|
||||||
if (!write_token_valid) {
|
if (!write_token_valid) {
|
||||||
|
// Only count failed attempt (increment the limiter)
|
||||||
|
auth_rate_limiter_->is_allowed(client_ip); // This will increment the count
|
||||||
res.status = 403;
|
res.status = 403;
|
||||||
nlohmann::json response = {{"result", "error"}, {"error", "Invalid write token"}};
|
nlohmann::json response = {{"result", "error"}, {"error", "Invalid write token"}};
|
||||||
res.set_content(response.dump(), "application/json");
|
res.set_content(response.dump(), "application/json");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get other parameters from query params
|
// If authentication is successful, do not increment rate limiter
|
||||||
|
|
||||||
for (const auto& param : req.params) {
|
for (const auto& param : req.params) {
|
||||||
params[param.first] = param.second;
|
params[param.first] = param.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for required parameters
|
|
||||||
for (const auto& param : required_params) {
|
for (const auto& param : required_params) {
|
||||||
if (!req.has_param(param)) {
|
if (!req.has_param(param)) {
|
||||||
res.status = 400;
|
res.status = 400;
|
||||||
@@ -92,6 +104,12 @@ Server::Server(const ServerConfig& config)
|
|||||||
|
|
||||||
// Initialize the put handler
|
// Initialize the put handler
|
||||||
put_handler_ = std::make_unique<PutHandler>(*this);
|
put_handler_ = std::make_unique<PutHandler>(*this);
|
||||||
|
|
||||||
|
// Initialize rate limiter
|
||||||
|
auth_rate_limiter_ = std::make_unique<RateLimiter>(
|
||||||
|
config_.auth_rate_limit,
|
||||||
|
std::chrono::seconds(config_.auth_window_seconds)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Server::~Server() {
|
Server::~Server() {
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
#include "database.hpp"
|
#include "database.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
|
#include "rate_limiter.hpp"
|
||||||
|
|
||||||
namespace simple_object_storage {
|
namespace simple_object_storage {
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ private:
|
|||||||
httplib::Server server_;
|
httplib::Server server_;
|
||||||
bool running_;
|
bool running_;
|
||||||
std::unique_ptr<PutHandler> put_handler_;
|
std::unique_ptr<PutHandler> put_handler_;
|
||||||
|
std::unique_ptr<RateLimiter> auth_rate_limiter_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace simple_object_storage
|
} // namespace simple_object_storage
|
20
test.sh
20
test.sh
@@ -267,4 +267,24 @@ fi
|
|||||||
curl -s -H "Authorization: Bearer ${WRITE_TOKEN}" "${BASE_URL}/deleteobject?hash=${FIRST_HASH}" > /dev/null
|
curl -s -H "Authorization: Bearer ${WRITE_TOKEN}" "${BASE_URL}/deleteobject?hash=${FIRST_HASH}" > /dev/null
|
||||||
curl -s -H "Authorization: Bearer ${WRITE_TOKEN}" "${BASE_URL}/deleteobject?hash=${SECOND_HASH}" > /dev/null
|
curl -s -H "Authorization: Bearer ${WRITE_TOKEN}" "${BASE_URL}/deleteobject?hash=${SECOND_HASH}" > /dev/null
|
||||||
|
|
||||||
|
# Test 3: Verify rate limiting behavior
|
||||||
|
title "Testing rate limiting behavior"
|
||||||
|
|
||||||
|
# Use a known invalid token
|
||||||
|
INVALID_TOKEN="invalid_token"
|
||||||
|
|
||||||
|
# Make 5 requests with an invalid token
|
||||||
|
for i in {1..5}; do
|
||||||
|
echo "Attempt $i with invalid token"
|
||||||
|
RESPONSE=$(curl -s -X PUT -H "Authorization: Bearer ${INVALID_TOKEN}" -F "file=@${SCRIPT_DIR}/${SCRIPT_NAME}" -F "metadata={\"labeltags\":[\"test:latest\"]}" "${BASE_URL}/upload")
|
||||||
|
echo "Response: ${RESPONSE}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Now try a request with a valid token - should be rate limited
|
||||||
|
echo "Attempting request with valid token (should be rate limited)"
|
||||||
|
RESPONSE=$(curl -s -X PUT -H "Authorization: Bearer ${WRITE_TOKEN}" -F "file=@${SCRIPT_DIR}/${SCRIPT_NAME}" -F "metadata={\"labeltags\":[\"test:latest\"]}" "${BASE_URL}/upload")
|
||||||
|
if ! echo "${RESPONSE}" | jq -r '.error' | grep -q "Too many authentication attempts"; then
|
||||||
|
die "Expected rate limit error, got: ${RESPONSE}"
|
||||||
|
fi
|
||||||
|
|
||||||
title "ALL TESTS PASSED"
|
title "ALL TESTS PASSED"
|
||||||
|
Reference in New Issue
Block a user