add everything

main
vidner 2024-09-24 13:54:28 +08:00
parent ec2b1aaa9b
commit 0d9a248b4d
191 changed files with 21669 additions and 1 deletions

View File

@ -1,2 +1,35 @@
# gemastik-xvii-final
gemastik-xvii-final public repository
this repository provides the Docker environments for the gemastik-xvii final services.
## warmup-challenges
| challenges | author | category | writeup |
| ---------- | ------ | -------- |---------|
| bit-canvas | vidner | pwn | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| gift-card | deomkicer | crypto | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| go-green | vidner | rev | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| more-less | vidner | web | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
## final-challenges
| challenges | author | category | writeup |
| ---------- | ------ | -------- |---------|
| anti-alchemy | deomkicer | web | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| asmr | circleous | pwn | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| fjb | circleous | web-pwn | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| gift-voucher | deomkicer | crypto | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| gleam-drive | vidner | web-crypto | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| kode-viewer | vidner | web | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| tempest-poc | dummklloun | web | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
| ticketer | merricx | crypto | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
## how-to-contribute
contribute by sharing a link to your write-up. since services may have multiple vulnerabilities, different approaches are welcome.
### pull-request
```diff
- [contribute](https://github.com/vidner/gemastik-xvii-final/pulls)
+ [by yourname](https://link-to-writeup)
```
### discord
dm @vidner with the link to your write-up.

24
anti-alchemy/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM public.ecr.aws/docker/library/python:3.11-slim-buster
ARG PASSWORD
ENV DEBIAN_FRONTEND noninteractive
RUN echo root:${PASSWORD} | chpasswd
RUN apt-get update && apt-get install -y openssh-server curl nano
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN service ssh start
RUN useradd --user-group --system --create-home --no-log-init --shell /bin/bash ctf
WORKDIR /home/ctf/app
COPY requirements.txt .
RUN pip install -r ./requirements.txt && rm ./requirements.txt
COPY src/ .
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT [ "./entrypoint.sh" ]

106
anti-alchemy/db/dump.sql Normal file
View File

@ -0,0 +1,106 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 16.3
-- Dumped by pg_dump version 16.3
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: cwe; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.cwe (
id INTEGER PRIMARY KEY,
title TEXT,
description TEXT
);
CREATE TABLE public.users (
id SERIAL PRIMARY KEY,
username VARCHAR(255),
password VARCHAR(255)
);
CREATE TABLE public.salt (
id SERIAL PRIMARY KEY,
username VARCHAR(255),
salt VARCHAR(255)
);
--
-- Data for Name: cwe; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO public.cwe (id,title,description) VALUES
(20,'CWE-20: Improper Input Validation','The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.'),
(22,'CWE-22: Improper Limitation of a Pathname to a Restricted Directory (''Path Traversal'')','The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.'),
(73,'CWE-73: External Control of File Name or Path','The product allows user input to control or influence paths or file names that are used in filesystem operations.'),
(77,'CWE-77: Improper Neutralization of Special Elements used in a Command (''Command Injection'')','The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.'),
(78,'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')','The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.'),
(79,'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')','The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.'),
(88,'CWE-88: Improper Neutralization of Argument Delimiters in a Command (''Argument Injection'')','The product constructs a string for a command to be executed by a separate component in another control sphere, but it does not properly delimit the intended arguments, options, or switches within that command string.'),
(89,'CWE-89: Improper Neutralization of Special Elements used in an SQL Command (''SQL Injection'')','The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.'),
(94,'CWE-94: Improper Control of Generation of Code (''Code Injection'')','The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.'),
(119,'CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer','The product performs operations on a memory buffer, but it reads from or writes to a memory location outside the buffer''s intended boundary. This may result in read or write operations on unexpected memory locations that could be linked to other variables, data structures, or internal program data.');
INSERT INTO public.cwe (id,title,description) VALUES
(125,'CWE-125: Out-of-bounds Read','The product reads data past the end, or before the beginning, of the intended buffer.'),
(190,'CWE-190: Integer Overflow or Wraparound','The product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. This occurs when an integer value is incremented to a value that is too large to store in the associated representation. When this occurs, the value may become a very small or negative number.'),
(200,'CWE-200: Exposure of Sensitive Information to an Unauthorized Actor','The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.'),
(250,'CWE-250: Execution with Unnecessary Privileges','The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.'),
(266,'CWE-266: Incorrect Privilege Assignment','A product incorrectly assigns a privilege to a particular actor, creating an unintended sphere of control for that actor.'),
(269,'CWE-269: Improper Privilege Management','The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.'),
(276,'CWE-276: Incorrect Default Permissions','During installation, installed file permissions are set to allow anyone to modify those files.'),
(284,'CWE-284: Improper Access Control','The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.'),
(285,'CWE-285: Improper Authorization','The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.'),
(287,'CWE-287: Improper Authentication','When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.');
INSERT INTO public.cwe (id,title,description) VALUES
(295,'CWE-295: Improper Certificate Validation','The product does not validate, or incorrectly validates, a certificate.'),
(306,'CWE-306: Missing Authentication for Critical Function','The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.'),
(307,'CWE-307: Improper Restriction of Excessive Authentication Attempts','The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.'),
(311,'CWE-311: Missing Encryption of Sensitive Data','The product does not encrypt sensitive or critical information before storage or transmission.'),
(327,'CWE-327: Use of a Broken or Risky Cryptographic Algorithm','The product uses a broken or risky cryptographic algorithm or protocol.'),
(330,'CWE-330: Use of Insufficiently Random Values','The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.'),
(352,'CWE-352: Cross-Site Request Forgery (CSRF)','The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.'),
(362,'CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (''Race Condition'')','The product contains a code sequence that can run concurrently with other code, and the code sequence requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence that is operating concurrently.'),
(400,'CWE-400: Uncontrolled Resource Consumption','The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.'),
(416,'CWE-416: Use After Free','The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.');
INSERT INTO public.cwe (id,title,description) VALUES
(426,'CWE-426: Untrusted Search Path','The product searches for critical resources using an externally-supplied search path that can point to resources that are not under the product''s direct control.'),
(434,'CWE-434: Unrestricted Upload of File with Dangerous Type','The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.'),
(476,'CWE-476: NULL Pointer Dereference','The product dereferences a pointer that it expects to be valid but is NULL.'),
(502,'CWE-502: Deserialization of Untrusted Data','The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.'),
(522,'CWE-522: Insufficiently Protected Credentials','The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.'),
(611,'CWE-611: Improper Restriction of XML External Entity Reference','The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.'),
(732,'CWE-732: Incorrect Permission Assignment for Critical Resource','The product specifies permissions for a security-critical resource in a way that allows that resource to be read or modified by unintended actors.'),
(754,'CWE-754: Improper Check for Unusual or Exceptional Conditions','The product does not check or incorrectly checks for unusual or exceptional conditions that are not expected to occur frequently during day to day operation of the product.'),
(772,'CWE-772: Missing Release of Resource after Effective Lifetime','The product does not release a resource after its effective lifetime has ended, i.e., after the resource is no longer needed.'),
(787,'CWE-787: Out-of-bounds Write','The product writes data past the end, or before the beginning, of the intended buffer.');
INSERT INTO public.cwe (id,title,description) VALUES
(798,'CWE-798: Use of Hard-coded Credentials','The product contains hard-coded credentials, such as a password or cryptographic key.'),
(862,'CWE-862: Missing Authorization','The product does not perform an authorization check when an actor attempts to access a resource or perform an action.'),
(863,'CWE-863: Incorrect Authorization','The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.'),
(918,'CWE-918: Server-Side Request Forgery (SSRF)','The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.');
--
-- Name: cwe cwe_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
--
-- PostgreSQL database dump complete
--

View File

@ -0,0 +1,28 @@
services:
anti-alchemy:
hostname: anti-alchemy
restart: always
build:
args:
- PASSWORD=PASSWORD_11000
volumes:
- ./flag.txt:/flag.txt:ro
ports:
- "11000:5000"
- "11022:22"
depends_on:
- anti-alchemy-db
environment:
- DB_NAME=postgres
- DB_USER=postgres
- DB_PASS=password
- DB_HOST=anti-alchemy-db
- DB_PORT=5432
- SECRET_KEY=PASSWORD_11000
anti-alchemy-db:
image: public.ecr.aws/docker/library/postgres:16.3-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- ./db/dump.sql:/docker-entrypoint-initdb.d/init.sql

View File

@ -0,0 +1,6 @@
#!/bin/bash
/usr/sbin/sshd -D &
sleep 3
python3 ./misc/init_users.py
su ctf -c "gunicorn --bind 0.0.0.0:5000 --timeout 60 --workers 6 app:app"

1
anti-alchemy/flag.txt Normal file
View File

@ -0,0 +1 @@
PLACEHOLDER

View File

@ -0,0 +1,3 @@
Flask
gunicorn
psycopg2-binary

View File

@ -0,0 +1,83 @@
class ClauseBuilder:
def __init__(self) -> None:
self._queries = []
self._chars_to_sanitize = ["'", '"']
self._is_where_called = False
def _sanitize(self, q) -> str:
for c in self._chars_to_sanitize:
if c in q:
q = q.replace(c, c * 2)
return str(q).strip()
def _where(self, column, value, op) -> map:
if not self._is_where_called:
op = "WHERE"
self._is_where_called = True
return map(self._sanitize, [column, value, op])
def final(self) -> str:
q = " ".join(self._queries)
self.__init__()
return str(q).strip()
def order_by(self, column, value) -> None:
column, value = map(self._sanitize, [column, value])
self._queries.append("ORDER BY")
self._queries.append('"' + column + '"')
self._queries.append(value)
def select(self, table) -> None:
table = self._sanitize(table)
self._queries.append("SELECT")
self._queries.append("*")
self._queries.append("FROM")
self._queries.append('"' + table + '"')
def where(self, column, value, cmp="ILIKE", op="AND") -> None:
column, value, op = self._where(column, value, op)
self._queries.append(op)
self._queries.append('"' + column + '"')
if column == "id":
self._queries.append("=")
self._queries.append(str(int(value)))
elif cmp == "EQ":
self._queries.append("=")
self._queries.append("'" + value + "'")
else:
self._queries.append("ILIKE")
self._queries.append("'%" + value + "%'")
class QueryBuilder:
def __init__(self) -> None:
self._cb = ClauseBuilder()
self._obj = {}
self._defined_keys = ["table", "columns"]
self._sorting_values = ["asc", "desc"]
self._login_keys = ["username"]
def _order_by(self) -> None:
for value, column in self._obj.items():
if value in self._sorting_values:
self._cb.order_by(column, value)
break
def _select(self) -> None:
self._cb.select(self._obj["table"])
def _where(self) -> None:
for column, value in self._obj.items():
if column in self._defined_keys + self._sorting_values:
continue
elif column in self._login_keys:
self._cb.where(column, value, "EQ")
else:
self._cb.where(column, value)
def generate(self, obj) -> str:
self._obj = obj
self._select()
self._where()
self._order_by()
return self._cb.final()

