mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-28 15:56:39 +00:00
Chungus
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, TargetHealthCheck, targetHealthCheck } from "@server/db";
|
||||
import { newts, resources, sites, Target, targets } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -31,6 +31,25 @@ const createTargetSchema = z
|
||||
method: z.string().optional().nullable(),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
enabled: z.boolean().default(true),
|
||||
hcEnabled: z.boolean().optional(),
|
||||
hcPath: z.string().min(1).optional().nullable(),
|
||||
hcScheme: z.string().optional().nullable(),
|
||||
hcMode: z.string().optional().nullable(),
|
||||
hcHostname: z.string().optional().nullable(),
|
||||
hcPort: z.number().int().positive().optional().nullable(),
|
||||
hcInterval: z.number().int().positive().min(5).optional().nullable(),
|
||||
hcUnhealthyInterval: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.min(5)
|
||||
.optional()
|
||||
.nullable(),
|
||||
hcTimeout: z.number().int().positive().min(1).optional().nullable(),
|
||||
hcHeaders: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(),
|
||||
hcFollowRedirects: z.boolean().optional().nullable(),
|
||||
hcMethod: z.string().min(1).optional().nullable(),
|
||||
hcStatus: z.number().int().optional().nullable(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
||||
rewritePath: z.string().optional().nullable(),
|
||||
@@ -38,7 +57,7 @@ const createTargetSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CreateTargetResponse = Target;
|
||||
export type CreateTargetResponse = Target & TargetHealthCheck;
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
@@ -143,6 +162,7 @@ export async function createTarget(
|
||||
}
|
||||
|
||||
let newTarget: Target[] = [];
|
||||
let healthCheck: TargetHealthCheck[] = [];
|
||||
if (site.type == "local") {
|
||||
newTarget = await db
|
||||
.insert(targets)
|
||||
@@ -165,7 +185,10 @@ export async function createTarget(
|
||||
);
|
||||
}
|
||||
|
||||
const { internalPort, targetIps } = await pickPort(site.siteId!, db);
|
||||
const { internalPort, targetIps } = await pickPort(
|
||||
site.siteId!,
|
||||
db
|
||||
);
|
||||
|
||||
if (!internalPort) {
|
||||
return next(
|
||||
@@ -180,8 +203,40 @@ export async function createTarget(
|
||||
.insert(targets)
|
||||
.values({
|
||||
resourceId,
|
||||
siteId: site.siteId,
|
||||
ip: targetData.ip,
|
||||
method: targetData.method,
|
||||
port: targetData.port,
|
||||
internalPort,
|
||||
...targetData
|
||||
enabled: targetData.enabled,
|
||||
path: targetData.path,
|
||||
pathMatchType: targetData.pathMatchType
|
||||
})
|
||||
.returning();
|
||||
|
||||
let hcHeaders = null;
|
||||
if (targetData.hcHeaders) {
|
||||
hcHeaders = JSON.stringify(targetData.hcHeaders);
|
||||
}
|
||||
|
||||
healthCheck = await db
|
||||
.insert(targetHealthCheck)
|
||||
.values({
|
||||
targetId: newTarget[0].targetId,
|
||||
hcEnabled: targetData.hcEnabled ?? false,
|
||||
hcPath: targetData.hcPath ?? null,
|
||||
hcScheme: targetData.hcScheme ?? null,
|
||||
hcMode: targetData.hcMode ?? null,
|
||||
hcHostname: targetData.hcHostname ?? null,
|
||||
hcPort: targetData.hcPort ?? null,
|
||||
hcInterval: targetData.hcInterval ?? null,
|
||||
hcUnhealthyInterval: targetData.hcUnhealthyInterval ?? null,
|
||||
hcTimeout: targetData.hcTimeout ?? null,
|
||||
hcHeaders: hcHeaders,
|
||||
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
|
||||
hcMethod: targetData.hcMethod ?? null,
|
||||
hcStatus: targetData.hcStatus ?? null,
|
||||
hcHealth: "unknown"
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -205,6 +260,7 @@ export async function createTarget(
|
||||
await addTargets(
|
||||
newt.newtId,
|
||||
newTarget,
|
||||
healthCheck,
|
||||
resource.protocol,
|
||||
resource.proxyPort
|
||||
);
|
||||
@@ -213,7 +269,10 @@ export async function createTarget(
|
||||
}
|
||||
|
||||
return response<CreateTargetResponse>(res, {
|
||||
data: newTarget[0],
|
||||
data: {
|
||||
...newTarget[0],
|
||||
...healthCheck[0]
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Target created successfully",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, Target } from "@server/db";
|
||||
import { db, Target, targetHealthCheck, TargetHealthCheck } from "@server/db";
|
||||
import { targets } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -16,7 +16,9 @@ const getTargetSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
type GetTargetResponse = Target;
|
||||
type GetTargetResponse = Target & Omit<TargetHealthCheck, 'hcHeaders'> & {
|
||||
hcHeaders: { name: string; value: string; }[] | null;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
@@ -62,8 +64,29 @@ export async function getTarget(
|
||||
);
|
||||
}
|
||||
|
||||
const [targetHc] = await db
|
||||
.select()
|
||||
.from(targetHealthCheck)
|
||||
.where(eq(targetHealthCheck.targetId, targetId))
|
||||
.limit(1);
|
||||
|
||||
// Parse hcHeaders from JSON string back to array
|
||||
let parsedHcHeaders = null;
|
||||
if (targetHc?.hcHeaders) {
|
||||
try {
|
||||
parsedHcHeaders = JSON.parse(targetHc.hcHeaders);
|
||||
} catch (error) {
|
||||
// If parsing fails, keep as string for backward compatibility
|
||||
parsedHcHeaders = targetHc.hcHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
return response<GetTargetResponse>(res, {
|
||||
data: target[0],
|
||||
data: {
|
||||
...target[0],
|
||||
...targetHc,
|
||||
hcHeaders: parsedHcHeaders
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Target retrieved successfully",
|
||||
|
||||
114
server/routers/target/handleHealthcheckStatusMessage.ts
Normal file
114
server/routers/target/handleHealthcheckStatusMessage.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { db, targets, resources, sites, targetHealthCheck } from "@server/db";
|
||||
import { MessageHandler } from "../ws";
|
||||
import { Newt } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { unknown } from "zod";
|
||||
|
||||
interface TargetHealthStatus {
|
||||
status: string;
|
||||
lastCheck: string;
|
||||
checkCount: number;
|
||||
lastError?: string;
|
||||
config: {
|
||||
id: string;
|
||||
hcEnabled: boolean;
|
||||
hcPath?: string;
|
||||
hcScheme?: string;
|
||||
hcMode?: string;
|
||||
hcHostname?: string;
|
||||
hcPort?: number;
|
||||
hcInterval?: number;
|
||||
hcUnhealthyInterval?: number;
|
||||
hcTimeout?: number;
|
||||
hcHeaders?: any;
|
||||
hcMethod?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface HealthcheckStatusMessage {
|
||||
targets: Record<string, TargetHealthStatus>;
|
||||
}
|
||||
|
||||
export const handleHealthcheckStatusMessage: MessageHandler = async (context) => {
|
||||
const { message, client: c } = context;
|
||||
const newt = c as Newt;
|
||||
|
||||
logger.info("Handling healthcheck status message");
|
||||
|
||||
if (!newt) {
|
||||
logger.warn("Newt not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newt.siteId) {
|
||||
logger.warn("Newt has no site ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = message.data as HealthcheckStatusMessage;
|
||||
|
||||
if (!data.targets) {
|
||||
logger.warn("No targets data in healthcheck status message");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// Process each target status update
|
||||
for (const [targetId, healthStatus] of Object.entries(data.targets)) {
|
||||
logger.debug(`Processing health status for target ${targetId}: ${healthStatus.status}${healthStatus.lastError ? ` (${healthStatus.lastError})` : ''}`);
|
||||
|
||||
// Verify the target belongs to this newt's site before updating
|
||||
// This prevents unauthorized updates to targets from other sites
|
||||
const targetIdNum = parseInt(targetId);
|
||||
if (isNaN(targetIdNum)) {
|
||||
logger.warn(`Invalid target ID: ${targetId}`);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const [targetCheck] = await db
|
||||
.select({
|
||||
targetId: targets.targetId,
|
||||
siteId: targets.siteId
|
||||
})
|
||||
.from(targets)
|
||||
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(targets.targetId, targetIdNum),
|
||||
eq(sites.siteId, newt.siteId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!targetCheck) {
|
||||
logger.warn(`Target ${targetId} not found or does not belong to site ${newt.siteId}`);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update the target's health status in the database
|
||||
await db
|
||||
.update(targetHealthCheck)
|
||||
.set({
|
||||
hcHealth: healthStatus.status
|
||||
})
|
||||
.where(eq(targetHealthCheck.targetId, targetIdNum))
|
||||
.execute();
|
||||
|
||||
logger.debug(`Updated health status for target ${targetId} to ${healthStatus.status}`);
|
||||
successCount++;
|
||||
}
|
||||
|
||||
logger.debug(`Health status update complete: ${successCount} successful, ${errorCount} errors out of ${Object.keys(data.targets).length} targets`);
|
||||
} catch (error) {
|
||||
logger.error("Error processing healthcheck status message:", error);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
@@ -3,3 +3,4 @@ export * from "./createTarget";
|
||||
export * from "./deleteTarget";
|
||||
export * from "./updateTarget";
|
||||
export * from "./listTargets";
|
||||
export * from "./handleHealthcheckStatusMessage";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, sites } from "@server/db";
|
||||
import { db, sites, targetHealthCheck } from "@server/db";
|
||||
import { targets } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
@@ -45,6 +45,20 @@ function queryTargets(resourceId: number) {
|
||||
resourceId: targets.resourceId,
|
||||
siteId: targets.siteId,
|
||||
siteType: sites.type,
|
||||
hcEnabled: targetHealthCheck.hcEnabled,
|
||||
hcPath: targetHealthCheck.hcPath,
|
||||
hcScheme: targetHealthCheck.hcScheme,
|
||||
hcMode: targetHealthCheck.hcMode,
|
||||
hcHostname: targetHealthCheck.hcHostname,
|
||||
hcPort: targetHealthCheck.hcPort,
|
||||
hcInterval: targetHealthCheck.hcInterval,
|
||||
hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval,
|
||||
hcTimeout: targetHealthCheck.hcTimeout,
|
||||
hcHeaders: targetHealthCheck.hcHeaders,
|
||||
hcFollowRedirects: targetHealthCheck.hcFollowRedirects,
|
||||
hcMethod: targetHealthCheck.hcMethod,
|
||||
hcStatus: targetHealthCheck.hcStatus,
|
||||
hcHealth: targetHealthCheck.hcHealth,
|
||||
path: targets.path,
|
||||
pathMatchType: targets.pathMatchType,
|
||||
rewritePath: targets.rewritePath,
|
||||
@@ -52,13 +66,21 @@ function queryTargets(resourceId: number) {
|
||||
})
|
||||
.from(targets)
|
||||
.leftJoin(sites, eq(sites.siteId, targets.siteId))
|
||||
.leftJoin(
|
||||
targetHealthCheck,
|
||||
eq(targetHealthCheck.targetId, targets.targetId)
|
||||
)
|
||||
.where(eq(targets.resourceId, resourceId));
|
||||
|
||||
return baseQuery;
|
||||
}
|
||||
|
||||
type TargetWithParsedHeaders = Omit<Awaited<ReturnType<typeof queryTargets>>[0], 'hcHeaders'> & {
|
||||
hcHeaders: { name: string; value: string; }[] | null;
|
||||
};
|
||||
|
||||
export type ListTargetsResponse = {
|
||||
targets: Awaited<ReturnType<typeof queryTargets>>;
|
||||
targets: TargetWithParsedHeaders[];
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
@@ -113,9 +135,26 @@ export async function listTargets(
|
||||
const totalCountResult = await countQuery;
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
// Parse hcHeaders from JSON string back to array for each target
|
||||
const parsedTargetsList = targetsList.map(target => {
|
||||
let parsedHcHeaders = null;
|
||||
if (target.hcHeaders) {
|
||||
try {
|
||||
parsedHcHeaders = JSON.parse(target.hcHeaders);
|
||||
} catch (error) {
|
||||
// If parsing fails, keep as string for backward compatibility
|
||||
parsedHcHeaders = target.hcHeaders;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...target,
|
||||
hcHeaders: parsedHcHeaders
|
||||
};
|
||||
});
|
||||
|
||||
return response<ListTargetsResponse>(res, {
|
||||
data: {
|
||||
targets: targetsList,
|
||||
targets: parsedTargetsList,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, targetHealthCheck } from "@server/db";
|
||||
import { newts, resources, sites, targets } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -13,6 +13,7 @@ import { addTargets } from "../newt/targets";
|
||||
import { pickPort } from "./helpers";
|
||||
import { isTargetValid } from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { vs } from "@react-email/components";
|
||||
|
||||
const updateTargetParamsSchema = z
|
||||
.object({
|
||||
@@ -27,6 +28,25 @@ const updateTargetBodySchema = z
|
||||
method: z.string().min(1).max(10).optional().nullable(),
|
||||
port: z.number().int().min(1).max(65535).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
hcEnabled: z.boolean().optional().nullable(),
|
||||
hcPath: z.string().min(1).optional().nullable(),
|
||||
hcScheme: z.string().optional().nullable(),
|
||||
hcMode: z.string().optional().nullable(),
|
||||
hcHostname: z.string().optional().nullable(),
|
||||
hcPort: z.number().int().positive().optional().nullable(),
|
||||
hcInterval: z.number().int().positive().min(5).optional().nullable(),
|
||||
hcUnhealthyInterval: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.min(5)
|
||||
.optional()
|
||||
.nullable(),
|
||||
hcTimeout: z.number().int().positive().min(1).optional().nullable(),
|
||||
hcHeaders: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(),
|
||||
hcFollowRedirects: z.boolean().optional().nullable(),
|
||||
hcMethod: z.string().min(1).optional().nullable(),
|
||||
hcStatus: z.number().int().optional().nullable(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
||||
rewritePath: z.string().optional().nullable(),
|
||||
@@ -171,12 +191,43 @@ export async function updateTarget(
|
||||
const [updatedTarget] = await db
|
||||
.update(targets)
|
||||
.set({
|
||||
...parsedBody.data,
|
||||
internalPort
|
||||
siteId: parsedBody.data.siteId,
|
||||
ip: parsedBody.data.ip,
|
||||
method: parsedBody.data.method,
|
||||
port: parsedBody.data.port,
|
||||
internalPort,
|
||||
enabled: parsedBody.data.enabled,
|
||||
path: parsedBody.data.path,
|
||||
pathMatchType: parsedBody.data.pathMatchType
|
||||
})
|
||||
.where(eq(targets.targetId, targetId))
|
||||
.returning();
|
||||
|
||||
let hcHeaders = null;
|
||||
if (parsedBody.data.hcHeaders) {
|
||||
hcHeaders = JSON.stringify(parsedBody.data.hcHeaders);
|
||||
}
|
||||
|
||||
const [updatedHc] = await db
|
||||
.update(targetHealthCheck)
|
||||
.set({
|
||||
hcEnabled: parsedBody.data.hcEnabled || false,
|
||||
hcPath: parsedBody.data.hcPath,
|
||||
hcScheme: parsedBody.data.hcScheme,
|
||||
hcMode: parsedBody.data.hcMode,
|
||||
hcHostname: parsedBody.data.hcHostname,
|
||||
hcPort: parsedBody.data.hcPort,
|
||||
hcInterval: parsedBody.data.hcInterval,
|
||||
hcUnhealthyInterval: parsedBody.data.hcUnhealthyInterval,
|
||||
hcTimeout: parsedBody.data.hcTimeout,
|
||||
hcHeaders: hcHeaders,
|
||||
hcFollowRedirects: parsedBody.data.hcFollowRedirects,
|
||||
hcMethod: parsedBody.data.hcMethod,
|
||||
hcStatus: parsedBody.data.hcStatus
|
||||
})
|
||||
.where(eq(targetHealthCheck.targetId, targetId))
|
||||
.returning();
|
||||
|
||||
if (site.pubKey) {
|
||||
if (site.type == "wireguard") {
|
||||
await addPeer(site.exitNodeId!, {
|
||||
@@ -194,13 +245,17 @@ export async function updateTarget(
|
||||
await addTargets(
|
||||
newt.newtId,
|
||||
[updatedTarget],
|
||||
[updatedHc],
|
||||
resource.protocol,
|
||||
resource.proxyPort
|
||||
);
|
||||
}
|
||||
}
|
||||
return response(res, {
|
||||
data: updatedTarget,
|
||||
data: {
|
||||
...updatedTarget,
|
||||
...updatedHc
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Target updated successfully",
|
||||
|
||||
Reference in New Issue
Block a user