Merge branch 'dev' into distribution

This commit is contained in:
miloschwartz
2025-10-13 11:06:14 -07:00
85 changed files with 906 additions and 1639 deletions

View File

@@ -0,0 +1,8 @@
export type TransferSessionResponse = {
valid: boolean;
cookie?: string;
};
export type GetSessionTransferTokenRenponse = {
token: string;
};

View File

@@ -33,7 +33,9 @@ import createHttpError from "http-errors";
import NodeCache from "node-cache";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { getCountryCodeForIp, remoteGetCountryCodeForIp } from "@server/lib/geoip";
import {
getCountryCodeForIp,
} from "@server/lib/geoip";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { verifyPassword } from "@server/auth/password";
@@ -106,23 +108,23 @@ export async function verifyResourceSession(
const clientIp = requestIp
? (() => {
logger.debug("Request IP:", { requestIp });
if (requestIp.startsWith("[") && requestIp.includes("]")) {
// if brackets are found, extract the IPv6 address from between the brackets
const ipv6Match = requestIp.match(/\[(.*?)\]/);
if (ipv6Match) {
return ipv6Match[1];
}
}
logger.debug("Request IP:", { requestIp });
if (requestIp.startsWith("[") && requestIp.includes("]")) {
// if brackets are found, extract the IPv6 address from between the brackets
const ipv6Match = requestIp.match(/\[(.*?)\]/);
if (ipv6Match) {
return ipv6Match[1];
}
}
// ivp4
// split at last colon
const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex);
}
return requestIp;
})()
// ivp4
// split at last colon
const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex);
}
return requestIp;
})()
: undefined;
logger.debug("Client IP:", { clientIp });
@@ -137,11 +139,11 @@ export async function verifyResourceSession(
const resourceCacheKey = `resource:${cleanHost}`;
let resourceData:
| {
resource: Resource | null;
pincode: ResourcePincode | null;
password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null;
}
resource: Resource | null;
pincode: ResourcePincode | null;
password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null;
}
| undefined = cache.get(resourceCacheKey);
if (!resourceData) {
@@ -213,21 +215,21 @@ export async function verifyResourceSession(
headers &&
headers[
config.getRawConfig().server.resource_access_token_headers.id
] &&
] &&
headers[
config.getRawConfig().server.resource_access_token_headers.token
]
]
) {
const accessTokenId =
headers[
config.getRawConfig().server.resource_access_token_headers
.id
];
];
const accessToken =
headers[
config.getRawConfig().server.resource_access_token_headers
.token
];
];
const { valid, error, tokenItem } = await verifyResourceAccessToken(
{
@@ -294,10 +296,17 @@ export async function verifyResourceSession(
// check for HTTP Basic Auth header
if (headerAuth && clientHeaderAuth) {
if(cache.get(clientHeaderAuth)) {
logger.debug("Resource allowed because header auth is valid (cached)");
if (cache.get(clientHeaderAuth)) {
logger.debug(
"Resource allowed because header auth is valid (cached)"
);
return allowed(res);
}else if(await verifyPassword(clientHeaderAuth, headerAuth.headerAuthHash)){
} else if (
await verifyPassword(
clientHeaderAuth,
headerAuth.headerAuthHash
)
) {
cache.set(clientHeaderAuth, clientHeaderAuth);
logger.debug("Resource allowed because header auth is valid");
return allowed(res);
@@ -477,7 +486,11 @@ function extractResourceSessionToken(
return latest.token;
}
async function notAllowed(res: Response, redirectPath?: string, orgId?: string) {
async function notAllowed(
res: Response,
redirectPath?: string,
orgId?: string
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const { tier } = await getOrgTierData(orgId); // returns null in oss
@@ -491,14 +504,11 @@ async function notAllowed(res: Response, redirectPath?: string, orgId?: string)
let endpoint: string;
if (loginPage && loginPage.domainId && loginPage.fullDomain) {
const secure = config.getRawConfig().app.dashboard_url?.startsWith("https");
const secure = config
.getRawConfig()
.app.dashboard_url?.startsWith("https");
const method = secure ? "https" : "http";
endpoint = `${method}://${loginPage.fullDomain}`;
} else if (config.isManagedMode()) {
endpoint =
config.getRawConfig().managed?.redirect_endpoint ||
config.getRawConfig().managed?.endpoint ||
"";
} else {
endpoint = config.getRawConfig().app.dashboard_url!;
}
@@ -803,11 +813,7 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise<boolean> {
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
if (!cachedCountryCode) {
if (config.isManagedMode()) {
cachedCountryCode = await remoteGetCountryCodeForIp(ip);
} else {
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
}
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
// Cache for longer since IP geolocation doesn't change frequently
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
}
@@ -817,7 +823,9 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise<boolean> {
return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase();
}
function extractBasicAuth(headers: Record<string, string> | undefined): string | undefined {
function extractBasicAuth(
headers: Record<string, string> | undefined
): string | undefined {
if (!headers || (!headers.authorization && !headers.Authorization)) {
return;
}
@@ -833,8 +841,9 @@ function extractBasicAuth(headers: Record<string, string> | undefined): string |
try {
// Extract the base64 encoded credentials
return authHeader.slice("Basic ".length);
} catch (error) {
logger.debug("Basic Auth: Failed to decode credentials", { error: error instanceof Error ? error.message : "Unknown error" });
logger.debug("Basic Auth: Failed to decode credentials", {
error: error instanceof Error ? error.message : "Unknown error"
});
}
}

View File

@@ -0,0 +1,17 @@
import { Limit, Subscription, SubscriptionItem, Usage } from "@server/db";
export type GetOrgSubscriptionResponse = {
subscription: Subscription | null;
items: SubscriptionItem[];
};
export type GetOrgUsageResponse = {
usage: Usage[];
limits: Limit[];
};
export type GetOrgTierResponse = {
tier: string | null;
active: boolean;
};

View File

@@ -0,0 +1,13 @@
export type GetCertificateResponse = {
certId: number;
domain: string;
domainId: string;
wildcard: boolean;
status: string; // pending, requested, valid, expired, failed
expiresAt: string | null;
lastRenewalAttempt: Date | null;
createdAt: string;
updatedAt: string;
errorMessage?: string | null;
renewalCount: number;
}

View File

@@ -0,0 +1,8 @@
export type CheckDomainAvailabilityResponse = {
available: boolean;
options: {
domainNamespaceId: string;
domainId: string;
fullDomain: string;
}[];
};

View File

@@ -9,7 +9,6 @@ import logger from "@server/logger";
import config from "@server/lib/config";
import { fromError } from "zod-validation-error";
import { getAllowedIps } from "../target/helpers";
import { proxyToRemote } from "@server/lib/remoteProxy";
import { createExitNode } from "#dynamic/routers/gerbil/createExitNode";
// Define Zod schema for request validation
@@ -63,16 +62,6 @@ export async function getConfig(
);
}
// STOP HERE IN HYBRID MODE
if (config.isManagedMode()) {
req.body = {
...req.body,
endpoint: exitNode.endpoint,
listenPort: exitNode.listenPort
};
return proxyToRemote(req, res, next, "hybrid/gerbil/get-config");
}
const configResponse = await generateGerbilConfig(exitNode);
logger.debug("Sending config: ", configResponse);

View File

@@ -6,8 +6,6 @@ import * as badger from "./badger";
import * as auth from "@server/routers/auth";
import * as supporterKey from "@server/routers/supporterKey";
import * as idp from "@server/routers/idp";
import { proxyToRemote } from "@server/lib/remoteProxy";
import config from "@server/lib/config";
import HttpCode from "@server/types/HttpCode";
import {
verifyResourceAccess,
@@ -48,34 +46,11 @@ internalRouter.get("/idp/:idpId", idp.getIdp);
const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter);
if (config.isManagedMode()) {
// Use proxy router to forward requests to remote cloud server
// Proxy endpoints for each gerbil route
gerbilRouter.post("/receive-bandwidth", (req, res, next) =>
proxyToRemote(req, res, next, "hybrid/gerbil/receive-bandwidth")
);
gerbilRouter.post("/update-hole-punch", (req, res, next) =>
proxyToRemote(req, res, next, "hybrid/gerbil/update-hole-punch")
);
gerbilRouter.post("/get-all-relays", (req, res, next) =>
proxyToRemote(req, res, next, "hybrid/gerbil/get-all-relays")
);
gerbilRouter.post("/get-resolved-hostname", (req, res, next) =>
proxyToRemote(req, res, next, `hybrid/gerbil/get-resolved-hostname`)
);
// GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER
// SO IT CAN REGISTER THE LOCAL EXIT NODE
} else {
// Use local gerbil endpoints
gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname);
}
// Use local gerbil endpoints
gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname);
// WE HANDLE THE PROXY INSIDE OF THIS FUNCTION
// SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL
@@ -87,10 +62,4 @@ internalRouter.use("/badger", badgerRouter);
badgerRouter.post("/verify-session", badger.verifyResourceSession);
if (config.isManagedMode()) {
badgerRouter.post("/exchange-session", (req, res, next) =>
proxyToRemote(req, res, next, "hybrid/badger/exchange-session")
);
} else {
badgerRouter.post("/exchange-session", badger.exchangeSession);
}
badgerRouter.post("/exchange-session", badger.exchangeSession);

View File

@@ -0,0 +1,11 @@
import { LoginPage } from "@server/db";
export type CreateLoginPageResponse = LoginPage;
export type DeleteLoginPageResponse = LoginPage;
export type GetLoginPageResponse = LoginPage;
export type UpdateLoginPageResponse = LoginPage;
export type LoadLoginPageResponse = LoginPage & { orgId: string };

View File

@@ -0,0 +1,27 @@
import { Idp, IdpOidcConfig } from "@server/db";
export type CreateOrgIdpResponse = {
idpId: number;
redirectUrl: string;
};
export type GetOrgIdpResponse = {
idp: Idp,
idpOidcConfig: IdpOidcConfig | null,
redirectUrl: string
}
export type ListOrgIdpsResponse = {
idps: {
idpId: number;
orgId: string;
name: string;
type: string;
variant: string;
}[],
pagination: {
total: number;
limit: number;
offset: number;
};
};

View File

@@ -0,0 +1,34 @@
import { RemoteExitNode } from "@server/db";
export type CreateRemoteExitNodeResponse = {
token: string;
remoteExitNodeId: string;
secret: string;
};
export type PickRemoteExitNodeDefaultsResponse = {
remoteExitNodeId: string;
secret: string;
};
export type QuickStartRemoteExitNodeResponse = {
remoteExitNodeId: string;
secret: string;
};
export type ListRemoteExitNodesResponse = {
remoteExitNodes: {
remoteExitNodeId: string;
dateCreated: string;
version: string | null;
exitNodeId: number | null;
name: string;
address: string;
endpoint: string;
online: boolean;
type: string | null;
}[];
pagination: { total: number; limit: number; offset: number };
};
export type GetRemoteExitNodeResponse = { remoteExitNodeId: string; dateCreated: string; version: string | null; exitNodeId: number | null; name: string; address: string; endpoint: string; online: boolean; type: string | null; }