119
anti-alchemy/src/app.py Normal file
View File

@ -0,0 +1,119 @@
from flask import Flask, render_template, session, redirect, url_for
from os import environ
from antialchemy import *
from helper import *
app = Flask(__name__)
app.config["SECRET_KEY"] = environ.get("SECRET_KEY", "SECRET_KEY")
app.config["PERMANENT_SESSION_LIFETIME"] = 3 * 60
qb = QueryBuilder()
@app.get("/api/flag")
@check_login_status
def flag():
if session["user"] == "admin":
try:
return make_resp(200, open("/flag.txt").read())
except:
return make_resp(500, "Flag not found, please contact problem setter")
return make_resp(401, "You need to log in as admin to get the flag")
@app.post("/api/login")
@check_request_body
def login(*args, **kwargs):
payload = kwargs.copy()
try:
conn = create_db_conn()
cur = conn.cursor()
cur.execute(
qb.generate(
{
"table": "users",
"username": payload["username"],
}
)
)
row = cur.fetchone()
if not row:
return make_resp(401, "Invalid username/password")
_, username, password_hash = row
password = payload.pop("password")
cur.execute(
qb.generate(
{
"table": "salt",
"username": username,
}
)
)
row = cur.fetchone()
if not row:
return make_resp(401, "Invalid username/password")
_, _, salt = row
if not check_password_hash(password, salt, password_hash):
return make_resp(401, "Invalid username/password")
session.clear()
session["user"] = username
return redirect(url_for("index"))
except Exception as e:
print(f"Exception: {e}")
return make_resp(400, "Bad Request")
finally:
cur.close()
conn.close()
@app.get("/api/logout")
@check_login_status
def logout():
session.clear()
return redirect(url_for("index"))
@app.post("/api/view")
@check_login_status
@check_request_body
def view(*args, **kwargs):
payload = kwargs.copy()
try:
conn = create_db_conn()
cur = conn.cursor()
cur.execute(qb.generate(payload | {"table": "cwe"}))
rows = cur.fetchall()
return make_resp(200, "OK", {"rows": rows})
except Exception as e:
print(f"Exception: {e}")
return make_resp(400, "Bad Request")
finally:
cur.close()
conn.close()
@app.get("/")
def index():
try:
if session["user"]:
return render_template("dashboard.html")
except:
pass
return render_template("login.html")
if __name__ == "__main__":
app.run(debug=True)

View File

@ -0,0 +1,73 @@
from flask import request, session
from functools import wraps
from hashlib import sha1
from json import dumps
from os import environ
from psycopg2 import connect
from string import printable
from time import sleep
def create_db_conn():
while True:
try:
return connect(
database=environ.get("DB_NAME", "postgres"),
user=environ.get("DB_USER", "postgres"),
password=environ.get("DB_PASS", "password"),
host=environ.get("DB_HOST", "localhost"),
port=environ.get("DB_PORT", "5432"),
)
except:
sleep(1)
def make_resp(status, message, data=None):
return {"status": status, "message": message, "data": data}
def generate_password_hash(password, salt):
return sha1((salt + password).encode()).hexdigest()
def check_password_hash(password, salt, password_hash):
return generate_password_hash(password, salt) == password_hash
def check_login_status(f):
@wraps(f)
def inner(*args, **kwargs):
try:
session["user"]
return f(*args, **kwargs)
except Exception as e:
print(f"Exception: {e}")
return make_resp(401, "Unauthorized")
return inner
def check_request_body(f):
check_blist = lambda s: all(x not in s.lower() for x in ["pg_"])
check_wlist = lambda s: all(c in printable for c in s)
@wraps(f)
def inner(*args, **kwargs):
try:
data = request.get_json()
data = {k: v for k, v in data.items() if data.get(k)}
dd = dumps(data, separators=(",", ":"))
assert len(dd) < 256 and check_blist(dd), "Bad payload"
for item in data.items():
assert all(map(check_wlist, item)), "Bad payload"
kwargs.update(data)
return f(*args, **kwargs)
except Exception as e:
print(f"Exception: {e}")
return make_resp(400, "Bad Request")
return inner

View File

@ -0,0 +1,28 @@
import sys
sys.path += [".", ".."]
from helper import create_db_conn
from os import environ
print("Getting environment variables...")
print(environ.get("SECRET_KEY", "SECRET_KEY"))
print()
print("Getting rows of users and salt...")
conn = create_db_conn()
cur = conn.cursor()
cur.execute("SELECT * FROM users")
rows = cur.fetchall()
for row in rows:
print(row)
cur.execute("SELECT * FROM salt")
rows = cur.fetchall()
for row in rows:
print(row)
print()
print("Done")
cur.close()
conn.close()

View File

@ -0,0 +1,25 @@
import sys
sys.path += [".", ".."]
from helper import create_db_conn, generate_password_hash
from os import environ, urandom
conn = create_db_conn()
cur = conn.cursor()
users = [
("admin", environ.get("SECRET_KEY", "SECRET_KEY"), urandom(8).hex()),
("gemastik", "P@ssw0rd", urandom(8).hex()),
]
for username, password, salt in users:
cur.execute(
"INSERT INTO users (username, password) VALUES (%s, %s)",
(username, generate_password_hash(password, salt)),
)
cur.execute("INSERT INTO salt (username, salt) VALUES (%s, %s)", (username, salt))
conn.commit()
cur.close()
conn.close()

View File

@ -0,0 +1 @@
.sortable-column,button,strong{cursor:pointer}a,strong{color:#44509e}#pagination,#search{border:var(--debug);gap:4px;width:300px;padding:4px}*{box-sizing:border-box}body{font-family:Arial,Helvetica,sans-serif}table{width:100%;border-collapse:collapse}td,th{padding:.5rem;text-align:left;border-bottom:1px solid #ddd}td{height:6rem}a{text-decoration:none;font-weight:700}button:disabled{cursor:not-allowed}.container{height:inherit;display:flex;flex-direction:column;justify-content:space-between;border:var(--debug)}#search label,.atas{flex-direction:row;display:flex}.sortable th:first-child{width:48px}.sortable th:nth-child(2){width:96px}.sortable th:nth-child(3){width:25%}.sortable-column:hover{background-color:#eee}.sortable-column.no-sort{pointer-events:none}.sortable-column::after,.sortable-column::before{transition:color .2s ease-in-out;font-size:1.2rem;color:transparent}.sortable-column:hover::after{color:inherit}.sortable-column::after{content:"\025B8"}.sortable-column.dir-d::after{content:"\025BE"}.sortable-column.dir-u::after{content:"\025B4"}.atas{gap:1rem;justify-content:space-between;border:var(--debug)}.bawah{margin-top:5px;padding:5px;border:var(--debug);height:auto}@media (max-width:600px){.atas{flex-direction:column;align-items:center}#search{margin-top:1rem}}#search{display:flex;flex-direction:column}#search label{justify-content:flex-end;gap:8px}#search input{width:200px}#search button{align-self:flex-end;width:200px}#pagination{display:flex;flex-direction:row;justify-content:space-between;align-content:end}#pagination button{width:80px;height:1.5rem;margin-top:auto}#pagination p{margin:auto 0 0;padding:2px}

View File

@ -0,0 +1 @@
const step=10;var rawResp={},currentData=[],totalData=[],currentPage=0,totalPage=Math.floor((rawResp.length+9)/10),pload={};function fillBody(e,t){let a=document.getElementById("fillable-body");a.innerHTML="",e.forEach((e,n)=>{let r=document.createElement("tr"),l=document.createElement("td"),i=document.createElement("td"),c=document.createElement("td"),o=document.createElement("td"),s=document.createElement("a");l.textContent=n+t,s.href=`https://cwe.mitre.org/data/definitions/${e[0]}.html`,s.textContent=e[0],c.textContent=e[1],o.textContent=e[2],i.appendChild(s),r.appendChild(l),r.appendChild(i),r.appendChild(c),r.appendChild(o),a.appendChild(r)})}function fillPage(e,t){let a=document.getElementById("page");a.innerHTML=`Page ${e}/${t}`}async function sendReq(e,t,a){try{let n=await fetch(t,{method:e,headers:{"Content-Type":"application/json"},body:a});var{status:r,message:l,data:i}=rawResp=await n.json();200===r?(rawResp=i.rows,totalPage=Math.floor((rawResp.length+9)/10),currentPage=1,currentData=rawResp.slice((currentPage-1)*10,10*currentPage),fillPage(currentPage,totalPage),fillBody(currentData,(currentPage-1)*10+1),toggleButton()):(console.error("Failed to fetch data:",l),alert(l))}catch(c){console.error("Error fetching data:",c)}}async function search(){let e=document.getElementById("cwe-id").value,t=document.getElementById("title").value,a=document.getElementById("description").value;try{await sendReq("POST","/api/view",JSON.stringify(pload={id:e,title:t,description:a}))}catch(n){console.log(n)}}async function sort(e){let t=document.getElementsByClassName("sortable-column"),a=["id","title","description"];removeNeighborClass(e),t[e].classList.contains("dir-d")?(t[e].classList.remove("dir-d"),t[e].classList.add("dir-u"),delete pload.asc,pload={...pload,desc:a[e]}):(t[e].classList.remove("dir-u"),t[e].classList.add("dir-d"),delete pload.desc,pload={...pload,asc:a[e]});try{await sendReq("POST","/api/view",JSON.stringify(pload))}catch(n){console.log(n)}}function toggleButton(){document.getElementById("prev-page").disabled=1===currentPage,document.getElementById("next-page").disabled=currentPage===totalPage}function removeNeighborClass(e){let t=document.getElementsByClassName("sortable-column");t[(e+1)%3].classList.remove("dir-d"),t[(e+1)%3].classList.remove("dir-u"),t[(e+2)%3].classList.remove("dir-d"),t[(e+2)%3].classList.remove("dir-u")}document.getElementById("prev-page").addEventListener("click",function(e){e.preventDefault(),currentPage--,totalPage=Math.floor((rawResp.length+9)/10),currentData=rawResp.slice((currentPage-1)*10,10*currentPage),fillPage(currentPage,totalPage),fillBody(currentData,(currentPage-1)*10+1),toggleButton()}),document.getElementById("next-page").addEventListener("click",function(e){e.preventDefault(),currentPage++,totalPage=Math.floor((rawResp.length+9)/10),currentData=rawResp.slice((currentPage-1)*10,10*currentPage),fillPage(currentPage,totalPage),fillBody(currentData,(currentPage-1)*10+1),toggleButton()}),document.getElementById("access-flag").addEventListener("click",async function(e){try{let t=await fetch("/api/flag"),a=await t.json();alert(a.message)}catch(n){console.log(n)}}),document.getElementById("logout").addEventListener("click",async function(e){try{await fetch("/api/logout"),window.location.reload()}catch(t){console.log(t)}}),async function(){try{await sendReq("POST","/api/view",JSON.stringify(pload))}catch(e){console.log(e)}}();

1
anti-alchemy/src/static/login.min.css vendored Normal file
View File

@ -0,0 +1 @@
.form-group button{width:100%}.container{max-width:640px;position:absolute;top:50%;left:50%;-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}h1{text-align:center}

1
anti-alchemy/src/static/login.min.js vendored Normal file
View File

@ -0,0 +1 @@
document.getElementById("form-submit").addEventListener("click",async function(e){e.preventDefault();let t=document.getElementById("form-username").value,a=document.getElementById("form-password").value;if(!t||!a){alert("Username/password cannot be empty");return}try{let n=await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:a})}),o=await n.json();alert(o.message)}catch(r){console.log(r),window.location.reload()}});

