Compare commits

...

47 Commits

Author SHA1 Message Date
Christian Risi
37d977e444 Fixed a bug where the map wasn't initialized before use 2025-07-04 10:11:57 +00:00
Christian Risi
15f38a3f62 Fixed a parsing bug relative to paths 2025-07-04 09:00:53 +00:00
Christian Risi
7f5d5ba1b1 Fixed a bug where you would receive relative paths by default while asking for contents of a dir 2025-07-04 09:00:39 +00:00
Christian Risi
67c8e9c31d V 0.1.2 Ended writing backend utils 2025-07-04 03:05:43 +00:00
Christian Risi
3eda2cf0a1 TODO: add Brokers for both SSLTerminationFS and Manual, complete broker manager init
Co-authored-by: Oscar Urselli <oscar0urselli@users.noreply.github.com>
2025-07-03 21:34:58 +00:00
Christian Risi
f23cb0e1d2 V 0.1.1 intermediate changes
Co-authored-by: Oscar Urselli <oscar0urselli@users.noreply.github.com>
2025-07-03 18:25:50 +00:00
19457d97ae V 0.1.0 Added barebones for creating nginx conf and reworked registration during deploy 2025-07-02 18:22:41 +00:00
6d3036c285 Removed unused imports and marked todo as completed 2025-07-02 15:11:39 +00:00
0f49a24dec Updated Logger to have a single source of truth 2025-07-02 15:03:18 +00:00
d7282c0c89 Porting from bun to node 2025-07-02 14:49:23 +00:00
0fbbfec737 Rework to add endpoint creation and better handling 2025-07-01 17:59:23 +00:00
3de4354458 Fixed a buf for not waiting async method 2025-06-30 20:10:43 +00:00
d7a87f54bb Added UI to handle login and registration 2025-06-30 20:06:47 +00:00
b62d101a88 Fixed bugs and added logging 2025-06-30 20:06:30 +00:00
5f89985939 Fixed Key generation and update signing logic 2025-06-30 20:05:49 +00:00
f55cc48656 Fixed some Query bugs 2025-06-30 20:05:28 +00:00
177382d9c3 Added handle for UI 2025-06-30 20:04:48 +00:00
6c4dd63ee9 Updated GitIgnore to exclude DB Files 2025-06-30 20:04:30 +00:00
b5ecbbca52 Remove logic to prevent running init 2 times 2025-06-30 20:01:45 +00:00
Christian Risi
a0639f6094 Added Routes to permit user login and registration 2025-06-30 11:57:04 +00:00
Christian Risi
4b63a236a3 Reflected changes of requirements for routes 2025-06-30 11:56:35 +00:00
Christian Risi
64453aa176 Reflected changes in requirements and added logging 2025-06-30 11:56:09 +00:00
Christian Risi
3cac439056 Added Application Session 2025-06-30 11:55:44 +00:00
Christian Risi
678fe8c300 Added Logging 2025-06-30 11:55:29 +00:00
Christian Risi
14bfab994d Added Logging 2025-06-30 11:55:17 +00:00
Christian Risi
125409fda3 Fixed bugs and redid interface 2025-06-30 11:54:43 +00:00
Christian Risi
363c25045c Added handles to handle traffic to api 2025-06-30 11:54:20 +00:00
Christian Risi
91adb5ec98 Initialize app 2025-06-30 11:53:53 +00:00
Christian Risi
fcab0782d5 Reflected changes in requirements 2025-06-30 11:53:28 +00:00
Christian Risi
9419ce3533 Changed to expose server 2025-06-30 11:53:06 +00:00
Christian Risi
ea578433f3 Changed Tree to reflect changes in requirements 2025-06-29 17:56:21 +00:00
Christian Risi
4f6f48a992 Changes to fix file structure 2025-06-29 17:53:06 +00:00
Christian Risi
932b770c8f Added utils for cryptography and filesystem interactions 2025-06-29 17:52:46 +00:00
Christian Risi
002fd58585 Update Session to reflect asjustments to structure 2025-06-29 17:52:21 +00:00
Christian Risi
f13a44ba5a Deleted files due to changes in requirements 2025-06-29 17:51:48 +00:00
Christian Risi
4cf4a01e25 Added Stubs for JWT 2025-06-29 11:05:39 +00:00
Christian Risi
db769f4f96 Added Support for Sessions 2025-06-29 11:04:47 +00:00
Christian Risi
6532a1988d Added support from Foreign Keys 2025-06-29 11:03:05 +00:00
Christian Risi
a38431f6ca Deleted External Sessions as Requirements Changed 2025-06-29 11:02:49 +00:00
Christian Risi
df97ce321e Moved Files to reflect previous changes to the tree structure 2025-06-29 11:02:23 +00:00
Christian Risi
560cf8fdb1 Added Jose for JWT Signing 2025-06-29 11:01:47 +00:00
Christian Risi
85da2dbdc6 Changes to support Sessions better 2025-06-29 11:01:31 +00:00
Christian Risi
9a3be1cfe0 Added stub for database testing 2025-06-28 21:53:02 +00:00
Christian Risi
3618af361b Created barebones for user database 2025-06-28 21:52:44 +00:00
Christian Risi
925610161e Added Argon2 as a Dependency 2025-06-28 21:52:24 +00:00
Christian Risi
22eacc6bca Added Stubs for SQlite Database 2025-06-28 17:19:40 +00:00
Christian Risi
ddac609b97 Initial Commit 2025-06-28 17:07:53 +00:00
89 changed files with 7711 additions and 87 deletions

View File

