Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37d977e444 | ||
|
|
15f38a3f62 | ||
|
|
7f5d5ba1b1 | ||
|
|
67c8e9c31d | ||
|
|
3eda2cf0a1 | ||
|
|
f23cb0e1d2 | ||
| 19457d97ae | |||
| 6d3036c285 | |||
| 0f49a24dec | |||
| d7282c0c89 | |||
| 0fbbfec737 | |||
| 3de4354458 | |||
| d7a87f54bb | |||
| b62d101a88 | |||
| 5f89985939 | |||
| f55cc48656 | |||
| 177382d9c3 | |||
| 6c4dd63ee9 | |||
| b5ecbbca52 | |||
|
|
a0639f6094 | ||
|
|
4b63a236a3 | ||
|
|
64453aa176 | ||
|
|
3cac439056 | ||
|
|
678fe8c300 | ||
|
|
14bfab994d | ||
|
|
125409fda3 | ||
|
|
363c25045c | ||
|
|
91adb5ec98 | ||
|
|
fcab0782d5 | ||
|
|
9419ce3533 | ||
|
|
ea578433f3 | ||
|
|
4f6f48a992 | ||
|
|
932b770c8f | ||
|
|
002fd58585 | ||
|
|
f13a44ba5a | ||
|
|
4cf4a01e25 | ||
|
|
db769f4f96 | ||
|
|
6532a1988d | ||
|
|
a38431f6ca | ||
|
|
df97ce321e | ||
|
|
560cf8fdb1 | ||
|
|
85da2dbdc6 | ||
|
|
9a3be1cfe0 | ||
|
|
3618af361b | ||
|
|
925610161e | ||
|
|
22eacc6bca | ||
|
|
ddac609b97 |
@ -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
31
.gitignore
vendored
@ -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
|
||||
|
||||
|
||||
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
DOCKERFILE
10
DOCKERFILE
@ -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
105
README.md
@ -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
|
||||
```
|
||||
|
||||

|
||||
## 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.
|
||||
|
||||
@ -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
6
e2e/demo.test.ts
Normal 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
39
eslint.config.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
73
helper-scripts/services/custom-nginx.sh
Normal file
73
helper-scripts/services/custom-nginx.sh
Normal 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"
|
||||
}
|
||||
@ -12,9 +12,6 @@ server {
|
||||
# endpoint port
|
||||
listen PORT ssl;
|
||||
|
||||
# Uncomment if http2
|
||||
# http2 on;
|
||||
|
||||
# Here put the unencrypted
|
||||
# endpoint port
|
||||
location / {
|
||||
50
nginx/inactive/manual/http2/conf.example
Normal file
50
nginx/inactive/manual/http2/conf.example
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
0
nginx/inactive/manual/stream/.gitkeep
Normal file
0
nginx/inactive/manual/stream/.gitkeep
Normal 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
3269
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal 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
9
playwright.config.ts
Normal 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
20
src/app.d.ts
vendored
Normal 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
12
src/app.html
Normal 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
0
src/db/.gitkeep
Normal file
65
src/hooks.server.ts
Normal file
65
src/hooks.server.ts
Normal 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
|
||||
465
src/lib/server/broker-utils/FileSystem/Endpoint-Manager.ts
Normal file
465
src/lib/server/broker-utils/FileSystem/Endpoint-Manager.ts
Normal 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
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
48
src/lib/server/broker-utils/FileSystem/Manual.ts
Normal file
48
src/lib/server/broker-utils/FileSystem/Manual.ts
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
139
src/lib/server/broker-utils/FileSystem/SSLTerminations.ts
Normal file
139
src/lib/server/broker-utils/FileSystem/SSLTerminations.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
132
src/lib/server/broker-utils/FileSystem/utils.ts
Normal file
132
src/lib/server/broker-utils/FileSystem/utils.ts
Normal 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
|
||||
}
|
||||
54
src/lib/server/broker-utils/SQLite/Database.ts
Normal file
54
src/lib/server/broker-utils/SQLite/Database.ts
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
25
src/lib/server/broker-utils/SQLite/SQL/Sessions.sql
Normal file
25
src/lib/server/broker-utils/SQLite/SQL/Sessions.sql
Normal 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;
|
||||
|
||||
|
||||
|
||||
|
||||
21
src/lib/server/broker-utils/SQLite/SQL/Users.sql
Normal file
21
src/lib/server/broker-utils/SQLite/SQL/Users.sql
Normal 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;
|
||||
|
||||
207
src/lib/server/broker-utils/SQLite/Sessions.ts
Normal file
207
src/lib/server/broker-utils/SQLite/Sessions.ts
Normal 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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
230
src/lib/server/broker-utils/SQLite/Users.ts
Normal file
230
src/lib/server/broker-utils/SQLite/Users.ts
Normal 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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
79
src/lib/server/classes/appdata.ts
Normal file
79
src/lib/server/classes/appdata.ts
Normal 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}`
|
||||
}
|
||||
|
||||
}
|
||||
64
src/lib/server/classes/endpoints/endpoint-manager.ts
Normal file
64
src/lib/server/classes/endpoints/endpoint-manager.ts
Normal 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()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
9
src/lib/server/classes/endpoints/endpoints-interfaces.ts
Normal file
9
src/lib/server/classes/endpoints/endpoints-interfaces.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { EndpointType } from "$lib/server/enums/endpoints";
|
||||
|
||||
export interface IEndpoint {
|
||||
|
||||
type: EndpointType
|
||||
name: string
|
||||
path: string
|
||||
|
||||
}
|
||||
115
src/lib/server/classes/endpoints/manual-endpoint.ts
Normal file
115
src/lib/server/classes/endpoints/manual-endpoint.ts
Normal 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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
250
src/lib/server/classes/endpoints/ssl-termination-endpoint.ts
Normal file
250
src/lib/server/classes/endpoints/ssl-termination-endpoint.ts
Normal 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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
79
src/lib/server/classes/sessions.ts
Normal file
79
src/lib/server/classes/sessions.ts
Normal 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!")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
89
src/lib/server/classes/users.ts
Normal file
89
src/lib/server/classes/users.ts
Normal 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!")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
23
src/lib/server/enums/endpoints.ts
Normal file
23
src/lib/server/enums/endpoints.ts
Normal 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
|
||||
}
|
||||
}
|
||||
0
src/lib/server/enums/file-modes.ts
Normal file
0
src/lib/server/enums/file-modes.ts
Normal file
65
src/lib/server/enums/protocols.ts
Normal file
65
src/lib/server/enums/protocols.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
104
src/lib/server/handles/handle.ts
Normal file
104
src/lib/server/handles/handle.ts
Normal 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
2
src/lib/server/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
export {PKG} from "./utils/constants";
|
||||
13
src/lib/server/utils/constants.ts
Normal file
13
src/lib/server/utils/constants.ts
Normal 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
|
||||
185
src/lib/server/utils/filesystem-utils.ts
Normal file
185
src/lib/server/utils/filesystem-utils.ts
Normal 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}`
|
||||
})
|
||||
}
|
||||
0
src/lib/server/utils/firegex.ts
Normal file
0
src/lib/server/utils/firegex.ts
Normal file
123
src/lib/server/utils/jtw-utils.ts
Normal file
123
src/lib/server/utils/jtw-utils.ts
Normal 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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
48
src/lib/server/utils/logger.ts
Normal file
48
src/lib/server/utils/logger.ts
Normal 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()
|
||||
45
src/lib/server/utils/nginx-utils.ts
Normal file
45
src/lib/server/utils/nginx-utils.ts
Normal 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
|
||||
|
||||
}
|
||||
51
src/lib/server/utils/openssl-utils.ts
Normal file
51
src/lib/server/utils/openssl-utils.ts
Normal 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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
71
src/lib/server/utils/ports-utils.ts
Normal file
71
src/lib/server/utils/ports-utils.ts
Normal 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
|
||||
}
|
||||
9
src/lib/server/utils/shell-commands.ts
Normal file
9
src/lib/server/utils/shell-commands.ts
Normal 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
|
||||
}
|
||||
15
src/lib/shared/constants.ts
Normal file
15
src/lib/shared/constants.ts
Normal 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 = "####################################################################"
|
||||
0
src/lib/shared/utils/cookies-utils.ts
Normal file
0
src/lib/shared/utils/cookies-utils.ts
Normal file
8
src/lib/shared/utils/path-utils.ts
Normal file
8
src/lib/shared/utils/path-utils.ts
Normal 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
0
src/private/.gitkeep
Normal file
17
src/routes/+page.svelte
Normal file
17
src/routes/+page.svelte
Normal 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>
|
||||
105
src/routes/api/login/+server.ts
Normal file
105
src/routes/api/login/+server.ts
Normal 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
|
||||
};
|
||||
51
src/routes/api/logout/+server.ts
Normal file
51
src/routes/api/logout/+server.ts
Normal 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
|
||||
};
|
||||
21
src/routes/api/program/endpoint/activate/+server.ts
Normal file
21
src/routes/api/program/endpoint/activate/+server.ts
Normal 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");
|
||||
}
|
||||
};
|
||||
9
src/routes/api/program/endpoint/all/+server.ts
Normal file
9
src/routes/api/program/endpoint/all/+server.ts
Normal 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);
|
||||
}
|
||||
21
src/routes/api/program/endpoint/deactivate/+server.ts
Normal file
21
src/routes/api/program/endpoint/deactivate/+server.ts
Normal 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");
|
||||
}
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
9
src/routes/api/program/endpoint/status/+server.ts
Normal file
9
src/routes/api/program/endpoint/status/+server.ts
Normal 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 });
|
||||
};
|
||||
114
src/routes/api/program/register/+server.ts
Normal file
114
src/routes/api/program/register/+server.ts
Normal 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
|
||||
};
|
||||
69
src/routes/api/program/reload/+server.ts
Normal file
69
src/routes/api/program/reload/+server.ts
Normal 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
|
||||
};
|
||||
116
src/routes/api/register/+server.ts
Normal file
116
src/routes/api/register/+server.ts
Normal 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
|
||||
};
|
||||
9
src/routes/api/version/+server.ts
Normal file
9
src/routes/api/version/+server.ts
Normal 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)
|
||||
|
||||
}
|
||||
58
src/routes/app/login/+page.svelte
Normal file
58
src/routes/app/login/+page.svelte
Normal 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>
|
||||
|
||||
|
||||
3
src/routes/app/program/home/+page.svelte
Normal file
3
src/routes/app/program/home/+page.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<h1>Home</h1>
|
||||
|
||||
Welcome to SSL-Sniffer
|
||||
59
src/routes/app/register/+page.svelte
Normal file
59
src/routes/app/register/+page.svelte
Normal 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
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
19
svelte.config.js
Normal file
19
svelte.config.js
Normal 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;
|
||||
8
tests/unit/database.test.ts
Normal file
8
tests/unit/database.test.ts
Normal 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
25
tests/unit/demo.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
27
tests/unit/filewatch.test.ts
Normal file
27
tests/unit/filewatch.test.ts
Normal 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
19
tsconfig.json
Normal 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
48
vite.config.ts
Normal 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
2
vitest-setup-client.ts
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="@vitest/browser/matchers" />
|
||||
/// <reference types="@vitest/browser/providers/playwright" />
|
||||
Loading…
x
Reference in New Issue
Block a user