View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>List of Top CWE - Common Weakness Enumeration</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="{{ url_for('static', filename='dashboard.min.css') }}" />
</head>
<body>
<div class="container">
<div class="atas">
<form action="javascript:search()" id="search">
<label for="code">CWE ID<input type="number" name="CWE ID" id="cwe-id" min="0" autofocus /></label>
<label for="title">Title<input type="text" name="Title" id="title" maxlength="255" /></label>
<label for="description">Description<input type="text" name="Description" id="description"
maxlength="255" /></label>
<button type="submit">Search</button>
</form>
<div style="margin: auto; text-align: center;">
<p>
Welcome to <span style="font-weight: bold">List of Top CWE</span>,
{{ session["user"] }}!
</p>
<p>
Click here to access admin <strong id="access-flag">flag</strong> or
<strong id="logout">logout</strong>.
</p>
</div>
<form id="pagination">
<button type="submit" id="prev-page">Prev</button>
<p id="page"></p>
<button type="submit" id="next-page">Next</button>
</form>
</div>
<div class="bawah">
<table class="sortable">
<thead>
<tr>
<th>#</th>
<th class="sortable-column" onclick="javascript:sort(0)">
CWE ID
</th>
<th class="sortable-column" onclick="javascript:sort(1)">
Title
</th>
<th class="sortable-column" onclick="javascript:sort(2)">
Description
</th>
</tr>
</thead>
<tbody id="fillable-body"></tbody>
</table>
</div>
</div>
</body>
<script src="{{ url_for('static', filename='dashboard.min.js') }}"></script>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login - Common Weakness Enumeration</title>
<link rel="icon" href="data:," />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
<link rel="stylesheet" href="{{ url_for('static', filename='login.min.css') }}">
</head>
<body>
<div class="container">
<h1>Login</h1>
<form>
<div class="form-group pt-3">
<input type="text" class="form-control" id="form-username" placeholder="Username" />
</div>
<div class="form-group pt-3">
<input type="password" class="form-control" id="form-password" placeholder="Password" />
</div>
<div class="form-group pt-3">
<button type="submit" class="btn btn-primary" id="form-submit">
Submit
</button>
</div>
</form>
</div>
<script src="{{ url_for('static', filename='login.min.js') }}"></script>
</body>
</html>

40
asmr/Dockerfile Normal file
View File

@ -0,0 +1,40 @@
FROM public.ecr.aws/docker/library/ubuntu:24.04@sha256:dfc10878be8d8fc9c61cbff33166cb1d1fe44391539243703c72766894fa834a
ARG PASSWORD
ENV DEBIAN_FRONTEND noninteractive
RUN echo root:${PASSWORD} | chpasswd
RUN apt-get update && \
apt-get install -y --no-install-recommends \
openssh-server \
xinetd \
curl \
python3 \
python3-pip \
python3-setuptools && \
rm -rf /var/cache/apt/archives /var/lib/apt/lists
RUN pip3 install --break-system-packages unicorn
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN service ssh start
RUN useradd -m ctf
WORKDIR /ctf
RUN echo "Connection blocked" > /etc/banner_fail
COPY ctf.xinetd /etc/xinetd.d/ctf
COPY ./src/asmr.py ./
COPY ./run.sh ./
COPY ./start.sh ./
RUN touch /flag.txt
RUN chmod -R 755 /ctf
RUN chmod +x /ctf/start.sh
CMD ./start.sh
EXPOSE 8000

16
asmr/ctf.xinetd Normal file
View File

@ -0,0 +1,16 @@
service ctf
{
disable = no
socket_type = stream
protocol = tcp
wait = no
user = root
type = UNLISTED
port = 8000
bind = 0.0.0.0
server = /bin/sh
server_args = /ctf/run.sh
banner_fail = /etc/banner_fail
# safety options
rlimit_cpu = 1 # the maximum number of CPU seconds that the service may use
}

11
asmr/docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
services:
asmr:
hostname: asmr
restart: always
build:
context: .
args:
- PASSWORD=root
ports:
- "15000:8000"
- "15022:22"

1
asmr/run.sh Normal file
View File

@ -0,0 +1 @@
su ctf -c "timeout 60s python3 -u /ctf/asmr.py"

145
asmr/src/asmr.py Normal file
View File

@ -0,0 +1,145 @@
import ctypes
from ctypes.util import find_library
from random import randbytes
from typing import Any
from unicorn import Uc, UcError, x86_const
from unicorn.unicorn_const import UC_ARCH_X86, UC_HOOK_INSN, UC_MODE_64
MAX_NOTES = 16
engine = Uc(UC_ARCH_X86, UC_MODE_64)
notes = [0 for _ in range(MAX_NOTES)]
libc = ctypes.CDLL(find_library("c"))
def find_empty():
for i in range(MAX_NOTES):
if notes[i] == 0:
return i
return -1
def syscall(uc: Uc, data: Any):
rax = uc.reg_read(x86_const.UC_X86_REG_RAX)
rdi = uc.reg_read(x86_const.UC_X86_REG_RDI)
rsi = uc.reg_read(x86_const.UC_X86_REG_RSI)
rdx = uc.reg_read(x86_const.UC_X86_REG_RDX)
def create(size):
idx = find_empty()
if idx == -1:
uc.reg_write(x86_const.UC_X86_REG_RAX, -1)
return
notes[idx] = libc.malloc(size)
uc.reg_write(x86_const.UC_X86_REG_RAX, idx)
return
def edit(idx, buf, size):
if notes[idx] == 0:
uc.reg_write(x86_const.UC_X86_REG_RAX, -1)
return
ubuf = uc.mem_read(buf, size)
ctypes.memmove(
notes[idx],
ctypes.create_string_buffer(bytes(ubuf)),
size,
)
uc.reg_write(x86_const.UC_X86_REG_RAX, 0)
return
def delete(idx):
if notes[idx] == 0:
uc.reg_write(x86_const.UC_X86_REG_RAX, -1)
return
libc.free(notes[idx])
notes[idx] = 0
uc.reg_write(x86_const.UC_X86_REG_RAX, 0)
return
def view(idx, buf, size):
if notes[idx] == 0:
uc.reg_write(x86_const.UC_X86_REG_RAX, -1)
return
libc.puts(notes[idx])
kbuf = ctypes.string_at(notes[idx], size)
uc.mem_write(buf, kbuf)
uc.reg_write(x86_const.UC_X86_REG_RAX, 0)
return
def dbg():
print(f"RAX {uc.reg_read(x86_const.UC_X86_REG_RAX):#x}")
print(f"RBX {uc.reg_read(x86_const.UC_X86_REG_RBX):#x}")
print(f"RCX {uc.reg_read(x86_const.UC_X86_REG_RCX):#x}")
print(f"RDX {uc.reg_read(x86_const.UC_X86_REG_RDX):#x}")
print(f"RDI {uc.reg_read(x86_const.UC_X86_REG_RDI):#x}")
print(f"RSI {uc.reg_read(x86_const.UC_X86_REG_RSI):#x}")
print(f"R8 {uc.reg_read(x86_const.UC_X86_REG_R8):#x}")
print(f"R9 {uc.reg_read(x86_const.UC_X86_REG_R9):#x}")
print(f"R10 {uc.reg_read(x86_const.UC_X86_REG_R10):#x}")
print(f"R11 {uc.reg_read(x86_const.UC_X86_REG_R11):#x}")
print(f"R12 {uc.reg_read(x86_const.UC_X86_REG_R12):#x}")
print(f"R13 {uc.reg_read(x86_const.UC_X86_REG_R13):#x}")
print(f"R14 {uc.reg_read(x86_const.UC_X86_REG_R14):#x}")
print(f"R15 {uc.reg_read(x86_const.UC_X86_REG_R15):#x}")
print(f"RBP {uc.reg_read(x86_const.UC_X86_REG_RBP):#x}")
print(f"RSP {uc.reg_read(x86_const.UC_X86_REG_RSP):#x}")
print(f"RIP {uc.reg_read(x86_const.UC_X86_REG_RIP):#x}")
if rax == 1:
create(rdi)
elif rax == 2:
edit(rdi, rsi, rdx)
elif rax == 3:
delete(rdi)
elif rax == 4:
view(rdi, rsi, rdx)
elif rax == 5:
dbg()
else:
uc.emu_stop()
raise ValueError("invalid syscall number")
def main():
code_sz = 8 << 20 # 8MB
base = 0x400000
engine.mem_map(base, code_sz)
stack_sz = 2 << 20 # 2MB
stack = int.from_bytes(randbytes(5), "big") & ~0xFFF
stack = 0x7FF0_00000000 | stack
engine.mem_map(stack, stack_sz)
engine.hook_add(UC_HOOK_INSN, syscall, None, 1, 0,
x86_const.UC_X86_INS_SYSCALL)
while True:
try:
code = input("Code (in hex): ")
sc = bytes.fromhex(code)[:code_sz]
break
except ValueError:
pass
engine.reg_write(x86_const.UC_X86_REG_RSP, stack + stack_sz - 0x1000)
engine.mem_write(base, sc)
try:
engine.emu_start(base, base + code_sz, 30_000) # 30s
except (ValueError, UcError) as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()

3
asmr/start.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/sh
/usr/sbin/sshd -D &
/usr/sbin/xinetd -dontfork

31
bit-canvas/Dockerfile Normal file
View File

@ -0,0 +1,31 @@
FROM public.ecr.aws/docker/library/ubuntu:22.04
ARG PASSWORD
ENV DEBIAN_FRONTEND noninteractive
RUN echo root:${PASSWORD} | chpasswd
RUN apt-get update && apt-get install -y openssh-server xinetd cmake gcc curl
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN service ssh start
RUN useradd -m ctf
WORKDIR /ctf
RUN echo "Connection blocked" > /etc/banner_fail
COPY ctf.xinetd /etc/xinetd.d/ctf
COPY ./src/main.c ./
COPY ./run.sh ./
COPY ./start.sh ./
RUN gcc /ctf/main.c -o /ctf/main
RUN touch /flag.txt
RUN chmod -R 755 /ctf
RUN chmod +x /ctf/start.sh
CMD ./start.sh
EXPOSE 8000

