Skip to content

Commit cd37d65

Browse files
committed
I Quit OWASP. DDos This SHIT
1 parent 9d370f3 commit cd37d65

1 file changed

Lines changed: 20 additions & 363 deletions

File tree

servidor.py

Lines changed: 20 additions & 363 deletions
Original file line numberDiff line numberDiff line change
@@ -19,366 +19,23 @@
1919
along with this program. If not, see <https://www.gnu.org/licenses/>.
2020
2121
'''
22-
# 👇 esto siempre va primero
23-
import eventlet
24-
eventlet.monkey_patch()
25-
26-
import os
27-
import json
28-
from datetime import timedelta
29-
from urllib.parse import urlparse
30-
from werkzeug.middleware.proxy_fix import ProxyFix
31-
from werkzeug.utils import secure_filename
32-
import logging
33-
from logging.handlers import RotatingFileHandler
34-
import bleach
35-
36-
from flask import Flask, request, redirect, url_for, send_from_directory, render_template
37-
from flask_socketio import SocketIO, join_room, leave_room, send
38-
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
39-
from flask_argon2 import Argon2
40-
from argon2 import PasswordHasher
41-
from flask_talisman import Talisman
42-
from flask_limiter import Limiter
43-
from flask_limiter.util import get_remote_address
44-
from flask_wtf import CSRFProtect
45-
46-
# ---------------------------
47-
# Utiles para sanitizar / orígenes
48-
# ---------------------------
49-
def normalize_origin(o: str):
50-
if not o:
51-
return None
52-
o = o.strip()
53-
if '://' not in o:
54-
o = 'https://' + o
55-
parsed = urlparse(o)
56-
netloc = parsed.netloc or parsed.path
57-
scheme = parsed.scheme or 'https'
58-
return f"{scheme}://{netloc}"
59-
60-
# ---------------------------
61-
# CONFIG INICIAL
62-
# ---------------------------
63-
app = Flask(__name__)
64-
65-
# Configuración de ProxyFix, crucial para render y otros servicios de proxy
66-
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
67-
68-
app.secret_key = os.environ.get("SECRET_KEY", "dev_secret")
69-
70-
app.config.update(
71-
SESSION_COOKIE_SECURE=True,
72-
SESSION_COOKIE_HTTPONLY=True,
73-
# El valor 'None' es necesario para que las cookies de sesión se envíen
74-
# con los handshakes de websocket, si el cliente y el servidor están
75-
# en dominios diferentes. 'Lax' puede causar problemas en este escenario.
76-
SESSION_COOKIE_SAMESITE=None,
77-
REMEMBER_COOKIE_HTTPONLY=True,
78-
PERMANENT_SESSION_LIFETIME=timedelta(hours=8),
79-
MAX_CONTENT_LENGTH=25 * 1024 * 1024,
80-
)
81-
82-
# ---------------------------
83-
# ARCHIVOS / UPLOAD
84-
# ---------------------------
85-
UPLOAD_FOLDER = './cuarentena'
86-
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
87-
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
88-
ALLOWED_EXTENSIONS = {'.txt', '.pdf', '.png', '.jpg', '.jpeg', '.gif'}
89-
90-
def allowed_file(filename: str) -> bool:
91-
_, ext = os.path.splitext(filename.lower())
92-
return ext in ALLOWED_EXTENSIONS
93-
94-
# ---------------------------
95-
# AUTH / USERS (demo)
96-
# ---------------------------
97-
argon2 = Argon2(app)
98-
ph = PasswordHasher()
99-
100-
users = {
101-
os.getenv("ADMIN_USER", "admin"): {
102-
"password": ph.hash(os.getenv("ADMIN_PASS", "admin123")),
103-
"role": "administrator"
104-
},
105-
os.getenv("CLIENT_USER", "cliente"): {
106-
"password": ph.hash(os.getenv("CLIENT_PASS", "cliente123")),
107-
"role": "cliente"
108-
},
109-
os.getenv("USR_USER", "usuario"): {
110-
"password": ph.hash(os.getenv("USR_PASS", "usuario123")),
111-
"role": "usuario"
112-
}
113-
}
114-
115-
try:
116-
demo_users_env = os.getenv("DEMO_USERS", "[]")
117-
demo_users = json.loads(demo_users_env)
118-
for u in demo_users:
119-
users[u["username"]] = {
120-
"password": ph.hash(u["password"]),
121-
"role": u.get("role", "usuario")
122-
}
123-
except Exception as e:
124-
print(f"[WARN] No se pudieron cargar demo_users: {e}")
125-
126-
login_manager = LoginManager(app)
127-
login_manager.login_view = 'login'
128-
129-
class Usuario(UserMixin):
130-
def __init__(self, username, role):
131-
self.id = username
132-
self.rol = role
133-
134-
@login_manager.user_loader
135-
def load_user(user_id):
136-
if user_id in users:
137-
return Usuario(user_id, users[user_id]['role'])
138-
return None
139-
140-
# ---------------------------
141-
# CSRF, TALISMAN, RATE LIMITER
142-
# ---------------------------
143-
csrf = CSRFProtect(app)
144-
limiter = Limiter(
145-
get_remote_address,
146-
app=app,
147-
default_limits=["200 per day", "50 per hour"]
148-
)
149-
150-
_default_origins = "https://pichat-k0bi.onrender.com,http://localhost:10000"
151-
origins_env = os.environ.get("ALLOWED_ORIGINS", _default_origins)
152-
ALLOWED_ORIGINS = []
153-
for part in origins_env.split(","):
154-
n = normalize_origin(part)
155-
if n:
156-
ALLOWED_ORIGINS.append(n)
157-
158-
# Volvemos a 'manage_session=False' para evitar conflictos con Flask-Login.
159-
# La combinación de Flask-Login y Socket.IO es compleja, y esta configuración
160-
# es a menudo necesaria para que la sesión HTTP esté disponible en los
161-
# eventos de Socket.IO.
162-
socketio = SocketIO(app, cors_allowed_origins=ALLOWED_ORIGINS, manage_session=False)
163-
164-
# Intentamos eximir SocketIO del CSRF (lo ideal es eximir la ruta /socket.io)
165-
try:
166-
csrf.exempt(socketio)
167-
except Exception:
168-
pass
169-
170-
wss_origins = []
171-
for o in ALLOWED_ORIGINS:
172-
if o.startswith("https://"):
173-
wss_origins.append(o.replace("https://", "wss://"))
174-
elif o.startswith("http://"):
175-
wss_origins.append(o.replace("http://", "ws://"))
176-
else:
177-
wss_origins.append(o)
178-
179-
connect_src_value = "'self'"
180-
if wss_origins:
181-
connect_src_value += " " + " ".join(wss_origins)
182-
183-
csp = {
184-
'default-src': "'self'",
185-
'img-src': "'self' data:",
186-
'style-src': "'self' 'unsafe-inline'",
187-
'script-src': "'self'",
188-
'connect-src': connect_src_value,
189-
}
190-
191-
Talisman(
192-
app,
193-
content_security_policy=csp,
194-
force_https=True,
195-
strict_transport_security=True,
196-
strict_transport_security_max_age=31536000,
197-
frame_options="DENY",
198-
referrer_policy="no-referrer",
199-
session_cookie_secure=True,
200-
content_security_policy_nonce_in=['script-src'],
201-
)
202-
203-
# ---------------------------
204-
# RUTAS / ENDPOINTS HTTP
205-
# ---------------------------
206-
@app.route('/')
207-
def home():
208-
if current_user.is_authenticated:
209-
return redirect(url_for('inicio'))
210-
return redirect(url_for('login'))
211-
212-
@app.route('/login', methods=['GET', 'POST'])
213-
@limiter.limit("5/minute; 20/hour")
214-
def login():
215-
if current_user.is_authenticated:
216-
return redirect(url_for('inicio'))
217-
if request.method == 'POST':
218-
user = (request.form.get('usuario') or "")[:64]
219-
password = (request.form.get('clave') or "")[:256]
220-
if user in users:
221-
try:
222-
ph.verify(users[user]['password'], password)
223-
login_user(Usuario(user, users[user]['role']))
224-
return redirect(url_for('inicio'))
225-
except Exception:
226-
pass
227-
return render_template("login.html", error="Credenciales inválidas.")
228-
return render_template("login.html")
229-
230-
@app.route('/logout')
231-
@login_required
232-
def logout():
233-
logout_user()
234-
return redirect(url_for('login'))
235-
236-
@app.route('/inicio')
237-
@login_required
238-
def inicio():
239-
return render_template('inicio.html', current_user=current_user)
240-
241-
@app.route('/subir', methods=['GET', 'POST'])
242-
@login_required
243-
@limiter.limit("20/hour")
244-
def subir():
245-
if current_user.rol == 'usuario':
246-
return 'No tienes permiso para subir archivos', 403
247-
if request.method == 'POST':
248-
f = request.files.get('archivo')
249-
if not f or f.filename == '':
250-
return 'No se seleccionó archivo', 400
251-
filename = secure_filename(f.filename)
252-
if not allowed_file(filename):
253-
return 'Tipo de archivo no permitido', 400
254-
f.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
255-
return redirect(url_for('listar'))
256-
return render_template("subir.html")
257-
258-
@app.route('/listar')
259-
@login_required
260-
def listar():
261-
archivos = os.listdir(UPLOAD_FOLDER)
262-
return render_template("listar.html", archivos=archivos)
263-
264-
@app.route('/descargar/<nombre>')
265-
@login_required
266-
def descargar(nombre):
267-
return send_from_directory(UPLOAD_FOLDER, nombre, as_attachment=True)
268-
269-
@app.route('/eliminar/<nombre>', methods=['POST'])
270-
@login_required
271-
def eliminar(nombre):
272-
if current_user.rol != 'administrator':
273-
return 'No tienes permiso para eliminar archivos', 403
274-
safe = secure_filename(nombre)
275-
target = os.path.join(UPLOAD_FOLDER, safe)
276-
if os.path.commonpath([os.path.abspath(target), os.path.abspath(UPLOAD_FOLDER)]) != os.path.abspath(UPLOAD_FOLDER):
277-
return "Ruta inválida", 400
278-
try:
279-
os.remove(target)
280-
except FileNotFoundError:
281-
pass
282-
return redirect(url_for('listar'))
283-
284-
@app.route('/chat')
285-
@login_required
286-
def chat():
287-
return render_template('chat.html', current_user=current_user)
288-
289-
# ---------------------------
290-
# SOCKET.IO EVENTS
291-
# ---------------------------
292-
chat_rooms = {}
293-
294-
@socketio.on('join')
295-
@limiter.limit("1/minute")
296-
def on_join(data):
297-
# Validar que el usuario esté autenticado. 'current_user' proviene de la cookie
298-
# de sesión, que debe enviarse con el handshake de websocket.
299-
username = getattr(current_user, "id", None)
300-
if not username:
301-
send({'msg': 'Usuario no autenticado.', 'type': 'error'})
302-
return
303-
304-
# Validar que los datos existan.
305-
room_code = (data.get('room') or "")[:64]
306-
password = (data.get('password') or "")[:128]
307-
is_group = bool(data.get('is_group', False))
308-
309-
if not room_code or not password:
310-
send({'msg': 'Parámetros inválidos.', 'type': 'error'})
311-
return
312-
313-
if room_code not in chat_rooms:
314-
chat_rooms[room_code] = argon2.generate_password_hash(password)
315-
else:
316-
if not argon2.check_password_hash(chat_rooms[room_code], password):
317-
send({'msg': 'Acceso denegado.', 'type': 'error'})
318-
return
319-
320-
join_room(room_code)
321-
send({'msg': f"👋 {username} se ha unido.", 'user': 'Servidor', 'is_group': is_group}, to=room_code)
322-
323-
@socketio.on('leave')
324-
def on_leave(data):
325-
username = getattr(current_user, "id", None) or "Anon"
326-
room_code = data.get('room')
327-
if room_code:
328-
leave_room(room_code)
329-
send({'msg': f"🚪 {username} ha salido.", 'user': 'Servidor'}, to=room_code)
330-
331-
@socketio.on('message')
332-
@limiter.limit("1/minute")
333-
def handle_message(data):
334-
username = getattr(current_user, "id", None) or "Anon"
335-
room = (data.get('room') or "")[:64]
336-
raw_msg = data.get('msg')
337-
msg = clean_text(raw_msg)
338-
is_group = bool(data.get('is_group', False))
339-
340-
send({'msg': msg, 'user': username, 'is_group': is_group}, to=room)
341-
342-
# ---------------------------
343-
# LOGGING & ERRORES
344-
# ---------------------------
345-
if not app.debug:
346-
handler = RotatingFileHandler('app.log', maxBytes=5_000_000, backupCount=3)
347-
handler.setLevel(logging.INFO)
348-
app.logger.addHandler(handler)
349-
350-
@app.errorhandler(400)
351-
def bad_request(e): return "Solicitud inválida", 400
352-
353-
@app.errorhandler(403)
354-
def forbidden(e): return "Prohibido", 403
355-
356-
@app.errorhandler(404)
357-
def not_found(e): return "No encontrado", 404
358-
359-
@app.errorhandler(500)
360-
def server_error(e):
361-
app.logger.exception("Error 500")
362-
return "Error del servidor", 500
363-
364-
@login_manager.unauthorized_handler
365-
def unauthorized():
366-
return redirect(url_for('login'))
367-
368-
def clean_text(s: str) -> str:
369-
s = (s or "")[:2000]
370-
ALLOWED_TAGS = []
371-
ALLOWED_ATTRS = {}
372-
ALLOWED_PROTOCOLS = ['http', 'https']
373-
return bleach.clean(s, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, protocols=ALLOWED_PROTOCOLS, strip=True)
374-
375-
# ---------------------------
376-
# ENTRYPOINTS
377-
# ---------------------------
378-
if __name__ == '__main__':
379-
port = int(os.environ.get("PORT", 10000))
380-
socketio.run(app, host='0.0.0.0', port=port)
381-
382-
# para Gunicorn / Render / Railway
383-
application = app
384-
socketio_app = socketio
22+
from flask import Flask, request, jsonify, redirect, url_for, send_from_directory, render_template from flask_socketio import SocketIO, join_room, leave_room, send from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user from werkzeug.utils import secure_filename from flask_argon2 import Argon2 import os, json from argon2 import PasswordHasher
23+
# --- CONFIGURACIÓN INICIAL ---
24+
app = Flask(__name__) socketio = SocketIO(app, cors_allowed_origins="*")
25+
# SocketIO envuelve a Flask
26+
app.secret_key = os.environ.get("SECRET_KEY", "a-very-secret-key-for-dev") argon2 = Argon2(app) ph = PasswordHasher()
27+
# --- CONFIGURACIÓN DE CARPETAS ---
28+
UPLOAD_FOLDER = './cuarentena' os.makedirs(UPLOAD_FOLDER, exist_ok=True) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # --- USUARIOS BASE ---
29+
users = { os.getenv("ADMIN_USER", "admin"): { "password": ph.hash(os.getenv("ADMIN_PASS", "admin123")), "role": "administrator" }, os.getenv("CLIENT_USER", "cliente"): { "password": ph.hash(os.getenv("CLIENT_PASS", "cliente123")), "role": "cliente" }, os.getenv("USR_USER", "usuario"): { "password": ph.hash(os.getenv("USR_PASS", "usuario123")), "role": "usuario" } } # --- DEMO USERS DESDE ENV ---
30+
try: demo_users_env = os.getenv("DEMO_USERS", "[]") demo_users = json.loads(demo_users_env) for u in demo_users: users[u["username"]] = { "password": ph.hash(u["password"]), "role": u.get("role", "usuario") } except Exception as e: print(f"[WARN] No se pudieron cargar demo_users: {e}")
31+
# --- LOGIN MANAGER ---
32+
login_manager = LoginManager(app) login_manager.login_view = 'login' class Usuario(UserMixin): def __init__(self, username, role): self.id = username self.rol = role @login_manager.user_loader def load_user(user_id): if user_id in users: return Usuario(user_id, users[user_id]['role']) return None
33+
# --- RUTAS ---
34+
@app.route('/') def home(): if current_user.is_authenticated: return redirect(url_for('inicio')) return redirect(url_for('login')) @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('inicio')) if request.method == 'POST': user = request.form['usuario'] password = request.form['clave'] if user in users: try: ph.verify(users[user]['password'], password) login_user(Usuario(user, users[user]['role'])) return redirect(url_for('inicio')) except Exception: pass return render_template("login.html", error="Credenciales inválidas.") return render_template("login.html") @app.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('login')) @app.route('/inicio') @login_required def inicio(): return render_template('inicio.html', current_user=current_user)
35+
# --- FUNCIONALIDAD DE ARCHIVOS ---
36+
@app.route('/subir', methods=['GET', 'POST']) @login_required def subir(): if current_user.rol == 'usuario': return 'No tienes permiso para subir archivos', 403 if request.method == 'POST': if 'archivo' not in request.files: return 'No se encontró el archivo', 400 archivo = request.files['archivo'] if archivo.filename == '': return 'No se seleccionó ningún archivo', 400 filename = secure_filename(archivo.filename) archivo.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) return redirect(url_for('listar')) return render_template("subir.html") @app.route('/listar') @login_required def listar(): archivos = os.listdir(UPLOAD_FOLDER) return render_template("listar.html", archivos=archivos) @app.route('/descargar/<nombre>') @login_required def descargar(nombre): return send_from_directory(UPLOAD_FOLDER, nombre, as_attachment=True) @app.route('/eliminar/<nombre>') @login_required def eliminar(nombre): if current_user.rol != 'administrator': return 'No tienes permiso para eliminar archivos', 403 try: os.remove(os.path.join(UPLOAD_FOLDER, secure_filename(nombre))) except FileNotFoundError: pass return redirect(url_for('listar')) @app.route('/chat') @login_required def chat(): return render_template('chat.html', current_user=current_user)
37+
# --- SOCKET.IO ---
38+
chat_rooms = {} @socketio.on('join') def on_join(data): username = current_user.id room_code = data['room'] password = data['password'] is_group = data.get('is_group', False) if room_code not in chat_rooms: chat_rooms[room_code] = argon2.generate_password_hash(password) else: if not argon2.check_password_hash(chat_rooms[room_code], password): send({'msg': 'Contraseña incorrecta.', 'type': 'error'}) return join_room(room_code) send({'msg': f"👋 {username} se ha unido.", 'user': 'Servidor', 'is_group': is_group}, to=room_code) @socketio.on('leave') def on_leave(data): username = current_user.id room_code = data['room'] leave_room(room_code) send({'msg': f"🚪 {username} ha salido.", 'user': 'Servidor'}, to=room_code) @socketio.on('message') def handle_message(data): username = current_user.id room = data['room'] msg = data['msg'] is_group = data.get('is_group', False) send({'msg': msg, 'user': username, 'is_group': is_group}, to=room)
39+
# --- INICIO ---
40+
if __name__ == '__main__': port = int(os.environ.get("PORT", 10000)) socketio.run(app, host='0.0.0.0', port=port) # 👉 Para gunicorn/render
41+
application = app

0 commit comments

Comments
 (0)