diff --git a/app.py b/app.py index 67f6410..8981639 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,29 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for, flash +from flask_httpauth import HTTPBasicAuth +from werkzeug.utils import secure_filename import os import subprocess import shlex +import datetime from config import Config app = Flask(__name__) app.config.from_object(Config) +# Initialize HTTP Basic Auth +auth = HTTPBasicAuth() + +@auth.verify_password +def verify_password(username, password): + """Verify basic authentication credentials against multiple users""" + return (username in app.config['BASIC_AUTH_USERS'] and + app.config['BASIC_AUTH_USERS'][username] == password) + +def allowed_file(filename): + """Check if file extension is allowed""" + return ('.' in filename and + filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']) + @app.route('/') def index(): """Home page route""" @@ -117,11 +134,148 @@ def api_info(): '/finger', '/finger/', '/api/hello', - '/api/info' + '/api/info', + '/api/upload' ], 'framework': 'Flask' }) +@app.route('/api/upload', methods=['POST']) +@auth.login_required +def upload_file(): + """File upload endpoint with basic authentication and SCP transfer""" + try: + # Extract authenticated username + authenticated_user = auth.current_user() + + # Check if the post request has the file part + if 'file' not in request.files: + return jsonify({ + 'error': 'No file part in the request', + 'status': 'error' + }), 400 + + file = request.files['file'] + + # Check if user selected a file + if file.filename == '': + return jsonify({ + 'error': 'No file selected', + 'status': 'error' + }), 400 + + # Check if file is allowed + if not allowed_file(file.filename): + return jsonify({ + 'error': f'File type not allowed. Allowed types: {", ".join(app.config["ALLOWED_EXTENSIONS"])}', + 'status': 'error' + }), 400 + + if file: + # Generate secure filename with timestamp + original_filename = secure_filename(file.filename) + timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"{timestamp}_{original_filename}" + + # Ensure upload directory exists + upload_folder = app.config['UPLOAD_FOLDER'] + if not os.path.exists(upload_folder): + os.makedirs(upload_folder) + + # Save file locally + file_path = os.path.join(upload_folder, filename) + file.save(file_path) + + # Get file info + file_size = os.path.getsize(file_path) + + # Initialize response data + response_data = { + 'message': 'File uploaded successfully', + 'status': 'success', + 'file_info': { + 'original_filename': original_filename, + 'saved_filename': filename, + 'local_file_path': file_path, + 'file_size': file_size, + 'upload_time': datetime.datetime.now().isoformat() + } + } + + # SCP file to remote server if enabled + if app.config['SCP_ENABLED']: + try: + remote_file_path = os.path.join(app.config['REMOTE_PATH'], authenticated_user).replace('\\', '/') + scp_destination = f"{app.config['REMOTE_USER']}@{app.config['REMOTE_HOST']}:{remote_file_path}" + + # Build SCP command + scp_cmd = [ + 'scp', + '-P', str(app.config['REMOTE_PORT']), + '-o', 'StrictHostKeyChecking=no', + '-o', 'UserKnownHostsFile=/dev/null' + ] + + # Add private key if specified + if app.config['REMOTE_PRIVATE_KEY']: + scp_cmd.extend(['-i', app.config['REMOTE_PRIVATE_KEY']]) + + # Add source and destination + scp_cmd.extend([file_path, scp_destination]) + + # Execute SCP command + scp_process = subprocess.run( + scp_cmd, + capture_output=True, + text=True, + timeout=30 + ) + + if scp_process.returncode == 0: + response_data['scp_info'] = { + 'remote_host': app.config['REMOTE_HOST'], + 'remote_path': remote_file_path, + 'scp_status': 'success', + 'scp_message': 'File successfully transferred to remote server' + } + response_data['message'] = 'File uploaded and transferred to remote server successfully' + else: + response_data['scp_info'] = { + 'scp_status': 'failed', + 'scp_error': scp_process.stderr or 'SCP transfer failed', + 'scp_message': 'File uploaded locally but remote transfer failed' + } + response_data['message'] = 'File uploaded locally but remote transfer failed' + + except subprocess.TimeoutExpired: + response_data['scp_info'] = { + 'scp_status': 'timeout', + 'scp_error': 'SCP transfer timed out', + 'scp_message': 'File uploaded locally but remote transfer timed out' + } + response_data['message'] = 'File uploaded locally but remote transfer timed out' + + except Exception as scp_error: + response_data['scp_info'] = { + 'scp_status': 'error', + 'scp_error': str(scp_error), + 'scp_message': 'File uploaded locally but SCP failed' + } + response_data['message'] = 'File uploaded locally but SCP failed' + else: + response_data['scp_info'] = { + 'scp_status': 'disabled', + 'scp_message': 'SCP transfer is disabled' + } + + return jsonify(response_data), 200 + + except Exception as e: + return jsonify({ + 'error': f'Upload failed: {str(e)}', + 'status': 'error' + }), 500 + @app.errorhandler(404) def not_found_error(error): """Handle 404 errors""" diff --git a/config.py b/config.py index 067ceb8..e790466 100644 --- a/config.py +++ b/config.py @@ -15,3 +15,27 @@ class Config: MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1'] MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') + + # File upload configuration + MAX_CONTENT_LENGTH = int(os.environ.get('MAX_FILE_SIZE', 16 * 1024 * 1024)) # 16MB default + UPLOAD_FOLDER = '/tmp' + ALLOWED_EXTENSIONS = {'txt'} + + # Remote server configuration for SCP + REMOTE_HOST = os.environ.get('REMOTE_HOST') or 'example.com' + REMOTE_USER = os.environ.get('REMOTE_USER') or 'user' + REMOTE_PATH = os.environ.get('REMOTE_PATH') or '/home/user/uploads/' + REMOTE_PORT = int(os.environ.get('REMOTE_PORT', 22)) + REMOTE_PRIVATE_KEY = os.environ.get('REMOTE_PRIVATE_KEY') # Path to SSH private key file + SCP_ENABLED = os.environ.get('SCP_ENABLED', 'false').lower() == 'true' + + # Basic authentication credentials - multiple users support + # Environment variable format: "user1:pass1,user2:pass2,user3:pass3" + BASIC_AUTH_USERS_STR = os.environ.get('BASIC_AUTH_USERS') or 'admin:password,uploader:upload123,user:user123' + + # Parse users string into dictionary + BASIC_AUTH_USERS = {} + for user_pass in BASIC_AUTH_USERS_STR.split(','): + if ':' in user_pass: + username, password = user_pass.strip().split(':', 1) # Split only on first colon + BASIC_AUTH_USERS[username.strip()] = password.strip() diff --git a/requirements.txt b/requirements.txt index 241479f..5c36f22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ MarkupSafe==2.1.3 itsdangerous==2.1.2 click==8.1.7 blinker==1.6.3 +Flask-HTTPAuth==4.8.0