Merge branch 'dev' of https://github.com/fosrl/pangolin into dev

This commit is contained in:
Milo Schwartz 2025-01-11 14:13:08 -05:00
commit 82192fa180
No known key found for this signature in database
12 changed files with 96 additions and 28 deletions

View file

@ -21,8 +21,9 @@ traefik:
gerbil: gerbil:
start_port: 51820 start_port: 51820
base_endpoint: localhost base_endpoint: localhost
block_size: 16 block_size: 24
subnet_group: 10.0.0.0/8 site_block_size: 30
subnet_group: 100.89.137.0/20
use_subdomain: true use_subdomain: true
rate_limits: rate_limits:

View file

@ -2,11 +2,13 @@
all: build all: build
build: build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o installer CGO_ENABLED=0 go build -o bin/installer
build-release: release:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64 CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64
clean: clean:
rm installer rm bin/installer
rm bin/installer_linux_amd64
rm bin/installer_linux_arm64

View file

@ -1,6 +1,6 @@
app: app:
dashboard_url: https://{{.Domain}} dashboard_url: https://{{.DashboardDomain}}
base_domain: {{.Domain}} base_domain: {{.BaseDomain}}
log_level: info log_level: info
save_logs: false save_logs: false
@ -23,8 +23,9 @@ gerbil:
start_port: 51820 start_port: 51820
base_endpoint: {{.Domain}} base_endpoint: {{.Domain}}
use_subdomain: false use_subdomain: false
block_size: 16 block_size: 24
subnet_group: 10.0.0.0/8 site_block_size: 30
subnet_group: 100.89.137.0/20
rate_limits: rate_limits:
global: global:

View file

