2025-07-03 18:25:50 +00:00
|
|
|
import type { FSHeader, IEndpointFS } from "$lib/server/broker-utils/FileSystem/endpoints/endpoints"
|
2025-07-03 21:34:58 +00:00
|
|
|
import { NGINX_ACTIVE, NGINX_BASE, NGINX_INACTIVE, NGINX_TRACKED } from "$lib/server/utils/constants"
|
2025-07-04 03:05:43 +00:00
|
|
|
import { doesFileExist, isDir, listFiles, loadFile } from "$lib/server/utils/filesystem-utils"
|
2025-07-03 18:25:50 +00:00
|
|
|
import { watch, type FSWatcher } from "node:fs"
|
|
|
|
|
import { parseConf } from "./utils"
|
|
|
|
|
import { logger } from "$lib/server/utils/logger"
|
2025-07-03 21:34:58 +00:00
|
|
|
import { EndpointStatus, EndpointType } from "$lib/server/enums/endpoints"
|
|
|
|
|
import { validatePath } from "$lib/shared/utils/path-utils"
|
2025-07-02 18:22:41 +00:00
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
export class EndpointBrokerManagerFS {
|
2025-07-02 18:22:41 +00:00
|
|
|
|
|
|
|
|
private static initialized = false
|
2025-07-03 18:25:50 +00:00
|
|
|
private static watcher: FSWatcher
|
|
|
|
|
/** Here we store all endpoints
|
|
|
|
|
* - Key: path
|
|
|
|
|
* - Value: any IEndpointFS
|
|
|
|
|
*/
|
2025-07-02 18:22:41 +00:00
|
|
|
private static endpoints: Map<string, IEndpointFS>
|
2025-07-03 18:25:50 +00:00
|
|
|
private static usedPorts: number[]
|
2025-07-02 18:22:41 +00:00
|
|
|
private static lastNginxReload: Date = new Date()
|
|
|
|
|
|
|
|
|
|
public static get ready() {
|
2025-07-03 18:25:50 +00:00
|
|
|
return EndpointBrokerManagerFS.initialized
|
2025-07-02 18:22:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-03 21:34:58 +00:00
|
|
|
public static async init() {
|
|
|
|
|
|
|
|
|
|
if (EndpointBrokerManagerFS.ready) {
|
|
|
|
|
// UGLY: be specific
|
|
|
|
|
throw new Error("Broker already initialized")
|
|
|
|
|
}
|
2025-07-04 03:05:43 +00:00
|
|
|
|
|
|
|
|
const configurations = (await listFiles(NGINX_TRACKED, true)).filter( async (path) => {
|
|
|
|
|
if (await isDir(path)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!path.endsWith(".conf")) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for (const confPath of configurations) {
|
|
|
|
|
const file = await loadFile(confPath)
|
|
|
|
|
await file.lock()
|
|
|
|
|
|
|
|
|
|
const conf = await file.text()
|
|
|
|
|
const fileStats = await file.getStats()
|
|
|
|
|
|
|
|
|
|
const fsHeader: FSHeader = {
|
|
|
|
|
name: confPath.split("/").pop()!,
|
|
|
|
|
stats: fileStats,
|
|
|
|
|
path: confPath,
|
|
|
|
|
hash: await file.hash()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await file.release()
|
|
|
|
|
const endpoint = await parseConf(fsHeader, conf)
|
2025-07-02 18:22:41 +00:00
|
|
|
|
2025-07-04 03:05:43 +00:00
|
|
|
const relativePath = confPath.replace(NGINX_TRACKED, "")
|
|
|
|
|
EndpointBrokerManagerFS.endpoints.set(
|
|
|
|
|
relativePath,
|
|
|
|
|
endpoint
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 18:22:41 +00:00
|
|
|
|
|
|
|
|
// TODO: Initialize a file watcher
|
2025-07-03 21:34:58 +00:00
|
|
|
EndpointBrokerManagerFS.watcher = EndpointBrokerManagerFS.watchNginxDirectory()
|
|
|
|
|
EndpointBrokerManagerFS.initialized = true
|
2025-07-03 18:25:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async createEndpoint(endpoint: IEndpointFS) {
|
2025-07-03 21:34:58 +00:00
|
|
|
if (endpoint.type === EndpointType.MANUAL) {
|
|
|
|
|
// UGLY: be specific
|
|
|
|
|
throw new Error("You can't create a manual conf automatically")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const REAL_PATH = `${NGINX_TRACKED}/${endpoint.path}`
|
|
|
|
|
|
|
|
|
|
if (await doesFileExist(REAL_PATH) ) {
|
|
|
|
|
// UGLY: be specific
|
|
|
|
|
throw new Error("File already existant")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = await loadFile(REAL_PATH, true)
|
|
|
|
|
await file.lock()
|
|
|
|
|
await file.write(endpoint.toConf())
|
|
|
|
|
await file.release()
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async deleteEndpoint(path: string) {
|
|
|
|
|
|
|
|
|
|
validatePath(path)
|
|
|
|
|
|
|
|
|
|
const REAL_PATH = `${NGINX_TRACKED}/${path}`
|
|
|
|
|
|
|
|
|
|
if (! await doesFileExist(REAL_PATH)) {
|
|
|
|
|
// UGLY: be specific
|
|
|
|
|
throw new Error("This path does not exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = await loadFile(REAL_PATH)
|
|
|
|
|
await file.delete()
|
2025-07-04 03:05:43 +00:00
|
|
|
const endpoint = EndpointBrokerManagerFS.endpoints.get(
|
|
|
|
|
path
|
|
|
|
|
)
|
2025-07-03 21:34:58 +00:00
|
|
|
EndpointBrokerManagerFS.endpoints.delete(
|
|
|
|
|
path
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-04 03:05:43 +00:00
|
|
|
return endpoint
|
|
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async changeEndpoint(path: string, newEndpoint: IEndpointFS) {
|
|
|
|
|
|
2025-07-04 03:05:43 +00:00
|
|
|
if (newEndpoint.type === EndpointType.MANUAL) {
|
|
|
|
|
// UGLY: be specific
|
|
|
|
|
throw new Error("Change of Manual endpoint is not supported yet")
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 21:34:58 +00:00
|
|
|
validatePath(path)
|
|
|
|
|
|
|
|
|
|
const REAL_PATH = `${NGINX_TRACKED}/${path}`
|
|
|
|
|
|
|
|
|
|
if (! await doesFileExist(REAL_PATH)) {
|
|
|
|
|
// UGLY: more specific
|
|
|
|
|
throw new Error("The requested file does not exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const epFile = await loadFile(REAL_PATH)
|
|
|
|
|
await epFile.lock()
|
|
|
|
|
|
|
|
|
|
await epFile.write(
|
|
|
|
|
newEndpoint.toConf()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await epFile.release()
|
|
|
|
|
|
|
|
|
|
EndpointBrokerManagerFS.endpoints.set(path, newEndpoint)
|
|
|
|
|
|
|
|
|
|
const status = await EndpointBrokerManagerFS.getStatus(path)
|
|
|
|
|
|
|
|
|
|
if (status === EndpointStatus.ACTIVE) {
|
|
|
|
|
await this.activateEndpoint(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async activateEndpoint(path: string): Promise<boolean> {
|
2025-07-03 21:34:58 +00:00
|
|
|
|
|
|
|
|
validatePath(path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const endpoint = EndpointBrokerManagerFS.getEndpointByPath(path)
|
|
|
|
|
|
|
|
|
|
if (!endpoint) {
|
|
|
|
|
// UGLY: be specific
|
|
|
|
|
throw new Error("This specified endpoint doesn't exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const activePath = `${NGINX_ACTIVE}/${path}`
|
|
|
|
|
|
|
|
|
|
if (! await doesFileExist(activePath)) {
|
|
|
|
|
const file = await loadFile(activePath, true)
|
|
|
|
|
await file.write(
|
|
|
|
|
endpoint.toConf()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = await loadFile(activePath)
|
|
|
|
|
await file.lock()
|
|
|
|
|
|
|
|
|
|
const fileHash = await file.hash()
|
|
|
|
|
|
|
|
|
|
if (endpoint.hash === fileHash) {
|
|
|
|
|
await file.release()
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await file.write(
|
|
|
|
|
endpoint.toConf()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await file.release()
|
2025-07-03 18:25:50 +00:00
|
|
|
return true
|
2025-07-03 21:34:58 +00:00
|
|
|
|
|
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
}
|
2025-07-02 18:22:41 +00:00
|
|
|
|
2025-07-03 21:34:58 +00:00
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
public static async deactivateEndpoint(path: string): Promise<boolean> {
|
2025-07-03 21:34:58 +00:00
|
|
|
|
|
|
|
|
validatePath(path)
|
|
|
|
|
|
|
|
|
|
const endpoint = EndpointBrokerManagerFS.getEndpointByPath(path)
|
|
|
|
|
|
|
|
|
|
if (!endpoint) {
|
|
|
|
|
// UGLY: be specific
|
|
|
|
|
throw new Error("This specified endpoint doesn't exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const activePath = `${NGINX_ACTIVE}/${path}`
|
|
|
|
|
|
|
|
|
|
if (! await doesFileExist(activePath)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = await loadFile(activePath)
|
|
|
|
|
await file.delete()
|
|
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
return true
|
2025-07-03 21:34:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-02 18:22:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
public static getEndpointByPath(path: string): IEndpointFS | null {
|
|
|
|
|
|
2025-07-03 21:34:58 +00:00
|
|
|
validatePath(path)
|
|
|
|
|
|
|
|
|
|
const endpoint = EndpointBrokerManagerFS.endpoints.get(
|
2025-07-03 18:25:50 +00:00
|
|
|
path
|
|
|
|
|
)
|
2025-07-02 18:22:41 +00:00
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
if (!endpoint) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
2025-07-02 18:22:41 +00:00
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
return endpoint
|
2025-07-02 18:22:41 +00:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
|
|
|
|
|
public static async getAll(): Promise<IEndpointFS[]> {
|
|
|
|
|
return Array.from(EndpointBrokerManagerFS.endpoints.values())
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 21:34:58 +00:00
|
|
|
public static async getStatus(path: string) {
|
|
|
|
|
|
|
|
|
|
validatePath(path)
|
|
|
|
|
|
|
|
|
|
const REAL_ACTIVE_PATH = `${NGINX_ACTIVE}/${path}`
|
|
|
|
|
const REAL_TRACKED_PATH = `${NGINX_TRACKED}/${path}`
|
|
|
|
|
|
|
|
|
|
if (await doesFileExist(REAL_ACTIVE_PATH)) {
|
|
|
|
|
return EndpointStatus.ACTIVE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (await doesFileExist(REAL_TRACKED_PATH)) {
|
|
|
|
|
return EndpointStatus.INACTIVE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UGLY: more specific
|
|
|
|
|
throw new Error("This path is non existant")
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
|
|
|
|
|
// MARK: private methods
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This method is the only one that adds or removes files from
|
|
|
|
|
* our manager. No other method should mess up with the mapping
|
|
|
|
|
* of the manager
|
|
|
|
|
*
|
|
|
|
|
* @returns FSWatcher, to avoid having it garbage collected
|
|
|
|
|
*/
|
|
|
|
|
private static watchNginxDirectory(): FSWatcher {
|
|
|
|
|
|
|
|
|
|
const OPTIONS = {
|
|
|
|
|
recursive: true
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 21:34:58 +00:00
|
|
|
const NGINX_TRACK = NGINX_TRACKED
|
|
|
|
|
|
|
|
|
|
const WATCHER = watch(NGINX_TRACK, OPTIONS, async (eventType, filename) => {
|
|
|
|
|
|
|
|
|
|
if (!filename) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-07-03 18:25:50 +00:00
|
|
|
|
|
|
|
|
const RELATIVE_PATH = filename
|
2025-07-03 21:34:58 +00:00
|
|
|
const FULL_PATH = `${NGINX_TRACK}/${RELATIVE_PATH}`
|
|
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
|
|
|
|
|
// TODO: check if it's a directory, if so, skip
|
|
|
|
|
|
|
|
|
|
if (await isDir(FULL_PATH)) {
|
2025-07-03 21:34:58 +00:00
|
|
|
return
|
2025-07-03 18:25:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UGLY: there may be race conditions, rarely, but
|
|
|
|
|
// UGLY: there may be
|
|
|
|
|
// UGLY: probably solved
|
|
|
|
|
|
|
|
|
|
// TODO: Find a way to lock files
|
|
|
|
|
// TODO: probably solved
|
|
|
|
|
switch (eventType) {
|
|
|
|
|
|
|
|
|
|
case "change": {
|
|
|
|
|
|
|
|
|
|
const oldEndpoint = EndpointBrokerManagerFS.endpoints.get(
|
2025-07-03 21:34:58 +00:00
|
|
|
RELATIVE_PATH
|
2025-07-03 18:25:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!oldEndpoint) {
|
|
|
|
|
logger.debug(`File changed but was never tracked\nPATH: ${FULL_PATH}`, "EP Manager")
|
2025-07-03 21:34:58 +00:00
|
|
|
return
|
2025-07-03 18:25:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nothing to do, it's not managed by us
|
2025-07-03 21:34:58 +00:00
|
|
|
if (oldEndpoint.type === EndpointType.MANUAL) {
|
2025-07-03 18:25:50 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 21:34:58 +00:00
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
const file = await loadFile(FULL_PATH)
|
|
|
|
|
await file.lock()
|
|
|
|
|
|
|
|
|
|
// NOTE: HERE USE FILE
|
|
|
|
|
const newHash = await file.hash()
|
|
|
|
|
const oldHash = oldEndpoint.hash
|
|
|
|
|
|
2025-07-03 21:34:58 +00:00
|
|
|
|
2025-07-03 18:25:50 +00:00
|
|
|
if (newHash === oldHash) {
|
|
|
|
|
// Files are equal
|
|
|
|
|
// or we are very unlucky
|
|
|
|
|
await file.release()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOTE: HERE USE FILE
|
|
|
|
|
const stats = await file.getStats()
|
|
|
|
|
const hash = await file.hash()
|
|
|
|
|
const conf = await file.text()
|
|
|
|
|
|
|
|
|
|
const fsHeader: FSHeader = {
|
|
|
|
|
name: filename!.split("/").pop()!,
|
|
|
|
|
stats: stats,
|
2025-07-04 03:05:43 +00:00
|
|
|
path: FULL_PATH,
|
2025-07-03 18:25:50 +00:00
|
|
|
hash: hash
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newEndpoint = parseConf(fsHeader, conf)
|
|
|
|
|
|
|
|
|
|
// Check if files are trying to represent
|
|
|
|
|
// the same endpoint
|
|
|
|
|
if (oldEndpoint.headerHash() !== newEndpoint.headerHash()) {
|
|
|
|
|
// Files are not equal
|
|
|
|
|
// or we are very unlucky
|
|
|
|
|
await file.release()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Endpoints are different, but changes were never
|
|
|
|
|
// coming from here
|
|
|
|
|
logger.debug(
|
|
|
|
|
"Corrupted file detected", "EP Manager Corruption Detected"
|
|
|
|
|
)
|
|
|
|
|
await file.write(oldEndpoint.toConf())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await file.release()
|
|
|
|
|
// NOTE: HERE DO NOT USE FILE
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Basically it's rename
|
|
|
|
|
default: {
|
|
|
|
|
const isNew = await doesFileExist(FULL_PATH)
|
|
|
|
|
|
|
|
|
|
if (!isNew) {
|
|
|
|
|
// This means that the file doesn't exist
|
|
|
|
|
// let's just acknowledge this
|
|
|
|
|
|
|
|
|
|
// UGLY: not checking for false values
|
|
|
|
|
// UGLY: hints that something went wrong
|
|
|
|
|
EndpointBrokerManagerFS.endpoints.delete(
|
2025-07-03 21:34:58 +00:00
|
|
|
RELATIVE_PATH
|
2025-07-03 18:25:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Technically the file is new
|
|
|
|
|
const file = await loadFile(FULL_PATH)
|
|
|
|
|
await file.lock()
|
|
|
|
|
|
|
|
|
|
// NOTE: HERE USE FILE
|
|
|
|
|
const stats = await file.getStats()
|
|
|
|
|
const hash = await file.hash()
|
|
|
|
|
const conf = await file.text()
|
|
|
|
|
|
|
|
|
|
await file.release()
|
|
|
|
|
|
|
|
|
|
// NOTE: HERE DO NOT USE FILE
|
|
|
|
|
|
|
|
|
|
// UGLY: forcing typecasting
|
|
|
|
|
const fsHeader: FSHeader = {
|
|
|
|
|
name: filename!.split("/").pop()!,
|
|
|
|
|
stats: stats,
|
|
|
|
|
path: FULL_PATH,
|
|
|
|
|
hash: hash
|
|
|
|
|
}
|
|
|
|
|
const endpoint = parseConf(fsHeader, conf)
|
|
|
|
|
|
|
|
|
|
EndpointBrokerManagerFS.endpoints.set(
|
2025-07-03 21:34:58 +00:00
|
|
|
RELATIVE_PATH,
|
2025-07-03 18:25:50 +00:00
|
|
|
endpoint
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return WATCHER
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-02 18:22:41 +00:00
|
|
|
}
|
|
|
|
|
|