mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-22 13:34:13 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d50a18de9 | |||
| c30fe5b574 | |||
| 113b7a0b84 | |||
| 5d20956a0e | |||
| e5398d441e | |||
| d4138e2141 | |||
| 09d5d9082e | |||
| 5cb316f4e9 | |||
| 6b6c9cf4d8 | |||
| 05617c63c0 | |||
| d4c52bbf2f | |||
| 87f50bf0cc | |||
| 2c66da1b19 | |||
| ab19955502 | |||
| 1db9dcec81 | |||
| 49c2d3163e | |||
| 45b9e13a13 |
@@ -77,7 +77,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -204,7 +204,7 @@ jobs:
|
|||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -264,7 +264,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25
|
go-version: 1.25
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry (for cosign)
|
- name: Login to GitHub Container Registry (for cosign)
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '24'
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '24'
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ services:
|
|||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.6
|
image: traefik:v3.7
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ services:
|
|||||||
- 80:80{{end}}
|
- 80:80{{end}}
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: docker.io/traefik:v3.6
|
image: docker.io/traefik:v3.7
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
|
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
|
||||||
|
|||||||
@@ -1995,9 +1995,6 @@
|
|||||||
"domainPickerSubdomain": "Subdomain: {subdomain}",
|
"domainPickerSubdomain": "Subdomain: {subdomain}",
|
||||||
"domainPickerNamespace": "Namespace: {namespace}",
|
"domainPickerNamespace": "Namespace: {namespace}",
|
||||||
"domainPickerShowMore": "Show More",
|
"domainPickerShowMore": "Show More",
|
||||||
"domainPickerNoDomainsAvailableTitle": "No domains available",
|
|
||||||
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
|
|
||||||
"domainPickerNoDomainsAvailableAction": "Go to Domains",
|
|
||||||
"regionSelectorTitle": "Select Region",
|
"regionSelectorTitle": "Select Region",
|
||||||
"domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.",
|
"domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.",
|
||||||
"regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.",
|
"regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.",
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ export const NotifyTrialExpiring = ({
|
|||||||
const isLastDay = daysRemaining === 1;
|
const isLastDay = daysRemaining === 1;
|
||||||
|
|
||||||
const previewText = hasEnded
|
const previewText = hasEnded
|
||||||
? `Your cloud trial for ${orgName} has ended.`
|
? `Your trial for ${orgName} has ended.`
|
||||||
: isLastDay
|
: isLastDay
|
||||||
? `Your cloud trial for ${orgName} ends tomorrow.`
|
? `Your trial for ${orgName} ends tomorrow.`
|
||||||
: `Your cloud trial for ${orgName} ends in ${daysRemaining} days.`;
|
: `Your trial for ${orgName} ends in ${daysRemaining} days.`;
|
||||||
|
|
||||||
const heading = hasEnded
|
const heading = hasEnded
|
||||||
? "Your Cloud Trial Ended"
|
? "Your Trial Ended"
|
||||||
: "Your Cloud Trial is Ending Soon";
|
: "Your Trial is Ending Soon";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
@@ -55,7 +55,7 @@ export const NotifyTrialExpiring = ({
|
|||||||
{hasEnded ? (
|
{hasEnded ? (
|
||||||
<>
|
<>
|
||||||
<EmailText>
|
<EmailText>
|
||||||
Your cloud free trial for{" "}
|
Your free trial for{" "}
|
||||||
<strong>{orgName}</strong> ended on{" "}
|
<strong>{orgName}</strong> ended on{" "}
|
||||||
<strong>{trialEndsAt}</strong>. Your account
|
<strong>{trialEndsAt}</strong>. Your account
|
||||||
has been moved to the free plan, which
|
has been moved to the free plan, which
|
||||||
@@ -64,11 +64,10 @@ export const NotifyTrialExpiring = ({
|
|||||||
|
|
||||||
<EmailText>
|
<EmailText>
|
||||||
Some features and resources may now be
|
Some features and resources may now be
|
||||||
restricted. To restore full access and
|
restricted. To restore full
|
||||||
continue using all the features you had
|
access and continue using all the features
|
||||||
during your trial, please upgrade to a paid
|
you had during your trial, please upgrade to
|
||||||
plan. This does not effect any self hosted
|
a paid plan.
|
||||||
licenses.
|
|
||||||
</EmailText>
|
</EmailText>
|
||||||
|
|
||||||
<EmailText>
|
<EmailText>
|
||||||
@@ -94,8 +93,7 @@ export const NotifyTrialExpiring = ({
|
|||||||
<EmailText>
|
<EmailText>
|
||||||
After your trial ends, your account will be
|
After your trial ends, your account will be
|
||||||
moved to the free plan and some
|
moved to the free plan and some
|
||||||
functionality may be restricted. This does
|
functionality may be restricted.
|
||||||
not effect any self hosted licenses.
|
|
||||||
</EmailText>
|
</EmailText>
|
||||||
|
|
||||||
<EmailText>
|
<EmailText>
|
||||||
|
|||||||
+3
-34
@@ -15,41 +15,10 @@ function getSegmentRegex(patternPart: string): RegExp {
|
|||||||
return regex;
|
return regex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decodes percent-encoding (so an encoded slash like `%2F` is treated as a
|
|
||||||
// real path separator, matching what most backends will do) and then
|
|
||||||
// resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/`
|
|
||||||
// or `/public/../admin/` is matched as `/admin/`, not as a literal segment
|
|
||||||
// or a wildcard-swallowed sequence under `/public/*`.
|
|
||||||
function decodeAndResolvePath(p: string): string[] {
|
|
||||||
const rawParts = p.split("/").filter(Boolean);
|
|
||||||
|
|
||||||
const resolved: string[] = [];
|
|
||||||
for (const rawPart of rawParts) {
|
|
||||||
let part: string;
|
|
||||||
try {
|
|
||||||
part = decodeURIComponent(rawPart);
|
|
||||||
} catch {
|
|
||||||
part = rawPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// an encoded slash can turn one raw segment into several real ones
|
|
||||||
for (const segment of part.split("/").filter(Boolean)) {
|
|
||||||
if (segment === ".") {
|
|
||||||
continue;
|
|
||||||
} else if (segment === "..") {
|
|
||||||
resolved.pop();
|
|
||||||
} else {
|
|
||||||
resolved.push(segment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPathAllowed(pattern: string, path: string): boolean {
|
export function isPathAllowed(pattern: string, path: string): boolean {
|
||||||
const patternParts = pattern.split("/").filter(Boolean);
|
const normalize = (p: string) => p.split("/").filter(Boolean);
|
||||||
const pathParts = decodeAndResolvePath(path);
|
const patternParts = normalize(pattern);
|
||||||
|
const pathParts = normalize(path);
|
||||||
|
|
||||||
function matchSegments(
|
function matchSegments(
|
||||||
patternIndex: number,
|
patternIndex: number,
|
||||||
|
|||||||
@@ -236,38 +236,6 @@ function runTests() {
|
|||||||
"Root path should not match non-root path"
|
"Root path should not match non-root path"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Path traversal / encoded-slash bypass regression tests
|
|
||||||
assertEquals(
|
|
||||||
isPathAllowed("public/*", "public/../admin"),
|
|
||||||
false,
|
|
||||||
"Literal .. traversal out of an allowed prefix must not match"
|
|
||||||
);
|
|
||||||
assertEquals(
|
|
||||||
isPathAllowed("public/*", "public%2F..%2Fadmin"),
|
|
||||||
false,
|
|
||||||
"Encoded-slash traversal out of an allowed prefix must not match"
|
|
||||||
);
|
|
||||||
assertEquals(
|
|
||||||
isPathAllowed("public/*", "public%2f..%2fadmin%2ffile"),
|
|
||||||
false,
|
|
||||||
"Encoded-slash traversal is case-insensitively decoded before matching"
|
|
||||||
);
|
|
||||||
assertEquals(
|
|
||||||
isPathAllowed("admin/*", "public/../admin/secret"),
|
|
||||||
true,
|
|
||||||
".. traversal INTO a restricted path should still be caught by its own rule"
|
|
||||||
);
|
|
||||||
assertEquals(
|
|
||||||
isPathAllowed("public/*", "public%2Ffoo"),
|
|
||||||
true,
|
|
||||||
"Encoded slash without traversal should still resolve and match normally"
|
|
||||||
);
|
|
||||||
assertEquals(
|
|
||||||
isPathAllowed("public/*", "public/./foo"),
|
|
||||||
true,
|
|
||||||
"Single-dot segments are a no-op and should not affect matching"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("All path matching tests passed!");
|
console.log("All path matching tests passed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check for HTTP Basic Auth header
|
// check for HTTP Basic Auth header
|
||||||
const clientHeaderAuthKey = `headerAuth:${resource.resourceId}:${clientHeaderAuth}`;
|
const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`;
|
||||||
if (headerAuth && clientHeaderAuth) {
|
if (headerAuth && clientHeaderAuth) {
|
||||||
if (localCache.get(clientHeaderAuthKey)) {
|
if (localCache.get(clientHeaderAuthKey)) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@@ -43,12 +43,10 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Globe,
|
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Zap
|
Zap
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { usePaidStatus } from "@/hooks/usePaidStatus";
|
import { usePaidStatus } from "@/hooks/usePaidStatus";
|
||||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
@@ -496,28 +494,6 @@ export default function DomainPicker({
|
|||||||
const hasMoreProvided =
|
const hasMoreProvided =
|
||||||
sortedAvailableOptions.length > providedDomainsShown;
|
sortedAvailableOptions.length > providedDomainsShown;
|
||||||
|
|
||||||
const noDomainsAvailable =
|
|
||||||
!loadingDomains &&
|
|
||||||
organizationDomains.length === 0 &&
|
|
||||||
(build === "oss" || hideFreeDomain || requiresPaywall);
|
|
||||||
|
|
||||||
if (noDomainsAvailable) {
|
|
||||||
return (
|
|
||||||
<Alert>
|
|
||||||
<Globe className="h-4 w-4" />
|
|
||||||
<AlertTitle>{t("domainPickerNoDomainsAvailableTitle")}</AlertTitle>
|
|
||||||
<AlertDescription className="space-y-3">
|
|
||||||
<p>{t("domainPickerNoDomainsAvailableDescription")}</p>
|
|
||||||
<Button asChild size="sm" variant="outline">
|
|
||||||
<Link href={`/${orgId}/settings/domains`}>
|
|
||||||
{t("domainPickerNoDomainsAvailableAction")}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -95,10 +95,7 @@ export function useOptimisticLabels({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
// Only refresh if the labels have been modified
|
router.refresh();
|
||||||
if (pendingActions.length > 0) {
|
|
||||||
router.refresh();
|
|
||||||
}
|
|
||||||
setPendingActions([]);
|
setPendingActions([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user