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

@@ -1,75 +0,0 @@
name: Create Dev-Image
on:
pull_request:
branches:
- main
- dev
types:
- opened
- synchronize
- reopened
jobs:
docker:
runs-on: ubuntu-latest
env:
TAG_URL: https://hub.docker.com/r/${{ vars.DOCKER_HUB_REPO }}/tags
TAG: ${{ vars.DOCKER_HUB_REPO }}:dev-pr${{ github.event.pull_request.number }}
TAG_PG: ${{ vars.DOCKER_HUB_REPO }}:postgresql-dev-pr${{ github.event.pull_request.number }}
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image SQLITE
uses: docker/build-push-action@v6
with:
platforms: linux/amd64
push: true
tags: ${{ env.TAG }}
cache-from: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache
cache-to: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache,mode=max
build-args: DATABASE=sqlite
- name: Build and push Docker image PG
uses: docker/build-push-action@v6
with:
platforms: linux/amd64
push: true
tags: ${{ env.TAG_PG }}
cache-from: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache-pg
cache-to: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache-pg,mode=max
build-args: DATABASE=pg
- uses: actions/github-script@v8
with:
script: |
const repoUrl = process.env.TAG_URL;
const tag = process.env.TAG;
const tagPg = process.env.TAG_PG;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `👋 Thanks for your PR!
Dev images for this PR are now available on [docker hub](${repoUrl}):
**SQLITE Image:**
\`\`\`
${tag}
\`\`\`
**Postgresql Image:**
\`\`\`
${tagPg}
\`\`\``
})

View File

@@ -1541,8 +1541,8 @@
"autoLoginError": "Auto Login Error", "autoLoginError": "Auto Login Error",
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"remoteExitNodeManageRemoteExitNodes": "Managed Nodes", "remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
"remoteExitNodeDescription": "Self-host one or more nodes for tunnel exit servers", "remoteExitNodeDescription": "Self-host one or more remote nodes for tunnel exit servers",
"remoteExitNodes": "Nodes", "remoteExitNodes": "Nodes",
"searchRemoteExitNodes": "Search nodes...", "searchRemoteExitNodes": "Search nodes...",
"remoteExitNodeAdd": "Add Node", "remoteExitNodeAdd": "Add Node",
@@ -1552,7 +1552,7 @@
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.", "remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
"remoteExitNodeConfirmDelete": "Confirm Delete Node", "remoteExitNodeConfirmDelete": "Confirm Delete Node",
"remoteExitNodeDelete": "Delete Node", "remoteExitNodeDelete": "Delete Node",
"sidebarRemoteExitNodes": "Nodes", "sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Create Node", "title": "Create Node",
"description": "Create a new node to extend your network connectivity", "description": "Create a new node to extend your network connectivity",

View File

@@ -44,27 +44,25 @@ export function createApiServer() {
} }
const corsConfig = config.getRawConfig().server.cors; const corsConfig = config.getRawConfig().server.cors;
const options = {
...(corsConfig?.origins
? { origin: corsConfig.origins }
: {
origin: (origin: any, callback: any) => {
callback(null, true);
}
}),
...(corsConfig?.methods && { methods: corsConfig.methods }),
...(corsConfig?.allowed_headers && {
allowedHeaders: corsConfig.allowed_headers
}),
credentials: !(corsConfig?.credentials === false)
};
if (build == "oss") { if (build == "oss" || !corsConfig) {
const options = {
...(corsConfig?.origins
? { origin: corsConfig.origins }
: {
origin: (origin: any, callback: any) => {
callback(null, true);
}
}),
...(corsConfig?.methods && { methods: corsConfig.methods }),
...(corsConfig?.allowed_headers && {
allowedHeaders: corsConfig.allowed_headers
}),
credentials: !(corsConfig?.credentials === false)
};
logger.debug("Using CORS options", options); logger.debug("Using CORS options", options);
apiServer.use(cors(options)); apiServer.use(cors(options));
} else { } else if (corsConfig) {
// Use the custom CORS middleware with loginPage support // Use the custom CORS middleware with loginPage support
apiServer.use(corsWithLoginPageSupport(corsConfig)); apiServer.use(corsWithLoginPageSupport(corsConfig));
} }

View File

@@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db";
import { db } from "@server/db"; import { db } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
import axios from "axios";
import logger from "@server/logger";
import { tokenManager } from "@server/lib/tokenManager";
export const SESSION_COOKIE_NAME = export const SESSION_COOKIE_NAME =
config.getRawConfig().server.session_cookie_name; config.getRawConfig().server.session_cookie_name;
@@ -65,29 +62,6 @@ export async function validateResourceSessionToken(
token: string, token: string,
resourceId: number resourceId: number
): Promise<ResourceSessionValidationResult> { ): Promise<ResourceSessionValidationResult> {
if (config.isManagedMode()) {
try {
const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
token: token
}, await tokenManager.getAuthHeader());
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error validating resource session token in hybrid mode:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error validating resource session token in hybrid mode:", error);
}
return { resourceSession: null };
}
}
const sessionId = encodeHexLowerCase( const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)) sha256(new TextEncoder().encode(token))
); );

View File

@@ -721,3 +721,4 @@ export type SiteResource = InferSelectModel<typeof siteResources>;
export type SetupToken = InferSelectModel<typeof setupTokens>; export type SetupToken = InferSelectModel<typeof setupTokens>;
export type HostMeta = InferSelectModel<typeof hostMeta>; export type HostMeta = InferSelectModel<typeof hostMeta>;
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>; export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;

View File

