shit dont work yet. fix chatting

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
This commit is contained in:
2025-08-23 18:52:48 +02:00
parent 98af0cc6c4
commit e20fb6311a
5 changed files with 2555 additions and 11 deletions

View File

@@ -1,4 +1,8 @@
Flask>=3.1.2
Flask-SQLAlchemy>=3.0.5
Flask-Cache>=0.13.1
cryptography>=45.0.6
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

View File

@@ -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= """
<body style="background-color: #121212; color: #FFFFFF; font-family: Arial, sans-serif;">
"""
return htmlbasic + "<p>" + str(todo) + "</p>" + "</body>"
def index():
return render_template('chat.html')
@app.route('/room/<room_id>')
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/<room_id>/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)

0
src/static/script.js Normal file
View File

437
src/static/styles.css Normal file
View File

@@ -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;
}
}

1560
src/templates/chat.html Normal file

File diff suppressed because it is too large Load Diff