-
Notifications
You must be signed in to change notification settings - Fork 37
Add UI setting to bind to 0.0.0.0 #233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,3 +7,4 @@ testext | |
| test/python/__pycache__/ | ||
| .Rhistory | ||
| .vscode/ | ||
| node_modules/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| #include "settings.hpp" | ||
| #include "state.hpp" | ||
| #include "utils/encoding.hpp" | ||
| #include "utils/token.hpp" | ||
| #include "utils/env.hpp" | ||
| #include "utils/helpers.hpp" | ||
| #include "utils/md_helpers.hpp" | ||
|
|
@@ -101,27 +102,34 @@ const HttpServer &HttpServer::Start(ClientContext &context, bool *was_started) { | |
| *was_started = false; | ||
| } | ||
|
|
||
| const auto host = GetLocalHost(context); | ||
| const auto remote_url = GetRemoteUrl(context); | ||
| const auto port = GetLocalPort(context); | ||
| auto &http_util = HTTPUtil::Get(*context.db); | ||
| // FIXME - https://github.com/duckdb/duckdb/pull/17655 will remove `unused` | ||
| auto http_params = http_util.InitializeParameters(context, "unused"); | ||
| auto server = GetInstance(context); | ||
| server->DoStart(port, remote_url, std::move(http_params)); | ||
| const auto token_auth = GetEnableTokenAuth(context); | ||
| server->DoStart(host, port, remote_url, std::move(http_params), token_auth); | ||
| return *server; | ||
| } | ||
|
|
||
| void HttpServer::DoStart(const uint16_t _local_port, | ||
| void HttpServer::DoStart(const std::string &_local_host, | ||
| const uint16_t _local_port, | ||
| const std::string &_remote_url, | ||
| unique_ptr<HTTPParams> _http_params) { | ||
| unique_ptr<HTTPParams> _http_params, | ||
| bool _token_auth_enabled) { | ||
| if (Started()) { | ||
| throw std::runtime_error("HttpServer already started"); | ||
| } | ||
|
|
||
| local_host = _local_host; | ||
| local_port = _local_port; | ||
| local_url = StringUtil::Format("http://localhost:%d", local_port); | ||
| local_url = StringUtil::Format("http://%s:%d", local_host, local_port); | ||
| remote_url = _remote_url; | ||
| http_params = std::move(_http_params); | ||
| token_auth_enabled = _token_auth_enabled; | ||
| auth_token = GenerateToken(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't generate a token if token auth is disabled, both to avoid the unnecessary work, and also to avoid it showing up in the URL. |
||
| user_agent = | ||
| StringUtil::Format("duckdb-ui/%s-%s(%s)", DuckDB::LibraryVersion(), | ||
| UI_EXTENSION_VERSION, DuckDB::Platform()); | ||
|
|
@@ -163,11 +171,72 @@ void HttpServer::DoStop() { | |
| http_params = nullptr; | ||
| event_dispatcher = nullptr; | ||
| remote_url = ""; | ||
| local_host = ""; | ||
| local_port = 0; | ||
| } | ||
|
|
||
| std::string HttpServer::LocalUrl() const { | ||
| return StringUtil::Format("http://localhost:%d/", local_port); | ||
| return StringUtil::Format("http://%s:%d/", local_host, local_port); | ||
| } | ||
|
|
||
| std::string HttpServer::GetAuthToken() const { | ||
| return auth_token; | ||
| } | ||
|
|
||
| std::string HttpServer::ExtractTokenFromCookie(const std::string &cookie_header) { | ||
| const std::string prefix = "duckdb_ui_token="; | ||
| size_t pos = cookie_header.find(prefix); | ||
| if (pos == std::string::npos) { | ||
| return ""; | ||
| } | ||
| pos += prefix.size(); | ||
| size_t end = cookie_header.find(';', pos); | ||
| if (end == std::string::npos) { | ||
| return cookie_header.substr(pos); | ||
| } | ||
| return cookie_header.substr(pos, end - pos); | ||
| } | ||
|
|
||
| void HttpServer::SetTokenCookie(httplib::Response &res) { | ||
| res.set_header("Set-Cookie", | ||
| "duckdb_ui_token=" + auth_token + | ||
| "; Path=/; HttpOnly; SameSite=Strict"); | ||
| } | ||
|
|
||
| bool HttpServer::ValidateToken(const httplib::Request &req, | ||
| httplib::Response &res) { | ||
| if (!token_auth_enabled) { | ||
| return true; | ||
| } | ||
|
|
||
| // Check Authorization header: "token <TOKEN>" | ||
| auto auth_header = req.get_header_value("Authorization"); | ||
| if (auth_header.size() > 6 && auth_header.compare(0, 6, "token ") == 0) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should make this auth scheme more specific, such as "duckdb-ui-token" or "duckdb_ui_token", to match the cookie. |
||
| if (auth_header.substr(6) == auth_token) { | ||
| SetTokenCookie(res); | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| // Check URL parameter: ?token=<TOKEN> | ||
| if (req.has_param("token")) { | ||
| if (req.get_param_value("token") == auth_token) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should name this URL param a bit more uniquely, to prevent it from interacting with other potential URL params. How about "duckdb-ui-token" or "duckdb_ui_token", to match the cookie? |
||
| SetTokenCookie(res); | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| // Check cookie: duckdb_ui_token=<TOKEN> | ||
| auto cookie_header = req.get_header_value("Cookie"); | ||
| if (!cookie_header.empty()) { | ||
| auto cookie_token = ExtractTokenFromCookie(cookie_header); | ||
| if (cookie_token == auth_token) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| res.status = 401; | ||
| return false; | ||
| } | ||
|
|
||
| shared_ptr<DatabaseInstance> HttpServer::LockDatabaseInstance() { | ||
|
|
@@ -203,7 +272,7 @@ void HttpServer::Run() { | |
| const httplib::ContentReader &content_reader) { | ||
| HandleTokenize(req, res, content_reader); | ||
| }); | ||
| server.listen("localhost", local_port); | ||
| server.listen(local_host, local_port); | ||
| } | ||
|
|
||
| void HttpServer::HandleGetInfo(const httplib::Request &req, | ||
|
|
@@ -217,6 +286,10 @@ void HttpServer::HandleGetInfo(const httplib::Request &req, | |
|
|
||
| void HttpServer::HandleGetLocalEvents(const httplib::Request &req, | ||
| httplib::Response &res) { | ||
| if (!ValidateToken(req, res)) { | ||
| return; | ||
| } | ||
|
|
||
| res.set_chunked_content_provider( | ||
| "text/event-stream", [&](size_t /*offset*/, httplib::DataSink &sink) { | ||
| if (event_dispatcher && event_dispatcher->WaitEvent(&sink)) { | ||
|
|
@@ -230,11 +303,7 @@ void HttpServer::HandleGetLocalEvents(const httplib::Request &req, | |
|
|
||
| void HttpServer::HandleGetLocalToken(const httplib::Request &req, | ||
| httplib::Response &res) { | ||
| // GET requests don't include Origin, so use Referer instead. | ||
| // Referer includes the path, so only compare the start. | ||
| auto referer = req.get_header_value("Referer"); | ||
| if (referer.compare(0, local_url.size(), local_url) != 0) { | ||
| res.status = 401; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should keep this check unless the local_host setting is something other than "localhost". (This is a variant of the other origin checks I mentioned.) |
||
| if (!ValidateToken(req, res)) { | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -282,6 +351,10 @@ void HttpServer::InitClientFromParams(httplib::Client &client) { | |
|
|
||
| void HttpServer::HandleGet(const httplib::Request &req, | ||
| httplib::Response &res) { | ||
| if (!ValidateToken(req, res)) { | ||
| return; | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need (and shouldn't) require the token for these forwarded GET requests. The purpose of the token auth is to protect access to the DuckDB instance, but this function just proxies requests to the Internet, so there's nothing local to protect. |
||
|
|
||
| // Create HTTP client to remote URL | ||
| // TODO: Can this be created once and shared? | ||
| httplib::Client client(remote_url); | ||
|
|
@@ -298,7 +371,10 @@ void HttpServer::HandleGet(const httplib::Request &req, | |
| } | ||
|
|
||
| // forward GET to remote URL | ||
| auto result = client.Get(req.path, req.params, headers); | ||
| // Strip the token parameter before forwarding to remote | ||
| auto params = req.params; | ||
| params.erase("token"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is where the general name "token" is most likely to cause problems, hence my other comment to make it more unique (e.g. "duckdb-ui-token"). |
||
| auto result = client.Get(req.path, params, headers); | ||
| if (!result) { | ||
| res.status = 500; | ||
| res.set_content("Could not fetch: '" + req.path + "' from '" + remote_url + | ||
|
|
@@ -325,6 +401,10 @@ void HttpServer::HandleGet(const httplib::Request &req, | |
|
|
||
| void HttpServer::HandleInterrupt(const httplib::Request &req, | ||
| httplib::Response &res) { | ||
| if (!ValidateToken(req, res)) { | ||
| return; | ||
| } | ||
|
|
||
| auto origin = req.get_header_value("Origin"); | ||
| if (origin != local_url) { | ||
| res.status = 401; | ||
|
|
@@ -365,6 +445,10 @@ void HttpServer::HandleRun(const httplib::Request &req, httplib::Response &res, | |
| void HttpServer::DoHandleRun(const httplib::Request &req, | ||
| httplib::Response &res, | ||
| const httplib::ContentReader &content_reader) { | ||
| if (!ValidateToken(req, res)) { | ||
| return; | ||
| } | ||
|
|
||
| auto origin = req.get_header_value("Origin"); | ||
| if (origin != local_url) { | ||
| res.status = 401; | ||
|
|
@@ -665,6 +749,10 @@ void HttpServer::DoHandleRun(const httplib::Request &req, | |
| void HttpServer::HandleTokenize(const httplib::Request &req, | ||
| httplib::Response &res, | ||
| const httplib::ContentReader &content_reader) { | ||
| if (!ValidateToken(req, res)) { | ||
| return; | ||
| } | ||
|
|
||
| auto origin = req.get_header_value("Origin"); | ||
| if (origin != local_url) { | ||
| res.status = 401; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,16 @@ | |
| #include <duckdb/common/exception.hpp> | ||
| #include <duckdb/main/client_context.hpp> | ||
|
|
||
| #define UI_LOCAL_HOST_SETTING_NAME "ui_local_host" | ||
| #define UI_LOCAL_HOST_SETTING_DEFAULT "localhost" | ||
| #define UI_LOCAL_PORT_SETTING_NAME "ui_local_port" | ||
| #define UI_LOCAL_PORT_SETTING_DEFAULT 4213 | ||
| #define UI_REMOTE_URL_SETTING_NAME "ui_remote_url" | ||
| #define UI_REMOTE_URL_SETTING_DEFAULT "https://ui.duckdb.org" | ||
| #define UI_POLLING_INTERVAL_SETTING_NAME "ui_polling_interval" | ||
| #define UI_POLLING_INTERVAL_SETTING_DEFAULT 284 | ||
| #define UI_ENABLE_TOKEN_AUTH_SETTING_NAME "ui_enable_token_auth" | ||
| #define UI_ENABLE_TOKEN_AUTH_SETTING_DEFAULT true | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should:
|
||
|
|
||
| namespace duckdb { | ||
|
|
||
|
|
@@ -25,8 +29,10 @@ T GetSetting(const ClientContext &context, const char *setting_name) { | |
| } | ||
| } // namespace internal | ||
|
|
||
| std::string GetLocalHost(const ClientContext &); | ||
| std::string GetRemoteUrl(const ClientContext &); | ||
| uint16_t GetLocalPort(const ClientContext &); | ||
| uint32_t GetPollingInterval(const ClientContext &); | ||
| bool GetEnableTokenAuth(const ClientContext &); | ||
|
|
||
| } // namespace duckdb | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #pragma once | ||
|
|
||
| #include <string> | ||
|
|
||
| namespace duckdb { | ||
|
|
||
| std::string GenerateToken(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. General naming comment: There's an existing use of "token" in the extension - the "localToken" endpoint and the "GetMDToken" helper - for (optional) MotherDuck support. This change adds a second kind of token. To avoid confusion, we should qualify this new token wherever we refer to it, in function names, variables, etc. I recommend "DuckDBUIToken". |
||
|
|
||
| } // namespace duckdb | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: alphabetize