16
bit-canvas/ctf.xinetd Normal file
View File

@ -0,0 +1,16 @@
service ctf
{
disable = no
socket_type = stream
protocol = tcp
wait = no
user = root
type = UNLISTED
port = 8000
bind = 0.0.0.0
server = /bin/sh
server_args = /ctf/run.sh
banner_fail = /etc/banner_fail
# safety options
rlimit_cpu = 1 # the maximum number of CPU seconds that the service may use
}

View File

@ -0,0 +1,11 @@
services:
bit-canvas:
hostname: bit-canvas
restart: always
build:
context: .
args:
- PASSWORD=root
ports:
- "20000:8000"
- "20022:22"

1
bit-canvas/run.sh Normal file
View File

@ -0,0 +1 @@
su ctf -c "timeout 60s /ctf/main"

112
bit-canvas/src/main.c Normal file
View File

@ -0,0 +1,112 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
int main() {
setbuf(stdout, NULL);
int choice = 0;
int debug = 0;
int size = 16;
int canvas[16] = {0};
puts("bit-canvas @ gemastik-xvii");
puts("1. draw");
puts("2. clear");
puts("3. resize");
puts("4. admin");
puts("5. exit");
while (1) {
printf("choice? ");
scanf("%d", &choice);
switch (choice) {
case 1:
int n = 0;
int x = 0;
int y = 0;
printf("how many bits? ");
scanf("%d", &n);
if (n > (size * size * 2)) {
puts("too many bits");
return 1;
}
for (int i = 0; i < n; i++) {
printf("where x y? ");
scanf("%d %d", &x, &y);
bool prev = 0;
bool next = 0;
prev = canvas[x] >> y & 1;
next = prev ^ 1;
canvas[x] ^= 1 << y;
if (debug) printf("[DEBUG] x: %d, y: %d, prev: %d, next: %d\n", x, y, prev, next);
}
break;
case 2:
for (int i = 0; i < size; i++) {
for (int j = 0; j < size * 2; j++) {
canvas[i] = 0;
}
}
break;
case 3:
printf("size? ");
scanf("%d", &size);
break;
case 4:
FILE *f = fopen("/flag.txt", "r");
if (f == NULL) {
puts("flag not found");
return 1;
}
char flag[43];
char buffer[43];
fgets(flag, sizeof(flag), f);
fclose(f);
printf("password? ");
scanf("%s", buffer);
if (!strcmp(buffer, flag)) {
debug = 1;
puts("debug mode enabled");
} else {
puts("invalid password");
}
break;
case 5:
puts("bye");
return 0;
default:
printf("invalid choice %d\n", choice);
return 1;
}
for (int i = 0; i < size; i++) {
for (int j = 0; j < size * 2; j++) {
printf("%d", canvas[i] >> j & 1);
}
puts("");
}
}
return 0;
}

3
bit-canvas/start.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/sh
/usr/sbin/sshd -D &
/usr/sbin/xinetd -dontfork

4
fjb/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
**/node_modules/
**/dist/
**/*.so
**/*.o

5
fjb/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store

67
fjb/Dockerfile Normal file
View File

@ -0,0 +1,67 @@
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml /app/
RUN pnpm install --frozen-lockfile
FROM base AS build
COPY ./frontend/ /app/
RUN pnpm run build
FROM ghcr.io/circleous/httpd:latest@sha256:8a353b1208a4871233845d9a84eb4f40c4581a7acaed505de4ccdd52c8d56615
WORKDIR /app
COPY ./kauth /app/kauth/
RUN set eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
openssh-server \
pkg-config \
gcc \
curl \
make \
lua5.3 \
liblua5.3-dev \
libsodium-dev \
libsqlite3-dev \
luarocks \
; \
luarocks-5.3 install lua-cjson; \
luarocks-5.3 install lsqlite3; \
luarocks-5.3 install bcrypt; \
cd kauth; \
luarocks-5.3 make; \
cd ..; \
rm -rf kauth;\
apt-get remove -y \
pkg-config \
gcc \
make \
lua5.3 \
liblua5.3-dev; \
echo root:${PASSWORD} | chpasswd \
echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config; \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config; \
service ssh start; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false;
COPY httpd-foreground /usr/local/bin/
COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf
COPY --chown=www-data:www-data ./fjb.db /app/data/fjb.db
COPY --from=build /app/dist /usr/local/apache2/htdocs
COPY ./src/ /app/lua/
RUN sed -ri "s/SECRETSECRETSECRETSECRETSECRETSE/$(openssl rand -hex 16)/g" /app/lua/secret.lua && \
chmod 644 /app/lua/secret.lua
EXPOSE 80
CMD ["httpd-foreground"]

9
fjb/docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
services:
fjb:
build:
context: .
args:
- PASSWORD=root
ports:
- 17000:80
- 17022:22

BIN
fjb/fjb.db Normal file

Binary file not shown.

24
fjb/frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

8
fjb/frontend/README.md Normal file
View File

@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

View File

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View File

@ -0,0 +1,39 @@
import js from "@eslint/js";
import globals from "globals";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
export default [
{ ignores: ["dist"] },
{
files: ["**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
sourceType: "module",
},
},
settings: { react: { version: "18.3" } },
plugins: {
react,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs["jsx-runtime"].rules,
...reactHooks.configs.recommended.rules,
"react/jsx-no-target-blank": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"react/prop-types": "off",
},
},
];

13
fjb/frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FJB</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}

49
fjb/frontend/package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-query": "^5.56.2",
"@tanstack/react-router": "^1.58.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"ky": "^1.7.2",
"lucide-react": "^0.441.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@tanstack/router-devtools": "^1.58.3",
"@tanstack/router-plugin": "^1.58.4",
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.12",
"vite": "^5.4.1"
}
}

4683
fjb/frontend/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f38ba8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-store"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"/><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"/><path d="M2 7h20"/><path d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"/></svg>

After

Width:  |  Height:  |  Size: 690 B

View File

@ -0,0 +1,238 @@
import { useCallback, useState, useEffect } from "react";
import {
Loader2,
ShoppingBag,
ShoppingCart,
LogOut,
LogIn,
Plus,
Eye,
EyeOff,
} from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Link, useNavigate, useLocation } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import useAuth from "@/hooks/useAuth";
import router from "@/lib/router";
import httpClient from "@/lib/httpClient";
function AuthButton() {
const navigate = useNavigate();
const { isPending, authSuccess } = useAuth();
const queryClient = useQueryClient();
const logout = useCallback(
(e) => {
e.preventDefault();
document.cookie = "session=";
queryClient.invalidateQueries({ queryKey: ["user"] });
router.invalidate();
navigate({ to: "/login" });
},
[queryClient, navigate],
);
if (isPending) {
return (
<Button className="flex items-center gap-2" disabled>
<Loader2 className="h-5 w-5 animate-spin" />
<span className="hidden sm:inline">Loading</span>
</Button>
);
}
if (authSuccess) {
return (
<Button className="flex items-center gap-2" onClick={logout}>
<LogOut className="h-5 w-5" />
<span className="hidden sm:inline">Logout</span>
</Button>
);
}
return (
<Link to="/login">
<Button className="flex items-center gap-2">
<LogIn className="h-5 w-5" />
<span className="hidden sm:inline">Login</span>
</Button>
</Link>
);
}
export default function Navigation() {
const [data, setData] = useState({
name: "",
description: "",
valuable: "",
price: "",
});
const [valueHidden, setValueHidden] = useState(true);
const { pathname } = useLocation();
const [dialogClass, setDialogClass] = useState(null);
const [open, setOpen] = useState(false);
const { isSuccess: authSuccess } = useAuth();
const queryClient = useQueryClient();
const { isPending, mutate } = useMutation({
mutationKey: ["post-add-item"],
mutationFn: (data) => httpClient.post("/api/catalog", { json: data }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["items"] });
setOpen(false);
},
});
useEffect(() => {
if (pathname === "/" && authSuccess) {
setDialogClass("flex items-center gap-2");
} else {
setDialogClass("hidden");
}
}, [pathname, authSuccess]);
const handleChange = (event) => {
const { name, value } = event.target;
setData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (event) => {
event.preventDefault();
mutate(data);
};
const toggleValuableVisibility = () => {
setValueHidden((prev) => !prev);
};
return (
<div className="flex justify-between items-center mb-6 flex-wrap gap-4">
<Link to="/">
<h1 className="text-3xl font-bold">FJB</h1>
</Link>
<div className="flex gap-2">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className={dialogClass}>
<Plus className="h-5 w-5" />
<span className="hidden sm:inline">Add Item</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add New Item</DialogTitle>
<DialogDescription>
Enter the details of the item you want to add to the
marketplace.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input
id="name"
name="name"
value={data.name}
onChange={handleChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Textarea
id="description"
name="description"
value={data.description}
onChange={handleChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="valuable" className="text-right">
Valuable
</Label>
<div className="col-span-3 flex">
<Input
id="valuable"
name="valuable"
type={valueHidden ? "text" : "password"}
value={data.valuable}
onChange={handleChange}
className="flex-grow"
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={toggleValuableVisibility}
className="ml-2"
>
{valueHidden ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="price" className="text-right">
Price
</Label>
<Input
id="price"
name="price"
value={data.price}
onChange={handleChange}
type="number"
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending && (
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-primary border-r-transparent" />
)}
Submit
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Link to="/">
<Button variant="outline" className="flex items-center gap-2">
<ShoppingBag className="h-5 w-5" />
<span className="hidden sm:inline">Marketplace</span>
</Button>
</Link>
<Link to="/cart">
<Button variant="outline" className="flex items-center gap-2">
<ShoppingCart className="h-5 w-5" />
<span className="hidden sm:inline">Cart</span>
</Button>
</Link>
<AuthButton />
</div>
</div>
);
}

View File

@ -0,0 +1,34 @@
import * as React from "react"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
...props
}) {
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,48 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
(<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />)
);
})
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,50 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
{...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} />
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props} />
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props} />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,94 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props} />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}>
{children}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}) => (
<div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props} />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
(<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props} />)
);
})
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,16 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,86 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props} />
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props} />
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
{...props} />
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props} />
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props} />
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props} />
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props} />
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,41 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props} />
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props} />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props} />
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
(<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props} />)
);
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props} />
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,9 @@
const PAGE = {
LOGIN: 0,
MARKETPLACE: 1,
CART: 2,
};
const TOKEN_KEY = "TOKEN";
export { PAGE, TOKEN_KEY };

View File