@ -1,9 +1,9 @@
{
// Displayed name
"name": "Vulnbox",
"name": "SSL-Sniffer",
// Service name from compose file
"service": "vulnbox",
"service": "ssl-sniffer",
// Compose-File
"dockerComposeFile": ["../compose.yaml"],
@ -12,6 +12,8 @@
"customizations": {
"vscode": {
"extensions": [
"svelte.svelte-vscode",
"vitest.explorer",
"william-voyek.vscode-nginx",
"fabiospampinato.vscode-highlight",
"fabiospampinato.vscode-todo-plus"
@ -20,7 +22,7 @@
},
// The WorkspaceFolder inside container
"workspaceFolder": "/etc/nginx",
"workspaceFolder": "/workspace",
// Env in container
"containerEnv": {

31
.gitignore vendored
View File

@ -1,7 +1,36 @@
test-results
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Custom
config/*/*
private/**
services/**
src/private/**
src/db/**
**/http/*
!**/*.gitkeep
!**/*.example
!**/http/example.conf
!**/http/example.conf

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

14
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"command": "npm run dev -- --open --host",
"name": "Node Launch server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@ -1,4 +1,4 @@
FROM alpine
FROM node:alpine
ENV PATH="$PATH:/docker-bin"
@ -6,7 +6,7 @@ RUN apk update && apk upgrade
RUN apk add nginx openrc \
openssl nginx-mod-stream \
nginx-mod-http-headers-more \
tcpdump
tcpdump curl git
# NGINX
RUN adduser -D -g 'www' www
@ -19,9 +19,13 @@ RUN cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig
RUN mkdir /run/openrc
RUN touch /run/openrc/softlevel
# Make workdir
RUN mkdir /workspace
# Make entrypoint
WORKDIR /docker-bin
COPY ./helper-scripts /docker-bin
COPY ./helper-scripts/scripts /docker-bin
RUN chmod +x /docker-bin/*

105
README.md
View File

@ -1,81 +1,38 @@
# SSL Sniffer
# sv
> [!CAUTION]
> While the name may suggest this software has `packet-sniffing`
> capabilities,
> this software only ***ease*** the `sniffing-process` by terminating `TLS`
> in a transparent way.
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## How to use the software
## Creating a project
- Have [Firegex](https://github.com/Pwnzer0tt1/firegex) installed on the `vulnbox` (OPTIONAL)
- Copy all keys on `ssl-sniffer/private/<service-name>/[key|cert].pem`
- Copy a template from one of the available templates:
- `grpc`: `cp ssl-sniffer/nginx/grpc/conf.example ssl-sniffer/nginx/grpc/<service-name>.conf`
- `http`: `cp ssl-sniffer/nginx/http/conf.example ssl-sniffer/nginx/http/<service-name>.conf`
- Modify the copied template
- Add a rule to hijack the port to the one specified in your conf (OPTIONAL)
- Run `docker compose up -d --build`
- Check that your service is still reachable
If you're seeing this, you've probably already done this step. Congrats!
> [!TIP]
> Remember to capture traffic from the `lo` interface, otherwise you won't
> see any benefit in setting such infrastructure
```bash
# create a new project in the current directory
npx sv create
## Full example
### Cheesy Cheats-API Template
```nginx
# CheesyAPI conf
# CheesyAPI TLS endpoint
server {
# Use this to avoid port scanners to know
# what you are using
more_clear_headers Server;
# Here put the TLS termination
# endpoint port
listen 15555 ssl;
http2 on;
# Here put the unencrypted
# endpoint port
location / {
grpc_pass grpc://127.0.0.1:15554;
}
# Put relevant keys here
ssl_certificate /services-keys/CheesyAPI/cert.pem;
ssl_certificate_key /services-keys/CheesyAPI/key.pem;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
}
# Example Termination endpoint
server {
# Use this to avoid port scanners to know
# what you are using
more_clear_headers Server;
# Here put the unencrypted
# endpoint port
listen 127.0.0.1:15554;
http2 on;
# Here put the original
# service endpoint port
location / {
grpc_pass grpcs://127.0.0.1:5555;
}
}
# create a new project in my-app
npx sv create my-app
```
![firegex-example-image](images/firegex-example.png)
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

View File

@ -6,5 +6,6 @@ services:
volumes:
- ./nginx:/etc/nginx/
- ./private/:/services-keys
- .:/workspace
network_mode: host
entrypoint: entry.sh

6
e2e/demo.test.ts Normal file
View File

@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});

39
eslint.config.js Normal file
View File

@ -0,0 +1,39 @@
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: { // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
"no-undef": 'off' }
},
{
files: [
'**/*.svelte',
'**/*.svelte.ts',
'**/*.svelte.js'
],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

View File

@ -0,0 +1,73 @@
#!/sbin/openrc-run
description="Nginx http and reverse proxy server"
extra_commands="checkconfig"
extra_started_commands="reload reopen upgrade"
cfgfile=${cfgfile:-/etc/nginx/nginx.conf}
pidfile=/run/nginx/nginx.pid
command=${command:-/usr/sbin/nginx}
command_args="-c $cfgfile"
required_files="$cfgfile"
depend() {
need net
use dns logger netmount
}
start_pre() {
checkpath --directory --owner nginx:nginx ${pidfile%/*}
$command $command_args -t -q
}
checkconfig() {
ebegin "Checking $RC_SVCNAME configuration"
start_pre
eend $?
}
reload() {
ebegin "Reloading $RC_SVCNAME configuration"
# start_pre && start-stop-daemon --signal HUP --pidfile $pidfile
nginx -s reload
# call our service
eend $?
}
reopen() {
ebegin "Reopening $RC_SVCNAME log files"
start-stop-daemon --signal USR1 --pidfile $pidfile
eend $?
}
upgrade() {
start_pre || return 1
ebegin "Upgrading $RC_SVCNAME binary"
einfo "Sending USR2 to old binary"
start-stop-daemon --signal USR2 --pidfile $pidfile
einfo "Sleeping 3 seconds before pid-files checking"
sleep 3
if [ ! -f $pidfile.oldbin ]; then
eerror "File with old pid ($pidfile.oldbin) not found"
return 1
fi
if [ ! -f $pidfile ]; then
eerror "New binary failed to start"
return 1
fi
einfo "Sleeping 3 seconds before WINCH"
sleep 3 ; start-stop-daemon --signal 28 --pidfile $pidfile.oldbin
einfo "Sending QUIT to old binary"
start-stop-daemon --signal QUIT --pidfile $pidfile.oldbin
einfo "Upgrade completed"
eend $? "Upgrade failed"
}

View File

@ -12,9 +12,6 @@ server {
# endpoint port
listen PORT ssl;
# Uncomment if http2
# http2 on;
# Here put the unencrypted
# endpoint port
location / {

View File

@ -0,0 +1,50 @@
# Example conf
# Example TLS endpoint
server {
# Use this to avoid port scanners to know
# what you are using
more_clear_headers Server;
# Here put the TLS termination
# endpoint port
listen PORT ssl;
http2 on;
# Here put the unencrypted
# endpoint port
location / {
proxy_pass http://localhost:8080;
}
# Put relevant keys here
ssl_certificate /services-keys/Example/cert.pem;
ssl_certificate_key /services-keys/Example/key.pem;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
}
# Example Termination endpoint
server {
# Use this to avoid port scanners to know
# what you are using
more_clear_headers Server;
# Here put the unencrypted
# endpoint port
listen 127.0.0.1:8080;
# Uncomment if http2
# http2 on;
# Here put the original
# service endpoint port
location / {
proxy_pass https://127.0.0.1:PORT;
}
}

View File

View File

@ -35,12 +35,14 @@ http {
# Includes virtual hosts configs.
include /etc/nginx/http/*.conf;
include /etc/nginx/grpc/*.conf;
include /etc/nginx/active/http/*.conf;
}
stream {
include /etc/nginx/stream*.conf;
# Include stream config
include /etc/nginx/active/stream/*.conf;
}

3269
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "workspace",
"version": "0.0.1",
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.49.1",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@vitest/browser": "^3.2.3",
"eslint": "^9.18.0",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"playwright": "^1.53.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.2.6",
"vitest": "^3.2.4",
"vitest-browser-svelte": "^0.1.0"
},
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run && npm run test:e2e",
"test:e2e": "playwright test"
},
"type": "module",
"dependencies": {
"argon2": "^0.43.0",
"jose": "^6.0.11"
}
}

9
playwright.config.ts Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'e2e'
});

20
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
import type { AppData } from "$lib/server/classes/appdata";
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
interface Locals {
session: AppData | null
}
}
}
export { };

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

0
src/db/.gitkeep Normal file
View File

65
src/hooks.server.ts Normal file
View File

@ -0,0 +1,65 @@
import type { ServerInit } from '@sveltejs/kit';
import { handles } from './lib/server/handles/handle';
import { DatabaseBrokerManager } from '$lib/server/broker-utils/SQLite/Database';
import { UserApp } from '$lib/server/classes/users';
import { UserDBBroker } from '$lib/server/broker-utils/SQLite/Users';
import { SessionApp } from '$lib/server/classes/sessions';
import { SessionDBBroker } from '$lib/server/broker-utils/SQLite/Sessions';
import { AppData } from '$lib/server/classes/appdata';
import { logger } from '$lib/server/utils/logger';
import { JoseApp } from '$lib/server/utils/jtw-utils';
import { EndpointManagerApp } from '$lib/server/classes/endpoints/endpoint-manager';
import { SSLTerminationEndpointApp } from '$lib/server/classes/endpoints/ssl-termination-endpoint';
import { SSLTerminationFSBroker } from '$lib/server/broker-utils/FileSystem/SSLTerminations';
import { ManualEndpointApp } from '$lib/server/classes/endpoints/manual-endpoint';
import { ManualFSBroker } from '$lib/server/broker-utils/FileSystem/Manual';
export const init: ServerInit = async () => {
logger.debug("Starting app", "App Init")
if (!DatabaseBrokerManager.ready) {
DatabaseBrokerManager.init()
}
if (!UserApp.ready) {
UserApp.init(
new UserDBBroker()
)
}
if (!SessionApp.ready) {
SessionApp.init(
new SessionDBBroker()
)
}
if (!JoseApp.ready) {
// This is async
await JoseApp.init()
}
if(!EndpointManagerApp.ready) {
// This is async
await EndpointManagerApp.init()
}
if (!SSLTerminationEndpointApp.ready) {
SSLTerminationEndpointApp.init(
new SSLTerminationFSBroker()
)
}
if (!ManualEndpointApp.ready) {
ManualEndpointApp.init(
new ManualFSBroker()
)
}
logger.debug("Init run successfully", "App Init")
};
export const handle = handles

View File

@ -0,0 +1,465 @@
import type { FSHeader, IEndpointFS } from "$lib/server/broker-utils/FileSystem/endpoints/endpoints"
import { NGINX_ACTIVE, NGINX_BASE, NGINX_INACTIVE, NGINX_TRACKED } from "$lib/server/utils/constants"
import { doesFileExist, isDir, listFiles, 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 { EndpointStatus, EndpointType } from "$lib/server/enums/endpoints"
import { validatePath } from "$lib/shared/utils/path-utils"
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 EndpointBrokerManagerFS.initialized
}
public static async init() {
if (EndpointBrokerManagerFS.ready) {
// UGLY: be specific
throw new Error("Broker already initialized")
}
EndpointBrokerManagerFS.endpoints = new Map()
const candidateConfigurations = (await listFiles(NGINX_TRACKED, true))
const configurations: string[] = []
for (const candidatePath of candidateConfigurations) {
if (await isDir(candidatePath)) {
continue
}
if (!candidatePath.endsWith(".conf")) {
continue
}
configurations.push(candidatePath)
}
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)
logger.debug(
`Parsed Conf:\n${endpoint}`,
"Manager Init"
)
const relativePath = confPath.replace(NGINX_TRACKED, "")
EndpointBrokerManagerFS.endpoints.set(
relativePath,
endpoint
)
}
// TODO: Initialize a file watcher
EndpointBrokerManagerFS.watcher = EndpointBrokerManagerFS.watchNginxDirectory()
EndpointBrokerManagerFS.initialized = true
}
public static async createEndpoint(endpoint: IEndpointFS) {
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()
const endpoint = EndpointBrokerManagerFS.endpoints.get(
path
)
EndpointBrokerManagerFS.endpoints.delete(
path
)
return endpoint
}
public static async changeEndpoint(path: string, newEndpoint: IEndpointFS) {
if (newEndpoint.type === EndpointType.MANUAL) {
// UGLY: be specific
throw new Error("Change of Manual endpoint is not supported yet")
}
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)
}
}
public static async activateEndpoint(path: string): Promise<boolean> {
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()
return true
}
public static async deactivateEndpoint(path: string): Promise<boolean> {
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()
return true
}
public static getEndpointByPath(path: string): IEndpointFS | null {
validatePath(path)
const endpoint = EndpointBrokerManagerFS.endpoints.get(
path
)
if (!endpoint) {
return null
}
return endpoint
}
public static async getAll(): Promise<IEndpointFS[]> {
return Array.from(EndpointBrokerManagerFS.endpoints.values())
}
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")
}
// 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 NGINX_TRACK = NGINX_TRACKED
const WATCHER = watch(NGINX_TRACK, OPTIONS, async (eventType, filename) => {
logger.debug(
`EVENT: ${eventType}\nFile: ${filename}`,
"Watcher "
)
if (!filename) {
return
}
const RELATIVE_PATH = filename
const FULL_PATH = `${NGINX_TRACK}/${RELATIVE_PATH}`
if (!RELATIVE_PATH.endsWith(".conf")) {
return
}
// 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(
RELATIVE_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(
RELATIVE_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)
logger.debug(
`Endpoint:\n${endpoint}`,
"Watcher Parsing"
)
EndpointBrokerManagerFS.endpoints.set(
RELATIVE_PATH,
endpoint
)
return
}
}
})
return WATCHER
}
}

View File

@ -0,0 +1,48 @@
import type { IManualBroker, Manual } from "$lib/server/classes/endpoints/manual-endpoint";
import { EndpointType } from "$lib/server/enums/endpoints";
import { EndpointBrokerManagerFS } from "./Endpoint-Manager";
import type { ManualFS } from "./endpoints/manual-fs";
export class ManualFSBroker implements IManualBroker {
public async init(): Promise<void> {
// Do nothing?
}
public async getManualByPath(path: string): Promise<Manual| null> {
const endpoint = EndpointBrokerManagerFS.getEndpointByPath(path)
if (!endpoint) {
return null
}
if (endpoint.type !== EndpointType.MANUAL) {
// UGLY: be specific
throw new Error("This is not a Manual Endpoint")
}
return endpoint.toIEndpoint() as Manual
}
public async getAllManuals(): Promise<Manual[]> {
const endpoints = (await EndpointBrokerManagerFS.getAll()).filter(async (endpoint) => {
if (endpoint.type !== EndpointType.MANUAL) {
return false
}
return true
}) as ManualFS[]
return endpoints.map( (endpoint) => {
return endpoint.toIEndpoint() as Manual
})
}
public async activateEndpointByPath(path: string): Promise<boolean> {
return await EndpointBrokerManagerFS.activateEndpoint(path)
}
public async deactivateEndpointByPath(path: string): Promise<boolean> {
return await EndpointBrokerManagerFS.deactivateEndpoint(path)
}
}

View File

@ -0,0 +1,139 @@
import type { ISSLTerminationBroker, SSLTermination, SSLTerminationChanges } from "$lib/server/classes/endpoints/ssl-termination-endpoint"
import { EndpointType } from "$lib/server/enums/endpoints"
import type { NginxProtocol } from "$lib/server/enums/protocols"
import { isPortAvailable, validatePort } from "$lib/server/utils/ports-utils"
import { validatePath } from "$lib/shared/utils/path-utils"
import { EndpointBrokerManagerFS } from "./Endpoint-Manager"
import { SSLTerminationFS } from "./endpoints/ssltermination-fs"
export class SSLTerminationFSBroker implements ISSLTerminationBroker {
public async init(): Promise<void> {
// Do nothing?
}
public async createSSLTermination(
name: string,
sslPort: number,
clearPort: number,
servicePort: number,
serviceEndpoint: string,
protocol: NginxProtocol,
certificateURI: string,
privateKeyURI: string
): Promise<SSLTermination> {
if (
!isPortAvailable(sslPort) ||
!isPortAvailable(clearPort)
) {
// UGLY be specific
throw new Error("some ports were already in use")
}
validatePort(servicePort)
const newEndpoint = new SSLTerminationFS(
name,
null,
sslPort,
clearPort,
servicePort,
serviceEndpoint,
protocol,
certificateURI,
privateKeyURI
)
await EndpointBrokerManagerFS.createEndpoint(newEndpoint)
return (await this.getSSLTerminationFSByPath(newEndpoint.path))?.toIEndpoint() as SSLTermination
}
public async activateEndpointByPath(path: string): Promise<boolean> {
return await EndpointBrokerManagerFS.activateEndpoint(path)
}
public async deactivateEndpointByPath(path: string): Promise<boolean> {
return await EndpointBrokerManagerFS.deactivateEndpoint(path)
}
public async getSSLTerminationByPath(path: string): Promise<SSLTermination | null> {
const endpoint = await this.getSSLTerminationFSByPath(path)
return endpoint?.toIEndpoint() as SSLTermination
}
public async modifySSLTerminationByPath(path: string, changes: SSLTerminationChanges): Promise<SSLTermination> {
const oldEndpoint = await this.getSSLTerminationFSByPath(path)
if (!oldEndpoint) {
// UGLY: be specific
throw new Error("There was no old endpoint at this path")
}
const newEndpoint = new SSLTerminationFS(
changes.name ?? oldEndpoint.name,
oldEndpoint.stats,
changes.sslPort ?? oldEndpoint.sslPort,
changes.clearPort ?? oldEndpoint.clearPort,
changes.servicePort ?? oldEndpoint.servicePort,
changes.serviceEndpoint ?? oldEndpoint.serviceEndpoint,
changes.protocol ?? oldEndpoint.protocol,
changes.certificateURI ?? oldEndpoint.certificateURI,
changes.privateKeyURI ?? oldEndpoint.privateKeyURI
)
EndpointBrokerManagerFS.changeEndpoint(path, newEndpoint)
return newEndpoint.toIEndpoint() as SSLTermination
}
public async deleteSSLTerminationByPath(path: string): Promise<SSLTermination | null> {
const candidate = await EndpointBrokerManagerFS.deleteEndpoint(path)
if (!candidate) {
return null
}
return candidate as SSLTerminationFS
}
// Technically expensive to implement
public async getAllSSLTerminations(): Promise<SSLTermination[]> {
const endpoints = (await EndpointBrokerManagerFS.getAll()).filter((endpoint) => {
if (endpoint.type !== EndpointType.SSL_TERMINATION) {
return false
}
return true
}) as SSLTerminationFS[]
return endpoints.map((endpoint) => {
return endpoint.toIEndpoint() as SSLTermination
})
}
private async getSSLTerminationFSByPath(path: string): Promise<SSLTerminationFS | null> {
const endpoint = EndpointBrokerManagerFS.getEndpointByPath(path)
if (!endpoint) {
return null
}
if (endpoint.type !== EndpointType.SSL_TERMINATION) {
// UGLY: be specific
throw new Error("This is not an SSLEndpoint")
}
return endpoint as SSLTerminationFS
}
}

View File

@ -0,0 +1,42 @@
import type { IEndpoint } from "$lib/server/classes/endpoints/endpoints-interfaces";
import { EndpointType } from "$lib/server/enums/endpoints";
import type { FileStats } from "$lib/server/utils/filesystem-utils";
export interface IEndpointFS {
/**
* Specifies which Enpoint this is
*/
type: EndpointType
/** File name */
name: string
/**
* File stats,
* useful to understand
* whether the file has been actually
* parsed by nginx during last reload
*/
stats: FileStats | null
/** File path */
path: string
/** Hash of the file */
hash: string
/** converts the IEndpoint to */
toConf(): string
ports(): number[]
headerHash(): string
toIEndpoint(): IEndpoint
}
export type FSHeader = {
name: string,
stats: FileStats
path: string,
hash: string
}

View File

@ -0,0 +1,70 @@
import { EndpointType } from "$lib/server/enums/endpoints";
import type { Stats } from "fs";
import type { FSHeader, IEndpointFS } from "./endpoints";
import type { IEndpoint } from "$lib/server/classes/endpoints/endpoints-interfaces";
import { Manual } from "$lib/server/classes/endpoints/manual-endpoint";
// 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
}
toIEndpoint(): IEndpoint {
return new Manual(
this.name,
this.path,
this.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

@ -0,0 +1,294 @@
import { EndpointType } from "$lib/server/enums/endpoints"
import { httpVersion, protocolToString, proxyProtocol, secureProtocol, stringToProtocol, type NginxProtocol } from "$lib/server/enums/protocols"
import { validatePort } from "$lib/server/utils/ports-utils"
import type { Stats } from "fs"
import type { FSHeader, IEndpointFS } from "./endpoints"
import { createHeader, parseDefaultHeader, parseGenericHeader } from "../utils"
import { hashUtil } from "$lib/server/utils/filesystem-utils"
import type { IEndpoint } from "$lib/server/classes/endpoints/endpoints-interfaces"
import { SSLTermination, type ISSLTerminationBroker, type SSLTerminationChanges } from "$lib/server/classes/endpoints/ssl-termination-endpoint"
import { EndpointBrokerManagerFS } from "../Endpoint-Manager"
// TODO: add broker implementation
export class SSLTerminationFS implements IEndpointFS {
private static __type = EndpointType.SSL_TERMINATION
public get type() {
return SSLTerminationFS.__type
}
public name: string
public stats: Stats|null
public get path() {
return `${this.protocol}/name.conf`
}
public get hash() {
return hashUtil(
this.toConf()
)
}
public sslPort: number
public clearPort: number
public servicePort: number
public serviceEndpoint: string
public protocol: NginxProtocol
public certificateURI: string
public privateKeyURI: string
constructor(
name: string,
stats: Stats|null,
sslPort: number,
clearPort: number,
servicePort: number,
serviceEndpoint: string,
protocol: NginxProtocol,
certificateURI: string,
privateKeyURI: string
) {
this.name = name
this.stats = stats
this.sslPort = sslPort
this.clearPort = clearPort
this.servicePort = servicePort
this.serviceEndpoint = serviceEndpoint
this.protocol = protocol
this.certificateURI = certificateURI
this.privateKeyURI = privateKeyURI
}
public toIEndpoint(): IEndpoint {
return new SSLTermination(
this.name,
this.path,
this.sslPort,
this.clearPort,
this.servicePort,
this.serviceEndpoint,
this.protocol,
this.certificateURI,
this.privateKeyURI
)
}
headerHash(): string {
return hashUtil(
this.createHeader()
)
}
toConf(): string {
const HEADER = this.createHeader()
const SSL_SERVER = this.createSSLServer()
const CLEAR_SERVER = this.createClearServer()
const CONF = HEADER + SSL_SERVER + CLEAR_SERVER
return CONF
}
ports(): number[] {
return [
this.sslPort,
this.clearPort
]
}
public static parseConf(fsHeader: FSHeader, conf: string): SSLTerminationFS {
// TODO: parse header
const defHeader = parseDefaultHeader(conf)
const keyValue = parseGenericHeader(conf)
const name = keyValue.get("NAME")
if (!name) {
throw new Error("Could not parse")
}
const protocol = stringToProtocol(
keyValue.get("PROTOCOL") ?? ""
)
const sslPort = Number.parseInt(
keyValue.get(
"SSL_PORT"
) ?? ""
)
validatePort(sslPort)
const clearPort = Number.parseInt(
keyValue.get(
"CLEAR_PORT"
) ?? ""
)
validatePort(clearPort)
const servicePort = Number.parseInt(
keyValue.get(
"SERVICE_PORT"
) ?? ""
)
validatePort(servicePort)
const serviceEndpoint = keyValue.get("SERVICE_ENDPOINT")
if (!serviceEndpoint) {
throw new Error("Could not parse")
}
const certificateURI = keyValue.get("CERTIFICATE_PATH")
if (!certificateURI) {
throw new Error("Could not parse")
}
const privateKeyURI = keyValue.get("KEY_PATH")
if (!privateKeyURI) {
throw new Error("Could not parse")
}
return new SSLTerminationFS(
name,
fsHeader.stats,
sslPort,
clearPort,
servicePort,
serviceEndpoint,
protocol,
certificateURI,
privateKeyURI
)
}
private createHeader() {
return createHeader(
this,
[
{
key: "NAME",
value: this.name
},
{
key: "PROTOCOL",
value: protocolToString(this.protocol)
},
{
key: "SSL_PORT",
value: this.sslPort
},
{
key: "CLEAR_PORT",
value: this.clearPort
},
{
key: "SERVICE_PORT",
value: this.servicePort
},
{
key: "SERVICE_ENDPOINT",
value: this.serviceEndpoint
},
{
key: "CERTIFICATE_PATH",
value: this.certificateURI
},
{
key: "KEY_PATH",
value: this.privateKeyURI
}
]
)
}
// UGLY: refactor into a flexible method
private createSSLServer() {
const CLEAR_PROTOCOL = `${this.protocol}`
const HTTP_VERSION = httpVersion(this.protocol)
const PROXY_OPTION = proxyProtocol(this.protocol)
// UGLY: put to constants
let conf = [
"server {\n",
"\tmore_clear_headers Server;\n",
`\tlisten ${this.sslPort};`
]
if (HTTP_VERSION !== 1) {
conf.push(
`\thttp${HTTP_VERSION} on;`
)
}
// TODO: check if we should support less protocols
conf.push(
"\n",
"\tlocation / {",
`\t\t${PROXY_OPTION} ${CLEAR_PROTOCOL}://127.0.0.1:${this.clearPort};`,
"\t}",
"\n",
`ssl_certificate ${this.certificateURI};`,
`ssl_certificate_key ${this.privateKeyURI};`,
"\tssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;",
"\n",
"}"
)
return conf.join("\n")
}
private createClearServer() {
const SSL_PROTOCOL = secureProtocol(this.protocol)
const HTTP_VERSION = httpVersion(this.protocol)
const PROXY_OPTION = proxyProtocol(this.protocol)
// UGLY: put to constants
let conf = [
"server {\n",
"\tmore_clear_headers Server;\n",
`\tlisten ${this.clearPort};`
]
if (HTTP_VERSION !== 1) {
conf.push(
`\thttp${HTTP_VERSION} on;`
)
}
// TODO: check if we should support less protocols
conf.push(
"\n",
"\tlocation / {",
`\t\t${PROXY_OPTION} ${SSL_PROTOCOL}://${this.serviceEndpoint}:${this.servicePort};`,
"\t}",
"\n",
"}"
)
return conf.join("\n")
}
}

View File

@ -0,0 +1,132 @@
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 = "**********************************************************"
export const HEADER_UPPER = `/*${HEADER_BOUNDARY}`
export const HEADER_LOWER = `${HEADER_BOUNDARY}*/`
export const SPLITTING = ": "
export const DEFAULT_HEADER_LEN = 5
export const CUSTOM_HEADER_START = DEFAULT_HEADER_LEN + 1
export type HeaderKeyValueFS = {
key: string,
value: any
}
export type DefaultHeader = {
type: EndpointType,
name: string
}
export function parseConf(
fsHeader: FSHeader,
conf: string
): IEndpointFS {
const TYPE = getType(conf)
switch (TYPE) {
case EndpointType.SSL_TERMINATION:
return SSLTerminationFS.parseConf(fsHeader, conf)
default:
return ManualFS.parseConf(fsHeader, conf)
}
}
export function createHeader(endpoint: IEndpointFS, variables?: HeaderKeyValueFS[]): string {
if (!variables) {
variables = []
}
let header = [
HEADER_UPPER,
"SSL-SNIFFER AUTOMATICALLY GENERATED",
`TYPE: ${endpoint.type}`,
`NAME: ${endpoint.name}`,
HEADER_BOUNDARY
]
for (const variable of variables) {
header.push(
`${variable.key}${SPLITTING}${variable.value}`
)
}
header.push(HEADER_LOWER)
return header.join("\n")
}
function getType(conf: string): EndpointType {
/** Row (remember array order) where we find the type */
const TYPE_DATA_ROW = 2
/** What separator was used */
const TYPE_DATA_SEPARATOR = " "
/** Whether the value is on the left(0) of right(1) */
const TYPE_DATA_ARRAY_POSITION = 1
const DEFAULT_LABEL = EndpointType.MANUAL
const confLines = conf.split("\n")
if (confLines.length < 4 ) {
return matchEndpoint(DEFAULT_LABEL)
}
//
const label = confLines[TYPE_DATA_ROW]
.split(TYPE_DATA_SEPARATOR)[TYPE_DATA_ARRAY_POSITION]
return matchEndpoint(label)
}
export function parseDefaultHeader(conf: string) : DefaultHeader {
const confLines = conf.split("\n")
const HEADER = confLines.slice(0, DEFAULT_HEADER_LEN + 1)
const type = matchEndpoint(HEADER[2].split(" ")[1])
const name = HEADER[3].split(" ")[1]
return {
type: type,
name: name
}
}
export function parseGenericHeader(conf: string) : Map<string, string> {
const confLines = conf.split("\n")
const headerProbableLines = confLines.slice(CUSTOM_HEADER_START)
const keyValueMap = new Map<string, string>()
let index = 0
let parsed = false
while (!parsed) {
const line = headerProbableLines[index]
if (line === HEADER_LOWER) {
parsed = true
continue
}
const keyValue = line.split(SPLITTING)
keyValueMap.set(
keyValue[0],
keyValue[1]
)
index++
}
return keyValueMap
}

View File

@ -0,0 +1,54 @@
import { DB_PATH } from "$lib/server/utils/constants";
import { logger } from "$lib/server/utils/logger";
// TODO: remove bun dependencies
import { DatabaseSync } from 'node:sqlite';
export class DatabaseBrokerManager {
private static initialized = false
private static db: DatabaseSync
// UGLY: should be more flexible
public static init() {
logger.debug("Initializing Database", "SSLSnifferApp")
if (DatabaseBrokerManager.initialized) {
logger.debug("database initialized Twice?", "SSLSnifferApp")
throw new Error("SSLSniffer has already been initialized")
}
DatabaseBrokerManager.db = DatabaseBrokerManager.initDatabase()
DatabaseBrokerManager.initialized = true
}
public static get ready() {
return DatabaseBrokerManager.initialized
}
public static prepare(query: string) {
logger.debug(`Statement: ${query}`, "SQLite Query Preparation")
return DatabaseBrokerManager.db.prepare(query)
}
private static initDatabase() {
const db = new DatabaseSync(DB_PATH)
// Improve performance
db.exec("PRAGMA journal_mode = WAL;");
// Activate cascade operations
db.exec("PRAGMA foreign_keys = ON")
return db
}
private static closeDatabase() {
DatabaseBrokerManager.db.close()
}
}

View File

@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS sessions (
session_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE NOT NULL,
session_token TEXT UNIQUE NOT NULL,
FOREIGN KEY (user_id) references users(user_id) ON DELETE CASCADE
);
INSERT INTO sessions (user_id, session_token)
VALUES (@userID, @token);
SELECT session_id, user_id, session_token
FROM sessions
WHERE session_token = @token;
SELECT session_id, user_id, session_token
FROM sessions
WHERE user_id = @userID;
SELECT session_id, user_id, session_token
FROM sessions
WHERE session_id = @sessionID;

View File

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL ,
password_hash TEXT NOT NULL
);
INSERT INTO users (username, password_hash)
VALUES (@username, @password);
SELECT user_id, username, password_hash
FROM users
WHERE username = @username;
SELECT user_id, username, password_hash
FROM users
WHERE user_id = @user_id;
UPDATE users
SET password_hash = @newPassword
WHERE username = @username;

View File

@ -0,0 +1,207 @@
import { Session, type ISessionBroker } from "$lib/server/classes/sessions"
import { logger } from "$lib/server/utils/logger"
import { DatabaseBrokerManager } from "./Database"
class SessionDB {
public session_id: number
public user_id: number
public session_token: string
// TODO: support fingerprinting to
// TODO: increase security
// TODO: while logging in
constructor(
session_id: number,
user_id: number,
session_token: string
) {
this.session_id = session_id
this.user_id = user_id
this.session_token = session_token
}
}
export class SessionDBBroker implements ISessionBroker {
private static initialized = false
constructor() {
if (SessionDBBroker.initialized) {
// UGLY: make more specific
throw Error("SessionBroker has already been initialized")
}
logger.debug("Correctly initialized", "SessionDBBroker")
}
createTable(): void {
const stmt = DatabaseBrokerManager.prepare(
`
CREATE TABLE IF NOT EXISTS sessions (
session_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE NOT NULL,
session_token TEXT UNIQUE NOT NULL,
FOREIGN KEY (user_id) references users(user_id) ON DELETE CASCADE
);
`
)
stmt.run()
}
createSessionFromUserID(userID: number): Session {
// Check for existing Sessions
const sessionCheck = this.getSessionFromUserID(userID)
if (sessionCheck) {
// UGLY: more specific
throw new Error("There's already a session associated with the user")
}
// Create new Session
const token : string = crypto.randomUUID();
// Insert into DB
const stmt = DatabaseBrokerManager.prepare(
`
INSERT INTO sessions (user_id, session_token)
VALUES (@userID, @token);
`
)
stmt.run({
userID: userID,
token: token
})
// Check if Session has been successfully created
const session = this.getSessionFromUserID(userID)
logger.debug(`session: ${session}`, "DB Session Create")
if (!session) {
// UGLY: more specific
throw new Error("Something wrong happened during the creationg of the session")
}
return session
}
getSessionFromUserID(userID: number): Session | null {
const candidateSession = this.getSessionDBFromUserID(userID)
if (!candidateSession) {
return null
}
return new Session(
candidateSession.session_id,
candidateSession.user_id,
candidateSession.session_token
)
}
getSessionFromToken(token: string): Session | null {
const candidateSession = this.getSessionDBFromToken(token)
if (!candidateSession) {
return null
}
return new Session(
candidateSession.session_id,
candidateSession.user_id,
candidateSession.session_token
)
}
private getSessionDBFromToken(token: string): SessionDB | null {
logger.debug(`token: ${token}`, "DB Session from Token")
const stmt = DatabaseBrokerManager.prepare(
`
SELECT session_id, user_id, session_token
FROM sessions
WHERE session_token = @token;
`
)
const sessions = stmt.all({
token: token
})
return this.parseSessionDBUnique(sessions)
}
private getSessionDBFromUserID(userID: number): SessionDB | null {
const stmt = DatabaseBrokerManager.prepare(
`
SELECT session_id, user_id, session_token
FROM sessions
WHERE user_id = @userID;
`
)
const sessions = stmt.all({
userID: userID
})
return this.parseSessionDBUnique(sessions)
}
private getSessionDBFromSessionID(sessionID: number): SessionDB | null {
const stmt = DatabaseBrokerManager.prepare(
`
SELECT session_id, user_id, session_token
FROM sessions
WHERE session_id = @sessionID;
`
)
const sessions = stmt.all({
sessionID: sessionID
})
return this.parseSessionDBUnique(sessions)
}
private parseSessionDBUnique(sessions: any[]) {
if (sessions.length > 1) {
// UGLY: be specific
throw new Error("Duplicate session?")
}
if (sessions.length < 1) {
return null
}
const session: any = sessions[0]
return new SessionDB(
session.session_id,
session.user_id,
session.session_token
)
}
}

View File

@ -0,0 +1,230 @@
import type { Session, SessionApp } from "$lib/server/classes/sessions";
import { User, type IUserBroker } from "$lib/server/classes/users";
import { logger } from "$lib/server/utils/logger";
import { DatabaseBrokerManager } from "./Database";
import * as argon2 from "argon2";
class UserDB {
public user_id: number
public username: string
public password_hash: string
constructor(
user_id: number,
username: string,
password_hash: string
) {
this.user_id = user_id
this.username = username
this.password_hash = password_hash
}
}
export class UserDBBroker implements IUserBroker {
private static initialized = false
constructor() {
if (UserDBBroker.initialized) {
// UGLY: make more specific
throw Error("UserDB has been already initialized")
}
logger.debug("Correctly initialized", "UserDBBroker")
}
public createTable(): void {
const stmt = DatabaseBrokerManager.prepare(
`
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL ,
password_hash TEXT NOT NULL
);
`
)
stmt.run()
}
public async createUser(username: string, password: string): Promise<User> {
this.validateUniqueness(username)
const insertUser = DatabaseBrokerManager.prepare(
`
INSERT INTO users (username, password_hash)
VALUES (@username, @password);
`
)
const passwordHash = await argon2.hash(password)
try {
insertUser.run({
username: username,
password: passwordHash
})
} catch (error) {
// UGLY: make this more specific
console.error("Duplicate after check??")
// UGLY: create a logger
console.error(`Insert User ${Date.now()}:\n\t${error}\n\n`)
throw new Error("You can't have duplicates")
}
const user = await this.getUser(username, password)
if (!user) {
// UGLY: make this more specific
throw new Error("Something went wrong during the creation of the user")
}
return user
}
public async getUser(username: string, password: string): Promise<User | null> {
const userToVerify = this.getUserFromUsername(username)
if (!userToVerify) {
// UGLY: make this more specific
throw new Error("The specified user does not exist on the database")
}
let match = false
try {
match = await argon2.verify(userToVerify.password_hash, password)
} catch (error) {
// UGLY: make this more specific
throw new Error("Argon2 had an error")
}
if (!match) {
return null
}
return new User(
userToVerify.user_id,
userToVerify.username
)
}
public async updatePassword(username: string, password: string, newPassword: string): Promise<void> {
const userToUpdate = await this.getUser(username, password)
if (!userToUpdate) {
// UGLY: make this more specific
throw new Error("Something went wrong while fetching the user")
}
const passwordHash = await argon2.hash(newPassword)
const stmt = DatabaseBrokerManager.prepare(
`
UPDATE users
SET password_hash = @newPassword
WHERE username = @username;
`
)
stmt.run({
username: userToUpdate.username,
newPassword: passwordHash
})
}
public getUserFromSession(session: Session): User {
const userDB = this.getUserFromUserID(session.userID)
if (!userDB) {
// UGLY: be specific
throw new Error("Could not find user inside database")
}
return new User(
userDB.user_id,
userDB.username
)
}
private validateUniqueness(username: string) {
const user = this.getUserFromUsername(username)
if (!user) {
return
}
throw new Error("User is already on the system")
}
private getUserFromUsername(username: string): UserDB | null {
const stmt = DatabaseBrokerManager.prepare(
`
SELECT user_id, username, password_hash
FROM users
WHERE username = @username;
`
)
const user: any | null = stmt.get({
username: username,
})
if (!user) {
return null
}
return new UserDB(
user.user_id,
user.username,
user.password_hash
)
}
private getUserFromUserID(userID: number): UserDB | null {
const stmt = DatabaseBrokerManager.prepare(
`
SELECT user_id, username, password_hash
FROM users
WHERE user_id = @user_id;
`
)
const user: any | null = stmt.get({
user_id: userID,
})
if (!user) {
return null
}
return new UserDB(
user.user_id,
user.username,
user.password_hash
)
}
}

View File

@ -0,0 +1,79 @@
import type { Cookies } from "@sveltejs/kit";
import { SessionApp, type Session } from "./sessions";
import { UserApp, type User } from "./users";
import { JoseApp } from "$lib/server/utils/jtw-utils";
import { logger } from "$lib/server/utils/logger";
import { SESSION_COOKIE_NAME } from "$lib/shared/constants";
export class AppData {
public session: Session
public user: User
public constructor(
session: Session,
user: User
) {
this.session = session
this.user = user
}
public async toCookie() {
const signedSession = await JoseApp.signObject({token: this.session.sessionToken})
const encodedSession = btoa(signedSession)
return encodedSession
}
public static async extractAppDataFromCookies(cookies: Cookies) {
const encodedSessionToken = cookies.get(SESSION_COOKIE_NAME)
logger.debug(`Session Cookie: ${encodedSessionToken}`, "APP Session Building 1")
if (!encodedSessionToken) {
return null
}
const decodedSessionToken = atob(encodedSessionToken)
logger.debug(`Session Cookie: ${decodedSessionToken}`, "APP Session Building 2")
const candidateToken = (await JoseApp.verifyObject(decodedSessionToken))
if (!candidateToken) {
cookies.delete(SESSION_COOKIE_NAME, {
path: "/"
})
return null
}
const sessionToken : string = candidateToken.token
logger.debug(`Session Token: ${sessionToken}`, "APP Session Building 3")
const session = SessionApp.getSessionFromToken(sessionToken)
if (!session) {
return null
}
const user = UserApp.getUserFromSession(
session
)
return new AppData(
session,
user
)
}
public toString() {
return `User:\t${this.user}\nSession:\t${this.session}`
}
}

View File

@ -0,0 +1,64 @@
import { EndpointBrokerManagerFS } from "$lib/server/broker-utils/FileSystem/Endpoint-Manager"
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 async init() {
await EndpointBrokerManagerFS.init()
}
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 async getStatus(path: string) {
return await EndpointBrokerManagerFS.getStatus(path)
}
public static getEndpointByPath(path: string): IEndpoint | null {
const endpoint = EndpointBrokerManagerFS.getEndpointByPath(path)
if(!endpoint) {
return null
}
return endpoint.toIEndpoint()
}
public static async getAll(): Promise<IEndpoint[]> {
const endpoints = await EndpointBrokerManagerFS.getAll()
return endpoints.map( (endpoint) => {
return endpoint.toIEndpoint()
})
}
}

View File

@ -0,0 +1,9 @@
import type { EndpointType } from "$lib/server/enums/endpoints";
export interface IEndpoint {
type: EndpointType
name: string
path: string
}

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| null>
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

@ -0,0 +1,250 @@
import { EndpointType } from "$lib/server/enums/endpoints"
import type { NginxProtocol } from "$lib/server/enums/protocols"
import { validatePort } from "$lib/server/utils/ports-utils"
import type { IEndpoint } from "./endpoints-interfaces"
// TODO: inherit from a super class
export interface ISSLTerminationBroker {
/**
* Initialize the Broker and everything related to it
*/
init(): Promise<void>
// TODO: in the next version support
// TODO: creation of endpoints
// TODO: according to path
// NOTES: it's useless to generate ports backend
// NOTES: generate them frontend and validate backend
createSSLTermination(
name: string,
sslPort: number,
clearPort: number,
servicePort: number,
serviceEndpoint: string,
protocol: NginxProtocol,
certificateURI: string,
privateKeyURI: string
): Promise<SSLTermination>
activateEndpointByPath(
path: string
): Promise<boolean>
deactivateEndpointByPath(
path: string
): Promise<boolean>
// Getting endpoints may be null, react over them
getSSLTerminationByPath(
path: string
): Promise<SSLTermination|null>
// Throw if something goes wrong
modifySSLTerminationByPath(
path: string,
changes: SSLTerminationChanges
): Promise<SSLTermination>
deleteSSLTerminationByPath(
path: string
): Promise<SSLTermination|null>
getAllSSLTerminations(): Promise<SSLTermination[]>
}
/**
* This class represents an SSL Termination Endpoint.
*
* While it's possible to create it directly, it is
* discouraged in favor of the Factory methods as it does
* more checks than this class
*/
export class SSLTermination implements IEndpoint {
private static __type = EndpointType.SSL_TERMINATION
public get type() {
return SSLTermination.__type
}
public name: string
public path: string
public sslPort: number
public clearPort: number
public servicePort: number
public serviceEndpoint: string
public protocol: NginxProtocol
public certificateURI: string
public privateKeyURI: string
constructor(
name: string,
path: string,
sslPort: number,
clearPort: number,
servicePort: number,
serviceEndpoint: string,
protocol: NginxProtocol,
certificateURI: string,
privateKeyURI: string
) {
this.name = name
this.path = path
this.sslPort = sslPort
this.clearPort = clearPort
this.servicePort = servicePort
this.serviceEndpoint = serviceEndpoint
this.protocol = protocol
this.certificateURI = certificateURI
this.privateKeyURI = privateKeyURI
}
}
export type SSLTerminationChanges = {
name?: string,
path?: string,
sslPort?: number,
clearPort?: number,
servicePort?: number,
serviceEndpoint?: string,
protocol?: NginxProtocol,
certificateURI?: string,
privateKeyURI?: string
}
export class SSLTerminationEndpointApp {
private static initialized: boolean = false
private static broker: ISSLTerminationBroker
public static get ready() {
return SSLTerminationEndpointApp.initialized
}
public static init(broker: ISSLTerminationBroker) {
SSLTerminationEndpointApp.assureNotInitialized()
SSLTerminationEndpointApp.broker = broker
broker.init()
SSLTerminationEndpointApp.initialized = true
}
public static async createSSLTermination(
name: string,
sslPort: number,
clearPort: number,
servicePort: number,
serviceEndpoint: string,
protocol: NginxProtocol,
certificateURI: string,
privateKeyURI: string
): Promise<SSLTermination> {
SSLTerminationEndpointApp.assureInitialized()
return await this.broker.createSSLTermination(
name,
sslPort,
clearPort,
servicePort,
serviceEndpoint,
protocol,
certificateURI,
privateKeyURI
)
}
// Getting endpoints may be null, react over them
public static async getSSLTerminationByPath(
name: string
): Promise<SSLTermination|null> {
SSLTerminationEndpointApp.assureInitialized()
return await this.broker.getSSLTerminationByPath(
name
)
}
// Throw if something goes wrong
public static async modifySSLTerminationByPath(
name: string,
changes: SSLTerminationChanges
): Promise<SSLTermination> {
SSLTerminationEndpointApp.assureInitialized()
return await this.broker.modifySSLTerminationByPath(
name,
changes
)
}
public static async deleteSSLTerminationByPath(
name: string
): Promise<SSLTermination|null> {
SSLTerminationEndpointApp.assureInitialized()
return await this.broker.deleteSSLTerminationByPath(
name
)
}
public static async getAllSSLTerminations(): Promise<SSLTermination[]> {
SSLTerminationEndpointApp.assureInitialized()
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
)
}
private static assureNotInitialized() {
if (SSLTerminationEndpointApp.initialized) {
// UGLY: more specific
throw new Error("SSLTerminationEndpointApp has been already initialized")
}
}
private static assureInitialized() {
if (SSLTerminationEndpointApp.initialized) {
// UGLY: more specific
throw new Error("SSLTerminationEndpointApp has not been initialized yet")
}
}
}

View File

@ -0,0 +1,79 @@
export interface ISessionBroker {
// TODO: change in init()
createTable(): void
createSessionFromUserID(userID: number): Session
getSessionFromUserID(userID: number): Session | null
getSessionFromToken(token: string): Session | null
}
export class Session {
public sessionID: number
public userID: number
public sessionToken: string
constructor(
sessionID: number,
userID: number,
sessionToken: string
) {
this.sessionID = sessionID
this.userID = userID
this.sessionToken = sessionToken
}
public toString() {
return this.sessionToken
}
}
export class SessionApp {
private static broker: ISessionBroker
private static initialized: boolean = false
public static get ready() {
return SessionApp.initialized
}
public static init(broker: ISessionBroker) {
if (SessionApp.initialized) {
// UGLY: make this Error more specific
throw Error("SessionApp has already been initialized")
}
SessionApp.initialized = true
SessionApp.broker = broker
SessionApp.broker.createTable()
}
public static createSessionFromUserID(userID: number): Session {
SessionApp.assertInitialized()
return SessionApp.broker.createSessionFromUserID(userID)
}
public static getSessionFromUserID(userID: number): Session | null {
SessionApp.assertInitialized()
return SessionApp.broker.getSessionFromUserID(userID)
}
public static getSessionFromToken(token: string): Session | null {
SessionApp.assertInitialized()
return SessionApp.broker.getSessionFromToken(token)
}
private static assertInitialized() {
if (!SessionApp.initialized) {
// UGLY: make more specific
throw Error("SessionApp has't been Initialized yet!")
}
}
}

View File

@ -0,0 +1,89 @@
import type { Session } from "./sessions"
export interface IUserBroker {
// TODO: change in init()
createTable(): void
createUser(username: string, password: string): Promise<User>
getUser(username: string, password: string): Promise<User|null>
updatePassword(username: string, password: string, newPassword: string): Promise<void>
getUserFromSession(session: Session): User
}
export class User {
public userID: number
public username: string
constructor(
userID: number,
username: string
) {
this.userID = userID
this.username = username
}
public toString() {
return `userID:\t${this.userID}\nusername:\t${this.username}`
}
}
export class UserApp {
private static broker : IUserBroker
private static initialized: boolean = false
public static get ready() {
return UserApp.initialized
}
public static init(broker: IUserBroker) {
if (UserApp.initialized) {
// UGLY: make this Error more specific
throw Error("UserApp has already been initialized")
}
UserApp.initialized = true
UserApp.broker = broker
UserApp.broker.createTable()
}
public static getUserFromSession(session: Session): User {
UserApp.assertInitialized()
return UserApp.broker.getUserFromSession(session)
}
public static async createUser(username: string, password: string): Promise<User> {
UserApp.assertInitialized()
return await UserApp.broker.createUser(username, password)
}
public static async getUser(username: string, password: string): Promise<User|null> {
UserApp.assertInitialized()
return await UserApp.broker.getUser(username, password)
}
public static async updatePassword(username: string, password: string, newPassword: string) {
UserApp.assertInitialized()
return await UserApp.broker.updatePassword(username, password, newPassword)
}
private static assertInitialized() {
if (!UserApp.initialized) {
// UGLY: make more specific
throw Error("User app has't been Initialized yet!")
}
}
}

View File

@ -0,0 +1,23 @@
export enum EndpointType {
SSL_TERMINATION = "SSLTermination",
MANUAL = "Manual"
}
export enum EndpointStatus {
ACTIVE,
INACTIVE
}
export function matchEndpoint(label: string) {
switch(label) {
case "SSLTermination":
return EndpointType.SSL_TERMINATION
default:
return EndpointType.MANUAL
}
}

View File

View File

@ -0,0 +1,65 @@
export enum NginxProtocol {
HTTP = "http",
HTTP2 = "http",
QUIC = "http",
GRPC = "grpc"
}
// UGLY: move these fnction into utils
export function httpVersion(protocol: NginxProtocol): number {
switch (protocol) {
case NginxProtocol.HTTP2:
case NginxProtocol.GRPC:
return 2
case NginxProtocol.QUIC:
return 3
default:
return 1
}
}
export function protocolToString(procotol: NginxProtocol) {
switch (procotol) {
case NginxProtocol.HTTP:
return "HTTP"
case NginxProtocol.HTTP2:
return "HTTP2"
case NginxProtocol.QUIC:
return "QUIC"
case NginxProtocol.GRPC:
return "GRPC"
}
}
export function stringToProtocol(label: string) {
switch (label) {
case "HTTP":
return NginxProtocol.HTTP
case "HTTP2":
return NginxProtocol.HTTP2
case "QUIC":
return NginxProtocol.QUIC
case "GRPC":
return NginxProtocol.GRPC
default:
// UGLY: be specific
throw new Error("Protocol not found")
}
}
export function secureProtocol(protocol: NginxProtocol) {
return `${protocol}s`
}
export function proxyProtocol(protocol: NginxProtocol) {
switch (protocol) {
case NginxProtocol.GRPC:
return "grpc_pass"
default:
return "proxy_pass"
}
}

View File

@ -0,0 +1,104 @@
import { AppData } from "$lib/server/classes/appdata";
import { logger } from "$lib/server/utils/logger";
import { APP_HOME, SESSION_COOKIE_NAME } from "$lib/shared/constants";
import { error, redirect, type Handle } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
const sessionConstructorHandle = (async ({ event, resolve }) => {
const data = await AppData.extractAppDataFromCookies(event.cookies)
// Prevents stray cookies from remaining
// in session (e.g. User has been deleted,
// but cookie is still in session)
if (!data) {
event.cookies.delete(
SESSION_COOKIE_NAME,
{
path: "/"
}
)
}
logger.debug(`User: ${data?.user.username}\nToken ${data?.session.sessionToken}`, "Session Handle")
event.locals.session = data
return await resolve(event)
}) satisfies Handle
const apiHandle = (async ({ event, resolve }) => {
logger.debug(event.url.pathname, "API Handle")
logger.debug(`Session Data: ${event.locals.session}`, "API Handle")
if (!event.url.pathname.startsWith("/api/program")) {
// next handle
return await resolve(event)
}
// It's a backend, should not redirect
if (!event.locals.session) {
// Satisfies HTTP Codes:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401
return error(401, "Not Authorized")
}
return await resolve(event)
}) satisfies Handle
const appHandle = (async ({ event, resolve }) => {
logger.debug(event.url.pathname, "APP Handle")
logger.debug(`Session Data: ${event.locals.session}`, "APP Handle")
if (!event.url.pathname.startsWith("/app/program")) {
// next handle
return await resolve(event)
}
// It's for frontend, should redirect
if (!event.locals.session) {
return redirect(302, "/app/login")
}
return await resolve(event)
}) satisfies Handle
const appNonAuthHandle = (async ({ event, resolve }) => {
logger.debug(`Session Data: ${event.locals.session}`, "APP Non Auth")
if (
!event.url.pathname.startsWith("/app/login") &&
!event.url.pathname.startsWith("/app/register")
) {
// next handle
return await resolve(event)
}
// It's for frontend, should redirect
if (event.locals.session) {
return redirect(302, APP_HOME)
}
return await resolve(event)
}) satisfies Handle
export const handles = sequence(
sessionConstructorHandle,
apiHandle,
appHandle,
appNonAuthHandle
)

2
src/lib/server/index.ts Normal file
View File

@ -0,0 +1,2 @@
// place files you want to import through the `$lib` alias in this folder.
export {PKG} from "./utils/constants";

View File

@ -0,0 +1,13 @@
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
// NGINX
export const NGINX_BASE = "/etc/nginx"
export const NGINX_INACTIVE = `${NGINX_BASE}/inactive`
export const NGINX_ACTIVE = `${NGINX_BASE}/active`
export const NGINX_TRACKED = NGINX_INACTIVE

View File

@ -0,0 +1,185 @@
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 })
return fileContent
}
public async write(text: string, append?: boolean) {
if (!append) {
append = true
}
const flag: OpenMode = append ? "a+" : "w"
await Node.writeFile(this.file, text, { flag: flag })
}
public async lastChange() {
const stats = await Node.stat(this.path)
return stats.mtime
}
public async getStats() {
const stats = await Node.stat(this.path)
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
}
public async delete() {
await Node.unlink(this.path)
}
}
/**
* Checks if the file exists
*
* @param path `string` path for the file
* @returns if the file is readable
*/
export async function doesFileExist(path: string): Promise<boolean> {
try {
await Node.access(path)
} catch {
return false
}
return true
}
export async function isDir(path: string): Promise<boolean> {
const stats = await Node.stat(path)
return stats.isDirectory()
}
export async function loadFile(path: string, create?: boolean): Promise<FileHandle> {
const DEFAULT_MODE = 0o600
// Safe to use
// worst case: create = false -> create = false
if (!create) {
create = false
}
let fd: Node.FileHandle | null = null
try {
fd = await Node.open(path, "r+", DEFAULT_MODE)
} catch {
}
// We have a FD, return it
if (fd) {
await fd.close()
return new FileHandle(path)
}
if (!create) {
// UGLY: make more specific
throw new Error("The required file does not exist")
}
// 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
}
export async function listFiles(path: string, recursive?: boolean, relative?: boolean,): Promise<string[]> {
if (!recursive) {
recursive = false
}
if (!relative) {
relative = false
}
if (!await isDir(path)) {
// UGLY: be specific
throw new Error("This is not a directory")
}
const relativePaths = await Node.readdir(path, {recursive: recursive})
if (relative) {
return relativePaths
}
return relativePaths.map( (_path) => {
return `${path}/${_path}`
})
}

