From 85605fe1666b7dded30c7b2e9551c3b4fc016a70 Mon Sep 17 00:00:00 2001 From: Willie Zutz Date: Thu, 8 May 2025 20:58:04 -0600 Subject: [PATCH] feat(opensearch): Add BASE_URL config to support reverse proxy deployments --- README.md | 31 ++++++++++++++++++++ sample.config.toml | 1 + src/app/api/config/route.ts | 2 ++ src/app/api/opensearch/route.ts | 51 ++++++++++++++++++++++++++++----- src/lib/config.ts | 3 ++ 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fc93244..94a3dc4 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,37 @@ For more details, check out the full documentation [here](https://github.com/Itz Perplexica runs on Next.js and handles all API requests. It works right away on the same network and stays accessible even with port forwarding. +### Running Behind a Reverse Proxy + +When running Perplexica behind a reverse proxy (like Nginx, Apache, or Traefik), follow these steps to ensure proper functionality: + +1. **Configure the BASE_URL setting**: + - In `config.toml`, set the `BASE_URL` parameter under the `[GENERAL]` section to your public-facing URL (e.g., `https://perplexica.yourdomain.com`) + +2. **Ensure proper headers forwarding**: + - Your reverse proxy should forward the following headers: + - `X-Forwarded-Host` + - `X-Forwarded-Proto` + - `X-Forwarded-Port` (if using non-standard ports) + +3. **Example Nginx configuration**: + ```nginx + server { + listen 80; + server_name perplexica.yourdomain.com; + + location / { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + } + } + ``` + +This ensures that OpenSearch descriptions, browser integrations, and all URLs work properly when accessing Perplexica through your reverse proxy. + ## One-Click Deployment [![Deploy to Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica) diff --git a/sample.config.toml b/sample.config.toml index 1db2125..03fa407 100644 --- a/sample.config.toml +++ b/sample.config.toml @@ -1,6 +1,7 @@ [GENERAL] SIMILARITY_MEASURE = "cosine" # "cosine" or "dot" KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m") +BASE_URL = "" # Optional. When set, overrides detected URL for OpenSearch and other public URLs [MODELS.OPENAI] API_KEY = "" diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts index c1e5bbd..e4384a8 100644 --- a/src/app/api/config/route.ts +++ b/src/app/api/config/route.ts @@ -1,5 +1,6 @@ import { getAnthropicApiKey, + getBaseUrl, getCustomOpenaiApiKey, getCustomOpenaiApiUrl, getCustomOpenaiModelName, @@ -60,6 +61,7 @@ export const GET = async (req: Request) => { config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl(); config['customOpenaiApiKey'] = getCustomOpenaiApiKey(); config['customOpenaiModelName'] = getCustomOpenaiModelName(); + config['baseUrl'] = getBaseUrl(); return Response.json({ ...config }, { status: 200 }); } catch (err) { diff --git a/src/app/api/opensearch/route.ts b/src/app/api/opensearch/route.ts index 438e15d..a5f4079 100644 --- a/src/app/api/opensearch/route.ts +++ b/src/app/api/opensearch/route.ts @@ -1,11 +1,10 @@ import { NextResponse } from 'next/server'; +import { getBaseUrl } from '@/lib/config'; -export async function GET(request: Request) { - // Get the host from the request - const url = new URL(request.url); - const origin = url.origin; - - // Create the OpenSearch XML with the correct origin +/** + * Creates an OpenSearch XML response with the given origin URL + */ +function generateOpenSearchResponse(origin: string): NextResponse { const opensearchXml = ` Perplexica @@ -17,10 +16,48 @@ export async function GET(request: Request) { `; - // Return the XML with the correct content type return new NextResponse(opensearchXml, { headers: { 'Content-Type': 'application/opensearchdescription+xml', }, }); } + +export async function GET(request: Request) { + // Check if a BASE_URL is explicitly configured + const configBaseUrl = getBaseUrl(); + + // If BASE_URL is configured, use it, otherwise detect from request + if (configBaseUrl) { + // Remove any trailing slashes for consistency + let origin = configBaseUrl.replace(/\/+$/, ''); + return generateOpenSearchResponse(origin); + } + + // Detect the correct origin, taking into account reverse proxy headers + const url = new URL(request.url); + let origin = url.origin; + + // Extract headers + const headers = Object.fromEntries(request.headers); + + // Check for X-Forwarded-Host and related headers to handle reverse proxies + if (headers['x-forwarded-host']) { + // Determine protocol: prefer X-Forwarded-Proto, fall back to original or https + const protocol = headers['x-forwarded-proto'] || url.protocol.replace(':', ''); + // Build the correct public-facing origin + origin = `${protocol}://${headers['x-forwarded-host']}`; + + // Handle non-standard ports if specified in X-Forwarded-Port + if (headers['x-forwarded-port']) { + const port = headers['x-forwarded-port']; + // Don't append standard ports (80 for HTTP, 443 for HTTPS) + if (!((protocol === 'http' && port === '80') || (protocol === 'https' && port === '443'))) { + origin = `${origin}:${port}`; + } + } + } + + // Generate and return the OpenSearch response + return generateOpenSearchResponse(origin); +} diff --git a/src/lib/config.ts b/src/lib/config.ts index 78ad09c..143ee28 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -15,6 +15,7 @@ interface Config { GENERAL: { SIMILARITY_MEASURE: string; KEEP_ALIVE: string; + BASE_URL?: string; }; MODELS: { OPENAI: { @@ -70,6 +71,8 @@ export const getSimilarityMeasure = () => export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE; +export const getBaseUrl = () => loadConfig().GENERAL.BASE_URL; + export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY; export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY;