Compare commits
3 Commits
backup-bef
...
v1.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
| e11686f4a0 | |||
| 5821ee7fc8 | |||
| c52ad95c35 |
180
.github/workflows/build-linux-amd64.yml
vendored
180
.github/workflows/build-linux-amd64.yml
vendored
@@ -6,6 +6,8 @@ on:
|
|||||||
description: Package version (defaults to tag name or short SHA)
|
description: Package version (defaults to tag name or short SHA)
|
||||||
required: false
|
required: false
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
@@ -16,6 +18,33 @@ jobs:
|
|||||||
EXTENSION_NAME: ui
|
EXTENSION_NAME: ui
|
||||||
DUCKDB_PLATFORM: linux_amd64
|
DUCKDB_PLATFORM: linux_amd64
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository (manual)
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -d .git ]; then
|
||||||
|
echo "Repository already present"
|
||||||
|
else
|
||||||
|
server="${GITHUB_SERVER_URL:-${{ github.server_url }}}"
|
||||||
|
repo_full="${GITHUB_REPOSITORY:-${{ github.repository }}}"
|
||||||
|
sha="${GITHUB_SHA:-${{ github.sha }}}"
|
||||||
|
host="$(echo "$server" | sed -E 's#^https?://([^/]+).*#\1#')"
|
||||||
|
if [ -n "${TOKEN:-}" ]; then
|
||||||
|
umask 077
|
||||||
|
printf "machine %s\n login token\n password %s\n" "$host" "$TOKEN" > "$HOME/.netrc"
|
||||||
|
fi
|
||||||
|
git init .
|
||||||
|
git config --global --add safe.directory "$(pwd)"
|
||||||
|
git remote add origin "$server/$repo_full.git"
|
||||||
|
git -c http.https://$host/.extraheader="" fetch --depth=1 origin "$sha"
|
||||||
|
git checkout -q FETCH_HEAD
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Show workspace status
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
git --no-pager status | cat
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
@@ -24,9 +53,154 @@ jobs:
|
|||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
build-essential cmake ninja-build python3 python3-venv pkg-config \
|
build-essential cmake ninja-build python3 python3-venv pkg-config \
|
||||||
libssl-dev curl git ca-certificates
|
libssl-dev curl git ca-certificates
|
||||||
# ... submodule init, build release, find artifact ...
|
- name: Preflight Gitea upload (fast-fail)
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
: "${GITEA_TOKEN:?GITEA_TOKEN secret is required}"
|
||||||
|
server="${GITHUB_SERVER_URL:-${{ github.server_url }}}"
|
||||||
|
owner="${GITHUB_REPOSITORY_OWNER:-${{ github.repository_owner }}}"
|
||||||
|
repo_full="${GITHUB_REPOSITORY:-${{ github.repository }}}"
|
||||||
|
pkg="${repo_full##*/}-preflight"
|
||||||
|
version="preflight-${GITHUB_RUN_ID:-0}-${GITHUB_RUN_ATTEMPT:-0}-$(date +%s)"
|
||||||
|
name="check.bin"
|
||||||
|
tmpfile="$(mktemp)"
|
||||||
|
printf "auth check %s\n" "$(date -u +%FT%TZ)" > "$tmpfile"
|
||||||
|
# Normalize server to effective scheme+host (handles http->https redirects)
|
||||||
|
base_no_trail="$(echo "$server" | sed 's#/*$##')"
|
||||||
|
# Use GET (not HEAD) to avoid servers that don't support HEAD on this endpoint
|
||||||
|
effective_version_url=$(curl -sS -L -o /dev/null -w '%{url_effective}' "$base_no_trail/api/v1/version" || echo "")
|
||||||
|
normalized_server=$(echo "$effective_version_url" | sed -E 's#^(https?://[^/]+).*$#\1#')
|
||||||
|
if [ -n "$normalized_server" ]; then
|
||||||
|
server="$normalized_server"
|
||||||
|
fi
|
||||||
|
url="$server/api/packages/$owner/generic/$pkg/$version/$name?replace=1"
|
||||||
|
auth_user="${ACTOR:-$owner}"
|
||||||
|
echo "Preflight: server=$server owner=$owner package=$pkg version=$version"
|
||||||
|
# Perform preflight upload using Basic auth directly
|
||||||
|
if curl -fS -L -X PUT \
|
||||||
|
-u "$auth_user:${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--upload-file "$tmpfile" "$url" >/dev/null; then
|
||||||
|
echo "Preflight upload succeeded, cleaning up"
|
||||||
|
else
|
||||||
|
echo "Preflight upload failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Cleanup the uploaded dummy package version (best effort)
|
||||||
|
curl -sS -L -o /dev/null -w " delete -> HTTP %{http_code}\n" \
|
||||||
|
-u "$auth_user:${GITEA_TOKEN}" -X DELETE \
|
||||||
|
"$server/api/packages/$owner/generic/$pkg/$version" || true
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
- name: Build release (linux_amd64)
|
||||||
|
env:
|
||||||
|
GEN: ""
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
make -j"$(nproc)" release
|
||||||
|
|
||||||
|
- name: Find extension artifact
|
||||||
|
id: artifact
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
path="$(ls -1 build/release/extension/${EXTENSION_NAME}/${EXTENSION_NAME}.duckdb_extension 2>/dev/null || true)"
|
||||||
|
if [ -z "$path" ]; then
|
||||||
|
path="$(find build/release -type f -name '*.duckdb_extension' | head -n 1 || true)"
|
||||||
|
fi
|
||||||
|
if [ -z "$path" ]; then
|
||||||
|
echo "Extension artifact not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "file=$path" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Found: $path"
|
||||||
|
|
||||||
|
- name: Compute package version
|
||||||
|
id: ver
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version='${{ inputs.version }}'
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||||
|
version='${{ github.ref_name }}'
|
||||||
|
else
|
||||||
|
version="dev-${GITHUB_SHA::8}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Using version: $version"
|
||||||
- name: Upload to Gitea Packages (generic)
|
- name: Upload to Gitea Packages (generic)
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
run: |
|
run: |
|
||||||
curl -fsSL -X PUT -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/octet-stream" --upload-file "$file" "$server/api/packages/$owner/generic/$pkg/$version/$name?replace=1"
|
set -euo pipefail
|
||||||
|
: "${GITEA_TOKEN:?GITEA_TOKEN secret is required}"
|
||||||
|
# Derive server/owner/pkg from env if not provided
|
||||||
|
server="${GITHUB_SERVER_URL:-${{ github.server_url }}}"
|
||||||
|
owner="${GITHUB_REPOSITORY_OWNER:-${{ github.repository_owner }}}"
|
||||||
|
repo_full="${GITHUB_REPOSITORY:-${{ github.repository }}}"
|
||||||
|
pkg="${repo_full##*/}"
|
||||||
|
# Use previously computed version & artifact if available
|
||||||
|
version='${{ steps.ver.outputs.version }}'
|
||||||
|
file='${{ steps.artifact.outputs.file }}'
|
||||||
|
# Fallbacks if steps were skipped
|
||||||
|
if [ -z "${version}" ]; then
|
||||||
|
if [ -n "${GITHUB_REF_TYPE:-}" ] && [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||||
|
version="${GITHUB_REF_NAME:-dev-${GITHUB_SHA::8}}"
|
||||||
|
else
|
||||||
|
version="dev-${GITHUB_SHA::8}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "${file}" ]; then
|
||||||
|
file="$(ls -1 build/release/extension/${EXTENSION_NAME}/${EXTENSION_NAME}.duckdb_extension 2>/dev/null || true)"
|
||||||
|
if [ -z "$file" ]; then
|
||||||
|
file="$(find build/release -type f -name '*.duckdb_extension' | head -n 1 || true)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ -n "$server" ] || { echo "server not set" >&2; exit 1; }
|
||||||
|
[ -n "$owner" ] || { echo "owner not set" >&2; exit 1; }
|
||||||
|
[ -n "$pkg" ] || { echo "pkg not set" >&2; exit 1; }
|
||||||
|
[ -n "$version" ] || { echo "version not set" >&2; exit 1; }
|
||||||
|
[ -n "$file" ] || { echo "file not set" >&2; exit 1; }
|
||||||
|
# Normalize server using effective URL of /api/v1/version (handles http->https)
|
||||||
|
base_no_trail="$(echo "$server" | sed 's#/*$##')"
|
||||||
|
effective_version_url=$(curl -sS -L -o /dev/null -w '%{url_effective}' "$base_no_trail/api/v1/version" || echo "")
|
||||||
|
normalized_server=$(echo "$effective_version_url" | sed -E 's#^(https?://[^/]+).*$#\1#')
|
||||||
|
if [ -n "$normalized_server" ]; then
|
||||||
|
server="$normalized_server"
|
||||||
|
fi
|
||||||
|
# Use the GitHub actor as basic auth username by default
|
||||||
|
auth_user="${ACTOR:-$owner}"
|
||||||
|
name="$(basename "$file")"
|
||||||
|
url="$server/api/packages/$owner/generic/$pkg/$version/$name?replace=1"
|
||||||
|
echo "Uploading $file to $url"
|
||||||
|
echo " auth user=$auth_user"
|
||||||
|
|
||||||
|
# Use Basic auth directly (works with package registry)
|
||||||
|
curl -fS -L -X PUT \
|
||||||
|
-u "$auth_user:${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--retry 2 --retry-delay 2 --max-time 300 \
|
||||||
|
--upload-file "$file" "$url"
|
||||||
|
echo "Upload complete."
|
||||||
|
# Also upload the DuckDB shell binary
|
||||||
|
bin_path="./build/release/duckdb"
|
||||||
|
if [ ! -f "$bin_path" ]; then
|
||||||
|
echo "duckdb binary not found at $bin_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
bin_name="$(basename "$bin_path")"
|
||||||
|
bin_url="$server/api/packages/$owner/generic/$pkg/$version/$bin_name?replace=1"
|
||||||
|
echo "Uploading $bin_path to $bin_url"
|
||||||
|
curl -fS -L -X PUT \
|
||||||
|
-u "$auth_user:${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--retry 2 --retry-delay 2 --max-time 300 \
|
||||||
|
--upload-file "$bin_path" "$bin_url"
|
||||||
|
echo "DuckDB binary upload complete."
|
||||||
@@ -22,6 +22,76 @@ namespace ui {
|
|||||||
|
|
||||||
unique_ptr<HttpServer> HttpServer::server_instance;
|
unique_ptr<HttpServer> HttpServer::server_instance;
|
||||||
|
|
||||||
|
// Helpers for validating request origin/referer in deployments where the UI is
|
||||||
|
// exposed on a non-localhost host (e.g., Docker, k8s, reverse proxies). These
|
||||||
|
// checks allow either the configured local_url, or the runtime host derived
|
||||||
|
// from the request headers. They also allow an escape hatch via the
|
||||||
|
// environment variable `ui_allow_any_origin=1|true`.
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Returns true if the given referer begins with any of the expected base URLs.
|
||||||
|
static bool RefererStartsWithAny(const std::string &referer,
|
||||||
|
const std::vector<std::string> &bases) {
|
||||||
|
for (const auto &base : bases) {
|
||||||
|
if (!base.empty() && referer.compare(0, base.size(), base) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string>
|
||||||
|
ExpectedBaseUrls(const httplib::Request &req, const std::string &local_url) {
|
||||||
|
// Prefer forwarded host if present, otherwise fall back to Host.
|
||||||
|
auto forwarded_host = req.get_header_value("X-Forwarded-Host");
|
||||||
|
auto host = forwarded_host.empty() ? req.get_header_value("Host")
|
||||||
|
: forwarded_host;
|
||||||
|
|
||||||
|
std::vector<std::string> bases;
|
||||||
|
bases.push_back(local_url);
|
||||||
|
if (!host.empty()) {
|
||||||
|
bases.push_back(StringUtil::Format("http://%s", host));
|
||||||
|
bases.push_back(StringUtil::Format("https://%s", host));
|
||||||
|
}
|
||||||
|
return bases;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsOriginAllowed(const httplib::Request &req,
|
||||||
|
const std::string &local_url) {
|
||||||
|
if (IsEnvEnabled("ui_allow_any_origin")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto origin = req.get_header_value("Origin");
|
||||||
|
if (origin.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bases = ExpectedBaseUrls(req, local_url);
|
||||||
|
for (const auto &base : bases) {
|
||||||
|
if (origin == base) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsRefererAllowed(const httplib::Request &req,
|
||||||
|
const std::string &local_url) {
|
||||||
|
if (IsEnvEnabled("ui_allow_any_origin")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto referer = req.get_header_value("Referer");
|
||||||
|
if (referer.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefererStartsWithAny(referer, ExpectedBaseUrls(req, local_url));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
HttpServer *HttpServer::GetInstance(ClientContext &context) {
|
HttpServer *HttpServer::GetInstance(ClientContext &context) {
|
||||||
if (server_instance) {
|
if (server_instance) {
|
||||||
// We already have an instance, make sure we're running on the right DB
|
// We already have an instance, make sure we're running on the right DB
|
||||||
@@ -228,8 +298,7 @@ void HttpServer::HandleGetLocalToken(const httplib::Request &req,
|
|||||||
httplib::Response &res) {
|
httplib::Response &res) {
|
||||||
// GET requests don't include Origin, so use Referer instead.
|
// GET requests don't include Origin, so use Referer instead.
|
||||||
// Referer includes the path, so only compare the start.
|
// Referer includes the path, so only compare the start.
|
||||||
auto referer = req.get_header_value("Referer");
|
if (!IsRefererAllowed(req, local_url)) {
|
||||||
if (referer.compare(0, local_url.size(), local_url) != 0) {
|
|
||||||
res.status = 401;
|
res.status = 401;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -321,8 +390,7 @@ void HttpServer::HandleGet(const httplib::Request &req,
|
|||||||
|
|
||||||
void HttpServer::HandleInterrupt(const httplib::Request &req,
|
void HttpServer::HandleInterrupt(const httplib::Request &req,
|
||||||
httplib::Response &res) {
|
httplib::Response &res) {
|
||||||
auto origin = req.get_header_value("Origin");
|
if (!IsOriginAllowed(req, local_url)) {
|
||||||
if (origin != local_url) {
|
|
||||||
res.status = 401;
|
res.status = 401;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -361,8 +429,7 @@ void HttpServer::HandleRun(const httplib::Request &req, httplib::Response &res,
|
|||||||
void HttpServer::DoHandleRun(const httplib::Request &req,
|
void HttpServer::DoHandleRun(const httplib::Request &req,
|
||||||
httplib::Response &res,
|
httplib::Response &res,
|
||||||
const httplib::ContentReader &content_reader) {
|
const httplib::ContentReader &content_reader) {
|
||||||
auto origin = req.get_header_value("Origin");
|
if (!IsOriginAllowed(req, local_url)) {
|
||||||
if (origin != local_url) {
|
|
||||||
res.status = 401;
|
res.status = 401;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -625,8 +692,7 @@ void HttpServer::DoHandleRun(const httplib::Request &req,
|
|||||||
void HttpServer::HandleTokenize(const httplib::Request &req,
|
void HttpServer::HandleTokenize(const httplib::Request &req,
|
||||||
httplib::Response &res,
|
httplib::Response &res,
|
||||||
const httplib::ContentReader &content_reader) {
|
const httplib::ContentReader &content_reader) {
|
||||||
auto origin = req.get_header_value("Origin");
|
if (!IsOriginAllowed(req, local_url)) {
|
||||||
if (origin != local_url) {
|
|
||||||
res.status = 401;
|
res.status = 401;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user