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 252 additions and 134 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 Every file that is written in human-readable format, meant to be read
associated documentation files (the "Software"), to deal in the Software without restriction, including by humans and not used by the software itself (documentation, readme, etc.),
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell has to end with the word "izbi", separated from the rest of the
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the aforementioned file by a newline character.
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial Only other students and/or staff of AGH's Faculty of Computer Science
portions of the Software. (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 Any other person, who is unrelated to WI AGH and wishes to use this software is allowed to,
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO but cannot publish for profit or modify any version of the full software
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER or parts of its source code, including (but not limited to) merging and/or sublicensing,
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE except where such modification is private and cannot be discovered or
USE OR OTHER DEALINGS IN THE SOFTWARE. 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
+3 -1
View File
@@ -1,3 +1,5 @@
# IZBI DNS # IZBI DNS
Dynamic DNS for `.izbi` domains. 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: 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") raise ValueError("Token does not exist")
with sqlite3.connect(db_path) as connection: with sqlite3.connect(db_path) as connection:
@@ -429,4 +429,4 @@ class Token:
WHERE tkhash = ? WHERE tkhash = ?
''', (tkhash, )) ''', (tkhash, ))
return cursor.fetcone()[0] return cursor.fetchone()[0]
+159 -99
View File
@@ -3,7 +3,10 @@ import auth
import db import db
import re import re
from os import environ, urandom 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" 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" 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["PERMANENT_SESSION_LIFETIME"] = 3600
app.config["SECRET_KEY"] = environ["SECRET_KEY"] if "SECRET_KEY" in environ else urandom(32).hex() app.config["SECRET_KEY"] = environ["SECRET_KEY"] if "SECRET_KEY" in environ else urandom(32).hex()
hosts = {} csrf = CSRFProtect(app)
hosts = {}
# Update hosts file # Update hosts file
def update_hosts(path: str, hosts: dict = {}) -> bool: def update_hosts(path: str, hosts: dict = {}) -> bool:
@@ -38,34 +42,61 @@ def validate_ip(ip: str) -> bool:
return re.search(exp, ip) return re.search(exp, ip)
class LoginForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
password = PasswordField('password', validators=[DataRequired()])
# Serve homepage # Serve homepage
@app.route('/', methods=["GET"]) @app.route('/', methods=["GET", "POST"])
def homepage(): def index():
if session.get("USERID") is not None: if session.get("USERID") is not None:
return redirect("/dashboard") return redirect("/dashboard")
return render_template("index.html")
login_form = LoginForm()
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")
return redirect("/")
session["USERID"] = userid
return redirect("/dashboard")
return render_template("index.html", login_form=login_form)
# Handle login class LogoutForm(FlaskForm):
@app.route('/login', methods=["POST"]) pass
def login():
userid = auth.user_login(DB_PATH, request.form.get("user"), request.form.get("pass"))
if userid is None: class GenerateTokenForm(FlaskForm):
flash("Invalid user or password") token_name = StringField('token-name', validators=[DataRequired()])
return redirect("/")
session["USERID"] = userid class RevokeTokenForm(FlaskForm):
token_name = StringField('token-name', validators=[DataRequired()])
return redirect("/dashboard") 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')
# Log out by clearing session data class UpdateUserForm(FlaskForm): #TODO
@app.route('/logout', methods=["GET"]) pass
def logout():
session.clear()
return redirect("/")
class DeleteUserForm(FlaskForm):
userid = HiddenField('userid', validators=[DataRequired()])
# Display dashboard for logged in users # Display dashboard for logged in users
@app.route('/dashboard', methods=["GET"]) @app.route('/dashboard', methods=["GET"])
@@ -73,6 +104,11 @@ def dashboard():
if session.get("USERID") is None: if session.get("USERID") is None:
return redirect("/") return redirect("/")
logout_form = LogoutForm()
generate_token_form = GenerateTokenForm()
revoke_token_form = RevokeTokenForm()
change_password_form = ChangePasswordForm()
userid = session.get("USERID") userid = session.get("USERID")
username = db.User.get_username(DB_PATH, userid) username = db.User.get_username(DB_PATH, userid)
@@ -81,10 +117,32 @@ def dashboard():
# If user is admin, display additional options # If user is admin, display additional options
if db.User.is_admin(DB_PATH, userid): if db.User.is_admin(DB_PATH, userid):
users = db.list_users(DB_PATH) create_user_form = CreateUserForm()
return render_template("dashboard.html", username=username, domains=domains, tokens=tokens, is_admin=True, users=users) 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 # Generate new access token with a name
@@ -93,18 +151,18 @@ def generate_token():
if session.get("USERID") is None: if session.get("USERID") is None:
return redirect("/") return redirect("/")
if "token" not in request.form: generate_token_form = GenerateTokenForm()
flash("Token name not supplied")
return redirect("/dashboard")
try: if generate_token_form.validate_on_submit():
token = db.User.generate_token(DB_PATH, session.get("USERID"), request.form["token"]) token_name = generate_token_form.token_name.data
except ValueError as e: try:
flash(str(e)) token = db.User.generate_token(DB_PATH, session.get("USERID"), token_name)
token = None except ValueError as e:
flash(str(e))
token = None
if token is not None: if token is not None:
flash(token) flash(token)
return redirect("/dashboard") return redirect("/dashboard")
@@ -115,14 +173,42 @@ def revoke_token():
if session.get("USERID") is None: if session.get("USERID") is None:
return redirect("/") return redirect("/")
if "token" not in request.form: revoke_token_form = RevokeTokenForm()
flash("Token name not supplied")
return redirect("/dashboard")
try: if revoke_token_form.validate_on_submit():
db.User.revoke_token(DB_PATH, session.get("USERID"), request.form["token"]) token_name = revoke_token_form.token_name.data
except ValueError as e: try:
flash(str(e)) 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:
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") return redirect("/dashboard")
@@ -135,37 +221,32 @@ def create_user():
if not db.User.is_admin(DB_PATH, session.get("USERID")): if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard") return redirect("/dashboard")
if 'username' not in request.form or 'password' not in request.form: create_user_form = CreateUserForm()
flash('Required fields not supplied')
return redirect("/dashboard")
username = request.form.get("username").strip() if create_user_form.validate_on_submit():
password = request.form.get("password") 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: if domains is None:
domains = [] domains = []
else: else:
# Collapse whitespaces # Collapse whitespaces
collapse_exp = re.compile(r'\s+') collapse_exp = re.compile(r'\s+')
domains = collapse_exp.sub(' ', domains.strip()) domains = collapse_exp.sub(' ', domains.strip())
domains = domains.split(" ") domains = domains.split(" ")
is_admin = request.form.get("is_admin") is_admin = create_user_form.is_admin.data
if is_admin is not None:
is_admin = is_admin == "true"
else:
is_admin = False
email = request.form.get("email") email = create_user_form.email.data
if email is not None: if email is not None:
if email == "": if email == "":
email = None email = None
try: try:
db.User.new(DB_PATH, username, password, domains, is_admin, email) db.User.new(DB_PATH, username, password, domains, is_admin, email)
except ValueError as e: except ValueError as e:
flash(str(e)) flash(str(e))
return redirect("/dashboard") return redirect("/dashboard")
@@ -178,6 +259,11 @@ def update_user():
if not db.User.is_admin(DB_PATH, session.get("USERID")): if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard") return redirect("/dashboard")
update_user_form = UpdateUserForm()
if not update_user_form.validate_on_submit():
return redirect("/dashboard")
userid = request.form.get("userid") userid = request.form.get("userid")
if userid is None: if userid is None:
flash("UserID not supplied") flash("UserID not supplied")
@@ -226,46 +312,20 @@ def delete_user():
if not db.User.is_admin(DB_PATH, session.get("USERID")): if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard") return redirect("/dashboard")
userid = request.form.get("userid") delete_user_form = DeleteUserForm()
if userid is None:
return redirect("/dashboard")
if db.User.exists(DB_PATH, userid): if delete_user_form.validate_on_submit():
flash("User does not exst") userid = delete_user_form.userid.data
return redirect("/dashboard")
db.User.delete(DB_PATH, userid) if not db.User.exists(DB_PATH, userid):
flash("User does not exst")
return redirect("/dashboard")
db.User.delete(DB_PATH, userid)
return redirect("/dashboard") 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 ??? # API health endpoint ???
@app.route('/health', methods=["GET"]) @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> <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> <form method="POST" action="/create-user"><tr>
<td><input type="text" name="username" placeholder="*user name" required /></td> {{ create_user_form.hidden_tag() }}
<td><input type="text" name="domains" placeholder="space separated domains" /></td> <td>{{ create_user_form.username(placeholder="*Username") }}</td>
<td><input type="password" name="password" placeholder="*password" required /></td> <td>{{ create_user_form.domains(placeholder="Space separated domains") }}</td>
<td><input type="checkbox" name="is_admin" /></td> <td>{{ create_user_form.password(placeholder="*Password") }}</td>
<td><input type="email" name="email" placeholder="email" /></td> <td>{{ create_user_form.is_admin() }}</td>
<td>{{ create_user_form.email(placeholder="Email") }}</td>
<td><button type="submit">Add</button></td> <td><button type="submit">Add</button></td>
</tr></form> </tr></form>
@@ -19,28 +20,33 @@
<tr> <tr>
<td>{{ user["username"] }}</td> <td>{{ user["username"] }}</td>
<td><form method="POST" action="/update-user"> <td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> <input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="text" name="domains" placeholder="space separated domains" value="{{ user["domains"] | join(' ') }}" /> <input type="text" name="domains" placeholder="space separated domains" value="{{ user["domains"] | join(' ') }}" />
<button type="submit">update domains</button> <button type="submit">update domains</button>
</form></td> </form></td>
<td><form method="POST" action="/update-user"> <td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> <input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="password" name="password" placeholder="*password" required /> <input type="password" name="password" placeholder="*password" required />
<button type="submit">update password</button> <button type="submit">update password</button>
</form></td> </form></td>
<td><form method="POST" action="/update-user"> <td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> <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="hidden" name="is_admin" value="{% if user["is_admin"] %}false{% else %}true{% endif %}" />
<input type="checkbox" disabled {% if user["is_admin"] %}checked{% endif %} /> <input type="checkbox" disabled {% if user["is_admin"] %}checked{% endif %} />
<button type="submit">toggle admin</button> <button type="submit">toggle admin</button>
</form></td> </form></td>
<td><form method="POST" action="/update-user"> <td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> <input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="email" name="email" placeholder="email" value="{{ user["email"] if user["email"] }}"/> <input type="email" name="email" placeholder="email" value="{{ user["email"] if user["email"] }}"/>
<button type="submit">update email</button> <button type="submit">update email</button>
</form></td> </form></td>
<td><form method="POST" action="/delete-user"> <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> <button type="submit">Delete</button>
</form></td> </form></td>
</tr> </tr>
+16 -9
View File
@@ -4,7 +4,11 @@
<title>Dashboard - IZBI DNS</title> <title>Dashboard - IZBI DNS</title>
</head> </head>
<body> <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()%} {% with messages = get_flashed_messages()%}
{% if messages%} {% if messages%}
@@ -23,14 +27,16 @@
<p>Your tokens: <p>Your tokens:
<form method="POST" action="/generate-token"> <form method="POST" action="/generate-token">
<input type="text" name="token" placeholder="token name" /> {{ generate_token_form.hidden_tag() }}
<button type="submit">new&nbsp;token</button> {{ generate_token_form.token_name(placeholder="Token Name") }}
<button type="submit">New&nbsp;Token</button>
</form> </form>
<ul>{% for token in tokens %} <ul>{% for token in tokens %}
<li> <li>
<form method="POST" action="/revoke-token"> <form method="POST" action="/revoke-token">
<input type="text" name="token" readonly value="{{ token }}" /> {{ revoke_token_form.hidden_tag() }}
<button type="submit">revoke</button> {{ revoke_token_form.token_name(placeholder="Token Name", readonly=True, value=token) }}
<button type="submit">Revoke</button>
</form> </form>
</li> </li>
{% endfor %} {% endfor %}
@@ -39,10 +45,11 @@
<p>Password change <p>Password change
<form method="POST" action="/change-password"> <form method="POST" action="/change-password">
<input type="password" name="pass" placeholder="old password" /><br /> {{ change_password_form.hidden_tag() }}
<input type="password" name="pass-new" placeholder="new password" /><br /> {{ change_password_form.password(placeholder="Old Password") }}<br />
<input type="password" name="pass-rep" placeholder="repeat password" /><br /> {{ change_password_form.new_password(placeholder="New Password") }}<br />
<button type="submit">change password</button> {{ change_password_form.repeat_password(placeholder="Repeat Password") }}<br />
<button type="submit">Change Password</button>
</form> </form>
</p>{% if is_admin %}{% include 'admin.html' %}{% endif %} </p>{% if is_admin %}{% include 'admin.html' %}{% endif %}
</body> </body>
+4 -3
View File
@@ -14,9 +14,10 @@
{%endif%} {%endif%}
{%endwith%} {%endwith%}
<form method="POST" action="/login"> <form method="POST" action="/">
<input type="text" name="user" placeholder="username" required autofocus /><br /> {{ login_form.hidden_tag() }}
<input type="password" name="pass" placeholder="password" required /><br /> {{ login_form.username(placeholder="Username", autofocus=True) }}<br />
{{ login_form.password(placeholder="Password") }}<br />
<button type="submit">log in</button> <button type="submit">log in</button>
</form> </form>
</body> </body>
+1
View File
@@ -1 +1,2 @@
flask flask
Flask-WTF