@ -0,0 +1,9 @@
import httpClient from "@/lib/httpClient";
import { useQuery } from "@tanstack/react-query";
export default function useAuth() {
return useQuery({
queryKey: ["user"],
queryFn: () => httpClient.get("/api/user").json(),
});
}

View File

@ -0,0 +1,67 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -0,0 +1,9 @@
import ky from "ky";
export default ky.create({
credentials: "include",
retry: {
limit: 1,
statusCodes: [500, 504],
},
});

View File

@ -0,0 +1,12 @@
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "@/routeTree.gen";
const router = createRouter({
routeTree,
context: {
auth: null,
queryClient: null,
},
});
export default router;

View File

@ -0,0 +1,6 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

27
fjb/frontend/src/main.jsx Normal file
View File

@ -0,0 +1,27 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { RouterProvider } from "@tanstack/react-router";
import router from "@/lib/router";
import "./index.css";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
createRoot(document.getElementById("root")).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>,
);

View File

@ -0,0 +1,159 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { Loader2, Trash2 } from "lucide-react";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import httpClient from "@/lib/httpClient";
import useAuth from "@/hooks/useAuth";
export default function CartCheckout() {
const auth = useAuth();
const queryClient = useQueryClient();
const { isLoading, data: cart } = useQuery({
queryKey: ["cart"],
queryFn: () => httpClient("/api/cart").json(),
enabled: auth.isSuccess,
});
const { mutate } = useMutation({
mutationKey: ["delete-cart"],
mutationFn: (id) =>
httpClient
.delete("/api/cart", {
searchParams: new URLSearchParams([["id", id]]),
})
.json(),
onSuccess: async () => {
queryClient.invalidateQueries({
queryKey: ["cart"],
});
},
});
const calculateTotal = () => {
return cart ? cart.items.reduce((total, item) => total + item.price, 0) : 0;
};
const handleDelete = useCallback(
(index) => {
mutate(index);
},
[mutate],
);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
return (
<>
{!cart || !cart.items || cart.items.length === 0 ? (
<div className="flex justify-center items-center">
Your cart is empty
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="md:col-span-2">
<CardHeader>
<CardTitle>Your Items</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Item</TableHead>
<TableHead>Price</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{cart.items.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.name}</TableCell>
<TableCell>${item.price.toFixed(2)}</TableCell>
<TableCell className="text-right">
<Button
variant="destructive"
onClick={() => handleDelete(item.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card className="h-fit">
<CardHeader>
<CardTitle>Order Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span>Subtotal</span>
<span>${calculateTotal().toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>Shipping</span>
<span>Free</span>
</div>
<div className="flex justify-between font-bold">
<span>Total</span>
<span>${calculateTotal().toFixed(2)}</span>
</div>
</div>
</CardContent>
<CardFooter>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-block w-full">
<Button
variant="outline"
disabled
className="cursor-not-allowed w-full"
>
{/* TODO: create checkout */}
<span>Proceed to Payment</span>
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
<p>This action is currently unavailable</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardFooter>
</Card>
</div>
)}
</>
);
}

View File

@ -0,0 +1,142 @@
import { useRef } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import httpClient from "@/lib/httpClient";
export default function LoginRegister() {
const usernameRef = useRef();
const passwordRef = useRef();
const queryClient = useQueryClient();
const navigate = useNavigate();
const { isLoading, mutate } = useMutation({
mutationKey: ["login"],
mutationFn: (data) => httpClient.post("/api/login", { json: data }).json(),
onSettled(data, err) {
if (err || data?.error) {
queryClient.setQueryData(["user"], null);
return;
}
queryClient.setQueryData(["user"], data);
navigate({ to: "/" });
},
retry: false,
});
async function onSubmit(event) {
event.preventDefault();
const username = usernameRef.current.value;
const password = passwordRef.current.value;
mutate({ username, password });
}
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Account</CardTitle>
<CardDescription>Login or create a new account.</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="login" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
</TabsList>
<TabsContent value="login">
<form onSubmit={onSubmit}>
<div className="grid gap-2">
<div className="grid gap-1">
<Label htmlFor="username">Username</Label>
<Input
id="username"
placeholder=""
type="username"
autoCapitalize="none"
autoComplete="username"
autoCorrect="off"
disabled={isLoading}
ref={usernameRef}
/>
</div>
<div className="grid gap-1">
<Label htmlFor="password">Password</Label>
<Input
id="password"
placeholder="********"
type="password"
autoCapitalize="none"
autoComplete="current-password"
disabled={isLoading}
ref={passwordRef}
/>
</div>
<Button disabled={isLoading}>
{isLoading && (
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-primary border-r-transparent" />
)}
Sign In
</Button>
</div>
</form>
</TabsContent>
<TabsContent value="register">
<form onSubmit={onSubmit}>
<div className="grid gap-2">
<div className="grid gap-1">
<Label htmlFor="username">Username</Label>
<Input
id="username"
placeholder=""
type="username"
autoCapitalize="none"
autoComplete="username"
autoCorrect="off"
disabled={isLoading}
/>
</div>
<div className="grid gap-1">
<Label htmlFor="password">Password</Label>
<Input
id="password"
placeholder="********"
type="password"
autoCapitalize="none"
autoComplete="new-password"
disabled={isLoading}
/>
</div>
<div className="grid gap-1">
<Label htmlFor="confirm-password">Confirm Password</Label>
<Input
id="confirm-password"
placeholder="********"
type="password"
autoCapitalize="none"
autoComplete="new-password"
disabled={isLoading}
/>
</div>
<Button disabled={isLoading}>
{isLoading && (
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-primary border-r-transparent" />
)}
Create Account
</Button>
</div>
</form>
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,94 @@
import { useCallback } from "react";
import { formatDistanceToNow } from "date-fns";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ShoppingBasket, Loader2 } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import httpClient from "@/lib/httpClient";
export default function MarketplaceListing() {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ["items"],
queryFn: () => httpClient.get("/api/catalog").json(),
});
const { mutate } = useMutation({
mutationKey: ["post-cart"],
mutationFn: (data) => httpClient.post("/api/cart", { json: data }).json(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] });
},
});
const handleAddToCart = useCallback(
(id) => {
mutate({ item: id });
},
[mutate],
);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
if (!data) {
return (
<div className="flex justify-center items-center h-screen">
No data available
</div>
);
}
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{data.items.map((item) => (
<Card key={item.id} className="flex flex-col">
<CardHeader>
<CardTitle>{item.name}</CardTitle>
<CardDescription>{item.description}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<p className="text-2xl font-bold">${item.price.toFixed(2)}</p>
<p className="text-sm text-gray-500">
Posted {formatDistanceToNow(new Date(item.date_posted))} ago
</p>
</CardContent>
<CardFooter className="flex justify-between items-center">
<Badge variant="secondary">Owner ID: {item.owner}</Badge>
<Button
className="flex items-center gap-2"
onClick={(e) => {
e.preventDefault();
console.log(e.target);
e.target.setAttribute("disabled", "true");
handleAddToCart(item.id);
setTimeout(() => {
e.target.removeAttribute("disabled");
}, 1000);
}}
>
<ShoppingBasket className="h-5 w-5" />
<span className="hidden sm:inline">Add to Cart</span>
</Button>
</CardFooter>
</Card>
))}
</div>
</>
);
}

View File

@ -0,0 +1,138 @@
/* prettier-ignore-start */
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file is auto-generated by TanStack Router
import { createFileRoute } from '@tanstack/react-router'
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as CartImport } from './routes/cart'
// Create Virtual Routes
const LoginLazyImport = createFileRoute('/login')()
const IndexLazyImport = createFileRoute('/')()
// Create/Update Routes
const LoginLazyRoute = LoginLazyImport.update({
path: '/login',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/login.lazy').then((d) => d.Route))
const CartRoute = CartImport.update({
path: '/cart',
getParentRoute: () => rootRoute,
} as any)
const IndexLazyRoute = IndexLazyImport.update({
path: '/',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexLazyImport
parentRoute: typeof rootRoute
}
'/cart': {
id: '/cart'
path: '/cart'
fullPath: '/cart'
preLoaderRoute: typeof CartImport
parentRoute: typeof rootRoute
}
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginLazyImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof IndexLazyRoute
'/cart': typeof CartRoute
'/login': typeof LoginLazyRoute
}
export interface FileRoutesByTo {
'/': typeof IndexLazyRoute
'/cart': typeof CartRoute
'/login': typeof LoginLazyRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexLazyRoute
'/cart': typeof CartRoute
'/login': typeof LoginLazyRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/cart' | '/login'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/cart' | '/login'
id: '__root__' | '/' | '/cart' | '/login'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexLazyRoute: typeof IndexLazyRoute
CartRoute: typeof CartRoute
LoginLazyRoute: typeof LoginLazyRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexLazyRoute: IndexLazyRoute,
CartRoute: CartRoute,
LoginLazyRoute: LoginLazyRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* prettier-ignore-end */
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.jsx",
"children": [
"/",
"/cart",
"/login"
]
},
"/": {
"filePath": "index.lazy.jsx"
},
"/cart": {
"filePath": "cart.jsx"
},
"/login": {
"filePath": "login.lazy.jsx"
}
}
}
ROUTE_MANIFEST_END */

View File

@ -0,0 +1,15 @@
import Navigation from "@/components/navigation";
import { createRootRoute, Outlet } from "@tanstack/react-router";
function RootComponent() {
return (
<div className="container mx-auto py-8 px-4">
<Navigation />
<Outlet />
</div>
);
}
export const Route = createRootRoute({
component: () => <RootComponent />,
});

View File

@ -0,0 +1,6 @@
import CartCheckout from '@/pages/cart-checkout'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/cart')({
component: () => <CartCheckout />,
})

View File

@ -0,0 +1,6 @@
import MarketplaceListing from '@/pages/marketplace-lisiting'
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/')({
component: () => <MarketplaceListing />,
})

View File

@ -0,0 +1,10 @@
import LoginRegister from "@/pages/login-register";
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/login")({
component: () => (
<div className="flex justify-center bg-background">
<LoginRegister />
</div>
),
});

View File

@ -0,0 +1,57 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
1: "hsl(var(--chart-1))",
2: "hsl(var(--chart-2))",
3: "hsl(var(--chart-3))",
4: "hsl(var(--chart-4))",
5: "hsl(var(--chart-5))",
},
},
},
},
plugins: [require("tailwindcss-animate")],
};

View File

