From 9a167b5acba29832d9d5558bde2f22fa336e1c98 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 2 May 2025 14:16:10 -0400 Subject: [PATCH 1/9] Dont overwrite the secret and crowdsec vars --- install/main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/install/main.go b/install/main.go index abb67ac..a0d74a4 100644 --- a/install/main.go +++ b/install/main.go @@ -64,15 +64,15 @@ func main() { } var config Config - config.DoCrowdsecInstall = false - config.Secret = generateRandomSecretKey() - + // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { config = collectUserInput(reader) - + loadVersions(&config) - + config.DoCrowdsecInstall = false + config.Secret = generateRandomSecretKey() + if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) os.Exit(1) From 198810121ca3913d02b7ed46d02d293ce4f12e4b Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Fri, 2 May 2025 12:25:49 -0400 Subject: [PATCH 2/9] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 5eb1909..15ca7ad 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,6 @@ You can use Pangolin as an easy way to expose your business applications to your **Use Case Example - IoT Networks**: IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups. -_Resources page of Pangolin dashboard (dark mode) showing HTTPS and TCP resources with access control rules._ - ## Similar Projects and Inspirations **Cloudflare Tunnels**: From f25990a9a7579bfca92103d18030fc8e8e58186b Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 9 May 2025 16:46:51 -0400 Subject: [PATCH 3/9] add id token and claims to debug logs --- server/routers/idp/validateOidcCallback.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 7d588fe..4fb52be 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -160,7 +160,9 @@ export async function validateOidcCallback( ); const idToken = tokens.idToken(); + logger.debug("ID token", { idToken }); const claims = arctic.decodeIdToken(idToken); + logger.debug("ID token claims", { claims }); const userIdentifier = jmespath.search( claims, From f66fb7d4a3905fcc72393e2b04d8a8c14b6aa3d0 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 9 May 2025 17:09:22 -0400 Subject: [PATCH 4/9] fix justification for profile icon --- src/app/auth/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 9a149f7..7edc12d 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -21,7 +21,7 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
{user && ( -
+
From 4ed98c227b12478449ab0ee39a5e376ac5b89891 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 9 May 2025 17:12:01 -0400 Subject: [PATCH 5/9] fix setting tlsServerName and hostHeader conflict --- server/routers/resource/updateResource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index a857e10..9198bb8 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -318,8 +318,8 @@ async function updateHttpResource( domainId: updatePayload.domainId, enabled: updatePayload.enabled, stickySession: updatePayload.stickySession, - tlsServerName: updatePayload.tlsServerName || null, - setHostHeader: updatePayload.setHostHeader || null, + tlsServerName: updatePayload.tlsServerName, + setHostHeader: updatePayload.setHostHeader, fullDomain: updatePayload.fullDomain }) .where(eq(resources.resourceId, resource.resourceId)) From e9cc48a3aeeb3256adcd6973679b323c70e6b716 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 9 May 2025 17:18:42 -0400 Subject: [PATCH 6/9] fix bug causing duplicate targets --- src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx | 2 ++ src/components/Settings.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index 90e05ff..deefd80 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -320,8 +320,10 @@ export default function ReverseProxyTargets(props: { AxiosResponse >(`/resource/${params.resourceId}/target`, data); target.targetId = res.data.data.targetId; + target.new = false; } else if (target.updated) { await api.post(`/target/${target.targetId}`, data); + target.updated = false; } } diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 7fa689f..410d309 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -55,7 +55,7 @@ export function SettingsSectionFooter({ }: { children: React.ReactNode; }) { - return
{children}
; + return
{children}
; } export function SettingsSectionGrid({ From caded23b5152ce9c3e178d3e18a58a18dbced5f6 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 9 May 2025 17:37:55 -0400 Subject: [PATCH 7/9] allow root path --- server/lib/validators.ts | 4 + server/routers/badger/verifySession.test.ts | 97 ++++++++++++++++--- .../resources/[resourceId]/proxy/page.tsx | 6 ++ .../resources/[resourceId]/rules/page.tsx | 1 - 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/server/lib/validators.ts b/server/lib/validators.ts index e33c918..50ff567 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -9,6 +9,10 @@ export function isValidIP(ip: string): boolean { } export function isValidUrlGlobPattern(pattern: string): boolean { + if (pattern === "/") { + return true; + } + // Remove leading slash if present pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern; diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts index 0a459dc..b0ad987 100644 --- a/server/routers/badger/verifySession.test.ts +++ b/server/routers/badger/verifySession.test.ts @@ -1,61 +1,136 @@ -import { isPathAllowed } from './verifySession'; import { assertEquals } from '@test/assert'; +function isPathAllowed(pattern: string, path: string): boolean { + + // Normalize and split paths into segments + const normalize = (p: string) => p.split("/").filter(Boolean); + const patternParts = normalize(pattern); + const pathParts = normalize(path); + + + // Recursive function to try different wildcard matches + function matchSegments(patternIndex: number, pathIndex: number): boolean { + const indent = " ".repeat(pathIndex); // Indent based on recursion depth + const currentPatternPart = patternParts[patternIndex]; + const currentPathPart = pathParts[pathIndex]; + + // If we've consumed all pattern parts, we should have consumed all path parts + if (patternIndex >= patternParts.length) { + const result = pathIndex >= pathParts.length; + return result; + } + + // If we've consumed all path parts but still have pattern parts + if (pathIndex >= pathParts.length) { + // The only way this can match is if all remaining pattern parts are wildcards + const remainingPattern = patternParts.slice(patternIndex); + const result = remainingPattern.every((p) => p === "*"); + return result; + } + + // For full segment wildcards, try consuming different numbers of path segments + if (currentPatternPart === "*") { + + // Try consuming 0 segments (skip the wildcard) + if (matchSegments(patternIndex + 1, pathIndex)) { + return true; + } + + // Try consuming current segment and recursively try rest + if (matchSegments(patternIndex, pathIndex + 1)) { + return true; + } + + return false; + } + + // Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix") + if (currentPatternPart.includes("*")) { + // Convert the pattern segment to a regex pattern + const regexPattern = currentPatternPart + .replace(/\*/g, ".*") // Replace * with .* for regex wildcard + .replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed + + const regex = new RegExp(`^${regexPattern}$`); + + if (regex.test(currentPathPart)) { + return matchSegments(patternIndex + 1, pathIndex + 1); + } + + return false; + } + + // For regular segments, they must match exactly + if (currentPatternPart !== currentPathPart) { + return false; + } + + // Move to next segments in both pattern and path + return matchSegments(patternIndex + 1, pathIndex + 1); + } + + const result = matchSegments(0, 0); + return result; +} + function runTests() { console.log('Running path matching tests...'); - + // Test exact matching assertEquals(isPathAllowed('foo', 'foo'), true, 'Exact match should be allowed'); assertEquals(isPathAllowed('foo', 'bar'), false, 'Different segments should not match'); assertEquals(isPathAllowed('foo/bar', 'foo/bar'), true, 'Exact multi-segment match should be allowed'); assertEquals(isPathAllowed('foo/bar', 'foo/baz'), false, 'Partial multi-segment match should not be allowed'); - + // Test with leading and trailing slashes assertEquals(isPathAllowed('/foo', 'foo'), true, 'Pattern with leading slash should match'); assertEquals(isPathAllowed('foo/', 'foo'), true, 'Pattern with trailing slash should match'); assertEquals(isPathAllowed('/foo/', 'foo'), true, 'Pattern with both leading and trailing slashes should match'); assertEquals(isPathAllowed('foo', '/foo/'), true, 'Path with leading and trailing slashes should match'); - + // Test simple wildcard matching assertEquals(isPathAllowed('*', 'foo'), true, 'Single wildcard should match any single segment'); assertEquals(isPathAllowed('*', 'foo/bar'), true, 'Single wildcard should match multiple segments'); assertEquals(isPathAllowed('*/bar', 'foo/bar'), true, 'Wildcard prefix should match'); assertEquals(isPathAllowed('foo/*', 'foo/bar'), true, 'Wildcard suffix should match'); assertEquals(isPathAllowed('foo/*/baz', 'foo/bar/baz'), true, 'Wildcard in middle should match'); - + // Test multiple wildcards assertEquals(isPathAllowed('*/*', 'foo/bar'), true, 'Multiple wildcards should match corresponding segments'); assertEquals(isPathAllowed('*/*/*', 'foo/bar/baz'), true, 'Three wildcards should match three segments'); assertEquals(isPathAllowed('foo/*/*', 'foo/bar/baz'), true, 'Specific prefix with wildcards should match'); assertEquals(isPathAllowed('*/*/baz', 'foo/bar/baz'), true, 'Wildcards with specific suffix should match'); - + // Test wildcard consumption behavior assertEquals(isPathAllowed('*', ''), true, 'Wildcard should optionally consume segments'); assertEquals(isPathAllowed('foo/*', 'foo'), true, 'Trailing wildcard should be optional'); assertEquals(isPathAllowed('*/*', 'foo'), true, 'Multiple wildcards can match fewer segments'); assertEquals(isPathAllowed('*/*/*', 'foo/bar'), true, 'Extra wildcards can be skipped'); - + // Test complex nested paths assertEquals(isPathAllowed('api/*/users', 'api/v1/users'), true, 'API versioning pattern should match'); assertEquals(isPathAllowed('api/*/users/*', 'api/v1/users/123'), true, 'API resource pattern should match'); assertEquals(isPathAllowed('api/*/users/*/profile', 'api/v1/users/123/profile'), true, 'Nested API pattern should match'); - + // Test for the requested padbootstrap* pattern assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap'), true, 'padbootstrap* should match padbootstrap'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrapv1'), true, 'padbootstrap* should match padbootstrapv1'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap/files'), false, 'padbootstrap* should not match padbootstrap/files'); assertEquals(isPathAllowed('padbootstrap*/*', 'padbootstrap/files'), true, 'padbootstrap*/* should match padbootstrap/files'); assertEquals(isPathAllowed('padbootstrap*/files', 'padbootstrapv1/files'), true, 'padbootstrap*/files should not match padbootstrapv1/files (wildcard is segment-based, not partial)'); - + // Test wildcard edge cases assertEquals(isPathAllowed('*/*/*/*/*/*', 'a/b'), true, 'Many wildcards can match few segments'); assertEquals(isPathAllowed('a/*/b/*/c', 'a/anything/b/something/c'), true, 'Multiple wildcards in pattern should match corresponding segments'); - + // Test patterns with partial segment matches assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap-123'), true, 'Wildcards in isPathAllowed should be segment-based, not character-based'); assertEquals(isPathAllowed('test*', 'testuser'), true, 'Asterisk as part of segment name is treated as a literal, not a wildcard'); assertEquals(isPathAllowed('my*app', 'myapp'), true, 'Asterisk in middle of segment name is treated as a literal, not a wildcard'); + assertEquals(isPathAllowed('/', '/'), true, 'Root path should match root path'); + assertEquals(isPathAllowed('/', '/test'), false, 'Root path should not match non-root path'); + console.log('All tests passed!'); } @@ -64,4 +139,4 @@ try { runTests(); } catch (error) { console.error('Test failed:', error); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index deefd80..ddf255e 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -798,6 +798,12 @@ export default function ReverseProxyTargets(props: { type="submit" variant="outlinePrimary" className="mt-6" + disabled={ + !( + addTargetForm.getValues("ip") && + addTargetForm.getValues("port") + ) + } > Add Target diff --git a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx index 2a9fa00..f7b3914 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx @@ -64,7 +64,6 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; -import { Separator } from "@app/components/ui/separator"; import { InfoPopup } from "@app/components/ui/info-popup"; import { isValidCIDR, From 492669f68a85b9169258e0672cbf270dc8685c01 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 9 May 2025 18:32:14 -0400 Subject: [PATCH 8/9] set default congig values --- server/lib/config.ts | 142 +++++++++++++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 45 deletions(-) diff --git a/server/lib/config.ts b/server/lib/config.ts index a19b4a2..935522e 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -29,9 +29,12 @@ const configSchema = z.object({ .optional() .pipe(z.string().url()) .transform((url) => url.toLowerCase()), - log_level: z.enum(["debug", "info", "warn", "error"]), - save_logs: z.boolean(), - log_failed_attempts: z.boolean().optional() + log_level: z + .enum(["debug", "info", "warn", "error"]) + .optional() + .default("info"), + save_logs: z.boolean().optional().default(false), + log_failed_attempts: z.boolean().optional().default(false) }), domains: z .record( @@ -41,8 +44,8 @@ const configSchema = z.object({ .string() .nonempty("base_domain must not be empty") .transform((url) => url.toLowerCase()), - cert_resolver: z.string().optional(), - prefer_wildcard_cert: z.boolean().optional() + cert_resolver: z.string().optional().default("letsencrypt"), + prefer_wildcard_cert: z.boolean().optional().default(false) }) ) .refine( @@ -62,19 +65,42 @@ const configSchema = z.object({ server: z.object({ integration_port: portSchema .optional() + .default(3003) .transform(stoi) .pipe(portSchema.optional()), - external_port: portSchema.optional().transform(stoi).pipe(portSchema), - internal_port: portSchema.optional().transform(stoi).pipe(portSchema), - next_port: portSchema.optional().transform(stoi).pipe(portSchema), - internal_hostname: z.string().transform((url) => url.toLowerCase()), - session_cookie_name: z.string(), - resource_access_token_param: z.string(), - resource_access_token_headers: z.object({ - id: z.string(), - token: z.string() - }), - resource_session_request_param: z.string(), + external_port: portSchema + .optional() + .default(3000) + .transform(stoi) + .pipe(portSchema), + internal_port: portSchema + .optional() + .default(3001) + .transform(stoi) + .pipe(portSchema), + next_port: portSchema + .optional() + .default(3002) + .transform(stoi) + .pipe(portSchema), + internal_hostname: z + .string() + .optional() + .default("pangolin") + .transform((url) => url.toLowerCase()), + session_cookie_name: z.string().optional().default("p_session_token"), + resource_access_token_param: z.string().optional().default("p_token"), + resource_access_token_headers: z + .object({ + id: z.string().optional().default("P-Access-Token-Id"), + token: z.string().optional().default("P-Access-Token") + }) + .optional() + .default({}), + resource_session_request_param: z + .string() + .optional() + .default("resource_session_request_param"), dashboard_session_length_hours: z .number() .positive() @@ -102,35 +128,61 @@ const configSchema = z.object({ .transform(getEnvOrYaml("SERVER_SECRET")) .pipe(z.string().min(8)) }), - traefik: z.object({ - http_entrypoint: z.string(), - https_entrypoint: z.string().optional(), - additional_middlewares: z.array(z.string()).optional() - }), - gerbil: z.object({ - start_port: portSchema.optional().transform(stoi).pipe(portSchema), - base_endpoint: z - .string() - .optional() - .pipe(z.string()) - .transform((url) => url.toLowerCase()), - use_subdomain: z.boolean(), - subnet_group: z.string(), - block_size: z.number().positive().gt(0), - site_block_size: z.number().positive().gt(0) - }), - rate_limits: z.object({ - global: z.object({ - window_minutes: z.number().positive().gt(0), - max_requests: z.number().positive().gt(0) - }), - auth: z - .object({ - window_minutes: z.number().positive().gt(0), - max_requests: z.number().positive().gt(0) - }) - .optional() - }), + traefik: z + .object({ + http_entrypoint: z.string().optional().default("web"), + https_entrypoint: z.string().optional().default("websecure"), + additional_middlewares: z.array(z.string()).optional() + }) + .optional() + .default({}), + gerbil: z + .object({ + start_port: portSchema + .optional() + .default(51820) + .transform(stoi) + .pipe(portSchema), + base_endpoint: z + .string() + .optional() + .pipe(z.string()) + .transform((url) => url.toLowerCase()), + use_subdomain: z.boolean().optional().default(false), + subnet_group: z.string().optional().default("100.89.137.0/20"), + block_size: z.number().positive().gt(0).optional().default(24), + site_block_size: z.number().positive().gt(0).optional().default(30) + }) + .optional() + .default({}), + rate_limits: z + .object({ + global: z + .object({ + window_minutes: z + .number() + .positive() + .gt(0) + .optional() + .default(1), + max_requests: z + .number() + .positive() + .gt(0) + .optional() + .default(500) + }) + .optional() + .default({}), + auth: z + .object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }) + .optional() + }) + .optional() + .default({}), email: z .object({ smtp_host: z.string().optional(), From d9eccd6c13c443fc1963814c0a33d55b49acc6f6 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sat, 10 May 2025 11:28:22 -0400 Subject: [PATCH 9/9] bump version --- server/lib/consts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/consts.ts b/server/lib/consts.ts index 94d2716..c7c4e87 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.3.0"; +export const APP_VERSION = "1.3.2"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME);