mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-27 15:26:41 +00:00
Seperate managed node code to fosrl/pangolin-node
This commit is contained in:
@@ -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))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -101,10 +101,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();
|
||||||
@@ -157,10 +154,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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -39,15 +39,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(),
|
||||||
@@ -320,10 +311,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;
|
||||||
}
|
}
|
||||||
@@ -335,10 +323,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;
|
||||||
@@ -351,10 +335,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 data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0;
|
return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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();
|
|
||||||
@@ -6,12 +6,10 @@ 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,
|
getValidCertificatesForDomains,
|
||||||
getValidCertificatesForDomainsHybrid
|
|
||||||
} from "#dynamic/lib/certificates";
|
} 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";
|
||||||
@@ -348,17 +346,8 @@ export class TraefikConfigManager {
|
|||||||
|
|
||||||
if (domainsToFetch.size > 0) {
|
if (domainsToFetch.size > 0) {
|
||||||
// Get valid certificates for domains not covered by wildcards
|
// Get valid certificates for domains not covered by wildcards
|
||||||
if (config.isManagedMode()) {
|
validCertificates =
|
||||||
validCertificates =
|
await getValidCertificatesForDomains(domainsToFetch);
|
||||||
await getValidCertificatesForDomainsHybrid(
|
|
||||||
domainsToFetch
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
validCertificates =
|
|
||||||
await getValidCertificatesForDomains(
|
|
||||||
domainsToFetch
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.lastCertificateFetch = new Date();
|
this.lastCertificateFetch = new Date();
|
||||||
this.lastKnownDomains = new Set(domains);
|
this.lastKnownDomains = new Set(domains);
|
||||||
|
|
||||||
@@ -448,32 +437,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>();
|
||||||
|
|
||||||
@@ -842,7 +814,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import * as auth from "@server/routers/auth";
|
|||||||
import * as supporterKey from "@server/routers/supporterKey";
|
import * as supporterKey from "@server/routers/supporterKey";
|
||||||
import * as license from "@server/routers/license";
|
import * as license from "@server/routers/license";
|
||||||
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,
|
||||||
@@ -51,34 +49,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
|
||||||
@@ -90,10 +65,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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user