@ -0,0 +1,19 @@
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite(), react()],
server: {
proxy: {
"/api": "http://localhost:17000",
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

10
fjb/httpd-foreground Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
set -e
# start sshd
/usr/sbin/sshd -D &
# Apache gets grumpy about PID files pre-existing
rm -f /usr/local/apache2/logs/httpd.pid
exec httpd -DFOREGROUND "$@"

116
fjb/httpd.conf Normal file
View File

@ -0,0 +1,116 @@
ServerRoot "/usr/local/apache2"
Listen 80
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule authn_file_module modules/mod_authn_file.so
#LoadModule authn_dbm_module modules/mod_authn_dbm.so
#LoadModule authn_anon_module modules/mod_authn_anon.so
#LoadModule authn_dbd_module modules/mod_authn_dbd.so
#LoadModule authn_socache_module modules/mod_authn_socache.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
#LoadModule authz_dbm_module modules/mod_authz_dbm.so
#LoadModule authz_owner_module modules/mod_authz_owner.so
#LoadModule authz_dbd_module modules/mod_authz_dbd.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule dbd_module modules/mod_dbd.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule filter_module modules/mod_filter.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule lua_module modules/mod_lua.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule dir_module modules/mod_dir.so
LoadModule alias_module modules/mod_alias.so
LoadModule rewrite_module modules/mod_rewrite.so
<IfModule unixd_module>
User www-data
Group www-data
</IfModule>
ServerAdmin hermit@kaos.engineer
ServerName fjb.kaos.engineer:80
<Directory />
AllowOverride none
Require all denied
</Directory>
#
# Note that from this point forward you must specifically allow
# particular features to be enabled - so if something's not working as
# you might expect, make sure that you have specifically enabled it
# below.
#
#
# DocumentRoot: The directory out of which you will serve your
# documents. By default, all requests are taken from this directory, but
# symbolic links and aliases may be used to point to other locations.
#
DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
AllowOverride None
Options Indexes FollowSymLinks
Require all granted
RewriteEngine on
# Don't rewrite files or directories
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Rewrite everything else to index.html to allow html5 state links
RewriteRule ^ index.html [L]
</Directory>
LuaMapHandler /api/(\w+) /app/lua/handler/api.lua
LuaPackageCPath /usr/local/lib/lua/5.3/?.so
LuaPackagePath /app/lua/?.lua
<IfModule dir_module>
DirectoryIndex index.html
</IfModule>
<Files ".ht*">
Require all denied
</Files>
ErrorLog /proc/self/fd/2
LogLevel warn
<IfModule log_config_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog /proc/self/fd/1 common
</IfModule>
<IfModule alias_module>
#
</IfModule>
<IfModule headers_module>
RequestHeader unset Proxy early
</IfModule>
<IfModule mime_module>
TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
</IfModule>
# Various default settings
#Include conf/extra/httpd-default.conf

2
fjb/kauth/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.o
*.so

12
fjb/kauth/Makefile Normal file
View File

@ -0,0 +1,12 @@
# CFLAGS += -c -O2 -fPIC $$(pkg-config --cflags lua-5.3 libsodium)
# LDFLAGS += -shared -fPIC $$(pkg-config --libs libsodium lua-5.3)
all: default
kauth.so: kauth.c
luarocks make --no-install
clean:
rm kauth.o kauth.so
.PHONY: clean

View File

@ -0,0 +1,26 @@
package = "kauth"
version = "1.0-1"
source = {
url = "https://kaos.engineer"
}
dependencies = {
"lua >= 5.3",
}
local srcs = {
"kauth.c",
}
build = {
type = "builtin",
modules = {
kauth = {
sources = srcs,
libraries = {"sodium"},
},
},
platforms = {
unix = { modules = { kauth = srcs } },
},
}

164
fjb/kauth/kauth.c Normal file
View File

@ -0,0 +1,164 @@
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <sodium.h>
#include <string.h>
#include <time.h>
#include "kauth.h"
#define kauth_token_claim_BYTES \
(sodium_base64_ENCODED_LEN(sizeof(struct kauth_user), \
sodium_base64_VARIANT_URLSAFE_NO_PADDING))
#define kauth_token_sign_BYTES \
(sodium_base64_ENCODED_LEN(crypto_auth_hmacsha256_BYTES, \
sodium_base64_VARIANT_URLSAFE_NO_PADDING))
#define kauth_token_BYTES (kauth_token_claim_BYTES + kauth_token_sign_BYTES) - 1
static int encode(lua_State *L) {
int typ;
struct kauth_user claim;
unsigned char h[crypto_auth_hmacsha256_BYTES];
unsigned char token[kauth_token_BYTES];
memset(h, 0, crypto_auth_hmacsha256_BYTES);
memset(token, 0, kauth_token_BYTES);
memset(&claim, 0, sizeof(struct kauth_user));
size_t keylen = 0;
const char *key = luaL_checklstring(L, 1, &keylen);
if (keylen != crypto_auth_hmacsha256_KEYBYTES) {
lua_pushstring(L, "invalid key len");
return lua_error(L);
}
luaL_checktype(L, 2, LUA_TTABLE);
if ((typ = lua_getfield(L, 2, "id")) != LUA_TNUMBER) {
lua_pop(L, 1);
lua_pushstring(L, "invalid type id (number expected)");
return lua_error(L);
}
lua_Integer id = lua_tointeger(L, -1);
lua_pop(L, 1);
claim.id = id;
if ((typ = lua_getfield(L, 2, "name")) != LUA_TSTRING) {
lua_pop(L, 1);
lua_pushstring(L, "invalid type name (string expected)");
return lua_error(L);
}
const char *name = lua_tostring(L, -1);
lua_pop(L, 1);
strcpy((char *)claim.name, name);
claim.exp = time(0) + 1800; // 30 min
crypto_auth_hmacsha256(h, (const unsigned char *)&claim,
sizeof(struct kauth_user), (const unsigned char *)key);
sodium_bin2base64((char *)token, kauth_token_claim_BYTES,
(const unsigned char *)&claim, sizeof(struct kauth_user),
sodium_base64_VARIANT_URLSAFE_NO_PADDING);
sodium_bin2base64((char *)(token + kauth_token_claim_BYTES),
kauth_token_sign_BYTES, h, crypto_auth_hmacsha256_BYTES,
sodium_base64_VARIANT_URLSAFE_NO_PADDING);
token[kauth_token_claim_BYTES - 1] = '.';
lua_pushlstring(L, (char *)token, kauth_token_BYTES);
return 1;
}
static int decode(lua_State *L) {
struct kauth_user claim;
const char name[129];
const unsigned char h[crypto_auth_hmacsha256_BYTES];
memset(&claim, 0, sizeof(struct kauth_user));
memset((void *)name, 0, sizeof(name));
memset((void *)h, 0, sizeof(h));
size_t keylen = 0;
const char *key = luaL_checklstring(L, 1, &keylen);
if (keylen != crypto_auth_hmacsha256_BYTES) {
lua_pushstring(L, "invalid key len");
return lua_error(L);
}
size_t tokenlen = 0;
const char *token = luaL_checklstring(L, 2, &tokenlen);
if (tokenlen != kauth_token_BYTES) {
lua_pushstring(L, "invalid token len");
return lua_error(L);
}
if (token[kauth_token_claim_BYTES - 1] != '.') {
lua_pushstring(L, "invalid token");
return lua_error(L);
}
size_t clen = 0;
const char *b64_end = NULL;
if (sodium_base642bin((unsigned char *)&claim, sizeof(struct kauth_user),
token, kauth_token_claim_BYTES, NULL, &clen, &b64_end,
sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0) {
lua_pushstring(L, "invalid token: failed to decode claim");
return lua_error(L);
}
strncpy((char *)name, claim.name, sizeof(name));
clen = 0;
b64_end = NULL;
if (sodium_base642bin((unsigned char *)h, crypto_auth_hmacsha256_BYTES,
token + kauth_token_claim_BYTES, kauth_token_sign_BYTES,
NULL, &clen, &b64_end,
sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0) {
lua_pushstring(L, "invalid token: failed to decode hash");
return lua_error(L);
}
if (crypto_auth_hmacsha256_verify(h, (const unsigned char *)&claim,
sizeof(struct kauth_user),
(const unsigned char *)key) != 0) {
lua_pushstring(L, "invalid token: failed to verify");
return lua_error(L);
}
if (claim.exp <= time(0)) {
lua_pushstring(L, "invalid token: expired");
return lua_error(L);
}
lua_newtable(L);
lua_pushstring(L, name);
lua_setfield(L, -2, "name");
lua_pushinteger(L, claim.id);
lua_setfield(L, -2, "id");
return 1;
}
static const struct luaL_Reg luakauth_lib[] = {
{"encode", encode},
{"decode", decode},
{NULL, NULL},
};
LUALIB_API int luaopen_kauth(lua_State *L) {
if (sodium_init() == -1) {
lua_pushstring(L, "crypto init failed");
return lua_error(L);
}
luaL_newlib(L, luakauth_lib);
return 1;
}

9
fjb/kauth/kauth.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <stddef.h>
struct kauth_user {
const char name[128];
unsigned long long id;
unsigned long long exp;
};

256
fjb/src/database.lua Normal file
View File

@ -0,0 +1,256 @@
local sqlite3 = require "lsqlite3"
local bcrypt = require "bcrypt"
local _M = {}
function _M.register(username, password)
local user = nil
local err = nil
local conn = sqlite3.open("/app/data/fjb.db", sqlite3.OPEN_READWRITE)
local hashed_password = bcrypt.digest(password, 9)
local stmt = conn:prepare("INSERT INTO users (username, password) VALUES (?, ?)")
stmt:bind_values(username, hashed_password)
local result = stmt:step()
if result == sqlite3.DONE then
user = conn:last_insert_rowid()
elseif result == sqlite3.CONSTRAINT then
else
err = conn:errmsg()
end
conn:close()
return user, err
end
function _M.login(username, password)
local user = nil
local err = nil
local conn = sqlite3.open("/app/data/fjb.db")
local stmt = conn:prepare("SELECT id, username, password FROM users WHERE username = ?")
stmt:bind_values(username)
local result = stmt:step()
if result == sqlite3.ROW then
local hashed_password = stmt:get_value(2)
if bcrypt.verify(password, hashed_password) then
user = {
name = stmt:get_value(1),
id = stmt:get_value(0),
}
end
elseif result == sqlite3.DONE then
else
err = conn:errmsg()
end
conn:close()
return user, err
end
function _M.get_user(id)
local user = nil
local err = nil
local conn = sqlite3.open("/app/data/fjb.db")
local stmt = conn:prepare("SELECT username FROM users WHERE id = ?")
stmt:bind_values(id)
local result = stmt:step()
if result == sqlite3.ROW then
user = stmt:get_value(0)
else
err = conn:errmsg()
end
conn:close()
return user, err
end
function _M.get_wallet(id)
local wallet = nil
local err = nil
local conn = sqlite3.open("/app/data/fjb.db")
local stmt = conn:prepare("SELECT wallet FROM users WHERE id = ?")
stmt:bind_values(id)
local result = stmt:step()
if result == sqlite3.ROW then
wallet = stmt:get_value(0)
else
err = conn:errmsg()
end
conn:close()
return wallet, err
end
function _M.add_item(uid, item_name, description, valuable, price)
local id = nil
local err = nil
local conn = sqlite3.open("/app/data/fjb.db", sqlite3.OPEN_READWRITE)
local created_at = os.date("!%Y-%m-%dT%H:%M:%S+00:00")
local stmt = conn:prepare [[
INSERT INTO catalog
(user_id, item_name, description, value, price, created_at)
VALUES (?, ?, ?, ?, ?, ?)
]]
stmt:bind_values(uid, item_name, description, valuable, price, created_at)
local result = stmt:step()
if result == sqlite3.DONE then
id = conn:last_insert_rowid()
else
err = conn:errmsg()
end
conn:close()
return id, err
end
function _M.get_all_items(uid)
local conn = sqlite3.open("/app/data/fjb.db")
local query = "SELECT * FROM catalog"
if uid then
query = query .. " WHERE user_id = ?"
end
local stmt = conn:prepare(query)
if uid then
stmt:bind_values(uid)
end
local items = {}
local result = stmt:step()
while result == sqlite3.ROW do
local id = stmt:get_value(0)
local user_id = stmt:get_value(1)
local item_name = stmt:get_value(2)
local description = stmt:get_value(3)
local value = stmt:get_value(4)
local price = stmt:get_value(5)
local date_posted = stmt:get_value(6)
table.insert(items, {
id = id,
name = item_name,
description = description,
owner = user_id,
value = uid == user_id and value or nil,
price = price,
date_posted = date_posted,
})
result = stmt:step()
end
conn:close()
return items
end
function _M.get_item(uid, item_id)
local conn = sqlite3.open("/app/data/fjb.db")
local stmt = conn:prepare("SELECT * FROM catalog WHERE id = ?")
stmt:bind_values(item_id)
local result = stmt:step()
if result ~= sqlite3.ROW then
return nil
end
local user_id = stmt:get_value(1)
if user_id ~= uid then
return nil
end
local id = stmt:get_value(0)
local item_name = stmt:get_value(2)
local description = stmt:get_value(3)
local value = stmt:get_value(4)
local price = stmt:get_value(5)
local date_posted = stmt:get_value(6)
local item = {
id = id,
name = item_name,
description = description,
value = value,
price = price,
date_posted = date_posted,
}
conn:close()
return item
end
function _M.add_to_cart(uid, item_id)
local id = nil
local err = nil
local conn = sqlite3.open("/app/data/fjb.db", sqlite3.OPEN_READWRITE)
local stmt = conn:prepare("SELECT user_id FROM catalog WHERE id = ?")
stmt:bind_values(item_id)
local result = stmt:step()
if result ~= sqlite3.ROW then
return nil, "invalid item_id"
end
if stmt:get_value(0) == uid then
return nil, "cant add your own item to cart"
end
stmt = conn:prepare("INSERT INTO cart (user_id, item_id) VALUES (?, ?)")
stmt:bind_values(uid, item_id)
local result = stmt:step()
if result == sqlite3.DONE then
id = conn:last_insert_rowid()
else
err = conn:errmsg()
end
conn:close()
return id, err
end
function _M.get_items_from_cart(uid, checkout)
local pay = checkout ~= nil
local conn = sqlite3.open("/app/data/fjb.db")
local items = {}
local stmt = conn:prepare [[
SELECT car.id, cat.id, cat.item_name, cat.price, cat.value
FROM cart car
JOIN catalog cat ON car.item_id = cat.id
WHERE car.user_id = ?
]]
stmt:bind_values(uid)
local result = stmt:step()
while result == sqlite3.ROW do
local id = stmt:get_value(0)
local item_id = stmt:get_value(1)
local name = stmt:get_value(2)
local price = stmt:get_value(3)
local valuable = stmt:get_value(4)
table.insert(items, {
id = id,
item_id = item_id,
name = name,
price = price,
valuable = pay and valuable or nil,
})
result = stmt:step()
end
conn:close()
return items
end
function _M.delete_item_from_cart(uid, id)
local err = nil
local conn = sqlite3.open("/app/data/fjb.db")
local stmt = conn:prepare [[
DELETE FROM
cart
WHERE
user_id = ? AND id = ?
]]
stmt:bind_values(uid, id)
local result = stmt:step()
if result ~= sqlite3.DONE then
err = conn:errmsg()
end
conn:close()
return err
end
return _M

40
fjb/src/handler/api.lua Normal file
View File

@ -0,0 +1,40 @@
local cjson = require "cjson"
local middleware = require "middleware"
local login = require "handler.login"
local register = require "handler.register"
local user = require "handler.user"
local catalog = require "handler.catalog"
local cart = require "handler.cart"
local checkout = require "handler.checkout"
function handle(r)
if r.uri == "/api/login" and r.method == "POST" then
return login.handler(r)
elseif r.uri == "/api/register" and r.method == "POST" then
return register.handler(r)
elseif r.uri == "/api/user" and r.method == "GET" then
return middleware.auth(r, user.handler)
elseif r.uri == "/api/catalog" and r.method == "GET" then
return catalog.get(r)
elseif r.uri == "/api/catalog/me" and r.method == "GET" then
return middleware.auth(r, catalog.get_me)
elseif r.uri == "/api/catalog" and r.method == "POST" then
return middleware.auth(r, catalog.post)
elseif r.uri == "/api/cart" and r.method == "GET" then
return middleware.auth(r, cart.get)
elseif r.uri == "/api/cart" and r.method == "POST" then
return middleware.auth(r, cart.post)
elseif r.uri == "/api/cart" and r.method == "DELETE" then
return middleware.auth(r, cart.delete)
elseif r.uri == "/api/checkout" and r.method == "POST" then
return middleware.auth(r, checkout.post)
else
r.status = 404
r.content_type = "application/json"
r:write(cjson.encode({
error = "Not found"
}))
return apache2.OK
end
end

73
fjb/src/handler/cart.lua Normal file
View File

@ -0,0 +1,73 @@
local cjson = require "cjson"
cjson.encode_empty_table_as_object(false)
local db = require "database"
local util = require "lib.utils"
local _M = {}
function _M.get(r)
local items = db.get_items_from_cart(tonumber(r.notes.kauth_id))
r.content_type = "application/json"
r:write(cjson.encode({
items = items,
}))
return apache2.OK
end
function _M.delete(r)
local spec = r:parseargs()
if util.isempty(spec.id) then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: missing id"
}))
return apache2.OK
end
r.content_type = "application/json"
local err = db.delete_item_from_cart(tonumber(r.notes.kauth_id), tonumber(spec.id))
if err then
r:write(cjson.encode({
error = "Internal error: " .. err,
}))
return apache2.OK
end
r:write(cjson.encode({
message = "Successfully removed from cart",
}))
return apache2.OK
end
function _M.post(r)
local input = r:requestbody()
local spec = cjson.decode(input or "{}")
if util.isempty(spec.item) then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: missing name"
}))
return apache2.OK
end
r.content_type = "application/json"
local _, err = db.add_to_cart(tonumber(r.notes.kauth_id), tonumber(spec.item))
if err then
r:write(cjson.encode({
error = "Internal error: " .. err,
}))
return apache2.OK
end
r:write(cjson.encode({
message = "Successfully added to cart",
}))
return apache2.OK
end
return _M

