From b8daea66f2bc841a4b4d1e42b7a98f90a4c2c69d Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 22 Jul 2026 02:15:25 +0300 Subject: [PATCH] Cleanup: channels support, security hardening, remove cruft --- Anime/README.md | 3 + Games/README.md | 3 + Politics/README.md | 3 + README.md | 53 +++- chat.db | Bin 8192 -> 0 bytes dbworker.py | 191 --------------- index.html | 590 ++++++++++++++++----------------------------- index.py | 256 +++++++++----------- pyproject.toml | 2 - 9 files changed, 394 insertions(+), 707 deletions(-) create mode 100644 Anime/README.md create mode 100644 Games/README.md create mode 100644 Politics/README.md delete mode 100644 chat.db delete mode 100644 dbworker.py delete mode 100644 pyproject.toml diff --git a/Anime/README.md b/Anime/README.md new file mode 100644 index 0000000..f0abc44 --- /dev/null +++ b/Anime/README.md @@ -0,0 +1,3 @@ +# Anime Channel + +This directory is dedicated to discussions and content about anime. \ No newline at end of file diff --git a/Games/README.md b/Games/README.md new file mode 100644 index 0000000..6143c99 --- /dev/null +++ b/Games/README.md @@ -0,0 +1,3 @@ +# Games Channel + +This directory is dedicated to discussions and content about games. \ No newline at end of file diff --git a/Politics/README.md b/Politics/README.md new file mode 100644 index 0000000..2ea54bd --- /dev/null +++ b/Politics/README.md @@ -0,0 +1,3 @@ +# Politics Channel + +This directory is dedicated to discussions and content about politics. \ No newline at end of file diff --git a/README.md b/README.md index 038ae64..1585101 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/chat.db b/chat.db deleted file mode 100644 index 0d5345da5a2e33f6af38efd7ccdeecec0e9e98ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeI#Pfx-y6aes+_$QJWym=z&5fU*Q6M+MX0|p{vZetAaVm8?3l#M?d3=bSIeg)5d z9zT?cL*ZaT;#HFO()P8lO_Tm^ZA!fD8^jz=9L*;TIzbqtD?$)Lk#!H#ci?c`ThCW% zwf-w2==|;DI6Q@84HPyA5C8!X009sH0T2KI5C8!X009u#eSsx~4^QK9{KEK}J}?}^ z^EAuwHaSv~Iac9_!WMasY^o#?J+k|xCMaB$lZkMv5TV8MXCy3-h|0>PYxZ}!H*|f& z^|$W%c6hI^Iisx?igK;N%3V_Dx+G!ulhNeQ-+X~$i{0mk7k~f=fB*=900@8p2!H?x zfB*=9z`qn&B7A^S%ho%H`)mZdV9#jc__?$8fTkX9$6$BpUsU>YzN)||Pk z`_oRZTyMHh)y_!BicHPC7b-J)9#Eo@nRAjJNEMdu8oe7!EnAF`%eB?2NzGh0-GKN*^JWhK diff --git a/dbworker.py b/dbworker.py deleted file mode 100644 index 9e77111..0000000 --- a/dbworker.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/index.html b/index.html index 54b90af..1143a96 100644 --- a/index.html +++ b/index.html @@ -3,459 +3,297 @@ - Secure Chat + FuckingChat
-

๐Ÿ”’ Secure Chat

- โ— Encrypted +

๐Ÿ’ฌ FuckingChat