View File

View File

@ -0,0 +1,123 @@
import * as jose from "jose";
import { doesFileExist, loadFile } from "./filesystem-utils";
import { SERVER_PRIVATE_KEY_PATH, SERVER_PUBLIC_KEY_PATH } from "./constants";
import { openSSLInit } from "./openssl-utils";
import { logger } from "./logger";
export class JoseApp {
private static initialized = false
private static privateKey: CryptoKey
private static publicKey: CryptoKey
public static get ready() {
return JoseApp.initialized
}
public static async init() {
JoseApp.assureNotInitialized()
if (
!await doesFileExist(SERVER_PRIVATE_KEY_PATH) ||
!await doesFileExist(SERVER_PUBLIC_KEY_PATH)
) {
await openSSLInit()
}
JoseApp.privateKey = await JoseApp.loadPrivateKey()
JoseApp.publicKey = await JoseApp.loadPublicKey()
JoseApp.initialized = true
}
private static async loadPrivateKey() {
JoseApp.assureNotInitialized()
const privateKeyFile = await loadFile(SERVER_PRIVATE_KEY_PATH)
return await jose.importPKCS8(
await privateKeyFile.text(),
"ES512"
)
}
private static async loadPublicKey() {
JoseApp.assureNotInitialized()
const publicKeyFile = await loadFile(SERVER_PUBLIC_KEY_PATH)
return await jose.importSPKI(
await publicKeyFile.text(),
"ES512"
)
}
public static async signObject(object: any) {
JoseApp.assureInitialized()
const payload = new TextEncoder().encode(
JSON.stringify(object)
)
return await new jose.CompactSign(
payload
).setProtectedHeader({
alg: "ES512"
}).sign(JoseApp.privateKey)
}
public static async verifyObject(jwt: string) {
JoseApp.assureInitialized()
let _payload: Uint8Array
try {
const { payload, protectedHeader } = await jose.compactVerify(
jwt,
JoseApp.publicKey
)
_payload = payload
} catch (err) {
logger.debug(`Error: ${err}`, "JOSE Verify")
return null
}
logger.debug(`Payload: ${new TextDecoder().decode(_payload)}`, "JOSE Verify")
return JSON.parse(
new TextDecoder().decode(_payload)
)
}
private static assureInitialized() {
if (!JoseApp.initialized) {
// UGLY: Be specific
throw new Error("JoseSingleton hasn't been initialized")
}
}
private static assureNotInitialized() {
if (JoseApp.initialized) {
// UGLY: Be specific
throw new Error("JoseSingleton has already been initialized")
}
}
}

