V 0.1.1 intermediate changes

Co-authored-by: Oscar Urselli <oscar0urselli@users.noreply.github.com>
This commit is contained in:
Christian Risi 2025-07-03 18:25:50 +00:00
parent 19457d97ae
commit f23cb0e1d2
33 changed files with 603 additions and 56 deletions

View File

@ -13,6 +13,7 @@
"vscode": {
"extensions": [
"svelte.svelte-vscode",
"vitest.explorer",
"william-voyek.vscode-nginx",
"fabiospampinato.vscode-highlight",
"fabiospampinato.vscode-todo-plus"

View File

@ -34,25 +34,15 @@ http {
'"$http_user_agent" "$http_x_forwarded_for"';
# Includes automatic virtual hosts configs.
include /etc/nginx/active/automatic/grpc/*.conf;
include /etc/nginx/active/automatic/http/*.conf;
include /etc/nginx/active/automatic/http2/*.conf;
# Includes virtual hosts configs.
include /etc/nginx/active/http/*.conf;
# Includes manual configs
include /etc/nginx/active/manual/grpc/*.conf;
include /etc/nginx/active/manual/http/*.conf;
include /etc/nginx/active/manual/http2/*.conf;
include /etc/nginx/active/manual/custom/*.conf;
}
stream {
# Include automatic stream config
include /etc/nginx/active/automatic/stream/*.conf;
# Include stream config
include /etc/nginx/active/stream/*.conf;
# Include manual configs
include /etc/nginx/active/manual/stream/*.conf;
}

View File

@ -1,13 +1,25 @@
import type { IEndpointFS } from "$lib/server/broker-utils/FileSystem/endpoints/endpoints"
import type { FSHeader, IEndpointFS } from "$lib/server/broker-utils/FileSystem/endpoints/endpoints"
import { NGINX_BASE } from "$lib/server/utils/constants"
import { doesFileExist, isDir, loadFile } from "$lib/server/utils/filesystem-utils"
import { watch, type FSWatcher } from "node:fs"
import { parseConf } from "./utils"
import { logger } from "$lib/server/utils/logger"
import { EndpointType } from "$lib/server/enums/endpoints"
export class EndpointBrokerManager {
export class EndpointBrokerManagerFS {
private static initialized = false
private static watcher: FSWatcher
/** Here we store all endpoints
* - Key: path
* - Value: any IEndpointFS
*/
private static endpoints: Map<string, IEndpointFS>
private static usedPorts: number[]
private static lastNginxReload: Date = new Date()
public static get ready() {
return EndpointBrokerManager.initialized
return EndpointBrokerManagerFS.initialized
}
public static init() {
@ -16,15 +28,204 @@ export class EndpointBrokerManager {
// TODO: QUICK parse them
// TODO: Initialize a file watcher
}
public static async createEndpoint(endpoint: IEndpointFS) {
}
public static async changeEndpoint(path: string, newEndpoint: IEndpointFS) {
}
public static async activateEndpoint(path: string): Promise<boolean> {
return true
}
public static async deactivateEndpoint(path: string): Promise<boolean> {
return true
}
public static getEndpointByPath(path: string): IEndpointFS | null {
const endpoint = EndpointBrokerManagerFS.endpoints.get(
path
)
if (!endpoint) {
return null
}
return endpoint
}
public async getEndpointByName(name: string): IEndpoint {
public static async getAll(): Promise<IEndpointFS[]> {
return Array.from(EndpointBrokerManagerFS.endpoints.values())
}
// 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
}
const WATCHER = watch(NGINX_BASE, OPTIONS, async (eventType, filename) => {
const RELATIVE_PATH = filename
const FULL_PATH = `${NGINX_BASE}/${RELATIVE_PATH}`
// TODO: check if it's a directory, if so, skip
if (await isDir(FULL_PATH)) {
return
}
// 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(
FULL_PATH
)
if (!oldEndpoint) {
logger.debug(`File changed but was never tracked\nPATH: ${FULL_PATH}`, "EP Manager")
return
}
// Nothing to do, it's not managed by us
if(oldEndpoint.type === EndpointType.MANUAL) {
return
}
const file = await loadFile(FULL_PATH)
await file.lock()
// NOTE: HERE USE FILE
const newHash = await file.hash()
const oldHash = oldEndpoint.hash
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,
path: FULL_PATH,
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(
FULL_PATH
)
}
// 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(
FULL_PATH,
endpoint
)
return
}
}
})
return WATCHER
}
}

