mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-11 05:06:39 +00:00
Compare commits
10 Commits
dependabot
...
jit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0503c6e66e | ||
|
|
9405b0b70a | ||
|
|
2a5c9465e9 | ||
|
|
f36b66e397 | ||
|
|
8c6d44677d | ||
|
|
1bfff630bf | ||
|
|
ebcef28b05 | ||
|
|
e87e12898c | ||
|
|
d60ab281cf | ||
|
|
c73a39f797 |
4029
package-lock.json
generated
4029
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||
"@aws-sdk/client-s3": "3.1004.0",
|
||||
"@aws-sdk/client-s3": "3.989.0",
|
||||
"@faker-js/faker": "10.3.0",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
@@ -80,16 +80,16 @@
|
||||
"d3": "7.9.0",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.3.0",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"glob": "13.0.6",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.1",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.10.0",
|
||||
"ioredis": "5.9.3",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "0.577.0",
|
||||
"lucide-react": "0.563.0",
|
||||
"maxmind": "5.0.5",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.12",
|
||||
@@ -99,21 +99,20 @@
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "8.0.1",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.20.0",
|
||||
"posthog-node": "5.28.0",
|
||||
"pg": "8.19.0",
|
||||
"posthog-node": "5.26.0",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.4",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-day-picker": "9.13.2",
|
||||
"react-dom": "19.2.4",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.71.2",
|
||||
"react-icons": "5.6.0",
|
||||
"react-icons": "5.5.0",
|
||||
"recharts": "2.15.4",
|
||||
"reodotdev": "1.1.0",
|
||||
"resend": "6.9.2",
|
||||
"reodotdev": "1.0.0",
|
||||
"semver": "7.7.4",
|
||||
"sshpk": "^1.18.0",
|
||||
"stripe": "20.4.1",
|
||||
"stripe": "20.3.1",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tailwind-merge": "3.5.0",
|
||||
"topojson-client": "3.1.0",
|
||||
@@ -131,10 +130,10 @@
|
||||
"zod-validation-error": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.54.1",
|
||||
"@dotenvx/dotenvx": "1.52.0",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@react-email/preview-server": "5.2.9",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@react-email/preview-server": "5.2.8",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
@@ -146,10 +145,10 @@
|
||||
"@types/jmespath": "0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "25.3.5",
|
||||
"@types/node": "25.2.3",
|
||||
"@types/nodemailer": "7.0.11",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/pg": "8.18.0",
|
||||
"@types/pg": "8.16.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/semver": "7.7.1",
|
||||
@@ -164,17 +163,13 @@
|
||||
"esbuild-node-externals": "1.20.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"postcss": "8.5.8",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.8.1",
|
||||
"react-email": "5.2.9",
|
||||
"tailwindcss": "4.2.1",
|
||||
"react-email": "5.2.8",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.56.1"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.27.3",
|
||||
"dompurify": "3.3.2"
|
||||
"typescript-eslint": "8.55.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,6 +720,7 @@ export const clientSitesAssociationsCache = pgTable(
|
||||
.notNull(),
|
||||
siteId: integer("siteId").notNull(),
|
||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||
isJitMode: boolean("isJitMode").notNull().default(false),
|
||||
endpoint: varchar("endpoint"),
|
||||
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||
}
|
||||
|
||||
@@ -409,6 +409,9 @@ export const clientSitesAssociationsCache = sqliteTable(
|
||||
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isJitMode: integer("isJitMode", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
endpoint: text("endpoint"),
|
||||
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export enum AudienceIds {
|
||||
SignUps = "",
|
||||
Subscribed = "",
|
||||
Churned = "",
|
||||
Newsletter = ""
|
||||
}
|
||||
|
||||
let resend;
|
||||
export default resend;
|
||||
|
||||
export async function moveEmailToAudience(
|
||||
email: string,
|
||||
audienceId: AudienceIds
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -38,10 +38,6 @@ export const privateConfigSchema = z.object({
|
||||
.string()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("SERVER_ENCRYPTION_KEY")),
|
||||
resend_api_key: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("RESEND_API_KEY")),
|
||||
reo_client_id: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { Resend } from "resend";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export enum AudienceIds {
|
||||
SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a",
|
||||
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
|
||||
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
|
||||
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"
|
||||
}
|
||||
|
||||
const resend = new Resend(
|
||||
privateConfig.getRawPrivateConfig().server.resend_api_key || "missing"
|
||||
);
|
||||
|
||||
export default resend;
|
||||
|
||||
export async function moveEmailToAudience(
|
||||
email: string,
|
||||
audienceId: AudienceIds
|
||||
) {
|
||||
if (process.env.ENVIRONMENT !== "prod") {
|
||||
logger.debug(
|
||||
`Skipping moving email ${email} to audience ${audienceId} in non-prod environment`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { error, data } = await retryWithBackoff(async () => {
|
||||
const { data, error } = await resend.contacts.create({
|
||||
email,
|
||||
unsubscribed: false,
|
||||
audienceId
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(
|
||||
`Error adding email ${email} to audience ${audienceId}: ${error}`
|
||||
);
|
||||
}
|
||||
return { error, data };
|
||||
});
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Error adding email ${email} to audience ${audienceId}: ${error}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
logger.debug(
|
||||
`Added email ${email} to audience ${audienceId} with contact ID ${data.id}`
|
||||
);
|
||||
}
|
||||
|
||||
const otherAudiences = Object.values(AudienceIds).filter(
|
||||
(id) => id !== audienceId
|
||||
);
|
||||
|
||||
for (const otherAudienceId of otherAudiences) {
|
||||
const { error, data } = await retryWithBackoff(async () => {
|
||||
const { data, error } = await resend.contacts.remove({
|
||||
email,
|
||||
audienceId: otherAudienceId
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(
|
||||
`Error removing email ${email} from audience ${otherAudienceId}: ${error}`
|
||||
);
|
||||
}
|
||||
return { error, data };
|
||||
});
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Error removing email ${email} from audience ${otherAudienceId}: ${error}`
|
||||
);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
logger.info(
|
||||
`Removed email ${email} from audience ${otherAudienceId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RetryOptions = {
|
||||
retries?: number;
|
||||
initialDelayMs?: number;
|
||||
factor?: number;
|
||||
};
|
||||
|
||||
export async function retryWithBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: RetryOptions = {}
|
||||
): Promise<T> {
|
||||
const { retries = 5, initialDelayMs = 500, factor = 2 } = options;
|
||||
|
||||
let attempt = 0;
|
||||
let delay = initialDelayMs;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
attempt++;
|
||||
|
||||
if (attempt > retries) throw err;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
delay *= factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import { eq, and } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import stripe from "#private/lib/stripe";
|
||||
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
|
||||
import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
|
||||
import { getSubType } from "./getSubType";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses";
|
||||
@@ -172,7 +171,7 @@ export async function handleSubscriptionCreated(
|
||||
const email = orgUserRes.user.email;
|
||||
|
||||
if (email) {
|
||||
moveEmailToAudience(email, AudienceIds.Subscribed);
|
||||
// TODO: update user in Sendy
|
||||
}
|
||||
}
|
||||
} else if (type === "license") {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
|
||||
import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
|
||||
import { getSubType } from "./getSubType";
|
||||
import stripe from "#private/lib/stripe";
|
||||
import privateConfig from "#private/lib/config";
|
||||
@@ -109,7 +108,7 @@ export async function handleSubscriptionDeleted(
|
||||
const email = orgUserRes.user.email;
|
||||
|
||||
if (email) {
|
||||
moveEmailToAudience(email, AudienceIds.Churned);
|
||||
// TODO: update user in Sendy
|
||||
}
|
||||
}
|
||||
} else if (type === "license") {
|
||||
|
||||
@@ -29,7 +29,6 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { eq, or, and } from "drizzle-orm";
|
||||
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
|
||||
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
|
||||
@@ -64,6 +63,7 @@ export type SignSshKeyResponse = {
|
||||
sshUsername: string;
|
||||
sshHost: string;
|
||||
resourceId: number;
|
||||
siteId: number;
|
||||
keyId: string;
|
||||
validPrincipals: string[];
|
||||
validAfter: string;
|
||||
@@ -453,6 +453,7 @@ export async function signSshKey(
|
||||
sshUsername: usernameToUse,
|
||||
sshHost: sshHost,
|
||||
resourceId: resource.siteResourceId,
|
||||
siteId: resource.siteId,
|
||||
keyId: cert.keyId,
|
||||
validPrincipals: cert.validPrincipals,
|
||||
validAfter: cert.validAfter.toISOString(),
|
||||
|
||||
@@ -76,7 +76,7 @@ const processMessage = async (
|
||||
clientId,
|
||||
message.type, // Pass message type for granular limiting
|
||||
100, // max requests per window
|
||||
20, // max requests per message type per window
|
||||
100, // max requests per message type per window
|
||||
60 * 1000 // window in milliseconds
|
||||
);
|
||||
if (rateLimitResult.isLimited) {
|
||||
|
||||
@@ -22,7 +22,6 @@ import { checkValidInvite } from "@server/auth/checkValidInvite";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { build } from "@server/build";
|
||||
import resend, { AudienceIds, moveEmailToAudience } from "#dynamic/lib/resend";
|
||||
|
||||
export const signupBodySchema = z.object({
|
||||
email: z.email().toLowerCase(),
|
||||
@@ -213,7 +212,7 @@ export async function signup(
|
||||
logger.debug(
|
||||
`User ${email} opted in to marketing emails during signup.`
|
||||
);
|
||||
moveEmailToAudience(email, AudienceIds.SignUps);
|
||||
// TODO: update user in Sendy
|
||||
}
|
||||
|
||||
if (config.getRawConfig().flags?.require_email_verification) {
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { clients, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, ExitNode, resources, Site, siteResources, targetHealthCheck, targets } from "@server/db";
|
||||
import {
|
||||
clients,
|
||||
clientSiteResourcesAssociationsCache,
|
||||
clientSitesAssociationsCache,
|
||||
db,
|
||||
ExitNode,
|
||||
resources,
|
||||
Site,
|
||||
siteResources,
|
||||
targetHealthCheck,
|
||||
targets
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { initPeerAddHandshake, updatePeer } from "../olm/peers";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
@@ -69,40 +80,42 @@ export async function buildClientConfigurationForNewtClient(
|
||||
// )
|
||||
// );
|
||||
|
||||
// update the peer info on the olm
|
||||
// if the peer has not been added yet this will be a no-op
|
||||
await updatePeer(client.clients.clientId, {
|
||||
siteId: site.siteId,
|
||||
endpoint: site.endpoint!,
|
||||
relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`,
|
||||
publicKey: site.publicKey!,
|
||||
serverIP: site.address,
|
||||
serverPort: site.listenPort
|
||||
// remoteSubnets: generateRemoteSubnets(
|
||||
// allSiteResources.map(
|
||||
// ({ siteResources }) => siteResources
|
||||
// )
|
||||
// ),
|
||||
// aliases: generateAliasConfig(
|
||||
// allSiteResources.map(
|
||||
// ({ siteResources }) => siteResources
|
||||
// )
|
||||
// )
|
||||
});
|
||||
if (!client.clientSitesAssociationsCache.isJitMode) { // if we are adding sites through jit then dont add the site to the olm
|
||||
// update the peer info on the olm
|
||||
// if the peer has not been added yet this will be a no-op
|
||||
await updatePeer(client.clients.clientId, {
|
||||
siteId: site.siteId,
|
||||
endpoint: site.endpoint!,
|
||||
relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`,
|
||||
publicKey: site.publicKey!,
|
||||
serverIP: site.address,
|
||||
serverPort: site.listenPort
|
||||
// remoteSubnets: generateRemoteSubnets(
|
||||
// allSiteResources.map(
|
||||
// ({ siteResources }) => siteResources
|
||||
// )
|
||||
// ),
|
||||
// aliases: generateAliasConfig(
|
||||
// allSiteResources.map(
|
||||
// ({ siteResources }) => siteResources
|
||||
// )
|
||||
// )
|
||||
});
|
||||
|
||||
// also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch
|
||||
// if it has already been added this will be a no-op
|
||||
await initPeerAddHandshake(
|
||||
// this will kick off the add peer process for the client
|
||||
client.clients.clientId,
|
||||
{
|
||||
siteId,
|
||||
exitNode: {
|
||||
publicKey: exitNode.publicKey,
|
||||
endpoint: exitNode.endpoint
|
||||
// also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch
|
||||
// if it has already been added this will be a no-op
|
||||
await initPeerAddHandshake(
|
||||
// this will kick off the add peer process for the client
|
||||
client.clients.clientId,
|
||||
{
|
||||
siteId,
|
||||
exitNode: {
|
||||
publicKey: exitNode.publicKey,
|
||||
endpoint: exitNode.endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
publicKey: client.clients.pubKey!,
|
||||
|
||||
@@ -17,6 +17,8 @@ import { getUserDeviceName } from "@server/db/names";
|
||||
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
|
||||
import { OlmErrorCodes, sendOlmError } from "./error";
|
||||
import { handleFingerprintInsertion } from "./fingerprintingUtils";
|
||||
import { Alias } from "@server/lib/ip";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
logger.info("Handling register olm message!");
|
||||
@@ -207,6 +209,32 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Get all sites data
|
||||
const sitesCountResult = await db
|
||||
.select({ count: count() })
|
||||
.from(sites)
|
||||
.innerJoin(
|
||||
clientSitesAssociationsCache,
|
||||
eq(sites.siteId, clientSitesAssociationsCache.siteId)
|
||||
)
|
||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||
|
||||
// Extract the count value from the result array
|
||||
const sitesCount =
|
||||
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
||||
|
||||
// Prepare an array to store site configurations
|
||||
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
||||
|
||||
let jitMode = false;
|
||||
if (sitesCount > 250 && build == "saas") {
|
||||
// THIS IS THE MAX ON THE BUSINESS TIER
|
||||
// we have too many sites
|
||||
// If we have too many sites we need to drop into fully JIT mode by not sending any of the sites
|
||||
logger.info("Too many sites (%d), dropping into JIT mode", sitesCount)
|
||||
jitMode = true;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
||||
);
|
||||
@@ -233,28 +261,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
await db
|
||||
.update(clientSitesAssociationsCache)
|
||||
.set({
|
||||
isRelayed: relay == true
|
||||
isRelayed: relay == true,
|
||||
isJitMode: jitMode
|
||||
})
|
||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||
}
|
||||
|
||||
// Get all sites data
|
||||
const sitesCountResult = await db
|
||||
.select({ count: count() })
|
||||
.from(sites)
|
||||
.innerJoin(
|
||||
clientSitesAssociationsCache,
|
||||
eq(sites.siteId, clientSitesAssociationsCache.siteId)
|
||||
)
|
||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||
|
||||
// Extract the count value from the result array
|
||||
const sitesCount =
|
||||
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
||||
|
||||
// Prepare an array to store site configurations
|
||||
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
||||
|
||||
// this prevents us from accepting a register from an olm that has not hole punched yet.
|
||||
// the olm will pump the register so we can keep checking
|
||||
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||
@@ -265,18 +277,25 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: its important that the client here is the old client and the public key is the new key
|
||||
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
||||
client,
|
||||
publicKey,
|
||||
relay
|
||||
);
|
||||
let siteConfigurations: {
|
||||
siteId: number;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
publicKey: string | null;
|
||||
serverIP: string | null;
|
||||
serverPort: number | null;
|
||||
remoteSubnets: string[];
|
||||
aliases: Alias[];
|
||||
}[] = [];
|
||||
|
||||
// REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES
|
||||
// if (siteConfigurations.length === 0) {
|
||||
// logger.warn("No valid site configurations found");
|
||||
// return;
|
||||
// }
|
||||
if (!jitMode) {
|
||||
// NOTE: its important that the client here is the old client and the public key is the new key
|
||||
siteConfigurations = await buildSiteConfigurationForOlmClient(
|
||||
client,
|
||||
publicKey,
|
||||
relay
|
||||
);
|
||||
}
|
||||
|
||||
// Return connect message with all site configurations
|
||||
return {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
||||
}
|
||||
|
||||
if (!olm.clientId) {
|
||||
logger.warn("Olm has no site!"); // TODO: Maybe we create the site here?
|
||||
logger.warn("Olm has no client!");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { siteId } = message.data;
|
||||
const { siteId, chainId } = message.data;
|
||||
|
||||
// Get the site
|
||||
const [site] = await db
|
||||
@@ -90,7 +90,8 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
||||
data: {
|
||||
siteId: siteId,
|
||||
relayEndpoint: exitNode.endpoint,
|
||||
relayPort: config.getRawConfig().gerbil.clients_start_port
|
||||
relayPort: config.getRawConfig().gerbil.clients_start_port,
|
||||
chainId
|
||||
}
|
||||
},
|
||||
broadcast: false,
|
||||
|
||||
241
server/routers/olm/handleOlmServerInitAddPeerHandshake.ts
Normal file
241
server/routers/olm/handleOlmServerInitAddPeerHandshake.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
clientSiteResourcesAssociationsCache,
|
||||
clientSitesAssociationsCache,
|
||||
db,
|
||||
exitNodes,
|
||||
Site,
|
||||
siteResources
|
||||
} from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { clients, Olm, sites } from "@server/db";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { initPeerAddHandshake } from "./peers";
|
||||
|
||||
export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
context
|
||||
) => {
|
||||
logger.info("Handling register olm message!");
|
||||
const { message, client: c, sendToClient } = context;
|
||||
const olm = c as Olm;
|
||||
|
||||
if (!olm) {
|
||||
logger.warn("Olm not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!olm.clientId) {
|
||||
logger.warn("Olm has no client!"); // TODO: Maybe we create the site here?
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = olm.clientId;
|
||||
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
logger.warn("Client not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const { siteId, resourceId, chainId } = message.data;
|
||||
|
||||
let site: Site | null = null;
|
||||
if (siteId) {
|
||||
// get the site
|
||||
const [siteRes] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
if (siteRes) {
|
||||
site = siteRes;
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceId && !site) {
|
||||
const resources = await db
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.where(
|
||||
and(
|
||||
or(
|
||||
eq(siteResources.niceId, resourceId),
|
||||
eq(siteResources.alias, resourceId)
|
||||
),
|
||||
eq(siteResources.orgId, client.orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (!resources || resources.length === 0) {
|
||||
logger.error(`handleOlmServerPeerAddMessage: Resource not found`);
|
||||
// cancel the request from the olm side to not keep doing this
|
||||
await sendToClient(
|
||||
olm.olmId,
|
||||
{
|
||||
type: "olm/wg/peer/chain/cancel",
|
||||
data: {
|
||||
chainId
|
||||
}
|
||||
},
|
||||
{ incrementConfigVersion: false }
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (resources.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
|
||||
logger.error(
|
||||
`handleOlmServerPeerAddMessage: Multiple resources found matching the criteria`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const resource = resources[0];
|
||||
|
||||
const currentResourceAssociationCaches = await db
|
||||
.select()
|
||||
.from(clientSiteResourcesAssociationsCache)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
resource.siteResourceId
|
||||
),
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (currentResourceAssociationCaches.length === 0) {
|
||||
logger.error(
|
||||
`handleOlmServerPeerAddMessage: Client ${client.clientId} does not have access to resource ${resource.siteResourceId}`
|
||||
);
|
||||
// cancel the request from the olm side to not keep doing this
|
||||
await sendToClient(
|
||||
olm.olmId,
|
||||
{
|
||||
type: "olm/wg/peer/chain/cancel",
|
||||
data: {
|
||||
chainId
|
||||
}
|
||||
},
|
||||
{ incrementConfigVersion: false }
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const siteIdFromResource = resource.siteId;
|
||||
|
||||
// get the site
|
||||
const [siteRes] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteIdFromResource));
|
||||
if (!siteRes) {
|
||||
logger.error(
|
||||
`handleOlmServerPeerAddMessage: Site with ID ${site} not found`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
site = siteRes;
|
||||
}
|
||||
|
||||
if (!site) {
|
||||
logger.error(`handleOlmServerPeerAddMessage: Site not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the client can access this site using the cache
|
||||
const currentSiteAssociationCaches = await db
|
||||
.select()
|
||||
.from(clientSitesAssociationsCache)
|
||||
.where(
|
||||
and(
|
||||
eq(clientSitesAssociationsCache.clientId, client.clientId),
|
||||
eq(clientSitesAssociationsCache.siteId, site.siteId)
|
||||
)
|
||||
);
|
||||
|
||||
if (currentSiteAssociationCaches.length === 0) {
|
||||
logger.error(
|
||||
`handleOlmServerPeerAddMessage: Client ${client.clientId} does not have access to site ${site.siteId}`
|
||||
);
|
||||
// cancel the request from the olm side to not keep doing this
|
||||
await sendToClient(
|
||||
olm.olmId,
|
||||
{
|
||||
type: "olm/wg/peer/chain/cancel",
|
||||
data: {
|
||||
chainId
|
||||
}
|
||||
},
|
||||
{ incrementConfigVersion: false }
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!site.exitNodeId) {
|
||||
logger.error(
|
||||
`handleOlmServerPeerAddMessage: Site with ID ${site.siteId} has no exit node`
|
||||
);
|
||||
// cancel the request from the olm side to not keep doing this
|
||||
await sendToClient(
|
||||
olm.olmId,
|
||||
{
|
||||
type: "olm/wg/peer/chain/cancel",
|
||||
data: {
|
||||
chainId
|
||||
}
|
||||
},
|
||||
{ incrementConfigVersion: false }
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// get the exit node from the side
|
||||
const [exitNode] = await db
|
||||
.select()
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.exitNodeId, site.exitNodeId));
|
||||
|
||||
if (!exitNode) {
|
||||
logger.error(
|
||||
`handleOlmServerPeerAddMessage: Site with ID ${site.siteId} has no exit node`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch
|
||||
// if it has already been added this will be a no-op
|
||||
await initPeerAddHandshake(
|
||||
// this will kick off the add peer process for the client
|
||||
client.clientId,
|
||||
{
|
||||
siteId: site.siteId,
|
||||
exitNode: {
|
||||
publicKey: exitNode.publicKey,
|
||||
endpoint: exitNode.endpoint
|
||||
}
|
||||
},
|
||||
olm.olmId,
|
||||
chainId
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
@@ -54,7 +54,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||
return;
|
||||
}
|
||||
|
||||
const { siteId } = message.data;
|
||||
const { siteId, chainId } = message.data;
|
||||
|
||||
// get the site
|
||||
const [site] = await db
|
||||
@@ -179,7 +179,8 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||
),
|
||||
aliases: generateAliasConfig(
|
||||
allSiteResources.map(({ siteResources }) => siteResources)
|
||||
)
|
||||
),
|
||||
chainId: chainId,
|
||||
}
|
||||
},
|
||||
broadcast: false,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
||||
}
|
||||
|
||||
if (!olm.clientId) {
|
||||
logger.warn("Olm has no site!"); // TODO: Maybe we create the site here?
|
||||
logger.warn("Olm has no client!");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { siteId } = message.data;
|
||||
const { siteId, chainId } = message.data;
|
||||
|
||||
// Get the site
|
||||
const [site] = await db
|
||||
@@ -87,7 +87,8 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
||||
type: "olm/wg/peer/unrelay",
|
||||
data: {
|
||||
siteId: siteId,
|
||||
endpoint: site.endpoint
|
||||
endpoint: site.endpoint,
|
||||
chainId
|
||||
}
|
||||
},
|
||||
broadcast: false,
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from "./handleOlmServerPeerAddMessage";
|
||||
export * from "./handleOlmUnRelayMessage";
|
||||
export * from "./recoverOlmWithFingerprint";
|
||||
export * from "./handleOlmDisconnectingMessage";
|
||||
export * from "./handleOlmServerInitAddPeerHandshake";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { db, olms } from "@server/db";
|
||||
import { clientSitesAssociationsCache, db, olms } from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { Alias } from "yaml";
|
||||
|
||||
export async function addPeer(
|
||||
@@ -149,7 +149,8 @@ export async function initPeerAddHandshake(
|
||||
endpoint: string;
|
||||
};
|
||||
},
|
||||
olmId?: string
|
||||
olmId?: string,
|
||||
chainId?: string
|
||||
) {
|
||||
if (!olmId) {
|
||||
const [olm] = await db
|
||||
@@ -173,7 +174,8 @@ export async function initPeerAddHandshake(
|
||||
publicKey: peer.exitNode.publicKey,
|
||||
relayPort: config.getRawConfig().gerbil.clients_start_port,
|
||||
endpoint: peer.exitNode.endpoint
|
||||
}
|
||||
},
|
||||
chainId
|
||||
}
|
||||
},
|
||||
{ incrementConfigVersion: true }
|
||||
@@ -181,6 +183,17 @@ export async function initPeerAddHandshake(
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
|
||||
// update the clientSiteAssociationsCache to make the isJitMode flag false so that JIT mode is disabled for this site if it restarts or something after the connection
|
||||
await db
|
||||
.update(clientSitesAssociationsCache)
|
||||
.set({ isJitMode: false })
|
||||
.where(
|
||||
and(
|
||||
eq(clientSitesAssociationsCache.clientId, clientId),
|
||||
eq(clientSitesAssociationsCache.siteId, peer.siteId)
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`
|
||||
);
|
||||
|
||||
@@ -223,20 +223,6 @@ async function createHttpResource(
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent creating resource with same domain as dashboard
|
||||
const dashboardUrl = config.getRawConfig().app.dashboard_url;
|
||||
if (dashboardUrl) {
|
||||
const dashboardHost = new URL(dashboardUrl).hostname;
|
||||
if (fullDomain === dashboardHost) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"Resource domain cannot be the same as the dashboard domain"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (build != "oss") {
|
||||
const existingLoginPages = await db
|
||||
.select()
|
||||
|
||||
@@ -353,20 +353,6 @@ async function updateHttpResource(
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent updating resource with same domain as dashboard
|
||||
const dashboardUrl = config.getRawConfig().app.dashboard_url;
|
||||
if (dashboardUrl) {
|
||||
const dashboardHost = new URL(dashboardUrl).hostname;
|
||||
if (fullDomain === dashboardHost) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"Resource domain cannot be the same as the dashboard domain"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (build != "oss") {
|
||||
const existingLoginPages = await db
|
||||
.select()
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
startOlmOfflineChecker,
|
||||
handleOlmServerPeerAddMessage,
|
||||
handleOlmUnRelayMessage,
|
||||
handleOlmDisconnecingMessage
|
||||
handleOlmDisconnecingMessage,
|
||||
handleOlmServerInitAddPeerHandshake
|
||||
} from "../olm";
|
||||
import { handleHealthcheckStatusMessage } from "../target";
|
||||
import { handleRoundTripMessage } from "./handleRoundTripMessage";
|
||||
@@ -23,6 +24,7 @@ import { MessageHandler } from "./types";
|
||||
|
||||
export const messageHandlers: Record<string, MessageHandler> = {
|
||||
"olm/wg/server/peer/add": handleOlmServerPeerAddMessage,
|
||||
"olm/wg/server/peer/init": handleOlmServerInitAddPeerHandshake,
|
||||
"olm/wg/register": handleOlmRegisterMessage,
|
||||
"olm/wg/relay": handleOlmRelayMessage,
|
||||
"olm/wg/unrelay": handleOlmUnRelayMessage,
|
||||
|
||||
@@ -559,7 +559,7 @@ export default function Page() {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("resourceErrorCreate"),
|
||||
description: formatAxiosError(e, t("resourceErrorCreateMessageDescription"))
|
||||
description: t("resourceErrorCreateMessageDescription")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
||||
return (
|
||||
<CredenzaContent
|
||||
className={cn(
|
||||
"overflow-y-auto max-h-[100dvh] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0",
|
||||
"overflow-y-auto max-h-[100dvh] md:max-h-screen md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user