eod commit, working beta deployed (https://dev.security-command.org/)

This commit is contained in:
2025-11-26 19:51:26 +01:00
parent 70eb05c10b
commit e0298c9934
7 changed files with 128 additions and 56 deletions

View File

@@ -0,0 +1,33 @@
FROM ghcr.io/prantlf/vlang:latest AS builder
WORKDIR ./src/
COPY . .
RUN mkdir -p ./src/../../build/
# Build release binary. Output to /src/bin/cdn_app
# If your main file is main.v in the root of the context, this will work.
RUN v -prod -o ./src/../../build/ ./main.v
# Stage 2: runtime
FROM debian:bookworm-slim AS runtime
WORKDIR /app
# create non-root user (good practice)
RUN useradd -m appuser
# copy binary from builder
COPY --from=builder /src/bin/cdn_app /app/cdn_app
# copy templates/assets if your binary loads embedded files at runtime or expects files on disk
# (If you use V's $embed_file at compile time then templates are already embedded and this is optional)
COPY template/ /app/template/
COPY util/ /app/util/
COPY database/ /app/database/
RUN chown -R appuser:appuser /app
USER appuser
# expose container ports your app might use (adjust if you use different ports)
EXPOSE 8080 6767
ENTRYPOINT ["/app/cdn_app"]

View File

@@ -1,8 +1,10 @@
module database module database
import os
import rand import rand
import util
// import db.mysql // import db.mysql
import db.redis // import db.redis
import fleximus.argon2 import fleximus.argon2
pub struct Crypto {} pub struct Crypto {}
@@ -17,6 +19,13 @@ pub fn Crypto.hash_verify(password string, hash string) !bool {
return argon2.verify(hash, password.bytes()) or { return error('argon2 verify failed: ${err}') } return argon2.verify(hash, password.bytes()) or { return error('argon2 verify failed: ${err}') }
} }
pub fn get_dummy_exclusion_list(exe string, root string) []string {
return [
exe,
util.Utility.normalize_path(os.join_path(root, 'update.sh')),
]
}
// struct Database { // struct Database {
// mut: // mut:
// conn mysql.DB // conn mysql.DB
@@ -46,13 +55,13 @@ pub fn Crypto.hash_verify(password string, hash string) !bool {
// //
// pub fn Database.test() {} // pub fn Database.test() {}
pub fn test() ! { // pub fn test() ! {
mut r := redis.connect(redis.Config{ // mut r := redis.connect(redis.Config{
host: 'vpn.security-command.org' // host: 'vpn.security-command.org'
port: 6767 // port: 6767
password: 'SuperSecretPassword123' // password: 'SuperSecretPassword123'
})! // })!
//
pong := r.ping() or { panic(err) } // pong := r.ping() or { panic(err) }
println(pong) // println(pong)
} // }

View File

@@ -1,22 +1,23 @@
module database module database
@[table: 'users'] //
struct Users { // @[table: 'users']
id int @[primary; serial] // struct Users {
name string @[nonnull; unique] // id int @[primary; serial]
password_hash string @[nonnull] // name string @[nonnull; unique]
} // password_hash string @[nonnull]
// }
@[table: 'login_attempts'] //
struct Logins { // @[table: 'login_attempts']
ip string @[primary] // struct Logins {
attempts int @[nonnull] // ip string @[primary]
attempt_time string @[default: 'CURRENT_TIMESTAMP'; nonnull] // attempts int @[nonnull]
} // attempt_time string @[default: 'CURRENT_TIMESTAMP'; nonnull]
// }
@[table: 'files'] //
struct Files { // @[table: 'files']
id int @[primary; serial] // struct Files {
path string @[nonnull; unique] // id int @[primary; serial]
visible bool @[default: false; nonnull] // path string @[nonnull; unique]
} // visible bool @[default: false; nonnull]
// }

View File

@@ -3,6 +3,7 @@ module main
import os import os
import veb import veb
import util import util
import database
import thomaspeissl.dotenv import thomaspeissl.dotenv
// structs // structs
@@ -54,7 +55,8 @@ pub fn (app &App) root(mut ctx Context, path string) veb.Result {
return ctx.text(content) return ctx.text(content)
} }
entries := util.Utility.list_files(abs_path, abs_root) or { return ctx.not_found() } exclude := database.get_dummy_exclusion_list(app.cfg.exe, app.cfg.root)
entries := util.Utility.list_files(abs_path, abs_root, exclude) or { return ctx.not_found() }
directory := util.HtmlBuilder.generate_breadcrumbs(abs_path.split(abs_root)[1]) directory := util.HtmlBuilder.generate_breadcrumbs(abs_path.split(abs_root)[1])
files, meta := util.HtmlBuilder.generate_file_list(entries, abs_path) files, meta := util.HtmlBuilder.generate_file_list(entries, abs_path)
@@ -94,7 +96,7 @@ pub fn (mut ctx Context) request_error(msg string) veb.Result {
pub fn (mut ctx Context) forbidden() veb.Result { pub fn (mut ctx Context) forbidden() veb.Result {
ctx.res.set_status(.forbidden) ctx.res.set_status(.forbidden)
return ctx.html(ctx.error_page(403, 'Forbidden', 'Oops! You aren\'t allowed around here.')) return ctx.html(ctx.error_page(403, 'Forbidden', "Oops! You aren't allowed around here."))
} }
pub fn (mut ctx Context) not_found() veb.Result { pub fn (mut ctx Context) not_found() veb.Result {
@@ -109,24 +111,37 @@ pub fn (mut ctx Context) server_error(msg string) veb.Result {
// main // main
fn populate() (&util.Config, &util.Embedded) fn populate() (&util.Config, &util.Embedded) {
{
dotenv.load() dotenv.load()
dotenv.require('CDN_DB_HOST', 'CDN_DB_PORT') dotenv.require('CDN_DB_HOST', 'CDN_DB_PORT')
def_root := $d('root', '.') executable := os.executable()
def_root := os.dir(executable)
def_port := int($d('port', 6767)) def_port := int($d('port', 6767))
def_user := $d('username', 'cdn') def_user := $d('username', 'cdn')
def_pass := $d('password', 'totallySafeCdnDatabasePassword1235') def_pass := $d('password', 'totallySafeCdnDatabasePassword1235')
return &util.Config{ return &util.Config{
root: if os.getenv('CDN_ROOT') != '' { os.getenv('CDN_ROOT').str() } else { def_root } exe: util.Utility.normalize_path(executable)
root: util.Utility.normalize_path(if os.getenv('CDN_ROOT') != '' {
util.Utility.resolve_path(def_root, os.getenv('CDN_ROOT').str())
} else {
def_root
})
port: if os.getenv('CDN_PORT') != '' { os.getenv('CDN_PORT').int() } else { def_port } port: if os.getenv('CDN_PORT') != '' { os.getenv('CDN_PORT').int() } else { def_port }
database: &util.Database{ database: &util.Database{
host: os.getenv('CDN_DB_HOST').str() host: os.getenv('CDN_DB_HOST').str()
port: os.getenv('CDN_DB_PORT').int() port: os.getenv('CDN_DB_PORT').int()
username: if os.getenv('CDN_DB_USERNAME') != '' { os.getenv('CDN_DB_USERNAME').str() } else { def_user } username: if os.getenv('CDN_DB_USERNAME') != '' {
password: if os.getenv('CDN_DB_PASSWORD') != '' { os.getenv('CDN_DB_PASSWORD').str() } else { def_pass } os.getenv('CDN_DB_USERNAME').str()
} else {
def_user
}
password: if os.getenv('CDN_DB_PASSWORD') != '' {
os.getenv('CDN_DB_PASSWORD').str()
} else {
def_pass
}
} }
}, &util.Embedded{ }, &util.Embedded{
style_css: $embed_file('template/assets/style.css', .zlib).to_string() style_css: $embed_file('template/assets/style.css', .zlib).to_string()
@@ -134,11 +149,15 @@ fn populate() (&util.Config, &util.Embedded)
} }
} }
fn main() fn main() {
{
cfg, mut embed := populate() cfg, mut embed := populate()
mut app := &App{ cfg: cfg, embed: embed } mut app := &App{
mut auth := &Auth{ app: &app } cfg: cfg
embed: embed
}
mut auth := &Auth{
app: &app
}
app.register_controller[Auth, Context]('/auth', mut auth)! app.register_controller[Auth, Context]('/auth', mut auth)!

View File

@@ -63,7 +63,7 @@ body {
.header { .header {
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: 3rem; padding: 2rem 3rem 1rem 3rem;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;

View File

@@ -3,6 +3,7 @@ module util
import os import os
import time import time
import encoding.base64
pub struct Database { pub struct Database {
pub: pub:
@@ -14,6 +15,7 @@ pub:
pub struct Config { pub struct Config {
pub: pub:
exe string
root string root string
port int port int
database Database database Database
@@ -71,13 +73,11 @@ pub fn Utility.get_icon(ext string, is_dir bool) string {
'unknown': '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/></svg>' 'unknown': '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/></svg>'
} }
return Utility.svg_to_data_uri(if is_dir { icons['dir'] } else { icons[ext] or { return if is_dir { icons['dir'] } else { icons[ext] or { icons['unknown'] } }
icons['unknown']} })
} }
pub fn Utility.svg_to_data_uri(svg string) string { pub fn Utility.svg_to_data_uri(svg string) string {
return svg return 'data:image/svg+xml;base64,${base64.encode(svg.bytes())}'
// return 'data:image/svg+xml;base64,${base64.encode(svg.bytes())}'
} }
pub fn Utility.format_size(bytes i64) string { pub fn Utility.format_size(bytes i64) string {
@@ -105,20 +105,34 @@ pub fn Utility.format_date(unix_time i64) string {
return time.unix(unix_time).format_ss_milli() return time.unix(unix_time).format_ss_milli()
} }
pub fn Utility.list_files(dir_path string, relative string) ![]FileEntry { pub fn Utility.resolve_path(base string, path string) string {
if os.is_abs_path(path) {
return path
}
return os.real_path(os.join_path(base, path))
}
pub fn Utility.list_files(dir_path string, relative string, exclude []string) ![]FileEntry {
mut entries := []FileEntry{} mut entries := []FileEntry{}
files := os.ls(dir_path)! files := os.ls(dir_path)!
for file in files { for file in files {
full_path := os.join_path(dir_path, file) full_path := os.join_path(dir_path, file)
normalized := Utility.normalize_path(full_path)
if normalized in exclude {
continue
}
is_dir := os.is_dir(full_path) is_dir := os.is_dir(full_path)
file_info := os.stat(full_path)! file_info := os.stat(full_path)!
file_ext := full_path.split('.').last() file_ext := full_path.split('.').last()
entries << FileEntry{ entries << FileEntry{
name: if is_dir { '${file}/' } else { file } name: if is_dir { '${file}/' } else { file }
abs_path: Utility.normalize_path(full_path) abs_path: normalized
path: Utility.normalize_path(full_path).split(relative)[1] or { 'err' } path: Utility.normalize_path(full_path).split(relative)[1] or { 'err' }
is_dir: is_dir is_dir: is_dir
size: if !is_dir { file_info.size } else { 0 } size: if !is_dir { file_info.size } else { 0 }

View File

@@ -1,15 +1,11 @@
services: services:
app: app:
build:
context: ./app
dockerfile: Dockerfile
container_name: cdn_web container_name: cdn_web
depends_on: depends_on:
- db - db
ports: ports:
- "8080:8080" - "8080:8080" // how do these get passed to my exe?, etc
environment: environment:
# env shee
DB_HOST: db DB_HOST: db
DB_USER: appuser DB_USER: appuser
DB_PASS: s3cret DB_PASS: s3cret