mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-20 12:36:33 +02:00
Compare commits
4 Commits
1.21.0-rc.0
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 9561d23f1e | |||
| a2e1c7b751 | |||
| 9e2ec72ced | |||
| 02fe1f3abd |
@@ -1995,6 +1995,9 @@
|
||||
"domainPickerSubdomain": "Subdomain: {subdomain}",
|
||||
"domainPickerNamespace": "Namespace: {namespace}",
|
||||
"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",
|
||||
"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.",
|
||||
|
||||
@@ -30,14 +30,14 @@ export const NotifyTrialExpiring = ({
|
||||
const isLastDay = daysRemaining === 1;
|
||||
|
||||
const previewText = hasEnded
|
||||
? `Your trial for ${orgName} has ended.`
|
||||
? `Your cloud trial for ${orgName} has ended.`
|
||||
: isLastDay
|
||||
? `Your trial for ${orgName} ends tomorrow.`
|
||||
: `Your trial for ${orgName} ends in ${daysRemaining} days.`;
|
||||
? `Your cloud trial for ${orgName} ends tomorrow.`
|
||||
: `Your cloud trial for ${orgName} ends in ${daysRemaining} days.`;
|
||||
|
||||
const heading = hasEnded
|
||||
? "Your Trial Ended"
|
||||
: "Your Trial is Ending Soon";
|
||||
? "Your Cloud Trial Ended"
|
||||
: "Your Cloud Trial is Ending Soon";
|
||||
|
||||
return (
|
||||
<Html>
|
||||
@@ -55,7 +55,7 @@ export const NotifyTrialExpiring = ({
|
||||
{hasEnded ? (
|
||||
<>
|
||||
<EmailText>
|
||||
Your free trial for{" "}
|
||||
Your cloud free trial for{" "}
|
||||
<strong>{orgName}</strong> ended on{" "}
|
||||
<strong>{trialEndsAt}</strong>. Your account
|
||||
has been moved to the free plan, which
|
||||
@@ -64,10 +64,11 @@ export const NotifyTrialExpiring = ({
|
||||
|
||||
<EmailText>
|
||||
Some features and resources may now be
|
||||
restricted. To restore full
|
||||
access and continue using all the features
|
||||
you had during your trial, please upgrade to
|
||||
a paid plan.
|
||||
restricted. To restore full access and
|
||||
continue using all the features you had
|
||||
during your trial, please upgrade to a paid
|
||||
plan. This does not effect any self hosted
|
||||
licenses.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
@@ -93,7 +94,8 @@ export const NotifyTrialExpiring = ({
|
||||
<EmailText>
|
||||
After your trial ends, your account will be
|
||||
moved to the free plan and some
|
||||
functionality may be restricted.
|
||||
functionality may be restricted. This does
|
||||
not effect any self hosted licenses.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
|
||||
+34
-3
@@ -15,10 +15,41 @@ function getSegmentRegex(patternPart: string): RegExp {
|
||||
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 {
|
||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
||||
const patternParts = normalize(pattern);
|
||||
const pathParts = normalize(path);
|
||||
const patternParts = pattern.split("/").filter(Boolean);
|
||||
const pathParts = decodeAndResolvePath(path);
|
||||
|
||||
function matchSegments(
|
||||
patternIndex: number,
|
||||
|
||||
@@ -236,6 +236,38 @@ function runTests() {
|
||||
"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!");
|
||||
}
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
// check for HTTP Basic Auth header
|
||||
const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`;
|
||||
const clientHeaderAuthKey = `headerAuth:${resource.resourceId}:${clientHeaderAuth}`;
|
||||
if (headerAuth && clientHeaderAuth) {
|
||||
if (localCache.get(clientHeaderAuthKey)) {
|
||||
logger.debug(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
@@ -43,10 +43,12 @@ import {
|
||||
CheckCircle2,
|
||||
ChevronsUpDown,
|
||||
ExternalLink,
|
||||
Globe,
|
||||
KeyRound,
|
||||
Zap
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { usePaidStatus } from "@/hooks/usePaidStatus";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -494,6 +496,28 @@ export default function DomainPicker({
|
||||
const hasMoreProvided =
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
|
||||
Reference in New Issue
Block a user