100 lines
4.1 KiB
Python
Executable File
100 lines
4.1 KiB
Python
Executable File
#!/usr/bin/python3
|
||
from flask import Flask, render_template, request, redirect, abort
|
||
import os
|
||
import requests as r
|
||
app = Flask(__name__, template_folder='files')
|
||
database_node_port = 5002
|
||
content_node_port = 5004
|
||
database_node = "database:" + str(database_node_port)
|
||
INTERNAL = os.environ.get("RUSGRAM_INTERNAL", "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9")
|
||
DBH = {"X-Internal": INTERNAL}
|
||
|
||
# PATCH V1/V5: template_folder ('files') — это тот же том, где лежит db/users/<логин>.
|
||
# Раньше render_template(path) с путём из URL позволял отрендерить файл юзера
|
||
# как Jinja-шаблон => SSTI => RCE. Рендерим только реально существующие страницы.
|
||
ALLOWED_TEMPLATES = {"index.html", "login.html", "register.html", "specific.html"}
|
||
|
||
# PATCH V8: сколько картинок реально есть в db/images
|
||
IMG_MIN, IMG_MAX = 1, 16
|
||
|
||
|
||
def get_login():
|
||
"""Возвращает имя юзера или None. Никогда не бросает исключение."""
|
||
session = request.cookies.get("session")
|
||
if session is None:
|
||
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 _field(username, name):
|
||
try:
|
||
resp = r.get(f"http://{database_node}/users/{username}/{name}",
|
||
headers=DBH, timeout=5)
|
||
return resp.json()["data"] if resp.status_code == 200 else ""
|
||
except (r.RequestException, ValueError, KeyError):
|
||
return ""
|
||
|
||
|
||
@app.route('/<path:path>', methods=["GET", "POST"])
|
||
def route(path):
|
||
if path == "index.html":
|
||
if (username := get_login()) is None:
|
||
return redirect("/login.html", code=302)
|
||
imgs = list()
|
||
for i in range(IMG_MIN, IMG_MAX + 1):
|
||
try:
|
||
imgs.append(r.get(f"http://{database_node}/images/{i}",
|
||
headers=DBH, timeout=5).json()["data"])
|
||
except (r.RequestException, ValueError, KeyError):
|
||
continue
|
||
imgs[-1]["id"] = str(i).zfill(2)
|
||
user = dict()
|
||
user["first_name"] = _field(username, "first_name")
|
||
user["second_name"] = _field(username, "second_name")
|
||
user["email"] = _field(username, "email")
|
||
user["msg"] = _field(username, "msg")
|
||
return render_template("index.html", data=imgs, user=user)
|
||
elif path == "specific.html":
|
||
if get_login() is None:
|
||
return redirect("/login.html", code=302)
|
||
# PATCH V8: img приходит из URL. Раньше любое нечисловое/вне диапазона
|
||
# значение давало 500, а незакавыченная подстановка {{img['id']}} внутрь
|
||
# <script> в шаблоне позволяла подмешать JS.
|
||
try:
|
||
img_id = int(request.args.get('img', ''))
|
||
except (TypeError, ValueError):
|
||
abort(404)
|
||
if not (IMG_MIN <= img_id <= IMG_MAX):
|
||
abort(404)
|
||
try:
|
||
img = r.get(f"http://{database_node}/images/{img_id}",
|
||
headers=DBH, timeout=5).json()["data"]
|
||
except (r.RequestException, ValueError, KeyError):
|
||
abort(404)
|
||
img["id"] = str(img_id).zfill(2)
|
||
return render_template("specific.html", img=img)
|
||
else:
|
||
# PATCH V1/V5: всё, чего нет в белом списке, до Jinja не доходит.
|
||
if path not in ALLOWED_TEMPLATES:
|
||
abort(404)
|
||
return render_template(path)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
app.run(host="0.0.0.0", port=content_node_port)
|