mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-17 16:01:22 +02:00
Merge branch 'dev' of https://github.com/fosrl/pangolin into dev
This commit is contained in:
commit
82192fa180
12 changed files with 96 additions and 28 deletions
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
42
server/setup/scripts/1.0.0-beta3.ts
Normal file
42
server/setup/scripts/1.0.0-beta3.ts
Normal 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.");
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue