Files
nathanwoodburn 759b96a848
Check Code Quality / RuffCheck (push) Successful in 1m25s
Build Docker / BuildImage (push) Successful in 2m5s
feat: Try to improve performance even more
2026-04-11 15:05:58 +10:00

882 lines
28 KiB
Python

import json
from flask import (
Flask,
make_response,
redirect,
request,
jsonify,
render_template,
send_from_directory,
send_file,
)
from flask_cors import CORS
import os
import dotenv
import requests
import datetime
import qrcode
import re
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
from ansi2html import Ansi2HTMLConverter
from PIL import Image
from zoneinfo import ZoneInfo
import argparse
# Import blueprints
from blueprints import now, blog, wellknown, api, podcast, acme, spotify
from tools import (
isCLI,
getAddress,
getFilePath,
error_response,
getClientIP,
json_response,
getHandshakeScript,
get_tools_data,
)
from curl import curl_response
from cache_helper import (
get_git_latest_activity,
get_projects,
get_wallet_tokens,
get_coin_names,
get_wallet_domains,
)
app = Flask(__name__)
CORS(app)
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 30 * (24 * 60 * 60) # 30 days in seconds
# Cache-busting and static file metadata cache
_STATIC_VERSION_CACHE: dict[str, tuple[int, int, str]] = {}
_HTML_URL_ATTR_RE = re.compile(
r'(?P<prefix>\b(?:src|href)=(["\']))(?P<url>/[^"\']+)(?P<suffix>\2)',
re.IGNORECASE,
)
_STYLESHEET_LINK_RE = re.compile(
r'<link\s+[^>]*rel=["\']stylesheet["\'][^>]*>',
re.IGNORECASE,
)
_HREF_ATTR_RE = re.compile(r'href=["\'](?P<href>[^"\']+)["\']', re.IGNORECASE)
# Register blueprints
for module in [now, blog, wellknown, api, podcast, acme, spotify]:
app.register_blueprint(module.app)
dotenv.load_dotenv()
# region Config/Constants
# Rate limiting for hosting enquiries
EMAIL_REQUEST_COUNT = {} # Track requests by email
IP_REQUEST_COUNT = {} # Track requests by IP
EMAIL_RATE_LIMIT = 3 # Max 3 requests per email per hour
IP_RATE_LIMIT = 5 # Max 5 requests per IP per hour
RATE_LIMIT_WINDOW = 3600 # 1 hour in seconds
RESTRICTED_ROUTES = ["ascii"]
REDIRECT_ROUTES = {
"contact": "/#contact",
"/meet": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
"/meeting": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
"/appointment": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
}
DOWNLOAD_ROUTES = {"pgp": "data/nathanwoodburn.asc"}
SITES = []
if os.path.isfile("data/sites.json"):
with open("data/sites.json") as file:
SITES = json.load(file)
# Remove any sites that are not enabled
SITES = [site for site in SITES if "enabled" not in site or site["enabled"]]
TZ = ZoneInfo(os.getenv("TIMEZONE", "Australia/Sydney"))
# endregion
# region request hooks
def _resolve_static_file(url_path: str) -> str | None:
clean_path = url_path.split("?", 1)[0].split("#", 1)[0]
if clean_path.startswith("/assets/"):
relative_path = clean_path[len("/assets/") :]
resolved = os.path.join("templates/assets", relative_path)
return resolved if os.path.isfile(resolved) else None
if clean_path.startswith("/fonts/"):
relative_path = clean_path[len("/fonts/") :]
resolved = os.path.join("templates/assets/fonts", relative_path)
return resolved if os.path.isfile(resolved) else None
if clean_path == "/manifest.json":
return "pwa/manifest.json"
if clean_path == "/sw.js":
return "pwa/sw.js"
if clean_path in ("/favicon.png", "/favicon.svg", "/favicon.ico"):
ext = clean_path.rsplit(".", 1)[-1]
resolved = f"templates/assets/img/favicon/favicon.{ext}"
return resolved if os.path.isfile(resolved) else None
if clean_path.count("/") == 1 and clean_path.endswith(".js"):
filename = clean_path.split("/")[-1]
resolved = os.path.join("templates/assets/js", filename)
return resolved if os.path.isfile(resolved) else None
return None
def _get_asset_version(url_path: str) -> str | None:
resolved = _resolve_static_file(url_path)
if not resolved:
return None
stat = os.stat(resolved)
mtime_ns = stat.st_mtime_ns
size = stat.st_size
cached = _STATIC_VERSION_CACHE.get(resolved)
if cached and cached[0] == mtime_ns and cached[1] == size:
return cached[2]
# Compact deterministic token derived from file metadata.
token = f"{mtime_ns:x}{size:x}"[-16:]
_STATIC_VERSION_CACHE[resolved] = (mtime_ns, size, token)
return token
def _append_cache_busting_to_html(html: str) -> str:
def _replace(match: re.Match[str]) -> str:
original_url = match.group("url")
if original_url.startswith("//"):
return match.group(0)
version = _get_asset_version(original_url)
if not version:
return match.group(0)
separator = "&" if "?" in original_url else "?"
if "?v=" in original_url or "&v=" in original_url:
return match.group(0)
cache_busted = f"{original_url}{separator}v={version}"
return f"{match.group('prefix')}{cache_busted}{match.group('suffix')}"
return _HTML_URL_ATTR_RE.sub(_replace, html)
def _is_non_blocking_stylesheet_target(href: str) -> bool:
normalized = href.lower().split("?", 1)[0]
# Keep the core layout CSS blocking.
if normalized.endswith("/assets/bootstrap/css/bootstrap.min.css"):
return False
if normalized.endswith("/assets/css/styles.min.css"):
return False
if "fonts.googleapis.com/css" in normalized:
return True
if normalized.startswith("/assets/fonts/"):
return True
return normalized in {
"/assets/css/brand-reveal.min.css",
"/assets/css/index.min.css",
"/assets/css/profile.min.css",
"/assets/css/social-icons.min.css",
"/assets/css/swiper.min.css",
}
def _async_stylesheet_tag(href: str) -> str:
return (
f'<link rel="preload" as="style" href="{href}">'
f'<link rel="stylesheet" href="{href}" media="print" onload="this.media=\'all\'">'
f'<noscript><link rel="stylesheet" href="{href}"></noscript>'
)
def _optimize_html_for_performance(html: str, path: str) -> str:
# Keep scope tight to avoid unexpected changes on unrelated pages.
if path != "/":
return html
if "fonts.googleapis.com/css" in html and "fonts.gstatic.com" not in html:
html = html.replace(
"</head>",
'<link rel="preconnect" href="https://fonts.googleapis.com">'
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>'
"</head>",
1,
)
def _replace_stylesheet(match: re.Match[str]) -> str:
tag = match.group(0)
href_match = _HREF_ATTR_RE.search(tag)
if not href_match:
return tag
href = href_match.group("href")
if 'media="print"' in tag or "media='print'" in tag:
return tag
if not _is_non_blocking_stylesheet_target(href):
return tag
return _async_stylesheet_tag(href)
html = _STYLESHEET_LINK_RE.sub(_replace_stylesheet, html)
# Replace icon-font navbar toggler to avoid dependency on icon fonts for first paint.
html = html.replace(
'<i class="fa fa-bars"></i>', '<span class="navbar-toggler-icon"></span>'
)
return html
def _is_static_request(path: str) -> bool:
return (
path.startswith("/assets/")
or path.startswith("/fonts/")
or path
in ("/manifest.json", "/sw.js", "/favicon.png", "/favicon.svg", "/favicon.ico")
or (path.count("/") == 1 and path.endswith(".js"))
)
@app.after_request
def add_header(response):
path = request.path
# HTML should be revalidated so clients can discover new cache-busted asset URLs quickly.
if response.status_code == 200 and response.mimetype == "text/html":
if not response.direct_passthrough:
html = response.get_data(as_text=True)
rewritten = _optimize_html_for_performance(html, path)
rewritten = _append_cache_busting_to_html(rewritten)
if rewritten != html:
response.set_data(rewritten)
response.headers["Cache-Control"] = "public, max-age=0, must-revalidate"
if request.method in ("GET", "HEAD"):
response.add_etag()
response.make_conditional(request)
return response
if _is_static_request(path):
if path == "/sw.js":
# Service workers should be revalidated frequently for update checks.
response.headers["Cache-Control"] = "public, max-age=0, must-revalidate"
elif request.args.get("v"):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
else:
response.headers["Cache-Control"] = (
"public, max-age=604800, stale-while-revalidate=86400"
)
return response
if "Cache-Control" not in response.headers:
response.headers["Cache-Control"] = "no-cache"
return response
# endregion
# region Assets routes
@app.route("/assets/<path:path>")
def asset(path):
if path.endswith(".json"):
return send_from_directory(
"templates/assets", path, mimetype="application/json"
)
if os.path.isfile("templates/assets/" + path):
return send_from_directory("templates/assets", path)
# Custom matching for images
pathMap = {
"img/hns/w": "img/external/HNS/white",
"img/hns/b": "img/external/HNS/black",
"img/hns": "img/external/HNS/black",
}
for key in pathMap:
if path.startswith(key):
tmpPath = path.replace(key, pathMap[key])
if os.path.isfile("templates/assets/" + tmpPath):
return send_from_directory("templates/assets", tmpPath)
# Try looking in one of the directories
filename: str = path.split("/")[-1]
if (
filename.endswith(".png")
or filename.endswith(".jpg")
or filename.endswith(".jpeg")
or filename.endswith(".svg")
):
if os.path.isfile("templates/assets/img/" + filename):
return send_from_directory("templates/assets/img", filename)
if os.path.isfile("templates/assets/img/favicon/" + filename):
return send_from_directory("templates/assets/img/favicon", filename)
return error_response(request)
@app.route("/fonts/<path:path>")
def fonts(path):
if os.path.isfile("templates/assets/fonts/" + path):
return send_from_directory("templates/assets/fonts", path)
return error_response(request)
@app.route("/sitemap")
@app.route("/sitemap.xml")
def sitemap():
# Remove all .html from sitemap
if not os.path.isfile("templates/sitemap.xml"):
return error_response(request)
with open("templates/sitemap.xml") as file:
sitemap = file.read()
sitemap = sitemap.replace(".html", "")
return make_response(sitemap, 200, {"Content-Type": "application/xml"})
@app.route("/favicon.<ext>")
def favicon(ext):
if ext not in ("png", "svg", "ico"):
return error_response(request)
return send_from_directory("templates/assets/img/favicon", f"favicon.{ext}")
@app.route("/<name>.js")
def javascript(name):
# Check if file in js directory
if not os.path.isfile("templates/assets/js/" + request.path.split("/")[-1]):
return error_response(request)
return send_from_directory("templates/assets/js", request.path.split("/")[-1])
@app.route("/download/<path:path>")
def download(path):
if path not in DOWNLOAD_ROUTES:
return error_response(request, message="Invalid download")
# Check if file exists
path = DOWNLOAD_ROUTES[path]
if os.path.isfile(path):
return send_file(path)
return error_response(request, message="File not found")
# endregion
# region PWA routes
@app.route("/manifest.json")
def manifest():
host = request.host
# Read as json
with open("pwa/manifest.json") as file:
manifest = json.load(file)
url = f"https://{host}"
if host == "localhost:5000" or host == "127.0.0.1:5000":
url = "http://127.0.0.1:5000"
manifest["start_url"] = url
manifest["scope"] = url
return jsonify(manifest)
@app.route("/sw.js")
def serviceWorker():
return send_from_directory("pwa", "sw.js")
# endregion
# region Misc routes
@app.route("/links")
def links():
return render_template("link.html")
@app.route("/actions.json")
def sol_actions():
return jsonify(
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
)
@app.route("/api/<path:function>")
def api_legacy(function):
# Check if function is in api blueprint
for rule in app.url_map.iter_rules():
# Check if the redirect route exists
if rule.rule == f"/api/v1/{function}":
return redirect(f"/api/v1/{function}", code=301)
return error_response(request, message="404 Not Found", code=404)
# endregion
# region Main routes
@app.route("/")
def index():
# Check if host if podcast.woodburn.au
if "podcast.woodburn.au" in request.host:
return render_template("podcast.html")
if isCLI(request):
return curl_response(request)
# Use cached git data
git = get_git_latest_activity()
repo_name = git["repo"]["name"].lower()
repo_description = git["repo"]["description"]
# Use cached projects data
projects = get_projects(limit=3)
# Special names
if repo_name == "nathanwoodburn.github.io":
repo_name = "Nathan.Woodburn/"
html_url = git["repo"]["html_url"]
repo = '<a href="' + html_url + '" target="_blank">' + repo_name + "</a>"
timezone_offset = TZ.utcoffset(datetime.datetime.now()).total_seconds() / 3600 # type: ignore
time = datetime.datetime.now().strftime("%B %d")
time += """
<span id=\"time\"></span>
<script>
function startClock(timezoneOffset) {
function updateClock() {
const now = new Date();
const localTime = new Date(now.getTime() + timezoneOffset * 3600 * 1000);
const tzName = timezoneOffset >= 0 ? `UTC+${timezoneOffset}` : `UTC`;
const hours = String(localTime.getUTCHours()).padStart(2, '0');
const minutes = String(localTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(localTime.getUTCSeconds()).padStart(2, '0');
const timeString = `${hours}:${minutes}:${seconds} ${tzName}`;
document.getElementById('time').textContent = timeString;
}
updateClock();
setInterval(updateClock, 1000);
}
"""
time += f"startClock({timezone_offset});"
time += "</script>"
HNSaddress = getAddress("HNS")
SOLaddress = getAddress("SOL")
BTCaddress = getAddress("BTC")
ETHaddress = getAddress("ETH")
return render_template(
"index.html",
handshake_scripts=getHandshakeScript(request.host),
HNS=HNSaddress,
SOL=SOLaddress,
BTC=BTCaddress,
ETH=ETHaddress,
repo=repo,
repo_description=repo_description,
sites=SITES,
projects=projects,
time=time,
message="",
)
# region Donate
@app.route("/donate")
def donate():
if isCLI(request):
return curl_response(request)
coinList = os.listdir(".well-known/wallets")
coinList = [file for file in coinList if file[0] != "."]
coinList.sort()
tokenList = get_wallet_tokens()
coinNames = get_coin_names()
coins = ""
default_coins = ["btc", "eth", "hns", "sol", "xrp", "ada", "dot"]
for file in coinList:
coin_name = coinNames.get(file, file)
display_style = "" if file.lower() in default_coins else "display:none;"
coins += f'<a class="dropdown-item" style="{display_style}" href="?c={file.lower()}">{coin_name}</a>'
for token in tokenList:
chain_display = f" on {token['chain']}" if token["chain"] != "null" else ""
symbol_display = (
f" ({token['symbol']}{chain_display})"
if token["symbol"] != token["name"]
else chain_display
)
coins += f'<a class="dropdown-item" style="display:none;" href="?t={token["symbol"].lower()}&c={token["chain"].lower()}">{token["name"]}{symbol_display}</a>'
crypto = request.args.get("c")
if not crypto:
instructions = (
"<br>Donate with cryptocurrency:<br>Select a coin from the dropdown above."
)
return render_template(
"donate.html",
handshake_scripts=getHandshakeScript(request.host),
coins=coins,
default_coins=default_coins,
crypto=instructions,
)
crypto = crypto.upper()
token = request.args.get("t")
if token:
token = token.upper()
for t in tokenList:
if t["symbol"].upper() == token and t["chain"].upper() == crypto:
token = t
break
if not isinstance(token, dict):
token = {"name": "Unknown token", "symbol": token, "chain": crypto}
address = ""
cryptoHTML = ""
proof = ""
if os.path.isfile(f".well-known/wallets/.{crypto}.proof"):
proof = f'<a href="/.well-known/wallets/.{crypto}.proof" target="_blank"><img src="/assets/img/proof.png" alt="Proof of ownership" style="width: 100%; max-width: 30px; margin-left: 10px;"></a>'
if os.path.isfile(f".well-known/wallets/{crypto}"):
with open(f".well-known/wallets/{crypto}") as file:
address = file.read()
coin_display = coinNames.get(crypto, crypto)
if not token:
cryptoHTML += f"<br>Donate with {coin_display}:"
else:
token_symbol = (
f" ({token['symbol']})" if token["symbol"] != token["name"] else ""
)
cryptoHTML += (
f"<br>Donate with {token['name']}{token_symbol} on {crypto}:"
)
cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>'
if proof:
cryptoHTML += proof
elif token:
if "address" in token:
address = token["address"]
token_symbol = (
f" ({token['symbol']})" if token["symbol"] != token["name"] else ""
)
chain_display = f" on {crypto}" if crypto != "NULL" else ""
cryptoHTML += (
f"<br>Donate with {token['name']}{token_symbol}{chain_display}:"
)
cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>'
if proof:
cryptoHTML += proof
else:
cryptoHTML += f"<br>Invalid offchain token: {token['symbol']}<br>"
else:
cryptoHTML += f"<br>Invalid chain: {crypto}<br>"
domains = get_wallet_domains()
if crypto in domains:
domain = domains[crypto]
cryptoHTML += "<br>Or send to this domain on compatible wallets:<br>"
cryptoHTML += f'<code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-domain" class="address" style="color: rgb(242,90,5);display: block;" data-bs-original-title="Click to copy">{domain}</code>'
if address:
cryptoHTML += (
'<br><img src="/address/'
+ address
+ '" alt="QR Code" style="width: 100%; max-width: 200px; margin: 20px auto;">'
)
copyScript = '<script>document.getElementById("crypto-address").addEventListener("click", function() {navigator.clipboard.writeText(this.innerText);this.setAttribute("data-bs-original-title", "Copied!");const tooltips = document.querySelectorAll(".tooltip-inner");tooltips.forEach(tooltip => {tooltip.innerText = "Copied!";});});document.getElementById("crypto-domain").addEventListener("click", function() {navigator.clipboard.writeText(this.innerText);this.setAttribute("data-bs-original-title", "Copied!");const tooltips = document.querySelectorAll(".tooltip-inner");tooltips.forEach(tooltip => {tooltip.innerText = "Copied!";});});</script>'
cryptoHTML += copyScript
return render_template(
"donate.html",
handshake_scripts=getHandshakeScript(request.host),
crypto=cryptoHTML,
coins=coins,
default_coins=default_coins,
)
@app.route("/address/<path:address>")
def qraddress(address):
qr = qrcode.QRCode(
version=1,
error_correction=ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(address)
qr.make(fit=True)
qr_image = qr.make_image(fill_color="#110033", back_color="white")
# Save the QR code image to a temporary file
qr_image_path = "/tmp/qr_code.png"
qr_image.save(qr_image_path) # type: ignore
# Return the QR code image as a response
return send_file(qr_image_path, mimetype="image/png")
@app.route("/qrcode/<path:data>")
@app.route("/qr/<path:data>")
def qrcodee(data):
qr = qrcode.QRCode(error_correction=ERROR_CORRECT_H, box_size=10, border=2)
qr.add_data(data)
qr.make()
qr_image: Image.Image = qr.make_image(
fill_color="black", back_color="white"
).convert("RGB") # type: ignore
# Add logo
logo = Image.open("templates/assets/img/favicon/logo.png")
basewidth = qr_image.size[0] // 3
wpercent = basewidth / float(logo.size[0])
hsize = int((float(logo.size[1]) * float(wpercent)))
logo = logo.resize((basewidth, hsize), Image.Resampling.LANCZOS)
pos = (
(qr_image.size[0] - logo.size[0]) // 2,
(qr_image.size[1] - logo.size[1]) // 2,
)
qr_image.paste(logo, pos, mask=logo)
qr_image.save("/tmp/qr_code.png")
return send_file("/tmp/qr_code.png", mimetype="image/png")
# endregion
@app.route("/supersecretpath")
def supersecretpath():
ascii_art = ""
if os.path.isfile("data/ascii.txt"):
with open("data/ascii.txt") as file:
ascii_art = file.read()
converter = Ansi2HTMLConverter()
ascii_art_html = converter.convert(ascii_art)
return render_template("ascii.html", ascii_art=ascii_art_html)
@app.route("/hosting/send-enquiry", methods=["POST"])
def hosting_post():
global EMAIL_REQUEST_COUNT
global IP_REQUEST_COUNT
if not request.is_json or not request.json:
return json_response(request, "No JSON data provided", 415)
# Keys
# email, cpus, memory, disk, backups, message
required_keys = ["email", "cpus", "memory", "disk", "backups", "message"]
for key in required_keys:
if key not in request.json:
return json_response(request, f"Missing key: {key}", 400)
email = request.json["email"]
ip = getClientIP(request)
print(f"Hosting enquiry from {email} ({ip})")
# Check rate limits
current_time = datetime.datetime.now().timestamp()
# Check email rate limit
if email in EMAIL_REQUEST_COUNT:
if (
current_time - EMAIL_REQUEST_COUNT[email]["last_reset"]
) > RATE_LIMIT_WINDOW:
# Reset counter if the time window has passed
EMAIL_REQUEST_COUNT[email] = {"count": 1, "last_reset": current_time}
else:
# Increment counter
EMAIL_REQUEST_COUNT[email]["count"] += 1
if EMAIL_REQUEST_COUNT[email]["count"] > EMAIL_RATE_LIMIT:
return json_response(
request, "Rate limit exceeded. Please try again later.", 429
)
else:
# First request for this email
EMAIL_REQUEST_COUNT[email] = {"count": 1, "last_reset": current_time}
# Check IP rate limit
if ip in IP_REQUEST_COUNT:
if (current_time - IP_REQUEST_COUNT[ip]["last_reset"]) > RATE_LIMIT_WINDOW:
# Reset counter if the time window has passed
IP_REQUEST_COUNT[ip] = {"count": 1, "last_reset": current_time}
else:
# Increment counter
IP_REQUEST_COUNT[ip]["count"] += 1
if IP_REQUEST_COUNT[ip]["count"] > IP_RATE_LIMIT:
return json_response(
request, "Rate limit exceeded. Please try again later.", 429
)
else:
# First request for this IP
IP_REQUEST_COUNT[ip] = {"count": 1, "last_reset": current_time}
cpus = request.json["cpus"]
memory = request.json["memory"]
disk = request.json["disk"]
backups = request.json["backups"]
message = request.json["message"]
# Try to convert to correct types
try:
cpus = int(cpus)
memory = float(memory)
disk = int(disk)
backups = backups in [True, "true", "True", 1, "1", "yes", "Yes"]
message = str(message)
email = str(email)
except ValueError:
return json_response(request, "Invalid data types", 400)
# Basic validation
if not isinstance(cpus, int) or cpus < 1 or cpus > 64:
return json_response(request, "Invalid CPUs", 400)
if not isinstance(memory, float) or memory < 0.5 or memory > 512:
return json_response(request, "Invalid memory", 400)
if not isinstance(disk, int) or disk < 10 or disk > 500:
return json_response(request, "Invalid disk", 400)
if not isinstance(backups, bool):
return json_response(request, "Invalid backups", 400)
if not isinstance(message, str) or len(message) > 1000:
return json_response(request, "Invalid message", 400)
if not isinstance(email, str) or len(email) > 100 or "@" not in email:
return json_response(request, "Invalid email", 400)
# Send to Discord webhook
webhook_url = os.getenv("HOSTING_WEBHOOK")
if not webhook_url:
return json_response(request, "Hosting webhook not set", 500)
data = {
"content": "",
"embeds": [
{
"title": "Hosting Enquiry",
"description": f"Email: {email}\nCPUs: {cpus}\nMemory: {memory}GB\nDisk: {disk}GB\nBackups: {backups}\nMessage: {message}",
"color": 16711680, # Red color
}
],
}
headers = {
"Content-Type": "application/json",
}
response = requests.post(webhook_url, json=data, headers=headers)
if response.status_code != 204 and response.status_code != 200:
return json_response(request, "Failed to send enquiry", 500)
return json_response(request, "Enquiry sent", 200)
@app.route("/resume")
def resume():
# Check if arg for support is passed
support = request.args.get("support")
return render_template("resume.html", support=support)
@app.route("/resume.pdf")
def resume_pdf():
# Check if arg for support is passed
support = request.args.get("support")
if support:
return send_file("data/resume_support.pdf")
return send_file("data/resume.pdf")
@app.route("/tools")
def tools():
if isCLI(request):
return curl_response(request)
return render_template("tools.html", tools=get_tools_data())
# endregion
# region Error Catching
# Catch all for GET requests
@app.route("/<path:path>")
def catch_all(path: str):
if path.lower().replace(".html", "") in RESTRICTED_ROUTES:
return error_response(request, message="Restricted route", code=403)
# If curl request, return curl response
if isCLI(request):
return curl_response(request)
if path in REDIRECT_ROUTES:
return redirect(REDIRECT_ROUTES[path], code=302)
# If file exists, load it
if os.path.isfile("templates/" + path):
return render_template(
path, handshake_scripts=getHandshakeScript(request.host), sites=SITES
)
# Try with .html
if os.path.isfile("templates/" + path + ".html"):
return render_template(
path + ".html",
handshake_scripts=getHandshakeScript(request.host),
sites=SITES,
)
if os.path.isfile("templates/" + path.strip("/") + ".html"):
return render_template(
path.strip("/") + ".html",
handshake_scripts=getHandshakeScript(request.host),
sites=SITES,
)
# Try to find a file matching
if path.count("/") < 1:
# Try to find a file matching
filename = getFilePath(path, "templates")
if filename:
return send_file(filename)
return error_response(request)
@app.errorhandler(404)
def not_found(e):
return error_response(request)
# endregion
if __name__ == "__main__":
# If --host argument is passed, use that as host, otherwise use 127.0.0.1
parser = argparse.ArgumentParser(description="Run the Flask server.")
parser.add_argument("--host", type=str, default="127.0.0.1")
args = parser.parse_args()
app.run(debug=True, port=5000, host=args.host)