View File

@ -0,0 +1,48 @@
import { DEBUG } from "./constants"
import { LOGGER_DELIMITER } from "../../shared/constants"
class Logger {
private static initialized = false
constructor(
) {
if (Logger.initialized) {
// UGLY: be specific
throw new Error("Logger has already been initialized")
}
}
public debug(message: any, title?: string) {
if (!DEBUG) {
return
}
if (!title) {
title = "INFO"
}
const currentTime: string = new Date().toLocaleString()
const debugMessage =
`
${LOGGER_DELIMITER}
${title} DEBUG ${currentTime}
${message}
${LOGGER_DELIMITER}
`
console.info(
debugMessage
)
}
}
export const logger = new Logger()

View File

@ -0,0 +1,45 @@
// TODO: remove bun dependencies
import { logger } from "./logger"
import { shell } from "./shell-commands"
export async function reloadNginx() {
if (!await validateSchema()) {
// UGLY: make this a specific error
throw new Error("Something went wrong while validating")
}
// Start nginx, should be side-effect free
startNginx()
const output = await shell(`rc-service nginx reload`)
logger.debug(`rc-service reload output:\n${output.stdout}`, "NGINX - RELOAD")
}
export async function startNginx() {
if (!await validateSchema()) {
// UGLY: make this a specific error
throw new Error("Something went wrong while validating")
}
const output = await shell(`rc-service nginx start`)
}
export async function validateSchema() {
const output = await shell(`nginx -t 2>&1`)
logger.debug(`nginx -t output:\n${output.stdout}`, "NGINX - VALIDATE")
const successRegex = new RegExp("test is successful", "gm")
const result = successRegex.test(output.stdout)
return result
}

