Files
ALPHA-TRAIN2/services/rusgram/logic/logic.py
T
2026-08-26 12:53:27 +03:00

191 lines
7.5 KiB
Python
Executable File

#!/usr/bin/python3
from flask import Flask, make_response, request, redirect
import os
import requests as r
app = Flask(__name__)
database_node_port = 5002
logic_node_port = 5003
database_node = "database:" + str(database_node_port)
IMG_MIN, IMG_MAX = 1, 16
INTERNAL = os.environ.get("RUSGRAM_INTERNAL", "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9")
DBH = {"X-Internal": INTERNAL}
def bad_request(status="error", code=400):
return make_response({"status": status}, code)
def unauthorized():
"""PATCH V3/V6: раньше здесь был redirect(request.headers.get("Origin","") + "/login.html").
Origin контролирует атакующий, а proxy ходит по Location серверсайд =>
SSRF во внутреннюю сеть. Location теперь фиксированный и относительный."""
return redirect("/login.html", code=302)
def valid_username(username):
"""PATCH V1/V8 (defense in depth): запрещаем только то, что реально опасно.
Остальное (юникод, точки внутри имени) оставляем — чтобы не сломать чекер."""
if not username or len(username) > 64:
return False
if "||" in username: # ломает формат куки session=<login>||<password>
return False
if "/" in username or "\\" in username or username in (".", ".."):
return False # имя юзера = имя файла в db/users/
if username.lower().endswith((".html", ".htm")):
return False # имя, которое content мог бы отрендерить как шаблон
if "{{" in username or "{%" in username or "{#" in username:
return False
return True
def clean_value(value):
"""Не даём записать Jinja-конструкции в поля, которые когда-то рендерились как шаблон."""
return isinstance(value, str) and "{{" not in value and "{%" not in value and "{#" not in value
def current_user():
"""Возвращает имя авторизованного юзера или None. Не бросает исключений."""
session = request.cookies.get("session")
if session is None: # PATCH V8: раньше KeyError -> 500
return None
parts = session.split("||")
if len(parts) != 2: # PATCH V8: раньше ValueError -> 500
return None
username, password = parts
if not username:
return None
try:
resp = r.get(f"http://{database_node}/users/{username}/password",
headers=DBH, timeout=5)
if resp.status_code != 200: # PATCH V8: несуществующий юзер -> KeyError -> 500
return None
if resp.json().get("data") != password:
return None
except (r.RequestException, ValueError):
return None
return username
def json_body():
data = request.get_json(silent=True)
return data if isinstance(data, dict) else None
@app.route('/login', methods=["POST"])
def login():
data = json_body()
if data is None or "login" not in data or "password" not in data:
return bad_request()
username, password = data["login"], data["password"]
if not isinstance(username, str) or not isinstance(password, str):
return bad_request()
try:
resp = r.get(f"http://{database_node}/users/{username}/password",
headers=DBH, timeout=5)
correct_password = resp.json()["data"] if resp.status_code == 200 else None
except (r.RequestException, ValueError, KeyError):
return bad_request()
if correct_password is not None and correct_password == password:
out = make_response({"status": "ok"}, 200)
out.set_cookie("session", f"{username}||{password}", httponly=True, samesite="Lax")
return out
return make_response({"status": "wrong_creds"}, 401)
@app.route('/register', methods=["POST"])
def register():
data = json_body()
if data is None or "login" not in data or "password" not in data:
return bad_request()
username, password = data["login"], data["password"]
if not isinstance(username, str) or not isinstance(password, str):
return bad_request()
if not valid_username(username):
return make_response({"status": "bad_login"}, 400)
# PATCH V2: раньше регистрация существующего юзера просто ПЕРЕЗАПИСЫВАЛА
# его пароль (остальные поля, включая флаг, сохранялись) => полный захват
# чужого аккаунта без единого запроса на аутентификацию.
try:
exists = r.get(f"http://{database_node}/users/{username}/password",
headers=DBH, timeout=5)
if exists.status_code == 200:
return make_response({"status": "user_exists"}, 409)
except r.RequestException:
return bad_request()
try:
resp = r.post(f"http://{database_node}/users/{username}/password",
json={"value": password}, headers=DBH, timeout=5)
except r.RequestException:
return bad_request()
if resp.status_code == 200:
out = make_response({"status": "ok"}, 200)
out.set_cookie("session", f"{username}||{password}", httponly=True, samesite="Lax")
return out
return make_response({"status": "error"}, resp.status_code)
@app.route('/update_personal', methods=["POST"])
def update_personal():
username = current_user()
if username is None:
return unauthorized()
data = json_body()
if data is None:
return bad_request()
for field in ("first_name", "second_name", "email"):
if field not in data or not clean_value(data[field]):
return bad_request()
try:
for field in ("first_name", "second_name", "email"):
r.post(f"http://{database_node}/users/{username}/{field}",
json={"value": data[field]}, headers=DBH, timeout=5)
except r.RequestException:
return bad_request()
return make_response({"status": "ok"}, 200)
@app.route('/send_msg', methods=["POST"])
def send_msg():
username = current_user()
if username is None:
return unauthorized()
data = json_body()
if data is None or not clean_value(data.get("msg")):
return bad_request()
try:
r.post(f"http://{database_node}/users/{username}/msg",
json={"value": data["msg"]}, headers=DBH, timeout=5)
except r.RequestException:
return bad_request()
return make_response({"status": "ok"}, 200)
@app.route('/post_comment', methods=["POST"])
def post_comment():
username = current_user()
if username is None:
return unauthorized()
data = json_body()
if data is None or not clean_value(data.get("msg")):
return bad_request()
# PATCH V8: img_id раньше уходил в URL внутреннего запроса как есть
try:
img_id = int(data.get("img_id"))
except (TypeError, ValueError):
return bad_request()
if not (IMG_MIN <= img_id <= IMG_MAX):
return bad_request()
try:
r.post(f"http://{database_node}/images/{img_id}",
json={"value": [username, data["msg"]]}, headers=DBH, timeout=5)
except r.RequestException:
return bad_request()
return make_response({"status": "ok"}, 200)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=logic_node_port)