commit 24da35f11e46b680f73139631f5b7f313e799fdb Author: Artyom Titov Date: Fri May 15 18:00:40 2026 +0300 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c99e5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +uploads/ +server.log +ssl-dir/*.key diff --git a/activate b/activate new file mode 120000 index 0000000..7dcd316 --- /dev/null +++ b/activate @@ -0,0 +1 @@ +/home/archibaldik/progs/nas-python/.venv/naspy/bin/activate \ No newline at end of file diff --git a/home.html b/home.html new file mode 100644 index 0000000..ef521fb --- /dev/null +++ b/home.html @@ -0,0 +1,198 @@ + + + + + + Network File Transfer + + + +
+

šŸ“ Network File Transfer

+

Upload and download files from your network drive

+
+ +
+

šŸ“¤ Upload Files

+
+

šŸ“ Drag and drop files here

+

or click to browse

+ +
+ +
+ +
+

šŸ“„ Download Files

+
+
Loading files...
+
+
+
+ + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..b7344fd --- /dev/null +++ b/index.html @@ -0,0 +1,1041 @@ + + + + + + + šŸ“ File Transfer - Secure & Fast + + + +
+ +
+
šŸ“
+

File Transfer Server

+

Secure HTTPS file upload and download

+
Encrypted connection
+
+ + +
+ +
+ + +
+
+ šŸ“¤ + Upload Files +
+ +
+
ā¬†ļø
+

Drag and drop files here

+

or click to browse your files

+ +
+ + +
+
+
+
+
+ + +
+
+ šŸ“„ + Download Files +
+ +
+
+
šŸ“­
+

Loading files...

+
+
+ + +
+
+ + + +
+ + + + diff --git a/server.py b/server.py new file mode 100644 index 0000000..b5ed66f --- /dev/null +++ b/server.py @@ -0,0 +1,39 @@ +import ssl +from http.server import BaseHTTPRequestHandler, HTTPServer +import socketserver + +with open("home.html", "r", encoding="utf-8" ) as f: + greeting_page = f.read() + +class Handler(BaseHTTPRequestHandler): + def sql_req(self): + data = "" + d_type = "" + if self.path == "/": + d_type = "text/html" + data = greeting_page + return (data, d_type) + + + def do_GET(self): + if self.path != "/": + self.send_response(404) + self.end_headers() + return + self.send_response(200) + self.send_header("Data type", self.sql_req()[1]) + self.end_headers() + self.wfile.write(self.sql_req()[0].encode()) + +server = HTTPServer(("0.0.0.0", 4443), Handler) + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +ssl_context.load_cert_chain(certfile="ssl-dir/server.crt",keyfile="ssl-dir/server.key") + +httpd = socketserver.TCPServer(("0.0.0.0", 4445), Handler) +httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side = True) + + +server.socket = ssl_context.wrap_socket(server.socket, server_side=True) + +server.serve_forever() diff --git a/server_core.py b/server_core.py new file mode 100644 index 0000000..d1525c2 --- /dev/null +++ b/server_core.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +""" +HTTPS File Transfer Server for OpenWRT/Notebook +Secure file upload, download, and management over HTTPS +""" + +import ssl +import os +import json +import mimetypes +import urllib.parse +from pathlib import Path +from http.server import BaseHTTPRequestHandler, HTTPServer +from datetime import datetime +import logging + +# ============================================================================ +# Configuration +# ============================================================================ + +# Server settings +HOST = "0.0.0.0" +HTTP_PORT = 8080 +HTTPS_PORT = 4443 + +# File settings +UPLOAD_DIR = "./uploads" +CERT_DIR = "./ssl-dir" +SERVER_CERT = os.path.join(CERT_DIR, "server.crt") +SERVER_KEY = os.path.join(CERT_DIR, "server.key") + +# Logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('server.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ============================================================================ +# Utility Functions +# ============================================================================ + +def ensure_upload_dir(): + """Create upload directory if it doesn't exist""" + Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) + logger.info(f"Upload directory: {os.path.abspath(UPLOAD_DIR)}") + +def ensure_ssl_certs(): + """Check if SSL certificates exist""" + if not os.path.exists(SERVER_CERT) or not os.path.exists(SERVER_KEY): + logger.error(f"SSL certificates not found in {CERT_DIR}") + logger.info("Generate certificates with:") + logger.info(f" mkdir -p {CERT_DIR}") + logger.info(f" openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \\") + logger.info(f" -keyout {SERVER_KEY} -out {SERVER_CERT} \\") + logger.info(f" -subj '/C=US/ST=State/L=City/O=Org/CN=localhost'") + return False + return True + +def get_file_list(): + """Get list of files in upload directory + Skips directories""" + try: + files = [] + for filename in os.listdir(UPLOAD_DIR): + filepath = os.path.join(UPLOAD_DIR, filename) + if os.path.isfile(filepath): + size = os.path.getsize(filepath) + mtime = os.path.getmtime(filepath) + files.append({ + "name": filename, + "size": size, + "modified": datetime.fromtimestamp(mtime).isoformat(), + "size_human": format_size(size) + }) + return sorted(files, key=lambda x: x['modified'], reverse=True) + except Exception as e: + logger.error(f"Error listing files: {e}") + return [] + +def format_size(bytes_size): + """Convert bytes to human readable format""" + for unit in ['B', 'KB', 'MB', 'GB']: + if bytes_size < 1024: + return f"{bytes_size:.1f}{unit}" + bytes_size /= 1024 + return f"{bytes_size:.1f}TB" + +def get_content_type(filename): + """Get MIME type for file""" + mime_type, _ = mimetypes.guess_type(filename) + return mime_type or 'application/octet-stream' + +# ============================================================================ +# HTML/CSS/JavaScript +# ============================================================================ + +with open("index.html", "r", encoding="utf-8") as f: + HTML_TEMPLATE = f.read() +# ============================================================================ +# Request Handler +# ============================================================================ + +class FileTransferHandler(BaseHTTPRequestHandler): + """HTTP request handler for file transfer operations""" + + def log_message(self, format, *args): + """Override logging to use our logger""" + logger.info(f"{self.client_address[0]} - {format % args}") + + def do_GET(self): + """Handle GET requests""" + try: + # Parse URL + parsed_url = urllib.parse.urlparse(self.path) + path = parsed_url.path + query_params = urllib.parse.parse_qs(parsed_url.query) + + # Route handling + if path == "/": + self.serve_index() + elif path == "/api/files": + self.serve_file_list() + elif path == "/download": + filename = query_params.get('file', [None])[0] + if filename: + self.serve_download(filename) + else: + self.send_error(400, "Missing file parameter") + elif path == "/status": + self.serve_status() + else: + self.send_error(404, "Not found") + + except Exception as e: + logger.error(f"GET error: {e}") + self.send_error(500, "Internal server error") + + def do_POST(self): + """Handle POST requests""" + try: + parsed_url = urllib.parse.urlparse(self.path) + path = parsed_url.path + query_params = urllib.parse.parse_qs(parsed_url.path.split('?')[0] if '?' in self.path else '') + if path == "/upload": + self.handle_upload() + elif path == "/delete": + query_params = urllib.parse.parse_qs(self.path.split('?')[1] if '?' in self.path else '') + + filename = query_params.get('file', [None])[0] + if filename: + self.handle_delete(filename) + else: + self.send_error(400, "Missing file parameter") + else: + self.send_error(404, "Not found") + + except Exception as e: + logger.error(f"POST error: {e}") + self.send_error(500, "Internal server error") + + def serve_index(self): + """Serve the main HTML page""" + self.send_response(200) + self.send_header("Content-type", "text/html; charset=utf-8") + self.end_headers() + self.wfile.write(HTML_TEMPLATE.encode('utf-8')) + + def serve_file_list(self): + """Serve JSON list of files""" + files = get_file_list() + response = { + "files": files, + "total_files": len(files), + "upload_dir": os.path.abspath(UPLOAD_DIR) + } + + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(response).encode('utf-8')) + + def serve_download(self, filename): + """Serve file download""" + # Security: prevent directory traversal + if ".." in filename or filename.startswith("/"): + self.send_error(403, "Forbidden") + return + + filepath = os.path.join(UPLOAD_DIR, filename) + + if not os.path.exists(filepath) or not os.path.isfile(filepath): + self.send_error(404, "File not found") + return + + try: + with open(filepath, 'rb') as f: + file_size = os.path.getsize(filepath) + self.send_response(200) + self.send_header("Content-type", get_content_type(filename)) + self.send_header("Content-Disposition", f'attachment; filename="{filename}"') + self.send_header("Content-Length", str(file_size)) + self.end_headers() + + # Send file in chunks + chunk_size = 8192 + while True: + chunk = f.read(chunk_size) + if not chunk: + break + self.wfile.write(chunk) + + logger.info(f"Downloaded: {filename}") + + except Exception as e: + logger.error(f"Download error: {e}") + self.send_error(500, "Error downloading file") + + def serve_status(self): + """Serve server status""" + files = get_file_list() + total_size = sum(f['size'] for f in files) + + status = { + "status": "online", + "timestamp": datetime.now().isoformat(), + "total_files": len(files), + "total_size": total_size, + "upload_dir": UPLOAD_DIR, + "https_port": HTTPS_PORT + } + + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(status).encode()) #encode('utf-8') + + def handle_upload(self): + """Handle file upload""" + try: + content_length = int(self.headers.get('Content-Length', 0)) + + if content_length > 0: + # Parse multipart form data + content_type = self.headers.get('Content-Type', '') + + if 'multipart/form-data' in content_type: + # Extract boundary + boundary = content_type.split("boundary=")[1].encode() + + # Read data + body = self.rfile.read(content_length) + + # Simple multipart parser + parts = body.split(b'--' + boundary) + for part in parts: + if b'filename=' in part: + # Extract filename + try: + filename_start = part.find(b'filename="') + 10 + filename_end = part.find(b'"', filename_start) + filename = part[filename_start:filename_end].decode() + + # Extract file data + data_start = part.find(b'\r\n\r\n') + 4 + data_end = part.rfind(b'\r\n') + file_data = part[data_start:data_end] + + # Save file + filepath = os.path.join(UPLOAD_DIR, filename) + with open(filepath, 'wb') as f: + f.write(file_data) + + logger.info(f"Uploaded: {filename} ({len(file_data)} bytes)") + except Exception as e: + logger.error(f"Upload parse error: {e}") + + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"status": "success"}).encode()) + else: + self.send_error(400, "No file provided") + + except Exception as e: + logger.error(f"Upload error: {e}") + self.send_error(500, "Upload failed") + + def handle_delete(self, filename): + """Handle file deletion""" + # Security: prevent directory traversal + if ".." in filename or filename.startswith("/"): + self.send_error(403, "Forbidden") + return + + filepath = os.path.join(UPLOAD_DIR, filename) + + if not os.path.exists(filepath): + self.send_error(404, "File not found") + return + + try: + os.remove(filepath) + logger.info(f"Deleted: {filename}") + + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"status": "success"}).encode()) + + except Exception as e: + logger.error(f"Delete error: {e}") + self.send_error(500, "Delete failed") + +# ============================================================================ +# Server Initialization +# ============================================================================ + +def create_https_server(): + """Create and configure HTTPS server""" + server = HTTPServer((HOST, HTTPS_PORT), FileTransferHandler) + + # Create SSL context + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(certfile=SERVER_CERT, keyfile=SERVER_KEY) + + # Optional: Improve security + context.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + context.minimum_version = ssl.TLSVersion.TLSv1_2 + + # Wrap socket + server.socket = context.wrap_socket(server.socket, server_side=True) + + return server + +def create_http_server(): + """Create plain HTTP server (for redirects if needed)""" + server = HTTPServer((HOST, HTTP_PORT), FileTransferHandler) + return server + +def main(): + """Main entry point""" + print("=" * 70) + print(" HTTPS File Transfer Server") + print("=" * 70) + + # Check requirements + if not ensure_ssl_certs(): + print("\nāŒ SSL certificates not found. Please generate them first.") + return + + ensure_upload_dir() + + # Create servers + https_server = create_https_server() + + logger.info("=" * 70) + logger.info("File Transfer Server Starting") + logger.info("=" * 70) + logger.info(f"šŸ“ Upload directory: {os.path.abspath(UPLOAD_DIR)}") + logger.info(f"šŸ”’ HTTPS port: {HTTPS_PORT}") + logger.info(f"šŸ“ Log file: server.log") + logger.info("=" * 70) + + print(f"\nāœ… Server starting...") + print(f"šŸ“± Access from iPhone: https://localhost:{HTTPS_PORT}") + print(f"🌐 Access from network: https://:{HTTPS_PORT}") + print(f"\nāš ļø Self-signed certificate - tap 'Continue' on first visit") + print(f"šŸ“ Files uploaded to: {os.path.abspath(UPLOAD_DIR)}") + print(f"šŸ“ Logs: server.log\n") + + try: + https_server.serve_forever() + except KeyboardInterrupt: + logger.info("Server shutting down...") + https_server.shutdown() + print("\nāœ… Server stopped") + +if __name__ == "__main__": + main() diff --git a/ssl-dir/rootCA.crt b/ssl-dir/rootCA.crt new file mode 100644 index 0000000..1d9bd90 --- /dev/null +++ b/ssl-dir/rootCA.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIUUSKIG/YEujhgnoH985AhHX8FXB4wDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9z +Y293MQswCQYDVQQKDAJNeTEmMCQGA1UEAwwdQWZpbmEgYW5kIEFydHlvbSBMdGQu +IFJvb3QgQ0EwHhcNMjYwNTE0MjE0MjA3WhcNMzAwNjIyMjE0MjA3WjBkMQswCQYD +VQQGEwJSVTEPMA0GA1UECAwGTW9zY293MQ8wDQYDVQQHDAZNb3Njb3cxCzAJBgNV +BAoMAk15MSYwJAYDVQQDDB1BZmluYSBhbmQgQXJ0eW9tIEx0ZC4gUm9vdCBDQTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ92j+S7CKOc2c1AiSiLG8q7 +p4wdwK8pdLkD1J0suK/EFxgNRLVFlP5EvTHrnJ5AyjkSvGeChInn47wEn8Qvf6BP +gx53REHRvD+tlONm/bMiMyBABAy9YYmEOJrFhYw2LR008Mhxly9sgQGxD6fahEXy +SvnfF7giPk0uWh7chYPQst07qdW4usNvHhWse38VGlZPrsf9M/MA7EyEEtDapWPt +o2/0W3Bk/ufRNdMJf6yrAYYOfYgmW8bVhvfKqIYYAinnTlD2OkUSGc74bsUiay91 +oz880kACsiG7sQVjM3K/5ZvDgIKCd7QdGGcCq4vmyBriLEwyetYxba2B+k8JCB+G +6b4YoZHQ63q9XV5ygDB5445WuzByLitVVE+4yUwAfvtXQkFZEZrO6IkMYFuIJxQL +9XeObx0xLcViQeCArvb7a11xvPXj/5whKsFrhrbuyTQFxD8JNTnsZHpw3CnOPJy3 +9/TmGQ8U/Mf+UxYX7Olj4J1ItHN1wvpHbRtUFGFz27Ygi5gIiT/RhXz3izWevDvU +0koUand/zzhSt5LwUlepixeRTpnlZTBe64hy5di/saKF4JbUKpel2JqIDauvbHlA +5wmsS87mi4/VIiVCMpBEmZmtIAK+OzQu8WucSE3nMDQkOSNN6/MS9xfW5IvcFV2G +chFrH59fc6MYi7bAdgxTAgMBAAGjUzBRMB0GA1UdDgQWBBQpHtv69SzHowfdbnad +oGrK4gcNbjAfBgNVHSMEGDAWgBQpHtv69SzHowfdbnadoGrK4gcNbjAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQApd0hAchvf/JElD2yuGsdXV2mw +FaQ7ANmeVjF9ezFXe5BeqsUpdmwX9SgFMJ4kMBM9TMrinANhVudf6lq3/kujNzvu +cB8PMQ5wjWKuMUFNGTibTqLiYRqRU5Mcr1NbsbF9p0D7TLMC8M+ZapmAdeosh6Mz +h8/kNju1VtdHBYR2YyTonuHr6RKt9qNRxZidco11nNUxhIIqof0mAaPyrjYqxhNU +FEyRVRE7S+e06p4JeiWI+JOuyZgy/8yLRNZyaslK7/6lpQU4x1xP9lugNYoHILge +0dVqiPxla5tYthomWwTDgCqQ/6Ogw7ilnC3aCpYiXvp4X/Ekt2sAZng6r9GfMOME +sa1S8z6X7WratEfSl1fiOF5paRVoxuFb76dNPGpfsb7vxivqgimUJzoVYV3tlmSB +TE/85X8wrCXjAcJv/iP+V1IgzyexIVkxXLYg19UB+ovQ4RGcufhN3ywWA9n1z7nT +8x0YTNdR+flVw3FrzM+X06Y7JLhVFUIBfQRtPTciYbc9TFSVhlXfGBLj3/TLG2Bt +KLrON5sH5oXofFYX7gyl+MQV5ya7dJby5b8LEFDY+9u8YY08ymzmaZRCgQaAHb7l +T0jlPknJZwNx6306w6OzIBekrEeVJLiwUC2MNZxOjtDxcpVnwyJVhIrVcB2aPmpe +P+A/5UbpCkkTtGLMsA== +-----END CERTIFICATE----- diff --git a/ssl-dir/server.conf b/ssl-dir/server.conf new file mode 100644 index 0000000..e2bb282 --- /dev/null +++ b/ssl-dir/server.conf @@ -0,0 +1,26 @@ +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = RU +ST = Moscow +L = Moscow +O = My +CN = Afina and Artyom Ltd. + +[v3_ca] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical,CA:true +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + +[v3_req] +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +IP.1 = 192.168.1.111 + diff --git a/ssl-dir/server.crt b/ssl-dir/server.crt new file mode 100644 index 0000000..f26acb2 --- /dev/null +++ b/ssl-dir/server.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIExjCCAq6gAwIBAgIUfoDCACbr/m52l9pLX+jnRtJvdK4wDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9z +Y293MQswCQYDVQQKDAJNeTEmMCQGA1UEAwwdQWZpbmEgYW5kIEFydHlvbSBMdGQu +IFJvb3QgQ0EwHhcNMjYwNTE0MjE0MzAzWhcNMjcwNTE0MjE0MzAzWjBcMQswCQYD +VQQGEwJSVTEPMA0GA1UECAwGTW9zY293MQ8wDQYDVQQHDAZNb3Njb3cxCzAJBgNV +BAoMAk15MR4wHAYDVQQDDBVBZmluYSBhbmQgQXJ0eW9tIEx0ZC4wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1Be3jL7dmATj8tJh3ChWWj2uJvLvVZo0z +jIktQ6d0gU2Y19UYy7ZIU/NyqrnM7E7USPm40C1YaYd5oyYGClzbARChTyylz7YN +LduU/8z2ED3F0dcyuxKFEtSTQnhvc8FyTKKZiy0/DjAyW4rY9ROn+PdwXccpI5k0 +azNnh7A1+eKyKEVLYIEFi08fDwpZNKh7rCWLYK3UydpJMedi+/rTK8CJmD5ZVZXK +fWadqKevEb49C7G3Ne0O38q/ILHy6fOM+ic0UQAr5CnGIRaRWoiaqRHkkGSpujS8 +04f92ZHXV3LAkUm+cUsD1qMXXxc9EukSlUvrpFvk/TNIFtFDcRfRAgMBAAGjeDB2 +MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAG +hwTAqAFvMB0GA1UdDgQWBBSthNDeS2eunXESe5OaChg36D07FDAfBgNVHSMEGDAW +gBQpHtv69SzHowfdbnadoGrK4gcNbjANBgkqhkiG9w0BAQsFAAOCAgEAnEvznC5J +7h7b8xqmUK2+SzfB3FSif56Fcqx+pJJHGvtrcNIUVjfjMIbHjdL+exdD5/wGgrD5 +03Mgm0g87PNBBa9H1g+Uw5ZLGVgNkiaSoqzIzuG1XD5dRJ9GVyHqadOlyLju3CoH +ILnbrqH68KcOS1Tnxg91jOkN2UwyZXgJefb5Vu7Qn9rzeJ6ZYv1YU6daJoXbu8A9 +vlGQkSBYYZGtlrmpzBYSRsNsRnCArij8N7g2XcuLvwwhhrlN7tVv2ywNeccsIbuL +GiismeuVxHj2vXANAHgFhltXIGimv7ytqjPDV/7Ek5FZT/FhKK3d4w04shGwtkCx +e0Joqj+zWk6yFPq1DblGLEUnQ9IWCwUcD2tVNY38reRRtudgH2fbH5PRbEFMGLbL +wlGzSQaQpblhV0IVUdSf+tDso2aUorF+T3qzx4TZlcPKxmyuhiKDs8pelnSvJp1T +Os2DvYoak87jzJ5hSXrVadJlvtOMDsam9MQCyz28U8nY5u7jxbQrVu53/uRfwRJr +5xNK6qn4sAy/1eudHnF08IopVcoKh3GAnvlVMcx18ZtkurMpM9unSd5FtLBwOS4u +eKMGhr/VE3fqNce66ELRIljrTUWZhs2NtuQXYYhXR2QbXNZY/ousjySiwXc2CKPj +WNqcLqoKoi1i++JT2wsBH7lRZUx1z9DI61A= +-----END CERTIFICATE-----