View File

@ -0,0 +1,51 @@
import { doesFileExist, loadFile, type FileHandle } from "./filesystem-utils";
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() {
await openSSLCreatePrivateKey()
await openSSLCreatePublicKey()
}
export async function openSSLCreatePrivateKey() {
// UGLY: may be refactored to output only the private key
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)
const [output, file] : [shellOutput, FileHandle]= await Promise.all([
outputPromise,
filePromise
])
await file.write(output.stdout)
}
export async function openSSLCreatePublicKey() {
// UGLY: may be refactored to output only the private key
if (! await doesFileExist(SERVER_PRIVATE_KEY_PATH)) {
// UGLY: make more specific
throw new Error("You must generate the private key before attempting to generate the public one")
}
const outputPromise = shell(`openssl ec -in ${SERVER_PRIVATE_KEY_PATH} -pubout `)
const filePromise = loadFile(SERVER_PUBLIC_KEY_PATH, true)
const [output, file] = await Promise.all([
outputPromise,
filePromise
])
await file.write(output.stdout)
}

View File

@ -0,0 +1,71 @@
import { shell } from "./shell-commands"
/**
* This methods runs `netstat -ltun`
* to take all ports occupied by
* other services on the host machine
*
* Since this is run in docker with
* network mode `host`, it takes all
* ports
*
* @returns occupied ports
*/
export async function portScan(): Promise<Set<number>> {
const portRegex = new RegExp("(?:\:)(?<port_number>[0-9]+)", "gm")
const netstatOutput = await shell(`netstat -ltun`)
const ports = netstatOutput.stdout.matchAll(portRegex)
const portArray : Set<number>= new Set()
for (const match of ports) {
portArray.add(
Number(match[1])
)
}
return portArray
}
/**
* This method checks if the port is actually a port,
* throws otherwise
* @param port_number port to validate
*/
export function validatePort(port_number: number): void {
// Validate against Float
if (port_number % 1 !== 0) {
throw new Error("The specified port is not an Integer")
}
// Validate for range
if (port_number < 1 || port_number > 65535 ) {
throw new Error("The specified port is not in the range 1...65535")
}
}
/**
* This method checks for the availability of a port
*
* Throws if the port is not a valid port
*
* @param port_number port to check
* @returns `true` if port is available, `false` otherwise
*/
export async function isPortAvailable(port_number: number): Promise<boolean> {
validatePort(port_number)
const occupied_ports = await portScan()
// Validate for availability
if (occupied_ports.has(port_number)) {
return false
}
return true
}

