mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-22 15:22:12 +00:00
Compare commits
11 Commits
dependabot
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a76c680914 | ||
|
|
4fd3d6078e | ||
|
|
bc28290d8e | ||
|
|
241610579c | ||
|
|
7c15c428b3 | ||
|
|
f3a52e31d1 | ||
|
|
5e26ceaf02 | ||
|
|
d6fe357fcb | ||
|
|
f9cc52ece9 | ||
|
|
522ca671b5 | ||
|
|
d94af2b8ea |
8
.github/workflows/cicd.yml
vendored
8
.github/workflows/cicd.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
|
||||
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build Docker image sqlite
|
||||
run: make dev-build-sqlite
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build Docker image pg
|
||||
run: make dev-build-pg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||
import Database from "better-sqlite3";
|
||||
import type BetterSqlite3 from "better-sqlite3";
|
||||
import * as schema from "./schema/schema";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
@@ -12,68 +11,31 @@ export const exists = checkFileExists(location);
|
||||
|
||||
bootstrapVolume();
|
||||
|
||||
/**
|
||||
* Wraps better-sqlite3 Statement to call `finalize()` immediately after
|
||||
* execution, freeing native sqlite3_stmt memory deterministically instead
|
||||
* of waiting for GC. Fixes steady off-heap growth under load (#2120).
|
||||
* WARNING: Finalizes after first execution — incompatible with drizzle's
|
||||
* reusable .prepare() builders. No such usage exists in this codebase.
|
||||
*/
|
||||
function autoFinalizeStatement(
|
||||
stmt: BetterSqlite3.Statement
|
||||
): BetterSqlite3.Statement {
|
||||
const wrapExec = <T extends (...args: any[]) => any>(fn: T): T => {
|
||||
return function (this: any, ...args: any[]) {
|
||||
try {
|
||||
return fn.apply(this, args);
|
||||
} finally {
|
||||
try {
|
||||
// finalize() exists on the native Statement at runtime but
|
||||
// is missing from @types/better-sqlite3.
|
||||
(stmt as any).finalize();
|
||||
} catch {
|
||||
// Already finalized — harmless
|
||||
}
|
||||
}
|
||||
} as unknown as T;
|
||||
};
|
||||
|
||||
stmt.run = wrapExec(stmt.run);
|
||||
stmt.get = wrapExec(stmt.get);
|
||||
stmt.all = wrapExec(stmt.all);
|
||||
|
||||
return stmt;
|
||||
}
|
||||
|
||||
function createDb() {
|
||||
const sqlite = new Database(location);
|
||||
|
||||
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") {
|
||||
// Enable WAL mode — allows concurrent readers + single writer, preventing
|
||||
// contention across subsystems (verifySession, Traefik, audit, ping).
|
||||
// NOTE: journal_mode persists in the DB file once set; unsetting this
|
||||
// env var does NOT revert an existing WAL database.
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
// NORMAL sync mode: safe with WAL, reduces write lock hold time.
|
||||
sqlite.pragma("synchronous = NORMAL");
|
||||
}
|
||||
|
||||
// Wait up to 5s on SQLITE_BUSY instead of failing — prevents audit log
|
||||
// retry loops that accumulate memory.
|
||||
sqlite.pragma("busy_timeout = 5000");
|
||||
// No busy_timeout pragma: better-sqlite3 already arms
|
||||
// sqlite3_busy_timeout(db, 5000) via its default `timeout` option
|
||||
// (lib/database.js), so an explicit pragma is redundant.
|
||||
|
||||
// 64 MB page cache (default 2 MB) — reduces I/O round-trips on large
|
||||
// TraefikConfigManager JOINs that block the event loop.
|
||||
sqlite.pragma("cache_size = -65536");
|
||||
// Intentionally NOT setting cache_size or mmap_size: a large page cache plus
|
||||
// a multi-hundred-MB mmap region inflate RSS and cause page-cache thrashing
|
||||
// on small (~1 GB) instances. Leave SQLite on its conservative defaults.
|
||||
|
||||
// 256 MB memory-mapped I/O — OS serves reads from page cache directly,
|
||||
// reducing event-loop blocking.
|
||||
sqlite.pragma("mmap_size = 268435456");
|
||||
|
||||
// Wrap prepare() so every drizzle-orm statement is auto-finalized after
|
||||
// first use, preventing sqlite3_stmt accumulation between GC cycles.
|
||||
const originalPrepare = sqlite.prepare.bind(sqlite);
|
||||
(sqlite as any).prepare = function autoFinalizePrepare(source: string) {
|
||||
return autoFinalizeStatement(originalPrepare(source));
|
||||
};
|
||||
// Intentionally NOT wrapping prepare()/statements: better-sqlite3 finalizes
|
||||
// sqlite3_stmt in the Statement destructor at GC, and drizzle-orm prepares a
|
||||
// fresh statement per query (no statement cache), so statements cannot
|
||||
// accumulate. better-sqlite3 11.x exposes no Statement.finalize() at all.
|
||||
|
||||
return DrizzleSqlite(sqlite, {
|
||||
schema
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { FeatureId, getFeatureMeterId } from "./features";
|
||||
import logger from "@server/logger";
|
||||
import { build } from "@server/build";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
|
||||
export function noop() {
|
||||
if (build !== "saas") {
|
||||
@@ -22,7 +22,6 @@ export function noop() {
|
||||
}
|
||||
|
||||
export class UsageService {
|
||||
|
||||
constructor() {
|
||||
if (noop()) {
|
||||
return;
|
||||
@@ -57,7 +56,10 @@ export class UsageService {
|
||||
try {
|
||||
let usage;
|
||||
if (transaction) {
|
||||
const orgIdToUse = await this.getBillingOrg(orgId, transaction);
|
||||
const orgIdToUse = await this.getBillingOrg(
|
||||
orgId,
|
||||
transaction
|
||||
);
|
||||
usage = await this.internalAddUsage(
|
||||
orgIdToUse,
|
||||
featureId,
|
||||
|
||||
@@ -48,18 +48,18 @@ export async function applyBlueprint({
|
||||
name,
|
||||
source = "API"
|
||||
}: ApplyBlueprintArgs): Promise<Blueprint> {
|
||||
// Validate the input data
|
||||
const validationResult = ConfigSchema.safeParse(configData);
|
||||
if (!validationResult.success) {
|
||||
throw new Error(fromError(validationResult.error).toString());
|
||||
}
|
||||
|
||||
const config: Config = validationResult.data;
|
||||
let blueprintSucceeded: boolean = false;
|
||||
let blueprintMessage: string;
|
||||
let blueprintMessage = "";
|
||||
let error: any | null = null;
|
||||
|
||||
try {
|
||||
const validationResult = ConfigSchema.safeParse(configData);
|
||||
if (!validationResult.success) {
|
||||
throw new Error(fromError(validationResult.error).toString());
|
||||
}
|
||||
|
||||
const config: Config = validationResult.data;
|
||||
|
||||
let proxyResourcesResults: PublicResourcesResults = [];
|
||||
let clientResourcesResults: ClientResourcesResults = [];
|
||||
await db.transaction(async (trx) => {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { isValidUrlGlobPattern } from "./validators";
|
||||
import {
|
||||
getResourceRuleValueValidationError,
|
||||
isValidUrlGlobPattern
|
||||
} from "./validators";
|
||||
import { assertEquals } from "@test/assert";
|
||||
|
||||
function runTests() {
|
||||
@@ -236,6 +239,43 @@ function runTests() {
|
||||
"Path with isolated percent sign should be invalid"
|
||||
);
|
||||
|
||||
// ASN validation tests
|
||||
assertEquals(
|
||||
getResourceRuleValueValidationError("ASN", "AS15169"),
|
||||
null,
|
||||
"Standard ASN should be valid"
|
||||
);
|
||||
assertEquals(
|
||||
getResourceRuleValueValidationError("ASN", " As15169 "),
|
||||
null,
|
||||
"Standard ASN should be valid with mixed case and whitespace"
|
||||
);
|
||||
assertEquals(
|
||||
getResourceRuleValueValidationError("ASN", "ALL"),
|
||||
null,
|
||||
"ALL ASN selector should be valid"
|
||||
);
|
||||
assertEquals(
|
||||
getResourceRuleValueValidationError("ASN", " all "),
|
||||
null,
|
||||
"ALL ASN selector should be valid with mixed case and whitespace"
|
||||
);
|
||||
assertEquals(
|
||||
getResourceRuleValueValidationError("ASN", "AS0"),
|
||||
null,
|
||||
"AS0 alias should be valid"
|
||||
);
|
||||
assertEquals(
|
||||
getResourceRuleValueValidationError("ASN", " as0 "),
|
||||
null,
|
||||
"AS0 alias should be valid with mixed case and whitespace"
|
||||
);
|
||||
assertEquals(
|
||||
getResourceRuleValueValidationError("ASN", "not-an-asn"),
|
||||
"Invalid ASN provided",
|
||||
"Invalid ASN should return an error"
|
||||
);
|
||||
|
||||
console.log("All tests passed!");
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,10 @@ export function getResourceRuleValueValidationError(
|
||||
? null
|
||||
: "Invalid country code provided";
|
||||
case "ASN":
|
||||
return /^AS\d+$/i.test(value.trim())
|
||||
const normalizedValue = value.trim().toUpperCase();
|
||||
return /^AS\d+$/.test(normalizedValue) ||
|
||||
normalizedValue === "ALL" ||
|
||||
normalizedValue === "AS0"
|
||||
? null
|
||||
: "Invalid ASN provided";
|
||||
default:
|
||||
|
||||
@@ -17,7 +17,7 @@ import { certificates, db } from "@server/db";
|
||||
import { and, eq, isNotNull, or, inArray, sql } from "drizzle-orm";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import logger from "@server/logger";
|
||||
import cache from "#private/lib/cache";
|
||||
import { regionalCache as cache } from "#private/lib/cache";
|
||||
import { build } from "@server/build";
|
||||
|
||||
// Define the return type for clarity and type safety
|
||||
|
||||
@@ -22,7 +22,7 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||
import cache from "#private/lib/cache";
|
||||
import { regionalCache as cache } from "#private/lib/cache";
|
||||
import semver from "semver";
|
||||
|
||||
let stalePangolinNodeVersion: string | null = null;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { verifyPassword } from "@server/auth/password";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
// Stale-while-revalidate in-memory fallback for the releases API.
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MessageHandler } from "@server/routers/ws";
|
||||
import logger from "@server/logger";
|
||||
import { Newt } from "@server/db";
|
||||
import { applyNewtDockerBlueprint } from "@server/lib/blueprints/applyNewtDockerBlueprint";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import cache from "#dynamic/lib/cache"; // not using regional here because we dont know where the site is
|
||||
|
||||
export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
||||
const { message, client, sendToClient } = context;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { handleFingerprintInsertion } from "./fingerprintingUtils";
|
||||
import { build } from "@server/build";
|
||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import config from "@server/lib/config";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import cache from "#dynamic/lib/cache"; // not using regional here because we need this in the register message handler before we know where the client is
|
||||
|
||||
const HOLEPUNCH_STALE_CHAIN_THRESHOLD = 18;
|
||||
const HOLEPUNCH_STALE_CHAIN_TTL_SECONDS = 1800;
|
||||
|
||||
@@ -15,8 +15,7 @@ import logger from "@server/logger";
|
||||
import { z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { localCache } from "#dynamic/lib/cache";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
|
||||
const USER_RESOURCE_ALIASES_CACHE_TTL_SEC = 60;
|
||||
|
||||
@@ -153,7 +152,7 @@ export async function listUserResourceAliases(
|
||||
pageSize
|
||||
);
|
||||
const cachedData: ListUserResourceAliasesResponse | undefined =
|
||||
localCache.get(cacheKey);
|
||||
await cache.get(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
@@ -211,7 +210,11 @@ export async function listUserResourceAliases(
|
||||
page
|
||||
}
|
||||
};
|
||||
localCache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC);
|
||||
await cache.set(
|
||||
cacheKey,
|
||||
data,
|
||||
USER_RESOURCE_ALIASES_CACHE_TTL_SEC
|
||||
);
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
data,
|
||||
success: true,
|
||||
@@ -256,7 +259,7 @@ export async function listUserResourceAliases(
|
||||
page
|
||||
}
|
||||
};
|
||||
localCache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC);
|
||||
await cache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC);
|
||||
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
data,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
siteLabels,
|
||||
type Label
|
||||
} from "@server/db";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
@@ -139,7 +139,6 @@ Restart=always
|
||||
RestartSec=2
|
||||
UMask=0077
|
||||
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -83,9 +83,19 @@ export function createPolicyRuleValueSchema(t: TranslateFn, match: string) {
|
||||
{ message: t("rulesErrorInvalidCountryDescription") }
|
||||
);
|
||||
case "ASN":
|
||||
return required.refine((value) => /^AS\d+$/i.test(value.trim()), {
|
||||
message: t("rulesErrorInvalidAsnDescription")
|
||||
});
|
||||
return required.refine(
|
||||
(value) => {
|
||||
const normalizedValue = value.trim().toUpperCase();
|
||||
return (
|
||||
/^AS\d+$/.test(normalizedValue) ||
|
||||
normalizedValue === "ALL" ||
|
||||
normalizedValue === "AS0"
|
||||
);
|
||||
},
|
||||
{
|
||||
message: t("rulesErrorInvalidAsnDescription")
|
||||
}
|
||||
);
|
||||
default:
|
||||
return required;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user