mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-18 13:22:03 +00:00
Merge branch 'rdp-ssh' into dev
This commit is contained in:
@@ -23,7 +23,10 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { build } from "@server/build";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { getUniqueResourceName } from "@server/db/names";
|
||||
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
||||
import {
|
||||
validateAndConstructDomain,
|
||||
checkWildcardDomainConflict
|
||||
} from "@server/lib/domainUtils";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -32,15 +35,58 @@ const createResourceParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
function resolveModeFromLegacyFields(data: {
|
||||
mode?: "http" | "ssh" | "rdp" | "vnc" | "tcp" | "udp";
|
||||
http?: boolean;
|
||||
protocol?: "tcp" | "udp";
|
||||
}): {
|
||||
mode?: "http" | "ssh" | "rdp" | "vnc" | "tcp" | "udp";
|
||||
error?: string;
|
||||
} {
|
||||
if (data.mode) {
|
||||
return { mode: data.mode };
|
||||
}
|
||||
|
||||
if (typeof data.http === "boolean" && data.protocol) {
|
||||
if (data.http && data.protocol === "tcp") {
|
||||
return { mode: "http" };
|
||||
}
|
||||
if (!data.http && data.protocol === "tcp") {
|
||||
return { mode: "tcp" };
|
||||
}
|
||||
if (!data.http && data.protocol === "udp") {
|
||||
return { mode: "udp" };
|
||||
}
|
||||
return {
|
||||
error: "Invalid deprecated http/protocol combination"
|
||||
};
|
||||
}
|
||||
|
||||
return { mode: undefined };
|
||||
}
|
||||
|
||||
const createHttpResourceSchema = z
|
||||
.strictObject({
|
||||
name: z.string().min(1).max(255),
|
||||
subdomain: z.string().nullable().optional(),
|
||||
http: z.boolean(),
|
||||
protocol: z.enum(["tcp", "udp"]),
|
||||
http: z.boolean().optional().openapi({
|
||||
deprecated: true,
|
||||
description:
|
||||
"Deprecated. Use `mode` instead. Legacy compatibility only."
|
||||
}),
|
||||
protocol: z.enum(["tcp", "udp"]).optional().openapi({
|
||||
deprecated: true,
|
||||
description:
|
||||
"Deprecated. Use `mode` instead. Legacy compatibility only."
|
||||
}),
|
||||
domainId: z.string(),
|
||||
stickySession: z.boolean().optional(),
|
||||
postAuthPath: z.string().nullable().optional()
|
||||
postAuthPath: z.string().nullable().optional(),
|
||||
mode: z.enum(["http", "ssh", "rdp", "vnc", "tcp", "udp"]).optional(),
|
||||
// SSH Settings
|
||||
pamMode: z.enum(["passthrough", "push"]).optional(),
|
||||
authDaemonPort: z.int().positive().optional(),
|
||||
authDaemonMode: z.enum(["site", "remote", "native"]).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -60,13 +106,27 @@ const createHttpResourceSchema = z
|
||||
const createRawResourceSchema = z
|
||||
.strictObject({
|
||||
name: z.string().min(1).max(255),
|
||||
http: z.boolean(),
|
||||
protocol: z.enum(["tcp", "udp"]),
|
||||
http: z.boolean().optional().openapi({
|
||||
deprecated: true,
|
||||
description:
|
||||
"Deprecated. Use `mode` instead. Legacy compatibility only."
|
||||
}),
|
||||
protocol: z.enum(["tcp", "udp"]).optional().openapi({
|
||||
deprecated: true,
|
||||
description:
|
||||
"Deprecated. Use `mode` instead. Legacy compatibility only."
|
||||
}),
|
||||
mode: z.enum(["tcp", "udp"]).optional(),
|
||||
proxyPort: z.int().min(1).max(65535)
|
||||
// enableProxy: z.boolean().default(true) // always true now
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const resolved = resolveModeFromLegacyFields(data);
|
||||
if (resolved.error || !resolved.mode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.getRawConfig().flags?.allow_raw_resources) {
|
||||
if (data.proxyPort !== undefined) {
|
||||
return false;
|
||||
@@ -143,17 +203,18 @@ export async function createResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof req.body.http !== "boolean") {
|
||||
const resolvedMode = resolveModeFromLegacyFields(req.body);
|
||||
if (resolvedMode.error) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "http field is required")
|
||||
createHttpError(HttpCode.BAD_REQUEST, resolvedMode.error)
|
||||
);
|
||||
}
|
||||
|
||||
const { http } = req.body;
|
||||
if (resolvedMode.mode) {
|
||||
req.body.mode = resolvedMode.mode;
|
||||
}
|
||||
|
||||
if (http) {
|
||||
return await createHttpResource({ req, res, next }, { orgId });
|
||||
} else {
|
||||
if (typeof req.body.proxyPort === "number") {
|
||||
if (
|
||||
!config.getRawConfig().flags?.allow_raw_resources &&
|
||||
build == "oss"
|
||||
@@ -167,6 +228,17 @@ export async function createResource(
|
||||
}
|
||||
return await createRawResource({ req, res, next }, { orgId });
|
||||
}
|
||||
|
||||
if (req.body.mode) {
|
||||
return await createHttpResource({ req, res, next }, { orgId });
|
||||
} else {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"mode is required when deprecated fields are not provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
@@ -198,7 +270,15 @@ async function createHttpResource(
|
||||
);
|
||||
}
|
||||
|
||||
const { name, domainId, postAuthPath } = parsedBody.data;
|
||||
const {
|
||||
name,
|
||||
domainId,
|
||||
postAuthPath,
|
||||
mode,
|
||||
authDaemonPort,
|
||||
authDaemonMode,
|
||||
pamMode
|
||||
} = parsedBody.data;
|
||||
const subdomain = parsedBody.data.subdomain;
|
||||
const stickySession = parsedBody.data.stickySession;
|
||||
|
||||
@@ -322,8 +402,10 @@ async function createHttpResource(
|
||||
orgId,
|
||||
name,
|
||||
subdomain: finalSubdomain,
|
||||
http: true,
|
||||
protocol: "tcp",
|
||||
mode: mode,
|
||||
pamMode: pamMode,
|
||||
authDaemonMode: authDaemonMode,
|
||||
authDaemonPort: authDaemonPort,
|
||||
ssl: true,
|
||||
stickySession: stickySession,
|
||||
postAuthPath: postAuthPath,
|
||||
@@ -405,7 +487,17 @@ async function createRawResource(
|
||||
);
|
||||
}
|
||||
|
||||
const { name, http, protocol, proxyPort } = parsedBody.data;
|
||||
const { name, proxyPort } = parsedBody.data;
|
||||
const resolvedMode = resolveModeFromLegacyFields(parsedBody.data);
|
||||
if (resolvedMode.error || !resolvedMode.mode) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
resolvedMode.error ||
|
||||
"mode is required when deprecated fields are not provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let resource: Resource | undefined;
|
||||
|
||||
@@ -418,9 +510,8 @@ async function createRawResource(
|
||||
niceId,
|
||||
orgId,
|
||||
name,
|
||||
http,
|
||||
protocol,
|
||||
proxyPort
|
||||
proxyPort,
|
||||
mode: resolvedMode.mode
|
||||
// enableProxy
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function createResourceRule(
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.http) {
|
||||
if (!["http", "ssh", "rdp", "vnc"].includes(resource.mode)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
|
||||
@@ -106,7 +106,7 @@ export async function deleteResource(
|
||||
// [target],
|
||||
[], // deleting the target from newt causes issues because we cant unbind the port. this needs to be fixed in newt before we can do this
|
||||
healthChecksToBeRemoved,
|
||||
deletedResource.protocol,
|
||||
deletedResource.mode === "udp" ? "udp" : "tcp",
|
||||
newt.version
|
||||
);
|
||||
}
|
||||
|
||||
109
server/routers/resource/getBrowserTarget.ts
Normal file
109
server/routers/resource/getBrowserTarget.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { browserGatewayTarget, db } from "@server/db";
|
||||
import { resources, targets } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
const getBrowserTargetSchema = z
|
||||
.object({
|
||||
fullDomain: z.string().min(1, "fullDomain is required")
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type GetBrowserTargetResponse = {
|
||||
ip: string;
|
||||
port: number;
|
||||
authToken: string;
|
||||
orgId: string;
|
||||
resourceId: number;
|
||||
niceId: string;
|
||||
pamMode: "passthrough" | "push" | null;
|
||||
authDaemonMode: "site" | "remote" | "native" | null;
|
||||
};
|
||||
|
||||
export async function getBrowserTarget(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsed = getBrowserTargetSchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsed.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { fullDomain } = parsed.data;
|
||||
|
||||
logger.info(`Retrieving browser target for domain: ${fullDomain}`);
|
||||
|
||||
const [browserTarget] = await db
|
||||
.select({
|
||||
destination: browserGatewayTarget.destination,
|
||||
destinationPort: browserGatewayTarget.destinationPort,
|
||||
authToken: browserGatewayTarget.authToken,
|
||||
resourceId: resources.resourceId,
|
||||
niceId: resources.niceId,
|
||||
orgId: resources.orgId,
|
||||
pamMode: resources.pamMode,
|
||||
authDaemonMode: resources.authDaemonMode
|
||||
})
|
||||
.from(browserGatewayTarget)
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(browserGatewayTarget.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(eq(resources.fullDomain, fullDomain))
|
||||
.limit(1);
|
||||
|
||||
const decryptedAuthToken = decrypt(
|
||||
browserTarget.authToken,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
|
||||
if (!browserTarget) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"No resource found for this domain"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response<GetBrowserTargetResponse>(res, {
|
||||
data: {
|
||||
ip: browserTarget.destination,
|
||||
port: browserTarget.destinationPort,
|
||||
authToken: decryptedAuthToken,
|
||||
pamMode: browserTarget.pamMode,
|
||||
authDaemonMode: browserTarget.authDaemonMode,
|
||||
orgId: browserTarget.orgId,
|
||||
resourceId: browserTarget.resourceId,
|
||||
niceId: browserTarget.niceId
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Browser target retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred while retrieving the browser target"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ export async function getUserResources(
|
||||
ssl: boolean;
|
||||
enabled: boolean;
|
||||
sso: boolean;
|
||||
protocol: string;
|
||||
mode: string;
|
||||
emailWhitelistEnabled: boolean;
|
||||
}> = [];
|
||||
if (accessibleResourceIds.length > 0) {
|
||||
@@ -139,7 +139,7 @@ export async function getUserResources(
|
||||
ssl: resources.ssl,
|
||||
enabled: resources.enabled,
|
||||
sso: resources.sso,
|
||||
protocol: resources.protocol,
|
||||
mode: resources.mode,
|
||||
emailWhitelistEnabled: resources.emailWhitelistEnabled
|
||||
})
|
||||
.from(resources)
|
||||
@@ -323,7 +323,7 @@ export async function getUserResources(
|
||||
hasPincode ||
|
||||
hasWhitelist
|
||||
),
|
||||
protocol: resource.protocol,
|
||||
mode: resource.mode,
|
||||
sso: resource.sso,
|
||||
password: hasPassword,
|
||||
pincode: hasPincode,
|
||||
@@ -339,7 +339,6 @@ export async function getUserResources(
|
||||
name: siteResource.name,
|
||||
destination: siteResource.destination,
|
||||
mode: siteResource.mode,
|
||||
protocol: siteResource.scheme,
|
||||
ssl: siteResource.ssl,
|
||||
fullDomain: siteResource.fullDomain,
|
||||
enabled: siteResource.enabled,
|
||||
@@ -387,14 +386,13 @@ export type GetUserResourcesResponse = {
|
||||
domain: string;
|
||||
enabled: boolean;
|
||||
protected: boolean;
|
||||
protocol: string;
|
||||
mode: string;
|
||||
}>;
|
||||
siteResources: Array<{
|
||||
siteResourceId: number;
|
||||
name: string;
|
||||
destination: string;
|
||||
mode: string;
|
||||
protocol: string | null;
|
||||
tcpPortRangeString: string | null;
|
||||
udpPortRangeString: string | null;
|
||||
disableIcmp: boolean | null;
|
||||
|
||||
@@ -33,3 +33,4 @@ export * from "./removeUserFromResource";
|
||||
export * from "./listAllResourceNames";
|
||||
export * from "./removeEmailFromResourceWhitelist";
|
||||
export * from "./getStatusHistory";
|
||||
export * from "./getBrowserTarget";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
browserGatewayTarget,
|
||||
db,
|
||||
labels,
|
||||
resourceHeaderAuth,
|
||||
@@ -156,8 +157,6 @@ export type ResourceWithTargets = {
|
||||
sso: boolean;
|
||||
pincodeId: number | null;
|
||||
whitelist: boolean;
|
||||
http: boolean;
|
||||
protocol: string;
|
||||
proxyPort: number | null;
|
||||
enabled: boolean;
|
||||
domainId: string | null;
|
||||
@@ -165,6 +164,7 @@ export type ResourceWithTargets = {
|
||||
headerAuthId: number | null;
|
||||
wildcard: boolean;
|
||||
health: string | null;
|
||||
mode: string | null;
|
||||
targets: Array<{
|
||||
targetId: number;
|
||||
ip: string;
|
||||
@@ -193,8 +193,6 @@ function queryResourcesBase() {
|
||||
sso: resources.sso,
|
||||
pincodeId: resourcePincode.pincodeId,
|
||||
whitelist: resources.emailWhitelistEnabled,
|
||||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort,
|
||||
enabled: resources.enabled,
|
||||
domainId: resources.domainId,
|
||||
@@ -203,7 +201,8 @@ function queryResourcesBase() {
|
||||
headerAuthId: resourceHeaderAuth.headerAuthId,
|
||||
headerAuthExtendedCompatibilityId:
|
||||
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
|
||||
health: resources.health
|
||||
health: resources.health,
|
||||
mode: resources.mode
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
@@ -364,7 +363,9 @@ export async function listResources(
|
||||
if (typeof authState !== "undefined") {
|
||||
switch (authState) {
|
||||
case "none":
|
||||
conditions.push(eq(resources.http, false));
|
||||
conditions.push(
|
||||
or(eq(resources.mode, "tcp"), eq(resources.mode, "udp"))
|
||||
);
|
||||
break;
|
||||
case "protected":
|
||||
conditions.push(
|
||||
@@ -520,6 +521,30 @@ export async function listResources(
|
||||
)
|
||||
.leftJoin(sites, eq(targets.siteId, sites.siteId));
|
||||
|
||||
const allBgTargetSites =
|
||||
resourceIdList.length === 0
|
||||
? []
|
||||
: await db
|
||||
.select({
|
||||
resourceId: browserGatewayTarget.resourceId,
|
||||
siteId: browserGatewayTarget.siteId,
|
||||
siteName: sites.name,
|
||||
siteNiceId: sites.niceId,
|
||||
siteOnline: sites.online,
|
||||
siteType: sites.type
|
||||
})
|
||||
.from(browserGatewayTarget)
|
||||
.where(
|
||||
inArray(
|
||||
browserGatewayTarget.resourceId,
|
||||
resourceIdList
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
sites,
|
||||
eq(sites.siteId, browserGatewayTarget.siteId)
|
||||
);
|
||||
|
||||
// avoids TS issues with reduce/never[]
|
||||
const map = new Map<number, ResourceWithTargets>();
|
||||
|
||||
@@ -536,10 +561,9 @@ export async function listResources(
|
||||
sso: row.sso,
|
||||
pincodeId: row.pincodeId,
|
||||
whitelist: row.whitelist,
|
||||
http: row.http,
|
||||
protocol: row.protocol,
|
||||
proxyPort: row.proxyPort,
|
||||
wildcard: row.wildcard,
|
||||
mode: row.mode,
|
||||
enabled: row.enabled,
|
||||
domainId: row.domainId,
|
||||
headerAuthId: row.headerAuthId,
|
||||
@@ -583,6 +607,21 @@ export async function listResources(
|
||||
online: isLocal ? undefined : Boolean(t.siteOnline)
|
||||
});
|
||||
}
|
||||
const bgRaw = allBgTargetSites.filter(
|
||||
(t) => t.resourceId === entry.resourceId
|
||||
);
|
||||
for (const t of bgRaw) {
|
||||
if (typeof t.siteId !== "number" || siteById.has(t.siteId)) {
|
||||
continue;
|
||||
}
|
||||
const isLocal = t.siteType === "local";
|
||||
siteById.set(t.siteId, {
|
||||
siteId: t.siteId,
|
||||
siteName: t.siteName ?? "",
|
||||
siteNiceId: t.siteNiceId ?? "",
|
||||
online: isLocal ? undefined : Boolean(t.siteOnline)
|
||||
});
|
||||
}
|
||||
entry.sites = Array.from(siteById.values());
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ import {
|
||||
import { registry } from "@server/openApi";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
||||
import {
|
||||
validateAndConstructDomain,
|
||||
checkWildcardDomainConflict
|
||||
} from "@server/lib/domainUtils";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -68,7 +71,11 @@ const updateHttpResourceBodySchema = z
|
||||
maintenanceTitle: z.string().max(255).nullable().optional(),
|
||||
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
||||
maintenanceEstimatedTime: z.string().max(100).nullable().optional(),
|
||||
postAuthPath: z.string().nullable().optional()
|
||||
postAuthPath: z.string().nullable().optional(),
|
||||
// SSH settings
|
||||
pamMode: z.enum(["passthrough", "push"]).optional(),
|
||||
authDaemonMode: z.enum(["site", "remote", "native"]).optional(),
|
||||
authDaemonPort: z.int().min(1).max(65535).nullable().optional()
|
||||
})
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
error: "At least one field must be provided for update"
|
||||
@@ -240,7 +247,7 @@ export async function updateResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.http) {
|
||||
if (["http", "ssh", "rdp", "vnc"].includes(resource.mode)) {
|
||||
// HANDLE UPDATING HTTP RESOURCES
|
||||
return await updateHttpResource(
|
||||
{
|
||||
|
||||
@@ -26,7 +26,9 @@ const updateResourceRuleParamsSchema = z.strictObject({
|
||||
const updateResourceRuleSchema = z
|
||||
.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"]).optional(),
|
||||
match: z
|
||||
.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"])
|
||||
.optional(),
|
||||
value: z.string().min(1).optional(),
|
||||
priority: z.int(),
|
||||
enabled: z.boolean().optional()
|
||||
@@ -102,7 +104,7 @@ export async function updateResourceRule(
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.http) {
|
||||
if (!["http", "ssh", "rdp", "vnc"].includes(resource.mode)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
|
||||
Reference in New Issue
Block a user