Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,13 @@ VITE_PROXYSCOTCH_ACCESS_TOKEN=

# Set to `true` for subpath based access
ENABLE_SUBPATH_BASED_ACCESS=false

#-----------------------Container Runtime Config---------------------#

# Caddy's in-container HTTP port (default 80). Set a free port (e.g. 8000) to run
# under a non-root UID like OpenShift (ports < 1024 need root or CAP_NET_BIND_SERVICE),
# and update your published mapping too. Avoid internal ports 8080, 3200, 3000, 3100,
# 3170. Non-root runs expect GID 0. On the AIO image this applies only when
# ENABLE_SUBPATH_BASED_ACCESS=true; the legacy HOPP_AIO_ALTERNATE_PORT is honoured
# there as a fallback.
# HOPP_ALTERNATE_PORT=8000
2 changes: 1 addition & 1 deletion aio-subpath-access.Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
persist_config off
}

:{$HOPP_AIO_ALTERNATE_PORT:80} {
:{$HOPP_ALTERNATE_PORT:80} {
# Serve the `selfhost-web` SPA by default
root * /site/selfhost-web
file_server
Expand Down
85 changes: 76 additions & 9 deletions aio_run.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,70 @@
#!/usr/local/bin/node
// @ts-check

import { execSync, spawn } from "child_process"
import { execFileSync, spawn } from "child_process"
import fs from "fs"
import net from "net"
import os from "os"
import path from "path"
import process from "process"

// Probe the real bind so the kernel decides (CAP_NET_BIND_SERVICE, port sysctls),
// not a UID guess.
async function assertPortBindable(port) {
await new Promise((resolve) => {
const probe = net.createServer()
probe.once("error", (err) => {
if (err.code === "EACCES") {
console.error(`Cannot bind port ${port} as the current user: set HOPP_ALTERNATE_PORT to a free port >= 1024, or run as root / grant CAP_NET_BIND_SERVICE.`)
process.exit(1)
}
if (err.code === "EADDRINUSE") {
console.error(`Port ${port} is already in use inside the container: set HOPP_ALTERNATE_PORT to a free port.`)
process.exit(1)
}
console.warn(`Skipping bind preflight for port ${port} (${err.code})`)
resolve()
})
probe.listen(port, () => probe.close(resolve))
})
}

// Empty means unset (compose passes undefined vars as ""), so the :80 default applies.
if (process.env.HOPP_ALTERNATE_PORT === "") delete process.env.HOPP_ALTERNATE_PORT
if (process.env.HOPP_AIO_ALTERNATE_PORT === "") delete process.env.HOPP_AIO_ALTERNATE_PORT

// Back-compat: fall back to the legacy var when the new one is unset.
const legacyPortApplied = !process.env.HOPP_ALTERNATE_PORT && !!process.env.HOPP_AIO_ALTERNATE_PORT
if (legacyPortApplied) {
process.env.HOPP_ALTERNATE_PORT = process.env.HOPP_AIO_ALTERNATE_PORT
}

const useSubpathAccess = process.env.ENABLE_SUBPATH_BASED_ACCESS === "true"

// Sanity-check the value; real bindability is probed below.
const RESERVED_PORTS = ["8080", "3200"]
const altPort = process.env.HOPP_ALTERNATE_PORT
// Name whichever var the operator actually set.
const altPortVar = legacyPortApplied ? "HOPP_AIO_ALTERNATE_PORT" : "HOPP_ALTERNATE_PORT"
if (altPort !== undefined) {
if (!(/^[0-9]+$/.test(altPort) && +altPort >= 1 && +altPort <= 65535)) {
console.error(`${altPortVar}="${altPort}" is invalid: use an integer in 1-65535 (e.g. 8000).`)
process.exit(1)
}
if (RESERVED_PORTS.includes(String(+altPort))) {
console.error(`${altPortVar}="${altPort}" is already used by this image (${RESERVED_PORTS.join(", ")}); pick another port (e.g. 8000).`)
process.exit(1)
}
if (!useSubpathAccess) {
console.warn(`${altPortVar} has no effect in multiport mode (Caddy binds 3000/3100/3170); it applies only when ENABLE_SUBPATH_BASED_ACCESS=true.`)
}
}

// Only subpath mode binds the configurable port; multiport uses fixed ports.
if (useSubpathAccess) {
await assertPortBindable(+(process.env.HOPP_ALTERNATE_PORT ?? 80))
}

function runChildProcessWithPrefix(command, args, prefix) {
const childProcess = spawn(command, args);

Expand Down Expand Up @@ -44,30 +104,37 @@ const envFileContent = Object.entries(process.env)
}`)
.join("\n")

fs.writeFileSync("build.env", envFileContent)

execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
// Write to a temp dir (not cwd) so a non-root UID needn't own the working directory.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "hopp-env-"))
const buildEnvPath = path.join(tmpDir, "build.env")

fs.rmSync("build.env")
try {
fs.writeFileSync(buildEnvPath, envFileContent)
// Call the global binary directly (not npx, which needs a writable $HOME cache).
execFileSync("import-meta-env", ["-x", buildEnvPath, "-e", buildEnvPath, "-p", "/site/**/*"], { stdio: "inherit" })
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true })
}

const caddyFileName = process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true' ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile'
const caddyFileName = useSubpathAccess ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile'
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", `/etc/caddy/${caddyFileName}`, "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const backendProcess = runChildProcessWithPrefix("node", ["/dist/backend/dist/src/main.js"], "Backend Server")
const webappProcess = runChildProcessWithPrefix("webapp-server", [], "Webapp Server")

caddyProcess.on("exit", (code) => {
console.log(`Exiting process because Caddy Server exited with code ${code}`)
process.exit(code)
// code is null on signal death; report failure, not success.
process.exit(code ?? 1)
})

backendProcess.on("exit", (code) => {
console.log(`Exiting process because Backend Server exited with code ${code}`)
process.exit(code)
process.exit(code ?? 1)
})

webappProcess.on("exit", (code) => {
console.log(`Exiting process because Webapp Server exited with code ${code}`)
process.exit(code)
process.exit(code ?? 1)
})

process.on('SIGINT', () => {
Expand Down
10 changes: 5 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ services:
hoppscotch-db:
condition: service_healthy
ports:
- "3180:80"
- "3180:${HOPP_ALTERNATE_PORT:-80}"
- "3170:3170"

# The main hoppscotch app with integrated webapp server. This will be hosted at port 3000
Expand All @@ -70,7 +70,7 @@ services:
depends_on:
- hoppscotch-backend
ports:
- "3080:80"
- "3080:${HOPP_ALTERNATE_PORT:-80}"
- "3000:3000"
- "3200:3200"

Expand All @@ -89,7 +89,7 @@ services:
depends_on:
- hoppscotch-backend
ports:
- "3280:80"
- "3280:${HOPP_ALTERNATE_PORT:-80}"
- "3100:3100"

# The service that spins up all services at once in one container
Expand All @@ -111,7 +111,7 @@ services:
- "3100:3100"
- "3170:3170"
- "3200:3200"
- "3080:80"
- "3080:${HOPP_ALTERNATE_PORT:-80}"

# Profile with no database dependency (purely developmental)
hoppscotch-aio-no-db:
Expand All @@ -129,7 +129,7 @@ services:
- "3100:3100"
- "3170:3170"
- "3200:3200"
- "3080:80"
- "3080:${HOPP_ALTERNATE_PORT:-80}"

# The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance
Expand Down
2 changes: 1 addition & 1 deletion healthcheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ if [ "$UPTIME" -lt 15 ]; then
fi

if [ "$ENABLE_SUBPATH_BASED_ACCESS" = "true" ]; then
curlCheck "http://localhost:${HOPP_AIO_ALTERNATE_PORT:-80}/backend/ping" || exit 1
curlCheck "http://localhost:${HOPP_ALTERNATE_PORT:-${HOPP_AIO_ALTERNATE_PORT:-80}}/backend/ping" || exit 1
else
curlCheck "http://localhost:3000" || exit 1
curlCheck "http://localhost:3100" || exit 1
Expand Down
2 changes: 1 addition & 1 deletion packages/hoppscotch-agent/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hoppscotch-agent",
"private": true,
"version": "0.1.17",
"version": "0.1.18",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
5 changes: 3 additions & 2 deletions packages/hoppscotch-agent/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/hoppscotch-agent/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hoppscotch-agent"
version = "0.1.17"
version = "0.1.18"
description = "A cross-platform HTTP request agent for Hoppscotch for advanced request handling including custom headers, certificates, proxies, and local system integration."
authors = ["AndrewBastin", "CuriousCorrelation"]
edition = "2021"
Expand Down Expand Up @@ -32,7 +32,7 @@ rand = "0.8.5"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json", "fmt", "std", "time"] }
tracing-appender = "0.2.3"
relay = { git = "https://github.com/CuriousCorrelation/relay.git" }
relay = { git = "https://github.com/CuriousCorrelation/relay.git", rev = "1c4f1e16106ed48983926ea6ddb114c1dbb2f688" }
thiserror = "1.0.69"
tauri-plugin-store = "2.4.1"
x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
Expand Down
2 changes: 1 addition & 1 deletion packages/hoppscotch-agent/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "Hoppscotch Agent",
"version": "0.1.17",
"version": "0.1.18",
"identifier": "io.hoppscotch.agent",
"build": {
"beforeDevCommand": "pnpm dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/hoppscotch-backend/backend.Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
persist_config off
}

:80 :3170 {
:{$HOPP_ALTERNATE_PORT:80} :3170 {
@mock {
header_regexp host Host ^[^.]+\.mock\..*$
}
Expand Down
2 changes: 1 addition & 1 deletion packages/hoppscotch-backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2026.6.0",
"version": "2026.6.1",
"description": "",
"author": "",
"private": true,
Expand Down
47 changes: 45 additions & 2 deletions packages/hoppscotch-backend/prod_run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,50 @@
// @ts-check

import { spawn } from 'child_process';
import net from 'net';
import process from 'process';

// Probe the real bind so the kernel decides (CAP_NET_BIND_SERVICE, port sysctls),
// not a UID guess.
async function assertPortBindable(port) {
await new Promise((resolve) => {
const probe = net.createServer();
probe.once('error', (err) => {
if (err.code === 'EACCES') {
console.error(`Cannot bind port ${port} as the current user: set HOPP_ALTERNATE_PORT to a free port >= 1024, or run as root / grant CAP_NET_BIND_SERVICE.`);
process.exit(1);
}
if (err.code === 'EADDRINUSE') {
console.error(`Port ${port} is already in use inside the container: set HOPP_ALTERNATE_PORT to a free port.`);
process.exit(1);
}
console.warn(`Skipping bind preflight for port ${port} (${err.code})`);
resolve();
});
probe.listen(port, () => probe.close(resolve));
});
}

// Empty means unset (compose passes undefined vars as ""), so the :80 default applies.
if (process.env.HOPP_ALTERNATE_PORT === '') delete process.env.HOPP_ALTERNATE_PORT;

// Sanity-check the value; real bindability is probed below.
const RESERVED_PORTS = ['3170', '8080'];
const altPort = process.env.HOPP_ALTERNATE_PORT;
if (altPort !== undefined) {
if (!(/^[0-9]+$/.test(altPort) && +altPort >= 1 && +altPort <= 65535)) {
console.error(`HOPP_ALTERNATE_PORT="${altPort}" is invalid: use an integer in 1-65535 (e.g. 8000).`);
process.exit(1);
}
if (RESERVED_PORTS.includes(String(+altPort))) {
console.error(`HOPP_ALTERNATE_PORT="${altPort}" is already used by this image (${RESERVED_PORTS.join(', ')}); pick another port (e.g. 8000).`);
process.exit(1);
}
}

// Caddy always binds this port (env value or the :80 default).
await assertPortBindable(+(process.env.HOPP_ALTERNATE_PORT ?? 80));

function runChildProcessWithPrefix(command, args, prefix) {
const childProcess = spawn(command, args);

Expand Down Expand Up @@ -46,14 +88,15 @@ const backendProcess = runChildProcessWithPrefix(

caddyProcess.on('exit', (code) => {
console.log(`Exiting process because Caddy Server exited with code ${code}`);
process.exit(code);
// code is null on signal death; report failure, not success.
process.exit(code ?? 1);
});

backendProcess.on('exit', (code) => {
console.log(
`Exiting process because Backend Server exited with code ${code}`,
);
process.exit(code);
process.exit(code ?? 1);
});

process.on('SIGINT', () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,27 @@ import {
/**
* Extracts an access token from a cookie in the request.
*
* A blank cookie is treated as absent so `extractToken` uses the
* `Authorization` header. `O.fromNullable` alone keeps `''` as `Some('')`,
* and a whitespace-only value (`access_token= `) is non-empty by length,
* so both would be read in preference to a valid bearer token and reject
* the request with an unusable credential. A real JWT has no whitespace,
* so the trim-and-length check drops both.
*
* The value may not be a string: `cookie-parser` JSON-decodes `j:`-prefixed
* cookies into objects, where calling `.trim()` throws before auth fallback.
*
* @param request - Express Request object
* @returns Option<string> containing the token if found
*/
const extractFromCookie = (request: Request): O.Option<string> =>
pipe(
O.fromNullable(request.cookies),
O.chain((cookies) => O.fromNullable(cookies['access_token'])),
O.filter(
(token): token is string =>
typeof token === 'string' && token.trim().length > 0,
),
);

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/hoppscotch-common/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2026.6.0",
"version": "2026.6.1",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
Expand Down
Loading
Loading