View File

@ -0,0 +1,9 @@
import { exec , spawn } from "node:child_process"
import { promisify } from "node:util"
export const shell = promisify(exec)
export type shellOutput = {
stdout: string,
stderr: string
}

View File

@ -0,0 +1,15 @@
// API ROUTES
export const API_BASE = "/api"
export const PROTECTED_API_BASE = `${API_BASE}/program`
// APP ROUTES
export const APP_BASE = "/app"
export const PROTECTED_APP_BASE = `${APP_BASE}/program`
export const APP_HOME = `${PROTECTED_APP_BASE}/home`
// Cookies
export const SESSION_COOKIE_NAME = "session"
// Logger
export const LOGGER_DELIMITER = "####################################################################"

View File

View File

@ -0,0 +1,8 @@
export function validatePath(path: string) {
const regex = new RegExp(".*\.\..*")
if (regex.test(path)) {
// UGLY: be specific
throw new Error("Thi spath is invalid")
}
}

0
src/private/.gitkeep Normal file
View File

17
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,17 @@
<script lang="ts">
</script>
<h1>SSL-Sniffer</h1>
<p>A Sniffer for all your needs</p>
<nav data-sveltekit-reload>
<a href="/app/login">Login</a>
or
<a href="/app/register">Register</a>
</nav>
<style>
</style>