View File

@ -0,0 +1,79 @@
local cjson = require "cjson"
cjson.encode_empty_table_as_object(false)
local db = require "database"
local util = require "lib.utils"
local _M = {}
function _M.get(r)
local items = db.get_all_items()
r.content_type = "application/json"
r:write(cjson.encode({
items = items,
}))
return apache2.OK
end
function _M.get_me(r)
local items = db.get_all_items(tonumber(r.notes.kauth_id))
r.content_type = "application/json"
r:write(cjson.encode({
items = items,
}))
return apache2.OK
end
function _M.post(r)
local input = r:requestbody()
local spec = cjson.decode(input or "{}")
if util.isempty(spec.name) then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: missing name"
}))
return apache2.OK
end
if util.isempty(spec.description) then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: missing description"
}))
return apache2.OK
end
if util.isempty(spec.valuable) then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: missing valuable"
}))
return apache2.OK
end
if util.isempty(spec.price) or tonumber(spec.price) <= 0 then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: missing price"
}))
return apache2.OK
end
r.content_type = "application/json"
local id, err = db.add_item(tonumber(r.notes.kauth_id), spec.name, spec.description, spec.valuable, tonumber(spec.price))
if err then
r:write(cjson.encode({
error = "Internal error: " .. err,
}))
return apache2.OK
end
r:write(cjson.encode({
message = "Successfully added to catalog",
}))
return apache2.OK
end
return _M

View File

@ -0,0 +1,40 @@
local cjson = require "cjson"
local db = require "database"
local util = require "lib.utils"
local _M = {}
function _M.post(r)
local cost = 0x0
r.content_type = "application/json"
local items = db.get_items_from_cart(tonumber(r.notes.kauth_id), true)
for i = 1, #items do
cost = cost + items[i].price
end
local wallet, err = db.get_wallet(tonumber(r.notes.kauth_id))
if err then
r.status = 500
r:write(cjson.encode({
error = "Internal error: " .. err,
}))
return apache2.OK
end
if wallet < cost then
r.status = 401
r:write(cjson.encode({
error = "Unathorized: not enough balance",
}))
return apache2.OK
end
r:write(cjson.encode({
items = items,
}))
return apache2.OK
end
return _M

65
fjb/src/handler/login.lua Normal file
View File

@ -0,0 +1,65 @@
local cjson = require "cjson"
local kauth = require "kauth"
local utils = require "lib.utils"
local db = require "database"
local secret = require "secret"
local _M = {}
function _M.handler(r)
local input = r:requestbody()
local spec = cjson.decode(input or "{}")
r.content_type = "application/json"
if utils.isempty(spec.username) or utils.isempty(spec.password) then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: missing username or password"
}))
return apache2.OK
end
local user, err = db.login(spec.username, spec.password)
if err then
r.status = 500
r:write(cjson.encode({
error = "Internal error: " .. err
}))
return apache2.OK
end
if not user then
r.status = 401
r:write(cjson.encode({
error = "Unathorized: invalid password or username"
}))
return apache2.OK
end
local ok, token = pcall(function ()
return kauth.encode(secret.key, user)
end)
if not ok then
r.status = 500
r:write(cjson.encode({
error = "Internal error: failed to generate token"
}))
return apache2.OK
end
r:setcookie({
key = "session",
value = token,
expires = os.time() + 1800,
})
r:write(cjson.encode({
user = user,
token = token,
}))
return apache2.OK
end
return _M

View File

