diff --git a/server/__pycache__/db_setup.cpython-311.pyc b/server/__pycache__/db_setup.cpython-311.pyc index 26b264e..faca429 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__/preview.cpython-311.pyc b/server/__pycache__/preview.cpython-311.pyc new file mode 100644 index 0000000..8ec8145 Binary files /dev/null and b/server/__pycache__/preview.cpython-311.pyc differ diff --git a/server/__pycache__/rename.cpython-311.pyc b/server/__pycache__/rename.cpython-311.pyc new file mode 100644 index 0000000..8de3d8a Binary files /dev/null and b/server/__pycache__/rename.cpython-311.pyc differ diff --git a/server/__pycache__/security.cpython-311.pyc b/server/__pycache__/security.cpython-311.pyc index 3033317..cb17c8d 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 6fb9e6d..1d1713f 100644 --- a/server/app.py +++ b/server/app.py @@ -1,10 +1,13 @@ -from flask import Flask, request, render_template, redirect, url_for, session, send_from_directory +from flask import Flask, request, render_template, redirect, url_for, session, send_from_directory, jsonify, send_file from flask_talisman import Talisman from functools import wraps import os from security import validate_user from data_handler import save_link, save_file, retrieve_uploads, handle_download, get_file_path from datetime import datetime +from zipfile import ZipFile +from preview import generate_preview # Import the preview module +from rename import rename_file # Import the rename module app = Flask(__name__, template_folder='../templates') app.secret_key = os.urandom(24) @@ -35,6 +38,8 @@ def login_required(f): def ensure_login(): if 'username' not in session and request.endpoint not in ('login', 'static'): return redirect(url_for('login')) + elif request.endpoint == 'login' and 'username' in session: + session.clear() @app.route('/') @login_required @@ -69,15 +74,18 @@ def upload_link(): save_link(uploader, link) return redirect(url_for('index')) -@app.route('/upload/file', methods=['POST']) +@app.route('/upload/files', methods=['POST']) @login_required -def upload_file(): - if 'file' not in request.files: +def upload_files(): + if 'files' not in request.files: return redirect(url_for('index')) - file = request.files['file'] + files = request.files.getlist('files') uploader = session['username'] - save_file(uploader, file) + + for file in files: + save_file(uploader, file) + return redirect(url_for('index')) @app.route('/uploads') @@ -104,14 +112,12 @@ def view_uploads(): def download_link(link_id): upload = handle_download(link_id) - if upload[2] == 'link': + if upload and upload[2] == 'link': link_content = upload[3] - x = 1 while os.path.exists(os.path.join(DOWNLOADS_DIRECTORY, f"link_{x}.txt")): x += 1 filename = f"link_{x}.txt" - filepath = os.path.join(DOWNLOADS_DIRECTORY, filename) with open(filepath, 'w') as f: f.write(link_content) @@ -119,7 +125,6 @@ def download_link(link_id): response = send_from_directory(DOWNLOADS_DIRECTORY, filename, as_attachment=True) handle_download(link_id, delete_only=True) - return response return "Link Not found", 404 @@ -137,13 +142,9 @@ def download_all_links(): for link in links: f.write(link[3] + "\n") - # Serve the combined links file from the Downloads directory response = send_from_directory(DOWNLOADS_DIRECTORY, filename, as_attachment=True) - - # Now delete all the link entries from the database after serving for link in links: handle_download(link[0], delete_only=True) - return response else: return redirect(url_for('view_uploads')) @@ -153,23 +154,22 @@ def download_all_links(): def download(upload_id): upload = handle_download(upload_id) - if not upload: - print(f"Error: No upload found with ID {upload_id}") - return "The requested file does not exist or you do not have permission to access it.", 404 - - if upload[2] == 'file': + if upload and upload[2] == 'file': file_path = get_file_path(upload[3]) - print(f"Trying to download file: {file_path}") - if not os.path.isfile(file_path): - print(f"Error: File not found at {file_path}") - return "File not found", 404 - - response = send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True) + return redirect(url_for('view_uploads'), code=404) + + response = send_from_directory( + directory=os.path.dirname(file_path), + path=os.path.basename(file_path), + as_attachment=True, + #download_name=os.path.basename(file_path) # Ensures the download retains the current name + download_name=upload[3] + ) handle_download(upload_id, delete_only=True) - - return response + return response + return "The requested file does not exist or you do not have permission to access it.", 404 @app.route('/delete_link/', methods=['GET']) @login_required @@ -183,6 +183,88 @@ def delete_file(file_id): handle_download(file_id, delete_only=True) return redirect(url_for('view_uploads')) -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, ssl_context='adhoc') +# Add routes for downloading all photos +@app.route('/download_all_photos', methods=['GET']) +@login_required +def download_all_photos(): + photos = [upload for upload in retrieve_uploads() if upload[2] == 'file' and upload[3].lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))] + if photos: + zip_filename = f"photos_{datetime.now().strftime('%m-%d-%Y')}.zip" + zip_filepath = os.path.join(DOWNLOADS_DIRECTORY, zip_filename) + + # Create a zip file containing all photos + with ZipFile(zip_filepath, 'w') as zipf: + for photo in photos: + file_path = get_file_path(photo[3]) + zipf.write(file_path, os.path.basename(file_path)) + handle_download(photo[0], delete_only=True) # Delete each photo entry + + return send_from_directory(DOWNLOADS_DIRECTORY, zip_filename, as_attachment=True) + else: + return redirect(url_for('view_uploads')) + +# Add similar routes for videos and misc +@app.route('/download_all_videos', methods=['GET']) +@login_required +def download_all_videos(): + videos = [upload for upload in retrieve_uploads() if upload[2] == 'file' and upload[3].lower().endswith(('.mp4', '.mkv', '.avi'))] + + if videos: + zip_filename = f"videos_{datetime.now().strftime('%m-%d-%Y')}.zip" + zip_filepath = os.path.join(DOWNLOADS_DIRECTORY, zip_filename) + + with ZipFile(zip_filepath, 'w') as zipf: + for video in videos: + file_path = get_file_path(video[3]) + zipf.write(file_path, os.path.basename(file_path)) + handle_download(video[0], delete_only=True) # Delete each video entry + + return send_from_directory(DOWNLOADS_DIRECTORY, zip_filename, as_attachment=True) + else: + return redirect(url_for('view_uploads')) + +@app.route('/download_all_misc', methods=['GET']) +@login_required +def download_all_misc(): + misc_files = [upload for upload in retrieve_uploads() if upload[2] == 'file' and upload not in videos + photos] + + if misc_files: + zip_filename = f"misc_{datetime.now().strftime('%m-%d-%Y')}.zip" + zip_filepath = os.path.join(DOWNLOADS_DIRECTORY, zip_filename) + + with ZipFile(zip_filepath, 'w') as zipf: + for item in misc_files: + file_path = get_file_path(item[3]) + zipf.write(file_path, os.path.basename(file_path)) + handle_download(item[0], delete_only=True) # Delete each misc entry + + return send_from_directory(DOWNLOADS_DIRECTORY, zip_filename, as_attachment=True) + else: + return redirect(url_for('view_uploads')) + +# Route to handle file renaming +@app.route('/rename/', methods=['POST']) +@login_required +def rename(upload_id): + data = request.get_json() # Expecting JSON data from the frontend + new_name = data.get('new_name', '').strip() + + if not new_name: + return jsonify({"error": "New name not provided"}), 400 + + success, message = rename_file(upload_id, new_name) + + if success: + return jsonify({"success": True}), 200 + else: + return jsonify({"error": message}), 400 + +# Route to handle file previews +@app.route('/preview/', methods=['GET']) +@login_required +def preview(upload_id): + return generate_preview(upload_id) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True, ssl_context='adhoc') diff --git a/server/app.py.bak b/server/app.py.bak index 1fb32f9..49b02c2 100644 --- a/server/app.py.bak +++ b/server/app.py.bak @@ -4,19 +4,23 @@ from functools import wraps import os from security import validate_user from data_handler import save_link, save_file, retrieve_uploads, handle_download, get_file_path +from datetime import datetime app = Flask(__name__, template_folder='../templates') -app.secret_key = os.urandom(24) # Generate a more secure secret key +app.secret_key = os.urandom(24) talisman = Talisman(app, content_security_policy={ 'default-src': ["'self'"], - 'script-src': ["'self'", "'unsafe-inline'"] # Allow inline scripts + 'script-src': ["'self'", "'unsafe-inline'"] }) UPLOAD_DIRECTORY = "../assets" if not os.path.exists(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) -# Login required decorator +DOWNLOADS_DIRECTORY = os.path.expanduser("~/Downloads") +if not os.path.exists(DOWNLOADS_DIRECTORY): + os.makedirs(DOWNLOADS_DIRECTORY) + def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): @@ -65,15 +69,18 @@ def upload_link(): save_link(uploader, link) return redirect(url_for('index')) -@app.route('/upload/file', methods=['POST']) +@app.route('/upload/files', methods=['POST']) @login_required -def upload_file(): - if 'file' not in request.files: +def upload_files(): + if 'files' not in request.files: return redirect(url_for('index')) - file = request.files['file'] + files = request.files.getlist('files') uploader = session['username'] - save_file(uploader, file) + + for file in files: + save_file(uploader, file) + return redirect(url_for('index')) @app.route('/uploads') @@ -81,7 +88,6 @@ def upload_file(): def view_uploads(): uploads = retrieve_uploads() - # Categorizing uploads links = [upload for upload in uploads if upload[2] == 'link'] videos = [upload for upload in uploads if upload[2] == 'file' and upload[3].lower().endswith(('.mp4', '.mkv', '.avi'))] photos = [upload for upload in uploads if upload[2] == 'file' and upload[3].lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))] @@ -99,24 +105,23 @@ def view_uploads(): @app.route('/download_link/', methods=['GET']) @login_required def download_link(link_id): - uploader = session['username'] - upload = handle_download(link_id, uploader) + upload = handle_download(link_id) - if upload[2] == 'link': + if upload and upload[2] == 'link': link_content = upload[3] - - # Create a unique filename x = 1 - while os.path.exists(f"link_{x}.txt"): + while os.path.exists(os.path.join(DOWNLOADS_DIRECTORY, f"link_{x}.txt")): x += 1 filename = f"link_{x}.txt" - - # Save the link content to the file - with open(filename, 'w') as f: + filepath = os.path.join(DOWNLOADS_DIRECTORY, filename) + with open(filepath, 'w') as f: f.write(link_content) - # Serve the file - return send_from_directory(directory=os.getcwd(), filename=filename, as_attachment=True) + response = send_from_directory(DOWNLOADS_DIRECTORY, filename, as_attachment=True) + + handle_download(link_id, delete_only=True) + return response + return "Link Not found", 404 @app.route('/download_all_links', methods=['GET']) @login_required @@ -124,26 +129,48 @@ def download_all_links(): links = [upload for upload in retrieve_uploads() if upload[2] == 'link'] if len(links) > 1: - with open("links_data.txt", 'w') as f: + current_date = datetime.now().strftime("%m-%d-%Y") + filename = f"links_{current_date}.txt" + + links_file_path = os.path.join(DOWNLOADS_DIRECTORY, filename) + with open(links_file_path, 'w') as f: for link in links: f.write(link[3] + "\n") - return send_from_directory(directory=os.getcwd(), filename="links_data.txt", as_attachment=True) + response = send_from_directory(DOWNLOADS_DIRECTORY, filename, as_attachment=True) + for link in links: + handle_download(link[0], delete_only=True) + return response else: return redirect(url_for('view_uploads')) @app.route('/download/', methods=['GET']) @login_required def download(upload_id): - uploader = session['username'] - upload = handle_download(upload_id, uploader) - - if upload[2] == 'link': - return f"{upload[3]}" - elif upload[2] == 'file': - # Integrate get_file_path here + upload = handle_download(upload_id) + + if upload and upload[2] == 'file': file_path = get_file_path(upload[3]) - return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True) + if not os.path.isfile(file_path): + return "File not found", 404 + + response = send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True) + handle_download(upload_id, delete_only=True) + return response + + return "The requested file does not exist or you do not have permission to access it.", 404 + +@app.route('/delete_link/', methods=['GET']) +@login_required +def delete_link(link_id): + handle_download(link_id, delete_only=True) + return redirect(url_for('view_uploads')) + +@app.route('/delete_file/', methods=['GET']) +@login_required +def delete_file(file_id): + handle_download(file_id, delete_only=True) + return redirect(url_for('view_uploads')) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, ssl_context='adhoc') diff --git a/server/db_setup.py b/server/db_setup.py index aed3081..d8bb482 100644 --- a/server/db_setup.py +++ b/server/db_setup.py @@ -1,18 +1,20 @@ # server/db_setup.py import sqlite3 import hashlib +import os from contextlib import closing DATABASE = 'transfer_service.db' def initialize_db(): with closing(sqlite3.connect(DATABASE)) as conn, conn, closing(conn.cursor()) as c: - # Create users table + # Create users table with salt c.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, + salt TEXT NOT NULL, login_attempts INTEGER DEFAULT 0 ) ''') @@ -28,21 +30,66 @@ def initialize_db(): ) ''') + # Check if the 'salt' column exists in the users table and add it if missing + c.execute("PRAGMA table_info(users)") + columns = [column[1] for column in c.fetchall()] + if 'salt' not in columns: + c.execute("ALTER TABLE users ADD COLUMN salt TEXT") + update_existing_users_with_salts() # Update existing users with salts + conn.commit() +def generate_salt(): + return os.urandom(16).hex() + +def hash_password(password, salt): + return hashlib.sha256((password + salt).encode()).hexdigest() + +def update_existing_users_with_salts(): + """ + Updates existing users to include a unique salt and rehashes their passwords. + """ + with closing(sqlite3.connect(DATABASE)) as conn, closing(conn.cursor()) as c: + c.execute('SELECT id, password FROM users') + users = c.fetchall() + + for user_id, password in users: + salt = generate_salt() + hashed_password = hash_password(password, salt) + c.execute('UPDATE users SET password = ?, salt = ? WHERE id = ?', (hashed_password, salt, user_id)) + + conn.commit() + print("Updated existing users with salts.") + def add_user(username, password): - hashed_password = hashlib.sha256(password.encode()).hexdigest() + # Generate a unique salt for each user + salt = os.urandom(16) + hashed_password = hashlib.sha256(salt + password.encode()).hexdigest() + try: with closing(sqlite3.connect(DATABASE)) as conn, conn, closing(conn.cursor()) as c: - c.execute('INSERT INTO users (username, password) VALUES (?, ?)', (username, hashed_password)) + c.execute('INSERT INTO users (username, password, salt) VALUES (?, ?, ?)', (username, hashed_password, salt)) conn.commit() print(f"User '{username}' added successfully.") except sqlite3.IntegrityError: print(f"User '{username}' already exists.") +def delete_user(username): + """ + Deletes a user from the database based on the provided username. + """ + try: + with closing(sqlite3.connect(DATABASE)) as conn, conn, closing(conn.cursor()) as c: + c.execute('DELETE FROM users WHERE username = ?', (username,)) + conn.commit() + print(f"User '{username}' deleted successfully.") + except sqlite3.Error as e: + print(f"Error deleting user '{username}': {e}") + def get_user(username): with closing(sqlite3.connect(DATABASE)) as conn, closing(conn.cursor()) as c: - c.execute('SELECT username, password, login_attempts FROM users WHERE username = ?', (username,)) + # Select only password, salt, and login_attempts + c.execute('SELECT password, salt, login_attempts FROM users WHERE username = ?', (username,)) return c.fetchone() def reset_login_attempts(username): @@ -70,8 +117,17 @@ def delete_upload(upload_id): c.execute('DELETE FROM uploads WHERE id = ?', (upload_id,)) conn.commit() +def update_upload_filename(upload_id, new_name): + with closing(sqlite3.connect(DATABASE)) as conn, closing(conn.cursor()) as c: + c.execute('UPDATE uploads SET content = ? WHERE id = ?', (new_name, upload_id)) + conn.commit() + if __name__ == '__main__': initialize_db() + # Example of initializing users (only run manually) -# add_user('iphone_user', 'your_secure_password') -# add_user('laptop_user', 'your_secure_password') + # add_user('iphone_user', 'your_secure_password') + # add_user('laptop_user', 'your_secure_password') + # Example of deleting user + # delete_user('iphone_user') + # delete_user('laptop_user') diff --git a/server/preview.py b/server/preview.py new file mode 100644 index 0000000..692b8b5 --- /dev/null +++ b/server/preview.py @@ -0,0 +1,37 @@ +# preview.py +import os +from flask import send_file, jsonify +import sqlite3 +import mimetypes + +UPLOAD_DIRECTORY = "../assets" + +def generate_preview(upload_id): + conn = sqlite3.connect('transfer_service.db') + c = conn.cursor() + + c.execute('SELECT file_type, content FROM uploads WHERE id = ?', (upload_id,)) + upload = c.fetchone() + + if not upload: + conn.close() + return jsonify({"error": "File not found"}), 404 + + file_type, filename = upload + file_path = os.path.join(UPLOAD_DIRECTORY, filename) + + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + # Detect the MIME type + mime_type, _ = mimetypes.guess_type(filename) + + # Handle different file types for preview + if file_type == "link": + return jsonify({"link": filename}), 200 + + # Send the file with the correct MIME type + if file_type == "file" and filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): + return send_file(file_path, mimetype='image/jpeg', download_name=os.path.basename(file_path)) + + return jsonify({"error": "Preview not supported for this file type"}), 400 diff --git a/server/rename.py b/server/rename.py new file mode 100644 index 0000000..b0ea355 --- /dev/null +++ b/server/rename.py @@ -0,0 +1,40 @@ +# rename.py +import os +import sqlite3 + +UPLOAD_DIRECTORY = "../assets" +DATABASE = 'transfer_service.db' # Define the DATABASE path + +def rename_file(upload_id, new_name): + conn = sqlite3.connect(DATABASE) + c = conn.cursor() + + # Retrieve the upload record based on the ID + c.execute('SELECT * FROM uploads WHERE id = ?', (upload_id,)) + upload = c.fetchone() + + if not upload: + conn.close() + return False, "File not found in database" + + old_filename = upload[3] + old_path = os.path.join(UPLOAD_DIRECTORY, old_filename) + new_path = os.path.join(UPLOAD_DIRECTORY, new_name) + + if os.path.exists(new_path): + conn.close() + return False, "A file with the new name already exists" + + # Rename the file in the filesystem + try: + os.rename(old_path, new_path) + except OSError as e: + conn.close() + return False, f"Error renaming file: {e}" + + # Update the filename in the database + c.execute('UPDATE uploads SET content = ? WHERE id = ?', (new_name, upload_id)) + conn.commit() + conn.close() + + return True, "File renamed successfully" diff --git a/server/security.py b/server/security.py index 7811360..95f1cb6 100644 --- a/server/security.py +++ b/server/security.py @@ -1,40 +1,79 @@ -# server/security.py -from flask import request, session +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_username, stored_password, login_attempts = user_data + 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." - hashed_password = hashlib.sha256(password.encode()).hexdigest() - + # 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) - return False, f"Invalid credentials. {MAX_ATTEMPTS - login_attempts - 1} attempt(s) remaining." + 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() - if "iPhone" in device_info['user_agent']: - return f"Uploaded by iPhone (IP: {device_info['ip']})" + 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: - return f"Uploaded by {device_info['isa']} {device_info['os']} (IP: {device_info['ip']})" + device_type = "Unknown Device" + + return f"Uploaded by {device_type} (IP: {device_info['ip']})" def get_device_info(): - user_agent = request.headers.get('User-Agent', 'Unknown') + """ + Extracts device information from the request. + """ return { - "ip": request.remote_addr, - "user_agent": user_agent, + "ip": request.remote_addr or "Unknown IP", + "user_agent": request.headers.get('User-Agent', 'Unknown'), } - diff --git a/server/transfer_service.db b/server/transfer_service.db index 4526916..be135b7 100644 Binary files a/server/transfer_service.db and b/server/transfer_service.db differ