View File

@ -0,0 +1,105 @@
import { error, json, text, type Cookies } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { User, UserApp } from '$lib/server/classes/users';
import { SessionApp } from '$lib/server/classes/sessions';
import { AppData } from '$lib/server/classes/appdata';
import { logger } from '$lib/server/utils/logger';
/***********************************************************
*
* Author: Christian Risi 26/06/2025
*
*
*
*
***********************************************************/
export const POST: RequestHandler = async ({ request, locals, cookies }) => {
const req: Request = request
const local: App.Locals = locals
const cookie: Cookies = cookies
logger.debug(`locals: ${local.session}`, "API Login")
const session = local.session
if (session) {
// The user is providing valid credentials
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403
return error(403, "Forbidden")
}
let userJson: { username: string, password: string }
let tmpJSON: any
try {
tmpJSON = await req.json()
} catch {
return error(400, "Bad Request")
}
if (!tmpJSON.username || !tmpJSON.password) {
return error(400, "Bad Request")
}
userJson = tmpJSON
let user: User | null
// If this fails, should be a 500
try {
user = await UserApp.getUser(
userJson.username,
userJson.password
)
} catch {
return error(400, "The provided credentials are non correct")
}
if (!user) {
return error(400, "The provided credentials are not correct")
}
const oldSession = SessionApp.getSessionFromUserID(
user.userID
)
// If we have no session, then probably a 500?
if (!oldSession) {
return error(500, "Internal Server Error")
}
const sessionCookie = await new AppData(
oldSession,
user
).toCookie()
cookie.set(
"session",
sessionCookie,
{
path: "/"
}
)
return text("OK")
}
export const fallback: RequestHandler = async ({ }) => {
// TODO: return method not allowed
const res = new Response(
null,
{
status: 405,
statusText: "Method Not Allowed",
headers: {
Allow: "POST"
}
}
)
return res
};

View File

@ -0,0 +1,51 @@
import { error, json, text, type Cookies } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { User, UserApp } from '$lib/server/classes/users';
import { SessionApp } from '$lib/server/classes/sessions';
import { AppData } from '$lib/server/classes/appdata';
import { logger } from '$lib/server/utils/logger';
import { SESSION_COOKIE_NAME } from '$lib/server/utils/constants';
/***********************************************************
*
* Author: Christian Risi 26/06/2025
*
*
*
*
***********************************************************/
export const GET: RequestHandler = async ({ request, locals, cookies }) => {
const req: Request = request
const local: App.Locals = locals
const cookie: Cookies = cookies
logger.debug(`locals: ${local.session}`, "API Logout")
cookie.delete(
SESSION_COOKIE_NAME,
{
path: "/"
}
)
return text("OK")
}
export const fallback: RequestHandler = async ({ }) => {
// TODO: return method not allowed
const res = new Response(
null,
{
status: 405,
statusText: "Method Not Allowed",
headers: {
Allow: "GET"
}
}
)
return res
};

View File

@ -0,0 +1,21 @@
import { EndpointManagerApp } from "$lib/server/classes/endpoints/endpoint-manager";
import { validatePath } from "$lib/shared/utils/path-utils";
import { type RequestHandler, error } from "@sveltejs/kit";
export const PATCH: RequestHandler = async ({ request }) => {
let path = await request.json();
try {
validatePath(path);
let res = await EndpointManagerApp.activateEndpoint(path);
return new Response(null, {
status: res ? 200 : 304
});
}
catch (e) {
return error(400, "Bad Request");
}
};

View File

@ -0,0 +1,9 @@
import { EndpointManagerApp } from "$lib/server/classes/endpoints/endpoint-manager";
import { type RequestHandler, json } from "@sveltejs/kit";
export const GET: RequestHandler = async ({ }) => {
let endpoints = await EndpointManagerApp.getAll();
return json(endpoints);
}

View File

