From e2aad6903fe75b40202880615c4bb567c9143922 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Sat, 23 Aug 2025 22:31:05 +0200 Subject: [PATCH] fixed some stuff mainly chat interface and made some stuff work --- dockerfile | 28 + src/static/favicon.png | Bin 0 -> 3632 bytes src/static/script.js | 925 ++++++++++++++++++++++++ src/static/styles.css | 437 ------------ src/static/stylesheet.css | 548 ++++++++++++++ src/templates/chat.html | 1414 +------------------------------------ 6 files changed, 1523 insertions(+), 1829 deletions(-) create mode 100644 dockerfile create mode 100644 src/static/favicon.png delete mode 100644 src/static/styles.css create mode 100644 src/static/stylesheet.css diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..72025e0 --- /dev/null +++ b/dockerfile @@ -0,0 +1,28 @@ +# Use an official lightweight Python image +FROM python:3.11-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Create a working directory +WORKDIR /app + +# Install system dependencies (if needed) +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the application code +COPY . . + +# Expose port +EXPOSE 3248 + +# Run with Gunicorn WSGI server +# Assumes your Flask app is named "app" inside "app.py" +CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:3248", "app:app"] diff --git a/src/static/favicon.png b/src/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..df69325b5fec9b8b4eee66d863b91951ccaef1c6 GIT binary patch literal 3632 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 literal 0 HcmV?d00001 diff --git a/src/static/script.js b/src/static/script.js index e69de29..d26f003 100644 --- a/src/static/script.js +++ b/src/static/script.js @@ -0,0 +1,925 @@ +// Content Security Policy enforcement +if (!window.crypto || !window.crypto.subtle) { + alert('This browser does not support required cryptographic features. Please use a modern browser.'); + throw new Error('WebCrypto API not available'); +} + +// Global variables +let socket = null; +let currentRoom = null; +let currentUserId = null; +let currentDisplayName = null; +let keyPair = null; +let sessionKey = null; +let roomUsers = {}; +let keysReady = false; +let lastMessageTime = 0; +let messageQueue = []; +let isConnected = false; +let reconnectAttempts = 0; +const MAX_RECONNECT_ATTEMPTS = 5; +const MESSAGE_RATE_LIMIT = 1000; +const MAX_MESSAGE_LENGTH = 4000; +const MAX_ROOM_ID_LENGTH = 32; +const MAX_MESSAGES = 256; + +// Security utilities +const SecurityUtils = { + sanitizeInput: function(input) { + if (typeof input !== 'string') return ''; + return input.trim().substring(0, MAX_MESSAGE_LENGTH); + }, + isValidRoomId: function(roomId) { + if (!roomId || typeof roomId !== 'string') return false; + if (roomId.length > MAX_ROOM_ID_LENGTH) return false; + return /^[a-zA-Z0-9\-_]+$/.test(roomId); + }, + canSendMessage: function() { + const now = Date.now(); + if (now - lastMessageTime < MESSAGE_RATE_LIMIT) { + return false; + } + lastMessageTime = now; + return true; + }, + generateSecureId: function(length = 8) { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase().substring(0, length); + }, + isValidBase64: function(str) { + try { + return btoa(atob(str)) === str; + } catch (err) { + return false; + } + } +}; + +class SecureChatError extends Error { + constructor(message, code = 'UNKNOWN') { + super(message); + this.name = 'SecureChatError'; + this.code = code; + } +} + +// Initialize the app +async function initializeApp() { + try { + console.log("Initializing bytechat"); + + // Initialize socket connection + socket = io({ + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionAttempts: MAX_RECONNECT_ATTEMPTS, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + timeout: 10000 + }); + + await generateKeyPair(); + setupSocketListeners(); + setupSecurityFeatures(); + + } catch (error) { + console.error('Failed to initialize app:', error); + updateStatus('Failed to initialize application', true); + } +} + +function setupSecurityFeatures() { + document.addEventListener('visibilitychange', function() { + if (document.hidden) { + const messageInput = document.getElementById('messageInput'); + if (messageInput) { + messageInput.value = ''; + } + } + }); + + window.addEventListener('beforeunload', function() { + clearSensitiveData(); + }); + + const securityIndicator = document.getElementById('securityIndicator'); + securityIndicator.style.display = 'block'; +} + +function clearSensitiveData() { + sessionKey = null; + keyPair = null; + roomUsers = {}; + + const passwordInput = document.getElementById('roomPasswordInput'); + if (passwordInput) { + passwordInput.value = ''; + } +} + +async function generateKeyPair() { + try { + updateStatus('Generating encryption keys...', false); + + keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 2048, // Reduced from 4096 for better performance + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256" + }, + false, + ["encrypt", "decrypt"] + ); + + keysReady = true; + updateStatus("Ready to join a room", false); + console.log("Key pair generated successfully"); + + } catch (error) { + console.error("Failed to generate key pair:", error); + updateStatus("Error: Failed to generate encryption keys", true); + throw new SecureChatError("Key generation failed", "KEY_GEN_ERROR"); + } +} + +async function generateSessionKey() { + try { + sessionKey = await window.crypto.subtle.generateKey( + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"] + ); + console.log("Session key generated"); + return sessionKey; + } catch (error) { + console.error("Failed to generate session key:", error); + throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR"); + } +} + +async function exportPublicKey(key) { + try { + const exported = await window.crypto.subtle.exportKey("spki", key); + const base64Key = btoa(String.fromCharCode(...new Uint8Array(exported))); + + if (!SecurityUtils.isValidBase64(base64Key)) { + throw new Error('Invalid key export'); + } + + return base64Key; + } catch (error) { + console.error("Failed to export public key:", error); + throw new SecureChatError("Public key export failed", "KEY_EXPORT_ERROR"); + } +} + +async function importPublicKey(keyString) { + try { + if (!SecurityUtils.isValidBase64(keyString)) { + throw new Error('Invalid base64 key format'); + } + + const keyData = Uint8Array.from(atob(keyString), c => c.charCodeAt(0)); + + return await window.crypto.subtle.importKey( + "spki", + keyData, + { name: "RSA-OAEP", hash: "SHA-256" }, + false, + ["encrypt"] + ); + } catch (error) { + console.error("Failed to import public key:", error); + throw new SecureChatError("Public key import failed", "KEY_IMPORT_ERROR"); + } +} + +async function encryptSessionKey(sessionKey, publicKey) { + try { + const keyData = await window.crypto.subtle.exportKey("raw", sessionKey); + const encrypted = await window.crypto.subtle.encrypt( + { name: "RSA-OAEP" }, + publicKey, + keyData + ); + return btoa(String.fromCharCode(...new Uint8Array(encrypted))); + } catch (error) { + console.error("Failed to encrypt session key:", error); + throw new SecureChatError("Session key encryption failed", "SESSION_KEY_ENCRYPT_ERROR"); + } +} + +async function decryptSessionKey(encryptedKey) { + try { + if (!SecurityUtils.isValidBase64(encryptedKey)) { + throw new Error('Invalid encrypted key format'); + } + + const keyData = Uint8Array.from(atob(encryptedKey), c => c.charCodeAt(0)); + const decrypted = await window.crypto.subtle.decrypt( + { name: "RSA-OAEP" }, + keyPair.privateKey, + keyData + ); + + return await window.crypto.subtle.importKey( + "raw", + decrypted, + { name: "AES-GCM" }, + false, + ["encrypt", "decrypt"] + ); + } catch (error) { + console.error("Failed to decrypt session key:", error); + throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR"); + } +} + +async function encryptMessage(message) { + if (!sessionKey) { + throw new SecureChatError("No session key available", "NO_SESSION_KEY"); + } + + try { + const sanitizedMessage = SecurityUtils.sanitizeInput(message); + const encoder = new TextEncoder(); + const data = encoder.encode(sanitizedMessage); + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + + const encrypted = await window.crypto.subtle.encrypt( + { name: "AES-GCM", iv: iv }, + sessionKey, + data + ); + + return { + encrypted: btoa(String.fromCharCode(...new Uint8Array(encrypted))), + iv: btoa(String.fromCharCode(...iv)) + }; + } catch (error) { + console.error("Failed to encrypt message:", error); + throw new SecureChatError("Message encryption failed", "MESSAGE_ENCRYPT_ERROR"); + } +} + +async function decryptMessage(encryptedData, ivString) { + if (!sessionKey) { + return "[Encrypted message - no session key]"; + } + + try { + if (!SecurityUtils.isValidBase64(encryptedData) || !SecurityUtils.isValidBase64(ivString)) { + return "[Invalid encrypted message format]"; + } + + const encrypted = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0)); + const iv = Uint8Array.from(atob(ivString), c => c.charCodeAt(0)); + + const decrypted = await window.crypto.subtle.decrypt( + { name: "AES-GCM", iv: iv }, + sessionKey, + encrypted + ); + + const decoder = new TextDecoder(); + const decryptedMessage = decoder.decode(decrypted); + + return SecurityUtils.sanitizeInput(decryptedMessage); + } catch (error) { + console.error("Failed to decrypt message:", error); + return "[Failed to decrypt message]"; + } +} + +// Enhanced socket listeners with fixed key exchange +function setupSocketListeners() { + socket.on('connect', () => { + isConnected = true; + reconnectAttempts = 0; + console.log("Socket connected securely"); + updateConnectionStatus(true); + updateInputState(); + + // If user is already in a room, re-emit join_room after reconnect + if (currentRoom && keyPair && keysReady) { + exportPublicKey(keyPair.publicKey).then((publicKeyString) => { + socket.emit('join_room', { + room_id: currentRoom, + public_key: publicKeyString, + password: '' + }); + }); + } + }); + + socket.on('disconnect', (reason) => { + isConnected = false; + updateStatus("Connection lost. Attempting to reconnect...", true); + updateConnectionStatus(false); + updateInputState(); + console.log('Socket disconnected:', reason); + + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + reconnectAttempts++; + setTimeout(() => { + if (!isConnected && reconnectAttempts <= MAX_RECONNECT_ATTEMPTS) { + socket.connect(); + } + }, 2000 * Math.pow(2, reconnectAttempts)); + } else { + updateStatus("Connection failed. Please refresh the page.", true); + updateInputState(); + } + }); + + socket.on('connect_error', (error) => { + console.error('Connection error:', error); + updateStatus("Connection error. Please check your internet connection.", true); + }); + + socket.on('user_connected', (data) => { + if (data && data.user_id) { + currentUserId = data.user_id; + console.log("Connected as user:", currentUserId); + } + }); + + socket.on('room_joined', async (data) => { + try { + if (data.error) { + updateStatus(data.error, true); + updateInputState(); + return; + } + + if (!data.room_id || !data.user_id || !SecurityUtils.isValidRoomId(data.room_id)) { + throw new SecureChatError("Invalid room data received", "INVALID_ROOM_DATA"); + } + + currentRoom = data.room_id; + currentUserId = data.user_id; + currentDisplayName = data.display_name; + roomUsers = data.user_keys || {}; + + // Switch to chat screen + document.getElementById('welcomeScreen').style.display = 'none'; + document.getElementById('chatScreen').style.display = 'flex'; + document.getElementById('roomInfo').style.display = 'flex'; + document.getElementById('inputSection').style.display = 'block'; + + // Update room info + document.getElementById('currentRoomId').textContent = currentRoom; + document.getElementById('userCounter').textContent = `Users: ${data.users?.length || 0}`; + document.getElementById('messageCounter').textContent = `Messages: ${data.message_count || 0}/256`; + + // Handle session key based on user status + if (data.is_first_user) { + console.log("First user - generating session key"); + await generateSessionKey(); + updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false); + updateInputState(); + } else if (sessionKey) { + console.log("Rejoining with existing session key"); + updateStatus("Session key present. You can chat.", false); + updateInputState(); + } else { + console.log("Waiting for session key from existing users"); + updateStatus("Waiting for session key from other users...", false); + updateInputState(); + } + + // Load existing messages + if (data.messages && Array.isArray(data.messages)) { + for (const msg of data.messages) { + if (msg && typeof msg === 'object') { + await displayMessage(msg); + } + } + } + + console.log("[room_joined] sessionKey:", !!sessionKey, "isFirstUser:", data.is_first_user); + + } catch (error) { + console.error("Error processing room_joined:", error); + updateStatus("Error joining room", true); + updateInputState(); + } + }); + + socket.on('user_joined', async (data) => { + try { + if (!data || !data.user_id || !data.public_key) return; + + roomUsers[data.user_id] = data.public_key; + updateUsersCount(); + addSystemMessage(`${data.display_name || 'User'} joined the room`); + + // If we have a session key, share it with the new user + if (sessionKey) { + console.log("Sharing session key with new user:", data.user_id); + try { + const publicKey = await importPublicKey(data.public_key); + const encryptedKey = await encryptSessionKey(sessionKey, publicKey); + + socket.emit('share_session_key', { + room_id: currentRoom, + target_user_id: data.user_id, + encrypted_key: encryptedKey + }); + + console.log("Session key shared with new user successfully"); + } catch (error) { + console.error("Failed to share session key with new user:", error); + } + } + } catch (error) { + console.error("Error processing user_joined:", error); + } + }); + + socket.on('request_session_key', async (data) => { + try { + console.log("Session key requested for new user:", data.new_user_id); + + if (!sessionKey || !data.new_user_id || !data.public_key) { + console.log("Cannot fulfill session key request - missing data"); + return; + } + + try { + const publicKey = await importPublicKey(data.public_key); + const encryptedKey = await encryptSessionKey(sessionKey, publicKey); + + socket.emit('share_session_key', { + room_id: currentRoom, + target_user_id: data.new_user_id, + encrypted_key: encryptedKey + }); + + console.log("Session key shared in response to request"); + } catch (error) { + console.error("Failed to share session key on request:", error); + } + } catch (error) { + console.error("Error processing request_session_key:", error); + } + }); + + socket.on('session_key_received', async (data) => { + try { + console.log("Session key received from:", data.from_user_id); + + if (!data || !data.encrypted_key) { + console.log("Invalid session key data received"); + return; + } + + if (!sessionKey) { + try { + sessionKey = await decryptSessionKey(data.encrypted_key); + console.log("Session key decrypted successfully"); + updateStatus("Session key received! You can now chat securely.", false); + updateInputState(); + addSystemMessage("๐Ÿ”‘ Session key received - you can now chat!"); + } catch (error) { + console.error("Failed to decrypt received session key:", error); + updateStatus("Failed to decrypt session key", true); + } + } else { + console.log("Session key already exists, ignoring duplicate"); + } + } catch (error) { + console.error("Failed to process session key:", error); + updateStatus("Error: Failed to decrypt session key", true); + } + }); + + socket.on('new_message', async (data) => { + try { + if (!data || typeof data !== 'object') return; + + await displayMessage(data); + + if (data.message_count) { + document.getElementById('messageCounter').textContent = `Messages: ${data.message_count}/256`; + } + } catch (error) { + console.error("Error processing new message:", error); + } + }); + + socket.on('user_left', (data) => { + try { + if (data && data.user_id) { + delete roomUsers[data.user_id]; + updateUsersCount(); + addSystemMessage(`User left the room`); + } + } catch (error) { + console.error("Error processing user_left:", error); + } + }); + + socket.on('error', (error) => { + console.error('Socket error:', error); + updateStatus("Connection error occurred", true); + }); + + socket.on('ping', () => { + socket.emit('pong'); + }); +} + +async function createRoom() { + try { + const roomId = SecurityUtils.generateSecureId(6); + const password = document.getElementById('roomPasswordInput').value; + await joinSpecificRoom(roomId, password); + } catch (error) { + console.error("Failed to create room:", error); + updateStatus("Error creating room", true); + } +} + +async function joinRoom() { + try { + const roomInput = document.getElementById('roomInput'); + const roomId = SecurityUtils.sanitizeInput(roomInput.value); + const password = document.getElementById('roomPasswordInput').value; + + if (!roomId) { + await createRoom(); + return; + } + + if (!SecurityUtils.isValidRoomId(roomId)) { + updateStatus("Invalid room ID format. Use only letters, numbers, hyphens and underscores.", true); + return; + } + + await joinSpecificRoom(roomId, password); + } catch (error) { + console.error("Failed to join room:", error); + updateStatus("Error joining room", true); + } +} + +async function joinSpecificRoom(roomId, password = "") { + if (!keysReady) { + updateStatus("Error: Encryption keys not ready", true); + return; + } + + if (!SecurityUtils.isValidRoomId(roomId)) { + updateStatus("Invalid room ID format", true); + return; + } + + try { + updateStatus("Joining room...", false); + + const publicKeyString = await exportPublicKey(keyPair.publicKey); + + socket.emit('join_room', { + room_id: roomId, + public_key: publicKeyString, + password: password + }); + } catch (error) { + console.error("Failed to join room:", error); + updateStatus("Error: Failed to join room", true); + } +} + +async function sendMessage() { + try { + const input = document.getElementById('messageInput'); + let message = SecurityUtils.sanitizeInput(input.value); + + if (!message || !sessionKey) return; + + if (!SecurityUtils.canSendMessage()) { + updateStatus("Please wait before sending another message", true); + return; + } + + if (message.length > MAX_MESSAGE_LENGTH) { + updateStatus("Message too long", true); + return; + } + + const encryptedData = await encryptMessage(message); + + socket.emit('send_message', { + encrypted_content: encryptedData.encrypted, + iv: encryptedData.iv + }); + + input.value = ''; + autoResize(input); + + } catch (error) { + console.error("Failed to send message:", error); + updateStatus("Error: Failed to send message", true); + } +} + +async function displayMessage(messageData) { + try { + if (!messageData || typeof messageData !== 'object') return; + + const container = document.getElementById('messagesContainer'); + const isOwnMessage = messageData.sender_id === currentUserId; + + let displayText; + if (messageData.iv && sessionKey && messageData.encrypted_content) { + displayText = await decryptMessage(messageData.encrypted_content, messageData.iv); + } else { + displayText = "[Encrypted message]"; + } + + const escapeHtml = (text) => { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }; + + const timestamp = messageData.timestamp ? + new Date(messageData.timestamp * 1000).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + }) : + new Date().toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + }); + + const displayName = messageData.display_name || `User-${messageData.sender_id?.substring(0, 8) || 'Unknown'}`; + const avatarText = displayName.substring(0, 2).toUpperCase(); + + const messageGroup = document.createElement('div'); + messageGroup.className = 'message-group'; + messageGroup.classList.add(isOwnMessage ? 'sent' : 'received'); + + messageGroup.innerHTML = ` +
+
${escapeHtml(avatarText)}
+ ${escapeHtml(displayName)} + ${escapeHtml(timestamp)} +
+
+ `; + + const messageContentEl = messageGroup.querySelector('.message-content'); + messageContentEl.textContent = displayText; + + container.appendChild(messageGroup); + container.scrollTop = container.scrollHeight; + + const messages = container.children; + if (messages.length > MAX_MESSAGES + 50) { + for (let i = 0; i < 50; i++) { + if (messages[0]) { + container.removeChild(messages[0]); + } + } + } + + } catch (error) { + console.error("Error displaying message:", error); + } +} + +function addSystemMessage(text) { + try { + const container = document.getElementById('messagesContainer'); + const systemMessage = document.createElement('div'); + systemMessage.className = 'message-group system'; // โœ… add system class + + const timestamp = new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); + + systemMessage.innerHTML = ` +
+
โ„น๏ธ
+ System + ${timestamp} +
+
+ `; + + const messageContentEl = systemMessage.querySelector('.message-content'); + messageContentEl.textContent = text; + + container.appendChild(systemMessage); + container.scrollTop = container.scrollHeight; + + } catch (error) { + console.error("Error adding system message:", error); + } +} + + +function updateStatus(message, isError = false) { + try { + const statusEl = document.getElementById('statusText'); + const statusDiv = document.createElement('div'); + + if (isError) { + statusDiv.className = 'error-message'; + } else { + statusDiv.className = 'success-message'; + } + + statusDiv.textContent = message; + statusEl.innerHTML = ''; + statusEl.appendChild(statusDiv); + + } catch (error) { + console.error("Error updating status:", error); + } +} + +function updateUsersCount() { + try { + const count = Object.keys(roomUsers).length; + const counterEl = document.getElementById('userCounter'); + if (counterEl) { + counterEl.textContent = `Users: ${count}`; + } + } catch (error) { + console.error("Error updating users count:", error); + } +} + +function updateInputState() { + try { + const input = document.getElementById('messageInput'); + const btn = document.getElementById('sendButton'); + + if (!input || !btn) return; + + if (sessionKey && isConnected) { + input.disabled = false; + btn.disabled = false; + input.placeholder = "Message ByteChatโ€ฆ"; + } else { + input.disabled = true; + btn.disabled = true; + input.placeholder = isConnected ? "Waiting for encryption key..." : "Disconnected..."; + } + } catch (error) { + console.error("Error updating input state:", error); + } +} + +function autoResize(textarea) { + try { + if (!textarea) return; + + textarea.style.height = 'auto'; + const newHeight = Math.min(Math.max(textarea.scrollHeight, 44), 200); + textarea.style.height = newHeight + 'px'; + } catch (error) { + console.error("Error in autoResize:", error); + } +} + +function updateConnectionStatus(connected) { + const encryptionBadge = document.getElementById('encryptionBadge'); + const securityIndicator = document.getElementById('securityIndicator'); + + if (connected) { + if (encryptionBadge) encryptionBadge.textContent = 'E2E Encrypted'; + if (securityIndicator) securityIndicator.textContent = '๐Ÿ”’ End-to-End Encrypted'; + } else { + if (encryptionBadge) encryptionBadge.textContent = 'Disconnected'; + if (securityIndicator) securityIndicator.textContent = 'โš ๏ธ Disconnected'; + } +} + +// Event listeners setup +document.addEventListener('DOMContentLoaded', () => { + try { + const messageInput = document.getElementById('messageInput'); + const roomInput = document.getElementById('roomInput'); + const roomPasswordInput = document.getElementById('roomPasswordInput'); + + if (messageInput) { + messageInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }); + + messageInput.addEventListener('input', (e) => { + autoResize(e.target); + + if (e.target.value.length > MAX_MESSAGE_LENGTH) { + e.target.value = e.target.value.substring(0, MAX_MESSAGE_LENGTH); + updateStatus(`Message truncated to ${MAX_MESSAGE_LENGTH} characters`, true); + } + }); + + messageInput.addEventListener('paste', (e) => { + setTimeout(() => { + if (messageInput.value.length > MAX_MESSAGE_LENGTH) { + messageInput.value = messageInput.value.substring(0, MAX_MESSAGE_LENGTH); + updateStatus(`Pasted content truncated to ${MAX_MESSAGE_LENGTH} characters`, true); + } + autoResize(messageInput); + }, 0); + }); + } + + if (roomInput) { + roomInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + joinRoom(); + } + }); + + roomInput.addEventListener('input', (e) => { + const value = e.target.value; + if (value && !SecurityUtils.isValidRoomId(value)) { + e.target.setCustomValidity('Only letters, numbers, hyphens and underscores allowed'); + } else { + e.target.setCustomValidity(''); + } + }); + } + + if (roomPasswordInput) { + roomPasswordInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + joinRoom(); + } + }); + } + + const joinRoomBtn = document.getElementById('joinRoomBtn'); + if (joinRoomBtn) joinRoomBtn.addEventListener('click', joinRoom); + + const createRoomBtn = document.getElementById('createRoomBtn'); + if (createRoomBtn) createRoomBtn.addEventListener('click', createRoom); + + const sendButton = document.getElementById('sendButton'); + if (sendButton) sendButton.addEventListener('click', sendMessage); + + document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && messageInput && !messageInput.disabled) { + e.preventDefault(); + sendMessage(); + } + + if (e.key === 'Escape' && messageInput) { + messageInput.value = ''; + autoResize(messageInput); + } + }); + + initializeApp(); + + } catch (error) { + console.error("Error setting up event listeners:", error); + updateStatus("Error initializing interface", true); + } +}); + +// Periodic connection health check +setInterval(() => { + if (socket && isConnected) { + socket.volatile.emit('ping'); + } +}, 30000); + +// Prevent clickjacking +if (top !== self) { + top.location = self.location; +} + +// CSP violation reporting +document.addEventListener('securitypolicyviolation', (e) => { + console.error('CSP Violation:', { + violatedDirective: e.violatedDirective, + blockedURI: e.blockedURI, + lineNumber: e.lineNumber, + columnNumber: e.columnNumber + }); +}); + +// Expose necessary functions +window.SecureChat = { + joinRoom, + createRoom, + sendMessage +}; + +// Prevent console access in production +if (location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { + console.log = console.warn = console.error = () => {}; +} \ No newline at end of file diff --git a/src/static/styles.css b/src/static/styles.css deleted file mode 100644 index 2450522..0000000 --- a/src/static/styles.css +++ /dev/null @@ -1,437 +0,0 @@ -* { - 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/static/stylesheet.css b/src/static/stylesheet.css new file mode 100644 index 0000000..af65442 --- /dev/null +++ b/src/static/stylesheet.css @@ -0,0 +1,548 @@ +/* Reset & base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + overflow: hidden; /* prevents double scrollbars */ +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%); + color: #ffffff; + display: flex; + flex-direction: column; +} + +/* Main container */ +.chat-container { + flex: 1; + display: flex; + flex-direction: column; + height: 100vh; + max-width: 100vw; +} + +/* Header */ +.chat-header { + background: rgba(20, 20, 20, 0.95); + border-bottom: 1px solid #333; + padding: 1rem 1.5rem; + backdrop-filter: blur(10px); + flex-shrink: 0; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + font-size: 1.5rem; + font-weight: 700; + color: #00ff88; +} + +.encryption-badge { + background: linear-gradient(45deg, #00ff88, #00cc6a); + color: #000; + padding: 0.5rem 1rem; + border-radius: 20px; + font-weight: 600; + font-size: 0.85rem; +} + +/* Welcome screen */ +.welcome-screen { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 2rem; + text-align: center; +} + +.welcome-title { + font-size: 3rem; + font-weight: 700; + margin-bottom: 1rem; + background: linear-gradient(45deg, #00ff88, #ffffff); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.welcome-subtitle { + font-size: 1.1rem; + color: #aaa; + margin-bottom: 3rem; + max-width: 600px; + line-height: 1.6; +} + +.room-section { + margin-bottom: 3rem; +} + +.room-input-container { + display: flex; + gap: 1rem; + margin-bottom: 1rem; + flex-wrap: wrap; + justify-content: center; +} + +.room-input { + padding: 0.75rem 1rem; + border: 2px solid #333; + border-radius: 8px; + background: #1a1a1a; + color: #fff; + font-size: 1rem; + min-width: 200px; +} + +.room-input:focus { + outline: none; + border-color: #00ff88; + box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1); +} + +/* Buttons */ +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + background: linear-gradient(45deg, #00ff88, #00cc6a); + color: #000; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + font-size: 1rem; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 255, 136, 0.3); +} + +.btn-secondary { + background: linear-gradient(45deg, #333, #555); + color: #fff; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +/* Status text */ +.status-text { + color: #aaa; + margin-top: 1rem; + min-height: 24px; +} + +.loading-message { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.loading-spinner { + width: 16px; + height: 16px; + border: 2px solid #333; + border-top: 2px solid #00ff88; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.error-message { + color: #ff4444; + font-weight: 600; +} + +.success-message { + color: #00ff88; + font-weight: 600; +} + +/* Features */ +.feature-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + max-width: 1000px; + margin-top: 2rem; +} + +.feature-item { + background: rgba(255, 255, 255, 0.03); + border: 1px solid #333; + border-radius: 12px; + padding: 1.5rem; + text-align: center; +} + +.feature-icon { + font-size: 2rem; + margin-bottom: 0.5rem; +} + +.feature-title { + font-weight: 600; + margin-bottom: 0.5rem; + color: #fff; +} + +.feature-desc { + color: #aaa; + font-size: 0.9rem; + line-height: 1.4; +} + +/* Room info */ +.room-info { + background: rgba(20, 20, 20, 0.95); + border-bottom: 1px solid #333; + padding: 0.75rem 1rem; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: 0.75rem; + position: sticky; + top: 0; + z-index: 10; +} + +.room-details { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: flex-start; + flex: 0 0 auto; + min-width: auto; +} + +.room-details span { + color: #aaa; + font-size: 0.85rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.room-details strong { + color: #00ff88; +} + +.room-actions { + display: flex; + gap: 0.5rem; + align-items: center; +} + +/* Chat screen & messages */ +.chat-screen { + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.messages-wrapper { + flex: 1; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + scroll-behavior: smooth; + padding-bottom: 80px; /* leave space for input */ +} + +.message-group { + display: flex; + flex-direction: column; + animation: fadeIn 0.3s ease-out; +} + +/* Align left vs right */ +.message-group.sent { + align-items: flex-end; + margin-left: auto; +} + +.message-group.received { + align-items: flex-start; + margin-right: auto; +} + +.message-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; + font-size: 0.8rem; + color: #888; +} + +.message-group.sent .message-header { + flex-direction: row-reverse; +} + +/* System messages */ +.message-group.system { + align-items: center; + justify-content: center; + margin: 0.5rem auto; + text-align: center; + max-width: 100%; +} + +.message-group.system .message-content { + background: rgba(255, 255, 255, 0.08); + border: 1px solid #444; + color: #ccc; + font-size: 0.85rem; + font-style: italic; + border-radius: 12px; + padding: 0.35rem 0.75rem; + width: fit-content; /* ๐Ÿ‘ˆ shrink to text */ + max-width: 90%; /* still wrap if very long */ + text-align: center; +} + +.user-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + background: linear-gradient(45deg, #00ff88, #00cc6a); + color: #000; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + font-weight: 700; +} + +.user-avatar.own { + background: linear-gradient(45deg, #0088ff, #0066cc); + color: #fff; +} + +.user-name { + font-weight: 600; +} + +.message-time { + font-size: 0.75rem; + opacity: 0.7; +} + +/* The bubble */ +.message-content { + display: inline-block; + background: #2d2d2d; + border: 1px solid #404040; + border-radius: 12px; + padding: 0.5rem 0.75rem; + word-wrap: break-word; + max-width: 75%; /* prevents overly long messages */ + width: auto; /* shrink to fit content */ + position: relative; + text-align: left; +} + +.message-group.sent .message-content { + background: #1a4d3a; + border-color: #00ff88; + text-align: left; /* keep text natural */ +} + +/* Input section */ +.input-section { + padding: 0.75rem 1rem; + background: rgba(20, 20, 20, 0.95); + border-top: 1px solid #333; + position: sticky; + bottom: 0; + left: 0; + width: 100%; + flex-shrink: 0; +} + +.input-container { + display: flex; + gap: 0.5rem; + align-items: flex-end; +} + +.message-input { + flex: 1; + border: 2px solid #333; + border-radius: 12px; + background: #1a1a1a; + color: #fff; + padding: 0.75rem 1rem; + font-size: 1rem; + font-family: inherit; + resize: none; + max-height: 120px; + min-height: 44px; +} + +.message-input:focus { + outline: none; + border-color: #00ff88; + box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1); +} + +.message-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.send-button { + width: 44px; + height: 44px; + border: none; + border-radius: 50%; + background: linear-gradient(45deg, #00ff88, #00cc6a); + color: #000; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + flex-shrink: 0; +} + +.send-button:hover:not(:disabled) { + transform: scale(1.1); + box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3); +} + +.send-button:disabled { + opacity: 0.3; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +/* Security indicator */ +.security-indicator { + position: fixed; + bottom: calc(1rem + 70px); + right: 20px; + background: rgba(0, 0, 0, 0.8); + color: #00ff88; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.8rem; + border: 1px solid #00ff88; + z-index: 1000; +} + +/* Animations */ +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Responsive */ +@media (max-width: 768px) { + .welcome-title { + font-size: 2rem; + } + + .room-input-container { + flex-direction: column; + align-items: stretch; + } + + .room-input { + min-width: 100%; + } + + .feature-list { + grid-template-columns: 1fr; + } + + .room-info { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + padding: 0.75rem; + } + + .room-details { + width: 100%; + } + + .room-actions { + width: 100%; + justify-content: flex-end; + } + + .message-content { + max-width: 85%; + } + + .input-section { + padding: 0.75rem; + } +} + +@media (max-width: 480px) { + .room-details span { + font-size: 0.8rem; + } + + .btn { + padding: 0.6rem 1rem; + font-size: 0.9rem; + } + + .message-content { + max-width: 90%; + } +} + +/* Accessibility */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.btn:focus, +.room-input:focus, +.message-input:focus, +.send-button:focus { + outline: 2px solid #00ff88; + outline-offset: 2px; +} diff --git a/src/templates/chat.html b/src/templates/chat.html index 06cc1bf..5e116ad 100644 --- a/src/templates/chat.html +++ b/src/templates/chat.html @@ -3,468 +3,21 @@ - Secure Chat Platform + ByteChat + + -
E2E Encrypted @@ -532,30 +85,33 @@
-