@@ -17,10 +17,6 @@ import {
users users
} from "@server/db"; } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import axios from "axios";
import config from "@server/lib/config";
import logger from "@server/logger";
import { tokenManager } from "@server/lib/tokenManager";
export type ResourceWithAuth = { export type ResourceWithAuth = {
resource: Resource | null; resource: Resource | null;
@@ -40,30 +36,6 @@ export type UserSessionWithUser = {
export async function getResourceByDomain( export async function getResourceByDomain(
domain: string domain: string
): Promise<ResourceWithAuth | null> { ): Promise<ResourceWithAuth | null> {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const [result] = await db const [result] = await db
.select() .select()
.from(resources) .from(resources)
@@ -100,30 +72,6 @@ export async function getResourceByDomain(
export async function getUserSessionWithUser( export async function getUserSessionWithUser(
userSessionId: string userSessionId: string
): Promise<UserSessionWithUser | null> { ): Promise<UserSessionWithUser | null> {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const [res] = await db const [res] = await db
.select() .select()
.from(sessions) .from(sessions)
@@ -144,30 +92,6 @@ export async function getUserSessionWithUser(
* Get user organization role * Get user organization role
*/ */
export async function getUserOrgRole(userId: string, orgId: string) { export async function getUserOrgRole(userId: string, orgId: string) {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const userOrgRole = await db const userOrgRole = await db
.select() .select()
.from(userOrgs) .from(userOrgs)
@@ -184,30 +108,6 @@ export async function getRoleResourceAccess(
resourceId: number, resourceId: number,
roleId: number roleId: number
) { ) {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const roleResourceAccess = await db const roleResourceAccess = await db
.select() .select()
.from(roleResources) .from(roleResources)
@@ -229,30 +129,6 @@ export async function getUserResourceAccess(
userId: string, userId: string,
resourceId: number resourceId: number
) { ) {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const userResourceAccess = await db const userResourceAccess = await db
.select() .select()
.from(userResources) .from(userResources)
@@ -273,30 +149,6 @@ export async function getUserResourceAccess(
export async function getResourceRules( export async function getResourceRules(
resourceId: number resourceId: number
): Promise<ResourceRule[]> { ): Promise<ResourceRule[]> {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return [];
}
}
const rules = await db const rules = await db
.select() .select()
.from(resourceRules) .from(resourceRules)
@@ -311,30 +163,6 @@ export async function getResourceRules(
export async function getOrgLoginPage( export async function getOrgLoginPage(
orgId: string orgId: string
): Promise<LoginPage | null> { ): Promise<LoginPage | null> {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/org/${orgId}/login-page`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const [result] = await db const [result] = await db
.select() .select()
.from(loginPageOrg) .from(loginPageOrg)

View File

@@ -760,3 +760,4 @@ export type OrgDomains = InferSelectModel<typeof orgDomains>;
export type SetupToken = InferSelectModel<typeof setupTokens>; export type SetupToken = InferSelectModel<typeof setupTokens>;
export type HostMeta = InferSelectModel<typeof hostMeta>; export type HostMeta = InferSelectModel<typeof hostMeta>;
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>; export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;

View File

@@ -6,11 +6,6 @@ import logger from "@server/logger";
import SMTPTransport from "nodemailer/lib/smtp-transport"; import SMTPTransport from "nodemailer/lib/smtp-transport";
function createEmailClient() { function createEmailClient() {
if (config.isManagedMode()) {
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
return;
}
const emailConfig = config.getRawConfig().email; const emailConfig = config.getRawConfig().email;
if (!emailConfig) { if (!emailConfig) {
logger.warn( logger.warn(

View File

@@ -1,151 +0,0 @@
import logger from "@server/logger";
import config from "@server/lib/config";
import { createWebSocketClient } from "./routers/ws/client";
import { addPeer, deletePeer } from "./routers/gerbil/peers";
import { db, exitNodes } from "./db";
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager";
import { tokenManager } from "./lib/tokenManager";
import { APP_VERSION } from "./lib/consts";
import axios from "axios";
export async function createHybridClientServer() {
logger.info("Starting hybrid client server...");
// Start the token manager
await tokenManager.start();
const token = await tokenManager.getToken();
const monitor = new TraefikConfigManager();
await monitor.start();
// Create client
const client = createWebSocketClient(
token,
config.getRawConfig().managed!.endpoint!,
{
reconnectInterval: 5000,
pingInterval: 30000,
pingTimeout: 10000
}
);
// Register message handlers
client.registerHandler("remoteExitNode/peers/add", async (message) => {
const { publicKey, allowedIps } = message.data;
// TODO: we are getting the exit node twice here
// NOTE: there should only be one gerbil registered so...
const [exitNode] = await db.select().from(exitNodes).limit(1);
await addPeer(exitNode.exitNodeId, {
publicKey: publicKey,
allowedIps: allowedIps || []
});
});
client.registerHandler("remoteExitNode/peers/remove", async (message) => {
const { publicKey } = message.data;
// TODO: we are getting the exit node twice here
// NOTE: there should only be one gerbil registered so...
const [exitNode] = await db.select().from(exitNodes).limit(1);
await deletePeer(exitNode.exitNodeId, publicKey);
});
// /update-proxy-mapping
client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => {
try {
const [exitNode] = await db.select().from(exitNodes).limit(1);
if (!exitNode) {
logger.error("No exit node found for proxy mapping update");
return;
}
const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data);
logger.info(`Successfully updated proxy mapping: ${response.status}`);
} catch (error) {
// pull data out of the axios error to log
if (axios.isAxiosError(error)) {
logger.error("Error updating proxy mapping:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error updating proxy mapping:", error);
}
}
});
// /update-destinations
client.registerHandler("remoteExitNode/update-destinations", async (message) => {
try {
const [exitNode] = await db.select().from(exitNodes).limit(1);
if (!exitNode) {
logger.error("No exit node found for destinations update");
return;
}
const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data);
logger.info(`Successfully updated destinations: ${response.status}`);
} catch (error) {
// pull data out of the axios error to log
if (axios.isAxiosError(error)) {
logger.error("Error updating destinations:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error updating destinations:", error);
}
}
});
client.registerHandler("remoteExitNode/traefik/reload", async (message) => {
await monitor.HandleTraefikConfig();
});
// Listen to connection events
client.on("connect", () => {
logger.info("Connected to WebSocket server");
client.sendMessage("remoteExitNode/register", {
remoteExitNodeVersion: APP_VERSION
});
});
client.on("disconnect", () => {
logger.info("Disconnected from WebSocket server");
});
client.on("message", (message) => {
logger.info(
`Received message: ${message.type} ${JSON.stringify(message.data)}`
);
});
// Connect to the server
try {
await client.connect();
logger.info("Connection initiated");
} catch (error) {
logger.error("Failed to connect:", error);
}
// Store the ping interval stop function for cleanup if needed
const stopPingInterval = client.sendMessageInterval(
"remoteExitNode/ping",
{ timestamp: Date.now() / 1000 },
60000
); // send every minute
// Return client and cleanup function for potential use
return { client, stopPingInterval };
}

View File

@@ -5,9 +5,15 @@ import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer"; import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer"; import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer"; import { createInternalServer } from "./internalServer";
import { ApiKey, ApiKeyOrg, RemoteExitNode, Session, User, UserOrg } from "@server/db"; import {
ApiKey,
ApiKeyOrg,
RemoteExitNode,
Session,
User,
UserOrg
} from "@server/db";
import { createIntegrationApiServer } from "./integrationApiServer"; import { createIntegrationApiServer } from "./integrationApiServer";
import { createHybridClientServer } from "./hybridServer";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { setHostMeta } from "@server/lib/hostMeta"; import { setHostMeta } from "@server/lib/hostMeta";
import { initTelemetryClient } from "./lib/telemetry.js"; import { initTelemetryClient } from "./lib/telemetry.js";
@@ -26,16 +32,11 @@ async function startServers() {
const apiServer = createApiServer(); const apiServer = createApiServer();
const internalServer = createInternalServer(); const internalServer = createInternalServer();
let hybridClientServer;
let nextServer; let nextServer;
if (config.isManagedMode()) { nextServer = await createNextServer();
hybridClientServer = await createHybridClientServer(); if (config.getRawConfig().traefik.file_mode) {
} else { const monitor = new TraefikConfigManager();
nextServer = await createNextServer(); await monitor.start();
if (config.getRawConfig().traefik.file_mode) {
const monitor = new TraefikConfigManager();
await monitor.start();
}
} }
let integrationServer; let integrationServer;
@@ -49,8 +50,7 @@ async function startServers() {
apiServer, apiServer,
nextServer, nextServer,
internalServer, internalServer,
integrationServer, integrationServer
hybridClientServer
}; };
} }

View File

@@ -31,6 +31,17 @@ interface StripeEvent {
}; };
} }
export function noop() {
if (
build !== "saas" ||
!process.env.S3_BUCKET ||
!process.env.LOCAL_FILE_PATH
) {
return true;
}
return false;
}
export class UsageService { export class UsageService {
private cache: NodeCache; private cache: NodeCache;
private bucketName: string | undefined; private bucketName: string | undefined;
@@ -41,7 +52,7 @@ export class UsageService {
constructor() { constructor() {
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
if (build !== "saas") { if (noop()) {
return; return;
} }
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket; // this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
@@ -71,7 +82,9 @@ export class UsageService {
private async initializeEventsDirectory(): Promise<void> { private async initializeEventsDirectory(): Promise<void> {
if (!this.eventsDir) { if (!this.eventsDir) {
logger.warn("Stripe local file path is not configured, skipping events directory initialization."); logger.warn(
"Stripe local file path is not configured, skipping events directory initialization."
);
return; return;
} }
try { try {
@@ -83,7 +96,9 @@ export class UsageService {
private async uploadPendingEventFilesOnStartup(): Promise<void> { private async uploadPendingEventFilesOnStartup(): Promise<void> {
if (!this.eventsDir || !this.bucketName) { if (!this.eventsDir || !this.bucketName) {
logger.warn("Stripe local file path or bucket name is not configured, skipping leftover event file upload."); logger.warn(
"Stripe local file path or bucket name is not configured, skipping leftover event file upload."
);
return; return;
} }
try { try {
@@ -106,15 +121,17 @@ export class UsageService {
ContentType: "application/json" ContentType: "application/json"
}); });
await s3Client.send(uploadCommand); await s3Client.send(uploadCommand);
// Check if file still exists before unlinking // Check if file still exists before unlinking
try { try {
await fs.access(filePath); await fs.access(filePath);
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
logger.debug(`Startup file ${file} was already deleted`); logger.debug(
`Startup file ${file} was already deleted`
);
} }
logger.info( logger.info(
`Uploaded leftover event file ${file} to S3 with ${events.length} events` `Uploaded leftover event file ${file} to S3 with ${events.length} events`
); );
@@ -124,7 +141,9 @@ export class UsageService {
await fs.access(filePath); await fs.access(filePath);
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
logger.debug(`Empty startup file ${file} was already deleted`); logger.debug(
`Empty startup file ${file} was already deleted`
);
} }
} }
} catch (err) { } catch (err) {
@@ -135,8 +154,8 @@ export class UsageService {
} }
} }
} }
} catch (err) { } catch (error) {
logger.error("Failed to scan for leftover event files:", err); logger.error("Failed to scan for leftover event files");
} }
} }
@@ -146,17 +165,17 @@ export class UsageService {
value: number, value: number,
transaction: any = null transaction: any = null
): Promise<Usage | null> { ): Promise<Usage | null> {
if (build !== "saas") { if (noop()) {
return null; return null;
} }
// Truncate value to 11 decimal places // Truncate value to 11 decimal places
value = this.truncateValue(value); value = this.truncateValue(value);
// Implement retry logic for deadlock handling // Implement retry logic for deadlock handling
const maxRetries = 3; const maxRetries = 3;
let attempt = 0; let attempt = 0;
while (attempt <= maxRetries) { while (attempt <= maxRetries) {
try { try {
// Get subscription data for this org (with caching) // Get subscription data for this org (with caching)
@@ -179,7 +198,12 @@ export class UsageService {
); );
} else { } else {
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
usage = await this.internalAddUsage(orgId, featureId, value, trx); usage = await this.internalAddUsage(
orgId,
featureId,
value,
trx
);
}); });
} }
@@ -189,25 +213,26 @@ export class UsageService {
return usage || null; return usage || null;
} catch (error: any) { } catch (error: any) {
// Check if this is a deadlock error // Check if this is a deadlock error
const isDeadlock = error?.code === '40P01' || const isDeadlock =
error?.cause?.code === '40P01' || error?.code === "40P01" ||
(error?.message && error.message.includes('deadlock')); error?.cause?.code === "40P01" ||
(error?.message && error.message.includes("deadlock"));
if (isDeadlock && attempt < maxRetries) { if (isDeadlock && attempt < maxRetries) {
attempt++; attempt++;
// Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms // Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms
const baseDelay = Math.pow(2, attempt - 1) * 50; const baseDelay = Math.pow(2, attempt - 1) * 50;
const jitter = Math.random() * baseDelay; const jitter = Math.random() * baseDelay;
const delay = baseDelay + jitter; const delay = baseDelay + jitter;
logger.warn( logger.warn(
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms` `Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
); );
await new Promise(resolve => setTimeout(resolve, delay)); await new Promise((resolve) => setTimeout(resolve, delay));
continue; continue;
} }
logger.error( logger.error(
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`, `Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
error error
@@ -227,10 +252,10 @@ export class UsageService {
): Promise<Usage> { ): Promise<Usage> {
// Truncate value to 11 decimal places // Truncate value to 11 decimal places
value = this.truncateValue(value); value = this.truncateValue(value);
const usageId = `${orgId}-${featureId}`; const usageId = `${orgId}-${featureId}`;
const meterId = getFeatureMeterId(featureId); const meterId = getFeatureMeterId(featureId);
// Use upsert: insert if not exists, otherwise increment // Use upsert: insert if not exists, otherwise increment
const [returnUsage] = await trx const [returnUsage] = await trx
.insert(usage) .insert(usage)
@@ -247,7 +272,8 @@ export class UsageService {
set: { set: {
latestValue: sql`${usage.latestValue} + ${value}` latestValue: sql`${usage.latestValue} + ${value}`
} }
}).returning(); })
.returning();
return returnUsage; return returnUsage;
} }
@@ -268,7 +294,7 @@ export class UsageService {
value?: number, value?: number,
customerId?: string customerId?: string
): Promise<void> { ): Promise<void> {
if (build !== "saas") { if (noop()) {
return; return;
} }
try { try {
@@ -339,7 +365,7 @@ export class UsageService {
.set({ .set({
latestValue: newRunningTotal, latestValue: newRunningTotal,
instantaneousValue: value, instantaneousValue: value,
updatedAt: Math.floor(Date.now() / 1000) updatedAt: Math.floor(Date.now() / 1000)
}) })
.where(eq(usage.usageId, usageId)); .where(eq(usage.usageId, usageId));
} }
@@ -354,7 +380,7 @@ export class UsageService {
meterId, meterId,
instantaneousValue: truncatedValue, instantaneousValue: truncatedValue,
latestValue: truncatedValue, latestValue: truncatedValue,
updatedAt: Math.floor(Date.now() / 1000) updatedAt: Math.floor(Date.now() / 1000)
}); });
} }
}); });
@@ -415,7 +441,7 @@ export class UsageService {
): Promise<void> { ): Promise<void> {
// Truncate value to 11 decimal places before sending to Stripe // Truncate value to 11 decimal places before sending to Stripe
const truncatedValue = this.truncateValue(value); const truncatedValue = this.truncateValue(value);
const event: StripeEvent = { const event: StripeEvent = {
identifier: uuidv4(), identifier: uuidv4(),
timestamp: Math.floor(new Date().getTime() / 1000), timestamp: Math.floor(new Date().getTime() / 1000),
@@ -432,7 +458,9 @@ export class UsageService {
private async writeEventToFile(event: StripeEvent): Promise<void> { private async writeEventToFile(event: StripeEvent): Promise<void> {
if (!this.eventsDir || !this.bucketName) { if (!this.eventsDir || !this.bucketName) {
logger.warn("Stripe local file path or bucket name is not configured, skipping event file write."); logger.warn(
"Stripe local file path or bucket name is not configured, skipping event file write."
);
return; return;
} }
if (!this.currentEventFile) { if (!this.currentEventFile) {
@@ -481,7 +509,9 @@ export class UsageService {
private async uploadFileToS3(): Promise<void> { private async uploadFileToS3(): Promise<void> {
if (!this.bucketName || !this.eventsDir) { if (!this.bucketName || !this.eventsDir) {
logger.warn("Stripe local file path or bucket name is not configured, skipping S3 upload."); logger.warn(
"Stripe local file path or bucket name is not configured, skipping S3 upload."
);
return; return;
} }
if (!this.currentEventFile) { if (!this.currentEventFile) {
@@ -493,7 +523,9 @@ export class UsageService {
// Check if this file is already being uploaded // Check if this file is already being uploaded
if (this.uploadingFiles.has(fileName)) { if (this.uploadingFiles.has(fileName)) {
logger.debug(`File ${fileName} is already being uploaded, skipping`); logger.debug(
`File ${fileName} is already being uploaded, skipping`
);
return; return;
} }
@@ -505,7 +537,9 @@ export class UsageService {
try { try {
await fs.access(filePath); await fs.access(filePath);
} catch (error) { } catch (error) {
logger.debug(`File ${fileName} does not exist, may have been already processed`); logger.debug(
`File ${fileName} does not exist, may have been already processed`
);
this.uploadingFiles.delete(fileName); this.uploadingFiles.delete(fileName);
// Reset current file if it was this file // Reset current file if it was this file
if (this.currentEventFile === fileName) { if (this.currentEventFile === fileName) {
@@ -525,7 +559,9 @@ export class UsageService {
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
// File may have been already deleted // File may have been already deleted
logger.debug(`File ${fileName} was already deleted during cleanup`); logger.debug(
`File ${fileName} was already deleted during cleanup`
);
} }
this.currentEventFile = null; this.currentEventFile = null;
this.uploadingFiles.delete(fileName); this.uploadingFiles.delete(fileName);
@@ -548,7 +584,9 @@ export class UsageService {
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
// File may have been already deleted by another process // File may have been already deleted by another process
logger.debug(`File ${fileName} was already deleted during upload`); logger.debug(
`File ${fileName} was already deleted during upload`
);
} }
logger.info( logger.info(
@@ -559,10 +597,7 @@ export class UsageService {
this.currentEventFile = null; this.currentEventFile = null;
this.currentFileStartTime = 0; this.currentFileStartTime = 0;
} catch (error) { } catch (error) {
logger.error( logger.error(`Failed to upload ${fileName} to S3:`, error);
`Failed to upload ${fileName} to S3:`,
error
);
} finally { } finally {
// Always remove from uploading set // Always remove from uploading set
this.uploadingFiles.delete(fileName); this.uploadingFiles.delete(fileName);
@@ -579,7 +614,7 @@ export class UsageService {
orgId: string, orgId: string,
featureId: FeatureId featureId: FeatureId
): Promise<Usage | null> { ): Promise<Usage | null> {
if (build !== "saas") { if (noop()) {
return null; return null;
} }
@@ -598,7 +633,7 @@ export class UsageService {
`Creating new usage record for ${orgId}/${featureId}` `Creating new usage record for ${orgId}/${featureId}`
); );
const meterId = getFeatureMeterId(featureId); const meterId = getFeatureMeterId(featureId);
try { try {
const [newUsage] = await db const [newUsage] = await db
.insert(usage) .insert(usage)
@@ -653,7 +688,7 @@ export class UsageService {
orgId: string, orgId: string,
featureId: FeatureId featureId: FeatureId
): Promise<Usage | null> { ): Promise<Usage | null> {
if (build !== "saas") { if (noop()) {
return null; return null;
} }
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
@@ -673,7 +708,9 @@ export class UsageService {
*/ */
private async uploadOldEventFiles(): Promise<void> { private async uploadOldEventFiles(): Promise<void> {
if (!this.eventsDir || !this.bucketName) { if (!this.eventsDir || !this.bucketName) {
logger.warn("Stripe local file path or bucket name is not configured, skipping old event file upload."); logger.warn(
"Stripe local file path or bucket name is not configured, skipping old event file upload."
);
return; return;
} }
try { try {
@@ -681,15 +718,17 @@ export class UsageService {
const now = Date.now(); const now = Date.now();
for (const file of files) { for (const file of files) {
if (!file.endsWith(".json")) continue; if (!file.endsWith(".json")) continue;
// Skip files that are already being uploaded // Skip files that are already being uploaded
if (this.uploadingFiles.has(file)) { if (this.uploadingFiles.has(file)) {
logger.debug(`Skipping file ${file} as it's already being uploaded`); logger.debug(
`Skipping file ${file} as it's already being uploaded`
);
continue; continue;
} }
const filePath = path.join(this.eventsDir, file); const filePath = path.join(this.eventsDir, file);
try { try {
// Check if file still exists before processing // Check if file still exists before processing
try { try {
@@ -704,7 +743,7 @@ export class UsageService {
if (age >= 90000) { if (age >= 90000) {
// 1.5 minutes - Mark as being uploaded // 1.5 minutes - Mark as being uploaded
this.uploadingFiles.add(file); this.uploadingFiles.add(file);
try { try {
const fileContent = await fs.readFile( const fileContent = await fs.readFile(
filePath, filePath,
@@ -720,15 +759,17 @@ export class UsageService {
ContentType: "application/json" ContentType: "application/json"
}); });
await s3Client.send(uploadCommand); await s3Client.send(uploadCommand);
// Check if file still exists before unlinking // Check if file still exists before unlinking
try { try {
await fs.access(filePath); await fs.access(filePath);
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
logger.debug(`File ${file} was already deleted during interval upload`); logger.debug(
`File ${file} was already deleted during interval upload`
);
} }
logger.info( logger.info(
`Interval: Uploaded event file ${file} to S3 with ${events.length} events` `Interval: Uploaded event file ${file} to S3 with ${events.length} events`
); );
@@ -743,7 +784,9 @@ export class UsageService {
await fs.access(filePath); await fs.access(filePath);
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
logger.debug(`Empty file ${file} was already deleted`); logger.debug(
`Empty file ${file} was already deleted`
);
} }
} }
} finally { } finally {
@@ -765,12 +808,17 @@ export class UsageService {
} }
} }
public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise<boolean> { public async checkLimitSet(
if (build !== "saas") { orgId: string,
kickSites = false,
featureId?: FeatureId,
usage?: Usage
): Promise<boolean> {
if (noop()) {
return false; return false;
} }
// This method should check the current usage against the limits set for the organization // This method should check the current usage against the limits set for the organization
// and kick out all of the sites on the org // and kick out all of the sites on the org
let hasExceededLimits = false; let hasExceededLimits = false;
try { try {
@@ -805,16 +853,30 @@ export class UsageService {
if (usage) { if (usage) {
currentUsage = usage; currentUsage = usage;
} else { } else {
currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId); currentUsage = await this.getUsage(
orgId,
limit.featureId as FeatureId
);
} }
const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 0; const usageValue =
logger.debug(`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`); currentUsage?.instantaneousValue ||
logger.debug(`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`); currentUsage?.latestValue ||
if (currentUsage && limit.value !== null && usageValue > limit.value) { 0;
logger.debug(
`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`
);
logger.debug(
`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`
);
if (
currentUsage &&
limit.value !== null &&
usageValue > limit.value
) {
logger.debug( logger.debug(
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` + `Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
`${usageValue} > ${limit.value}` `${usageValue} > ${limit.value}`
); );
hasExceededLimits = true; hasExceededLimits = true;
break; // Exit early if any limit is exceeded break; // Exit early if any limit is exceeded
@@ -823,7 +885,9 @@ export class UsageService {
// If any limits are exceeded, disconnect all sites for this organization // If any limits are exceeded, disconnect all sites for this organization
if (hasExceededLimits && kickSites) { if (hasExceededLimits && kickSites) {
logger.warn(`Disconnecting all sites for org ${orgId} due to exceeded limits`); logger.warn(
`Disconnecting all sites for org ${orgId} due to exceeded limits`
);
// Get all sites for this organization // Get all sites for this organization
const orgSites = await db const orgSites = await db
@@ -832,7 +896,7 @@ export class UsageService {
.where(eq(sites.orgId, orgId)); .where(eq(sites.orgId, orgId));
// Mark all sites as offline and send termination messages // Mark all sites as offline and send termination messages
const siteUpdates = orgSites.map(site => site.siteId); const siteUpdates = orgSites.map((site) => site.siteId);
if (siteUpdates.length > 0) { if (siteUpdates.length > 0) {
// Send termination messages to newt sites // Send termination messages to newt sites
@@ -853,17 +917,21 @@ export class UsageService {
}; };
// Don't await to prevent blocking // Don't await to prevent blocking
sendToClient(newt.newtId, payload).catch((error: any) => { sendToClient(newt.newtId, payload).catch(
logger.error( (error: any) => {
`Failed to send termination message to newt ${newt.newtId}:`, logger.error(
error `Failed to send termination message to newt ${newt.newtId}:`,
); error
}); );
}
);
} }
} }
} }
logger.info(`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`); logger.info(
`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
);
} }
} }
} catch (error) { } catch (error) {

View File

@@ -1,70 +1,3 @@
import axios from "axios";
import { tokenManager } from "./tokenManager";
import logger from "@server/logger";
import config from "./config";
/**
* Get valid certificates for the specified domains
*/
export async function getValidCertificatesForDomainsHybrid(domains: Set<string>): Promise<
Array<{
id: number;
domain: string;
wildcard: boolean | null;
certFile: string | null;
keyFile: string | null;
expiresAt: number | null;
updatedAt?: number | null;
}>
> {
if (domains.size === 0) {
return [];
}
const domainArray = Array.from(domains);
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`,
{
params: {
domains: domainArray
},
headers: (await tokenManager.getAuthHeader()).headers
}
);
if (response.status !== 200) {
logger.error(
`Failed to fetch certificates for domains: ${response.status} ${response.statusText}`,
{ responseData: response.data, domains: domainArray }
);
return [];
}
// logger.debug(
// `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains`
// );
return response.data.data;
} catch (error) {
// pull data out of the axios error to log
if (axios.isAxiosError(error)) {
logger.error("Error getting certificates:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error getting certificates:", error);
}
return [];
}
}
export async function getValidCertificatesForDomains(domains: Set<string>): Promise< export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
Array<{ Array<{
id: number; id: number;

View File

@@ -102,10 +102,7 @@ export class Config {
if (!this.rawConfig) { if (!this.rawConfig) {
throw new Error("Config not loaded. Call load() first."); throw new Error("Config not loaded. Call load() first.");
} }
if (this.rawConfig.managed) {
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN MANAGED
return;
}
license.setServerSecret(this.rawConfig.server.secret!); license.setServerSecret(this.rawConfig.server.secret!);
await this.checkKeyStatus(); await this.checkKeyStatus();
@@ -158,10 +155,6 @@ export class Config {
return false; return false;
} }
public isManagedMode() {
return typeof this.rawConfig?.managed === "object";
}
public async checkSupporterKey() { public async checkSupporterKey() {
const [key] = await db.select().from(supporterKey).limit(1); const [key] = await db.select().from(supporterKey).limit(1);

View File

@@ -15,8 +15,7 @@ import {
} from "@server/db"; } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { defaultRoleAllowedActions } from "@server/routers/role"; import { defaultRoleAllowedActions } from "@server/routers/role";
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing"; import { FeatureId, limitsService, sandboxLimitSet, createCustomer } from "@server/lib/billing";
import { createCustomer } from "@server/private/lib/billing/createCustomer";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
export async function createUserAccountOrg( export async function createUserAccountOrg(

View File

@@ -1,8 +1,5 @@
import logger from "@server/logger"; import logger from "@server/logger";
import { maxmindLookup } from "@server/db/maxmind"; import { maxmindLookup } from "@server/db/maxmind";
import axios from "axios";
import config from "./config";
import { tokenManager } from "./tokenManager";
export async function getCountryCodeForIp( export async function getCountryCodeForIp(
ip: string ip: string
@@ -33,32 +30,4 @@ export async function getCountryCodeForIp(
} }
return; return;
} }
export async function remoteGetCountryCodeForIp(
ip: string
): Promise<string | undefined> {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/geoip/${ip}`,
await tokenManager.getAuthHeader()
);
return response.data.data.countryCode;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
}
return;
}

View File

@@ -42,18 +42,6 @@ export const configSchema = z
anonymous_usage: true anonymous_usage: true
} }
}), }),
managed: z
.object({
name: z.string().optional(),
id: z.string().optional(),
secret: z.string().optional(),
endpoint: z
.string()
.optional()
.default("https://pangolin.fossorial.io"),
redirect_endpoint: z.string().optional()
})
.optional(),
domains: z domains: z
.record( .record(
z.string(), z.string(),
@@ -346,10 +334,7 @@ export const configSchema = z
if (data.flags?.disable_config_managed_domains) { if (data.flags?.disable_config_managed_domains) {
return true; return true;
} }
// If hybrid is defined, domains are not required
if (data.managed) {
return true;
}
if (keys.length === 0) { if (keys.length === 0) {
return false; return false;
} }
@@ -361,10 +346,6 @@ export const configSchema = z
) )
.refine( .refine(
(data) => { (data) => {
// If hybrid is defined, server secret is not required
if (data.managed) {
return true;
}
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env // If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env
if (data.server?.secret === undefined) { if (data.server?.secret === undefined) {
data.server.secret = process.env.SERVER_SECRET; data.server.secret = process.env.SERVER_SECRET;
@@ -380,10 +361,6 @@ export const configSchema = z
) )
.refine( .refine(
(data) => { (data) => {
// If hybrid is defined, dashboard_url is not required
if (data.managed) {
return true;
}
// If hybrid is not defined, dashboard_url must be defined // If hybrid is not defined, dashboard_url must be defined
return ( return (
data.app.dashboard_url !== undefined && data.app.dashboard_url !== undefined &&

View File

@@ -1,73 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { Router } from "express";
import axios from "axios";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import config from "@server/lib/config";
import { tokenManager } from "./tokenManager";
/**
* Proxy function that forwards requests to the remote cloud server
*/
export const proxyToRemote = async (
req: Request,
res: Response,
next: NextFunction,
endpoint: string
): Promise<any> => {
try {
const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`;
logger.debug(`Proxying request to remote server: ${remoteUrl}`);
// Forward the request to the remote server
const response = await axios({
method: req.method as any,
url: remoteUrl,
data: req.body,
headers: {
'Content-Type': 'application/json',
...(await tokenManager.getAuthHeader()).headers
},
params: req.query,
timeout: 30000, // 30 second timeout
validateStatus: () => true // Don't throw on non-2xx status codes
});
logger.debug(`Proxy response: ${JSON.stringify(response.data)}`);
// Forward the response status and data
return res.status(response.status).json(response.data);
} catch (error) {
logger.error("Error proxying request to remote server:", error);
if (axios.isAxiosError(error)) {
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
return next(
createHttpError(
HttpCode.SERVICE_UNAVAILABLE,
"Remote server is unavailable"
)
);
}
if (error.code === 'ECONNABORTED') {
return next(
createHttpError(
HttpCode.REQUEST_TIMEOUT,
"Request to remote server timed out"
)
);
}
}
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error communicating with remote server"
)
);
}
};

View File

@@ -1,274 +0,0 @@
import axios from "axios";
import config from "@server/lib/config";
import logger from "@server/logger";
export interface TokenResponse {
success: boolean;
message?: string;
data: {
token: string;
};
}
/**
* Token Manager - Handles automatic token refresh for hybrid server authentication
*
* Usage throughout the application:
* ```typescript
* import { tokenManager } from "@server/lib/tokenManager";
*
* // Get the current valid token
* const token = await tokenManager.getToken();
*
* // Force refresh if needed
* await tokenManager.refreshToken();
* ```
*
* The token manager automatically refreshes tokens every 24 hours by default
* and is started once in the privateHybridServer.ts file.
*/
export class TokenManager {
private token: string | null = null;
private refreshInterval: NodeJS.Timeout | null = null;
private isRefreshing: boolean = false;
private refreshIntervalMs: number;
private retryInterval: NodeJS.Timeout | null = null;
private retryIntervalMs: number;
private tokenAvailablePromise: Promise<void> | null = null;
private tokenAvailableResolve: (() => void) | null = null;
constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) {
// Default to 24 hours for refresh, 5 seconds for retry
this.refreshIntervalMs = refreshIntervalMs;
this.retryIntervalMs = retryIntervalMs;
this.setupTokenAvailablePromise();
}
/**
* Set up promise that resolves when token becomes available
*/
private setupTokenAvailablePromise(): void {
this.tokenAvailablePromise = new Promise((resolve) => {
this.tokenAvailableResolve = resolve;
});
}
/**
* Resolve the token available promise
*/
private resolveTokenAvailable(): void {
if (this.tokenAvailableResolve) {
this.tokenAvailableResolve();
this.tokenAvailableResolve = null;
}
}
/**
* Start the token manager - gets initial token and sets up refresh interval
* If initial token fetch fails, keeps retrying every few seconds until successful
*/
async start(): Promise<void> {
logger.info("Starting token manager...");
try {
await this.refreshToken();
this.setupRefreshInterval();
this.resolveTokenAvailable();
logger.info("Token manager started successfully");
} catch (error) {
logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error);
this.setupRetryInterval();
}
}
/**
* Set up retry interval for initial token acquisition
*/
private setupRetryInterval(): void {
if (this.retryInterval) {
clearInterval(this.retryInterval);
}
this.retryInterval = setInterval(async () => {
try {
logger.debug("Retrying initial token acquisition");
await this.refreshToken();
this.setupRefreshInterval();
this.clearRetryInterval();
this.resolveTokenAvailable();
logger.info("Token manager started successfully after retry");
} catch (error) {
logger.debug("Token acquisition retry failed, will try again");
}
}, this.retryIntervalMs);
}
/**
* Clear retry interval
*/
private clearRetryInterval(): void {
if (this.retryInterval) {
clearInterval(this.retryInterval);
this.retryInterval = null;
}
}
/**
* Stop the token manager and clear all intervals
*/
stop(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
this.clearRetryInterval();
logger.info("Token manager stopped");
}
/**
* Get the current valid token
*/
// TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT
async getToken(): Promise<string> {
// If we don't have a token yet, wait for it to become available
if (!this.token && this.tokenAvailablePromise) {
await this.tokenAvailablePromise;
}
if (!this.token) {
if (this.isRefreshing) {
// Wait for current refresh to complete
await this.waitForRefresh();
} else {
throw new Error("No valid token available");
}
}
if (!this.token) {
throw new Error("No valid token available");
}
return this.token;
}
async getAuthHeader() {
return {
headers: {
Authorization: `Bearer ${await this.getToken()}`,
"X-CSRF-Token": "x-csrf-protection",
}
};
}
/**
* Force refresh the token
*/
async refreshToken(): Promise<void> {
if (this.isRefreshing) {
await this.waitForRefresh();
return;
}
this.isRefreshing = true;
try {
const hybridConfig = config.getRawConfig().managed;
if (
!hybridConfig?.id ||
!hybridConfig?.secret ||
!hybridConfig?.endpoint
) {
throw new Error("Hybrid configuration is not defined");
}
const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`;
const tokenData = {
remoteExitNodeId: hybridConfig.id,
secret: hybridConfig.secret
};
logger.debug("Requesting new token from server");
const response = await axios.post<TokenResponse>(
tokenEndpoint,
tokenData,
{
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": "x-csrf-protection"
},
timeout: 10000 // 10 second timeout
}
);
if (!response.data.success) {
throw new Error(
`Failed to get token: ${response.data.message}`
);
}
if (!response.data.data.token) {
throw new Error("Received empty token from server");
}
this.token = response.data.data.token;
logger.debug("Token refreshed successfully");
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error updating proxy mapping:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error updating proxy mapping:", error);
}
throw new Error("Failed to refresh token");
} finally {
this.isRefreshing = false;
}
}
/**
* Set up automatic token refresh interval
*/
private setupRefreshInterval(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
this.refreshInterval = setInterval(async () => {
try {
logger.debug("Auto-refreshing token");
await this.refreshToken();
} catch (error) {
logger.error("Failed to auto-refresh token:", error);
}
}, this.refreshIntervalMs);
}
/**
* Wait for current refresh operation to complete
*/
private async waitForRefresh(): Promise<void> {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (!this.isRefreshing) {
clearInterval(checkInterval);
resolve();
}
}, 100);
});
}
}
// Export a singleton instance for use throughout the application
export const tokenManager = new TokenManager();

View File

@@ -6,13 +6,9 @@ import * as yaml from "js-yaml";
import axios from "axios"; import axios from "axios";
import { db, exitNodes } from "@server/db"; import { db, exitNodes } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { tokenManager } from "../tokenManager";
import { getCurrentExitNodeId } from "@server/lib/exitNodes"; import { getCurrentExitNodeId } from "@server/lib/exitNodes";
import { getTraefikConfig } from "#dynamic/lib/traefik"; import { getTraefikConfig } from "#dynamic/lib/traefik";
import { import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
getValidCertificatesForDomains,
getValidCertificatesForDomainsHybrid
} from "#dynamic/lib/certificates";
import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { build } from "@server/build"; import { build } from "@server/build";
@@ -313,93 +309,92 @@ export class TraefikConfigManager {
this.lastActiveDomains = new Set(domains); this.lastActiveDomains = new Set(domains);
} }
// Scan current local certificate state if (
this.lastLocalCertificateState = process.env.GENERATE_OWN_CERTIFICATES === "true" &&
await this.scanLocalCertificateState(); build != "oss"
) {
// Scan current local certificate state
this.lastLocalCertificateState =
await this.scanLocalCertificateState();
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check) // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
let validCertificates: Array<{ let validCertificates: Array<{
id: number; id: number;
domain: string; domain: string;
wildcard: boolean | null; wildcard: boolean | null;
certFile: string | null; certFile: string | null;
keyFile: string | null; keyFile: string | null;
expiresAt: number | null; expiresAt: number | null;
updatedAt?: number | null; updatedAt?: number | null;
}> = []; }> = [];
if (this.shouldFetchCertificates(domains)) { if (this.shouldFetchCertificates(domains)) {
// Filter out domains that are already covered by wildcard certificates // Filter out domains that are already covered by wildcard certificates
const domainsToFetch = new Set<string>(); const domainsToFetch = new Set<string>();
for (const domain of domains) { for (const domain of domains) {
if ( if (
!isDomainCoveredByWildcard( !isDomainCoveredByWildcard(
domain, domain,
this.lastLocalCertificateState this.lastLocalCertificateState
) )
) { ) {
domainsToFetch.add(domain); domainsToFetch.add(domain);
} else { } else {
logger.debug( logger.debug(
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch` `Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
);
}
}
if (domainsToFetch.size > 0) {
// Get valid certificates for domains not covered by wildcards
if (config.isManagedMode()) {
validCertificates =
await getValidCertificatesForDomainsHybrid(
domainsToFetch
); );
} else { }
}
if (domainsToFetch.size > 0) {
// Get valid certificates for domains not covered by wildcards
validCertificates = validCertificates =
await getValidCertificatesForDomains( await getValidCertificatesForDomains(
domainsToFetch domainsToFetch
); );
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
logger.info(
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
);
// Download and decrypt new certificates
await this.processValidCertificates(validCertificates);
} else {
logger.info(
"All domains are covered by existing wildcard certificates, no fetch needed"
);
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
} }
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
logger.info( // Always ensure all existing certificates (including wildcards) are in the config
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)` await this.updateDynamicConfigFromLocalCerts(domains);
);
// Download and decrypt new certificates
await this.processValidCertificates(validCertificates);
} else { } else {
logger.info( const timeSinceLastFetch = this.lastCertificateFetch
"All domains are covered by existing wildcard certificates, no fetch needed" ? Math.round(
); (Date.now() -
this.lastCertificateFetch = new Date(); this.lastCertificateFetch.getTime()) /
this.lastKnownDomains = new Set(domains); (1000 * 60)
)
: 0;
// logger.debug(
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
// );
// Still need to ensure config is up to date with existing certificates
await this.updateDynamicConfigFromLocalCerts(domains);
} }
// Always ensure all existing certificates (including wildcards) are in the config // Clean up certificates for domains no longer in use
await this.updateDynamicConfigFromLocalCerts(domains); await this.cleanupUnusedCertificates(domains);
} else {
const timeSinceLastFetch = this.lastCertificateFetch
? Math.round(
(Date.now() - this.lastCertificateFetch.getTime()) /
(1000 * 60)
)
: 0;
// logger.debug( // wait 1 second for traefik to pick up the new certificates
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` await new Promise((resolve) => setTimeout(resolve, 500));
// );
// Still need to ensure config is up to date with existing certificates
await this.updateDynamicConfigFromLocalCerts(domains);
} }
// Clean up certificates for domains no longer in use
await this.cleanupUnusedCertificates(domains);
// wait 1 second for traefik to pick up the new certificates
await new Promise((resolve) => setTimeout(resolve, 500));
// Write traefik config as YAML to a second dynamic config file if changed // Write traefik config as YAML to a second dynamic config file if changed
await this.writeTraefikDynamicConfig(traefikConfig); await this.writeTraefikDynamicConfig(traefikConfig);
@@ -448,32 +443,15 @@ export class TraefikConfigManager {
} | null> { } | null> {
let traefikConfig; let traefikConfig;
try { try {
if (config.isManagedMode()) { const currentExitNode = await getCurrentExitNodeId();
const resp = await axios.get( // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`, traefikConfig = await getTraefikConfig(
await tokenManager.getAuthHeader() // this is called by the local exit node to get its own config
); currentExitNode,
config.getRawConfig().traefik.site_types,
if (resp.status !== 200) { build == "oss", // filter out the namespace domains in open source
logger.error( build != "oss" // generate the login pages on the cloud and hybrid
`Failed to fetch traefik config: ${resp.status} ${resp.statusText}`, );
{ responseData: resp.data }
);
return null;
}
traefikConfig = resp.data.data;
} else {
const currentExitNode = await getCurrentExitNodeId();
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
traefikConfig = await getTraefikConfig(
// this is called by the local exit node to get its own config
currentExitNode,
config.getRawConfig().traefik.site_types,
build == "oss", // filter out the namespace domains in open source
build != "oss" // generate the login pages on the cloud and hybrid
);
}
const domains = new Set<string>(); const domains = new Set<string>();
@@ -718,7 +696,12 @@ export class TraefikConfigManager {
for (const cert of validCertificates) { for (const cert of validCertificates) {
try { try {
if (!cert.certFile || !cert.keyFile) { if (
!cert.certFile ||
!cert.keyFile ||
cert.certFile.length === 0 ||
cert.keyFile.length === 0
) {
logger.warn( logger.warn(
`Certificate for domain ${cert.domain} is missing cert or key file` `Certificate for domain ${cert.domain} is missing cert or key file`
); );
@@ -842,7 +825,9 @@ export class TraefikConfigManager {
const lastUpdateStr = fs const lastUpdateStr = fs
.readFileSync(lastUpdatePath, "utf8") .readFileSync(lastUpdatePath, "utf8")
.trim(); .trim();
lastUpdateTime = Math.floor(new Date(lastUpdateStr).getTime() / 1000); lastUpdateTime = Math.floor(
new Date(lastUpdateStr).getTime() / 1000
);
} catch { } catch {
lastUpdateTime = null; lastUpdateTime = null;
} }

View File

@@ -105,7 +105,12 @@ export async function getTraefikConfig(
const priority = row.priority ?? 100; const priority = row.priority ?? 100;
// Create a unique key combining resourceId, path config, and rewrite config // Create a unique key combining resourceId, path config, and rewrite config
const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType] const pathKey = [
targetPath,
pathMatchType,
rewritePath,
rewritePathType
]
.filter(Boolean) .filter(Boolean)
.join("-"); .join("-");
const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
@@ -120,13 +125,15 @@ export async function getTraefikConfig(
); );
if (!validation.isValid) { if (!validation.isValid) {
logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`); logger.error(
`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
);
return; return;
} }
resourcesMap.set(key, { resourcesMap.set(key, {
resourceId: row.resourceId, resourceId: row.resourceId,
name: resourceName, name: resourceName,
fullDomain: row.fullDomain, fullDomain: row.fullDomain,
ssl: row.ssl, ssl: row.ssl,
http: row.http, http: row.http,
@@ -239,21 +246,18 @@ export async function getTraefikConfig(
preferWildcardCert = configDomain.prefer_wildcard_cert; preferWildcardCert = configDomain.prefer_wildcard_cert;
} }
let tls = {}; const tls = {
if (build == "oss") { certResolver: certResolver,
tls = { ...(preferWildcardCert
certResolver: certResolver, ? {
...(preferWildcardCert domains: [
? { {
domains: [ main: wildCard
{ }
main: wildCard ]
} }
] : {})
} };
: {})
};
}
const additionalMiddlewares = const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || []; config.getRawConfig().traefik.additional_middlewares || [];
@@ -264,11 +268,12 @@ export async function getTraefikConfig(
]; ];
// Handle path rewriting middleware // Handle path rewriting middleware
if (resource.rewritePath && if (
resource.rewritePath &&
resource.path && resource.path &&
resource.pathMatchType && resource.pathMatchType &&
resource.rewritePathType) { resource.rewritePathType
) {
// Create a unique middleware name // Create a unique middleware name
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`; const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
@@ -287,7 +292,10 @@ export async function getTraefikConfig(
} }
// the middleware to the config // the middleware to the config
Object.assign(config_output.http.middlewares, rewriteResult.middlewares); Object.assign(
config_output.http.middlewares,
rewriteResult.middlewares
);
// middlewares to the router middleware chain // middlewares to the router middleware chain
if (rewriteResult.chain) { if (rewriteResult.chain) {
@@ -298,9 +306,13 @@ export async function getTraefikConfig(
routerMiddlewares.push(rewriteMiddlewareName); routerMiddlewares.push(rewriteMiddlewareName);
} }
logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); logger.debug(
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
);
} catch (error) { } catch (error) {
logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`); logger.error(
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
);
} }
} }
@@ -316,7 +328,9 @@ export async function getTraefikConfig(
value: string; value: string;
}[]; }[];
} catch (e) { } catch (e) {
logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`); logger.warn(
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
);
} }
headersArr.forEach((header) => { headersArr.forEach((header) => {
@@ -482,14 +496,14 @@ export async function getTraefikConfig(
})(), })(),
...(resource.stickySession ...(resource.stickySession
? { ? {
sticky: { sticky: {
cookie: { cookie: {
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
secure: resource.ssl, secure: resource.ssl,
httpOnly: true httpOnly: true
} }
} }
} }
: {}) : {})
} }
}; };
@@ -590,13 +604,13 @@ export async function getTraefikConfig(
})(), })(),
...(resource.stickySession ...(resource.stickySession
? { ? {
sticky: { sticky: {
ipStrategy: { ipStrategy: {
depth: 0, depth: 0,
sourcePort: true sourcePort: true
} }
} }
} }
: {}) : {})
} }
}; };

View File

@@ -1,3 +1,16 @@
/*
* 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 { rateLimitService } from "#private/lib/rateLimit"; import { rateLimitService } from "#private/lib/rateLimit";
import { cleanup as wsCleanup } from "#private/routers/ws"; import { cleanup as wsCleanup } from "#private/routers/ws";

View File

@@ -97,20 +97,4 @@ export async function getValidCertificatesForDomains(
}); });
return validCertsDecrypted; return validCertsDecrypted;
} }
export async function getValidCertificatesForDomainsHybrid(
domains: Set<string>
): Promise<
Array<{
id: number;
domain: string;
wildcard: boolean | null;
certFile: string | null;
keyFile: string | null;
expiresAt: number | null;
updatedAt?: number | null;
}>
> {
return []; // stub
}

View File

@@ -146,6 +146,10 @@ export class PrivateConfig {
if (parsedPrivateConfig.stripe?.s3Region) { if (parsedPrivateConfig.stripe?.s3Region) {
process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region; process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
} }
if (parsedPrivateConfig.flags?.generate_own_certificates) {
process.env.GENERATE_OWN_CERTIFICATES =
parsedPrivateConfig.flags.generate_own_certificates.toString();
}
} }
this.rawPrivateConfig = parsedPrivateConfig; this.rawPrivateConfig = parsedPrivateConfig;

View File

@@ -12,7 +12,7 @@
*/ */
import logger from "@server/logger"; import logger from "@server/logger";
import redisManager from "@server/private/lib/redis"; import redisManager from "#private/lib/redis";
import { build } from "@server/build"; import { build } from "@server/build";
// Rate limiting configuration // Rate limiting configuration

View File

@@ -17,7 +17,7 @@ import { MemoryStore, Store } from "express-rate-limit";
import RedisStore from "#private/lib/redisStore"; import RedisStore from "#private/lib/redisStore";
export function createStore(): Store { export function createStore(): Store {
if (build != "oss" && privateConfig.getRawPrivateConfig().flags?.enable_redis) { if (build != "oss" && privateConfig.getRawPrivateConfig().flags.enable_redis) {
const rateLimitStore: Store = new RedisStore({ const rateLimitStore: Store = new RedisStore({
prefix: "api-rate-limit", // Optional: customize Redis key prefix prefix: "api-rate-limit", // Optional: customize Redis key prefix
skipFailedRequests: true, // Don't count failed requests skipFailedRequests: true, // Don't count failed requests

View File

@@ -20,15 +20,18 @@ import { build } from "@server/build";
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
export const privateConfigSchema = z export const privateConfigSchema = z.object({
.object({ app: z
app: z.object({ .object({
region: z.string().optional().default("default"), region: z.string().optional().default("default"),
base_domain: z.string().optional() base_domain: z.string().optional()
}).optional().default({ })
.optional()
.default({
region: "default" region: "default"
}), }),
server: z.object({ server: z
.object({
encryption_key_path: z encryption_key_path: z
.string() .string()
.optional() .optional()
@@ -37,125 +40,132 @@ export const privateConfigSchema = z
resend_api_key: z.string().optional(), resend_api_key: z.string().optional(),
reo_client_id: z.string().optional(), reo_client_id: z.string().optional(),
fossorial_api_key: z.string().optional() fossorial_api_key: z.string().optional()
}).optional().default({ })
.optional()
.default({
encryption_key_path: "./config/encryption.pem" encryption_key_path: "./config/encryption.pem"
}), }),
redis: z redis: z
.object({ .object({
host: z.string(), host: z.string(),
port: portSchema, port: portSchema,
password: z.string().optional(), password: z.string().optional(),
db: z.number().int().nonnegative().optional().default(0), db: z.number().int().nonnegative().optional().default(0),
replicas: z replicas: z
.array( .array(
z.object({ z.object({
host: z.string(), host: z.string(),
port: portSchema, port: portSchema,
password: z.string().optional(), password: z.string().optional(),
db: z.number().int().nonnegative().optional().default(0) db: z.number().int().nonnegative().optional().default(0)
})
)
.optional()
// tls: z
// .object({
// reject_unauthorized: z
// .boolean()
// .optional()
// .default(true)
// })
// .optional()
})
.optional(),
gerbil: z
.object({
local_exit_node_reachable_at: z
.string()
.optional()
.default("http://gerbil:3003")
})
.optional()
.default({}),
flags: z
.object({
enable_redis: z.boolean().optional().default(false),
generate_own_certificates: z.boolean().optional().default(false)
})
.optional()
.default({}),
branding: z
.object({
app_name: z.string().optional(),
background_image_path: z.string().optional(),
colors: z
.object({
light: colorsSchema.optional(),
dark: colorsSchema.optional()
})
.optional(),
logo: z
.object({
light_path: z.string().optional(),
dark_path: z.string().optional(),
auth_page: z
.object({
width: z.number().optional(),
height: z.number().optional()
}) })
) .optional(),
.optional() navbar: z
// tls: z .object({
// .object({ width: z.number().optional(),
// reject_unauthorized: z height: z.number().optional()
// .boolean()
// .optional()
// .default(true)
// })
// .optional()
})
.optional(),
gerbil: z
.object({
local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003")
})
.optional()
.default({}),
flags: z
.object({
enable_redis: z.boolean().optional(),
})
.optional(),
branding: z
.object({
app_name: z.string().optional(),
background_image_path: z.string().optional(),
colors: z
.object({
light: colorsSchema.optional(),
dark: colorsSchema.optional()
})
.optional(),
logo: z
.object({
light_path: z.string().optional(),
dark_path: z.string().optional(),
auth_page: z
.object({
width: z.number().optional(),
height: z.number().optional()
})
.optional(),
navbar: z
.object({
width: z.number().optional(),
height: z.number().optional()
})
.optional()
})
.optional(),
favicon_path: z.string().optional(),
footer: z
.array(
z.object({
text: z.string(),
href: z.string().optional()
}) })
) .optional()
.optional(), })
login_page: z .optional(),
.object({ favicon_path: z.string().optional(),
subtitle_text: z.string().optional(), footer: z
title_text: z.string().optional() .array(
z.object({
text: z.string(),
href: z.string().optional()
}) })
.optional(), )
signup_page: z .optional(),
.object({ login_page: z
subtitle_text: z.string().optional(), .object({
title_text: z.string().optional() subtitle_text: z.string().optional(),
}) title_text: z.string().optional()
.optional(), })
resource_auth_page: z .optional(),
.object({ signup_page: z
show_logo: z.boolean().optional(), .object({
hide_powered_by: z.boolean().optional(), subtitle_text: z.string().optional(),
title_text: z.string().optional(), title_text: z.string().optional()
subtitle_text: z.string().optional() })
}) .optional(),
.optional(), resource_auth_page: z
emails: z .object({
.object({ show_logo: z.boolean().optional(),
signature: z.string().optional(), hide_powered_by: z.boolean().optional(),
colors: z title_text: z.string().optional(),
.object({ subtitle_text: z.string().optional()
primary: z.string().optional() })
}) .optional(),
.optional() emails: z
}) .object({
.optional() signature: z.string().optional(),
}) colors: z
.optional(), .object({
stripe: z primary: z.string().optional()
.object({ })
secret_key: z.string(), .optional()
webhook_secret: z.string(), })
s3Bucket: z.string(), .optional()
s3Region: z.string().default("us-east-1"), })
localFilePath: z.string() .optional(),
}) stripe: z
.optional(), .object({
}); secret_key: z.string(),
webhook_secret: z.string(),
s3Bucket: z.string(),
s3Region: z.string().default("us-east-1"),
localFilePath: z.string()
})
.optional()
});
export function readPrivateConfigFile() { export function readPrivateConfigFile() {
if (build == "oss") { if (build == "oss") {
@@ -186,9 +196,7 @@ export function readPrivateConfigFile() {
} }
if (!environment) { if (!environment) {
throw new Error( throw new Error("No private configuration file found.");
"No private configuration file found."
);
} }
return environment; return environment;

View File

@@ -46,7 +46,7 @@ class RedisManager {
this.isEnabled = false; this.isEnabled = false;
return; return;
} }
this.isEnabled = privateConfig.getRawPrivateConfig().flags?.enable_redis || false; this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false;
if (this.isEnabled) { if (this.isEnabled) {
this.initializeClients(); this.initializeClients();
} }

View File

@@ -14,10 +14,10 @@
import Stripe from "stripe"; import Stripe from "stripe";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
import logger from "@server/logger"; import logger from "@server/logger";
import { build } from "@server/build"; import { noop } from "@server/lib/billing/usageService";
let stripe: Stripe | undefined = undefined; let stripe: Stripe | undefined = undefined;
if (build == "saas") { if (!noop()) {
const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key; const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key;
if (!stripeApiKey) { if (!stripeApiKey) {
logger.error("Stripe secret key is not configured"); logger.error("Stripe secret key is not configured");

View File

@@ -21,11 +21,10 @@ import {
} from "@server/db"; } from "@server/db";
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm"; import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { orgs, resources, sites, Target, targets } from "@server/db"; import { orgs, resources, sites, Target, targets } from "@server/db";
import { build } from "@server/build";
import { sanitize } from "@server/lib/traefik/utils"; import { sanitize } from "@server/lib/traefik/utils";
import privateConfig from "#private/lib/config";
const redirectHttpsMiddlewareName = "redirect-to-https"; const redirectHttpsMiddlewareName = "redirect-to-https";
const redirectToRootMiddlewareName = "redirect-to-root"; const redirectToRootMiddlewareName = "redirect-to-root";
@@ -79,7 +78,7 @@ export async function getTraefikConfig(
path: targets.path, path: targets.path,
pathMatchType: targets.pathMatchType, pathMatchType: targets.pathMatchType,
priority: targets.priority, priority: targets.priority,
// Site fields // Site fields
siteId: sites.siteId, siteId: sites.siteId,
siteType: sites.type, siteType: sites.type,
@@ -234,12 +233,13 @@ export async function getTraefikConfig(
continue; continue;
} }
if (resource.certificateStatus !== "valid") { // TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail
logger.debug( // if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
`Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}` // logger.debug(
); // `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
continue; // );
} // continue;
// }
// add routers and services empty objects if they don't exist // add routers and services empty objects if they don't exist
if (!config_output.http.routers) { if (!config_output.http.routers) {
@@ -264,18 +264,21 @@ export async function getTraefikConfig(
const configDomain = config.getDomain(resource.domainId); const configDomain = config.getDomain(resource.domainId);
let certResolver: string, preferWildcardCert: boolean;
if (!configDomain) {
certResolver = config.getRawConfig().traefik.cert_resolver;
preferWildcardCert =
config.getRawConfig().traefik.prefer_wildcard_cert;
} else {
certResolver = configDomain.cert_resolver;
preferWildcardCert = configDomain.prefer_wildcard_cert;
}
let tls = {}; let tls = {};
if (build == "oss") { if (
!privateConfig.getRawPrivateConfig().flags
.generate_own_certificates
) {
let certResolver: string, preferWildcardCert: boolean;
if (!configDomain) {
certResolver = config.getRawConfig().traefik.cert_resolver;
preferWildcardCert =
config.getRawConfig().traefik.prefer_wildcard_cert;
} else {
certResolver = configDomain.cert_resolver;
preferWildcardCert = configDomain.prefer_wildcard_cert;
}
tls = { tls = {
certResolver: certResolver, certResolver: certResolver,
...(preferWildcardCert ...(preferWildcardCert
@@ -419,7 +422,7 @@ export async function getTraefikConfig(
return ( return (
(targets as TargetWithSite[]) (targets as TargetWithSite[])
.filter((target: TargetWithSite) => { .filter((target: TargetWithSite) => {
if (!target.enabled) { if (!target.enabled) {
return false; return false;
} }
@@ -440,7 +443,7 @@ export async function getTraefikConfig(
) { ) {
return false; return false;
} }
} else if (target.site.type === "newt") { } else if (target.site.type === "newt") {
if ( if (
!target.internalPort || !target.internalPort ||
!target.method || !target.method ||
@@ -448,10 +451,10 @@ export async function getTraefikConfig(
) { ) {
return false; return false;
} }
} }
return true; return true;
}) })
.map((target: TargetWithSite) => { .map((target: TargetWithSite) => {
if ( if (
target.site.type === "local" || target.site.type === "local" ||
target.site.type === "wireguard" target.site.type === "wireguard"
@@ -459,14 +462,14 @@ export async function getTraefikConfig(
return { return {
url: `${target.method}://${target.ip}:${target.port}` url: `${target.method}://${target.ip}:${target.port}`
}; };
} else if (target.site.type === "newt") { } else if (target.site.type === "newt") {
const ip = const ip =
target.site.subnet!.split("/")[0]; target.site.subnet!.split("/")[0];
return { return {
url: `${target.method}://${ip}:${target.internalPort}` url: `${target.method}://${ip}:${target.internalPort}`
}; };
} }
}) })
// filter out duplicates // filter out duplicates
.filter( .filter(
(v, i, a) => (v, i, a) =>
@@ -709,4 +712,4 @@ export async function getTraefikConfig(
} }
return config_output; return config_output;
} }

View File

@@ -26,6 +26,7 @@ import { sha256 } from "@oslojs/crypto/sha2";
import { serializeSessionCookie } from "@server/auth/sessions/app"; import { serializeSessionCookie } from "@server/auth/sessions/app";
import { decrypt } from "@server/lib/crypto"; import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { TransferSessionResponse } from "@server/routers/auth/types";
const bodySchema = z.object({ const bodySchema = z.object({
token: z.string() token: z.string()
@@ -33,11 +34,6 @@ const bodySchema = z.object({
export type TransferSessionBodySchema = z.infer<typeof bodySchema>; export type TransferSessionBodySchema = z.infer<typeof bodySchema>;
export type TransferSessionResponse = {
valid: boolean;
cookie?: string;
};
export async function transferSession( export async function transferSession(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -22,6 +22,8 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
// Import tables for billing // Import tables for billing
import { import {
customers, customers,
@@ -37,11 +39,6 @@ const getOrgSchema = z
}) })
.strict(); .strict();
export type GetOrgSubscriptionResponse = {
subscription: Subscription | null;
items: SubscriptionItem[];
};
registry.registerPath({ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/billing/subscription", path: "/org/{orgId}/billing/subscription",

View File

@@ -25,6 +25,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import { Limit, limits, Usage, usage } from "@server/db"; import { Limit, limits, Usage, usage } from "@server/db";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { FeatureId } from "@server/lib/billing";
import { GetOrgUsageResponse } from "@server/routers/billing/types";
const getOrgSchema = z const getOrgSchema = z
.object({ .object({
@@ -32,11 +33,6 @@ const getOrgSchema = z
}) })
.strict(); .strict();
export type GetOrgUsageResponse = {
usage: Usage[];
limits: Limit[];
};
registry.registerPath({ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/billing/usage", path: "/org/{orgId}/billing/usage",

View File

@@ -19,6 +19,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "#private/lib/billing";
import { GetOrgTierResponse } from "@server/routers/billing/types";
const getOrgSchema = z const getOrgSchema = z
.object({ .object({
@@ -26,11 +27,6 @@ const getOrgSchema = z
}) })
.strict(); .strict();
export type GetOrgTierResponse = {
tier: string | null;
active: boolean;
};
export async function getOrgTier( export async function getOrgTier(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -15,15 +15,19 @@ import { Certificate, certificates, db, domains } from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import { Transaction } from "@server/db"; import { Transaction } from "@server/db";
import { eq, or, and, like } from "drizzle-orm"; import { eq, or, and, like } from "drizzle-orm";
import { build } from "@server/build"; import privateConfig from "#private/lib/config";
/** /**
* Checks if a certificate exists for the given domain. * Checks if a certificate exists for the given domain.
* If not, creates a new certificate in 'pending' state. * If not, creates a new certificate in 'pending' state.
* Wildcard certs cover subdomains. * Wildcard certs cover subdomains.
*/ */
export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) { export async function createCertificate(
if (build !== "saas") { domainId: string,
domain: string,
trx: Transaction | typeof db
) {
if (!privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
return; return;
} }
@@ -39,7 +43,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
let existing: Certificate[] = []; let existing: Certificate[] = [];
if (domainRecord.type == "ns") { if (domainRecord.type == "ns") {
const domainLevelDown = domain.split('.').slice(1).join('.'); const domainLevelDown = domain.split(".").slice(1).join(".");
existing = await trx existing = await trx
.select() .select()
.from(certificates) .from(certificates)
@@ -49,7 +53,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
eq(certificates.wildcard, true), // only NS domains can have wildcard certs eq(certificates.wildcard, true), // only NS domains can have wildcard certs
or( or(
eq(certificates.domain, domain), eq(certificates.domain, domain),
eq(certificates.domain, domainLevelDown), eq(certificates.domain, domainLevelDown)
) )
) )
); );
@@ -67,9 +71,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
} }
if (existing.length > 0) { if (existing.length > 0) {
logger.info( logger.info(`Certificate already exists for domain ${domain}`);
`Certificate already exists for domain ${domain}`
);
return; return;
} }

View File

@@ -21,6 +21,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { GetCertificateResponse } from "@server/routers/certificates/types";
const getCertificateSchema = z const getCertificateSchema = z
.object({ .object({
@@ -96,20 +97,6 @@ async function query(domainId: string, domain: string) {
return existing.length > 0 ? existing[0] : null; return existing.length > 0 ? existing[0] : null;
} }
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;
}
registry.registerPath({ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/certificate/{domainId}/{domain}", path: "/org/{orgId}/certificate/{domainId}/{domain}",

View File

@@ -21,6 +21,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { db, domainNamespaces, resources } from "@server/db"; import { db, domainNamespaces, resources } from "@server/db";
import { inArray } from "drizzle-orm"; import { inArray } from "drizzle-orm";
import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types";
const paramsSchema = z.object({}).strict(); const paramsSchema = z.object({}).strict();
@@ -30,15 +31,6 @@ const querySchema = z
}) })
.strict(); .strict();
export type CheckDomainAvailabilityResponse = {
available: boolean;
options: {
domainNamespaceId: string;
domainId: string;
fullDomain: string;
}[];
};
registry.registerPath({ registry.registerPath({
method: "get", method: "get",
path: "/domain/check-namespace-availability", path: "/domain/check-namespace-availability",

View File

@@ -39,16 +39,17 @@ import {
import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import rateLimit, { ipKeyGenerator } from "express-rate-limit";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import {
unauthenticated as ua,
authenticated as a
} from "@server/routers/external";
import { verifyValidLicense } from "../middlewares/verifyValidLicense"; import { verifyValidLicense } from "../middlewares/verifyValidLicense";
import { build } from "@server/build"; import { build } from "@server/build";
import {
unauthenticated as ua,
authenticated as a,
authRouter as aa
} from "@server/routers/external";
export const authenticated = a; export const authenticated = a;
export const unauthenticated = ua; export const unauthenticated = ua;
export const authRouter = aa;
unauthenticated.post( unauthenticated.post(
"/remote-exit-node/quick-start", "/remote-exit-node/quick-start",
@@ -276,8 +277,6 @@ authenticated.get(
loginPage.getLoginPage loginPage.getLoginPage
); );
export const authRouter = Router();
authRouter.post( authRouter.post(
"/remoteExitNode/get-token", "/remoteExitNode/get-token",
verifyValidLicense, verifyValidLicense,

View File

@@ -68,10 +68,11 @@ import { decryptData } from "@server/lib/encryption";
import config from "@server/lib/config"; import config from "@server/lib/config";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
import * as fs from "fs"; import * as fs from "fs";
import { exchangeSession } from "@server/routers/badger"; import { exchangeSession } from "@server/routers/badger";
import { validateResourceSessionToken } from "@server/auth/sessions/resource"; import { validateResourceSessionToken } from "@server/auth/sessions/resource";
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes"; import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
import { maxmindLookup } from "@server/db/maxmind"; import { maxmindLookup } from "@server/db/maxmind";
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
// Zod schemas for request validation // Zod schemas for request validation
const getResourceByDomainParamsSchema = z const getResourceByDomainParamsSchema = z
@@ -162,6 +163,14 @@ const validateResourceSessionTokenBodySchema = z
}) })
.strict(); .strict();
const validateResourceAccessTokenBodySchema = z
.object({
accessTokenId: z.string().optional(),
resourceId: z.number().optional(),
accessToken: z.string()
})
.strict();
// Certificates by domains query validation // Certificates by domains query validation
const getCertificatesByDomainsQuerySchema = z const getCertificatesByDomainsQuerySchema = z
.object({ .object({
@@ -215,6 +224,33 @@ export type UserSessionWithUser = {
export const hybridRouter = Router(); export const hybridRouter = Router();
hybridRouter.use(verifySessionRemoteExitNodeMiddleware); hybridRouter.use(verifySessionRemoteExitNodeMiddleware);
hybridRouter.get(
"/general-config",
async (req: Request, res: Response, next: NextFunction) => {
return response(res, {
data: {
resource_session_request_param:
config.getRawConfig().server.resource_session_request_param,
resource_access_token_headers:
config.getRawConfig().server.resource_access_token_headers,
resource_access_token_param:
config.getRawConfig().server.resource_access_token_param,
session_cookie_name:
config.getRawConfig().server.session_cookie_name,
require_email_verification:
config.getRawConfig().flags?.require_email_verification ||
false,
resource_session_length_hours:
config.getRawConfig().server.resource_session_length_hours
},
success: true,
error: false,
message: "General config retrieved successfully",
status: HttpCode.OK
});
}
);
hybridRouter.get( hybridRouter.get(
"/traefik-config", "/traefik-config",
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -1101,6 +1137,52 @@ hybridRouter.post(
} }
); );
// Validate resource session token
hybridRouter.post(
"/resource/:resourceId/access-token/verify",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedBody = validateResourceAccessTokenBodySchema.safeParse(
req.body
);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { accessToken, resourceId, accessTokenId } = parsedBody.data;
const result = await verifyResourceAccessToken({
accessTokenId,
accessToken,
resourceId
});
return response(res, {
data: result,
success: true,
error: false,
message: result.valid
? "Resource access token is valid"
: "Resource access token is invalid or expired",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to validate resource session token"
)
);
}
}
);
const geoIpLookupParamsSchema = z.object({ const geoIpLookupParamsSchema = z.object({
ip: z.string().ip() ip: z.string().ip()
}); });
@@ -1489,4 +1571,4 @@ hybridRouter.post(
); );
} }
} }
); );

View File

@@ -33,6 +33,7 @@ import { createCertificate } from "#private/routers/certificates/createCertifica
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build"; import { build } from "@server/build";
import { CreateLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -49,8 +50,6 @@ const bodySchema = z
export type CreateLoginPageBody = z.infer<typeof bodySchema>; export type CreateLoginPageBody = z.infer<typeof bodySchema>;
export type CreateLoginPageResponse = LoginPage;
export async function createLoginPage( export async function createLoginPage(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -20,6 +20,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { DeleteLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -28,8 +29,6 @@ const paramsSchema = z
}) })
.strict(); .strict();
export type DeleteLoginPageResponse = LoginPage;
export async function deleteLoginPage( export async function deleteLoginPage(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -20,6 +20,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { GetLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -40,10 +41,6 @@ async function query(orgId: string) {
return res?.loginPage; return res?.loginPage;
} }
export type GetLoginPageResponse = NonNullable<
Awaited<ReturnType<typeof query>>
>;
export async function getLoginPage( export async function getLoginPage(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -20,6 +20,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { LoadLoginPageResponse } from "@server/routers/loginPage/types";
const querySchema = z.object({ const querySchema = z.object({
resourceId: z.coerce.number().int().positive().optional(), resourceId: z.coerce.number().int().positive().optional(),
@@ -70,10 +71,6 @@ async function query(orgId: string | undefined, fullDomain: string) {
}; };
} }
export type LoadLoginPageResponse = NonNullable<
Awaited<ReturnType<typeof query>>
> & { orgId: string };
export async function loadLoginPage( export async function loadLoginPage(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -26,6 +26,7 @@ import { createCertificate } from "#private/routers/certificates/createCertifica
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build"; import { build } from "@server/build";
import { UpdateLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -55,8 +56,6 @@ const bodySchema = z
export type UpdateLoginPageBody = z.infer<typeof bodySchema>; export type UpdateLoginPageBody = z.infer<typeof bodySchema>;
export type UpdateLoginPageResponse = LoginPage;
export async function updateLoginPage( export async function updateLoginPage(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -27,6 +27,7 @@ import config from "@server/lib/config";
import { build } from "@server/build"; import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
const paramsSchema = z.object({ orgId: z.string().nonempty() }).strict(); const paramsSchema = z.object({ orgId: z.string().nonempty() }).strict();
@@ -47,11 +48,6 @@ const bodySchema = z
}) })
.strict(); .strict();
export type CreateOrgIdpResponse = {
idpId: number;
redirectUrl: string;
};
// registry.registerPath({ // registry.registerPath({
// method: "put", // method: "put",
// path: "/idp/oidc", // path: "/idp/oidc",

View File

@@ -25,6 +25,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto"; import { decrypt } from "@server/lib/crypto";
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
import { GetOrgIdpResponse } from "@server/routers/orgIdp/types";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -47,10 +48,6 @@ async function query(idpId: number, orgId: string) {
return res; return res;
} }
export type GetOrgIdpResponse = NonNullable<
Awaited<ReturnType<typeof query>>
> & { redirectUrl: string };
// registry.registerPath({ // registry.registerPath({
// method: "get", // method: "get",
// path: "/idp/{idpId}", // path: "/idp/{idpId}",

View File

@@ -22,6 +22,7 @@ import { eq, sql } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
const querySchema = z const querySchema = z
.object({ .object({
@@ -65,15 +66,6 @@ async function query(orgId: string, limit: number, offset: number) {
return res; return res;
} }
export type ListOrgIdpsResponse = {
idps: Awaited<ReturnType<typeof query>>;
pagination: {
total: number;
limit: number;
offset: number;
};
};
// registry.registerPath({ // registry.registerPath({
// method: "get", // method: "get",
// path: "/idp", // path: "/idp",

View File

@@ -29,17 +29,12 @@ import { and, eq } from "drizzle-orm";
import { getNextAvailableSubnet } from "@server/lib/exitNodes"; import { getNextAvailableSubnet } from "@server/lib/exitNodes";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { FeatureId } from "@server/lib/billing";
import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
export const paramsSchema = z.object({ export const paramsSchema = z.object({
orgId: z.string() orgId: z.string()
}); });
export type CreateRemoteExitNodeResponse = {
token: string;
remoteExitNodeId: string;
secret: string;
};
const bodySchema = z const bodySchema = z
.object({ .object({
remoteExitNodeId: z.string().length(15), remoteExitNodeId: z.string().length(15),
@@ -89,30 +84,25 @@ export async function createRemoteExitNode(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES FeatureId.REMOTE_EXIT_NODES
); );
if (!usage) { if (usage) {
return next( const rejectRemoteExitNodes = await usageService.checkLimitSet(
createHttpError( orgId,
HttpCode.NOT_FOUND, false,
"No usage data found for this organization" FeatureId.REMOTE_EXIT_NODES,
) {
); ...usage,
} instantaneousValue: (usage.instantaneousValue || 0) + 1
const rejectRemoteExitNodes = await usageService.checkLimitSet( } // We need to add one to know if we are violating the limit
orgId,
false,
FeatureId.REMOTE_EXIT_NODES,
{
...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1
} // We need to add one to know if we are violating the limit
);
if (rejectRemoteExitNodes) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
)
); );
if (rejectRemoteExitNodes) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
)
);
}
} }
const secretHash = await hashPassword(secret); const secretHash = await hashPassword(secret);

View File

@@ -21,6 +21,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
const getRemoteExitNodeSchema = z const getRemoteExitNodeSchema = z
.object({ .object({
@@ -52,8 +53,6 @@ async function query(remoteExitNodeId: string) {
return remoteExitNode; return remoteExitNode;
} }
export type GetRemoteExitNodeResponse = Awaited<ReturnType<typeof query>>;
export async function getRemoteExitNode( export async function getRemoteExitNode(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -35,8 +35,6 @@ export const remoteExitNodeGetTokenBodySchema = z.object({
token: z.string().optional() token: z.string().optional()
}); });
export type RemoteExitNodeGetTokenBody = z.infer<typeof remoteExitNodeGetTokenBodySchema>;
export async function getRemoteExitNodeToken( export async function getRemoteExitNodeToken(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -21,6 +21,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
const listRemoteExitNodesParamsSchema = z const listRemoteExitNodesParamsSchema = z
.object({ .object({
@@ -43,7 +44,7 @@ const listRemoteExitNodesSchema = z.object({
.pipe(z.number().int().nonnegative()) .pipe(z.number().int().nonnegative())
}); });
function queryRemoteExitNodes(orgId: string) { export function queryRemoteExitNodes(orgId: string) {
return db return db
.select({ .select({
remoteExitNodeId: remoteExitNodes.remoteExitNodeId, remoteExitNodeId: remoteExitNodes.remoteExitNodeId,
@@ -65,11 +66,6 @@ function queryRemoteExitNodes(orgId: string) {
); );
} }
export type ListRemoteExitNodesResponse = {
remoteExitNodes: Awaited<ReturnType<typeof queryRemoteExitNodes>>;
pagination: { total: number; limit: number; offset: number };
};
export async function listRemoteExitNodes( export async function listRemoteExitNodes(
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -19,11 +19,7 @@ import logger from "@server/logger";
import { generateId } from "@server/auth/sessions/app"; import { generateId } from "@server/auth/sessions/app";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { z } from "zod"; import { z } from "zod";
import { PickRemoteExitNodeDefaultsResponse } from "@server/routers/remoteExitNode/types";
export type PickRemoteExitNodeDefaultsResponse = {
remoteExitNodeId: string;
secret: string;
};
const paramsSchema = z const paramsSchema = z
.object({ .object({

View File

@@ -12,7 +12,7 @@
*/ */
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { db, exitNodes, exitNodeOrgs } from "@server/db"; import { db } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { remoteExitNodes } from "@server/db"; import { remoteExitNodes } from "@server/db";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -24,11 +24,7 @@ import { hashPassword } from "@server/auth/password";
import logger from "@server/logger"; import logger from "@server/logger";
import z from "zod"; import z from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { QuickStartRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
export type QuickStartRemoteExitNodeResponse = {
remoteExitNodeId: string;
secret: string;
};
const INSTALLER_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"; const INSTALLER_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e";

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 NodeCache from "node-cache";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; 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 { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { verifyPassword } from "@server/auth/password"; import { verifyPassword } from "@server/auth/password";
@@ -106,23 +108,23 @@ export async function verifyResourceSession(
const clientIp = requestIp const clientIp = requestIp
? (() => { ? (() => {
logger.debug("Request IP:", { requestIp }); logger.debug("Request IP:", { requestIp });
if (requestIp.startsWith("[") && requestIp.includes("]")) { if (requestIp.startsWith("[") && requestIp.includes("]")) {
// if brackets are found, extract the IPv6 address from between the brackets // if brackets are found, extract the IPv6 address from between the brackets
const ipv6Match = requestIp.match(/\[(.*?)\]/); const ipv6Match = requestIp.match(/\[(.*?)\]/);
if (ipv6Match) { if (ipv6Match) {
return ipv6Match[1]; return ipv6Match[1];
} }
} }
// ivp4 // ivp4
// split at last colon // split at last colon
const lastColonIndex = requestIp.lastIndexOf(":"); const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) { if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex); return requestIp.substring(0, lastColonIndex);
} }
return requestIp; return requestIp;
})() })()
: undefined; : undefined;
logger.debug("Client IP:", { clientIp }); logger.debug("Client IP:", { clientIp });
@@ -137,11 +139,11 @@ export async function verifyResourceSession(
const resourceCacheKey = `resource:${cleanHost}`; const resourceCacheKey = `resource:${cleanHost}`;
let resourceData: let resourceData:
| { | {
resource: Resource | null; resource: Resource | null;
pincode: ResourcePincode | null; pincode: ResourcePincode | null;
password: ResourcePassword | null; password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null; headerAuth: ResourceHeaderAuth | null;
} }
| undefined = cache.get(resourceCacheKey); | undefined = cache.get(resourceCacheKey);
if (!resourceData) { if (!resourceData) {
@@ -213,21 +215,21 @@ export async function verifyResourceSession(
headers && headers &&
headers[ headers[
config.getRawConfig().server.resource_access_token_headers.id config.getRawConfig().server.resource_access_token_headers.id
] && ] &&
headers[ headers[
config.getRawConfig().server.resource_access_token_headers.token config.getRawConfig().server.resource_access_token_headers.token
] ]
) { ) {
const accessTokenId = const accessTokenId =
headers[ headers[
config.getRawConfig().server.resource_access_token_headers config.getRawConfig().server.resource_access_token_headers
.id .id
]; ];
const accessToken = const accessToken =
headers[ headers[
config.getRawConfig().server.resource_access_token_headers config.getRawConfig().server.resource_access_token_headers
.token .token
]; ];
const { valid, error, tokenItem } = await verifyResourceAccessToken( const { valid, error, tokenItem } = await verifyResourceAccessToken(
{ {
@@ -294,10 +296,17 @@ export async function verifyResourceSession(
// check for HTTP Basic Auth header // check for HTTP Basic Auth header
if (headerAuth && clientHeaderAuth) { if (headerAuth && clientHeaderAuth) {
if(cache.get(clientHeaderAuth)) { if (cache.get(clientHeaderAuth)) {
logger.debug("Resource allowed because header auth is valid (cached)"); logger.debug(
"Resource allowed because header auth is valid (cached)"
);
return allowed(res); return allowed(res);
}else if(await verifyPassword(clientHeaderAuth, headerAuth.headerAuthHash)){ } else if (
await verifyPassword(
clientHeaderAuth,
headerAuth.headerAuthHash
)
) {
cache.set(clientHeaderAuth, clientHeaderAuth); cache.set(clientHeaderAuth, clientHeaderAuth);
logger.debug("Resource allowed because header auth is valid"); logger.debug("Resource allowed because header auth is valid");
return allowed(res); return allowed(res);
@@ -477,7 +486,11 @@ function extractResourceSessionToken(
return latest.token; 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; let loginPage: LoginPage | null = null;
if (orgId) { if (orgId) {
const { tier } = await getOrgTierData(orgId); // returns null in oss 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; let endpoint: string;
if (loginPage && loginPage.domainId && loginPage.fullDomain) { 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"; const method = secure ? "https" : "http";
endpoint = `${method}://${loginPage.fullDomain}`; endpoint = `${method}://${loginPage.fullDomain}`;
} else if (config.isManagedMode()) {
endpoint =
config.getRawConfig().managed?.redirect_endpoint ||
config.getRawConfig().managed?.endpoint ||
"";
} else { } else {
endpoint = config.getRawConfig().app.dashboard_url!; 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); let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
if (!cachedCountryCode) { if (!cachedCountryCode) {
if (config.isManagedMode()) { cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
cachedCountryCode = await remoteGetCountryCodeForIp(ip);
} else {
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
}
// Cache for longer since IP geolocation doesn't change frequently // Cache for longer since IP geolocation doesn't change frequently
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes 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(); 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)) { if (!headers || (!headers.authorization && !headers.Authorization)) {
return; return;
} }
@@ -833,8 +841,9 @@ function extractBasicAuth(headers: Record<string, string> | undefined): string |
try { try {
// Extract the base64 encoded credentials // Extract the base64 encoded credentials
return authHeader.slice("Basic ".length); return authHeader.slice("Basic ".length);
} catch (error) { } 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 config from "@server/lib/config";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { getAllowedIps } from "../target/helpers"; import { getAllowedIps } from "../target/helpers";
import { proxyToRemote } from "@server/lib/remoteProxy";
import { createExitNode } from "#dynamic/routers/gerbil/createExitNode"; import { createExitNode } from "#dynamic/routers/gerbil/createExitNode";
// Define Zod schema for request validation // 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); const configResponse = await generateGerbilConfig(exitNode);
logger.debug("Sending config: ", configResponse); 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 auth from "@server/routers/auth";
import * as supporterKey from "@server/routers/supporterKey"; import * as supporterKey from "@server/routers/supporterKey";
import * as idp from "@server/routers/idp"; 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 HttpCode from "@server/types/HttpCode";
import { import {
verifyResourceAccess, verifyResourceAccess,
@@ -48,34 +46,11 @@ internalRouter.get("/idp/:idpId", idp.getIdp);
const gerbilRouter = Router(); const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter); internalRouter.use("/gerbil", gerbilRouter);
if (config.isManagedMode()) { // Use local gerbil endpoints
// Use proxy router to forward requests to remote cloud server gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
// Proxy endpoints for each gerbil route gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
gerbilRouter.post("/receive-bandwidth", (req, res, next) => gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
proxyToRemote(req, res, next, "hybrid/gerbil/receive-bandwidth") gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname);
);
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);
}
// WE HANDLE THE PROXY INSIDE OF THIS FUNCTION // WE HANDLE THE PROXY INSIDE OF THIS FUNCTION
// SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL // SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL
@@ -87,10 +62,4 @@ internalRouter.use("/badger", badgerRouter);
badgerRouter.post("/verify-session", badger.verifyResourceSession); badgerRouter.post("/verify-session", badger.verifyResourceSession);
if (config.isManagedMode()) { badgerRouter.post("/exchange-session", badger.exchangeSession);
badgerRouter.post("/exchange-session", (req, res, next) =>
proxyToRemote(req, res, next, "hybrid/badger/exchange-session")
);
} else {
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; }

View File

@@ -3,7 +3,6 @@ import { eq } from "drizzle-orm";
import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
import moment from "moment"; import moment from "moment";
import logger from "@server/logger"; import logger from "@server/logger";
import config from "@server/lib/config";
const random: RandomReader = { const random: RandomReader = {
read(bytes: Uint8Array): void { read(bytes: Uint8Array): void {
@@ -23,11 +22,6 @@ function generateId(length: number): string {
} }
export async function ensureSetupToken() { export async function ensureSetupToken() {
if (config.isManagedMode()) {
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID
return;
}
try { try {
// Check if a server admin already exists // Check if a server admin already exists
const [existingAdmin] = await db const [existingAdmin] = await db

View File

@@ -1,8 +1,6 @@
import { internal } from "@app/lib/api"; import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import ProfileIcon from "@app/components/ProfileIcon";
import { verifySession } from "@app/lib/auth/verifySession"; import { verifySession } from "@app/lib/auth/verifySession";
import UserProvider from "@app/providers/UserProvider";
import { GetOrgResponse } from "@server/routers/org"; import { GetOrgResponse } from "@server/routers/org";
import { GetOrgUserResponse } from "@server/routers/user"; import { GetOrgUserResponse } from "@server/routers/user";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
@@ -10,7 +8,7 @@ import { redirect } from "next/navigation";
import { cache } from "react"; import { cache } from "react";
import SetLastOrgCookie from "@app/components/SetLastOrgCookie"; import SetLastOrgCookie from "@app/components/SetLastOrgCookie";
import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider"; import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider";
import { GetOrgSubscriptionResponse } from "#private/routers/billing/getOrgSubscription"; import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
import { pullEnv } from "@app/lib/pullEnv"; import { pullEnv } from "@app/lib/pullEnv";
import { build } from "@server/build"; import { build } from "@server/build";

View File

@@ -37,7 +37,7 @@ import { InfoPopup } from "@/components/ui/info-popup";
import { import {
GetOrgSubscriptionResponse, GetOrgSubscriptionResponse,
GetOrgUsageResponse GetOrgUsageResponse
} from "#private/routers/billing"; } from "@server/routers/billing/types";
import { useTranslations } from "use-intl"; import { useTranslations } from "use-intl";
import Link from "next/link"; import Link from "next/link";

View File

@@ -9,7 +9,7 @@ import { cache } from "react";
import { import {
GetOrgSubscriptionResponse, GetOrgSubscriptionResponse,
GetOrgTierResponse GetOrgTierResponse
} from "#private/routers/billing"; } from "@server/routers/billing/types";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build"; import { build } from "@server/build";

View File

@@ -1,5 +1,5 @@
import { internal } from "@app/lib/api"; import { internal } from "@app/lib/api";
import { GetRemoteExitNodeResponse } from "#private/routers/remoteExitNode"; import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";

View File

@@ -30,7 +30,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { import {
QuickStartRemoteExitNodeResponse, QuickStartRemoteExitNodeResponse,
PickRemoteExitNodeDefaultsResponse PickRemoteExitNodeDefaultsResponse
} from "#private/routers/remoteExitNode"; } from "@server/routers/remoteExitNode/types";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useParams, useRouter, useSearchParams } from "next/navigation";

View File

@@ -1,6 +1,6 @@
import { internal } from "@app/lib/api"; import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { ListRemoteExitNodesResponse } from "#private/routers/remoteExitNode"; import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable"; import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";

View File

@@ -117,8 +117,8 @@ export default function ResourceRules(props: {
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const env = useEnvContext(); const { env } = useEnvContext();
const isMaxmindAvailable = env.env.server.maxmind_db_path && env.env.server.maxmind_db_path.length > 0; const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
const RuleAction = { const RuleAction = {
ACCEPT: t('alwaysAllow'), ACCEPT: t('alwaysAllow'),

View File

@@ -53,7 +53,7 @@ import {
CreateSiteResponse, CreateSiteResponse,
PickSiteDefaultsResponse PickSiteDefaultsResponse
} from "@server/routers/site"; } from "@server/routers/site";
import { ListRemoteExitNodesResponse } from "#private/routers/remoteExitNode"; import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";

View File

@@ -6,13 +6,12 @@ import { verifySession } from "@app/lib/auth/verifySession";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { pullEnv } from "@app/lib/pullEnv"; import { pullEnv } from "@app/lib/pullEnv";
import { LoginFormIDP } from "@app/components/LoginForm"; import { LoginFormIDP } from "@app/components/LoginForm";
import { ListOrgIdpsResponse } from "#private/routers/orgIdp"; import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
import { build } from "@server/build"; import { build } from "@server/build";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { import {
GetLoginPageResponse,
LoadLoginPageResponse LoadLoginPageResponse
} from "#private/routers/loginPage"; } from "@server/routers/loginPage/types";
import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; import IdpLoginButtons from "@app/components/private/IdpLoginButtons";
import { import {
Card, Card,
@@ -24,9 +23,9 @@ import {
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { GetSessionTransferTokenRenponse } from "#private/routers/auth/getSessionTransferToken"; import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types";
import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken"; import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken";
import { GetOrgTierResponse } from "#private/routers/billing"; import { GetOrgTierResponse } from "@server/routers/billing/types";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";

View File

@@ -6,7 +6,7 @@ import { AxiosResponse } from "axios";
import { GetIdpResponse } from "@server/routers/idp"; import { GetIdpResponse } from "@server/routers/idp";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { pullEnv } from "@app/lib/pullEnv"; import { pullEnv } from "@app/lib/pullEnv";
import { LoadLoginPageResponse } from "#private/routers/loginPage"; import { LoadLoginPageResponse } from "@server/routers/loginPage/types";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";

View File

@@ -15,12 +15,12 @@ import AccessToken from "@app/components/AccessToken";
import { pullEnv } from "@app/lib/pullEnv"; import { pullEnv } from "@app/lib/pullEnv";
import { LoginFormIDP } from "@app/components/LoginForm"; import { LoginFormIDP } from "@app/components/LoginForm";
import { ListIdpsResponse } from "@server/routers/idp"; import { ListIdpsResponse } from "@server/routers/idp";
import { ListOrgIdpsResponse } from "#private/routers/orgIdp"; import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
import AutoLoginHandler from "@app/components/AutoLoginHandler"; import AutoLoginHandler from "@app/components/AutoLoginHandler";
import { build } from "@server/build"; import { build } from "@server/build";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { GetLoginPageResponse } from "#private/routers/loginPage"; import { GetLoginPageResponse } from "@server/routers/loginPage/types";
import { GetOrgTierResponse } from "#private/routers/billing"; import { GetOrgTierResponse } from "@server/routers/billing/types";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";

View File

@@ -32,7 +32,7 @@ import { createApiClient, formatAxiosError } from "@/lib/api";
import { useEnvContext } from "@/hooks/useEnvContext"; import { useEnvContext } from "@/hooks/useEnvContext";
import { toast } from "@/hooks/useToast"; import { toast } from "@/hooks/useToast";
import { ListDomainsResponse } from "@server/routers/domain/listDomains"; import { ListDomainsResponse } from "@server/routers/domain/listDomains";
import { CheckDomainAvailabilityResponse } from "#private/routers/domain/checkDomainNamespaceAvailability"; import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";

View File

@@ -13,12 +13,14 @@ import {
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { build } from "@server/build"; import { build } from "@server/build";
import CertificateStatus from "@app/components/private/CertificateStatus"; import CertificateStatus from "@app/components/private/CertificateStatus";
import { toUnicode } from 'punycode'; import { toUnicode } from "punycode";
import { useEnvContext } from "@app/hooks/useEnvContext";
type ResourceInfoBoxType = {}; type ResourceInfoBoxType = {};
export default function ResourceInfoBox({ }: ResourceInfoBoxType) { export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { resource, authInfo } = useResourceContext(); const { resource, authInfo } = useResourceContext();
const { env } = useEnvContext();
const t = useTranslations(); const t = useTranslations();
@@ -28,7 +30,13 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
<Alert> <Alert>
<AlertDescription> <AlertDescription>
{/* 4 cols because of the certs */} {/* 4 cols because of the certs */}
<InfoSections cols={resource.http && build != "oss" ? 4 : 3}> <InfoSections
cols={
resource.http && env.flags.generateOwnCertificates
? 4
: 3
}
>
{resource.http ? ( {resource.http ? (
<> <>
<InfoSection> <InfoSection>
@@ -37,9 +45,9 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{authInfo.password || {authInfo.password ||
authInfo.pincode || authInfo.pincode ||
authInfo.sso || authInfo.sso ||
authInfo.whitelist ? ( authInfo.whitelist ? (
<div className="flex items-start space-x-2 text-green-500"> <div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" /> <ShieldCheck className="w-4 h-4 mt-0.5" />
<span>{t("protected")}</span> <span>{t("protected")}</span>
@@ -126,25 +134,28 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
{/* </InfoSectionContent> */} {/* </InfoSectionContent> */}
{/* </InfoSection> */} {/* </InfoSection> */}
{/* Certificate Status Column */} {/* Certificate Status Column */}
{resource.http && resource.domainId && resource.fullDomain && build != "oss" && ( {resource.http &&
<InfoSection> resource.domainId &&
<InfoSectionTitle> resource.fullDomain &&
{t("certificateStatus", { build != "oss" && (
defaultValue: "Certificate" <InfoSection>
})} <InfoSectionTitle>
</InfoSectionTitle> {t("certificateStatus", {
<InfoSectionContent> defaultValue: "Certificate"
<CertificateStatus })}
orgId={resource.orgId} </InfoSectionTitle>
domainId={resource.domainId} <InfoSectionContent>
fullDomain={resource.fullDomain} <CertificateStatus
autoFetch={true} orgId={resource.orgId}
showLabel={false} domainId={resource.domainId}
polling={true} fullDomain={resource.fullDomain}
/> autoFetch={true}
</InfoSectionContent> showLabel={false}
</InfoSection> polling={true}
)} />
</InfoSectionContent>
</InfoSection>
)}
<InfoSection> <InfoSection>
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle> <InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>

View File

@@ -1,4 +1,5 @@
"use client"; "use client";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
@@ -30,7 +31,7 @@ import {
SettingsSectionForm SettingsSectionForm
} from "@app/components/Settings"; } from "@app/components/Settings";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { GetLoginPageResponse } from "#private/routers/loginPage"; import { GetLoginPageResponse } from "@server/routers/loginPage/types";
import { ListDomainsResponse } from "@server/routers/domain"; import { ListDomainsResponse } from "@server/routers/domain";
import { DomainRow } from "@app/components/DomainsTable"; import { DomainRow } from "@app/components/DomainsTable";
import { toUnicode } from "punycode"; import { toUnicode } from "punycode";
@@ -78,6 +79,7 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext();
const subscription = useSubscriptionStatusContext(); const subscription = useSubscriptionStatusContext();
@@ -447,10 +449,21 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
</div> </div>
</div> </div>
{/* Certificate Status */} {!form.watch(
{(build === "enterprise" || "authPageDomainId"
(build === "saas" && ) && (
subscription?.subscribed)) && <div className="text-sm text-muted-foreground">
{t(
"addDomainToEnableCustomAuthPages"
)}
</div>
)}
{env.flags
.generateOwnCertificates &&
(build === "enterprise" ||
(build === "saas" &&
subscription?.subscribed)) &&
loginPage?.domainId && loginPage?.domainId &&
loginPage?.fullDomain && loginPage?.fullDomain &&
!hasUnsavedChanges && ( !hasUnsavedChanges && (
@@ -469,16 +482,6 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
polling={true} polling={true}
/> />
)} )}
{!form.watch(
"authPageDomainId"
) && (
<div className="text-sm text-muted-foreground">
{t(
"addDomainToEnableCustomAuthPages"
)}
</div>
)}
</div> </div>
</form> </form>
</Form> </Form>

View File

@@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { TransferSessionResponse } from "#private/routers/auth/transferSession"; import { TransferSessionResponse } from "@server/routers/auth/types";
type ValidateSessionTransferTokenParams = { type ValidateSessionTransferTokenParams = {
token: string; token: string;

View File

@@ -1,4 +1,4 @@
import { GetRemoteExitNodeResponse } from "#private/routers/remoteExitNode"; import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
import { createContext } from "react"; import { createContext } from "react";
type RemoteExitNodeContextType = { type RemoteExitNodeContextType = {

View File

@@ -1,4 +1,4 @@
import { GetOrgSubscriptionResponse } from "#private/routers/billing"; import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
import { createContext } from "react"; import { createContext } from "react";
type SubscriptionStatusContextType = { type SubscriptionStatusContextType = {

View File

@@ -2,7 +2,7 @@
import { useState, useCallback, useEffect } from "react"; import { useState, useCallback, useEffect } from "react";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { GetCertificateResponse } from "#private/routers/certificates"; import { GetCertificateResponse } from "@server/routers/certificates/types";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";

View File

@@ -48,7 +48,11 @@ export function pullEnv(): Env {
enableClients: enableClients:
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false, process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false,
hideSupporterKey: hideSupporterKey:
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false process.env.HIDE_SUPPORTER_KEY === "true" ? true : false,
generateOwnCertificates:
process.env.GENERATE_OWN_CERTIFICATES === "true"
? true
: false
}, },
branding: { branding: {

View File

@@ -28,6 +28,7 @@ export type Env = {
disableBasicWireguardSites: boolean; disableBasicWireguardSites: boolean;
enableClients: boolean; enableClients: boolean;
hideSupporterKey: boolean; hideSupporterKey: boolean;
generateOwnCertificates: boolean;
}, },
branding: { branding: {
appName?: string; appName?: string;

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import RemoteExitNodeContext from "@app/contexts/remoteExitNodeContext"; import RemoteExitNodeContext from "@app/contexts/remoteExitNodeContext";
import { GetRemoteExitNodeResponse } from "#private/routers/remoteExitNode"; import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
import { useState } from "react"; import { useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";

View File

@@ -2,7 +2,7 @@
import SubscriptionStatusContext from "@app/contexts/subscriptionStatusContext"; import SubscriptionStatusContext from "@app/contexts/subscriptionStatusContext";
import { getTierPriceSet, TierId } from "@server/lib/billing/tiers"; import { getTierPriceSet, TierId } from "@server/lib/billing/tiers";
import { GetOrgSubscriptionResponse } from "#private/routers/billing"; import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
import { useState } from "react"; import { useState } from "react";
import { build } from "@server/build"; import { build } from "@server/build";