Compare commits

..

8 Commits

Author SHA1 Message Date
dependabot[bot]
4319460fe7 Bump the github-actions-dependencies group across 1 directory with 2 updates
Bumps the github-actions-dependencies group with 2 updates in the / directory: [docker/login-action](https://github.com/docker/login-action) and [actions/setup-go](https://github.com/actions/setup-go).


Updates `docker/login-action` from 4.2.0 to 4.4.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](650006c6eb...af1e73f918)

Updates `actions/setup-go` from 6.4.0 to 6.5.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4a3601121d...924ae3a1cd)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: actions/setup-go
  dependency-version: 6.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 01:33:42 +00:00
Owen Schwartz
d4c52bbf2f Merge pull request #3396 from fosrl/dev
1.19.4-s.5
2026-07-03 16:15:18 -04:00
Owen Schwartz
87f50bf0cc Merge pull request #3389 from fosrl/dev
1.19.4-s.3
2026-07-03 10:29:15 -04:00
Owen Schwartz
2c66da1b19 Merge pull request #3386 from v1rusnl/main
Upgrade Traefik image to version 3.7
2026-07-03 10:18:44 -04:00
v1rusnl
ab19955502 Upgrade Traefik image to version 3.7 2026-07-03 08:22:13 +02:00
v1rusnl
1db9dcec81 Update Traefik image version to v3.7 2026-07-03 08:21:12 +02:00
Owen Schwartz
49c2d3163e Merge pull request #3381 from fosrl/dev
dev
2026-07-02 10:56:39 -04:00
Owen Schwartz
45b9e13a13 Merge pull request #3378 from fosrl/dev
1.19.4-s.1
2026-07-01 21:48:01 -04:00
7 changed files with 67 additions and 105 deletions

View File

@@ -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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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 }}

View File

@@ -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

View File

@@ -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}}

View File

@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Basic Header Auth", "policyAuthHeaderAuthTitle": "Basic Header Auth",
"policyAuthHeaderAuthDescription": "Validate a custom HTTP header name and value on each request", "policyAuthHeaderAuthDescription": "Validate a custom HTTP header name and value on each request",
"policyAuthHeaderAuthSummary": "Header configured", "policyAuthHeaderAuthSummary": "Header configured",
"policyAuthHeaderName": "Username", "policyAuthHeaderName": "Header name",
"policyAuthHeaderValue": "Password", "policyAuthHeaderValue": "Expected value",
"policyAuthSetPasscode": "Set Passcode", "policyAuthSetPasscode": "Set Passcode",
"policyAuthSetPincode": "Set PIN Code", "policyAuthSetPincode": "Set PIN Code",
"policyAuthSetEmailWhitelist": "Set Email Whitelist", "policyAuthSetEmailWhitelist": "Set Email Whitelist",

View File

