From e20fb6311a742af81425f2ccf5692ab1e12070bd Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Sat, 23 Aug 2025 18:52:48 +0200 Subject: [PATCH] shit dont work yet. fix chatting Signed-off-by: rattatwinko --- requirements.txt | 12 +- src/app.py | 557 +++++++++++++- src/static/script.js | 0 src/static/styles.css | 437 +++++++++++ src/templates/chat.html | 1560 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 2555 insertions(+), 11 deletions(-) create mode 100644 src/static/script.js create mode 100644 src/static/styles.css create mode 100644 src/templates/chat.html diff --git a/requirements.txt b/requirements.txt index 92e78ed..2650190 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,8 @@ -Flask>=3.1.2 -Flask-SQLAlchemy>=3.0.5 -Flask-Cache>=0.13.1 -cryptography>=45.0.6 \ No newline at end of file +Flask==3.0.0 +Flask-SocketIO==5.3.6 +Flask-Limiter==3.5.0 +redis==5.0.1 +cryptography==41.0.7 +bcrypt==4.1.2 +python-dotenv==1.0.0 +gunicorn==21.2.0 \ No newline at end of file diff --git a/src/app.py b/src/app.py index ec5ff68..62a0209 100644 --- a/src/app.py +++ b/src/app.py @@ -1,11 +1,554 @@ -from flask import Flask -from todo import todo +import eventlet +eventlet.monkey_patch() +from flask import Flask, make_response, render_template, request, jsonify +from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address +import collections +import threading +import time +import uuid +import json +import secrets +import hashlib +import hmac +import re +from datetime import datetime, timedelta +import logging +from functools import wraps +import os + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) app = Flask(__name__) +socketio = SocketIO(app) + +# Security constants +MAX_MESSAGES = 256 +MAX_ROOM_ID_LENGTH = 32 +MAX_MESSAGE_SIZE = 8192 # 8KB max encrypted message +MAX_ROOMS_PER_IP = 5 +MAX_USERS_PER_ROOM = 50 +ROOM_CLEANUP_INTERVAL = 3600 # 1 hour +USER_SESSION_TIMEOUT = 3600 # 1 hour +RATE_LIMIT_MESSAGE = "10 per minute" +RATE_LIMIT_ROOM_JOIN = "5 per minute" + +# In-memory storage with enhanced security +chat_rooms = {} +room_keys = {} +user_sessions = {} +room_passwords = {} # Separate password storage with hashing +room_creation_times = {} +ip_room_count = {} # Track rooms created per IP +failed_password_attempts = {} # Track failed password attempts +message_hashes = {} # Store message hashes for duplicate detection +room_session_keys = {} # Store session keys for each room + +class CircularMessageBuffer: + def __init__(self, max_size=MAX_MESSAGES): + self.buffer = collections.deque(maxlen=max_size) + self.lock = threading.Lock() + self.creation_time = time.time() + self.last_activity = time.time() + + def add_message(self, message_data): + with self.lock: + # Generate unique message ID and validate + message_id = str(uuid.uuid4()) + + # Create message hash for duplicate detection + message_hash = hashlib.sha256( + f"{message_data['encrypted_content']}{message_data['sender_id']}{message_data.get('iv', '')}".encode() + ).hexdigest() + + self.buffer.append({ + 'id': message_id, + 'encrypted_content': message_data['encrypted_content'], + 'sender_id': message_data['sender_id'], + 'timestamp': time.time(), + 'iv': message_data.get('iv'), + 'hash': message_hash + }) + + self.last_activity = time.time() + + def get_messages(self): + with self.lock: + return list(self.buffer) + + def get_message_count(self): + with self.lock: + return len(self.buffer) + + def is_expired(self, timeout_seconds=USER_SESSION_TIMEOUT): + return time.time() - self.last_activity > timeout_seconds + +# Security validation functions +def is_valid_room_id(room_id): + """Validate room ID format and length""" + if not room_id or len(room_id) > MAX_ROOM_ID_LENGTH: + return False + # Allow only alphanumeric characters and hyphens + return re.match(r'^[a-zA-Z0-9\-_]+$', room_id) is not None + +def is_valid_message(encrypted_content, iv=None): + """Validate message format and size""" + if not encrypted_content or len(encrypted_content) > MAX_MESSAGE_SIZE: + return False + if iv and len(iv) > 256: # IV should be reasonable size + return False + return True + +def is_valid_public_key(public_key): + """Validate public key format""" + if not public_key or len(public_key) > 2048: # RSA-2048 base64 encoded + return False + try: + # Basic base64 validation + import base64 + base64.b64decode(public_key) + return True + except Exception: + return False + +def hash_password(password, salt=None): + """Hash password with salt using PBKDF2""" + if salt is None: + salt = secrets.token_bytes(32) + else: + salt = bytes.fromhex(salt) + + hashed = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000) + return salt.hex() + hashed.hex() + +def verify_password(password, hashed): + """Verify password against hash""" + try: + salt = bytes.fromhex(hashed[:64]) + stored_hash = hashed[64:] + new_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000).hex() + return hmac.compare_digest(stored_hash, new_hash) + except Exception: + return False + +def cleanup_expired_rooms(): + """Clean up expired rooms and sessions""" + current_time = time.time() + expired_rooms = [] + + for room_id, room_buffer in chat_rooms.items(): + if room_buffer.is_expired(): + expired_rooms.append(room_id) + + for room_id in expired_rooms: + logger.info(f"Cleaning up expired room: {room_id}") + cleanup_room(room_id) + +def cleanup_room(room_id): + """Completely clean up a room""" + chat_rooms.pop(room_id, None) + room_keys.pop(room_id, None) + room_passwords.pop(room_id, None) + room_creation_times.pop(room_id, None) + message_hashes.pop(room_id, None) + room_session_keys.pop(room_id, None) # Clean up session keys + +def get_client_ip(): + """Get real client IP address""" + if request.headers.get('X-Forwarded-For'): + return request.headers.get('X-Forwarded-For').split(',')[0].strip() + elif request.headers.get('X-Real-IP'): + return request.headers.get('X-Real-IP') + return request.remote_addr + +# Authentication decorator +def require_valid_session(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if request.sid not in user_sessions: + logger.warning(f"Invalid session attempt from {get_client_ip()}") + disconnect() + return + return f(*args, **kwargs) + return decorated_function + +# CSP header as requested +@app.after_request +def apply_csp(response): + csp = ( + "default-src 'self'; " + "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; " + "connect-src 'self' ws://localhost:5000 wss://localhost:5000;" + ) + response.headers["Content-Security-Policy"] = csp + return response @app.route('/') -def hello(): - htmlbasic= """ - - """ - return htmlbasic + "

