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
|
// Displayed name
|
||||||
"name": "Vulnbox",
|
"name": "SSL-Sniffer",
|
||||||
|
|
||||||
// Service name from compose file
|
// Service name from compose file
|
||||||
"service": "vulnbox",
|
"service": "ssl-sniffer",
|
||||||
|
|
||||||
// Compose-File
|
// Compose-File
|
||||||
"dockerComposeFile": ["../compose.yaml"],
|
"dockerComposeFile": ["../compose.yaml"],
|
||||||
@ -12,6 +12,8 @@
|
|||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
"svelte.svelte-vscode",
|
||||||
|
"vitest.explorer",
|
||||||
"william-voyek.vscode-nginx",
|
"william-voyek.vscode-nginx",
|
||||||
"fabiospampinato.vscode-highlight",
|
"fabiospampinato.vscode-highlight",
|
||||||
"fabiospampinato.vscode-todo-plus"
|
"fabiospampinato.vscode-todo-plus"
|
||||||
@ -20,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// The WorkspaceFolder inside container
|
// The WorkspaceFolder inside container
|
||||||
"workspaceFolder": "/etc/nginx",
|
"workspaceFolder": "/workspace",
|
||||||
|
|
||||||
// Env in container
|
// Env in container
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
|
|||||||
29
.gitignore
vendored
29
.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/*/*
|
config/*/*
|
||||||
private/**
|
private/**
|
||||||
services/**
|
services/**
|
||||||
|
src/private/**
|
||||||
|
src/db/**
|
||||||
**/http/*
|
**/http/*
|
||||||
!**/*.gitkeep
|
!**/*.gitkeep
|
||||||
!**/*.example
|
!**/*.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"
|
ENV PATH="$PATH:/docker-bin"
|
||||||
|
|
||||||
@ -6,7 +6,7 @@ RUN apk update && apk upgrade
|
|||||||
RUN apk add nginx openrc \
|
RUN apk add nginx openrc \
|
||||||
openssl nginx-mod-stream \
|
openssl nginx-mod-stream \
|
||||||
nginx-mod-http-headers-more \
|
nginx-mod-http-headers-more \
|
||||||
tcpdump
|
tcpdump curl git
|
||||||
|
|
||||||
# NGINX
|
# NGINX
|
||||||
RUN adduser -D -g 'www' www
|
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 mkdir /run/openrc
|
||||||
RUN touch /run/openrc/softlevel
|
RUN touch /run/openrc/softlevel
|
||||||
|
|
||||||
|
# Make workdir
|
||||||
|
RUN mkdir /workspace
|
||||||
|
|
||||||
|
# Make entrypoint
|
||||||
WORKDIR /docker-bin
|
WORKDIR /docker-bin
|
||||||
|
|
||||||
COPY ./helper-scripts /docker-bin
|
COPY ./helper-scripts/scripts /docker-bin
|
||||||
|
|
||||||
RUN chmod +x /docker-bin/*
|
RUN chmod +x /docker-bin/*
|
||||||
|
|
||||||
|
|||||||
105
README.md
105
README.md
@ -1,81 +1,38 @@
|
|||||||
# SSL Sniffer
|
# sv
|
||||||
|
|
||||||
> [!CAUTION]
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
> 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.
|
|
||||||
|
|
||||||
## How to use the software
|
## Creating a project
|
||||||
|
|
||||||
- Have [Firegex](https://github.com/Pwnzer0tt1/firegex) installed on the `vulnbox` (OPTIONAL)
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
- 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
|
|
||||||
|
|
||||||
> [!TIP]
|
```bash
|
||||||
> Remember to capture traffic from the `lo` interface, otherwise you won't
|
# create a new project in the current directory
|
||||||
> see any benefit in setting such infrastructure
|
npx sv create
|
||||||
|
|
||||||
## Full example
|
# create a new project in my-app
|
||||||
|
npx sv create my-app
|
||||||
### 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||

|
## 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:
|
volumes:
|
||||||
- ./nginx:/etc/nginx/
|
- ./nginx:/etc/nginx/
|
||||||
- ./private/:/services-keys
|
- ./private/:/services-keys
|
||||||
|
- .:/workspace
|
||||||
network_mode: host
|
network_mode: host
|
||||||
entrypoint: entry.sh
|
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
|
# endpoint port
|
||||||
listen PORT ssl;
|
listen PORT ssl;
|
||||||
|
|
||||||
# Uncomment if http2
|
|
||||||
# http2 on;
|
|
||||||
|
|
||||||
# Here put the unencrypted
|
# Here put the unencrypted
|
||||||
# endpoint port
|
# endpoint port
|
||||||
location / {
|
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.
|
# Includes virtual hosts configs.
|
||||||
include /etc/nginx/http/*.conf;
|
include /etc/nginx/active/http/*.conf;
|
||||||
include /etc/nginx/grpc/*.conf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stream {
|
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