Files
pangolin/server/private/lib/exitNodes/exitNodeComms.ts
2025-10-13 11:49:48 -07:00

147 lines
4.8 KiB
TypeScript

/*
* 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 axios from "axios";
import logger from "@server/logger";
import { db, ExitNode, remoteExitNodes } from "@server/db";
import { eq } from "drizzle-orm";
import { sendToClient } from "#private/routers/ws";
import privateConfig from "#private/lib/config";
import config from "@server/lib/config";
interface ExitNodeRequest {
remoteType?: string;
localPath: string;
method?: "POST" | "DELETE" | "GET" | "PUT";
data?: any;
queryParams?: Record<string, string>;
}
/**
* Sends a request to an exit node, handling both remote and local exit nodes
* @param exitNode The exit node to send the request to
* @param request The request configuration
* @returns Promise<any> Response data for local nodes, undefined for remote nodes
*/
export async function sendToExitNode(
exitNode: ExitNode,
request: ExitNodeRequest
): Promise<any> {
if (exitNode.type === "remoteExitNode" && request.remoteType) {
const [remoteExitNode] = await db
.select()
.from(remoteExitNodes)
.where(eq(remoteExitNodes.exitNodeId, exitNode.exitNodeId))
.limit(1);
if (!remoteExitNode) {
throw new Error(
`Remote exit node with ID ${exitNode.exitNodeId} not found`
);
}
return sendToClient(remoteExitNode.remoteExitNodeId, {
type: request.remoteType,
data: request.data
});
} else {
let hostname = exitNode.reachableAt;
// logger.debug(`Exit node details:`, {
// type: exitNode.type,
// name: exitNode.name,
// reachableAt: exitNode.reachableAt,
// });
// logger.debug(`Configured local exit node name: ${config.getRawConfig().gerbil.exit_node_name}`);
if (exitNode.name == config.getRawConfig().gerbil.exit_node_name) {
hostname = privateConfig.getRawPrivateConfig().gerbil.local_exit_node_reachable_at;
}
if (!hostname) {
throw new Error(
`Exit node with ID ${exitNode.exitNodeId} is not reachable`
);
}
// logger.debug(`Sending request to exit node at ${hostname}`, {
// type: request.remoteType,
// data: request.data
// });
// Handle local exit node with HTTP API
const method = request.method || "POST";
let url = `${hostname}${request.localPath}`;
// Add query parameters if provided
if (request.queryParams) {
const params = new URLSearchParams(request.queryParams);
url += `?${params.toString()}`;
}
try {
let response;
switch (method) {
case "POST":
response = await axios.post(url, request.data, {
headers: {
"Content-Type": "application/json"
},
timeout: 8000
});
break;
case "DELETE":
response = await axios.delete(url, {
timeout: 8000
});
break;
case "GET":
response = await axios.get(url, {
timeout: 8000
});
break;
case "PUT":
response = await axios.put(url, request.data, {
headers: {
"Content-Type": "application/json"
},
timeout: 8000
});
break;
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
logger.debug(`Exit node request successful:`, {
method,
url,
status: response.data.status
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error(
`Error making ${method} request (can Pangolin see Gerbil HTTP API?) for exit node at ${hostname} (status: ${error.response?.status}): ${error.message}`
);
} else {
logger.error(
`Error making ${method} request for exit node at ${hostname}: ${error}`
);
}
}
}
}