@ -0,0 +1,21 @@
import { EndpointManagerApp } from "$lib/server/classes/endpoints/endpoint-manager";
import { validatePath } from "$lib/shared/utils/path-utils";
import { type RequestHandler, error } from "@sveltejs/kit";
export const PATCH: RequestHandler = async ({ request }) => {
let path = await request.json();
try {
validatePath(path);
let res = await EndpointManagerApp.deactivateEndpoint(path);
return new Response(null, {
status: res ? 200 : 304
});
}
catch (e) {
return error(400, "Bad Request");
}
};

View File

@ -0,0 +1,48 @@
import { error, json, text } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
/***********************************************************
*
* Author: Christian Risi 26/06/2025
*
* There is no POST handling here as it's not
* idempotent, so semantically it's better a
* PUT instead of a POST
*
***********************************************************/
export const GET: RequestHandler = async ({ request }) => {
return text("If you read this you are authenticated")
}
export const PUT: RequestHandler = async ({ request }) => {
return json(1)
}
export const PATCH: RequestHandler = async ({ request }) => {
return json(1)
}
export const DELETE: RequestHandler = async ({ request, }) => {
// TODO: make it delete the resource
return json(1)
}
export const fallback: RequestHandler = async ({ request }) => {
// TODO: return method not allowed
const res = new Response(
null,
{
status: 405,
statusText: "Method Not Allowed",
headers: {
Allow: "GET, PUT, PATCH, DELETE"
}
}
)
return res
};

View File

@ -0,0 +1,9 @@
import { EndpointManagerApp } from "$lib/server/classes/endpoints/endpoint-manager";
import { type RequestHandler, error, json } from "@sveltejs/kit";
export const GET: RequestHandler = async ({}) => {
let status = await EndpointManagerApp.getStatus();
return json({ status });
};

View File

@ -0,0 +1,114 @@
import { error, json, redirect, text, type Cookies } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { UserApp, User } from '$lib/server/classes/users';
import { SessionApp, Session } from '$lib/server/classes/sessions';
import { AppData } from '$lib/server/classes/appdata';
import { logger } from '$lib/server/utils/logger';
import { DEBUG } from '$lib/server/utils/constants';
/***********************************************************
*
* Author: Christian Risi 26/06/2025
*
*
*
*
***********************************************************/
export const POST: RequestHandler = async ({ request, locals, cookies }) => {
const req: Request = request
const local: App.Locals = locals
const cookie: Cookies = cookies
const session = local.session
if (DEBUG) {
return redirect(307, "api/register")
}
if (!session) {
// The user is not providing credentials
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401
return error(401, "Unauthorized")
}
let userJson: { username: string, password: string }
let tmpJSON: any
try {
tmpJSON = await req.json()
} catch {
return error(400, "Bad Request")
}
if (!tmpJSON.username || !tmpJSON.password) {
return error(400, "Bad Request")
}
userJson = tmpJSON
// If this fails, should be a 400?
let user: User
try {
user = await UserApp.createUser(
userJson.username,
userJson.password
)
} catch {
return error(400, "The user already exists")
}
let newSession
try {
newSession = SessionApp.createSessionFromUserID(
user.userID
)
} catch(err){
logger.debug(`error: ${err}`, "API Register")
return error(500, "Internal Server Error")
}
const sessionCookie = await new AppData(
newSession,
user
).toCookie()
cookie.set(
"session",
sessionCookie,
{
path: "/"
}
)
const res = new Response(
null,
{
status: 201,
statusText: "Created",
}
)
return res
}
export const fallback: RequestHandler = async ({ request }) => {
// TODO: return method not allowed
const res = new Response(
null,
{
status: 405,
statusText: "Method Not Allowed",
headers: {
Allow: "POST"
}
}
)
return res
};

View File

@ -0,0 +1,69 @@
import { error, type json, type RequestHandler } from "@sveltejs/kit";
// UGLY: this should be more flexible
class ReloadNginxReq {
public auth: string
public nginx: string
constructor(
json: any
) {
if (!json) {
throw new Error("This is an invalid JSON")
}
if (!json.nginx || !json.auth) {
throw new Error("Can't parse this JSON")
}
this.auth = json.auth
this.nginx = json.nginx
}
}
export const POST: RequestHandler = async ({ request }) => {
let parsedReq : ReloadNginxReq
try {
parsedReq = new ReloadNginxReq(
await request.json()
)
} catch (error) {
return new Response(null, {
status: 400,
statusText: "Bad Request"
})
}
// TODO: Reload Data about Nginx
// TODO: Notify frontends
return new Response(null, {
status: 200,
statusText: "OK"
})
}
export const fallback: RequestHandler = async () => {
// TODO: return method not allowed
const res = new Response(
null,
{
status: 405,
statusText: "Method Not Allowed",
headers: {
Allow: "POST"
}
}
)
return res
};

View File

@ -0,0 +1,116 @@
import { error, json, redirect, text, type Cookies } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { UserApp, User } from '$lib/server/classes/users';
import { SessionApp, Session } from '$lib/server/classes/sessions';
import { AppData } from '$lib/server/classes/appdata';
import { logger } from '$lib/server/utils/logger';
import { DEBUG } from '$lib/server/utils/constants';
/***********************************************************
*
* Author: Christian Risi 26/06/2025
*
*
*
*
***********************************************************/
export const POST: RequestHandler = async ({ request, locals, cookies }) => {
const req: Request = request
const local: App.Locals = locals
const cookie: Cookies = cookies
const session = local.session
if (!DEBUG) {
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
return error(403, "Forbidden")
}
let userJson: { username: string, password: string }
let tmpJSON: any
try {
tmpJSON = await req.json()
} catch {
return error(400, "Bad Request")
}
if (!tmpJSON.username || !tmpJSON.password) {
return error(400, "Bad Request")
}
userJson = tmpJSON
// If this fails, should be a 400?
let user: User
try {
user = await UserApp.createUser(
userJson.username,
userJson.password
)
} catch {
return error(400, "The user already exists")
}
let newSession
try {
newSession = SessionApp.createSessionFromUserID(
user.userID
)
} catch(err){
logger.debug(`error: ${err}`, "API Register")
return error(500, "Internal Server Error")
}
const sessionCookie = await new AppData(
newSession,
user
).toCookie()
cookie.set(
"session",
sessionCookie,
{
path: "/"
}
)
const res = new Response(
null,
{
status: 201,
statusText: "Created",
}
)
return res
}
export const fallback: RequestHandler = async ({ request }) => {
// TODO: return method not allowed
const res = new Response(
null,
{
status: 405,
statusText: "Method Not Allowed",
headers: {
Allow: "POST"
}
}
)
return res
};

View File

@ -0,0 +1,9 @@
import { PKG } from '$lib/server';
import { json } from '@sveltejs/kit';
export function GET() {
const version = `SSL-Sniffer version ${PKG.version}`
return json(version)
}

View File

@ -0,0 +1,58 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { APP_HOME } from "$lib/shared/constants";
import { redirect } from "@sveltejs/kit";
let error = $state("")
async function loginButton(event: SubmitEvent) {
event.preventDefault()
const form = event.target as HTMLFormElement
const username : string | null = form.username.value
const password : string | null = form.password.value
// UGLY: standardize
const jsonPayload = {
username: username,
password: password
}
const res = await fetch(
"/api/login",
{
method: "POST",
body: JSON.stringify(jsonPayload)
}
)
if (res.status !== 200) {
error = `${res.status}: ${(await res.json()).message}`
return
}
await goto(APP_HOME)
}
</script>
<h1>Login</h1>
<form action="" onsubmit={loginButton} >
<input name="username" type="text" required minlength="4" autocomplete="username" />
<input name="password" type="password" required minlength="8" autocomplete="current-password" />
<input type="submit">
</form>
{#if (error.length !== 0)}
<span>{error}</span>
{/if}
<a href="/app/register">Register</a>

View File

@ -0,0 +1,3 @@
<h1>Home</h1>
Welcome to SSL-Sniffer

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { APP_HOME } from "$lib/shared/constants";
import { redirect } from "@sveltejs/kit";
let error = $state("")
async function registerButton(event: SubmitEvent) {
event.preventDefault()
const form = event.target as HTMLFormElement
const username : string | null = form.username.value
const password : string | null = form.password.value
// UGLY: standardize
const jsonPayload = {
username: username,
password: password
}
const res = await fetch(
"/api/register",
{
method: "POST",
body: JSON.stringify(jsonPayload)
}
)
if (res.status != 201) {
error = `${res.status}: ${(await res.json()).message}`
return
}
goto(APP_HOME)
}
</script>
<h1>Register</h1>
<form action="" onsubmit={registerButton} >
<input name="username" type="text" required minlength="4" autocomplete="username" />
<input name="password" type="password" required minlength="8" autocomplete="new-password" />
<input name="confirm-password" type="password" required minlength="8" autocomplete="new-password" />
<input type="submit">
</form>
{#if (error.length !== 0)}
<span>{error}</span>
{/if}
<a href="/app/login">Login</a>

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

19
svelte.config.js Normal file
View File

@ -0,0 +1,19 @@
import adapter from "@sveltejs/adapter-auto"
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(),
}
};
export default config;

View File

@ -0,0 +1,8 @@
import { describe, it, expect } from 'vitest';
// TODO: make tests for Database
describe('create user database', () => {
it('creates ', () => {
expect(1 + 2).toBe(3);
});
});

25
tests/unit/demo.spec.ts Normal file
View File

@ -0,0 +1,25 @@
import { validateSchema } from '$lib/server/utils/nginx-utils';
import { portScan } from '$lib/server/utils/ports-utils';
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
describe('port gathering test', () => {
it('gathers ports from terminal', async () => {
const ports = await portScan()
console.log(ports)
expect(ports.size).not.toBe(0)
})
})
describe('nginx validation', () => {
it('validates nginx configurations', async () => {
const validation = await validateSchema()
expect(validation).toBe(true)
})
})

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")
});

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

48
vite.config.ts Normal file
View File

@ -0,0 +1,48 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
const file = fileURLToPath(new URL('package.json', import.meta.url));
const json = readFileSync(file, 'utf8');
const pkg = JSON.parse(json);
export default defineConfig({
plugins: [sveltekit()],
define: {
__PKG__: pkg
},
test: {
projects: [
{
extends: './vite.config.ts',
test: {
name: 'client',
environment: 'browser',
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }]
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: ['src/lib/server/**'],
setupFiles: ['./vitest-setup-client.ts']
}
},
{
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: [
'src/**/*.{test,spec}.{js,ts}',
'tests/**/*.{test,spec}.{js,ts}'
],
exclude: [
'src/**/*.svelte.{test,spec}.{js,ts}'
]
}
}
],
}
});

2
vitest-setup-client.ts Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="@vitest/browser/matchers" />
/// <reference types="@vitest/browser/providers/playwright" />