" + str(todo) + "

" + "" \ No newline at end of file +def index(): + return render_template('chat.html') + +@app.route('/room/') +def room(room_id): + if not is_valid_room_id(room_id): + return "Invalid room ID", 400 + return render_template('chat.html', room_id=room_id) + +@app.route('/api/room//info') +def get_room_info(room_id): + if not is_valid_room_id(room_id): + return jsonify({'error': 'Invalid room ID'}), 400 + + if room_id in chat_rooms: + return jsonify({ + 'exists': True, + 'message_count': chat_rooms[room_id].get_message_count(), + 'max_messages': MAX_MESSAGES, + 'user_count': len(room_keys.get(room_id, {})), + 'max_users': MAX_USERS_PER_ROOM + }) + return jsonify({'exists': False}) + +# Socket event handlers +@socketio.on('connect') +def handle_connect(): + client_ip = get_client_ip() + user_id = str(uuid.uuid4()) + + # Check connection limits + active_connections = sum(1 for session in user_sessions.values() + if session.get('ip') == client_ip) + + if active_connections > 10: # Max 10 connections per IP + logger.warning(f"Too many connections from IP: {client_ip}") + disconnect() + return + + user_sessions[request.sid] = { + 'user_id': user_id, + 'room': None, + 'display_name': f"User-{user_id[:8]}", + 'ip': client_ip, + 'connected_at': time.time(), + 'last_activity': time.time() + } + + emit('user_connected', {'user_id': user_id}) + logger.info(f"User {user_id} connected from {client_ip}") + +@socketio.on('disconnect') +def handle_disconnect(): + if request.sid in user_sessions: + user_data = user_sessions[request.sid] + room_id = user_data.get('room') + user_id = user_data.get('user_id') + + if room_id: + leave_room(room_id) + # Remove user's public key + if room_id in room_keys and user_id in room_keys[room_id]: + del room_keys[room_id][user_id] + + emit('user_left', {'user_id': user_id}, room=room_id) + + # Clean up empty rooms + if room_id in room_keys and len(room_keys[room_id]) == 0: + cleanup_room(room_id) + + del user_sessions[request.sid] + logger.info(f"User {user_id} disconnected") + +@socketio.on('join_room') +@require_valid_session +def handle_join_room(data): + try: + room_id = data.get('room_id', '').strip() + public_key = data.get('public_key', '') + password = data.get('password', '') + + # Validation + if not is_valid_room_id(room_id): + emit('room_joined', {'error': 'Invalid room ID format'}) + return + + if not is_valid_public_key(public_key): + emit('room_joined', {'error': 'Invalid public key format'}) + return + + client_ip = get_client_ip() + user_id = user_sessions[request.sid]['user_id'] + + is_first_user = False + + # Check if room exists + if room_id not in chat_rooms: + # Check room creation limits per IP + rooms_by_ip = ip_room_count.get(client_ip, 0) + if rooms_by_ip >= MAX_ROOMS_PER_IP: + emit('room_joined', {'error': 'Too many rooms created from this IP'}) + return + + # Create new room + chat_rooms[room_id] = CircularMessageBuffer() + room_keys[room_id] = {} + room_creation_times[room_id] = time.time() + message_hashes[room_id] = set() + room_session_keys[room_id] = None # Initialize session key storage + + # Store hashed password if provided + if password: + room_passwords[room_id] = hash_password(password) + + # Update IP room count + ip_room_count[client_ip] = rooms_by_ip + 1 + is_first_user = True + + logger.info(f"Room {room_id} created by {user_id}") + else: + # Check room capacity + if len(room_keys[room_id]) >= MAX_USERS_PER_ROOM: + emit('room_joined', {'error': 'Room is full'}) + return + + # Verify password if room is password protected + if room_id in room_passwords: + if not password: + emit('room_joined', {'error': 'Password required'}) + return + + # Check failed attempts + attempt_key = f"{client_ip}_{room_id}" + failed_attempts = failed_password_attempts.get(attempt_key, 0) + + if failed_attempts >= 5: # Max 5 failed attempts + emit('room_joined', {'error': 'Too many failed password attempts'}) + return + + if not verify_password(password, room_passwords[room_id]): + failed_password_attempts[attempt_key] = failed_attempts + 1 + emit('room_joined', {'error': 'Incorrect password'}) + return + else: + # Clear failed attempts on success + failed_password_attempts.pop(attempt_key, None) + + # Generate new UUID for this room session + new_user_id = str(uuid.uuid4()) + user_sessions[request.sid]['user_id'] = new_user_id + user_sessions[request.sid]['display_name'] = f"User-{new_user_id[:8]}" + user_sessions[request.sid]['last_activity'] = time.time() + + # Store user's public key + room_keys[room_id][new_user_id] = public_key + + # Join the room + join_room(room_id) + user_sessions[request.sid]['room'] = room_id + + # Get room data + messages = chat_rooms[room_id].get_messages() + user_keys = room_keys[room_id] + + # Send room joined confirmation + emit('room_joined', { + 'room_id': room_id, + 'user_id': new_user_id, + 'display_name': user_sessions[request.sid]['display_name'], + 'messages': messages, + 'users': list(user_keys.keys()), + 'user_keys': user_keys, + 'message_count': chat_rooms[room_id].get_message_count(), + 'is_first_user': is_first_user + }) + + # Notify others about new user + emit('user_joined', { + 'user_id': new_user_id, + 'display_name': user_sessions[request.sid]['display_name'], + 'public_key': public_key + }, room=room_id, include_self=False) + + # Handle session key distribution + if is_first_user: + # First user - they will generate the session key on client side + logger.info(f"First user {new_user_id} joined room {room_id} - will generate session key") + else: + # Not first user - request session key from existing users + logger.info(f"User {new_user_id} joined room {room_id} - requesting session key") + emit('request_session_key', { + 'new_user_id': new_user_id, + 'public_key': public_key + }, room=room_id, include_self=False) + + logger.info(f"User {new_user_id} joined room {room_id}") + + except Exception as e: + logger.error(f"Error in join_room: {str(e)}") + emit('room_joined', {'error': 'Internal server error'}) + +@socketio.on('send_message') +@require_valid_session +def handle_send_message(data): + try: + user_data = user_sessions[request.sid] + room_id = user_data['room'] + + if not room_id or room_id not in chat_rooms: + return + + encrypted_content = data.get('encrypted_content', '') + iv = data.get('iv', '') + + # Validate message + if not is_valid_message(encrypted_content, iv): + logger.warning(f"Invalid message from user {user_data['user_id']}") + return + + # Check for duplicate messages + message_hash = hashlib.sha256( + f"{encrypted_content}{user_data['user_id']}{iv}".encode() + ).hexdigest() + + if room_id not in message_hashes: + message_hashes[room_id] = set() + + if message_hash in message_hashes[room_id]: + logger.warning(f"Duplicate message detected from user {user_data['user_id']}") + return + + message_hashes[room_id].add(message_hash) + + # Limit message hash storage + if len(message_hashes[room_id]) > MAX_MESSAGES * 2: + message_hashes[room_id] = set(list(message_hashes[room_id])[-MAX_MESSAGES:]) + + message_data = { + 'encrypted_content': encrypted_content, + 'sender_id': user_data['user_id'], + 'display_name': user_data['display_name'], + 'iv': iv + } + + # Add to room buffer + chat_rooms[room_id].add_message(message_data) + + # Update user activity + user_sessions[request.sid]['last_activity'] = time.time() + + # Broadcast to room + emit('new_message', { + 'id': str(uuid.uuid4()), + 'encrypted_content': encrypted_content, + 'sender_id': user_data['user_id'], + 'display_name': user_data['display_name'], + 'timestamp': time.time(), + 'iv': iv, + 'message_count': chat_rooms[room_id].get_message_count() + }, room=room_id) + + except Exception as e: + logger.error(f"Error in send_message: {str(e)}") + +@socketio.on('share_session_key') +@require_valid_session +def handle_share_session_key(data): + """Handle sharing of session key with a specific user""" + try: + room_id = data.get('room_id', '') + target_user_id = data.get('target_user_id', '') + encrypted_key = data.get('encrypted_key', '') + + # Validation + if not room_id or not target_user_id or not encrypted_key: + logger.warning("Invalid share_session_key request") + return + + if len(encrypted_key) > 2048: # Reasonable limit for encrypted session key + logger.warning("Encrypted key too large") + return + + sender_id = user_sessions[request.sid]['user_id'] + sender_room = user_sessions[request.sid].get('room') + + # Verify sender is in the correct room + if room_id != sender_room: + logger.warning(f"User {sender_id} tried to share key for room {room_id} but is in {sender_room}") + return + + # Find target user's socket session + target_sid = None + for sid, session in user_sessions.items(): + if session.get('user_id') == target_user_id and session.get('room') == room_id: + target_sid = sid + break + + if target_sid: + # Send the encrypted session key directly to the target user + emit('session_key_received', { + 'from_user_id': sender_id, + 'encrypted_key': encrypted_key + }, room=target_sid) + logger.info(f"Session key shared from {sender_id} to {target_user_id} in room {room_id}") + else: + logger.warning(f"Target user {target_user_id} not found in room {room_id}") + + except Exception as e: + logger.error(f"Error in share_session_key: {str(e)}") + +@socketio.on('key_exchange') +@require_valid_session +def handle_key_exchange(data): + """Legacy key exchange handler - redirects to share_session_key""" + try: + # Map old format to new format + room_id = data.get('room_id', '') + target_user = data.get('target_user', '') + encrypted_key = data.get('encrypted_key', '') + + if room_id and target_user and encrypted_key: + handle_share_session_key({ + 'room_id': room_id, + 'target_user_id': target_user, + 'encrypted_key': encrypted_key + }) + except Exception as e: + logger.error(f"Error in legacy key_exchange: {str(e)}") + +# Background cleanup task +def start_cleanup_task(): + def cleanup_worker(): + while True: + try: + cleanup_expired_rooms() + time.sleep(ROOM_CLEANUP_INTERVAL) + except Exception as e: + logger.error(f"Error in cleanup task: {str(e)}") + + cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True) + cleanup_thread.start() + +# Error handlers +@app.errorhandler(429) +def ratelimit_handler(e): + return jsonify({'error': 'Rate limit exceeded'}), 429 + +@app.errorhandler(404) +def not_found(e): + return jsonify({'error': 'Not found'}), 404 + +@app.errorhandler(500) +def internal_error(e): + logger.error(f"Internal server error: {str(e)}") + return jsonify({'error': 'Internal server error'}), 500 + +if __name__ == "__main__": + try: + socketio.run(app, debug=True, allow_unsafe_werkzeug=True) + except BrokenPipeError: + # Suppress noisy broken pipe errors (client disconnects) + import sys + print("Broken pipe error suppressed.", file=sys.stderr) \ No newline at end of file diff --git a/src/static/script.js b/src/static/script.js new file mode 100644 index 0000000..e69de29 diff --git a/src/static/styles.css b/src/static/styles.css new file mode 100644 index 0000000..2450522 --- /dev/null +++ b/src/static/styles.css @@ -0,0 +1,437 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f0f0f; + color: #ececec; + height: 100vh; + display: flex; + flex-direction: column; +} + +.chat-container { + background: #212121; + flex: 1; + display: flex; + flex-direction: column; + max-width: 1200px; + margin: 0 auto; + width: 100%; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); +} + +.chat-header { + background: #0f0f0f; + border-bottom: 1px solid #333; + padding: 16px 20px; + position: relative; + z-index: 100; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1000px; + margin: 0 auto; +} + +.logo { + font-size: 18px; + font-weight: 600; + color: #fff; + display: flex; + align-items: center; + gap: 8px; +} + +.encryption-badge { + background: #10a37f; + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.room-section { + background: #2d2d2d; + padding: 20px; + margin: 20px; + border-radius: 12px; + border: 1px solid #404040; +} + +.room-input-container { + display: flex; + gap: 12px; + align-items: center; + margin-top: 16px; +} + +.room-input { + flex: 1; + background: #0f0f0f; + border: 1px solid #404040; + color: #ececec; + padding: 12px 16px; + border-radius: 8px; + font-size: 14px; + outline: none; + transition: border-color 0.2s; +} + +.room-input:focus { + border-color: #10a37f; + box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1); +} + +.btn { + background: #10a37f; + color: white; + border: none; + padding: 12px 20px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + font-size: 14px; + transition: all 0.2s; + white-space: nowrap; +} + +.btn:hover { + background: #0d8c6a; +} + +.btn-secondary { + background: #2d2d2d; + border: 1px solid #404040; +} + +.btn-secondary:hover { + background: #3d3d3d; +} + +.status-text { + color: #b4b4b4; + font-size: 14px; + margin-top: 12px; +} + +.room-info { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #2d2d2d; + border-radius: 8px; + font-size: 13px; + color: #b4b4b4; + margin-bottom: 16px; +} + +.room-details { + display: flex; + gap: 20px; +} + +.messages-container { + flex: 1; + overflow-y: auto; + padding: 20px; + background: #212121; + max-width: 1000px; + margin: 0 auto; + width: 100%; +} + +.messages-container::-webkit-scrollbar { + width: 8px; +} + +.messages-container::-webkit-scrollbar-track { + background: #2d2d2d; +} + +.messages-container::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 4px; +} + + +.message-group { + margin-bottom: 24px; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.message-group.sent { + align-items: flex-end; +} + +.message-group.received { + align-items: flex-start; +} + +.message-header { + display: flex; + align-items: center; + margin-bottom: 8px; + gap: 8px; +} + +.user-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: linear-gradient(45deg, #10a37f, #0d8c6a); + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: bold; + color: white; +} + +.user-avatar.own { + background: linear-gradient(45deg, #6366f1, #8b5cf6); +} + +.user-name { + font-weight: 600; + font-size: 14px; + color: #ececec; +} + +.message-time { + color: #888; + font-size: 12px; + margin-left: auto; +} + + +.message-content { + background: #2d2d2d; + padding: 12px 16px; + border-radius: 12px; + margin-left: 36px; + border: 1px solid #404040; + line-height: 1.5; + word-wrap: break-word; + max-width: 70%; + text-align: left; +} + +.message-group.sent .message-content { + background: #0f0f0f; + border-color: #333; + margin-left: 0; + margin-right: 36px; + text-align: right; + align-self: flex-end; +} + +.message-group.received .message-content { + align-self: flex-start; +} + +.input-section { + background: #0f0f0f; + padding: 20px; + border-top: 1px solid #333; +} + +.input-container { + max-width: 1000px; + margin: 0 auto; + position: relative; +} + +.message-input { + width: 100%; + background: #2d2d2d; + border: 1px solid #404040; + color: #ececec; + padding: 16px 60px 16px 20px; + border-radius: 12px; + font-size: 16px; + outline: none; + resize: none; + min-height: 24px; + max-height: 200px; + font-family: inherit; + line-height: 1.5; + transition: border-color 0.2s; +} + +.message-input:focus { + border-color: #10a37f; + box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1); +} + +.message-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.send-button { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + background: #10a37f; + border: none; + width: 40px; + height: 40px; + border-radius: 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + color: white; +} + +.send-button:hover:not(:disabled) { + background: #0d8c6a; +} + +.send-button:disabled { + background: #404040; + cursor: not-allowed; +} + +.welcome-screen { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + text-align: center; + padding: 40px 20px; +} + +.welcome-title { + font-size: 32px; + font-weight: 700; + margin-bottom: 16px; + background: linear-gradient(45deg, #10a37f, #6366f1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.welcome-subtitle { + font-size: 18px; + color: #b4b4b4; + margin-bottom: 32px; + max-width: 600px; +} + +.feature-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-top: 32px; + max-width: 800px; +} + +.feature-item { + background: #2d2d2d; + padding: 20px; + border-radius: 12px; + border: 1px solid #404040; +} + +.feature-icon { + font-size: 24px; + margin-bottom: 12px; +} + +.feature-title { + font-weight: 600; + margin-bottom: 8px; + color: #ececec; +} + +.feature-desc { + color: #b4b4b4; + font-size: 14px; + line-height: 1.4; +} + +.loading-message { + display: flex; + align-items: center; + gap: 8px; + color: #b4b4b4; + font-size: 14px; +} + +.loading-spinner { + width: 16px; + height: 16px; + border: 2px solid #404040; + border-top: 2px solid #10a37f; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-message { + background: #dc2626; + color: white; + padding: 12px 16px; + border-radius: 8px; + margin: 16px 0; + font-size: 14px; +} + +.typing-indicator { + color: #888; + font-style: italic; + font-size: 13px; + padding: 8px 20px; +} + +@media (max-width: 768px) { + .chat-container { + height: 100vh; + } + + .header-content { + padding: 0 4px; + } + + .room-input-container { + flex-direction: column; + gap: 8px; + } + + .room-input, .btn { + width: 100%; + } + + .room-details { + flex-direction: column; + gap: 8px; + } + + .message-content { + margin-left: 0; + margin-top: 8px; + } + + .feature-list { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/templates/chat.html b/src/templates/chat.html new file mode 100644 index 0000000..abb7701 --- /dev/null +++ b/src/templates/chat.html @@ -0,0 +1,1560 @@ + + + + + + Secure Chat Platform + + + + + + +
+
+
+ +
+ E2E Encrypted +
+
+
+ + +
+

End-to-End Encrypted Chat

+

+ Your messages are encrypted in your browser before being sent. + Messages are stored temporarily in memory and automatically deleted after 256 messages. +

+ +
+
+ + + + +
+
+
+
+ Initializing encryption keys... +
+
+
+ +
+
+ +
End-to-End Encryption
+
Messages are encrypted with AES-256 in your browser. The server never sees your messages.
+
+
+ +
Ephemeral Messages
+
Only 256 messages stored in memory. Older messages are automatically deleted.
+
+
+ +
Real-time Chat
+
Instant messaging with WebSocket technology for immediate message delivery.
+
+
+ +
Anonymous Users
+
Each session gets a new random UUID. No registration or personal info required.
+
+
+
+ + + + + +
+ + + + + + \ No newline at end of file