Skip to content

Commit ec47a4c

Browse files
authored
Merge pull request #6 from lmangani/auth
API Authentication (basic or x-header)
2 parents 29a7049 + bc6afe2 commit ec47a4c

File tree

2 files changed

+90
-11
lines changed

2 files changed

+90
-11
lines changed

docs/README.md

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,39 @@ LOAD httpserver;
3232
```
3333

3434
### 🔌 Usage
35-
Start the HTTP server providing the `host` and `port` parameters
35+
Start the HTTP server providing the `host`, `port` and `auth` parameters.<br>
36+
> If you want no authhentication, just pass an empty string.
37+
38+
#### Basic Auth
3639
```sql
37-
D SELECT httpserve_start('0.0.0.0',9999);
38-
┌─────────────────────────────────────┐
39-
│ httpserve_start('0.0.0.0', 9999) │
40-
varchar
41-
├─────────────────────────────────────┤
42-
│ HTTP server started on 0.0.0.0:9999
43-
└─────────────────────────────────────┘
40+
D SELECT httpserve_start('localhost', 9999, 'user:pass');
41+
42+
┌───────────────────────────────────────────────┐
43+
│ httpserve_start('0.0.0.0', 9999, 'user:pass') │
44+
varchar
45+
├───────────────────────────────────────────────┤
46+
│ HTTP server started on 0.0.0.0:9999
47+
└───────────────────────────────────────────────┘
4448
```
49+
```bash
50+
curl -X POST -d "SELECT 'hello', version()" "http://user:pass@localhost:9999/"
51+
```
52+
53+
#### Token Auth
54+
```sql
55+
SELECT httpserve_start('localhost', 9999, 'supersecretkey');
56+
57+
┌───────────────────────────────────────────────┐
58+
│ httpserve_start('0.0.0.0', 9999, 'secretkey') │
59+
varchar
60+
├───────────────────────────────────────────────┤
61+
│ HTTP server started on 0.0.0.0:9999
62+
└───────────────────────────────────────────────┘
63+
```
64+
```
65+
curl -X POST --header "X-API-Key: supersecretkey" -d "SELECT 'hello', version()" "http://localhost:9999/"
66+
```
67+
4568

4669
#### 👉 QUERY UI
4770
Browse to your endpoint and use the built-in quackplay interface _(experimental)_

src/httpserver_extension.cpp

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct HttpServerState {
3131
std::atomic<bool> is_running;
3232
DatabaseInstance* db_instance;
3333
unique_ptr<Allocator> allocator;
34+
std::string auth_token;
3435

3536
HttpServerState() : is_running(false), db_instance(nullptr) {}
3637
};
@@ -129,6 +130,51 @@ static std::string ConvertResultToJSON(MaterializedQueryResult &result, ReqStats
129130
return json_output;
130131
}
131132

133+
// New: Base64 decoding function
134+
std::string base64_decode(const std::string &in) {
135+
std::string out;
136+
std::vector<int> T(256, -1);
137+
for (int i = 0; i < 64; i++)
138+
T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i;
139+
140+
int val = 0, valb = -8;
141+
for (unsigned char c : in) {
142+
if (T[c] == -1) break;
143+
val = (val << 6) + T[c];
144+
valb += 6;
145+
if (valb >= 0) {
146+
out.push_back(char((val >> valb) & 0xFF));
147+
valb -= 8;
148+
}
149+
}
150+
return out;
151+
}
152+
153+
// Auth Check
154+
bool IsAuthenticated(const duckdb_httplib_openssl::Request& req) {
155+
if (global_state.auth_token.empty()) {
156+
return true; // No authentication required if no token is set
157+
}
158+
159+
// Check for X-API-Key header
160+
auto api_key = req.get_header_value("X-API-Key");
161+
if (!api_key.empty() && api_key == global_state.auth_token) {
162+
return true;
163+
}
164+
165+
// Check for Basic Auth
166+
auto auth = req.get_header_value("Authorization");
167+
if (!auth.empty() && auth.compare(0, 6, "Basic ") == 0) {
168+
std::string decoded_auth = base64_decode(auth.substr(6));
169+
if (decoded_auth == global_state.auth_token) {
170+
return true;
171+
}
172+
}
173+
174+
return false;
175+
}
176+
177+
132178
// Convert the query result to NDJSON (JSONEachRow) format
133179
static std::string ConvertResultToNDJSON(MaterializedQueryResult &result) {
134180
std::string ndjson_output;
@@ -208,6 +254,13 @@ static void HandleQuery(const string& query, duckdb_httplib_openssl::Response& r
208254
void HandleHttpRequest(const duckdb_httplib_openssl::Request& req, duckdb_httplib_openssl::Response& res) {
209255
std::string query;
210256

257+
// Check authentication
258+
if (!IsAuthenticated(req)) {
259+
res.status = 401;
260+
res.set_content("Unauthorized", "text/plain");
261+
return;
262+
}
263+
211264
// CORS allow
212265
res.set_header("Access-Control-Allow-Origin", "*");
213266
res.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT");
@@ -295,14 +348,15 @@ void HandleHttpRequest(const duckdb_httplib_openssl::Request& req, duckdb_httpli
295348
}
296349
}
297350

298-
void HttpServerStart(DatabaseInstance& db, string_t host, int32_t port) {
351+
void HttpServerStart(DatabaseInstance& db, string_t host, int32_t port, string_t auth = string_t()) {
299352
if (global_state.is_running) {
300353
throw IOException("HTTP server is already running");
301354
}
302355

303356
global_state.db_instance = &db;
304357
global_state.server = make_uniq<duckdb_httplib_openssl::Server>();
305358
global_state.is_running = true;
359+
global_state.auth_token = auth.GetString();
306360

307361
// CORS Preflight
308362
global_state.server->Options("/",
@@ -359,17 +413,19 @@ static void HttpServerCleanup() {
359413

360414
static void LoadInternal(DatabaseInstance &instance) {
361415
auto httpserve_start = ScalarFunction("httpserve_start",
362-
{LogicalType::VARCHAR, LogicalType::INTEGER},
416+
{LogicalType::VARCHAR, LogicalType::INTEGER, LogicalType::VARCHAR},
363417
LogicalType::VARCHAR,
364418
[&](DataChunk &args, ExpressionState &state, Vector &result) {
365419
auto &host_vector = args.data[0];
366420
auto &port_vector = args.data[1];
421+
auto &auth_vector = args.data[2];
367422

368423
UnaryExecutor::Execute<string_t, string_t>(
369424
host_vector, result, args.size(),
370425
[&](string_t host) {
371426
auto port = ((int32_t*)port_vector.GetData())[0];
372-
HttpServerStart(instance, host, port);
427+
auto auth = ((string_t*)auth_vector.GetData())[0];
428+
HttpServerStart(instance, host, port, auth);
373429
return StringVector::AddString(result, "HTTP server started on " + host.GetString() + ":" + std::to_string(port));
374430
});
375431
});

0 commit comments

Comments
 (0)