From 88f0155e6f83071ec8ae812ce091b49a2565acf5 Mon Sep 17 00:00:00 2001 From: lmangani Date: Tue, 15 Oct 2024 09:01:12 +0000 Subject: [PATCH 1/2] authentication --- src/httpserver_extension.cpp | 62 ++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/httpserver_extension.cpp b/src/httpserver_extension.cpp index 613398f..770bb0b 100644 --- a/src/httpserver_extension.cpp +++ b/src/httpserver_extension.cpp @@ -31,6 +31,7 @@ struct HttpServerState { std::atomic is_running; DatabaseInstance* db_instance; unique_ptr allocator; + std::string auth_token; HttpServerState() : is_running(false), db_instance(nullptr) {} }; @@ -129,6 +130,51 @@ static std::string ConvertResultToJSON(MaterializedQueryResult &result, ReqStats return json_output; } +// New: Base64 decoding function +std::string base64_decode(const std::string &in) { + std::string out; + std::vector T(256, -1); + for (int i = 0; i < 64; i++) + T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i; + + int val = 0, valb = -8; + for (unsigned char c : in) { + if (T[c] == -1) break; + val = (val << 6) + T[c]; + valb += 6; + if (valb >= 0) { + out.push_back(char((val >> valb) & 0xFF)); + valb -= 8; + } + } + return out; +} + +// Auth Check +bool IsAuthenticated(const duckdb_httplib_openssl::Request& req) { + if (global_state.auth_token.empty()) { + return true; // No authentication required if no token is set + } + + // Check for X-API-Key header + auto api_key = req.get_header_value("X-API-Key"); + if (!api_key.empty() && api_key == global_state.auth_token) { + return true; + } + + // Check for Basic Auth + auto auth = req.get_header_value("Authorization"); + if (!auth.empty() && auth.compare(0, 6, "Basic ") == 0) { + std::string decoded_auth = base64_decode(auth.substr(6)); + if (decoded_auth == global_state.auth_token) { + return true; + } + } + + return false; +} + + // Convert the query result to NDJSON (JSONEachRow) format static std::string ConvertResultToNDJSON(MaterializedQueryResult &result) { std::string ndjson_output; @@ -208,6 +254,13 @@ static void HandleQuery(const string& query, duckdb_httplib_openssl::Response& r void HandleHttpRequest(const duckdb_httplib_openssl::Request& req, duckdb_httplib_openssl::Response& res) { std::string query; + // Check authentication + if (!IsAuthenticated(req)) { + res.status = 401; + res.set_content("Unauthorized", "text/plain"); + return; + } + // CORS allow res.set_header("Access-Control-Allow-Origin", "*"); res.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT"); @@ -295,7 +348,7 @@ void HandleHttpRequest(const duckdb_httplib_openssl::Request& req, duckdb_httpli } } -void HttpServerStart(DatabaseInstance& db, string_t host, int32_t port) { +void HttpServerStart(DatabaseInstance& db, string_t host, int32_t port, string_t auth = string_t()) { if (global_state.is_running) { throw IOException("HTTP server is already running"); } @@ -303,6 +356,7 @@ void HttpServerStart(DatabaseInstance& db, string_t host, int32_t port) { global_state.db_instance = &db; global_state.server = make_uniq(); global_state.is_running = true; + global_state.auth_token = auth.GetString(); // CORS Preflight global_state.server->Options("/", @@ -359,17 +413,19 @@ static void HttpServerCleanup() { static void LoadInternal(DatabaseInstance &instance) { auto httpserve_start = ScalarFunction("httpserve_start", - {LogicalType::VARCHAR, LogicalType::INTEGER}, + {LogicalType::VARCHAR, LogicalType::INTEGER, LogicalType::VARCHAR}, LogicalType::VARCHAR, [&](DataChunk &args, ExpressionState &state, Vector &result) { auto &host_vector = args.data[0]; auto &port_vector = args.data[1]; + auto &auth_vector = args.data[2]; UnaryExecutor::Execute( host_vector, result, args.size(), [&](string_t host) { auto port = ((int32_t*)port_vector.GetData())[0]; - HttpServerStart(instance, host, port); + auto auth = ((string_t*)auth_vector.GetData())[0]; + HttpServerStart(instance, host, port, auth); return StringVector::AddString(result, "HTTP server started on " + host.GetString() + ":" + std::to_string(port)); }); }); From bc6afe280a18ccc0344a5d2a25b044cf6f8ed9c5 Mon Sep 17 00:00:00 2001 From: Lorenzo Mangani Date: Tue, 15 Oct 2024 12:55:21 +0200 Subject: [PATCH 2/2] Document Authentication --- docs/README.md | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/docs/README.md b/docs/README.md index 3c47057..24ab827 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,16 +32,39 @@ LOAD httpserver; ``` ### 🔌 Usage -Start the HTTP server providing the `host` and `port` parameters +Start the HTTP server providing the `host`, `port` and `auth` parameters.
+> If you want no authhentication, just pass an empty string. + +#### Basic Auth ```sql -D SELECT httpserve_start('0.0.0.0',9999); -┌─────────────────────────────────────┐ -│ httpserve_start('0.0.0.0', 9999) │ -│ varchar │ -├─────────────────────────────────────┤ -│ HTTP server started on 0.0.0.0:9999 │ -└─────────────────────────────────────┘ +D SELECT httpserve_start('localhost', 9999, 'user:pass'); + +┌───────────────────────────────────────────────┐ +│ httpserve_start('0.0.0.0', 9999, 'user:pass') │ +│ varchar │ +├───────────────────────────────────────────────┤ +│ HTTP server started on 0.0.0.0:9999 │ +└───────────────────────────────────────────────┘ ``` +```bash +curl -X POST -d "SELECT 'hello', version()" "http://user:pass@localhost:9999/" +``` + +#### Token Auth +```sql +SELECT httpserve_start('localhost', 9999, 'supersecretkey'); + +┌───────────────────────────────────────────────┐ +│ httpserve_start('0.0.0.0', 9999, 'secretkey') │ +│ varchar │ +├───────────────────────────────────────────────┤ +│ HTTP server started on 0.0.0.0:9999 │ +└───────────────────────────────────────────────┘ +``` +``` +curl -X POST --header "X-API-Key: supersecretkey" -d "SELECT 'hello', version()" "http://localhost:9999/" +``` + #### 👉 QUERY UI Browse to your endpoint and use the built-in quackplay interface _(experimental)_