@ -18,7 +18,8 @@ import (
var configFiles embed.FS var configFiles embed.FS
type Config struct { type Config struct {
Domain string `yaml:"domain"` BaseDomain string `yaml:"baseDomain"`
DashboardDomain string `yaml:"dashboardUrl"`
LetsEncryptEmail string `yaml:"letsEncryptEmail"` LetsEncryptEmail string `yaml:"letsEncryptEmail"`
AdminUserEmail string `yaml:"adminUserEmail"` AdminUserEmail string `yaml:"adminUserEmail"`
AdminUserPassword string `yaml:"adminUserPassword"` AdminUserPassword string `yaml:"adminUserPassword"`
@ -102,12 +103,13 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration // Basic configuration
fmt.Println("\n=== Basic Configuration ===") fmt.Println("\n=== Basic Configuration ===")
config.Domain = readString(reader, "Enter your domain name", "") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard (e.g. example.com OR proxy.example.com)", config.BaseDomain)
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
// Admin user configuration // Admin user configuration
fmt.Println("\n=== Admin User Configuration ===") fmt.Println("\n=== Admin User Configuration ===")
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.Domain) config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
for { for {
config.AdminUserPassword = readString(reader, "Enter admin user password", "") config.AdminUserPassword = readString(reader, "Enter admin user password", "")
if valid, message := validatePassword(config.AdminUserPassword); valid { if valid, message := validatePassword(config.AdminUserPassword); valid {
@ -140,10 +142,14 @@ func collectUserInput(reader *bufio.Reader) Config {
} }
// Validate required fields // Validate required fields
if config.Domain == "" { if config.BaseDomain == "" {
fmt.Println("Error: Domain name is required") fmt.Println("Error: Domain name is required")
os.Exit(1) os.Exit(1)
} }
if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required")
os.Exit(1)
}
if config.LetsEncryptEmail == "" { if config.LetsEncryptEmail == "" {
fmt.Println("Error: Let's Encrypt email is required") fmt.Println("Error: Let's Encrypt email is required")
os.Exit(1) os.Exit(1)

View file

@ -3,11 +3,7 @@ import yaml from "js-yaml";
import path from "path"; import path from "path";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
__DIRNAME,
configFilePath1,
configFilePath2
} from "@server/lib/consts";
import { loadAppVersion } from "@server/lib/loadAppVersion"; import { loadAppVersion } from "@server/lib/loadAppVersion";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
@ -15,9 +11,9 @@ const portSchema = z.number().positive().gt(0).lte(65535);
const hostnameSchema = z const hostnameSchema = z
.string() .string()
.regex( .regex(
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/ /^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(\.[a-zA-Z]{2,})*$/,
) "Invalid hostname. Must be a valid hostname like 'localhost' or 'test.example.com'."
.or(z.literal("localhost")); );
const environmentSchema = z.object({ const environmentSchema = z.object({
app: z.object({ app: z.object({
@ -49,7 +45,8 @@ const environmentSchema = z.object({
base_endpoint: z.string().transform((url) => url.toLowerCase()), base_endpoint: z.string().transform((url) => url.toLowerCase()),
use_subdomain: z.boolean(), use_subdomain: z.boolean(),
subnet_group: z.string(), subnet_group: z.string(),
block_size: z.number().positive().gt(0) block_size: z.number().positive().gt(0),
site_block_size: z.number().positive().gt(0)
}), }),
rate_limits: z.object({ rate_limits: z.object({
global: z.object({ global: z.object({

View file

@ -8,6 +8,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { findNextAvailableCidr } from "@server/lib/ip"; import { findNextAvailableCidr } from "@server/lib/ip";
import { generateId } from "@server/auth/sessions/app"; import { generateId } from "@server/auth/sessions/app";
import config from "@server/lib/config";
export type PickSiteDefaultsResponse = { export type PickSiteDefaultsResponse = {
exitNodeId: number; exitNodeId: number;
@ -51,9 +52,9 @@ export async function pickSiteDefaults(
// TODO: we need to lock this subnet for some time so someone else does not take it // TODO: we need to lock this subnet for some time so someone else does not take it
let subnets = sitesQuery.map((site) => site.subnet); let subnets = sitesQuery.map((site) => site.subnet);
// exclude the exit node address by replacing after the / with a /28 // exclude the exit node address by replacing after the / with a site block size
subnets.push(exitNode.address.replace(/\/\d+$/, "/29")); subnets.push(exitNode.address.replace(/\/\d+$/, `/${config.getRawConfig().gerbil.site_block_size}`));
const newSubnet = findNextAvailableCidr(subnets, 29, exitNode.address); const newSubnet = findNextAvailableCidr(subnets, config.getRawConfig().gerbil.site_block_size, exitNode.address);
if (!newSubnet) { if (!newSubnet) {
return next( return next(
createHttpError( createHttpError(

View file

@ -72,6 +72,16 @@ export async function acceptInvite(
const { user, session } = await verifySession(req); const { user, session } = await verifySession(req);
// at this point we know the user exists
if (!user) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"You must be logged in to accept an invite"
)
);
}
if (user && user.email !== existingInvite.email) { if (user && user.email !== existingInvite.email) {
return next( return next(
createHttpError( createHttpError(

View file

@ -8,6 +8,7 @@ import { __DIRNAME } from "@server/lib/consts";
import { loadAppVersion } from "@server/lib/loadAppVersion"; import { loadAppVersion } from "@server/lib/loadAppVersion";
import m1 from "./scripts/1.0.0-beta1"; import m1 from "./scripts/1.0.0-beta1";
import m2 from "./scripts/1.0.0-beta2"; import m2 from "./scripts/1.0.0-beta2";
import m3 from "./scripts/1.0.0-beta3";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA // EXCEPT FOR THE DATABASE AND THE SCHEMA
@ -15,7 +16,8 @@ import m2 from "./scripts/1.0.0-beta2";
// Define the migration list with versions and their corresponding functions // Define the migration list with versions and their corresponding functions
const migrations = [ const migrations = [
{ version: "1.0.0-beta.1", run: m1 }, { version: "1.0.0-beta.1", run: m1 },
{ version: "1.0.0-beta.2", run: m2 } { version: "1.0.0-beta.2", run: m2 },
{ version: "1.0.0-beta.3", run: m3 }
// Add new migrations here as they are created // Add new migrations here as they are created
] as const; ] as const;

View file

@ -0,0 +1,42 @@
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import fs from "fs";
import yaml from "js-yaml";
export default async function migration() {
console.log("Running setup script 1.0.0-beta.3...");
// Determine which config file exists
const filePaths = [configFilePath1, configFilePath2];
let filePath = "";
for (const path of filePaths) {
if (fs.existsSync(path)) {
filePath = path;
break;
}
}
if (!filePath) {
throw new Error(
`No config file found (expected config.yml or config.yaml).`
);
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
rawConfig = yaml.load(fileContents);
// Validate the structure
if (!rawConfig.gerbil) {
throw new Error(`Invalid config file: gerbil is missing.`);
}
// Update the config
rawConfig.gerbil.site_block_size = 29;
// Write the updated YAML back to the file
const updatedYaml = yaml.dump(rawConfig);
fs.writeFileSync(filePath, updatedYaml, "utf8");
console.log("Done.");
}

View file

@ -352,7 +352,7 @@ export default function ReverseProxyTargets(props: {
}, },
{ {
accessorKey: "ip", accessorKey: "ip",
header: "IP Address", header: "IP / Hostname",
cell: ({ row }) => ( cell: ({ row }) => (
<Input <Input
defaultValue={row.original.ip} defaultValue={row.original.ip}

View file

@ -14,7 +14,7 @@ import { XCircle } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
type InviteStatusCardProps = { type InviteStatusCardProps = {
type: "rejected" | "wrong_user" | "user_does_not_exist"; type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in";
token: string; token: string;
}; };

View file

@ -60,6 +60,8 @@ export default async function InvitePage(props: {
) )
) { ) {
return "user_does_not_exist"; return "user_does_not_exist";
} else if (error.includes("You must be logged in to accept an invite")) {
return "not_logged_in";
} else { } else {
return "rejected"; return "rejected";
} }
@ -71,6 +73,10 @@ export default async function InvitePage(props: {
redirect(`/auth/signup?redirect=/invite?token=${params.token}`); redirect(`/auth/signup?redirect=/invite?token=${params.token}`);
} }
if (!user && type === "not_logged_in") {
redirect(`/auth/login?redirect=/invite?token=${params.token}`);
}
return ( return (
<> <>
<InviteStatusCard type={type} token={tokenParam} /> <InviteStatusCard type={type} token={tokenParam} />