From e9199bac1236dfe019dbea53ea1d784f20bce85e Mon Sep 17 00:00:00 2001 From: Ivan Skodje <ivanskodje@users.noreply.github.com> Date: Tue, 2 Apr 2024 19:34:50 +0200 Subject: [PATCH] [FEAT] Check port access in docker before showing a default error (#961) * [FEAT] Added port checks in updateENV.validDockerizedUrl to prevent docker from assuming it cannot access localhost URLs * [CHORE] Updated error message to include Linux URL * Patch port checking for general loopbacks * typo --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> --- .../utils/helpers/portAvailabilityChecker.js | 46 +++++++++++++++++++ server/utils/helpers/updateENV.js | 37 +++++++++++---- 2 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 server/utils/helpers/portAvailabilityChecker.js diff --git a/server/utils/helpers/portAvailabilityChecker.js b/server/utils/helpers/portAvailabilityChecker.js new file mode 100644 index 000000000..66d6d1af7 --- /dev/null +++ b/server/utils/helpers/portAvailabilityChecker.js @@ -0,0 +1,46 @@ +// Get all loopback addresses that are available for use or binding. +function getLocalHosts() { + const os = require("os"); + const interfaces = os.networkInterfaces(); + const results = new Set([undefined, "0.0.0.0"]); + + for (const _interface of Object.values(interfaces)) { + for (const config of _interface) { + results.add(config.address); + } + } + + return Array.from(results); +} + +function checkPort(options = {}) { + const net = require("net"); + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on("error", reject); + + server.listen(options, () => { + server.close(() => { + resolve(true); + }); + }); + }); +} + +async function isPortInUse(port, host) { + try { + await checkPort({ port, host }); + return true; + } catch (error) { + if (!["EADDRNOTAVAIL", "EINVAL"].includes(error.code)) { + return false; + } + } + return false; +} + +module.exports = { + isPortInUse, + getLocalHosts, +}; diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 6e0e5daa6..12c45af26 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -462,14 +462,28 @@ function isDownloadedModel(input = "") { return files.includes(input); } -function validDockerizedUrl(input = "") { +async function validDockerizedUrl(input = "") { if (process.env.ANYTHING_LLM_RUNTIME !== "docker") return null; + try { - const { hostname } = new URL(input); - if (["localhost", "127.0.0.1", "0.0.0.0"].includes(hostname.toLowerCase())) - return "Localhost, 127.0.0.1, or 0.0.0.0 origins cannot be reached from inside the AnythingLLM container. Please use host.docker.internal, a real machine ip, or domain to connect to your service."; - return null; - } catch {} + const { isPortInUse, getLocalHosts } = require("./portAvailabilityChecker"); + const localInterfaces = getLocalHosts(); + const url = new URL(input); + const hostname = url.hostname.toLowerCase(); + const port = parseInt(url.port, 10); + + // If not a loopback, skip this check. + if (!localInterfaces.includes(hostname)) return null; + if (isNaN(port)) return "Invalid URL: Port is not specified or invalid"; + + const isPortAvailableFromDocker = await isPortInUse(port, hostname); + if (isPortAvailableFromDocker) + return "Port is not running a reachable service on loopback address from inside the AnythingLLM container. Please use host.docker.internal (for linux use 172.17.0.1), a real machine ip, or domain to connect to your service."; + } catch (error) { + console.error(error.message); + return "An error occurred while validating the URL"; + } + return null; } @@ -504,10 +518,8 @@ async function updateENV(newENVs = {}, force = false, userId = null) { const { envKey, checks, postUpdate = [] } = KEY_MAPPING[key]; const prevValue = process.env[envKey]; const nextValue = newENVs[key]; - const errors = checks - .map((validityCheck) => validityCheck(nextValue, force)) - .filter((err) => typeof err === "string"); + const errors = await executeValidationChecks(checks, nextValue, force); if (errors.length > 0) { error += errors.join("\n"); break; @@ -524,6 +536,13 @@ async function updateENV(newENVs = {}, force = false, userId = null) { return { newValues, error: error?.length > 0 ? error : false }; } +async function executeValidationChecks(checks, value, force) { + const results = await Promise.all( + checks.map((validator) => validator(value, force)) + ); + return results.filter((err) => typeof err === "string"); +} + async function logChangesToEventLog(newValues = {}, userId = null) { const { EventLogs } = require("../../models/eventLogs"); const eventMapping = {