diff --git a/.gitignore b/.gitignore index 5368713..b5af3d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /venv assets/* + diff --git a/server/__pycache__/db_setup.cpython-311.pyc b/server/__pycache__/db_setup.cpython-311.pyc index faca429..7b950fc 100644 Binary files a/server/__pycache__/db_setup.cpython-311.pyc and b/server/__pycache__/db_setup.cpython-311.pyc differ diff --git a/server/__pycache__/security.cpython-311.pyc b/server/__pycache__/security.cpython-311.pyc index 65a2471..d2b4c2b 100644 Binary files a/server/__pycache__/security.cpython-311.pyc and b/server/__pycache__/security.cpython-311.pyc differ diff --git a/server/app.py b/server/app.py index 387a901..d2e863e 100644 --- a/server/app.py +++ b/server/app.py @@ -2,7 +2,7 @@ from flask import Flask, request, render_template, redirect, url_for, session, s from flask_talisman import Talisman from functools import wraps import os -from security import validate_user +from security import validate_user, is_ip_locked from data_handler import save_link, save_file, retrieve_uploads, handle_download, get_file_path from datetime import datetime from zipfile import ZipFile @@ -36,11 +36,21 @@ def login_required(f): @app.before_request def ensure_login(): - if 'username' not in session and request.endpoint not in ('login', 'static'): + ip_address = request.remote_addr + + # Check if the IP is locked and redirect to lockout if it is + if is_ip_locked(ip_address) and request.endpoint != 'lockout': + return redirect(url_for('lockout')) + + if 'username' not in session and request.endpoint not in ('login', 'static', 'lockout'): return redirect(url_for('login')) elif request.endpoint == 'login' and 'username' in session: session.clear() +@app.route('/lockout') +def lockout(): + return render_template("lockout.html") + @app.route('/') @login_required def index(): @@ -48,6 +58,12 @@ def index(): @app.route('/login', methods=['GET', 'POST']) def login(): + ip_address = request.remote_addr + + # Check if the IP is locked out + if is_ip_locked(ip_address): + return redirect(url_for('lockout')) + if request.method == 'POST': username = request.form['username'] password = request.form['password'] @@ -77,14 +93,18 @@ def upload_link(): @app.route('/upload/files', methods=['POST']) @login_required def upload_files(): - if 'files' not in request.files: + # Check if the field name 'files[]' is present in the request + if 'files[]' not in request.files: return redirect(url_for('index')) - files = request.files.getlist('files') + # Retrieve the list of files with 'files[]' + files = request.files.getlist('files[]') uploader = session['username'] + # Save each file for file in files: - save_file(uploader, file) + if file: # Check if a file was actually selected + save_file(uploader, file) return redirect(url_for('index')) diff --git a/server/db_setup.py b/server/db_setup.py index d8bb482..d3a80c9 100644 --- a/server/db_setup.py +++ b/server/db_setup.py @@ -99,8 +99,14 @@ def reset_login_attempts(username): def increment_login_attempts(username): with closing(sqlite3.connect(DATABASE)) as conn, conn, closing(conn.cursor()) as c: - c.execute('UPDATE users SET login_attempts = login_attempts + 1 WHERE username = ?', (username,)) - conn.commit() + if username: + c.execute('UPDATE users SET login_attempts = login_attempts + 1 WHERE username = ?', (username,)) + c.execute('SELECT login_attempts FROM users WHERE username = ?', (username,)) + login_attempts = c.fetchone()[0] + conn.commit() + return login_attempts + else: + return None def add_upload(uploader, file_type, content): with closing(sqlite3.connect(DATABASE)) as conn, conn, closing(conn.cursor()) as c: diff --git a/server/security.add.py b/server/security.add.py new file mode 100644 index 0000000..4629489 --- /dev/null +++ b/server/security.add.py @@ -0,0 +1,39 @@ +def validate_user(username, password): + ip_address = request.remote_addr + + # Check if the IP is locked + if is_ip_locked(ip_address): + return False, "You have been locked out." + + user_data = get_user(username) + if not user_data: + increment_login_attempts(None) # Increment failed attempts for any non-existent username attempt + + # Check if IP should be locked + attempts = increment_login_attempts(None) + if attempts >= MAX_ATTEMPTS: + lock_ip(ip_address) + return False, "Maximum login attempts exceeded. You have been locked out." + + remaining_attempts = MAX_ATTEMPTS - attempts + return False, f"User does not exist. {remaining_attempts} attempt(s) remaining." + + stored_password, salt, login_attempts = user_data + + # Check if the maximum login attempts have been reached + if login_attempts >= MAX_ATTEMPTS: + lock_ip(ip_address) + return False, "Maximum login attempts exceeded. You have been locked out." + + hashed_password = hash_password(password, salt) + if hashed_password == stored_password: + reset_login_attempts(username) + return True, "Login successful." + else: + increment_login_attempts(username) + if login_attempts + 1 >= MAX_ATTEMPTS: + lock_ip(ip_address) + return False, "Maximum login attempts exceeded. You have been locked out." + + remaining_attempts = MAX_ATTEMPTS - login_attempts - 1 + return False, f"Invalid credentials. {remaining_attempts} attempt(s) remaining." diff --git a/server/security.py b/server/security.py index 95f1cb6..a7031bc 100644 --- a/server/security.py +++ b/server/security.py @@ -1,9 +1,16 @@ -import os # Import for generating random salts +import os import hashlib from flask import request from db_setup import get_user, increment_login_attempts, reset_login_attempts MAX_ATTEMPTS = 3 +LOCKOUT_FILE = "locked_ips.txt" +FAILED_ATTEMPTS = {} + +# Ensure the locked_ips.txt file exists +if not os.path.exists(LOCKOUT_FILE): + with open(LOCKOUT_FILE, 'w') as f: + pass def generate_salt(): """ @@ -12,39 +19,68 @@ def generate_salt(): return os.urandom(16) def hash_password(password, salt): + """ + Hashes the password with the provided salt using SHA-256. + """ # Convert the salt to bytes if it's a string if isinstance(salt, str): salt = salt.encode() return hashlib.sha256(salt + password.encode()).hexdigest() +def is_ip_locked(ip): + """ + Checks if the IP address is in the lockout list. + """ + if os.path.exists(LOCKOUT_FILE): + with open(LOCKOUT_FILE, 'r') as f: + locked_ips = f.read().splitlines() + return ip in locked_ips + return False + +def lock_ip(ip): + """ + Adds an IP address to the lockout list. + """ + with open(LOCKOUT_FILE, 'a') as f: + f.write(ip + "\n") + def validate_user(username, password): - """ - Validates the user's credentials against stored data. - """ + ip_address = request.remote_addr + + # Check if the IP is locked + if is_ip_locked(ip_address): + return False, "You have been locked out." + + # Check or increment failed attempts for this IP address + if ip_address not in FAILED_ATTEMPTS: + FAILED_ATTEMPTS[ip_address] = 0 + user_data = get_user(username) if not user_data: - print(f"User '{username}' does not exist.") - return False, "User does not exist." + FAILED_ATTEMPTS[ip_address] += 1 + + if FAILED_ATTEMPTS[ip_address] >= MAX_ATTEMPTS: + lock_ip(ip_address) + return False, "Maximum login attempts exceeded. You have been locked out." + + remaining_attempts = MAX_ATTEMPTS - FAILED_ATTEMPTS[ip_address] + return False, f"User does not exist. {remaining_attempts} attempt(s) remaining." stored_password, salt, login_attempts = user_data - # Check if the maximum login attempts have been reached - if login_attempts >= MAX_ATTEMPTS: - print(f"User '{username}' has exceeded max login attempts.") - return False, "Maximum login attempts exceeded. Please contact the administrator." - - # Hash the provided password with the salt hashed_password = hash_password(password, salt) - print(f"Provided hash: {hashed_password}, Stored hash: {stored_password}") - if hashed_password == stored_password: reset_login_attempts(username) - print(f"User '{username}' logged in successfully.") + # Clear failed attempts for this IP on a successful login + FAILED_ATTEMPTS.pop(ip_address, None) return True, "Login successful." else: - increment_login_attempts(username) - remaining_attempts = MAX_ATTEMPTS - login_attempts - 1 - print(f"Invalid credentials for '{username}'. {remaining_attempts} attempt(s) remaining.") + FAILED_ATTEMPTS[ip_address] += 1 + if FAILED_ATTEMPTS[ip_address] >= MAX_ATTEMPTS: + lock_ip(ip_address) + return False, "Maximum login attempts exceeded. You have been locked out." + + remaining_attempts = MAX_ATTEMPTS - FAILED_ATTEMPTS[ip_address] return False, f"Invalid credentials. {remaining_attempts} attempt(s) remaining." def identify_uploader(): diff --git a/server/security.py.bak b/server/security.py.bak new file mode 100644 index 0000000..95f1cb6 --- /dev/null +++ b/server/security.py.bak @@ -0,0 +1,79 @@ +import os # Import for generating random salts +import hashlib +from flask import request +from db_setup import get_user, increment_login_attempts, reset_login_attempts + +MAX_ATTEMPTS = 3 + +def generate_salt(): + """ + Generates a 16-byte random salt. + """ + return os.urandom(16) + +def hash_password(password, salt): + # Convert the salt to bytes if it's a string + if isinstance(salt, str): + salt = salt.encode() + return hashlib.sha256(salt + password.encode()).hexdigest() + +def validate_user(username, password): + """ + Validates the user's credentials against stored data. + """ + user_data = get_user(username) + if not user_data: + print(f"User '{username}' does not exist.") + return False, "User does not exist." + + stored_password, salt, login_attempts = user_data + + # Check if the maximum login attempts have been reached + if login_attempts >= MAX_ATTEMPTS: + print(f"User '{username}' has exceeded max login attempts.") + return False, "Maximum login attempts exceeded. Please contact the administrator." + + # Hash the provided password with the salt + hashed_password = hash_password(password, salt) + print(f"Provided hash: {hashed_password}, Stored hash: {stored_password}") + + if hashed_password == stored_password: + reset_login_attempts(username) + print(f"User '{username}' logged in successfully.") + return True, "Login successful." + else: + increment_login_attempts(username) + remaining_attempts = MAX_ATTEMPTS - login_attempts - 1 + print(f"Invalid credentials for '{username}'. {remaining_attempts} attempt(s) remaining.") + return False, f"Invalid credentials. {remaining_attempts} attempt(s) remaining." + +def identify_uploader(): + """ + Identifies the uploader's device information from the request headers. + """ + device_info = get_device_info() + user_agent = device_info['user_agent'] + + if "iPhone" in user_agent: + device_type = "iPhone" + elif "Android" in user_agent: + device_type = "Android" + elif "Windows" in user_agent: + device_type = "Windows PC" + elif "Mac" in user_agent: + device_type = "Mac" + elif "Linux" in user_agent: + device_type = "Linux Machine" + else: + device_type = "Unknown Device" + + return f"Uploaded by {device_type} (IP: {device_info['ip']})" + +def get_device_info(): + """ + Extracts device information from the request. + """ + return { + "ip": request.remote_addr or "Unknown IP", + "user_agent": request.headers.get('User-Agent', 'Unknown'), + } diff --git a/static/error.css b/static/error.css new file mode 100644 index 0000000..c4ae783 --- /dev/null +++ b/static/error.css @@ -0,0 +1,47 @@ +/* error.css */ +body { + font-family: Arial, sans-serif; + background-color: #f8d7da; + color: #721c24; + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + height: 100vh; +} + +.error-container { + background-color: #f5c6cb; + padding: 20px 40px; + border: 1px solid #f1b0b7; + border-radius: 8px; + text-align: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +h1 { + font-size: 2em; + margin: 0 0 10px 0; +} + +p { + font-size: 1.2em; + margin: 10px 0; +} + +a { + color: #721c24; + text-decoration: none; + font-weight: bold; + background-color: #f1b0b7; + padding: 8px 15px; + border-radius: 5px; + display: inline-block; + margin-top: 10px; +} + +a:hover { + background-color: #f8d7da; +} + diff --git a/static/eye.jpeg b/static/eye.jpeg new file mode 100644 index 0000000..75fa3af Binary files /dev/null and b/static/eye.jpeg differ diff --git a/static/index.css b/static/index.css new file mode 100644 index 0000000..95c8819 --- /dev/null +++ b/static/index.css @@ -0,0 +1,85 @@ +/* static/index.css */ +body { + background-color: #f0f4f8; + font-family: Arial, sans-serif; + color: #333; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + text-align: center; +} + +.container { + background-color: #ffffff; + padding: 30px 40px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + max-width: 500px; + width: 100%; +} + +h1 { + color: #007acc; + margin-bottom: 20px; +} + +h2 { + color: #333; + margin: 15px 0; + font-size: 1.2em; +} + +form { + margin: 15px 0; +} + +input[type="text"], input[type="file"] { + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + width: 100%; + box-sizing: border-box; + margin-bottom: 10px; +} + +button { + background-color: #007acc; + color: #fff; + border: none; + padding: 10px 15px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #005f99; +} + +button:active { + background-color: #004e7a; +} + +button:focus { + outline: none; + box-shadow: 0 0 5px #007acc; +} + +#linkForm, #fileForm { + display: flex; + flex-direction: column; + align-items: center; +} + +#linkForm input[type="text"], #fileForm input[type="file"] { + width: 80%; + margin-bottom: 10px; +} + +.view-btn { + margin-top: 20px; + display: inline-block; +} diff --git a/static/lockout.css b/static/lockout.css new file mode 100644 index 0000000..efd661c --- /dev/null +++ b/static/lockout.css @@ -0,0 +1,55 @@ +/* static/lockout.css */ +body { + background-color: #f8d7da; + font-family: Arial, sans-serif; + color: #721c24; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + text-align: center; +} + +.container { + background-color: #ffffff; + padding: 30px 40px; + border: 2px solid #f5c6cb; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + max-width: 600px; + width: 100%; +} + +h1 { + color: #721c24; + margin-bottom: 20px; +} + +p { + color: #856404; + background-color: #fff3cd; + border: 1px solid #ffeeba; + padding: 10px; + border-radius: 4px; +} + +.contact { + margin-top: 20px; + color: #004085; + background-color: #cce5ff; + border: 1px solid #b8daff; + padding: 10px; + border-radius: 4px; +} + +.contact a { + color: #004085; + text-decoration: none; + font-weight: bold; +} + +.contact a:hover { + text-decoration: underline; +} diff --git a/static/login.css b/static/login.css index 4f50b15..7faaeaa 100644 --- a/static/login.css +++ b/static/login.css @@ -1,6 +1,10 @@ /* Centering the login container */ body { - background-color: #f0f0f0; /* Light grey background for the whole page */ + background-image: url('/static/eye.jpeg'); + background-size: cover; /* Ensures the image covers the entire background */ + background-position: center; /* Centers the image */ + background-repeat: no-repeat; /* Prevents the background from repeating */ + background-color: #f0f0f0; /* Fallback color if the image doesn't load */ display: flex; justify-content: center; align-items: center; diff --git a/static/uploads.css b/static/uploads.css new file mode 100644 index 0000000..3005e46 --- /dev/null +++ b/static/uploads.css @@ -0,0 +1,102 @@ +/* static/uploads.css */ +body { + background-color: #f0f4f8; + font-family: Arial, sans-serif; + color: #333; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + text-align: center; +} + +.container { + background-color: #ffffff; + padding: 30px 40px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + max-width: 700px; + width: 100%; +} + +h1 { + color: #007acc; + margin-bottom: 20px; +} + +h2 { + color: #333; + margin: 15px 0; + font-size: 1.2em; + border-bottom: 2px solid #007acc; + display: inline-block; + padding-bottom: 5px; +} + +ul { + list-style-type: none; + padding: 0; +} + +li { + padding: 10px; + margin: 5px 0; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f9f9f9; + display: flex; + justify-content: space-between; + align-items: center; +} + +li:nth-child(odd) { + background-color: #eef3f7; +} + +button { + background-color: #007acc; + color: #fff; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; + margin-left: 5px; +} + +button:hover { + background-color: #005f99; +} + +button:focus { + outline: none; + box-shadow: 0 0 5px #007acc; +} + +a { + color: #007acc; + text-decoration: none; + transition: color 0.3s ease; +} + +a:hover { + color: #005f99; +} + +.back-link { + display: inline-block; + margin-top: 20px; + background-color: #007acc; + color: #fff; + padding: 10px 20px; + text-decoration: none; + border-radius: 4px; + transition: background-color 0.3s ease; +} + +.back-link:hover { + background-color: #005f99; +} + diff --git a/templates/error.html b/templates/error.html index 9fd3894..9be59d1 100644 --- a/templates/error.html +++ b/templates/error.html @@ -4,10 +4,13 @@
{{ error_message }}
- Go back to the main page +