+ Encrypted ๐Ÿ”’
- -
-
Loading messages...
-
- -
- - +
+
+
+ +
-
- - \ No newline at end of file + diff --git a/index.py b/index.py index 4de45b4..0fe3068 100644 --- a/index.py +++ b/index.py @@ -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 time import json +import os +import re from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC import base64 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 sessionmaker -from datetime import datetime -# Encryption setup -SALT = b'salt_123456789' # In production, use a random salt -PASSWORD = b'chat_secret_key_123' # In production, use environment variable +# --- Security config from environment --- +SALT = os.environ.get('CHAT_SALT', 'default_salt_change_me').encode() +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(): kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), @@ -33,11 +55,12 @@ class Messages(Base): __tablename__ = "messages" id: Mapped[int] = mapped_column(Integer, primary_key=True) - ip: Mapped[str] = mapped_column(String, nullable=False) - content: Mapped[str] = mapped_column(String, nullable=False) + channel: Mapped[str] = mapped_column(String(32), nullable=False, default='general') + 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) -engine = create_engine("sqlite:///chat.db", echo=False) +engine = create_engine(f"sqlite:///{DB_PATH}", echo=False) Base.metadata.create_all(engine) class DBfrontend(): @@ -45,136 +68,109 @@ class DBfrontend(): self.Session = sessionmaker(bind=engine) self.cipher = get_cipher() - def encrypt_message(self, message: str) -> str: - """Encrypt message before storing""" + def _encrypt(self, message): return self.cipher.encrypt(message.encode()).decode() - def decrypt_message(self, encrypted_message: str) -> str: - """Decrypt message after retrieval""" + def _decrypt(self, encrypted): try: - return self.cipher.decrypt(encrypted_message.encode()).decode() + return self.cipher.decrypt(encrypted.encode()).decode() except: return "[Decryption failed]" - def add_message(self, ip: str, content: str) -> int: - """Add a new encrypted message to the database""" + def add_message(self, channel, ip, content): with self.Session() as session: - encrypted_content = self.encrypt_message(content) - new_message = Messages( - ip=ip, - content=encrypted_content, + msg = Messages( + channel=channel, + ip=ip, + content=self._encrypt(content), timestamp=int(time.time() * 1000) ) - session.add(new_message) + session.add(msg) session.commit() - return new_message.id + return msg.id - def get_all_messages(self) -> list: - """Get all messages and decrypt them""" + def get_messages(self, channel='general', limit=50): with self.Session() as session: - messages = session.query(Messages).all() - for msg in messages: - msg.content = self.decrypt_message(msg.content) - return messages + q = session.query(Messages).filter(Messages.channel == channel) + q = q.order_by(Messages.id.desc()).limit(limit) + msgs = q.all() + 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: - """Get a specific message by its ID""" + def delete_message(self, msg_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) + msg = session.query(Messages).get(msg_id) + if msg: + session.delete(msg) 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""" - 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() -# Routes -import os +VALID_CHANNELS = {'general', 'games', 'anime', 'politics'} +CHANNEL_NAMES = { + 'general': 'General', + 'games': 'Games', + 'anime': 'Anime', + 'politics': 'Politics' +} @route('/') def serve_frontend(): - # ะŸะพะปัƒั‡ะฐะตะผ ะฟัƒั‚ัŒ ะบ ะดะธั€ะตะบั‚ะพั€ะธะธ, ะณะดะต ะฝะฐั…ะพะดะธั‚ัั index.py - current_dir = os.path.dirname(os.path.abspath(__file__)) - return static_file('index.html', root=current_dir) + return static_file('index.html', root=os.path.dirname(os.path.abspath(__file__))) + +@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') def post_message(): 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: data = request.json - if not data or 'message' not in data: + if not data: response.status = 400 - return {'success': False, 'error': 'No message provided'} + return {'success': False, 'error': 'No data provided'} - message = data.get('message', '').strip() - if not message: + channel = data.get('channel', 'general') + 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 return {'success': False, 'error': 'Empty message'} - # Get IP hash for privacy - ip_hash = hashlib.sha256(('salt' + ip).encode('utf-8')).hexdigest()[:16] + if len(content) > MAX_MSG_LENGTH: + response.status = 400 + return {'success': False, 'error': f'Message too long (max {MAX_MSG_LENGTH} chars)'} - # Save message with encrypted content - message_id = db.add_message(ip_hash, message) + # XSS filter - strip HTML tags + 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' - return { - 'success': True, - 'message_id': message_id, - 'ip': ip_hash - } + return {'success': True, 'message_id': msg_id, 'ip': ip_hash} except Exception as e: response.status = 500 @@ -183,42 +179,15 @@ def post_message(): @route('/api/messages', method='GET') def get_messages(): try: - limit = int(request.query.get('limit', 20)) - messages = db.get_latest_messages(limit) + channel = request.query.get('channel', 'general') + limit = min(int(request.query.get('limit', 50)), 200) - result = [] - for msg in messages: - result.append({ - 'id': msg.id, - 'ip': msg.ip, - 'content': msg.content, - 'timestamp': msg.timestamp - }) + if channel not in VALID_CHANNELS: + channel = 'general' + messages = db.get_messages(channel=channel, limit=limit) response.content_type = 'application/json' - return {'success': True, 'messages': result} - - except Exception as e: - response.status = 500 - return {'success': False, 'error': str(e)} - -@route('/api/messages/', 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} + return {'success': True, 'messages': messages} except Exception as e: response.status = 500 @@ -226,15 +195,28 @@ def update_message(message_id): @route('/api/messages/', method='DELETE') 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: success = db.delete_message(int(message_id)) - response.content_type = 'application/json' return {'success': success} - except Exception as e: response.status = 500 return {'success': False, 'error': str(e)} if __name__ == '__main__': - run(host='localhost', port=8080, debug=True) \ No newline at end of file + 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) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 3351538..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,2 +0,0 @@ -[tool.pyright] -python.pythonPath = "~/myenv/bin/python" \ No newline at end of file