@@ -616,8 +616,8 @@ export async function listResources(
and( and(
inArray(resources.mode, browserGatewayModes), inArray(resources.mode, browserGatewayModes),
or( or(
effectiveSso, eq(effectiveSso, true),
effectiveWhitelist, eq(effectiveWhitelist, true),
not(isNull(effectiveHeaderAuthId)), not(isNull(effectiveHeaderAuthId)),
not(isNull(effectivePincodeId)), not(isNull(effectivePincodeId)),
not(isNull(effectivePasswordId)) not(isNull(effectivePasswordId))
@@ -629,8 +629,8 @@ export async function listResources(
conditions.push( conditions.push(
and( and(
inArray(resources.mode, browserGatewayModes), inArray(resources.mode, browserGatewayModes),
not(effectiveSso), not(eq(effectiveSso, true)),
not(effectiveWhitelist), not(eq(effectiveWhitelist, true)),
isNull(effectiveHeaderAuthId), isNull(effectiveHeaderAuthId),
isNull(effectivePincodeId), isNull(effectivePincodeId),
isNull(effectivePasswordId) isNull(effectivePasswordId)

View File

@@ -41,7 +41,7 @@ import {
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useActionState, useState } from "react"; import { useActionState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -137,21 +137,11 @@ function ProxyResourceHttpForm({
}); });
const [, formAction, saveLoading] = useActionState(onSubmit, null); const [, formAction, saveLoading] = useActionState(onSubmit, null);
const [headersValid, setHeadersValid] = useState(true);
async function onSubmit() { async function onSubmit() {
const isValid = await form.trigger(); const isValid = await form.trigger();
if (!isValid) return; if (!isValid) return;
if (!headersValid) {
toast({
variant: "destructive",
title: t("settingsErrorUpdate"),
description: t("headersValidationError")
});
return;
}
const data = form.getValues(); const data = form.getValues();
const res = await api const res = await api
@@ -328,9 +318,6 @@ function ProxyResourceHttpForm({
onChange={ onChange={
field.onChange field.onChange
} }
onValidityChange={
setHeadersValid
}
rows={4} rows={4}
/> />
</FormControl> </FormControl>
@@ -354,7 +341,7 @@ function ProxyResourceHttpForm({
<Button <Button
type="submit" type="submit"
loading={saveLoading} loading={saveLoading}
disabled={saveLoading || !headersValid} disabled={saveLoading}
form="http-settings-form" form="http-settings-form"
> >
{t("saveSettings")} {t("saveSettings")}

View File

@@ -2,36 +2,24 @@
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { useTranslations } from "next-intl";
interface HeadersInputProps { interface HeadersInputProps {
value?: { name: string; value: string }[] | null; value?: { name: string; value: string }[] | null;
onChange: (value: { name: string; value: string }[] | null) => void; onChange: (value: { name: string; value: string }[] | null) => void;
onValidityChange?: (isValid: boolean) => void;
placeholder?: string; placeholder?: string;
rows?: number; rows?: number;
className?: string; className?: string;
} }
// Mirrors the server side validation in updateResource.ts so that invalid
// input is caught (and shown to the user) before it is ever submitted,
// instead of being silently dropped in favor of the last known good value.
const validHeaderNamePattern = /^[a-zA-Z0-9!#$%&'*+\-.^_`|~]+$/;
const validHeaderValuePattern = /^[\t\x20-\x7E]*$/;
const templatePattern = /\{\{[^}]+\}\}/;
export function HeadersInput({ export function HeadersInput({
value = [], value = [],
onChange, onChange,
onValidityChange,
placeholder = `X-Example-Header: example-value placeholder = `X-Example-Header: example-value
X-Another-Header: another-value`, X-Another-Header: another-value`,
rows = 4, rows = 4,
className className
}: HeadersInputProps) { }: HeadersInputProps) {
const t = useTranslations();
const [internalValue, setInternalValue] = useState(""); const [internalValue, setInternalValue] = useState("");
const [error, setError] = useState<string | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const isUserEditingRef = useRef(false); const isUserEditingRef = useRef(false);
@@ -46,56 +34,37 @@ X-Another-Header: another-value`,
.join("\n"); .join("\n");
}; };
// Parse newline-separated text into header objects, validating each line // Convert newline-separated string to header objects array
// against the same rules enforced by the server. Returns either the const convertToHeadersArray = (
// parsed headers or an error message describing the first invalid line.
const parseHeaders = (
newlineSeparated: string newlineSeparated: string
): ): { name: string; value: string }[] | null => {
| { headers: { name: string; value: string }[]; error: null } if (!newlineSeparated || newlineSeparated.trim() === "") return [];
| { headers: null; error: string } => {
if (!newlineSeparated || newlineSeparated.trim() === "") {
return { headers: [], error: null };
}
const lines = newlineSeparated return newlineSeparated
.split("\n") .split("\n")
.map((line) => line.trim()) .map((line) => line.trim())
.filter((line) => line.length > 0); .filter((line) => line.length > 0 && line.includes(":"))
.map((line) => {
const headers: { name: string; value: string }[] = [];
for (const line of lines) {
const colonIndex = line.indexOf(":"); const colonIndex = line.indexOf(":");
if (colonIndex === -1) {
return { headers: null, error: t("headersValidationError") };
}
const name = line.substring(0, colonIndex).trim(); const name = line.substring(0, colonIndex).trim();
const value = line.substring(colonIndex + 1).trim(); const value = line.substring(colonIndex + 1).trim();
if ( // Ensure header name conforms to HTTP header requirements
!validHeaderNamePattern.test(name) || // Header names should be case-insensitive, contain only ASCII letters, digits, and hyphens
!validHeaderValuePattern.test(value) || const normalizedName = name
templatePattern.test(name) || .replace(/[^a-zA-Z0-9\-]/g, "")
templatePattern.test(value) .toLowerCase();
) {
return { headers: null, error: t("headersValidationError") };
}
headers.push({ name, value }); return { name: normalizedName, value };
} })
.filter((header) => header.name.length > 0); // Filter out headers with invalid names
return { headers, error: null };
}; };
// Update internal value when external value changes // Update internal value when external value changes
// But only if the user is not currently editing (textarea not focused) // But only if the user is not currently editing (textarea not focused)
useEffect(() => { useEffect(() => {
if (!isUserEditingRef.current) { if (!isUserEditingRef.current) {
setInternalValue(convertToNewlineSeparated(value ?? [])); setInternalValue(convertToNewlineSeparated(value));
setError(null);
onValidityChange?.(true);
} }
}, [value]); }, [value]);
@@ -106,20 +75,31 @@ X-Another-Header: another-value`,
// Mark that user is actively editing // Mark that user is actively editing
isUserEditingRef.current = true; isUserEditingRef.current = true;
const result = parseHeaders(newValue); // Only update parent if the input is in a valid state
// Valid states: empty/whitespace only, or contains properly formatted headers
if (result.error) { if (newValue.trim() === "") {
// Surface the error and do not touch the last known good value. // Empty input is valid - represents no headers
// Silently dropping the update here (without telling the user) onChange([]);
// is what previously let stale data get saved without warning. } else {
setError(result.error); // Check if all non-empty lines are properly formatted (contain ':')
onValidityChange?.(false); const lines = newValue.split("\n");
return; const nonEmptyLines = lines
.map((line) => line.trim())
.filter((line) => line.length > 0);
// If there are no non-empty lines, or all non-empty lines contain ':', it's valid
const isValid =
nonEmptyLines.length === 0 ||
nonEmptyLines.every((line) => line.includes(":"));
if (isValid) {
// Safe to convert and update parent
const headersArray = convertToHeadersArray(newValue);
onChange(headersArray);
}
// If not valid, don't call onChange - let user continue typing
} }
setError(null);
onValidityChange?.(true);
onChange(result.headers);
}; };
const handleFocus = () => { const handleFocus = () => {
@@ -134,7 +114,6 @@ X-Another-Header: another-value`,
}; };
return ( return (
<div>
<Textarea <Textarea
ref={textareaRef} ref={textareaRef}
value={internalValue} value={internalValue}
@@ -145,9 +124,5 @@ X-Another-Header: another-value`,
rows={rows} rows={rows}
className={className} className={className}
/> />
{error && (
<p className="text-sm text-destructive mt-1.5">{error}</p>
)}
</div>
); );
} }