Merge branch 'free-me-up' into aig

This commit is contained in:
Owen
2026-08-17 14:54:05 -04:00
86 changed files with 3321 additions and 3843 deletions
@@ -0,0 +1,102 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, resources, targets } from "@server/db";
import { eq, and, inArray } 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";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
const getBrowserTargetSchema = z
.object({
fullDomain: z.string().min(1, "fullDomain is required")
})
.strict();
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 [row] = await db
.select({
ip: targets.ip,
port: targets.port,
authToken: targets.authToken,
resourceId: resources.resourceId,
niceId: resources.niceId,
name: resources.name,
orgId: resources.orgId,
pamMode: resources.pamMode,
authDaemonMode: resources.authDaemonMode
})
.from(targets)
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
.where(
and(
eq(resources.fullDomain, fullDomain),
eq(targets.enabled, true),
inArray(targets.mode, ["ssh", "rdp", "vnc"])
)
)
.limit(1);
if (!row) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"No resource found for this domain"
)
);
}
const decryptedAuthToken = row.authToken
? decrypt(row.authToken, config.getRawConfig().server.secret!)
: "";
return response<GetBrowserTargetResponse>(res, {
data: {
ip: row.ip,
port: row.port,
authToken: decryptedAuthToken,
pamMode: row.pamMode,
authDaemonMode: row.authDaemonMode,
orgId: row.orgId,
resourceId: row.resourceId,
niceId: row.niceId,
name: row.name ?? ""
},
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"
)
);
}
}
@@ -1 +1,2 @@
export * from "./types";
export * from "./getBrowserTarget";
@@ -1,9 +1,102 @@
import { db, Transaction } from "@server/db";
import { Certificate, certificates, db, domains } from "@server/db";
import logger from "@server/logger";
import { Transaction } from "@server/db";
import { eq, or, and, like } from "drizzle-orm";
/**
* Checks if a certificate exists for the given domain.
* If not, creates a new certificate in 'pending' state.
* Wildcard certs cover subdomains.
*/
export async function createCertificate(
domainId: string,
domain: string,
trx: Transaction | typeof db
) {
return;
const [domainRecord] = await trx
.select()
.from(domains)
.where(eq(domains.domainId, domainId))
.limit(1);
if (!domainRecord) {
throw new Error(`Domain with ID ${domainId} not found`);
}
let existing: Certificate[] = [];
if (domainRecord.type == "ns" || domainRecord.type == "wildcard") {
const domainLevelDown = domain.split(".").slice(1).join(".");
const wildcardPrefixed = `*.${domainLevelDown}`;
existing = await trx
.select()
.from(certificates)
.where(
and(
eq(certificates.domainId, domainId),
or(
eq(certificates.domain, domain),
and(
eq(certificates.wildcard, true),
or(
eq(certificates.domain, domainLevelDown),
eq(certificates.domain, wildcardPrefixed)
)
)
)
)
);
} else {
// For non-NS domains, we only match exact domain names
existing = await trx
.select()
.from(certificates)
.where(
and(
eq(certificates.domainId, domainId),
eq(certificates.domain, domain) // exact match for non-NS domains
)
);
}
if (existing.length > 0) {
logger.info(`Certificate already exists for domain ${domain}`);
return;
}
let domainToWrite = domain;
if (
domainRecord.type == "wildcard" && // this is to fix the wildcard certs for traefik in self hosted NOT ON THE CLOUD
domainRecord.preferWildcardCert &&
!domain.startsWith("*.")
) {
// in this case traefik is going to generate a domain one level down so we need to store it that way
const parts = domain.split(".");
if (parts.length > 2) {
domainToWrite = parts.slice(1).join(".");
domainToWrite = `*.${domainToWrite}`;
}
} else if (domainRecord.type == "ns") {
if (domain == domainRecord.baseDomain) {
domainToWrite = domainRecord.baseDomain;
} else {
const parts = domain.split(".");
if (parts.length > 2) {
domainToWrite = parts.slice(1).join(".");
}
}
}
// No cert found, create a new one in pending state
await trx.insert(certificates).values({
domain: domainToWrite,
domainId,
wildcard:
domainRecord.type == "ns" ||
(domainRecord.type == "wildcard" &&
domainRecord.preferWildcardCert), // we can only create wildcard certs for NS domains
status: "pending",
updatedAt: Math.floor(Date.now() / 1000),
createdAt: Math.floor(Date.now() / 1000)
});
}
@@ -0,0 +1,204 @@
import { certificates, db, domainNamespaces, domains, orgDomains } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import { type GetBatchedCertificateResponse } from "@server/routers/certificates/types";
import HttpCode from "@server/types/HttpCode";
import { and, eq, inArray, isNotNull, or } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const getCertificateParamSchema = z.strictObject({
orgId: z.string()
});
const getCertificateQuerySchema = z.object({
domains: z.preprocess(
(val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
if (Array.isArray(val)) {
return val;
}
// the array is returned as this
if (typeof val === "string") {
return val.split(",");
}
return undefined;
},
z.array(z.string().min(1).max(255))
)
});
async function query(orgId: string, domainList: string[]) {
// Try to get CNAME certificates first
const existingCertificates = await db
.select({
certId: certificates.certId,
domain: certificates.domain,
wildcard: certificates.wildcard,
status: certificates.status,
expiresAt: certificates.expiresAt,
lastRenewalAttempt: certificates.lastRenewalAttempt,
createdAt: certificates.createdAt,
updatedAt: certificates.updatedAt,
errorMessage: certificates.errorMessage,
renewalCount: certificates.renewalCount,
domainId: domains.domainId,
domainType: domains.type
})
.from(certificates)
.innerJoin(domains, eq(certificates.domainId, domains.domainId))
.leftJoin(
orgDomains,
and(
eq(domains.domainId, orgDomains.domainId),
eq(orgDomains.orgId, orgId)
)
)
.leftJoin(
domainNamespaces,
eq(domains.domainId, domainNamespaces.domainId)
)
.where(
and(
inArray(certificates.domain, domainList),
// Namespace domains are shared across all orgs, so they skip
// the org-ownership check (mirrors verifyCertificateAccess).
or(
isNotNull(orgDomains.orgId),
isNotNull(domainNamespaces.domainNamespaceId)
)
)
);
// All non resolved domain certificates might be `ns` or `wildcard`,
// which means exact domain certificates do not exist
const foundDomains = new Set(
existingCertificates.map((cert) => cert.domain)
);
const domainsWithMissingCertificates = domainList.filter(
(domain) => !foundDomains.has(domain)
);
if (domainsWithMissingCertificates.length > 0) {
const domainLevelDownSet = new Set<string>();
const wildcardDomainSet = new Set<string>();
for (const domain of domainsWithMissingCertificates) {
const domainLevelDown = domain.split(".").slice(1).join(".");
const wildcardPrefixed = `*.${domainLevelDown}`;
domainLevelDownSet.add(domainLevelDown);
wildcardDomainSet.add(wildcardPrefixed);
}
// Need to map the certificates to each domain
const wildcardCertificates = await db
.select({
certId: certificates.certId,
domain: certificates.domain,
wildcard: certificates.wildcard,
status: certificates.status,
expiresAt: certificates.expiresAt,
lastRenewalAttempt: certificates.lastRenewalAttempt,
createdAt: certificates.createdAt,
updatedAt: certificates.updatedAt,
errorMessage: certificates.errorMessage,
renewalCount: certificates.renewalCount,
domainId: domains.domainId,
domainType: domains.type
})
.from(certificates)
.innerJoin(domains, eq(certificates.domainId, domains.domainId))
.leftJoin(
orgDomains,
and(
eq(domains.domainId, orgDomains.domainId),
eq(orgDomains.orgId, orgId)
)
)
.leftJoin(
domainNamespaces,
eq(domains.domainId, domainNamespaces.domainId)
)
.where(
and(
eq(certificates.wildcard, true),
or(
inArray(certificates.domain, [...domainLevelDownSet]),
inArray(certificates.domain, [...wildcardDomainSet])
),
or(
isNotNull(orgDomains.orgId),
isNotNull(domainNamespaces.domainNamespaceId)
)
)
);
existingCertificates.push(...wildcardCertificates);
}
const certificateMap: Record<string, any> = {};
for (const domain of domainList) {
const domainLevelDown = domain.split(".").slice(1).join(".");
const wildcardPrefixed = `*.${domainLevelDown}`;
certificateMap[domain] =
existingCertificates.find(
(cert) =>
cert.domain === domain ||
cert.domain === domainLevelDown ||
cert.domain === wildcardPrefixed
) ?? null;
}
return certificateMap;
}
export async function getBatchedCertificates(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = getCertificateParamSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const parsedQuery = getCertificateQuerySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { domains } = parsedQuery.data;
const cert = await query(orgId, domains);
return response<GetBatchedCertificateResponse>(res, {
data: cert,
success: true,
error: false,
message: "Certificates retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -0,0 +1,163 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { certificates, db, domains } from "@server/db";
import { eq, and, or, like } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { registry } from "@server/openApi";
import { GetCertificateResponse } from "@server/routers/certificates/types";
const getCertificateSchema = z.strictObject({
domainId: z.string(),
domain: z.string().min(1).max(255),
orgId: z.string()
});
async function query(domainId: string, domain: string) {
const [domainRecord] = await db
.select()
.from(domains)
.where(eq(domains.domainId, domainId))
.limit(1);
if (!domainRecord) {
throw new Error(`Domain with ID ${domainId} not found`);
}
const domainType = domainRecord.type;
let existing: any[] = [];
if (domainRecord.type == "ns" || domainRecord.type == "wildcard") {
const domainLevelDown = domain.split(".").slice(1).join(".");
const wildcardPrefixed = `*.${domainLevelDown}`;
existing = await db
.select({
certId: certificates.certId,
domain: certificates.domain,
wildcard: certificates.wildcard,
status: certificates.status,
expiresAt: certificates.expiresAt,
lastRenewalAttempt: certificates.lastRenewalAttempt,
createdAt: certificates.createdAt,
updatedAt: certificates.updatedAt,
errorMessage: certificates.errorMessage,
renewalCount: certificates.renewalCount
})
.from(certificates)
.where(
and(
eq(certificates.domainId, domainId),
or(
eq(certificates.domain, domain),
and(
eq(certificates.wildcard, true),
or(
eq(certificates.domain, domainLevelDown),
eq(certificates.domain, wildcardPrefixed)
)
)
)
)
);
} else {
// For non-NS domains, we only match exact domain names
existing = await db
.select({
certId: certificates.certId,
domain: certificates.domain,
wildcard: certificates.wildcard,
status: certificates.status,
expiresAt: certificates.expiresAt,
lastRenewalAttempt: certificates.lastRenewalAttempt,
createdAt: certificates.createdAt,
updatedAt: certificates.updatedAt,
errorMessage: certificates.errorMessage,
renewalCount: certificates.renewalCount
})
.from(certificates)
.where(
and(
eq(certificates.domainId, domainId),
eq(certificates.domain, domain) // exact match for non-NS domains
)
);
}
return existing.length > 0 ? { ...existing[0], domainType } : null;
}
registry.registerPath({
method: "get",
path: "/org/{orgId}/certificate/{domainId}/{domain}",
description: "Get a certificate by domain.",
tags: ["Certificate"],
request: {
params: z.object({
domainId: z.string(),
domain: z.string().min(1).max(255),
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
export async function getCertificate(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = getCertificateSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { domainId, domain } = parsedParams.data;
const cert = await query(domainId, domain);
if (!cert) {
logger.warn(`Certificate not found for domain: ${domainId}`);
return next(
createHttpError(HttpCode.NOT_FOUND, "Certificate not found")
);
}
return response<GetCertificateResponse>(res, {
data: cert,
success: true,
error: false,
message: "Certificate retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
+5
View File
@@ -0,0 +1,5 @@
export * from "./getCertificate";
export * from "./restartCertificate";
export * from "./syncCertToNewts";
export * from "./getBatchedCertificates";
export * from "./createCertificate";
@@ -0,0 +1,111 @@
import { certificates, db } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import { registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
import { eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const restartCertificateParamsSchema = z.strictObject({
certId: z.coerce.number().int().positive(),
orgId: z.string()
});
registry.registerPath({
method: "post",
path: "/certificate/{certId}",
description: "Restart a certificate by ID.",
tags: ["Certificate"],
request: {
params: z.object({
certId: z.coerce.number().int().positive(),
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
export async function restartCertificate(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = restartCertificateParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { certId } = parsedParams.data;
// get the certificate by ID
const [cert] = await db
.select()
.from(certificates)
.where(eq(certificates.certId, certId))
.limit(1);
if (!cert) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Certificate not found")
);
}
if (cert.status != "failed" && cert.status != "expired") {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Certificate is already valid, no need to restart"
)
);
}
// update the certificate status to 'pending'
await db
.update(certificates)
.set({
status: "pending",
errorMessage: null,
lastRenewalAttempt: Math.floor(Date.now() / 1000)
})
.where(eq(certificates.certId, certId));
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Certificate restarted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -0,0 +1,55 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { pushCertUpdateToAffectedNewts } from "@server/lib/acmeCertSync";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { fromError } from "zod-validation-error";
const bodySchema = z.object({
domain: z.string().min(1),
domainId: z.string().nullable().optional().default(null)
});
export async function syncCertToNewts(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const parsed = bodySchema.safeParse(req.body);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsed.error).toString()
)
);
}
const { domain, domainId } = parsed.data;
logger.debug(
`syncCertToNewts: received request to push cert update for domain "${domain}" (domainId: ${domainId ?? "none"})`
);
try {
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
res.status(HttpCode.OK).json({
data: null,
success: true,
error: false,
message: `Certificate update pushed to affected newts for domain "${domain}"`
});
} catch (err) {
logger.error(
`syncCertToNewts: error pushing cert update for domain "${domain}": ${err}`
);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to push certificate update to affected newts"
)
);
}
}
+51 -2
View File
@@ -20,6 +20,7 @@ import * as logs from "./auditLogs";
import * as launcher from "./launcher";
import * as newt from "./newt";
import * as olm from "./olm";
import * as ssh from "./ssh";
import * as serverInfo from "./serverInfo";
import HttpCode from "@server/types/HttpCode";
import {
@@ -49,19 +50,21 @@ import {
verifyAiProviderAccess,
verifyAiModelAccess,
verifyAiBudgetAccess,
verifyVirtualApiKeyAccess
verifyVirtualApiKeyAccess,
logActionAudit,
verifyCertificateAccess
} from "@server/middlewares";
import { ActionsEnum } from "@server/auth/actions";
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
import createHttpError from "http-errors";
import { build } from "@server/build";
import { createStore } from "#dynamic/lib/rateLimitStore";
import { logActionAudit } from "#dynamic/middlewares";
import { checkRoundTripMessage } from "./ws";
import * as labels from "@server/routers/labels";
import * as aiProvider from "@server/routers/aiProvider";
import * as aiBudget from "@server/routers/aiBudget";
import * as virtualApiKey from "@server/routers/virtualApiKey";
import * as certificates from "@server/routers/certificates";
// Root routes
export const unauthenticated = Router();
@@ -1863,6 +1866,52 @@ authenticated.put(
labels.detachLabelFromItem
);
authenticated.post(
"/org/:orgId/ssh/sign-key",
verifyOrgAccess,
verifyLimits,
// verifyUserHasAction(ActionsEnum.signSshKey), // this check happens inside of the function now
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
ssh.signSshKey
);
authenticated.get(
"/client/:clientId/verify-associations-cache",
verifyClientAccess,
client.verifyClientAssociationsCache
);
authenticated.post(
"/client/:clientId/rebuild-associations-cache",
verifyClientAccess,
client.rebuildClientAssociationsCacheRoute
);
authenticated.get(
"/org/:orgId/certificate/:domainId/:domain",
verifyOrgAccess,
verifyCertificateAccess,
verifyUserHasAction(ActionsEnum.getCertificate),
certificates.getCertificate
);
authenticated.get(
"/org/:orgId/batched-certificates",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getCertificate),
certificates.getBatchedCertificates
);
authenticated.post(
"/org/:orgId/certificate/:certId/restart",
verifyOrgAccess,
verifyCertificateAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.restartCertificate),
logActionAudit(ActionsEnum.restartCertificate),
certificates.restartCertificate
);
// Auth routes
export const authRouter = Router();
unauthenticated.use("/auth", authRouter);
+20 -3
View File
@@ -1,16 +1,20 @@
import { Router } from "express";
import * as gerbil from "@server/routers/gerbil";
import * as traefik from "@server/routers/traefik";
import * as resource from "./resource";
import * as badger from "./badger";
import * as resource from "@server/routers/resource";
import * as badger from "@server/routers/badger";
import * as auth from "@server/routers/auth";
import * as supporterKey from "@server/routers/supporterKey";
import * as idp from "@server/routers/idp";
import * as ssh from "@server/routers/ssh";
import HttpCode from "@server/types/HttpCode";
import {
verifyResourceAccess,
verifySessionUserMiddleware
verifySessionUserMiddleware,
verifyUserFromResourceSessionMiddleware
} from "@server/middlewares";
import * as ws from "@server/routers/ws";
import * as browserTarget from "@server/routers/browserGatewayTarget";
// Root routes
export const internalRouter = Router();
@@ -42,6 +46,12 @@ internalRouter.get("/idp", idp.listIdps);
internalRouter.get("/idp/:idpId", idp.getIdp);
internalRouter.post(
"/org/:orgId/ssh/sign-key",
verifyUserFromResourceSessionMiddleware,
ssh.signSshKey
);
// Gerbil routes
const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter);
@@ -64,3 +74,10 @@ badgerRouter.post("/verify-session", badger.verifyResourceSession);
badgerRouter.post("/exchange-session", badger.exchangeSession);
internalRouter.get("/resource/browser-target", browserTarget.getBrowserTarget);
internalRouter.get(
"/ws/round-trip-message/:messageId",
verifyUserFromResourceSessionMiddleware,
ws.checkRoundTripMessage
);
-1
View File
@@ -33,7 +33,6 @@ import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsFor
import { doCidrsOverlap } from "@server/lib/ip";
import { generateCA } from "@server/lib/sshCA";
import { encrypt } from "@server/lib/crypto";
import { generateId } from "@server/auth/sessions/app";
const validOrgIdRegex = /^[a-z0-9_]+(-[a-z0-9_]+)*$/;
+3 -20
View File
@@ -24,14 +24,14 @@ import logger from "@server/logger";
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
import config from "@server/lib/config";
import { OpenAPITags, registry } from "@server/openApi";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { createCertificate } from "@server/routers/certificates";
import {
validateAndConstructDomain,
checkWildcardDomainConflict
} from "@server/lib/domainUtils";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import {
getUniqueResourceName,
getUniqueResourcePolicyName
@@ -454,21 +454,6 @@ async function createHttpResource(
}
}
if (
["ssh", "rdp", "vnc"].includes(effectiveMode) &&
!isLicensedOrSubscribed(
orgId!,
tierMatrix[TierFeature.AdvancedPublicResources]
)
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Your current subscription does not support browser gateway resources. Please upgrade to access this feature."
)
);
}
// Validate domain and construct full domain
const domainResult = await validateAndConstructDomain(
domainId,
@@ -647,9 +632,7 @@ async function createHttpResource(
);
}
if (build !== "oss") {
await createCertificate(domainId, fullDomain, db);
}
await createCertificate(domainId, fullDomain, db);
return response<CreateResourceResponse>(res, {
data: resource,
+2 -4
View File
@@ -38,7 +38,7 @@ import {
} from "@server/lib/schemas";
import { registry } from "@server/openApi";
import { OpenAPITags } from "@server/openApi";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { createCertificate } from "@server/routers/certificates/createCertificate";
import {
validateAndConstructDomain,
checkWildcardDomainConflict
@@ -678,9 +678,7 @@ async function updateHttpResource(
// Update the subdomain in the update data
updateData.subdomain = finalSubdomain;
if (build != "oss") {
await createCertificate(domainId, fullDomain, db);
}
await createCertificate(domainId, fullDomain, db);
}
let headers = undefined;
+1 -1
View File
@@ -135,7 +135,7 @@ export async function createRole(
const isLicensedSshPam = await isLicensedOrSubscribed(
orgId,
tierMatrix.advancedPrivateResources
tierMatrix.roleBasedSSHControls
);
const roleInsertValues: Record<string, unknown> = {
name: roleData.name,
+1 -1
View File
@@ -144,7 +144,7 @@ export async function updateRole(
const isLicensedSshPam = await isLicensedOrSubscribed(
orgId,
tierMatrix.advancedPrivateResources
tierMatrix.roleBasedSSHControls
);
if (!isLicensedSshPam) {
delete updateData.sshSudoMode;
@@ -10,8 +10,7 @@ import {
SiteResource,
siteResources,
sites,
userSiteResources,
primaryDb
userSiteResources
} from "@server/db";
import { getUniqueSiteResourceName } from "@server/db/names";
import {
@@ -19,8 +18,6 @@ import {
isIpInCidr,
portRangeStringSchema
} from "@server/lib/ip";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import {
rebuildClientAssociationsFromSiteResource,
isOrgRebuildRateLimited
@@ -35,7 +32,7 @@ import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { createCertificate } from "@server/routers/certificates/createCertificate";
import { build } from "@server/build";
import { usageService } from "@server/lib/billing/usageService";
import { LimitId } from "@server/lib/billing";
@@ -408,21 +405,6 @@ export async function createSiteResource(
}
}
if (mode == "http") {
const hasHttpFeature = await isLicensedOrSubscribed(
orgId,
tierMatrix[TierFeature.AdvancedPrivateResources]
);
if (!hasHttpFeature) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"HTTP private resources are not included in your current plan. Please upgrade."
)
);
}
}
// Verify the site exists and belongs to the org
const sitesToAssign = await db
.select()
@@ -557,20 +539,6 @@ export async function createSiteResource(
}
}
const isLicensedSshPam = await isLicensedOrSubscribed(
orgId,
tierMatrix.advancedPrivateResources
);
if (mode == "ssh" && !isLicensedSshPam) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"SSH private resources are not included in your current plan. Please upgrade."
)
);
}
let updatedNiceId = niceId;
if (!niceId) {
updatedNiceId = await getUniqueSiteResourceName(orgId);
@@ -646,13 +614,13 @@ export async function createSiteResource(
fullDomain,
requiresExitNodeConnection: mode === "inference" // in the future we might want to have different modes that do this
};
if (isLicensedSshPam) {
if (authDaemonPort !== undefined)
insertValues.authDaemonPort = authDaemonPort;
if (authDaemonMode !== undefined)
insertValues.authDaemonMode = authDaemonMode;
if (pamMode !== undefined) insertValues.pamMode = pamMode;
}
if (authDaemonPort !== undefined)
insertValues.authDaemonPort = authDaemonPort;
if (authDaemonMode !== undefined)
insertValues.authDaemonMode = authDaemonMode;
if (pamMode !== undefined) insertValues.pamMode = pamMode;
[newSiteResource] = await trx
.insert(siteResources)
.values(insertValues)
@@ -771,8 +739,7 @@ export async function createSiteResource(
ssl &&
(mode === "http" || mode == "inference") &&
domainId &&
fullDomain &&
build != "oss"
fullDomain
) {
await createCertificate(domainId, fullDomain, db);
}
@@ -10,8 +10,6 @@ import {
sites,
userSiteResources
} from "@server/db";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import response from "@server/lib/response";
import { eq, and, ne, inArray } from "drizzle-orm";
@@ -362,26 +360,6 @@ export async function updateSiteResource(
);
}
if (mode == "http") {
const hasHttpFeature = await isLicensedOrSubscribed(
existingSiteResource.orgId,
tierMatrix[TierFeature.AdvancedPrivateResources]
);
if (!hasHttpFeature) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"HTTP private resources are not included in your current plan. Please upgrade."
)
);
}
}
const isLicensedSshPam = await isLicensedOrSubscribed(
existingSiteResource.orgId,
tierMatrix.advancedPrivateResources
);
const [org] = await db
.select()
.from(orgs)
@@ -541,10 +519,9 @@ export async function updateSiteResource(
await db.transaction(async (trx) => {
// Update the site resource
const sshPamSet =
isLicensedSshPam &&
(authDaemonPort !== undefined ||
authDaemonMode !== undefined ||
pamMode !== undefined)
authDaemonPort !== undefined ||
authDaemonMode !== undefined ||
pamMode !== undefined
? {
...(authDaemonPort !== undefined && {
authDaemonPort
@@ -741,8 +718,7 @@ export async function updateSiteResource(
ssl &&
(mode === "http" || mode == "inference") &&
domainId &&
fullDomain &&
build != "oss"
fullDomain
) {
await createCertificate(domainId, fullDomain, db);
}
+1
View File
@@ -0,0 +1 @@
export * from "./signSshKey";
+799
View File
@@ -0,0 +1,799 @@
import { Request, Response, NextFunction } from "express";
import { randomInt } from "crypto";
import { z } from "zod";
import {
actionAuditLog,
db,
logsDb,
newts,
roles,
roleActions,
rolePolicies,
roleResources,
roleSiteResources,
resources,
roundTripMessageTracker,
siteResources,
siteNetworks,
targets,
userOrgs,
sites,
Resource,
SiteResource
} from "@server/db";
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { and, eq, inArray, isNull, or } from "drizzle-orm";
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
import config from "@server/lib/config";
import { sendToClient } from "#dynamic/routers/ws";
import { ActionsEnum } from "@server/auth/actions";
import type { SignSshKeyResponse } from "@server/routers/ssh/types";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
});
const bodySchema = z
.strictObject({
publicKey: z.string().nonempty(),
resourceId: z.number().int().positive().optional(),
resource: z.string().nonempty().optional(), // this is either the nice id or the alias
username: z.string().nonempty().optional(),
type: z.enum(["public", "private"]).default("private")
})
.refine(
(data) => {
const fields = [data.resourceId, data.resource];
const definedFields = fields.filter((field) => field !== undefined);
return definedFields.length === 1;
},
{
message:
"Exactly one of resourceId, niceId, or alias must be provided"
}
);
export async function signSshKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const {
publicKey,
resourceId,
type,
resource: resourceQueryString,
username
} = parsedBody.data;
const userId = req.user?.userId;
const roleIds = req.userOrgRoleIds ?? [];
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (roleIds.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User has no role in organization"
)
);
}
const [userOrg] = await db
.select()
.from(userOrgs)
.where(and(eq(userOrgs.orgId, orgId), eq(userOrgs.userId, userId)))
.limit(1);
if (!userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not belong to the specified organization"
)
);
}
const roleActionPermission = await db
.select({ roleId: roleActions.roleId })
.from(roleActions)
.where(
and(
eq(roleActions.actionId, ActionsEnum.signSshKey),
inArray(roleActions.roleId, roleIds),
eq(roleActions.orgId, orgId)
)
)
.limit(1);
if (roleActionPermission.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission perform this action"
)
);
}
// Get and decrypt the org's CA keys
const caKeys = await getOrgCAKeys(
orgId,
config.getRawConfig().server.secret!
);
if (!caKeys) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"SSH CA not configured for this organization"
)
);
}
let matchingResources: SiteResource[] | Resource[] = [];
// Verify the resource exists and belongs to the org.
// Build the where clause dynamically based on which field is provided.
let whereClause;
if (resourceId !== undefined) {
whereClause =
type === "private"
? eq(siteResources.siteResourceId, resourceId)
: eq(resources.resourceId, resourceId);
} else if (resourceQueryString !== undefined) {
whereClause =
type === "private"
? or(
eq(siteResources.niceId, resourceQueryString),
eq(siteResources.alias, resourceQueryString)
)
: eq(resources.niceId, resourceQueryString);
} else {
// This should never happen due to the schema validation, but TypeScript doesn't know that.
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"One of resourceId, niceId, or alias must be provided"
)
);
}
if (type === "private") {
matchingResources = await db
.select()
.from(siteResources)
.where(and(whereClause, eq(siteResources.orgId, orgId)));
} else {
matchingResources = await db
.select()
.from(resources)
.where(and(whereClause, eq(resources.orgId, orgId)));
}
if (!matchingResources || matchingResources.length === 0) {
return next(
createHttpError(HttpCode.NOT_FOUND, `Resource not found`)
);
}
if (matchingResources.length > 1) {
// error but this should not happen because the nice id cant contain a dot and the alias has to have a dot and both have to be unique within the org so there should never be multiple matches
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Multiple resources found matching the criteria`
)
);
}
const resource = matchingResources[0];
const normalizedResourceId =
type === "private"
? (resource as SiteResource).siteResourceId
: (resource as Resource).resourceId;
if (resource.orgId !== orgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Resource does not belong to the specified organization"
)
);
}
if (resource.mode == "cidr") {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"SSHing is not supported for CIDR resources"
)
);
}
// Check if the user has access to the resource
const hasAccess =
type === "private"
? await canUserAccessSiteResource({
userId: userId,
resourceId: (resource as SiteResource).siteResourceId,
roleIds
})
: await canUserAccessResource({
userId: userId,
resourceId: (resource as Resource).resourceId,
roleIds
});
if (!hasAccess) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this resource"
)
);
}
const siteAgentHostMap = new Map<number, string>();
let siteIds: number[] = [];
if (type === "private") {
const privateResource = resource as SiteResource;
const sitesFromNetworks = await db
.select({ siteId: siteNetworks.siteId })
.from(siteNetworks)
.where(eq(siteNetworks.networkId, privateResource.networkId!));
siteIds = sitesFromNetworks.map((site) => site.siteId);
for (const siteId of siteIds) {
if (privateResource.destination) {
siteAgentHostMap.set(siteId, privateResource.destination);
}
}
} else {
const publicResource = resource as Resource;
const targetRows = await db
.select({
siteId: targets.siteId,
ip: targets.ip
})
.from(targets)
.where(
and(
eq(targets.resourceId, publicResource.resourceId),
eq(targets.enabled, true),
eq(targets.mode, "ssh")
)
);
if (targetRows.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"No enabled targets found for the resource"
)
);
}
for (const targetRow of targetRows) {
if (!siteAgentHostMap.has(targetRow.siteId)) {
siteAgentHostMap.set(targetRow.siteId, targetRow.ip);
}
}
siteIds = Array.from(siteAgentHostMap.keys());
}
let expiresIn: number | undefined;
let messageIds: number[] = [];
let cert:
| {
certificate: string;
keyId: string;
validPrincipals: string[];
validAfter: Date;
validBefore: Date;
}
| undefined;
// if the pam mode is push then we generate the user's pam username and use that or pull it from the userOrgs table
// if the mode is passthrough then just use what was provided because the user will log in themselves
let usernameToUse;
if (resource.pamMode === "push") {
if (!userOrg.pamUsername) {
if (req.user?.email) {
// Extract username from email (first part before @)
usernameToUse = req.user?.email
.split("@")[0]
.replace(/[^a-zA-Z0-9_-]/g, "");
if (!usernameToUse) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Unable to extract username from email"
)
);
}
} else if (req.user?.username) {
usernameToUse = req.user.username;
// We need to clean out any spaces or special characters from the username to ensure it's valid for SSH certificates
usernameToUse = usernameToUse.replace(
/[^a-zA-Z0-9_-]/g,
"-"
);
if (!usernameToUse) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Username is not valid for SSH certificate"
)
);
}
} else {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"User does not have a valid email or username for SSH certificate"
)
);
}
// prefix with p-
usernameToUse = `p-${usernameToUse}`;
// check if we have a existing user in this org with the same
const [existingUserWithSameName] = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.orgId, orgId),
eq(userOrgs.pamUsername, usernameToUse)
)
)
.limit(1);
if (existingUserWithSameName) {
let foundUniqueUsername = false;
for (let attempt = 0; attempt < 20; attempt++) {
const randomNum = randomInt(0, 101); // 0 to 100
const candidateUsername = `${usernameToUse}${randomNum}`;
const [existingUser] = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.orgId, orgId),
eq(userOrgs.pamUsername, candidateUsername)
)
)
.limit(1);
if (!existingUser) {
usernameToUse = candidateUsername;
foundUniqueUsername = true;
break;
}
}
if (!foundUniqueUsername) {
return next(
createHttpError(
HttpCode.CONFLICT,
"Unable to generate a unique username for SSH certificate"
)
);
}
}
await db
.update(userOrgs)
.set({ pamUsername: usernameToUse })
.where(
and(
eq(userOrgs.orgId, orgId),
eq(userOrgs.userId, userId)
)
);
} else {
usernameToUse = userOrg.pamUsername;
}
type RoleSshMeta = {
roleId: number;
sshSudoCommands: string | null;
sshUnixGroups: string | null;
sshCreateHomeDir: boolean | null;
sshSudoMode: string | null;
};
let roleRows: RoleSshMeta[] = [];
if (type === "private") {
roleRows = await db
.select({
roleId: roles.roleId,
sshSudoCommands: roles.sshSudoCommands,
sshUnixGroups: roles.sshUnixGroups,
sshCreateHomeDir: roles.sshCreateHomeDir,
sshSudoMode: roles.sshSudoMode
})
.from(roles)
.innerJoin(
roleSiteResources,
eq(roleSiteResources.roleId, roles.roleId)
)
.where(
and(
inArray(roles.roleId, roleIds),
eq(
roleSiteResources.siteResourceId,
(resource as SiteResource).siteResourceId
)
)
);
} else {
const publicResourceId = (resource as Resource).resourceId;
const [directRoleRows, policyRoleRows] = await Promise.all([
db
.select({
roleId: roles.roleId,
sshSudoCommands: roles.sshSudoCommands,
sshUnixGroups: roles.sshUnixGroups,
sshCreateHomeDir: roles.sshCreateHomeDir,
sshSudoMode: roles.sshSudoMode
})
.from(roles)
.innerJoin(
roleResources,
eq(roleResources.roleId, roles.roleId)
)
.where(
and(
inArray(roles.roleId, roleIds),
eq(roleResources.resourceId, publicResourceId)
)
),
db
.select({
roleId: roles.roleId,
sshSudoCommands: roles.sshSudoCommands,
sshUnixGroups: roles.sshUnixGroups,
sshCreateHomeDir: roles.sshCreateHomeDir,
sshSudoMode: roles.sshSudoMode
})
.from(roles)
.innerJoin(
rolePolicies,
eq(rolePolicies.roleId, roles.roleId)
)
.innerJoin(
resources,
or(
eq(
resources.resourcePolicyId,
rolePolicies.resourcePolicyId
),
and(
isNull(resources.resourcePolicyId),
eq(
resources.defaultResourcePolicyId,
rolePolicies.resourcePolicyId
)
)
)
)
.where(
and(
inArray(roles.roleId, roleIds),
eq(resources.resourceId, publicResourceId)
)
)
]);
const uniqueByRoleId = new Map<number, RoleSshMeta>();
for (const row of [...directRoleRows, ...policyRoleRows]) {
if (!uniqueByRoleId.has(row.roleId)) {
uniqueByRoleId.set(row.roleId, row);
}
}
roleRows = Array.from(uniqueByRoleId.values());
}
const parsedSudoCommands: string[] = [];
const parsedGroupsSet = new Set<string>();
let homedir: boolean | null = null;
const sudoModeOrder = { none: 0, commands: 1, full: 2 };
let sudoMode: "none" | "commands" | "full" = "none";
for (const roleRow of roleRows) {
try {
const cmds = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
if (Array.isArray(cmds)) parsedSudoCommands.push(...cmds);
} catch {
// skip
}
try {
const grps = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
if (Array.isArray(grps))
grps.forEach((g: string) => parsedGroupsSet.add(g));
} catch {
// skip
}
if (roleRow?.sshCreateHomeDir === true) homedir = true;
const m = roleRow?.sshSudoMode ?? "none";
if (
sudoModeOrder[m as keyof typeof sudoModeOrder] >
sudoModeOrder[sudoMode]
) {
sudoMode = m as "none" | "commands" | "full";
}
}
const parsedGroups = Array.from(parsedGroupsSet);
if (homedir === null && roleRows.length > 0) {
homedir = roleRows[0].sshCreateHomeDir ?? null;
}
// Sign the public key
const now = BigInt(Math.floor(Date.now() / 1000));
// only valid for 5 minutes
const validFor = 300n;
expiresIn = Number(validFor); // seconds
cert = signPublicKey(caKeys.privateKeyPem, publicKey, {
keyId: `${usernameToUse}@${resource.niceId}`,
validPrincipals: [usernameToUse, resource.niceId],
validAfter: now - 60n, // Start 1 min ago for clock skew
validBefore: now + validFor
});
messageIds = [];
for (const siteId of siteIds) {
// get the site
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, siteId))
.limit(1);
if (!newt) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Site associated with resource not found"
)
);
}
const [message] = await db
.insert(roundTripMessageTracker)
.values({
wsClientId: newt.newtId,
messageType: `newt/pam/connection`,
sentAt: Math.floor(Date.now() / 1000)
})
.returning();
if (!message) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create message tracker entry"
)
);
}
messageIds.push(message.messageId);
const agentHost = siteAgentHostMap.get(siteId);
await sendToClient(newt.newtId, {
type: `newt/pam/connection`,
data: {
messageId: message.messageId,
orgId: orgId,
agentPort: resource.authDaemonPort ?? 22123,
authDaemonMode: resource.authDaemonMode, // site, remote, native where native is the pty mode
externalAuthDaemon:
resource.authDaemonMode === "remote", // keep this for backward compatibility but new newts are using the authDaemonMode field
agentHost,
caCert: caKeys.publicKeyOpenSSH,
username: usernameToUse,
niceId: resource.niceId,
metadata: {
sudoMode: sudoMode,
sudoCommands: parsedSudoCommands,
homedir: homedir,
groups: parsedGroups
}
}
});
}
} else if (resource.pamMode === "passthrough") {
usernameToUse = username;
if (!usernameToUse) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Username must be provided when PAM mode is passthrough"
)
);
}
} else {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Invalid PAM mode configured for resource"
)
);
}
let sshHost: string | undefined;
if (
resource.authDaemonMode === "site" ||
resource.authDaemonMode === "remote"
) {
if (type === "private") {
const privateResource = resource as SiteResource;
if (privateResource.alias && privateResource.alias !== "") {
sshHost = privateResource.alias;
} else {
sshHost = privateResource.destination || "";
}
} else {
const publicResource = resource as Resource;
sshHost =
publicResource.fullDomain ||
publicResource.subdomain ||
publicResource.niceId;
}
} else if (resource.authDaemonMode === "native") {
if (siteIds.length > 1) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Multiple sites associated with resource, unable to determine SSH host when in native mode"
)
);
}
// get the site
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteIds[0]))
.limit(1);
if (!site) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Site associated with resource not found"
)
);
}
if (!site.address) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Site address not configured, unable to determine SSH host when in native mode"
)
);
}
// its the address but split off the cidr if there is one
sshHost = site.address.split("/")[0];
}
if (!sshHost) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Unable to determine SSH host for the resource"
)
);
}
await logsDb.insert(actionAuditLog).values({
timestamp: Math.floor(Date.now() / 1000),
orgId: orgId,
actorType: "user",
actor: req.user?.username ?? "",
actorId: req.user?.userId ?? "",
action: ActionsEnum.signSshKey,
metadata: JSON.stringify({
resourceId: normalizedResourceId,
resourceType: type,
resource: resource.name,
siteIds: siteIds
})
});
await logAccessAudit({
action: true,
type: "ssh",
orgId: orgId,
resourceId:
type === "public"
? (resource as Resource).resourceId
: undefined,
siteResourceId:
type === "private"
? (resource as SiteResource).siteResourceId
: undefined,
user: req.user
? { username: req.user.username ?? "", userId: req.user.userId }
: undefined,
metadata: {
resourceName: resource.name,
siteIds: siteIds,
sshUsername: usernameToUse,
sshHost: sshHost
},
userAgent: req.headers["user-agent"],
requestIp: req.ip
});
return response<SignSshKeyResponse>(res, {
data: {
certificate: cert?.certificate,
messageIds: messageIds,
messageId: messageIds[0], // just pick the first one for backward compatibility with older olms
sshUsername: usernameToUse,
sshHost: sshHost, // just pick the first one for backward compatibility with older olms
resourceId: normalizedResourceId,
siteIds: siteIds,
siteId: siteIds[0], // just pick the first one for backward compatibility with older olms
keyId: cert?.keyId,
authDaemonMode: resource.authDaemonMode,
validPrincipals: cert?.validPrincipals,
validAfter: cert?.validAfter.toISOString(),
validBefore: cert?.validBefore.toISOString(),
expiresIn
},
success: true,
error: false,
message: "SSH key signed successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error("Error signing SSH key:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred while signing the SSH key"
)
);
}
}
-13
View File
@@ -1,4 +1,3 @@
import { build } from "@server/build";
import {
handleNewtRegisterMessage,
handleReceiveBandwidthMessage,
@@ -8,15 +7,12 @@ import {
handleNewtExitNodesRequestMessage,
handleApplyBlueprintMessage,
handleNewtPingMessage,
startNewtOfflineChecker,
handleNewtDisconnectingMessage
} from "../newt";
import { startPingAccumulator } from "../newt/pingAccumulator";
import {
handleOlmRegisterMessage,
handleOlmRelayMessage,
handleOlmPingMessage,
startOlmOfflineChecker,
handleOlmServerPeerAddMessage,
handleOlmUnRelayMessage,
handleOlmDisconnectingMessage,
@@ -52,12 +48,3 @@ export const messageHandlers: Record<string, MessageHandler> = {
"newt/healthcheck/status": handleHealthcheckStatusMessage,
"ws/round-trip/complete": handleRoundTripMessage
};
// Start the ping accumulator for all builds - it batches per-site online/lastPing
// updates into periodic bulk writes, preventing connection pool exhaustion.
startPingAccumulator();
if (build != "saas") {
startOlmOfflineChecker(); // this is to handle the offline check for olms
startNewtOfflineChecker(); // this is to handle the offline check for newts
}