Cleanup: channels support, security hardening, remove cruft

This commit is contained in:
Your Name 2026-07-22 02:15:25 +03:00
parent 19e115c926
commit b8daea66f2
9 changed files with 394 additions and 707 deletions

3
Anime/README.md Normal file
View File

@ -0,0 +1,3 @@
# Anime Channel
This directory is dedicated to discussions and content about anime.

3
Games/README.md Normal file
View File

@ -0,0 +1,3 @@
# Games Channel
This directory is dedicated to discussions and content about games.

3
Politics/README.md Normal file
View File

@ -0,0 +1,3 @@
# Politics Channel
This directory is dedicated to discussions and content about politics.

View File

@ -1,2 +1,53 @@
# FuckingChat # FuckingChat 🔒
Encrypted chat with channel support. Messages are encrypted at rest using Fernet (PBKDF2 + SHA256).
## Channels
- 💬 **General** — general discussion
- 🎮 **Games** — gaming chat
- 🎌 **Anime** — anime & manga
- ⚖️ **Politics** — politics discussion
## Setup
```bash
# Install dependencies
pip install bottle cryptography sqlalchemy
# Run (development)
python index.py
# Run (production - recommended)
CHAT_SECRET="your-strong-secret" \
CHAT_SALT="your-random-salt" \
CHAT_ADMIN_TOKEN="your-admin-token" \
CHAT_RATE_LIMIT=5 \
CHAT_HOST=0.0.0.0 \
CHAT_PORT=8080 \
python index.py
```
## Environment Variables
| Variable | Default | Description |
|---|---|---|
| `CHAT_SECRET` | `default_secret_change_me` | Encryption key |
| `CHAT_SALT` | `default_salt_change_me` | PBKDF2 salt |
| `CHAT_ADMIN_TOKEN` | (none) | Token for DELETE API |
| `CHAT_RATE_LIMIT` | `5` | Max messages per window |
| `CHAT_RATE_WINDOW` | `10` | Rate limit window (seconds) |
| `CHAT_MAX_MSG` | `2000` | Max message length |
| `CHAT_DB_PATH` | `./chat.db` | SQLite database path |
| `CHAT_HOST` | `localhost` | Listen address |
| `CHAT_PORT` | `8080` | Listen port |
| `CHAT_DEBUG` | `0` | Debug mode (set to `1` for dev) |
## Security Features
- ✅ Messages encrypted at rest (AES via Fernet)
- ✅ Rate limiting (configurable)
- ✅ IP hashing (privacy)
- ✅ XSS protection (HTML tag stripping)
- ✅ Admin token required for deletion
- ✅ No debug mode in production

BIN
chat.db

Binary file not shown.

View File

