Compare commits

..
4 Commits
Author SHA1 Message Date
wiktr 7acb426fea IZBI!!!1!!11 2026-09-10 00:38:16 +02:00
wiktr c3d61ada91 hotfix 2026-08-28 14:57:00 +02:00
wiktr 37442c9864 add CSRF protection 2026-08-28 14:48:58 +02:00
wiktr 8b94f36db5 fix Token db interface 2026-08-28 14:47:52 +02:00
8 changed files with 219 additions and 101 deletions

No files matched your search

+55 -14
View File
@@ -1,18 +1,59 @@
MIT License
The izbi license 1.01
Copyright (c) 2026 wiktr
Permission is hereby granted, free of charge, to any student and/or
graduate of Computer Science - Information Security Management
(pl. Informatyka - Zarządzanie Bezpieczeństwem Informacji "izbi")
from AGH University of Krakow ("AGH"), to freely use, modify and
redistribute the contents of this software, its source code
and any associated documentation ("full software").
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
Every file that is written in human-readable format, meant to be read
by humans and not used by the software itself (documentation, readme, etc.),
has to end with the word "izbi", separated from the rest of the
aforementioned file by a newline character.
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
Only other students and/or staff of AGH's Faculty of Computer Science
(pl. Wydział Informatyki Akademii Górniczo-Hutniczej "WI AGH"),
after being given the verbal (which may be revoked at any time by original authors) or written
permission from the author(s) of the full software may acquire the same rights
to the full software as the students of izbi, free of charge.
The use of this license by people affiliated with WI AGH is permitted only if they
know someone that attends and/or attended izbi.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
Any other person, who is unrelated to WI AGH and wishes to use this software is allowed to,
but cannot publish for profit or modify any version of the full software
or parts of its source code, including (but not limited to) merging and/or sublicensing,
except where such modification is private and cannot be discovered or
traced through the Clearnet or Deep web, in which case it is explicitly permitted.
Students of AGH's Faculty of Computer Science, Electronics and Telecommunications
(pl. Wydział Informatyki, Elektroniki i Telekomunikacji "WIET")
are not permitted to use and/or modify the full software.
However if they want to use the full software or its parts
they need to give 1 (one) full can of beer from the brand "Romper",
or any other beer stated in the beer.md file in which (if created),
every author needs to name the brand and type of beer they want to be given.
This beer must be given to every credited author of the full software,
even if parts of the software that the students of WIET used were not made by that person.
After supplying the author(s) with beer, WIET students are acquiring the
same rights as students of WI AGH.
The only people that are not allowed to use the full software in
any way, shape or form, including reading this license or any other file
are any past, present and future deans of WIET.
THE TEXT ABOVE HAS TO APPEAR IN ITS UNCHANGED FORM IN ANY COPIES
AND/OR FORKS OF FULL SOFTWARE THAT WERE NOT CREATED BY THE ORIGINAL
AUTHOR OF THE FULL SOFTWARE. THE ONLY PEOPLE WITH THE RIGHT TO
CHANGE THE LICENSE OF THE FULL SOFTWARE ARE THE AUTHORS
CREDITED IN THE HISTORY OF THE ORIGINAL REPOSITORY (OR SEPARATE
AUTHORS.md file).
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
izbi
+2
View File
@@ -1,3 +1,5 @@
# IZBI DNS
Dynamic DNS for `.izbi` domains.
izbi
+2 -2
View File
@@ -416,7 +416,7 @@ class Token:
def get_user(db_path: str, token: str) -> int:
if not Token.exists(db_path, userid):
if not Token.exists(db_path, token):
raise ValueError("Token does not exist")
with sqlite3.connect(db_path) as connection:
@@ -429,4 +429,4 @@ class Token:
WHERE tkhash = ?
''', (tkhash, ))
return cursor.fetcone()[0]
return cursor.fetchone()[0]
+127 -67
View File
@@ -3,7 +3,10 @@ import auth
import db
import re
from os import environ, urandom
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, EmailField, HiddenField
from wtforms.validators import DataRequired
from flask_wtf.csrf import CSRFProtect
HOSTS_PATH = ( environ["HOSTS"] if "HOSTS" in environ else"/app/data/hosts.d/" ) + "/izbi"
DB_PATH = environ["DB_PATH"] if "DB_PATH" in environ else "/app/data/sqlite.db"
@@ -14,8 +17,9 @@ app.config["SESSION_TYPE"] = "memcached"
app.config["PERMANENT_SESSION_LIFETIME"] = 3600
app.config["SECRET_KEY"] = environ["SECRET_KEY"] if "SECRET_KEY" in environ else urandom(32).hex()
hosts = {}
csrf = CSRFProtect(app)
hosts = {}
# Update hosts file
def update_hosts(path: str, hosts: dict = {}) -> bool:
@@ -38,18 +42,23 @@ def validate_ip(ip: str) -> bool:
return re.search(exp, ip)
class LoginForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
password = PasswordField('password', validators=[DataRequired()])
# Serve homepage
@app.route('/', methods=["GET"])
def homepage():
@app.route('/', methods=["GET", "POST"])
def index():
if session.get("USERID") is not None:
return redirect("/dashboard")
return render_template("index.html")
login_form = LoginForm()
# Handle login
@app.route('/login', methods=["POST"])
def login():
userid = auth.user_login(DB_PATH, request.form.get("user"), request.form.get("pass"))
if login_form.validate_on_submit():
username = login_form.username.data
password = login_form.password.data
userid = auth.user_login(DB_PATH, username, password)
if userid is None:
flash("Invalid user or password")
@@ -59,13 +68,35 @@ def login():
return redirect("/dashboard")
return render_template("index.html", login_form=login_form)
# Log out by clearing session data
@app.route('/logout', methods=["GET"])
def logout():
session.clear()
return redirect("/")
class LogoutForm(FlaskForm):
pass
class GenerateTokenForm(FlaskForm):
token_name = StringField('token-name', validators=[DataRequired()])
class RevokeTokenForm(FlaskForm):
token_name = StringField('token-name', validators=[DataRequired()])
class ChangePasswordForm(FlaskForm):
password = PasswordField('password', validators=[DataRequired()])
new_password = PasswordField('new-password', validators=[DataRequired()])
repeat_password = PasswordField('repeat-password', validators=[DataRequired()])
class CreateUserForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
domains = StringField('domains')
password = PasswordField('password', validators=[DataRequired()])
is_admin = BooleanField('is-admin')
email = EmailField('email')
class UpdateUserForm(FlaskForm): #TODO
pass
class DeleteUserForm(FlaskForm):
userid = HiddenField('userid', validators=[DataRequired()])
# Display dashboard for logged in users
@app.route('/dashboard', methods=["GET"])
@@ -73,6 +104,11 @@ def dashboard():
if session.get("USERID") is None:
return redirect("/")
logout_form = LogoutForm()
generate_token_form = GenerateTokenForm()
revoke_token_form = RevokeTokenForm()
change_password_form = ChangePasswordForm()
userid = session.get("USERID")
username = db.User.get_username(DB_PATH, userid)
@@ -81,10 +117,32 @@ def dashboard():
# If user is admin, display additional options
if db.User.is_admin(DB_PATH, userid):
users = db.list_users(DB_PATH)
return render_template("dashboard.html", username=username, domains=domains, tokens=tokens, is_admin=True, users=users)
create_user_form = CreateUserForm()
update_user_form = UpdateUserForm()
delete_user_form = DeleteUserForm()
return render_template("dashboard.html", username=username, domains=domains, tokens=tokens, is_admin=False)
users = db.list_users(DB_PATH)
return render_template("dashboard.html",
logout_form=logout_form, generate_token_form=generate_token_form, revoke_token_form=revoke_token_form, change_password_form=change_password_form, create_user_form=create_user_form, update_user_form=update_user_form, delete_user_form=delete_user_form,
username=username, domains=domains, tokens=tokens, is_admin=True, users=users
)
return render_template("dashboard.html",
logout_form=logout_form, generate_token_form=generate_token_form, revoke_token_form=revoke_token_form, change_password_form=change_password_form,
username=username, domains=domains, tokens=tokens, is_admin=False
)
# Log out by clearing session data
@app.route('/logout', methods=["POST"])
def logout():
logout_form = LogoutForm()
if logout_form.validate_on_submit():
session.clear()
return redirect("/")
# Generate new access token with a name
@@ -93,12 +151,12 @@ def generate_token():
if session.get("USERID") is None:
return redirect("/")
if "token" not in request.form:
flash("Token name not supplied")
return redirect("/dashboard")
generate_token_form = GenerateTokenForm()
if generate_token_form.validate_on_submit():
token_name = generate_token_form.token_name.data
try:
token = db.User.generate_token(DB_PATH, session.get("USERID"), request.form["token"])
token = db.User.generate_token(DB_PATH, session.get("USERID"), token_name)
except ValueError as e:
flash(str(e))
token = None
@@ -115,14 +173,42 @@ def revoke_token():
if session.get("USERID") is None:
return redirect("/")
if "token" not in request.form:
flash("Token name not supplied")
revoke_token_form = RevokeTokenForm()
if revoke_token_form.validate_on_submit():
token_name = revoke_token_form.token_name.data
try:
db.User.revoke_token(DB_PATH, session.get("USERID"), token_name)
except ValueError as e:
flash(str(e))
return redirect("/dashboard")
# Change password (by user)
@app.route('/change-password', methods=["POST"])
def change_password():
if session.get("USERID") is None:
return redirect("/")
change_password_form = ChangePasswordForm()
if change_password_form.validate_on_submit():
password = change_password_form.password.data
new_password = change_password_form.new_password.data
repeat_password = change_password_form.repeat_password.data
if new_password != repeat_password:
flash("Passwords do not match")
return redirect("/dashboard")
try:
db.User.revoke_token(DB_PATH, session.get("USERID"), request.form["token"])
auth.change_user_password(DB_PATH, session.get("USERID"), password, new_password)
except ValueError as e:
flash(str(e))
return redirect("/dashboard")
flash("Password changed successfully")
return redirect("/dashboard")
@@ -135,14 +221,13 @@ def create_user():
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
if 'username' not in request.form or 'password' not in request.form:
flash('Required fields not supplied')
return redirect("/dashboard")
create_user_form = CreateUserForm()
username = request.form.get("username").strip()
password = request.form.get("password")
if create_user_form.validate_on_submit():
username = create_user_form.username.data.strip()
password = create_user_form.password.data
domains = request.form.get('domains')
domains = create_user_form.domains.data
if domains is None:
domains = []
else:
@@ -151,13 +236,9 @@ def create_user():
domains = collapse_exp.sub(' ', domains.strip())
domains = domains.split(" ")
is_admin = request.form.get("is_admin")
if is_admin is not None:
is_admin = is_admin == "true"
else:
is_admin = False
is_admin = create_user_form.is_admin.data
email = request.form.get("email")
email = create_user_form.email.data
if email is not None:
if email == "":
email = None
@@ -178,6 +259,11 @@ def update_user():
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
update_user_form = UpdateUserForm()
if not update_user_form.validate_on_submit():
return redirect("/dashboard")
userid = request.form.get("userid")
if userid is None:
flash("UserID not supplied")
@@ -226,11 +312,12 @@ def delete_user():
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
userid = request.form.get("userid")
if userid is None:
return redirect("/dashboard")
delete_user_form = DeleteUserForm()
if db.User.exists(DB_PATH, userid):
if delete_user_form.validate_on_submit():
userid = delete_user_form.userid.data
if not db.User.exists(DB_PATH, userid):
flash("User does not exst")
return redirect("/dashboard")
@@ -239,33 +326,6 @@ def delete_user():
return redirect("/dashboard")
# Change password (by user)
@app.route('/change-password', methods=["POST"])
def change_password():
if session.get("USERID") is None:
return redirect("/")
if "pass" not in request.form or "pass-new" not in request.form or "pass-rep" not in request.form:
flash("Required values not supplied")
return redirect("/dashboard")
oldpass = request.form["pass"]
newpass = request.form["pass-new"]
reppass = request.form["pass-rep"]
if newpass != reppass:
flash("Passwords do not match")
return redirect("/dashboard")
try:
auth.change_user_password(DB_PATH, session.get("USERID"), oldpass, newpass)
except ValueError as e:
flash(str(e))
return redirect("/dashboard")
flash("Password changed successfully")
return redirect("/dashboard")
# API health endpoint ???
@app.route('/health', methods=["GET"])
+12 -6
View File
@@ -7,11 +7,12 @@
<tr><th>Username</th><th>Domains</th><th>Password</th><th>Admin</th><th>Email</th><th>Delete</th></tr>
<form method="POST" action="/create-user"><tr>
<td><input type="text" name="username" placeholder="*user name" required /></td>
<td><input type="text" name="domains" placeholder="space separated domains" /></td>
<td><input type="password" name="password" placeholder="*password" required /></td>
<td><input type="checkbox" name="is_admin" /></td>
<td><input type="email" name="email" placeholder="email" /></td>
{{ create_user_form.hidden_tag() }}
<td>{{ create_user_form.username(placeholder="*Username") }}</td>
<td>{{ create_user_form.domains(placeholder="Space separated domains") }}</td>
<td>{{ create_user_form.password(placeholder="*Password") }}</td>
<td>{{ create_user_form.is_admin() }}</td>
<td>{{ create_user_form.email(placeholder="Email") }}</td>
<td><button type="submit">Add</button></td>
</tr></form>
@@ -19,28 +20,33 @@
<tr>
<td>{{ user["username"] }}</td>
<td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="text" name="domains" placeholder="space separated domains" value="{{ user["domains"] | join(' ') }}" />
<button type="submit">update domains</button>
</form></td>
<td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="password" name="password" placeholder="*password" required />
<button type="submit">update password</button>
</form></td>
<td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="hidden" name="is_admin" value="{% if user["is_admin"] %}false{% else %}true{% endif %}" />
<input type="checkbox" disabled {% if user["is_admin"] %}checked{% endif %} />
<button type="submit">toggle admin</button>
</form></td>
<td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="email" name="email" placeholder="email" value="{{ user["email"] if user["email"] }}"/>
<button type="submit">update email</button>
</form></td>
<td><form method="POST" action="/delete-user">
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
{{ delete_user_form.csrf_token }}
{{ delete_user_form.userid(value=user["userid"]) }}
<button type="submit">Delete</button>
</form></td>
</tr>
+16 -9
View File
@@ -4,7 +4,11 @@
<title>Dashboard - IZBI DNS</title>
</head>
<body>
<h2>Hello {{ username }} <a href="/logout">log&nbsp;out</a></h2>
<h2>Hello {{ username }}</h2>
<form method="POST" action="/logout">
{{ logout_form.hidden_tag() }}
<button type="submit">Log&nbsp;Out</button>
</form>
{% with messages = get_flashed_messages()%}
{% if messages%}
@@ -23,14 +27,16 @@
<p>Your tokens:
<form method="POST" action="/generate-token">
<input type="text" name="token" placeholder="token name" />
<button type="submit">new&nbsp;token</button>
{{ generate_token_form.hidden_tag() }}
{{ generate_token_form.token_name(placeholder="Token Name") }}
<button type="submit">New&nbsp;Token</button>
</form>
<ul>{% for token in tokens %}
<li>
<form method="POST" action="/revoke-token">
<input type="text" name="token" readonly value="{{ token }}" />
<button type="submit">revoke</button>
{{ revoke_token_form.hidden_tag() }}
{{ revoke_token_form.token_name(placeholder="Token Name", readonly=True, value=token) }}
<button type="submit">Revoke</button>
</form>
</li>
{% endfor %}
@@ -39,10 +45,11 @@
<p>Password change
<form method="POST" action="/change-password">
<input type="password" name="pass" placeholder="old password" /><br />
<input type="password" name="pass-new" placeholder="new password" /><br />
<input type="password" name="pass-rep" placeholder="repeat password" /><br />
<button type="submit">change password</button>
{{ change_password_form.hidden_tag() }}
{{ change_password_form.password(placeholder="Old Password") }}<br />
{{ change_password_form.new_password(placeholder="New Password") }}<br />
{{ change_password_form.repeat_password(placeholder="Repeat Password") }}<br />
<button type="submit">Change Password</button>
</form>
</p>{% if is_admin %}{% include 'admin.html' %}{% endif %}
</body>
+4 -3
View File
@@ -14,9 +14,10 @@
{%endif%}
{%endwith%}
<form method="POST" action="/login">
<input type="text" name="user" placeholder="username" required autofocus /><br />
<input type="password" name="pass" placeholder="password" required /><br />
<form method="POST" action="/">
{{ login_form.hidden_tag() }}
{{ login_form.username(placeholder="Username", autofocus=True) }}<br />
{{ login_form.password(placeholder="Password") }}<br />
<button type="submit">log in</button>
</form>
</body>
+1
View File
@@ -1 +1,2 @@
flask
Flask-WTF