Files
caddy-proxy-manager/app/(dashboard)/l4-proxy-hosts/actions.ts
akanealw 99819b70ff
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
added caddy-proxy-manager for testing
2026-04-21 22:49:08 +00:00

277 lines
12 KiB
TypeScript
Executable File

"use server";
import { revalidatePath } from "next/cache";
import { requireAdmin } from "@/src/lib/auth";
import { actionError, actionSuccess, INITIAL_ACTION_STATE, type ActionState } from "@/src/lib/actions";
import {
createL4ProxyHost,
deleteL4ProxyHost,
updateL4ProxyHost,
type L4ProxyHostInput,
type L4Protocol,
type L4MatcherType,
type L4ProxyProtocolVersion,
type L4LoadBalancingPolicy,
type L4LoadBalancerConfig,
type L4DnsResolverConfig,
type L4UpstreamDnsResolutionConfig,
type L4GeoBlockConfig,
type L4GeoBlockMode,
} from "@/src/lib/models/l4-proxy-hosts";
import { parseCheckbox, parseCsv, parseUpstreams, parseOptionalText, parseOptionalNumber } from "@/src/lib/form-parse";
const VALID_PROTOCOLS: L4Protocol[] = ["tcp", "udp"];
const VALID_MATCHER_TYPES: L4MatcherType[] = ["none", "tls_sni", "http_host", "proxy_protocol"];
const VALID_PP_VERSIONS: L4ProxyProtocolVersion[] = ["v1", "v2"];
const VALID_L4_LB_POLICIES: L4LoadBalancingPolicy[] = ["random", "round_robin", "least_conn", "ip_hash", "first"];
const VALID_DNS_FAMILIES = ["ipv6", "ipv4", "both"] as const;
function parseL4LoadBalancerConfig(formData: FormData): Partial<L4LoadBalancerConfig> | undefined {
if (!formData.has("lb_present")) return undefined;
const enabled = formData.has("lb_enabled_present")
? parseCheckbox(formData.get("lb_enabled"))
: undefined;
const policyRaw = parseOptionalText(formData.get("lb_policy"));
const policy = policyRaw && VALID_L4_LB_POLICIES.includes(policyRaw as L4LoadBalancingPolicy)
? (policyRaw as L4LoadBalancingPolicy) : undefined;
const result: Partial<L4LoadBalancerConfig> = {};
if (enabled !== undefined) result.enabled = enabled;
if (policy) result.policy = policy;
const tryDuration = parseOptionalText(formData.get("lb_try_duration"));
if (tryDuration !== null) result.tryDuration = tryDuration;
const tryInterval = parseOptionalText(formData.get("lb_try_interval"));
if (tryInterval !== null) result.tryInterval = tryInterval;
const retries = parseOptionalNumber(formData.get("lb_retries"));
if (retries !== null) result.retries = retries;
// Active health check
if (formData.has("lb_active_health_enabled_present")) {
result.activeHealthCheck = {
enabled: parseCheckbox(formData.get("lb_active_health_enabled")),
port: parseOptionalNumber(formData.get("lb_active_health_port")),
interval: parseOptionalText(formData.get("lb_active_health_interval")),
timeout: parseOptionalText(formData.get("lb_active_health_timeout")),
};
}
// Passive health check
if (formData.has("lb_passive_health_enabled_present")) {
result.passiveHealthCheck = {
enabled: parseCheckbox(formData.get("lb_passive_health_enabled")),
failDuration: parseOptionalText(formData.get("lb_passive_health_fail_duration")),
maxFails: parseOptionalNumber(formData.get("lb_passive_health_max_fails")),
unhealthyLatency: parseOptionalText(formData.get("lb_passive_health_unhealthy_latency")),
};
}
return Object.keys(result).length > 0 ? result : undefined;
}
function parseL4DnsResolverConfig(formData: FormData): Partial<L4DnsResolverConfig> | undefined {
if (!formData.has("dns_present")) return undefined;
const enabled = formData.has("dns_enabled_present")
? parseCheckbox(formData.get("dns_enabled"))
: undefined;
const resolversRaw = parseOptionalText(formData.get("dns_resolvers"));
const resolvers = resolversRaw
? resolversRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean)
: undefined;
const fallbacksRaw = parseOptionalText(formData.get("dns_fallbacks"));
const fallbacks = fallbacksRaw
? fallbacksRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean)
: undefined;
const timeout = parseOptionalText(formData.get("dns_timeout"));
const result: Partial<L4DnsResolverConfig> = {};
if (enabled !== undefined) result.enabled = enabled;
if (resolvers) result.resolvers = resolvers;
if (fallbacks) result.fallbacks = fallbacks;
if (timeout !== null) result.timeout = timeout;
return Object.keys(result).length > 0 ? result : undefined;
}
function parseL4UpstreamDnsResolutionConfig(formData: FormData): Partial<L4UpstreamDnsResolutionConfig> | undefined {
if (!formData.has("upstream_dns_resolution_present")) return undefined;
const modeRaw = parseOptionalText(formData.get("upstream_dns_resolution_mode")) ?? "inherit";
const familyRaw = parseOptionalText(formData.get("upstream_dns_resolution_family")) ?? "inherit";
const result: Partial<L4UpstreamDnsResolutionConfig> = {};
if (modeRaw === "enabled") result.enabled = true;
else if (modeRaw === "disabled") result.enabled = false;
else if (modeRaw === "inherit") result.enabled = null;
if (familyRaw === "inherit") result.family = null;
else if (VALID_DNS_FAMILIES.includes(familyRaw as typeof VALID_DNS_FAMILIES[number])) {
result.family = familyRaw as "ipv6" | "ipv4" | "both";
}
return Object.keys(result).length > 0 ? result : undefined;
}
function parseL4GeoBlockConfig(formData: FormData): { geoblock: L4GeoBlockConfig | null; geoblock_mode: L4GeoBlockMode } {
if (!formData.has("geoblock_present")) {
return { geoblock: null, geoblock_mode: "merge" };
}
const enabled = parseCheckbox(formData.get("geoblock_enabled"));
const rawMode = formData.get("geoblock_mode");
const mode: L4GeoBlockMode = rawMode === "override" ? "override" : "merge";
const parseStringList = (key: string): string[] => {
const val = formData.get(key);
if (!val || typeof val !== "string") return [];
return val.split(",").map(s => s.trim()).filter(Boolean);
};
const parseNumberList = (key: string): number[] => {
return parseStringList(key).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
};
const config: L4GeoBlockConfig = {
enabled,
block_countries: parseStringList("geoblock_block_countries"),
block_continents: parseStringList("geoblock_block_continents"),
block_asns: parseNumberList("geoblock_block_asns"),
block_cidrs: parseStringList("geoblock_block_cidrs"),
block_ips: parseStringList("geoblock_block_ips"),
allow_countries: parseStringList("geoblock_allow_countries"),
allow_continents: parseStringList("geoblock_allow_continents"),
allow_asns: parseNumberList("geoblock_allow_asns"),
allow_cidrs: parseStringList("geoblock_allow_cidrs"),
allow_ips: parseStringList("geoblock_allow_ips"),
};
return { geoblock: config, geoblock_mode: mode };
}
function parseProtocol(formData: FormData): L4Protocol {
const raw = String(formData.get("protocol") ?? "tcp").trim().toLowerCase();
if (VALID_PROTOCOLS.includes(raw as L4Protocol)) return raw as L4Protocol;
return "tcp";
}
function parseMatcherType(formData: FormData): L4MatcherType {
const raw = String(formData.get("matcher_type") ?? "none").trim();
if (VALID_MATCHER_TYPES.includes(raw as L4MatcherType)) return raw as L4MatcherType;
return "none";
}
function parseProxyProtocolVersion(formData: FormData): L4ProxyProtocolVersion | null {
const raw = parseOptionalText(formData.get("proxy_protocol_version"));
if (raw && VALID_PP_VERSIONS.includes(raw as L4ProxyProtocolVersion)) return raw as L4ProxyProtocolVersion;
return null;
}
export async function createL4ProxyHostAction(
_prevState: ActionState = INITIAL_ACTION_STATE,
formData: FormData
): Promise<ActionState> {
void _prevState;
try {
const session = await requireAdmin();
const userId = Number(session.user.id);
const matcherType = parseMatcherType(formData);
const matcherValue = (matcherType === "tls_sni" || matcherType === "http_host")
? parseCsv(formData.get("matcher_value"))
: [];
const input: L4ProxyHostInput = {
name: String(formData.get("name") ?? "Untitled"),
protocol: parseProtocol(formData),
listenAddress: String(formData.get("listen_address") ?? "").trim(),
upstreams: parseUpstreams(formData.get("upstreams")),
matcherType: matcherType,
matcherValue: matcherValue,
tlsTermination: parseCheckbox(formData.get("tls_termination")),
proxyProtocolVersion: parseProxyProtocolVersion(formData),
proxyProtocolReceive: parseCheckbox(formData.get("proxy_protocol_receive")),
enabled: parseCheckbox(formData.get("enabled")),
loadBalancer: parseL4LoadBalancerConfig(formData),
dnsResolver: parseL4DnsResolverConfig(formData),
upstreamDnsResolution: parseL4UpstreamDnsResolutionConfig(formData),
...parseL4GeoBlockConfig(formData),
};
await createL4ProxyHost(input, userId);
revalidatePath("/l4-proxy-hosts");
return actionSuccess("L4 proxy host created and queued for Caddy reload.");
} catch (error) {
console.error("Failed to create L4 proxy host:", error);
return actionError(error, "Failed to create L4 proxy host.");
}
}
export async function updateL4ProxyHostAction(
id: number,
_prevState: ActionState = INITIAL_ACTION_STATE,
formData: FormData
): Promise<ActionState> {
void _prevState;
try {
const session = await requireAdmin();
const userId = Number(session.user.id);
const matcherType = parseMatcherType(formData);
const matcherValue = (matcherType === "tls_sni" || matcherType === "http_host")
? parseCsv(formData.get("matcher_value"))
: [];
const input: Partial<L4ProxyHostInput> = {
name: formData.get("name") ? String(formData.get("name")) : undefined,
protocol: parseProtocol(formData),
listenAddress: formData.get("listen_address") ? String(formData.get("listen_address")).trim() : undefined,
upstreams: formData.get("upstreams") ? parseUpstreams(formData.get("upstreams")) : undefined,
matcherType: matcherType,
matcherValue: matcherValue,
tlsTermination: parseCheckbox(formData.get("tls_termination")),
proxyProtocolVersion: parseProxyProtocolVersion(formData),
proxyProtocolReceive: parseCheckbox(formData.get("proxy_protocol_receive")),
enabled: formData.has("enabled_present") ? parseCheckbox(formData.get("enabled")) : undefined,
loadBalancer: parseL4LoadBalancerConfig(formData),
dnsResolver: parseL4DnsResolverConfig(formData),
upstreamDnsResolution: parseL4UpstreamDnsResolutionConfig(formData),
...parseL4GeoBlockConfig(formData),
};
await updateL4ProxyHost(id, input, userId);
revalidatePath("/l4-proxy-hosts");
return actionSuccess("L4 proxy host updated.");
} catch (error) {
console.error(`Failed to update L4 proxy host ${id}:`, error);
return actionError(error, "Failed to update L4 proxy host.");
}
}
export async function deleteL4ProxyHostAction(
id: number,
_prevState: ActionState = INITIAL_ACTION_STATE
): Promise<ActionState> {
void _prevState;
try {
const session = await requireAdmin();
const userId = Number(session.user.id);
await deleteL4ProxyHost(id, userId);
revalidatePath("/l4-proxy-hosts");
return actionSuccess("L4 proxy host deleted.");
} catch (error) {
console.error(`Failed to delete L4 proxy host ${id}:`, error);
return actionError(error, "Failed to delete L4 proxy host.");
}
}
export async function toggleL4ProxyHostAction(
id: number,
enabled: boolean
): Promise<ActionState> {
try {
const session = await requireAdmin();
const userId = Number(session.user.id);
await updateL4ProxyHost(id, { enabled }, userId);
revalidatePath("/l4-proxy-hosts");
return actionSuccess(`L4 proxy host ${enabled ? "enabled" : "disabled"}.`);
} catch (error) {
console.error(`Failed to toggle L4 proxy host ${id}:`, error);
return actionError(error, "Failed to toggle L4 proxy host.");
}
}