@ -1,191 +0,0 @@
import sqlalchemy
from sqlalchemy import create_engine, String, Integer, Text, DateTime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlalchemy.orm import sessionmaker
from datetime import datetime
import time
import os
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
# Encryption setup
SALT = b'salt_123456789'
PASSWORD = b'chat_secret_key_123'
def get_cipher():
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=SALT,
iterations=100000,
)
key = base64.urlsafe_b64encode(kdf.derive(PASSWORD))
return Fernet(key)
class Base(DeclarativeBase):
pass
class Messages(Base):
__tablename__ = "messages"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
ip: Mapped[str] = mapped_column(String(64), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
timestamp: Mapped[int] = mapped_column(Integer, nullable=False)
# MariaDB configuration
DB_USER = 'chat'
DB_PASSWORD = 'uqhyUb5eBGg3qad.'
DB_HOST = 'localhost'
DB_PORT = '3306'
DB_NAME = 'chat_db'
# Create engine for MariaDB
DATABASE_URL = f"mariadb+mariadbconnector://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
# Alternative with pymysql (uncomment if needed):
# DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
engine = create_engine(
DATABASE_URL,
echo=False,
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
pool_recycle=3600
)
# Create tables
Base.metadata.create_all(engine)
class DBfrontend():
def __init__(self):
self.Session = sessionmaker(bind=engine)
self.cipher = get_cipher()
def encrypt_message(self, message: str) -> str:
"""Encrypt message before storing"""
return self.cipher.encrypt(message.encode()).decode()
def decrypt_message(self, encrypted_message: str) -> str:
"""Decrypt message after retrieval"""
try:
return self.cipher.decrypt(encrypted_message.encode()).decode()
except:
return "[Decryption failed]"
def add_message(self, ip: str, content: str) -> int:
"""Add a new message to the database and return its ID"""
with self.Session() as session:
encrypted_content = self.encrypt_message(content)
new_message = Messages(
ip=ip,
content=encrypted_content,
timestamp=int(time.time() * 1000)
)
session.add(new_message)
session.commit()
return new_message.id
def get_all_messages(self) -> list:
"""Get all messages from the database"""
with self.Session() as session:
messages = session.query(Messages).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
def get_message_by_id(self, message_id: int) -> Messages | None:
"""Get a specific message by its ID"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
message.content = self.decrypt_message(message.content)
return message
def get_messages_by_ip(self, ip: str) -> list:
"""Get all messages from a specific IP address"""
with self.Session() as session:
messages = session.query(Messages).filter(Messages.ip == ip).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
def update_message(self, message_id: int, new_content: str) -> bool:
"""Update the content of a message"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
message.content = self.encrypt_message(new_content)
session.commit()
return True
return False
def delete_message(self, message_id: int) -> bool:
"""Delete a message by its ID"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
session.delete(message)
session.commit()
return True
return False
def delete_messages_by_ip(self, ip: str) -> int:
"""Delete all messages from a specific IP address and return count"""
with self.Session() as session:
messages = session.query(Messages).filter(Messages.ip == ip).all()
count = len(messages)
for message in messages:
session.delete(message)
session.commit()
return count
def get_message_count(self) -> int:
"""Get total number of messages"""
with self.Session() as session:
return session.query(Messages).count()
def get_latest_messages(self, limit: int = 10) -> list:
"""Get the latest N messages"""
with self.Session() as session:
messages = session.query(Messages).order_by(Messages.id.desc()).limit(limit).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
def clear_all_messages(self) -> int:
"""Delete all messages and return count"""
with self.Session() as session:
count = session.query(Messages).count()
session.query(Messages).delete()
session.commit()
return count
def get_messages_by_date_range(self, start_timestamp: int, end_timestamp: int) -> list:
"""Get messages within a timestamp range"""
with self.Session() as session:
messages = session.query(Messages).filter(
Messages.timestamp >= start_timestamp,
Messages.timestamp <= end_timestamp
).order_by(Messages.timestamp.desc()).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
# Optional: Test connection function
def test_connection():
"""Test if MariaDB connection is working"""
try:
with engine.connect() as conn:
result = conn.execute(sqlalchemy.text("SELECT 1"))
print("✅ MariaDB connection successful!")
return True
except Exception as e:
print(f"❌ MariaDB connection failed: {e}")
return False
# Run test if executed directly
if __name__ == "__main__":
test_connection()

View File

@ -3,459 +3,297 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Secure Chat</title> <title>FuckingChat</title>
<style> <style>
* { * { margin: 0; padding: 0; box-sizing: border-box; }
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #2c2c2c; background: #1a1a2e;
color: #d4d4d4; color: #e0e0e0;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
} }
.chat-container { .chat-container {
background: #3a3a3a; background: #16213e;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); box-shadow: 0 8px 32px rgba(0,0,0,0.5);
width: 100%; width: 100%;
max-width: 800px; max-width: 900px;
height: 90vh; height: 90vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.chat-header { .chat-header {
background: #2f2f2f; background: #0f3460;
padding: 20px; padding: 16px 20px;
border-bottom: 1px solid #4a4a4a; border-bottom: 1px solid #1a4a7a;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.chat-header h1 { .chat-header h1 {
color: #e0e0e0; color: #e94560;
font-size: 20px; font-size: 18px;
font-weight: 600; font-weight: 700;
letter-spacing: 0.5px; letter-spacing: 1px;
} }
.chat-header .status { .chat-header .status {
color: #888;
font-size: 12px; font-size: 12px;
background: #3f3f3f; background: #1a4a7a;
padding: 4px 12px; padding: 4px 12px;
border-radius: 12px; border-radius: 12px;
border: 1px solid #4a4a4a; color: #a0c4ff;
}
.channel-bar {
display: flex;
background: #1a2744;
padding: 8px 12px;
gap: 6px;
border-bottom: 1px solid #0f3460;
overflow-x: auto;
}
.channel-btn {
padding: 6px 16px;
border: 1px solid #2a4a7a;
border-radius: 20px;
background: transparent;
color: #8899aa;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
transition: all 0.2s;
}
.channel-btn:hover {
background: #1a3a5a;
color: #c0d0e0;
}
.channel-btn.active {
background: #e94560;
color: #fff;
border-color: #e94560;
} }
.chat-messages { .chat-messages {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 16px 20px;
background: #333; background: #121e38;
} }
.chat-messages::-webkit-scrollbar { width: 6px; }
.chat-messages::-webkit-scrollbar { .chat-messages::-webkit-scrollbar-track { background: #0d1528; }
width: 8px; .chat-messages::-webkit-scrollbar-thumb { background: #2a4a7a; border-radius: 3px; }
}
.chat-messages::-webkit-scrollbar-track {
background: #2c2c2c;
}
.chat-messages::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: #666;
}
.message { .message {
margin-bottom: 16px; background: #1a2d4a;
display: flex;
flex-direction: column;
}
.message-ip {
font-size: 11px;
color: #888;
margin-bottom: 4px;
font-family: 'Courier New', monospace;
}
.message-content {
background: #404040;
padding: 10px 14px;
border-radius: 8px; border-radius: 8px;
border-left: 3px solid #666; padding: 10px 14px;
word-wrap: break-word; margin-bottom: 8px;
line-height: 1.5; border-left: 3px solid #e94560;
position: relative;
padding-right: 60px;
} }
.message .msg-ip {
.message-content .timestamp {
font-size: 10px;
color: #777;
position: absolute;
right: 10px;
bottom: 6px;
}
.message-actions {
margin-top: 4px;
display: flex;
gap: 8px;
}
.message-actions button {
background: #4a4a4a;
border: none;
color: #aaa;
padding: 2px 10px;
border-radius: 4px;
font-size: 11px; font-size: 11px;
cursor: pointer; color: #6a8aaa;
transition: all 0.2s; margin-bottom: 4px;
border: 1px solid #555;
} }
.message .msg-content {
.message-actions button:hover { font-size: 14px;
background: #555; line-height: 1.5;
color: #ddd; word-wrap: break-word;
border-color: #666;
} }
.message .msg-time {
.message-actions button.delete-btn:hover { font-size: 11px;
background: #5a3a3a; color: #556677;
border-color: #8a4a4a; text-align: right;
color: #ff8888; margin-top: 4px;
} }
.empty-state {
.message-input-area { text-align: center;
background: #2f2f2f; color: #556677;
padding: 20px; padding: 40px;
border-top: 1px solid #4a4a4a; font-size: 14px;
}
.input-area {
background: #0f3460;
padding: 12px 16px;
border-top: 1px solid #1a4a7a;
display: flex; display: flex;
gap: 12px; gap: 10px;
flex-wrap: wrap; align-items: center;
} }
.input-area input {
.message-input-area input {
flex: 1; flex: 1;
padding: 10px 14px; padding: 10px 14px;
background: #3f3f3f; border: 1px solid #1a4a7a;
border: 1px solid #4a4a4a;
border-radius: 8px; border-radius: 8px;
background: #121e38;
color: #e0e0e0; color: #e0e0e0;
font-size: 14px; font-size: 14px;
min-width: 150px;
transition: border-color 0.2s;
}
.message-input-area input:focus {
outline: none; outline: none;
border-color: #666; transition: border 0.2s;
background: #454545;
} }
.input-area input:focus { border-color: #e94560; }
.message-input-area input::placeholder { .input-area input::placeholder { color: #4a6a8a; }
color: #777; .input-area button {
} padding: 10px 20px;
background: #e94560;
.message-input-area button { color: #fff;
padding: 10px 24px; border: none;
background: #4a4a4a;
border: 1px solid #5a5a5a;
border-radius: 8px; border-radius: 8px;
color: #e0e0e0;
font-size: 14px; font-size: 14px;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: background 0.2s;
font-weight: 500;
} }
.input-area button:hover { background: #d63850; }
.message-input-area button:hover { .input-area button:disabled {
background: #555; background: #4a5a7a;
border-color: #666; cursor: not-allowed;
transform: translateY(-1px);
} }
.message-input-area button:active {
transform: translateY(0);
}
.loading {
color: #666;
text-align: center;
padding: 20px;
font-style: italic;
}
.empty-state {
color: #666;
text-align: center;
padding: 40px;
font-size: 16px;
}
.toast { .toast {
position: fixed; position: fixed;
bottom: 30px; bottom: 30px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background: #4a4a4a;
color: #e0e0e0;
padding: 10px 24px; padding: 10px 24px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #5a5a5a;
font-size: 14px; font-size: 14px;
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
pointer-events: none; pointer-events: none;
white-space: nowrap; z-index: 1000;
}
.toast.visible {
opacity: 1;
}
.toast.success {
border-color: #5a8a5a;
background: #3a5a3a;
}
.toast.error {
border-color: #8a5a5a;
background: #5a3a3a;
}
@media (max-width: 600px) {
.chat-container {
height: 100vh;
border-radius: 0;
}
.message-input-area {
flex-direction: column;
}
.message-input-area input {
min-width: unset;
}
.message-input-area button {
width: 100%;
}
.chat-header h1 {
font-size: 16px;
}
.message-content {
padding-right: 50px;
}
} }
.toast.visible { opacity: 1; }
.toast.error { background: #c0392b; color: #fff; }
.toast.success { background: #27ae60; color: #fff; }
.toast.info { background: #2980b9; color: #fff; }
</style> </style>
</head> </head>
<body> <body>
<div class="chat-container"> <div class="chat-container">
<div class="chat-header"> <div class="chat-header">
<h1>🔒 Secure Chat</h1> <h1>💬 FuckingChat</h1>
<span class="status">Encrypted</span> <span class="status" id="status-badge">Encrypted 🔒</span>
</div> </div>
<div class="channel-bar" id="channel-bar"></div>
<div class="chat-messages" id="messageContainer"> <div class="chat-messages" id="messages"></div>
<div class="loading">Loading messages...</div> <div class="input-area">
</div> <input type="text" id="msg-input" placeholder="Type a message..." maxlength="2000">
<button id="send-btn">Send</button>
<div class="message-input-area">
<input
type="text"
id="messageInput"
placeholder="Type your message..."
maxlength="500"
autocomplete="off"
>
<button id="sendButton">Send</button>
</div> </div>
</div> </div>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script> <script>
class ChatApp { const CHANNELS = [
constructor() { { id: 'general', name: 'General', icon: '💬' },
this.messageContainer = document.getElementById('messageContainer'); { id: 'games', name: 'Games', icon: '🎮' },
this.messageInput = document.getElementById('messageInput'); { id: 'anime', name: 'Anime', icon: '🎌' },
this.sendButton = document.getElementById('sendButton'); { id: 'politics', name: 'Politics', icon: '⚖️' }
this.toast = document.getElementById('toast'); ];
this.toastTimeout = null;
this.encryptionEnabled = true;
this.init(); let currentChannel = 'general';
} let pollInterval = null;
init() { const messagesEl = document.getElementById('messages');
this.loadMessages(); const inputEl = document.getElementById('msg-input');
this.setupEventListeners(); const sendBtn = document.getElementById('send-btn');
const toastEl = document.getElementById('toast');
const channelBar = document.getElementById('channel-bar');
// Auto-refresh every 10 seconds function escapeHtml(text) {
setInterval(() => this.loadMessages(), 10000); const d = document.createElement('div');
} d.textContent = text;
return d.innerHTML;
}
setupEventListeners() { function showToast(msg, type = 'info') {
this.sendButton.addEventListener('click', () => this.sendMessage()); toastEl.textContent = msg;
this.messageInput.addEventListener('keypress', (e) => { toastEl.className = 'toast visible ' + type;
if (e.key === 'Enter') this.sendMessage(); setTimeout(() => toastEl.classList.remove('visible'), 3000);
}); }
}
async sendMessage() { function renderChannels() {
const message = this.messageInput.value.trim(); channelBar.innerHTML = '';
if (!message) { CHANNELS.forEach(ch => {
this.showToast('Please enter a message', 'error'); const btn = document.createElement('button');
return; btn.className = 'channel-btn' + (ch.id === currentChannel ? ' active' : '');
} btn.textContent = ch.icon + ' ' + ch.name;
btn.onclick = () => switchChannel(ch.id);
channelBar.appendChild(btn);
});
}
this.sendButton.disabled = true; function switchChannel(channel) {
this.sendButton.textContent = 'Sending...'; if (channel === currentChannel) return;
currentChannel = channel;
renderChannels();
messagesEl.innerHTML = '<div class="empty-state">Loading...</div>';
loadMessages();
}
try { async function loadMessages() {
const response = await fetch('/api/messages', { try {
method: 'POST', const r = await fetch(`/api/messages?channel=${currentChannel}&limit=50`);
headers: { const data = await r.json();
'Content-Type': 'application/json', if (data.success) renderMessages(data.messages || []);
}, else showToast('Failed to load messages', 'error');
body: JSON.stringify({ message }) } catch (e) {
}); showToast('Network error', 'error');
const data = await response.json();
if (data.success) {
this.messageInput.value = '';
this.showToast('Message sent! ✓', 'success');
this.loadMessages(); // Refresh messages
} else {
this.showToast('Error: ' + (data.error || 'Unknown error'), 'error');
}
} catch (error) {
this.showToast('Network error: ' + error.message, 'error');
} finally {
this.sendButton.disabled = false;
this.sendButton.textContent = 'Send';
}
}
async loadMessages() {
try {
const response = await fetch('/api/messages?limit=50');
const data = await response.json();
if (data.success) {
this.renderMessages(data.messages || []);
} else {
this.showToast('Failed to load messages', 'error');
}
} catch (error) {
this.showToast('Network error loading messages', 'error');
}
}
renderMessages(messages) {
if (messages.length === 0) {
this.messageContainer.innerHTML = `
<div class="empty-state">💬 No messages yet. Start the conversation!</div>
`;
return;
}
let html = '';
// Show newest messages at bottom
messages.slice().reverse().forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
html += `
<div class="message" data-id="${msg.id}">
<div class="message-ip">🔑 ${msg.ip}</div>
<div class="message-content">
${this.escapeHtml(msg.content)}
<span class="timestamp">${time}</span>
</div>
<div class="message-actions">
<button onclick="chatApp.deleteMessage(${msg.id})" class="delete-btn">Delete</button>
</div>
</div>
`;
});
this.messageContainer.innerHTML = html;
this.scrollToBottom();
}
async deleteMessage(id) {
if (!confirm('Delete this message?')) return;
try {
const response = await fetch(`/api/messages/${id}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
this.showToast('Message deleted ✓', 'success');
this.loadMessages();
} else {
this.showToast('Failed to delete message', 'error');
}
} catch (error) {
this.showToast('Network error deleting message', 'error');
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
scrollToBottom() {
this.messageContainer.scrollTop = this.messageContainer.scrollHeight;
}
showToast(message, type = 'info') {
this.toast.textContent = message;
this.toast.className = 'toast visible ' + type;
if (this.toastTimeout) {
clearTimeout(this.toastTimeout);
}
this.toastTimeout = setTimeout(() => {
this.toast.classList.remove('visible');
}, 3000);
} }
} }
// Initialize the chat app function renderMessages(msgs) {
const chatApp = new ChatApp(); if (msgs.length === 0) {
messagesEl.innerHTML = '<div class="empty-state">💬 No messages yet. Be the first!</div>';
return;
}
let html = '';
msgs.forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
html += `<div class="message">
<div class="msg-ip">🔑 ${escapeHtml(msg.ip)}</div>
<div class="msg-content">${escapeHtml(msg.content)}</div>
<div class="msg-time">${time}</div>
</div>`;
});
messagesEl.innerHTML = html;
messagesEl.scrollTop = messagesEl.scrollHeight;
}
async function sendMessage() {
const text = inputEl.value.trim();
if (!text) return;
sendBtn.disabled = true;
try {
const r = await fetch('/api/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel: currentChannel, message: text })
});
const data = await r.json();
if (data.success) {
inputEl.value = '';
loadMessages();
} else {
showToast(data.error || 'Failed to send', 'error');
}
} catch (e) {
showToast('Network error', 'error');
}
sendBtn.disabled = false;
}
// Init
renderChannels();
loadMessages();
sendBtn.addEventListener('click', sendMessage);
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); });
// Auto-poll every 3 seconds
setInterval(() => loadMessages(), 3000);
</script> </script>
</body> </body>
</html> </html>

254
index.py
View File

@ -1,21 +1,43 @@
from bottle import route, request, response, run, static_file from bottle import route, request, response, run, static_file, template
import hashlib import hashlib
import time import time
import json import json
import os
import re
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64 import base64
import sqlalchemy import sqlalchemy
from sqlalchemy import create_engine, String, Integer, DateTime from sqlalchemy import create_engine, String, Integer, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from datetime import datetime
# Encryption setup # --- Security config from environment ---
SALT = b'salt_123456789' # In production, use a random salt SALT = os.environ.get('CHAT_SALT', 'default_salt_change_me').encode()
PASSWORD = b'chat_secret_key_123' # In production, use environment variable PASSWORD = os.environ.get('CHAT_SECRET', 'default_secret_change_me').encode()
ADMIN_TOKEN = os.environ.get('CHAT_ADMIN_TOKEN', None)
RATE_LIMIT = int(os.environ.get('CHAT_RATE_LIMIT', 5)) # messages per window
RATE_WINDOW = int(os.environ.get('CHAT_RATE_WINDOW', 10)) # seconds
MAX_MSG_LENGTH = int(os.environ.get('CHAT_MAX_MSG', 2000))
DB_PATH = os.environ.get('CHAT_DB_PATH', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chat.db'))
# --- Rate limiting (in-memory) ---
rate_store = {}
def check_rate_limit(ip):
now = time.time()
window_start = now - RATE_WINDOW
if ip in rate_store:
rate_store[ip] = [t for t in rate_store[ip] if t > window_start]
if len(rate_store[ip]) >= RATE_LIMIT:
return False
rate_store[ip].append(now)
else:
rate_store[ip] = [now]
return True
# --- Encryption ---
def get_cipher(): def get_cipher():
kdf = PBKDF2HMAC( kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(), algorithm=hashes.SHA256(),
@ -33,11 +55,12 @@ class Messages(Base):
__tablename__ = "messages" __tablename__ = "messages"
id: Mapped[int] = mapped_column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
ip: Mapped[str] = mapped_column(String, nullable=False) channel: Mapped[str] = mapped_column(String(32), nullable=False, default='general')
content: Mapped[str] = mapped_column(String, nullable=False) ip: Mapped[str] = mapped_column(String(64), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
timestamp: Mapped[int] = mapped_column(Integer, nullable=False) timestamp: Mapped[int] = mapped_column(Integer, nullable=False)
engine = create_engine("sqlite:///chat.db", echo=False) engine = create_engine(f"sqlite:///{DB_PATH}", echo=False)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
class DBfrontend(): class DBfrontend():
@ -45,136 +68,109 @@ class DBfrontend():
self.Session = sessionmaker(bind=engine) self.Session = sessionmaker(bind=engine)
self.cipher = get_cipher() self.cipher = get_cipher()
def encrypt_message(self, message: str) -> str: def _encrypt(self, message):
"""Encrypt message before storing"""
return self.cipher.encrypt(message.encode()).decode() return self.cipher.encrypt(message.encode()).decode()
def decrypt_message(self, encrypted_message: str) -> str: def _decrypt(self, encrypted):
"""Decrypt message after retrieval"""
try: try:
return self.cipher.decrypt(encrypted_message.encode()).decode() return self.cipher.decrypt(encrypted.encode()).decode()
except: except:
return "[Decryption failed]" return "[Decryption failed]"
def add_message(self, ip: str, content: str) -> int: def add_message(self, channel, ip, content):
"""Add a new encrypted message to the database"""
with self.Session() as session: with self.Session() as session:
encrypted_content = self.encrypt_message(content) msg = Messages(
new_message = Messages( channel=channel,
ip=ip, ip=ip,
content=encrypted_content, content=self._encrypt(content),
timestamp=int(time.time() * 1000) timestamp=int(time.time() * 1000)
) )
session.add(new_message) session.add(msg)
session.commit() session.commit()
return new_message.id return msg.id
def get_all_messages(self) -> list: def get_messages(self, channel='general', limit=50):
"""Get all messages and decrypt them"""
with self.Session() as session: with self.Session() as session:
messages = session.query(Messages).all() q = session.query(Messages).filter(Messages.channel == channel)
for msg in messages: q = q.order_by(Messages.id.desc()).limit(limit)
msg.content = self.decrypt_message(msg.content) msgs = q.all()
return messages result = []
for msg in reversed(msgs):
result.append({
'id': msg.id,
'channel': msg.channel,
'ip': msg.ip,
'content': self._decrypt(msg.content),
'timestamp': msg.timestamp
})
return result
def get_message_by_id(self, message_id: int) -> Messages | None: def delete_message(self, msg_id):
"""Get a specific message by its ID"""
with self.Session() as session: with self.Session() as session:
message = session.query(Messages).get(message_id) msg = session.query(Messages).get(msg_id)
if message: if msg:
message.content = self.decrypt_message(message.content) session.delete(msg)
return message
def get_messages_by_ip(self, ip: str) -> list:
"""Get all messages from a specific IP address"""
with self.Session() as session:
messages = session.query(Messages).filter(Messages.ip == ip).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
def update_message(self, message_id: int, new_content: str) -> bool:
"""Update the content of a message"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
message.content = self.encrypt_message(new_content)
session.commit() session.commit()
return True return True
return False return False
def delete_message(self, message_id: int) -> bool:
"""Delete a message by its ID"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
session.delete(message)
session.commit()
return True
return False
def delete_messages_by_ip(self, ip: str) -> int:
"""Delete all messages from a specific IP address"""
with self.Session() as session:
messages = session.query(Messages).filter(Messages.ip == ip).all()
count = len(messages)
for message in messages:
session.delete(message)
session.commit()
return count
def get_message_count(self) -> int:
"""Get total number of messages"""
with self.Session() as session:
return session.query(Messages).count()
def get_latest_messages(self, limit: int = 10) -> list:
"""Get the latest N messages"""
with self.Session() as session:
messages = session.query(Messages).order_by(Messages.id.desc()).limit(limit).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
# Initialize database
db = DBfrontend() db = DBfrontend()
# Routes VALID_CHANNELS = {'general', 'games', 'anime', 'politics'}
import os CHANNEL_NAMES = {
'general': 'General',
'games': 'Games',
'anime': 'Anime',
'politics': 'Politics'
}
@route('/') @route('/')
def serve_frontend(): def serve_frontend():
# Получаем путь к директории, где находится index.py return static_file('index.html', root=os.path.dirname(os.path.abspath(__file__)))
current_dir = os.path.dirname(os.path.abspath(__file__))
return static_file('index.html', root=current_dir) @route('/api/channels', method='GET')
def list_channels():
response.content_type = 'application/json'
channels = [{'id': k, 'name': v} for k, v in CHANNEL_NAMES.items()]
return {'success': True, 'channels': channels}
@route('/api/messages', method='POST') @route('/api/messages', method='POST')
def post_message(): def post_message():
ip = request.environ.get('REMOTE_ADDR', '127.0.0.1') ip = request.environ.get('REMOTE_ADDR', '127.0.0.1')
if not check_rate_limit(ip):
response.status = 429
return {'success': False, 'error': 'Rate limit exceeded. Slow down.'}
try: try:
data = request.json data = request.json
if not data or 'message' not in data: if not data:
response.status = 400 response.status = 400
return {'success': False, 'error': 'No message provided'} return {'success': False, 'error': 'No data provided'}
message = data.get('message', '').strip() channel = data.get('channel', 'general')
if not message: content = data.get('message', '').strip()
if channel not in VALID_CHANNELS:
response.status = 400
return {'success': False, 'error': 'Invalid channel'}
if not content:
response.status = 400 response.status = 400
return {'success': False, 'error': 'Empty message'} return {'success': False, 'error': 'Empty message'}
# Get IP hash for privacy if len(content) > MAX_MSG_LENGTH:
ip_hash = hashlib.sha256(('salt' + ip).encode('utf-8')).hexdigest()[:16] response.status = 400
return {'success': False, 'error': f'Message too long (max {MAX_MSG_LENGTH} chars)'}
# Save message with encrypted content # XSS filter - strip HTML tags
message_id = db.add_message(ip_hash, message) content = re.sub(r'<[^>]*>', '', content)
ip_hash = hashlib.sha256((os.environ.get('CHAT_SALT', 'salt') + ip).encode()).hexdigest()[:16]
msg_id = db.add_message(channel, ip_hash, content)
response.content_type = 'application/json' response.content_type = 'application/json'
return { return {'success': True, 'message_id': msg_id, 'ip': ip_hash}
'success': True,
'message_id': message_id,
'ip': ip_hash
}
except Exception as e: except Exception as e:
response.status = 500 response.status = 500
@ -183,42 +179,15 @@ def post_message():
@route('/api/messages', method='GET') @route('/api/messages', method='GET')
def get_messages(): def get_messages():
try: try:
limit = int(request.query.get('limit', 20)) channel = request.query.get('channel', 'general')
messages = db.get_latest_messages(limit) limit = min(int(request.query.get('limit', 50)), 200)
result = [] if channel not in VALID_CHANNELS:
for msg in messages: channel = 'general'
result.append({
'id': msg.id,
'ip': msg.ip,
'content': msg.content,
'timestamp': msg.timestamp
})
messages = db.get_messages(channel=channel, limit=limit)
response.content_type = 'application/json' response.content_type = 'application/json'
return {'success': True, 'messages': result} return {'success': True, 'messages': messages}
except Exception as e:
response.status = 500
return {'success': False, 'error': str(e)}
@route('/api/messages/<message_id>', method='PUT')
def update_message(message_id):
try:
data = request.json
if not data or 'message' not in data:
response.status = 400
return {'success': False, 'error': 'No message provided'}
new_content = data.get('message', '').strip()
if not new_content:
response.status = 400
return {'success': False, 'error': 'Empty message'}
success = db.update_message(int(message_id), new_content)
response.content_type = 'application/json'
return {'success': success}
except Exception as e: except Exception as e:
response.status = 500 response.status = 500
@ -226,15 +195,28 @@ def update_message(message_id):
@route('/api/messages/<message_id>', method='DELETE') @route('/api/messages/<message_id>', method='DELETE')
def delete_message(message_id): def delete_message(message_id):
# Admin token required for deletion
auth = request.get_header('Authorization', '')
token = auth.replace('Bearer ', '')
if ADMIN_TOKEN and token != ADMIN_TOKEN:
response.status = 403
return {'success': False, 'error': 'Forbidden'}
try: try:
success = db.delete_message(int(message_id)) success = db.delete_message(int(message_id))
response.content_type = 'application/json' response.content_type = 'application/json'
return {'success': success} return {'success': success}
except Exception as e: except Exception as e:
response.status = 500 response.status = 500
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
if __name__ == '__main__': if __name__ == '__main__':
run(host='localhost', port=8080, debug=True) host = os.environ.get('CHAT_HOST', 'localhost')
port = int(os.environ.get('CHAT_PORT', 8080))
debug_mode = os.environ.get('CHAT_DEBUG', '0') == '1'
if debug_mode:
print(f"⚠️ WARNING: Debug mode is ON - do not use in production!")
run(host=host, port=port, debug=debug_mode)

View File

@ -1,2 +0,0 @@
[tool.pyright]
python.pythonPath = "~/myenv/bin/python"