View File

@ -38,15 +38,15 @@ export class SSLTerminationBroker implements ISSLTerminationBroker {
throw new Error("Method not implemented.");
}
async getSSLTerminationByName(name: string): Promise<SSLTermination | null> {
async getSSLTerminationByPath(name: string): Promise<SSLTermination | null> {
throw new Error("Method not implemented.");
}
async modifySSLTerminationByName(name: string, changes: SSLTerminationChanges): Promise<SSLTermination> {
async modifySSLTerminationByPath(name: string, changes: SSLTerminationChanges): Promise<SSLTermination> {
throw new Error("Method not implemented.");
}
async deleteSSLTerminationByName(name: string): Promise<SSLTermination | null> {
async deleteSSLTerminationByPath(name: string): Promise<SSLTermination | null> {
throw new Error("Method not implemented.");
}

View File

@ -26,6 +26,8 @@ export interface IEndpointFS {
ports(): number[]
headerHash(): string
}
export type FSHeader = {

View File

@ -0,0 +1,60 @@
import { EndpointType } from "$lib/server/enums/endpoints";
import type { Stats } from "fs";
import type { FSHeader, IEndpointFS } from "./endpoints";
// TODO: add broker implementation
export class ManualFS implements IEndpointFS {
private static __type = EndpointType.MANUAL
private static __hash = "emanuel"
public get type() {
return ManualFS.__type
}
public get hash() {
return ManualFS.__hash
}
name: string;
stats: Stats;
path: string;
body: string;
constructor(
name: string,
stats: Stats,
path: string,
body: string
) {
this.name = name
this.stats = stats
this.path = path
this.body = body
}
toConf(): string {
return this.body
}
ports(): number[] {
return []
}
headerHash(): string {
return this.hash
}
public static parseConf(fsHeader: FSHeader, conf: string) {
return new ManualFS(
fsHeader.name,
fsHeader.stats,
fsHeader.path,
conf
)
}
}

View File

@ -4,6 +4,10 @@ import { validatePort } from "$lib/server/utils/ports-utils"
import type { Stats } from "fs"
import type { FSHeader, IEndpointFS } from "./endpoints"
import { createHeader } from "../utils"
import { hashUtil } from "$lib/server/utils/filesystem-utils"
// TODO: add broker implementation
export class SSLTerminationFS implements IEndpointFS {
@ -42,10 +46,6 @@ export class SSLTerminationFS implements IEndpointFS {
privateKeyURI: string
) {
validatePort(sslPort)
validatePort(clearPort)
validatePort(servicePort)
this.name = name
this.stats = stats
this.path = path
@ -60,6 +60,13 @@ export class SSLTerminationFS implements IEndpointFS {
}
headerHash(): string {
return hashUtil(
this.createHeader()
)
}
toConf(): string {
const HEADER = this.createHeader()

View File

@ -1,5 +1,6 @@
import { EndpointType, matchEndpoint } from "$lib/server/enums/endpoints"
import type { FSHeader, IEndpointFS } from "./endpoints/endpoints"
import { ManualFS } from "./endpoints/manual-fs"
import { SSLTerminationFS } from "./endpoints/ssltermination-fs"
export const HEADER_BOUNDARY = "**********************************************************"
@ -23,7 +24,7 @@ export function parseConf(
case EndpointType.SSL_TERMINATION:
return SSLTerminationFS.parseConf(fsHeader, conf)
default:
// TODO: add manual endpoint
return ManualFS.parseConf(fsHeader, conf)
}
}
@ -37,6 +38,7 @@ export function createHeader(endpoint: IEndpointFS, variables?: HeaderKeyValueFS
HEADER_UPPER,
"SSL-SNIFFER AUTOMATICALLY GENERATED",
`TYPE: ${endpoint.type}`,
`NAME: ${endpoint.name}`,
HEADER_BOUNDARY
]

View File

@ -4,5 +4,6 @@ export interface IEndpoint {
type: EndpointType
name: string
path: string
}

View File

@ -0,0 +1,55 @@
import { EndpointBrokerManagerFS } from "$lib/server/broker-utils/FileSystem/Endpoint"
import type { IEndpoint } from "./endpoints-interfaces"
// UGLY: refactor this
export class EndpointManagerApp {
private static initialized = false
/** Here we store all endpoints
* - Key: path
* - Value: any IEndpointFS
*/
public static get ready() {
return EndpointManagerApp.initialized
}
public static init() {
// TODO: Read all files
// TODO: QUICK parse them
// TODO: Initialize a file watcher
}
public static async activateEndpoint(path: string): Promise<boolean> {
return await EndpointBrokerManagerFS.activateEndpoint(path)
}
public static async deactivateEndpoint(path: string): Promise<boolean> {
return await EndpointBrokerManagerFS.deactivateEndpoint(path)
}
public static getEndpointByPath(path: string): IEndpoint | null {
// UGLY: parse
return EndpointBrokerManagerFS.getEndpointByPath(path)
}
public static async getAll(): Promise<IEndpoint[]> {
// UGLY: parse
return await EndpointBrokerManagerFS.getAll()
}
}

View File

@ -0,0 +1,115 @@
import { EndpointType } from "$lib/server/enums/endpoints";
import type { init } from "../../../../hooks.server";
import type { IEndpoint } from "./endpoints-interfaces";
export interface IManualBroker {
init(): Promise<void>
getManualByPath(path: string): Promise<Manual>
getAllManuals(): Promise<Manual[]>
activateEndpointByPath(
path: string
): Promise<boolean>
deactivateEndpointByPath(
path: string
): Promise<boolean>
}
export class Manual implements IEndpoint {
private static __type = EndpointType.SSL_TERMINATION
public get type() {
return Manual.__type
}
public name: string;
public path: string;
public body: string;
constructor(
name: string,
path: string,
body: string
) {
this.name = name
this.path = path
this.body = body
}
}
export class ManualEndpointApp {
private static initialized: boolean = false
private static broker: IManualBroker
public static get ready() {
return ManualEndpointApp.initialized
}
public static init(broker: IManualBroker) {
ManualEndpointApp.assureNotInitialized()
ManualEndpointApp.broker = broker
broker.init()
ManualEndpointApp.initialized = true
}
public static async getManualByPath(path: string) {
ManualEndpointApp.assureInitialized()
return ManualEndpointApp.broker.getManualByPath(path)
}
public static async getAllManual() {
ManualEndpointApp.assureInitialized()
return await ManualEndpointApp.broker.getAllManuals()
}
public static async activateEndpointByPath(
path: string
): Promise<boolean> {
ManualEndpointApp.assureInitialized()
return await ManualEndpointApp.broker.activateEndpointByPath(path)
}
public static async deactivateEndpointByPath(
path: string
): Promise<boolean> {
ManualEndpointApp.assureInitialized()
return await ManualEndpointApp.broker.deactivateEndpointByPath(path)
}
private static assureNotInitialized() {
if (ManualEndpointApp.initialized) {
// UGLY: more specific
throw new Error("SSLTerminationEndpointApp has been already initialized")
}
}
private static assureInitialized() {
if (ManualEndpointApp.initialized) {
// UGLY: more specific
throw new Error("SSLTerminationEndpointApp has not been initialized yet")
}
}
}

View File

@ -11,6 +11,9 @@ export interface ISSLTerminationBroker {
*/
init(): Promise<void>
// TODO: in the next version support
// TODO: creation of endpoints
// TODO: according to path
// Creation should throw if something goes wrong
// with reasons why
createSSLTerminationSimple(
@ -31,22 +34,30 @@ export interface ISSLTerminationBroker {
privateKeyURI: string
): Promise<SSLTermination>
activateEndpointByPath(
path: string
): Promise<boolean>
deactivateEndpointByPath(
path: string
): Promise<boolean>
// Getting endpoints may be null, react over them
getSSLTerminationByName(
name: string
getSSLTerminationByPath(
path: string
): Promise<SSLTermination|null>
// Throw if something goes wrong
modifySSLTerminationByName(
name: string,
modifySSLTerminationByPath(
path: string,
changes: SSLTerminationChanges
): Promise<SSLTermination>
deleteSSLTerminationByName(
name: string
deleteSSLTerminationByPath(
path: string
): Promise<SSLTermination|null>
@ -70,6 +81,7 @@ export class SSLTermination implements IEndpoint {
}
public name: string
public path: string
public sslPort: number
public clearPort: number
public servicePort: number
@ -82,6 +94,7 @@ export class SSLTermination implements IEndpoint {
constructor(
name: string,
path: string,
sslPort: number,
clearPort: number,
servicePort: number,
@ -91,11 +104,8 @@ export class SSLTermination implements IEndpoint {
privateKeyURI: string
) {
validatePort(sslPort)
validatePort(clearPort)
validatePort(servicePort)
this.name = name
this.path = path
this.sslPort = sslPort
this.clearPort = clearPort
this.servicePort = servicePort
@ -111,6 +121,7 @@ export class SSLTermination implements IEndpoint {
export type SSLTerminationChanges = {
name?: string,
path?: string,
sslPort?: number,
clearPort?: number,
servicePort?: number,
@ -187,13 +198,13 @@ export class SSLTerminationEndpointApp {
// Getting endpoints may be null, react over them
public static async getSSLTerminationByName(
public static async getSSLTerminationByPath(
name: string
): Promise<SSLTermination|null> {
SSLTerminationEndpointApp.assureInitialized()
return await this.broker.getSSLTerminationByName(
return await this.broker.getSSLTerminationByPath(
name
)
@ -201,25 +212,25 @@ export class SSLTerminationEndpointApp {
// Throw if something goes wrong
public static async modifySSLTerminationByName(
public static async modifySSLTerminationByPath(
name: string,
changes: SSLTerminationChanges
): Promise<SSLTermination> {
SSLTerminationEndpointApp.assureInitialized()
return await this.broker.modifySSLTerminationByName(
return await this.broker.modifySSLTerminationByPath(
name,
changes
)
}
public static async deleteSSLTerminationByName(
public static async deleteSSLTerminationByPath(
name: string
): Promise<SSLTermination|null> {
SSLTerminationEndpointApp.assureInitialized()
return await this.broker.deleteSSLTerminationByName(
return await this.broker.deleteSSLTerminationByPath(
name
)
}
@ -228,7 +239,23 @@ export class SSLTerminationEndpointApp {
public static async getAllSSLTerminations(): Promise<SSLTermination[]> {
SSLTerminationEndpointApp.assureInitialized()
return await this.broker.getAllSSLTerminations()
return await SSLTerminationEndpointApp.broker.getAllSSLTerminations()
}
public static async activateEndpointByPath(path: string) {
SSLTerminationEndpointApp.assureInitialized()
return await SSLTerminationEndpointApp.broker.activateEndpointByPath(
path
)
}
public static async deactivateEndpointByPath(path: string) {
SSLTerminationEndpointApp.assureInitialized()
return await SSLTerminationEndpointApp.broker.deactivateEndpointByPath(
path
)
}

View File

@ -1,6 +1,7 @@
export const PKG = __PKG__
export const DB_PATH = "src/db/db.sqlite"
export const SERVER_PRIVATE_DIR = "src/private"
export const SERVER_TMP_FILE = `${SERVER_PRIVATE_DIR}/tmp.pem`
export const SERVER_PRIVATE_KEY_PATH = `${SERVER_PRIVATE_DIR}/key.pem`
export const SERVER_PUBLIC_KEY_PATH = `${SERVER_PRIVATE_DIR}/pub.pem`
export const DEBUG = import.meta.env.DEV

View File

@ -1,27 +1,37 @@
import { Stats, type OpenMode } from 'node:fs';
import { Stats, type OpenMode } from 'node:fs';
import * as Node from 'node:fs/promises';
import { createHash, type BinaryLike } from 'node:crypto';
export type FileStats = Stats
export class FileHandle {
private path: string
private fd: Node.FileHandle | null
private get file() {
if (!this.fd) {
return this.path
}
return this.fd
}
constructor(
path: string
) {
this.path = path
this.fd = null
}
public async text(encoding?: BufferEncoding) {
if (!encoding) {
encoding = "utf-8"
}
const fileContent = await Node.readFile(this.path, {encoding: encoding})
const fileContent = await Node.readFile(this.path, { encoding: encoding })
return fileContent
}
@ -33,9 +43,9 @@ export class FileHandle {
append = true
}
const flag : OpenMode = append ? "a+" : "w"
const flag: OpenMode = append ? "a+" : "w"
await Node.writeFile(this.path, text, {flag: flag})
await Node.writeFile(this.file, text, { flag: flag })
}
@ -49,6 +59,26 @@ export class FileHandle {
return stats
}
public async hash() {
return hashUtil(
await Node.readFile(this.file)
)
}
public async lock() {
this.fd = await Node.open(this.path, "r+")
}
public async release() {
if (!this.fd) {
return
}
await this.fd.close()
this.fd = null
}
}
@ -67,7 +97,13 @@ export async function doesFileExist(path: string): Promise<boolean> {
}
return true
}
export async function isDir(path: string): Promise<boolean> {
const stats = await Node.stat(path)
return stats.isDirectory()
}
@ -81,7 +117,7 @@ export async function loadFile(path: string, create?: boolean): Promise<FileHand
create = false
}
let fd : Node.FileHandle | null = null
let fd: Node.FileHandle | null = null
try {
fd = await Node.open(path, "r+", DEFAULT_MODE)
@ -104,7 +140,16 @@ export async function loadFile(path: string, create?: boolean): Promise<FileHand
// We do want this to throw without catching here
fd = await Node.open(path, "w", DEFAULT_MODE)
await fd.close()
return new FileHandle(path)
}
}
// UGLY: move this to a new file
export function hashUtil(data: BinaryLike) {
const hash = createHash("sha256")
.update(
data
).digest('base64')
return hash
}

View File

@ -1,5 +1,5 @@
import { doesFileExist, loadFile, type FileHandle } from "./filesystem-utils";
import { SERVER_PRIVATE_KEY_PATH, SERVER_PUBLIC_KEY_PATH } from "./constants";
import { SERVER_PRIVATE_KEY_PATH, SERVER_PUBLIC_KEY_PATH, SERVER_TMP_FILE } from "./constants";
import { shell, type shellOutput } from "./shell-commands";
export async function openSSLInit() {
@ -12,7 +12,9 @@ export async function openSSLInit() {
export async function openSSLCreatePrivateKey() {
// UGLY: may be refactored to output only the private key
const outputPromise = shell(`openssl ecparam -genkey -name secp521r1 -noout | openssl pkcs8 -topk8 -nocrypt`)
const pemPromise = await shell(`openssl ecparam -genkey -name secp521r1 -noout -out ${SERVER_TMP_FILE}`)
const outputPromise = shell(`openssl pkcs8 -topk8 -nocrypt < ${SERVER_TMP_FILE}`)
// const outputPromise = $`openssl ecparam -genkey -name secp521r1 -noout | openssl pkcs8 -topk8 -nocrypt`.text()
const filePromise = loadFile(SERVER_PRIVATE_KEY_PATH, true)

View File

@ -0,0 +1,6 @@
import { RequestHandler } from "@sveltejs/kit";
export const PATCH: RequestHandler = ({ url }) => {
};

View File

@ -0,0 +1,3 @@
export const GET: RequestHandler = ({}) => {
}

View File

@ -29,7 +29,7 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
if (!session) {
// The user is not providing credentials
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401
return error(401, "Unauthorized")
}

View File

@ -27,6 +27,8 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
return redirect(307, "api/program/register")
}
console.log(session)
if (session) {
// The user is providing valid credentials
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403

View File

@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest';
import { watch } from "node:fs"
import * as readline from "node:readline"
// TODO: make tests for Database
describe('watch nginx', () => {
const OPTIONS = {
recursive: true
}
const NGINX_BASE = "/etc/nginx"
const input = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
watch(NGINX_BASE, OPTIONS, (eventType, filename) => {
console.log(`event: ${eventType}`)
console.log(`file: ${filename}`)
})
console.log("Done")
});