feat: challenge registry-driven platform + XVI/XVII imports + admin toggle + domain rename

- Rename repo/domain: attack-defense-platform / attackdefense.imrnes.team (all refs replaced)
- challenge_registry.json: single source of truth (28 challs across gemastik18/xvi/xvii)
- teams.py: registry-driven CHALLENGES, set_challenge_enabled, sync_challenge_runtime
  (apply enable/disable to live teams: build/up or stop/remove + receiver restart)
- compose_gen.py: render per-team compose from canonical per-challenge templates
  (image reuse, per-team ports 30xxx, flag mounts, passwords)
- gen_canonical_composes.py: canonical docker-compose.yml for all services
- import_new_challenges.py: import XVI/XVII services + EOL base image fixes
  (debian:buster→bookworm, node:14→20, python:3.7-slim→3.11)
- receiver: xvi package (10 checkers) + xvii package (12 generic checkers),
  Challenge base reads PASSWORD_<team_port> from env; gen_receiver_main.py
  generates per-team main.py from registry
- main.py: /api/challenges returns full registry; PATCH /api/challenges/<name>
  toggles enabled + applies to live teams
- index.html: 🏗️ Challenge Manager tab (toggle per challenge, grouped by set)
- SLA bonus now dynamic (all enabled challenges, not hardcoded 6)
This commit is contained in:
MythEclipse
2026-09-25 14:04:33 +08:00
parent c35b23a37f
commit c6fd9ec268
1317 changed files with 306586 additions and 128 deletions
+5
View File
@@ -0,0 +1,5 @@
*.beam
*.ez
/build
erl_crash.dump
*.db
+36
View File
@@ -0,0 +1,36 @@
FROM public.ecr.aws/docker/library/alpine:latest
ARG PASSWORD
RUN \
apk update && \
apk add openrc --no-cache && \
apk add openssh-server && \
apk add openssl && \
rc-update add sshd && \
rc-status && \
touch /run/openrc/softlevel
RUN echo root:${PASSWORD} | chpasswd
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN apk add bash curl shadow
RUN chsh -s /bin/bash root
RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/community gleam rebar3 make gcc musl-dev
ADD https://raw.githubusercontent.com/openembedded/openembedded-core/master/meta/recipes-core/musl/bsd-headers/sys-queue.h /usr/include/sys/queue.h
WORKDIR /ctf/gleam-drive
COPY src src/
COPY *.toml .
COPY start.sh .
RUN gleam build
RUN cat /dev/urandom | head -c 16 > .aes-key
RUN openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=GleamDriveState/L=GleamDriveCity/O=GleamDriveCompany/OU=GleamDriveCompanySection/CN=GleamDrive"
RUN chmod +x start.sh
CMD ./start.sh
+3
View File
@@ -0,0 +1,3 @@
Acquire::AllowInsecureRepositories "true";
Acquire::AllowDowngradeToInsecureRepositories "true";
Apt::Get::AllowUnauthenticated "true";
+18
View File
@@ -0,0 +1,18 @@
services:
gleam-drive:
container_name: gleam-drive_container
hostname: gleam-drive
restart: always
build:
context: .
args:
- PASSWORD=$PASSWORD_12000
volumes:
- ../receiver/flags/gleam-drive.txt:/flag.txt:ro
- ../utils/bashrc:/root/.bashrc:ro
- ../utils/preexec.sh:/root/.preexec.sh:ro
ports:
- "12000:8000"
- "12022:22"
extra_hosts:
- "host.docker.internal:host-gateway"
+29
View File
@@ -0,0 +1,29 @@
name = "gdrive"
version = "1.0.0"
# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# description = ""
# licences = ["Apache-2.0"]
# repository = { type = "github", user = "", repo = "" }
# links = [{ title = "Website", href = "" }]
#
# For a full reference of all the available options, you can have a look at
# https://gleam.run/writing-gleam/gleam-toml/.
[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
filepath = ">= 1.0.0 and < 2.0.0"
gleam_erlang = ">= 0.25.0 and < 1.0.0"
mist = ">= 1.2.0 and < 2.0.0"
gleam_http = ">= 3.6.0 and < 4.0.0"
simplifile = ">= 2.0.1 and < 3.0.0"
beecrypt = ">= 0.3.1 and < 1.0.0"
sqlight = ">= 0.9.0 and < 1.0.0"
birl = ">= 1.7.1 and < 2.0.0"
gleam_json = ">= 1.0.1 and < 2.0.0"
wisp = ">= 0.16.0 and < 1.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
+44
View File
@@ -0,0 +1,44 @@
# This file was generated by Gleam
# You typically do not need to edit this file
packages = [
{ name = "bcrypt", version = "1.2.2", build_tools = ["rebar3"], requirements = ["poolboy"], otp_app = "bcrypt", source = "hex", outer_checksum = "6428D68BB2608490E150BE406A7AB75B5BCE4A2B5A3B0C3F36A12E367A141561" },
{ name = "beecrypt", version = "0.3.1", build_tools = ["gleam"], requirements = ["bcrypt", "gleam_crypto", "gleam_erlang", "gleam_stdlib"], otp_app = "beecrypt", source = "hex", outer_checksum = "9698221A179C4D823DE2F00954D7A10BBC2B577B6C40A269BD676C308A788C15" },
{ name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
{ name = "esqlite", version = "0.8.8", build_tools = ["rebar3"], requirements = [], otp_app = "esqlite", source = "hex", outer_checksum = "374902457C7D94DC9409C98D3BDD1CA0D50A60DC9F3BDF1FD8EB74C0DCDF02D6" },
{ name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
{ name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
{ name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
{ name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
{ name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
{ name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" },
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
{ name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
{ name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
{ name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
{ name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
{ name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
{ name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
{ name = "poolboy", version = "1.5.2", build_tools = ["rebar3"], requirements = [], otp_app = "poolboy", source = "hex", outer_checksum = "DAD79704CE5440F3D5A3681C8590B9DC25D1A561E8F5A9C995281012860901E3" },
{ name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
{ name = "simplifile", version = "2.0.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "5FFEBD0CAB39BDD343C3E1CCA6438B2848847DC170BA2386DF9D7064F34DF000" },
{ name = "sqlight", version = "0.9.0", build_tools = ["gleam"], requirements = ["esqlite", "gleam_stdlib"], otp_app = "sqlight", source = "hex", outer_checksum = "2D9C9BA420A5E7DCE7DB2DAAE4CAB0BE6218BEB48FD1531C583550B3D1316E94" },
{ name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
{ name = "wisp", version = "0.16.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "A13DAD6A84BED78FF5D7656F3B5125086801BE1AF47427A9A759EA8C484345AB" },
]
[requirements]
beecrypt = { version = ">= 0.3.1 and < 1.0.0" }
birl = { version = ">= 1.7.1 and < 2.0.0" }
filepath = { version = ">= 1.0.0 and < 2.0.0" }
gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" }
gleam_http = { version = ">= 3.6.0 and < 4.0.0" }
gleam_json = { version = ">= 1.0.1 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
mist = { version = ">= 1.2.0 and < 2.0.0" }
simplifile = { version = ">= 2.0.1 and < 3.0.0" }
sqlight = { version = ">= 0.9.0 and < 1.0.0" }
wisp = { version = ">= 0.16.0 and < 1.0.0"}
+10
View File
@@ -0,0 +1,10 @@
import gleam/result
import simplifile
pub const upload_dir = "/tmp/gleam-drive/"
pub const max_file_size = 524_288
pub fn aes_key() -> String {
result.unwrap(simplifile.read(".aes-key"), "change-me-please")
}
+54
View File
@@ -0,0 +1,54 @@
import gleam/bit_array
import gleam/result
import gleam/string
const block_size = 16
@external(erlang, "crypto_ffi", "encrypt")
pub fn encrypt_ffi(key: BitArray, data: BitArray) -> BitArray
@external(erlang, "crypto_ffi", "decrypt")
pub fn decrypt_ffi(key: BitArray, data: BitArray) -> BitArray
@external(erlang, "crypto_ffi", "hash")
pub fn hash(data: String) -> String
pub fn encrypt(key: String, data: String) -> String {
let key = bit_array.from_string(key)
let data = bit_array.from_string(pad(data))
let ciphertext = encrypt_ffi(key, data)
bit_array.base16_encode(ciphertext)
}
pub fn decrypt(key: String, data: String) -> String {
let key = bit_array.from_string(key)
let data = bit_array.base16_decode(data)
case data {
Ok(data) -> {
let plaintext = decrypt_ffi(key, data)
unpad(result.unwrap(bit_array.to_string(plaintext), ""))
}
Error(_) -> ""
}
}
pub fn pad(data: String) -> String {
let remainder = string.byte_size(data) % block_size
let padding = string.repeat(" ", block_size - remainder)
string.append(data, padding)
}
pub fn unpad(data: String) -> String {
string.trim_right(data)
}
pub fn encode(data: String) -> String {
bit_array.base16_encode(bit_array.from_string(data))
}
pub fn decode(data: String) -> String {
case bit_array.base16_decode(data) {
Ok(data) -> result.unwrap(bit_array.to_string(data), "")
Error(_) -> ""
}
}
+54
View File
@@ -0,0 +1,54 @@
import gleam/dynamic
import gleam/json.{int, object, string}
pub type User {
User(email: String, password: String)
}
pub type Session {
Session(email: String, expired_at: Int)
}
pub type Upload {
Upload(email: String, filename: String, filesize: Int, timestamp: String)
}
pub type Shared {
Shared(filepath: String, recepient: String)
}
pub fn session_to_json(session: Session) -> String {
object([
#("email", string(session.email)),
#("expired_at", int(session.expired_at)),
])
|> json.to_string
}
pub fn session_from_json(data: String) -> Result(Session, json.DecodeError) {
let session_decoder =
dynamic.decode2(
Session,
dynamic.field("email", of: dynamic.string),
dynamic.field("expired_at", of: dynamic.int),
)
json.decode(from: data, using: session_decoder)
}
pub fn shared_to_json(shared: Shared) -> String {
object([
#("filepath", string(shared.filepath)),
#("recepient", string(shared.recepient)),
])
|> json.to_string
}
pub fn shared_from_json(data: String) -> Result(Shared, json.DecodeError) {
let shared_decoder =
dynamic.decode2(
Shared,
dynamic.field("filepath", of: dynamic.string),
dynamic.field("recepient", of: dynamic.string),
)
json.decode(from: data, using: shared_decoder)
}
@@ -0,0 +1,153 @@
import app/crypto
import app/model
import gleam/dynamic
import gleam/int
import gleam/list
import sqlight
pub fn init(db: sqlight.Connection) {
let assert Ok(Nil) =
sqlight.exec(
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT, password TEXT)",
db,
)
let assert Ok(Nil) =
sqlight.exec(
"CREATE TABLE IF NOT EXISTS uploads (id INTEGER PRIMARY KEY, email TEXT, filename TEXT, filesize INTEGER, timestamp TEXT)",
db,
)
let assert Ok(Nil) =
sqlight.exec(
"CREATE INDEX IF NOT EXISTS uploads_owner_index ON uploads (email)",
db,
)
}
pub fn login(db: sqlight.Connection, email: String, password: String) -> Bool {
let sql = "SELECT * FROM users WHERE email = ? AND password = ?"
let user_decoder = dynamic.tuple3(dynamic.int, dynamic.string, dynamic.string)
let assert Ok(users) =
sqlight.query(
sql,
on: db,
with: [
sqlight.text(crypto.encode(email)),
sqlight.text(crypto.hash(password)),
],
expecting: user_decoder,
)
list.length(users) > 0
}
pub fn create_user(
db: sqlight.Connection,
email: String,
password: String,
) -> Result(String, String) {
let sql =
"INSERT INTO users (email, password) VALUES ('"
<> crypto.encode(email)
<> "', '"
<> crypto.hash(password)
<> "')"
case sqlight.exec(sql, db) {
Ok(_) -> Ok("User created")
Error(_) -> Error("Failed to create user")
}
}
pub fn user_exists(db: sqlight.Connection, email: String) -> Bool {
let sql = "SELECT * FROM users WHERE email = ?"
let user_decoder = dynamic.tuple3(dynamic.int, dynamic.string, dynamic.string)
let assert Ok(users) =
sqlight.query(
sql,
on: db,
with: [sqlight.text(crypto.encode(email))],
expecting: user_decoder,
)
list.length(users) > 0
}
pub fn create_upload(
db: sqlight.Connection,
email: String,
filename: String,
filesize: Int,
) {
let sql =
"INSERT INTO uploads (email, filename, filesize, timestamp) VALUES ('"
<> crypto.encode(email)
<> "', '"
<> crypto.encode(filename)
<> "', "
<> int.to_string(filesize)
<> ", datetime('now'))"
let assert Ok(Nil) = sqlight.exec(sql, db)
}
pub fn list_uploads(db: sqlight.Connection, email: String) -> List(model.Upload) {
let sql = "SELECT * FROM uploads WHERE email = ?"
let upload_decoder =
dynamic.tuple5(
dynamic.int,
dynamic.string,
dynamic.string,
dynamic.int,
dynamic.string,
)
let assert Ok(uploads) =
sqlight.query(
sql,
on: db,
with: [sqlight.text(crypto.encode(email))],
expecting: upload_decoder,
)
list.map(uploads, fn(upload) {
model.Upload(
crypto.decode(upload.1),
crypto.decode(upload.2),
upload.3,
upload.4,
)
})
}
pub fn find_file(
db: sqlight.Connection,
email: String,
filename: String,
) -> Result(model.Upload, String) {
let sql = "SELECT * FROM uploads WHERE email = ? AND filename = ?"
let upload_decoder =
dynamic.tuple5(
dynamic.int,
dynamic.string,
dynamic.string,
dynamic.int,
dynamic.string,
)
let assert Ok(uploads) =
sqlight.query(
sql,
on: db,
with: [
sqlight.text(crypto.encode(email)),
sqlight.text(crypto.encode(filename)),
],
expecting: upload_decoder,
)
case list.first(uploads) {
Ok(upload) ->
Ok(model.Upload(
crypto.decode(upload.1),
crypto.decode(upload.2),
upload.3,
upload.4,
))
Error(_) -> Error("File not found")
}
}
+267
View File
@@ -0,0 +1,267 @@
import app/config
import app/model
import app/service
import app/util
import app/view
import app/web
import birl
import filepath
import gleam/http.{Get, Post}
import gleam/io
import gleam/list
import gleam/result
import gleam/string_builder
import wisp.{type Request, type Response}
pub fn handle_request(req: Request, ctx: web.Context) -> Response {
use req <- web.middleware(req)
let session_cookie = case wisp.get_cookie(req, "session", wisp.PlainText) {
Ok(cookie) -> cookie
Error(_) -> ""
}
let session = service.extract_session_token(session_cookie)
case session.expired_at > birl.to_unix(birl.now()) {
True -> handle_protected_routes(req, ctx, session)
False -> handle_routes(req, ctx)
}
}
pub fn handle_routes(req: Request, ctx: web.Context) -> Response {
case wisp.path_segments(req) {
[] -> show_home(req)
["login"] ->
case req.method {
Post -> handle_login(req, ctx)
_ -> show_login()
}
["register"] ->
case req.method {
Post -> handle_register(req, ctx)
_ -> show_register()
}
["error"] -> show_error(req)
_ -> wisp.redirect("/login")
}
}
pub fn handle_protected_routes(
req: Request,
ctx: web.Context,
session: model.Session,
) -> Response {
case wisp.path_segments(req) {
["drive"] ->
case req.method {
Post -> handle_upload(req, ctx, session)
_ -> show_drive(ctx, session)
}
["share"] -> handle_share(req, ctx, session)
["details"] -> handle_details(req, ctx, session)
["download"] -> handle_download(req, session)
["logout"] -> destroy_session(req)
["error"] -> show_error(req)
_ -> wisp.redirect("/drive")
}
}
fn destroy_session(req: Request) -> Response {
let resp = wisp.redirect("/login")
wisp.set_cookie(resp, req, "session", "", wisp.PlainText, 0)
}
fn create_session(req: Request, token: String) -> Response {
let resp = wisp.redirect("/drive")
wisp.set_cookie(resp, req, "session", token, wisp.PlainText, 30 * 60)
}
fn error_response(message: String) -> Response {
wisp.redirect("/error?message=" <> message)
}
fn show_share(download_link: String) -> Response {
view.render_share_html(download_link)
|> string_builder.from_string
|> wisp.html_response(200)
}
fn send_file(shared: model.Shared, session: model.Session) -> Response {
case shared.recepient == session.email {
True -> {
wisp.ok()
|> wisp.set_header("content-type", "application/octet-stream")
|> wisp.file_download(
named: filepath.base_name(shared.filepath),
from: shared.filepath,
)
}
False -> error_response("Invalid token")
}
}
fn show_details(upload: model.Upload, token: String) -> Response {
view.render_detail_html(upload, token)
|> string_builder.from_string
|> wisp.html_response(200)
}
fn handle_details(
req: Request,
ctx: web.Context,
session: model.Session,
) -> Response {
let query = wisp.get_query(req)
let filename = case list.key_find(query, "filename") {
Ok(value) -> value
Error(_) -> ""
}
case service.find_upload(ctx.db, session, filename) {
Ok(upload) -> {
let filepath =
filepath.join(filepath.join(config.upload_dir, session.email), filename)
let token =
service.generate_shared_token(model.Shared(filepath, upload.email))
show_details(upload, token)
}
Error(message) -> error_response(message)
}
}
fn handle_download(req: Request, session: model.Session) -> Response {
let query = wisp.get_query(req)
case list.key_find(query, "token") {
Ok(token) -> {
case service.extract_shared_token(token) {
Ok(shared) -> send_file(shared, session)
Error(message) -> error_response(message)
}
}
Error(_) -> error_response("Invalid token")
}
}
fn handle_share(
req: Request,
ctx: web.Context,
session: model.Session,
) -> Response {
use forms <- wisp.require_form(req)
let query = wisp.get_query(req)
let payload = {
use recepient <- result.try(list.key_find(forms.values, "recepient"))
use filename <- result.try(list.key_find(query, "filename"))
Ok(#(recepient, filename))
}
case payload {
Ok(payload) -> {
case service.share(ctx.db, payload.0, payload.1, session) {
Ok(token) ->
show_share(util.get_base_url(req) <> "/download?token=" <> token)
Error(message) -> error_response(message)
}
}
Error(_) -> wisp.bad_request()
}
}
fn show_error(req: Request) -> Response {
let query = wisp.get_query(req)
let message = case list.key_find(query, "message") {
Ok(value) -> value
Error(_) -> "Unknown error"
}
view.render_error_html(message)
|> string_builder.from_string
|> wisp.html_response(200)
}
fn show_home(req: Request) -> Response {
use <- wisp.require_method(req, Get)
view.index_html
|> string_builder.from_string
|> wisp.html_response(200)
}
fn show_login() -> Response {
view.login_html
|> string_builder.from_string
|> wisp.html_response(200)
}
fn handle_login(req: Request, ctx: web.Context) -> Response {
use form <- wisp.require_form(req)
let payload = {
use email <- result.try(list.key_find(form.values, "email"))
use password <- result.try(list.key_find(form.values, "password"))
Ok(model.User(email, password))
}
case payload {
Ok(user) -> {
case service.login(ctx.db, user) {
Ok(token) -> create_session(req, token)
Error(message) -> error_response(message)
}
}
Error(_) -> wisp.bad_request()
}
}
fn show_register() -> Response {
view.register_html
|> string_builder.from_string
|> wisp.html_response(200)
}
fn handle_register(req: Request, ctx: web.Context) -> Response {
use form <- wisp.require_form(req)
let payload = {
use email <- result.try(list.key_find(form.values, "email"))
use password <- result.try(list.key_find(form.values, "password"))
Ok(model.User(email, password))
}
case payload {
Ok(user) -> {
case service.register(ctx.db, user) {
Ok(_) -> wisp.redirect("/login")
Error(message) -> error_response(message)
}
}
Error(_) -> wisp.bad_request()
}
}
fn show_drive(ctx: web.Context, session: model.Session) -> Response {
let drives = service.list_uploads(ctx.db, session)
view.render_drives_html(drives)
|> string_builder.from_string
|> wisp.html_response(200)
}
fn handle_upload(
req: Request,
ctx: web.Context,
session: model.Session,
) -> Response {
use form <- wisp.require_form(req)
let payload = {
use file <- result.try(list.key_find(form.files, "uploaded-file"))
Ok(file)
}
case payload {
Ok(file) -> {
case service.upload(ctx.db, file, session) {
Ok(_) -> wisp.redirect("/drive")
Error(message) -> error_response(message)
}
}
Error(_) -> {
io.debug("Failed to upload file")
wisp.bad_request()
}
}
}
+109
View File
@@ -0,0 +1,109 @@
import app/config
import app/crypto
import app/model
import app/repository
import app/util
import birl
import birl/duration
import filepath
import gleam/result
import simplifile
import sqlight
import wisp
pub fn login(db: sqlight.Connection, user: model.User) -> Result(String, String) {
case repository.login(db, user.email, user.password) {
True -> Ok(generate_session_token(user.email))
False -> Error("Invalid credentials")
}
}
pub fn register(
db: sqlight.Connection,
user: model.User,
) -> Result(String, String) {
let user_dir = filepath.join(config.upload_dir, user.email)
case repository.user_exists(db, user.email) || util.path_exists(user_dir) {
True -> Error("User already exists")
False -> {
result.unwrap(simplifile.create_directory_all(user_dir), Nil)
repository.create_user(db, user.email, user.password)
}
}
}
pub fn list_uploads(
db: sqlight.Connection,
session: model.Session,
) -> List(model.Upload) {
repository.list_uploads(db, session.email)
}
pub fn find_upload(
db: sqlight.Connection,
session: model.Session,
filename: String,
) -> Result(model.Upload, String) {
repository.find_file(db, session.email, filename)
}
pub fn upload(
db: sqlight.Connection,
file: wisp.UploadedFile,
session: model.Session,
) -> Result(String, String) {
let user_dir = filepath.join(config.upload_dir, session.email)
let filepath = filepath.join(user_dir, file.file_name)
let filesize = util.get_file_size(file.path)
case repository.create_upload(db, session.email, file.file_name, filesize) {
Ok(_) -> util.upload_file(file.path, filepath)
Error(_) -> Error("Failed to upload file")
}
}
pub fn share(
db: sqlight.Connection,
recepient: String,
filename: String,
session: model.Session,
) -> Result(String, String) {
let drive = repository.find_file(db, session.email, filename)
let user_dir = filepath.join(config.upload_dir, session.email)
let shared_file = filepath.join(user_dir, filename)
case drive {
Ok(_) -> Ok(generate_shared_token(model.Shared(shared_file, recepient)))
Error(_) -> Error("File not found")
}
}
pub fn extract_session_token(token: String) -> model.Session {
let key = config.aes_key()
case model.session_from_json(crypto.decrypt(key, token)) {
Ok(session) -> session
Error(_) -> model.Session("", 0)
}
}
pub fn extract_shared_token(token: String) -> Result(model.Shared, String) {
let key = config.aes_key()
case model.shared_from_json(crypto.decrypt(key, token)) {
Ok(shared) -> Ok(shared)
Error(_) -> Error("Invalid token")
}
}
pub fn generate_shared_token(data: model.Shared) -> String {
let key = config.aes_key()
crypto.encrypt(key, model.shared_to_json(data))
}
fn generate_session_token(email: String) -> String {
let key = config.aes_key()
crypto.encrypt(
key,
model.session_to_json(model.Session(
email,
birl.to_unix(birl.add(birl.now(), duration.minutes(30))),
)),
)
}
+37
View File
@@ -0,0 +1,37 @@
import app/config
import gleam/http
import gleam/int
import gleam/option
import gleam/result
import simplifile
import wisp
pub fn path_exists(path: String) -> Bool {
result.unwrap(simplifile.is_directory(path), True)
|| result.unwrap(simplifile.is_file(path), True)
|| result.unwrap(simplifile.is_symlink(path), True)
}
pub fn upload_file(src: String, dest: String) -> Result(String, String) {
case !path_exists(dest) && config.max_file_size > get_file_size(src) {
True -> {
result.unwrap(simplifile.copy_file(src, dest), Nil)
Ok("File uploaded")
}
False -> Error("Failed to upload file")
}
}
pub fn get_file_size(path: String) -> Int {
case simplifile.file_info(path) {
Ok(info) -> info.size
Error(_) -> 0
}
}
pub fn get_base_url(req: wisp.Request) -> String {
let scheme = http.scheme_to_string(req.scheme)
let host = req.host
let port = int.to_string(option.unwrap(req.port, 80))
scheme <> "://" <> host <> ":" <> port
}
File diff suppressed because one or more lines are too long
+17
View File
@@ -0,0 +1,17 @@
import app/config
import sqlight
import wisp
pub type Context {
Context(db: sqlight.Connection)
}
pub fn middleware(
req: wisp.Request,
handle_request: fn(wisp.Request) -> wisp.Response,
) -> wisp.Response {
use <- wisp.log_request(req)
use <- wisp.rescue_crashes
use req <- wisp.handle_head(req)
handle_request(req |> wisp.set_max_files_size(config.max_file_size))
}
+11
View File
@@ -0,0 +1,11 @@
-module(crypto_ffi).
-export([encrypt/2, decrypt/2, hash/1]).
encrypt(Key, Data) ->
crypto:crypto_one_time(aes_128_ecb, Key, Data, true).
decrypt(Key, Data) ->
crypto:crypto_one_time(aes_128_ecb, Key, Data, false).
hash(Data) ->
binary:encode_hex(crypto:hash(sha256, Data), uppercase).
+23
View File
@@ -0,0 +1,23 @@
import app/repository
import app/router
import app/web
import gleam/erlang/process
import mist
import sqlight
import wisp
pub fn main() {
wisp.configure_logger()
let secret_key_base = wisp.random_string(64)
use db <- sqlight.with_connection("gdrive.db")
let assert Ok(Nil) = repository.init(db)
let context = web.Context(db: db)
let handler = router.handle_request(_, context)
let assert Ok(_) =
wisp.mist_handler(handler, secret_key_base)
|> mist.new
|> mist.port(8000)
|> mist.start_https("cert.pem", "key.pem")
process.sleep_forever()
}
+5
View File
@@ -0,0 +1,5 @@
# run sshd
service sshd restart
# run the command
gleam run