@ -0,0 +1,45 @@
local cjson = require "cjson"
local utils = require "lib.utils"
local db = require "database"
local _M = {}
function _M.handler(r)
local input = r:requestbody()
local spec = cjson.decode(input or "{}")
r.content_type = "application/json"
if utils.isempty(spec.username) or utils.isempty(spec.password) then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: missing username or password"
}))
return apache2.OK
end
local uid, err = db.register(spec.username, spec.password)
if err then
r.status = 500
r:write(cjson.encode({
error = "Internal error: " .. err
}))
return apache2.OK
end
if uid == nil then
r.status = 400
r:write(cjson.encode({
error = "Bad Request: invalid password or username"
}))
return apache2.OK
end
r:write(cjson.encode({
message = "Successfully registered"
}))
return apache2.OK
end
return _M

16
fjb/src/handler/user.lua Normal file
View File

@ -0,0 +1,16 @@
local cjson = require "cjson"
local _M = {}
function _M.handler(r)
r.content_type = "application/json"
r:write(cjson.encode({
user = {
id = r.notes.kauth_id,
name = r.notes.kauth_name,
}
}))
return apache2.OK
end
return _M

7
fjb/src/lib/utils.lua Normal file
View File

@ -0,0 +1,7 @@
local _M = {}
function _M.isempty(s)
return s == nil or s == ""
end
return _M

47
fjb/src/middleware.lua Normal file
View File

@ -0,0 +1,47 @@
local kauth = require "kauth"
local cjson = require "cjson"
local database = require "database"
local secret = require "secret"
local _M = {}
function _M.auth(r, fn)
local session = r:getcookie("session")
if not session then
r.status = 401
r.content_type = "application/json"
r:write(cjson.encode({
error = "Unathorized: invalid session"
}))
return apache2.OK
end
local ok, res = pcall(function ()
return kauth.decode(secret.key, session)
end)
if not ok then
r.status = 401
r.content_type = "application/json"
r:write(cjson.encode({
error = "Unathorized: " .. res
}))
return apache2.OK
end
local _, err = database.get_user(res.id)
if err then
r.status = 401
r.content_type = "application/json"
r:write(cjson.encode({
error = "Unathorized: invalid session"
}))
return apache2.OK
end
r.user = res.name
r.notes.kauth_name = res.name
r.notes.kauth_id = res.id
return fn(r)
end
return _M

5
fjb/src/secret.lua Normal file
View File

@ -0,0 +1,5 @@
local _M = {}
_M.key = "SECRETSECRETSECRETSECRETSECRETSE"
return _M

19
gift-card/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM public.ecr.aws/docker/library/python:3.11-slim-buster
ARG PASSWORD
ENV DEBIAN_FRONTEND noninteractive
RUN echo root:${PASSWORD} | chpasswd
RUN apt-get update && apt-get install -y openssh-server curl socat nano
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN service ssh start
WORKDIR /ctf/gift-card
COPY src/ src/
COPY start.sh .
RUN chmod +x start.sh
CMD ./start.sh

View File

@ -0,0 +1,13 @@
services:
gift-card:
hostname: gift-card
restart: always
build:
context: .
args:
- PASSWORD=PASSWORD_21000
ports:
- "21000:5000"
- "21022:22"
volumes:
- ./flag.txt:/ctf/gift-card/flag.txt:ro

1
gift-card/flag.txt Normal file
View File

@ -0,0 +1 @@
PLACEHOLDER

126
gift-card/src/main.py Normal file
View File

@ -0,0 +1,126 @@
import random
import signal
FLAG = open("../flag.txt").read()
class GiftShop:
# TODO: design better giftcard generator and validator
def __init__(self, name: str) -> None:
self.mod = lambda x: x % 256
self.Hm = self.mod(sum(name.encode()))
def generate_giftcard(self) -> str:
r = random.randbytes(15)
s = int.to_bytes(self.mod(self.Hm - self.mod(sum(r))))
signature = r + s
return signature.hex()
def validate_giftcard(self, giftcard: str) -> bool:
signature = bytes.fromhex(giftcard)
return len(signature) == 16 and self.mod(sum(signature)) == self.Hm
class Challenge:
def __init__(self, name):
self.name = name
self.balance = 100
self.items = {
"bread": {"price": 33, "callable": self.__get_bread},
"giftcard": {"price": 50, "callable": self.__get_giftcard},
"flag": {"price": 420, "callable": self.__get_flag},
}
self.redeemed_giftcard = set()
self.gift_shop = GiftShop(self.name)
def greet(self):
msg = ""
msg += f"\nWelcome, {self.name}!"
msg += f"\nOption:"
msg += f"\n [1] Check available items"
msg += f"\n [2] Buy item"
msg += f"\n [3] Redeem giftcard"
msg += f"\n [9] Input admin code"
print(msg)
def __get_bread(self):
return chr(0x1F35E)
def __get_giftcard(self):
return self.gift_shop.generate_giftcard()
def __get_flag(self):
return FLAG
def check_available(self):
msg = ""
msg += f"\nAvailable items:"
for item_name, item_info in self.items.items():
item_price = item_info["price"]
msg += f"\n [+] {item_name} (${item_price})"
return msg
def buy_item(self, item):
if item not in self.items.keys():
return f"Item {item} is not available"
if self.balance < self.items[item]["price"]:
return f"Insufficient balance"
self.balance -= self.items[item]["price"]
return self.items[item]["callable"]()
def redeem_giftcard(self, giftcard):
if giftcard in self.redeemed_giftcard:
return "Your giftcard is already used"
if not self.gift_shop.validate_giftcard(giftcard):
return "Your giftcard is invalid"
value = random.randint(30, 60)
self.balance += value
self.redeemed_giftcard.add(giftcard)
return f"You got ${value} from the giftcard"
def admin_code(self, code):
if code != FLAG:
return "Incorrect code"
self.balance += self.items["flag"]["price"]
return "Correct code"
def user_input(s):
inp = input(s).strip()
assert len(inp) < 256
return inp
def main():
name = user_input("Name: ")
challenge = Challenge(name)
challenge.greet()
while True:
print(f"\nCurrent balance: ${challenge.balance}")
match int(user_input("> ")):
case 1:
print(challenge.check_available())
case 2:
item = user_input("Item: ")
print(challenge.buy_item(item))
case 3:
giftcard = user_input("Giftcard: ")
print(challenge.redeem_giftcard(giftcard))
case 9:
code = user_input("Code: ")
print(challenge.admin_code(code))
case _:
print("Invalid option")
break
if __name__ == "__main__":
signal.alarm(60)
main()

5
gift-card/start.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/sh
/usr/sbin/sshd -D &
cd src
socat TCP-LISTEN:5000,reuseaddr,fork EXEC:"python3 -u main.py"

21
gift-voucher/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM python:3.11-slim-buster
ARG PASSWORD
ENV DEBIAN_FRONTEND noninteractive
RUN echo root:${PASSWORD} | chpasswd
RUN apt-get update && apt-get install -y openssh-server curl socat nano python3 python3-pip
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN service ssh start
WORKDIR /ctf/gift-voucher
COPY src/ src/
COPY start.sh .
RUN pip install pycryptodome
RUN chmod +x start.sh
CMD ./start.sh

View File

@ -0,0 +1,13 @@
services:
gift-voucher:
hostname: gift-voucher
restart: always
build:
context: .
args:
- PASSWORD=PASSWORD_16000
ports:
- "16000:5000"
- "16022:22"
volumes:
- ./flag.txt:/ctf/gift-voucher/flag.txt:ro

1
gift-voucher/flag.txt Normal file
View File

@ -0,0 +1 @@
PLACEHOLDER

134
gift-voucher/src/main.py Normal file
View File

@ -0,0 +1,134 @@
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
import os
import random
import signal
import base64
FLAG = open("../flag.txt").read()
class GiftShop:
def __init__(self, name: str) -> None:
self.key = RSA.generate(1024)
self.Hm = SHA256.new(name.encode()).digest()
self.prefix = b"\x00\x02"
self.null = b"\x00"
def generate_voucher(self) -> str:
message = self.prefix + os.urandom(93) + self.null + self.Hm
voucher = pow(int.from_bytes(message, "big"), self.key.d, self.key.n)
return base64.b64encode(int.to_bytes(voucher, 128, "big")).decode()
def validate_voucher(self, voucher: str) -> bool:
voucher = int.from_bytes(base64.b64decode(voucher.encode()), "big")
message = int.to_bytes(pow(voucher, self.key.e, self.key.n), 128, "big")
return (
voucher.bit_length() <= 1024
and message.startswith(self.prefix)
and message.endswith(self.null + self.Hm)
)
class Challenge:
def __init__(self, name):
self.name = name
self.balance = 100
self.items = {
"bread": {"price": 33, "callable": self.__get_bread},
"voucher": {"price": 50, "callable": self.__get_voucher},
"flag": {"price": 420, "callable": self.__get_flag},
}
self.redeemed_voucher = set()
self.gift_shop = GiftShop(self.name)
def greet(self):
msg = ""
msg += f"\nWelcome, {self.name}!"
msg += f"\nOption:"
msg += f"\n [1] Check available items"
msg += f"\n [2] Buy item"
msg += f"\n [3] Redeem voucher"
msg += f"\n [9] Input admin code"
print(msg)
def __get_bread(self):
return self.gift_shop.key.public_key().export_key().decode().replace('-', chr(0x1F35E))
def __get_voucher(self):
return self.gift_shop.generate_voucher()
def __get_flag(self):
return FLAG
def check_available(self):
msg = ""
msg += f"\nAvailable items:"
for item_name, item_info in self.items.items():
item_price = item_info["price"]
msg += f"\n [+] {item_name} (${item_price})"
return msg
def buy_item(self, item):
if item not in self.items.keys():
return f"Item {item} is not available"
if self.balance < self.items[item]["price"]:
return f"Insufficient balance"
self.balance -= self.items[item]["price"]
return self.items[item]["callable"]()
def redeem_voucher(self, voucher):
if voucher in self.redeemed_voucher:
return "Your voucher is already used"
if not self.gift_shop.validate_voucher(voucher):
return "Your voucher is invalid"
value = random.randint(30, 60)
self.balance += value
self.redeemed_voucher.add(voucher)
return f"You got ${value} from the voucher"
def admin_code(self, code):
if code != FLAG:
return "Incorrect code"
self.balance += self.items["flag"]["price"]
return "Correct code"
def user_input(s):
inp = input(s).strip()
assert len(inp) < 1024
return inp
def main():
name = user_input("Name: ")
challenge = Challenge(name)
challenge.greet()
while True:
print(f"\nCurrent balance: ${challenge.balance}")
match int(user_input("> ")):
case 1:
print(challenge.check_available())
case 2:
item = user_input("Item: ")
print(challenge.buy_item(item))
case 3:
voucher = user_input("Voucher: ")
print(challenge.redeem_voucher(voucher))
case 9:
code = user_input("Code: ")
print(challenge.admin_code(code))
case _:
print("Invalid option")
break
if __name__ == "__main__":
signal.alarm(60)
main()

Some files were not shown because too many files have changed in this diff Show More