From 59ecab5738f5041510dc9a377a076c67c7d5434d Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 15 Oct 2025 10:39:18 -0700 Subject: [PATCH] Dont ping remote nodes; handle certs better --- install/get-installer.sh | 180 ++++++++++++++++++ server/private/lib/exitNodes/exitNodes.ts | 70 +++---- .../private/lib/traefik/getTraefikConfig.ts | 77 +++++++- 3 files changed, 282 insertions(+), 45 deletions(-) create mode 100644 install/get-installer.sh diff --git a/install/get-installer.sh b/install/get-installer.sh new file mode 100644 index 00000000..d7f684ce --- /dev/null +++ b/install/get-installer.sh @@ -0,0 +1,180 @@ +#!/bin/bash + +# Get installer - Cross-platform installation script +# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/installer/refs/heads/main/get-installer.sh | bash + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# GitHub repository info +REPO="fosrl/pangolin" +GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest" + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to get latest version from GitHub API +get_latest_version() { + local latest_info + + if command -v curl >/dev/null 2>&1; then + latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null) + elif command -v wget >/dev/null 2>&1; then + latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null) + else + print_error "Neither curl nor wget is available. Please install one of them." >&2 + exit 1 + fi + + if [ -z "$latest_info" ]; then + print_error "Failed to fetch latest version information" >&2 + exit 1 + fi + + # Extract version from JSON response (works without jq) + local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + + if [ -z "$version" ]; then + print_error "Could not parse version from GitHub API response" >&2 + exit 1 + fi + + # Remove 'v' prefix if present + version=$(echo "$version" | sed 's/^v//') + + echo "$version" +} + +# Detect OS and architecture +detect_platform() { + local os arch + + # Detect OS - only support Linux + case "$(uname -s)" in + Linux*) os="linux" ;; + *) + print_error "Unsupported operating system: $(uname -s). Only Linux is supported." + exit 1 + ;; + esac + + # Detect architecture - only support amd64 and arm64 + case "$(uname -m)" in + x86_64|amd64) arch="amd64" ;; + arm64|aarch64) arch="arm64" ;; + *) + print_error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported on Linux." + exit 1 + ;; + esac + + echo "${os}_${arch}" +} + +# Get installation directory +get_install_dir() { + # Install to the current directory + local install_dir="$(pwd)" + if [ ! -d "$install_dir" ]; then + print_error "Installation directory does not exist: $install_dir" + exit 1 + fi + echo "$install_dir" +} + +# Download and install installer +install_installer() { + local platform="$1" + local install_dir="$2" + local binary_name="installer_${platform}" + + local download_url="${BASE_URL}/${binary_name}" + local temp_file="/tmp/installer" + local final_path="${install_dir}/installer" + + print_status "Downloading installer from ${download_url}" + + # Download the binary + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$download_url" -o "$temp_file" + elif command -v wget >/dev/null 2>&1; then + wget -q "$download_url" -O "$temp_file" + else + print_error "Neither curl nor wget is available. Please install one of them." + exit 1 + fi + + # Create install directory if it doesn't exist + mkdir -p "$install_dir" + + # Move binary to install directory + mv "$temp_file" "$final_path" + + # Make executable + chmod +x "$final_path" + + print_status "Installer downloaded to ${final_path}" +} + +# Verify installation +verify_installation() { + local install_dir="$1" + local installer_path="${install_dir}/installer" + + if [ -f "$installer_path" ] && [ -x "$installer_path" ]; then + print_status "Installation successful!" + return 0 + else + print_error "Installation failed. Binary not found or not executable." + return 1 + fi +} + +# Main installation process +main() { + print_status "Installing latest version of installer..." + + # Get latest version + print_status "Fetching latest version from GitHub..." + VERSION=$(get_latest_version) + print_status "Latest version: v${VERSION}" + + # Set base URL with the fetched version + BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" + + # Detect platform + PLATFORM=$(detect_platform) + print_status "Detected platform: ${PLATFORM}" + + # Get install directory + INSTALL_DIR=$(get_install_dir) + print_status "Install directory: ${INSTALL_DIR}" + + # Install installer + install_installer "$PLATFORM" "$INSTALL_DIR" + + # Verify installation + if verify_installation "$INSTALL_DIR"; then + print_status "Installer is ready to use!" + else + exit 1 + fi +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/server/private/lib/exitNodes/exitNodes.ts b/server/private/lib/exitNodes/exitNodes.ts index ea83fe9d..0dad7714 100644 --- a/server/private/lib/exitNodes/exitNodes.ts +++ b/server/private/lib/exitNodes/exitNodes.ts @@ -183,47 +183,47 @@ export async function listExitNodes(orgId: string, filterOnline = false, noCloud return []; } - // Enhanced online checking: consider node offline if either DB says offline OR HTTP ping fails - const nodesWithRealOnlineStatus = await Promise.all( - allExitNodes.map(async (node) => { - // If database says it's online, verify with HTTP ping - let online: boolean; - if (filterOnline && node.type == "remoteExitNode") { - try { - const isActuallyOnline = await checkExitNodeOnlineStatus( - node.endpoint - ); + // // Enhanced online checking: consider node offline if either DB says offline OR HTTP ping fails + // const nodesWithRealOnlineStatus = await Promise.all( + // allExitNodes.map(async (node) => { + // // If database says it's online, verify with HTTP ping + // let online: boolean; + // if (filterOnline && node.type == "remoteExitNode") { + // try { + // const isActuallyOnline = await checkExitNodeOnlineStatus( + // node.endpoint + // ); - // set the item in the database if it is offline - if (isActuallyOnline != node.online) { - await db - .update(exitNodes) - .set({ online: isActuallyOnline }) - .where(eq(exitNodes.exitNodeId, node.exitNodeId)); - } - online = isActuallyOnline; - } catch (error) { - logger.warn( - `Failed to check online status for exit node ${node.name} (${node.endpoint}): ${error instanceof Error ? error.message : "Unknown error"}` - ); - online = false; - } - } else { - online = node.online; - } + // // set the item in the database if it is offline + // if (isActuallyOnline != node.online) { + // await db + // .update(exitNodes) + // .set({ online: isActuallyOnline }) + // .where(eq(exitNodes.exitNodeId, node.exitNodeId)); + // } + // online = isActuallyOnline; + // } catch (error) { + // logger.warn( + // `Failed to check online status for exit node ${node.name} (${node.endpoint}): ${error instanceof Error ? error.message : "Unknown error"}` + // ); + // online = false; + // } + // } else { + // online = node.online; + // } - return { - ...node, - online - }; - }) - ); + // return { + // ...node, + // online + // }; + // }) + // ); - const remoteExitNodes = nodesWithRealOnlineStatus.filter( + const remoteExitNodes = allExitNodes.filter( (node) => node.type === "remoteExitNode" && (!filterOnline || node.online) ); - const gerbilExitNodes = nodesWithRealOnlineStatus.filter( + const gerbilExitNodes = allExitNodes.filter( (node) => node.type === "gerbil" && (!filterOnline || node.online) && !noCloud ); diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index f6d1c8ab..e09af0df 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -90,13 +90,24 @@ export async function getTraefikConfig( exitNodeId: sites.exitNodeId, // Namespace domainNamespaceId: domainNamespaces.domainNamespaceId, - // Certificate + // Certificate fields - we'll get all valid certs and filter in application logic + certificateId: certificates.certId, + certificateDomain: certificates.domain, + certificateWildcard: certificates.wildcard, certificateStatus: certificates.status }) .from(sites) .innerJoin(targets, eq(targets.siteId, sites.siteId)) .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) - .leftJoin(certificates, eq(certificates.domainId, resources.domainId)) + .leftJoin( + certificates, + and( + eq(certificates.domainId, resources.domainId), + eq(certificates.status, "valid"), + isNotNull(certificates.certFile), + isNotNull(certificates.keyFile) + ) + ) .leftJoin( targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId) @@ -127,6 +138,14 @@ export async function getTraefikConfig( // Group by resource and include targets with their unique site data const resourcesMap = new Map(); + + // Track certificates per resource to determine the correct certificate status + const resourceCertificates = new Map>(); resourcesWithTargetsAndSites.forEach((row) => { const resourceId = row.resourceId; @@ -151,7 +170,25 @@ export async function getTraefikConfig( .filter(Boolean) .join("-"); const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); - const key = sanitize(mapKey); + const key = sanitize(mapKey) || ""; + + // Track certificates for this resource + if (row.certificateId && row.certificateDomain && row.certificateStatus) { + if (!resourceCertificates.has(key)) { + resourceCertificates.set(key, []); + } + + const certList = resourceCertificates.get(key)!; + // Only add if not already present (avoid duplicates from multiple targets) + if (!certList.some(cert => cert.id === row.certificateId)) { + certList.push({ + id: row.certificateId, + domain: row.certificateDomain, + wildcard: row.certificateWildcard, + status: row.certificateStatus + }); + } + } if (!resourcesMap.has(key)) { const validation = validatePathRewriteConfig( @@ -168,6 +205,26 @@ export async function getTraefikConfig( return; } + // Determine the correct certificate status for this resource + let certificateStatus: string | null = null; + const resourceCerts = resourceCertificates.get(key) || []; + + if (row.fullDomain && resourceCerts.length > 0) { + // Find the best matching certificate + // Priority: exact domain match > wildcard match + const exactMatch = resourceCerts.find(cert => + cert.domain === row.fullDomain + ); + + const wildcardMatch = resourceCerts.find(cert => + cert.wildcard && cert.domain && + row.fullDomain!.endsWith(`.${cert.domain}`) + ); + + const matchingCert = exactMatch || wildcardMatch; + certificateStatus = matchingCert?.status || null; + } + resourcesMap.set(key, { resourceId: row.resourceId, name: resourceName, @@ -183,7 +240,7 @@ export async function getTraefikConfig( tlsServerName: row.tlsServerName, setHostHeader: row.setHostHeader, enableProxy: row.enableProxy, - certificateStatus: row.certificateStatus, + certificateStatus: certificateStatus, targets: [], headers: row.headers, path: row.path, // the targets will all have the same path @@ -256,12 +313,12 @@ export async function getTraefikConfig( } // TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail - // if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { - // logger.debug( - // `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}` - // ); - // continue; - // } + if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { + logger.debug( + `Resource ${resource.resourceId} has certificate status ${resource.certificateStatus}` + ); + continue; + } // add routers and services empty objects if they don't exist if (!config_output.http.routers) {