fix: Security issue in download route and cleanup
All checks were successful
Build Docker / BuildImage (push) Successful in 2m13s

This commit is contained in:
2025-10-11 17:01:20 +11:00
parent fc56cafab8
commit 00d035a0e8
3 changed files with 169 additions and 153 deletions

View File

@@ -3,8 +3,7 @@ import os
import datetime import datetime
import requests import requests
from mail import sendEmail from mail import sendEmail
from sol import create_transaction, get_solana_address from sol import create_transaction
import json
api_bp = Blueprint('api', __name__) api_bp = Blueprint('api', __name__)
@@ -48,7 +47,7 @@ def getGitCommit():
@api_bp.route("/") @api_bp.route("/")
@api_bp.route("/help") @api_bp.route("/help")
def api_help_get(): def help_get():
return jsonify({ return jsonify({
"message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au", "message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au",
"endpoints": { "endpoints": {
@@ -64,11 +63,11 @@ def api_help_get():
}) })
@api_bp.route("/version") @api_bp.route("/version")
def api_version_get(): def version_get():
return jsonify({"version": getGitCommit()}) return jsonify({"version": getGitCommit()})
@api_bp.route("/time") @api_bp.route("/time")
def api_time_get(): def time_get():
timezone_offset = datetime.timedelta(hours=ncConfig["time-zone"]) timezone_offset = datetime.timedelta(hours=ncConfig["time-zone"])
timezone = datetime.timezone(offset=timezone_offset) timezone = datetime.timezone(offset=timezone_offset)
time = datetime.datetime.now(tz=timezone) time = datetime.datetime.now(tz=timezone)
@@ -80,11 +79,11 @@ def api_time_get():
}) })
@api_bp.route("/timezone") @api_bp.route("/timezone")
def api_timezone_get(): def timezone_get():
return jsonify({"timezone": ncConfig["time-zone"]}) return jsonify({"timezone": ncConfig["time-zone"]})
@api_bp.route("/timezone", methods=["POST"]) @api_bp.route("/timezone", methods=["POST"])
def api_timezone_post(): def timezone_post():
# Refresh config # Refresh config
global ncConfig global ncConfig
conf = requests.get( conf = requests.get(
@@ -101,17 +100,17 @@ def api_timezone_post():
return jsonify({"message": "Successfully pulled latest timezone", "timezone": ncConfig["time-zone"]}) return jsonify({"message": "Successfully pulled latest timezone", "timezone": ncConfig["time-zone"]})
@api_bp.route("/message") @api_bp.route("/message")
def api_message_get(): def message_get():
return jsonify({"message": ncConfig["message"]}) return jsonify({"message": ncConfig["message"]})
@api_bp.route("/ip") @api_bp.route("/ip")
def api_ip_get(): def ip_get():
return jsonify({"ip": getClientIP(request)}) return jsonify({"ip": getClientIP(request)})
@api_bp.route("/email", methods=["POST"]) @api_bp.route("/email", methods=["POST"])
def api_email_post(): def email_post():
# Verify json # Verify json
if not request.is_json: if not request.is_json:
return jsonify({ return jsonify({
@@ -143,7 +142,7 @@ def api_email_post():
@api_bp.route("/project") @api_bp.route("/project")
def api_project_get(): def project_get():
try: try:
git = requests.get( git = requests.get(
"https://git.woodburn.au/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1", "https://git.woodburn.au/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",

250
server.py
View File

@@ -18,13 +18,12 @@ import datetime
import qrcode import qrcode
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
from ansi2html import Ansi2HTMLConverter from ansi2html import Ansi2HTMLConverter
from functools import cache
from PIL import Image from PIL import Image
from blueprints.now import now_bp from blueprints.now import now_bp
from blueprints.blog import blog_bp from blueprints.blog import blog_bp
from blueprints.wellknown import wk_bp from blueprints.wellknown import wk_bp
from blueprints.api import api_bp, getGitCommit, getClientIP from blueprints.api import api_bp, getGitCommit, getClientIP
from sol import create_transaction from tools import isCurl, isCrawler, getAddress, getFilePath
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
@@ -37,67 +36,49 @@ app.register_blueprint(api_bp, url_prefix='/api/v1')
dotenv.load_dotenv() dotenv.load_dotenv()
# region Config/Constants
# Rate limiting for hosting enquiries # Rate limiting for hosting enquiries
email_request_count = {} # Track requests by email EMAIL_REQUEST_COUNT = {} # Track requests by email
ip_request_count = {} # Track requests by IP IP_REQUEST_COUNT = {} # Track requests by IP
EMAIL_RATE_LIMIT = 3 # Max 3 requests per email per hour EMAIL_RATE_LIMIT = 3 # Max 3 requests per email per hour
IP_RATE_LIMIT = 5 # Max 5 requests per IP per hour IP_RATE_LIMIT = 5 # Max 5 requests per IP per hour
RATE_LIMIT_WINDOW = 3600 # 1 hour in seconds RATE_LIMIT_WINDOW = 3600 # 1 hour in seconds
handshake_scripts = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>' HANDSHAKE_SCRIPTS = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
restricted = ["ascii"] RESTRICTED_ROUTES = ["ascii"]
redirects = { REDIRECT_ROUTES = {
"contact": "/#contact" "contact": "/#contact"
} }
downloads = { DOWNLOAD_ROUTES = {
"pgp": "data/nathanwoodburn.asc" "pgp": "data/nathanwoodburn.asc"
} }
SITES = []
sites = []
if os.path.isfile("data/sites.json"): if os.path.isfile("data/sites.json"):
with open("data/sites.json") as file: with open("data/sites.json") as file:
sites = json.load(file) SITES = json.load(file)
# Remove any sites that are not enabled # Remove any sites that are not enabled
sites = [ SITES = [
site for site in sites if "enabled" not in site or site["enabled"] site for site in SITES if "enabled" not in site or site["enabled"]
] ]
projects = [] PROJECTS = []
projectsUpdated = 0 PROJECTS_UPDATED = 0
NC_CONFIG = requests.get(
ncReq = requests.get(
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json" "https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
) ).json()
ncConfig = ncReq.json()
if 'time-zone' not in ncConfig:
ncConfig['time-zone'] = 10
# region Helper Functions
@cache
def getAddress(coin: str) -> str:
address = ""
if os.path.isfile(".well-known/wallets/" + coin.upper()):
with open(".well-known/wallets/" + coin.upper()) as file:
address = file.read()
return address
def getFilePath(name, path):
for root, dirs, files in os.walk(path):
if name in files:
return os.path.join(root, name)
if 'time-zone' not in NC_CONFIG:
NC_CONFIG['time-zone'] = 10
# endregion # endregion
# region Assets routes # region Assets routes
@app.route("/assets/<path:path>") @app.route("/assets/<path:path>")
def asset_get(path): def asset_get(path):
if path.endswith(".json"): if path.endswith(".json"):
@@ -206,21 +187,17 @@ def links_get():
return render_template("link.html") return render_template("link.html")
@app.route("/generator/")
def generator_get():
return render_template(request.path.split("/")[-2] + ".html")
@app.route("/api/<path:function>") @app.route("/api/<path:function>")
def api_old_get(function): def api_legacy_get(function):
return redirect(f"/api/v1/{function}", code=301) return redirect(f"/api/v1/{function}", code=301)
@app.route("/actions.json") @app.route("/actions.json")
def sol_actions_get(): def sol_actions_get():
return jsonify( return jsonify(
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]} {"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
) )
# endregion # endregion
# region Main routes # region Main routes
@@ -228,9 +205,9 @@ def sol_actions_get():
@app.route("/") @app.route("/")
def index_get(): def index_get():
global handshake_scripts global HANDSHAKE_SCRIPTS
global projects global PROJECTS
global projectsUpdated global PROJECTS_UPDATED
# Check if host if podcast.woodburn.au # Check if host if podcast.woodburn.au
if "podcast.woodburn.au" in request.host: if "podcast.woodburn.au" in request.host:
@@ -247,35 +224,27 @@ def index_get():
# Always load if load is in the query string # Always load if load is in the query string
if request.args.get("load"): if request.args.get("load"):
loaded = False loaded = False
if isCurl(request):
return jsonify(
{
"message": "Welcome to Nathan.Woodburn/! This is a personal website. For more information, visit https://nathan.woodburn.au",
"ip": getClientIP(request),
"dev": HANDSHAKE_SCRIPTS == "",
"version": getGitCommit()
}
)
# Check if crawler if not loaded and not isCrawler(request):
if request.headers and request.headers.get("User-Agent"): # Set cookie
# Check if curl resp = make_response(
if "curl" in request.headers.get("User-Agent", "curl"): render_template("loading.html").replace(
return jsonify( "https://nathan.woodburn.au/loading", "https://nathan.woodburn.au/"
{ ),
"message": "Welcome to Nathan.Woodburn/! This is a personal website. For more information, visit https://nathan.woodburn.au", 200,
"ip": getClientIP(request), {"Content-Type": "text/html"},
"dev": handshake_scripts == "", )
"version": getGitCommit() resp.set_cookie("loaded", "true", max_age=604800)
} return resp
)
if "Googlebot" not in request.headers.get(
"User-Agent", ""
) and "Bingbot" not in request.headers.get("User-Agent", ""):
# Check if cookie is set
if not loaded:
# Set cookie
resp = make_response(
render_template("loading.html").replace(
"https://nathan.woodburn.au/loading", "https://nathan.woodburn.au/"
),
200,
{"Content-Type": "text/html"},
)
resp.set_cookie("loaded", "true", max_age=604800)
return resp
try: try:
git = requests.get( git = requests.get(
@@ -300,14 +269,14 @@ def index_get():
print(f"Error getting git data: {e}") print(f"Error getting git data: {e}")
# Get only repo names for the newest updates # Get only repo names for the newest updates
if projects == [] or projectsUpdated < (datetime.datetime.now() - datetime.timedelta( if PROJECTS == [] or PROJECTS_UPDATED < (datetime.datetime.now() - datetime.timedelta(
hours=2 hours=2
)).timestamp(): )).timestamp():
projectsreq = requests.get( projectsreq = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos" "https://git.woodburn.au/api/v1/users/nathanwoodburn/repos"
) )
projects = projectsreq.json() PROJECTS = projectsreq.json()
# Check for next page # Check for next page
pageNum = 1 pageNum = 1
@@ -316,10 +285,10 @@ def index_get():
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page=" "https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page="
+ str(pageNum) + str(pageNum)
) )
projects += projectsreq.json() PROJECTS += projectsreq.json()
pageNum += 1 pageNum += 1
for project in projects: for project in PROJECTS:
if ( if (
project["avatar_url"] == "https://git.woodburn.au/" project["avatar_url"] == "https://git.woodburn.au/"
or project["avatar_url"] == "" or project["avatar_url"] == ""
@@ -329,16 +298,16 @@ def index_get():
"_", " ").replace("-", " ") "_", " ").replace("-", " ")
# Sort by last updated # Sort by last updated
projectsList = sorted( projectsList = sorted(
projects, key=lambda x: x["updated_at"], reverse=True) PROJECTS, key=lambda x: x["updated_at"], reverse=True)
projects = [] PROJECTS = []
projectNames = [] projectNames = []
projectNum = 0 projectNum = 0
while len(projects) < 3: while len(PROJECTS) < 3:
if projectsList[projectNum]["name"] not in projectNames: if projectsList[projectNum]["name"] not in projectNames:
projects.append(projectsList[projectNum]) PROJECTS.append(projectsList[projectNum])
projectNames.append(projectsList[projectNum]["name"]) projectNames.append(projectsList[projectNum]["name"])
projectNum += 1 projectNum += 1
projectsUpdated = datetime.datetime.now().timestamp() PROJECTS_UPDATED = datetime.datetime.now().timestamp()
custom = "" custom = ""
# Check for downtime # Check for downtime
@@ -363,10 +332,10 @@ def index_get():
or os.getenv("dev") == "true" or os.getenv("dev") == "true"
or request.host == "test.nathan.woodburn.au" or request.host == "test.nathan.woodburn.au"
): ):
handshake_scripts = "" HANDSHAKE_SCRIPTS = ""
# Get time # Get time
timezone_offset = datetime.timedelta(hours=ncConfig["time-zone"]) timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
timezone = datetime.timezone(offset=timezone_offset) timezone = datetime.timezone(offset=timezone_offset)
time = datetime.datetime.now(tz=timezone) time = datetime.datetime.now(tz=timezone)
@@ -389,7 +358,7 @@ def index_get():
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
} }
""" """
time += f"startClock({ncConfig['time-zone']});" time += f"startClock({NC_CONFIG['time-zone']});"
time += "</script>" time += "</script>"
HNSaddress = getAddress("HNS") HNSaddress = getAddress("HNS")
@@ -400,7 +369,7 @@ def index_get():
resp = make_response( resp = make_response(
render_template( render_template(
"index.html", "index.html",
handshake_scripts=handshake_scripts, handshake_scripts=HANDSHAKE_SCRIPTS,
HNS=HNSaddress, HNS=HNSaddress,
SOL=SOLaddress, SOL=SOLaddress,
BTC=BTCaddress, BTC=BTCaddress,
@@ -408,10 +377,10 @@ def index_get():
repo=repo, repo=repo,
repo_description=repo_description, repo_description=repo_description,
custom=custom, custom=custom,
sites=sites, sites=SITES,
projects=projects, projects=PROJECTS,
time=time, time=time,
message=ncConfig["message"], message=NC_CONFIG["message"],
), ),
200, 200,
{"Content-Type": "text/html"}, {"Content-Type": "text/html"},
@@ -425,7 +394,7 @@ def index_get():
@app.route("/donate") @app.route("/donate")
def donate_get(): def donate_get():
global handshake_scripts global HANDSHAKE_SCRIPTS
# If localhost, don't load handshake # If localhost, don't load handshake
if ( if (
request.host == "localhost:5000" request.host == "localhost:5000"
@@ -433,7 +402,7 @@ def donate_get():
or os.getenv("dev") == "true" or os.getenv("dev") == "true"
or request.host == "test.nathan.woodburn.au" or request.host == "test.nathan.woodburn.au"
): ):
handshake_scripts = "" HANDSHAKE_SCRIPTS = ""
coinList = os.listdir(".well-known/wallets") coinList = os.listdir(".well-known/wallets")
coinList = [file for file in coinList if file[0] != "."] coinList = [file for file in coinList if file[0] != "."]
@@ -472,7 +441,7 @@ def donate_get():
) )
return render_template( return render_template(
"donate.html", "donate.html",
handshake_scripts=handshake_scripts, handshake_scripts=HANDSHAKE_SCRIPTS,
coins=coins, coins=coins,
default_coins=default_coins, default_coins=default_coins,
crypto=instructions, crypto=instructions,
@@ -542,7 +511,7 @@ def donate_get():
return render_template( return render_template(
"donate.html", "donate.html",
handshake_scripts=handshake_scripts, handshake_scripts=HANDSHAKE_SCRIPTS,
crypto=cryptoHTML, crypto=cryptoHTML,
coins=coins, coins=coins,
default_coins=default_coins, default_coins=default_coins,
@@ -578,7 +547,7 @@ def qrcode_get(data):
qr.make() qr.make()
qr_image: Image.Image = qr.make_image( qr_image: Image.Image = qr.make_image(
fill_color="black", back_color="white").convert('RGB') # type: ignore fill_color="black", back_color="white").convert('RGB') # type: ignore
# Add logo # Add logo
logo = Image.open("templates/assets/img/favicon/logo.png") logo = Image.open("templates/assets/img/favicon/logo.png")
@@ -610,9 +579,10 @@ def supersecretpath_get():
@app.route("/download/<path:path>") @app.route("/download/<path:path>")
def download_get(path): def download_get(path):
if path not in DOWNLOAD_ROUTES:
return render_template("404.html"), 404
# Check if file exists # Check if file exists
if path in downloads: path = DOWNLOAD_ROUTES[path]
path = downloads[path]
if os.path.isfile(path): if os.path.isfile(path):
return send_file(path) return send_file(path)
return render_template("404.html"), 404 return render_template("404.html"), 404
@@ -620,8 +590,8 @@ def download_get(path):
@app.route("/hosting/send-enquiry", methods=["POST"]) @app.route("/hosting/send-enquiry", methods=["POST"])
def hosting_post(): def hosting_post():
global email_request_count global EMAIL_REQUEST_COUNT
global ip_request_count global IP_REQUEST_COUNT
if not request.json: if not request.json:
return jsonify({"status": "error", "message": "No JSON data provided"}), 400 return jsonify({"status": "error", "message": "No JSON data provided"}), 400
@@ -641,39 +611,39 @@ def hosting_post():
current_time = datetime.datetime.now().timestamp() current_time = datetime.datetime.now().timestamp()
# Check email rate limit # Check email rate limit
if email in email_request_count: if email in EMAIL_REQUEST_COUNT:
if (current_time - email_request_count[email]["last_reset"]) > RATE_LIMIT_WINDOW: if (current_time - EMAIL_REQUEST_COUNT[email]["last_reset"]) > RATE_LIMIT_WINDOW:
# Reset counter if the time window has passed # Reset counter if the time window has passed
email_request_count[email] = { EMAIL_REQUEST_COUNT[email] = {
"count": 1, "last_reset": current_time} "count": 1, "last_reset": current_time}
else: else:
# Increment counter # Increment counter
email_request_count[email]["count"] += 1 EMAIL_REQUEST_COUNT[email]["count"] += 1
if email_request_count[email]["count"] > EMAIL_RATE_LIMIT: if EMAIL_REQUEST_COUNT[email]["count"] > EMAIL_RATE_LIMIT:
return jsonify({ return jsonify({
"status": "error", "status": "error",
"message": "Rate limit exceeded. Please try again later." "message": "Rate limit exceeded. Please try again later."
}), 429 }), 429
else: else:
# First request for this email # First request for this email
email_request_count[email] = {"count": 1, "last_reset": current_time} EMAIL_REQUEST_COUNT[email] = {"count": 1, "last_reset": current_time}
# Check IP rate limit # Check IP rate limit
if ip in ip_request_count: if ip in IP_REQUEST_COUNT:
if (current_time - ip_request_count[ip]["last_reset"]) > RATE_LIMIT_WINDOW: if (current_time - IP_REQUEST_COUNT[ip]["last_reset"]) > RATE_LIMIT_WINDOW:
# Reset counter if the time window has passed # Reset counter if the time window has passed
ip_request_count[ip] = {"count": 1, "last_reset": current_time} IP_REQUEST_COUNT[ip] = {"count": 1, "last_reset": current_time}
else: else:
# Increment counter # Increment counter
ip_request_count[ip]["count"] += 1 IP_REQUEST_COUNT[ip]["count"] += 1
if ip_request_count[ip]["count"] > IP_RATE_LIMIT: if IP_REQUEST_COUNT[ip]["count"] > IP_RATE_LIMIT:
return jsonify({ return jsonify({
"status": "error", "status": "error",
"message": "Rate limit exceeded. Please try again later." "message": "Rate limit exceeded. Please try again later."
}), 429 }), 429
else: else:
# First request for this IP # First request for this IP
ip_request_count[ip] = {"count": 1, "last_reset": current_time} IP_REQUEST_COUNT[ip] = {"count": 1, "last_reset": current_time}
cpus = request.json["cpus"] cpus = request.json["cpus"]
memory = request.json["memory"] memory = request.json["memory"]
@@ -833,7 +803,7 @@ def podcast_podsync_get():
@app.route("/<path:path>") @app.route("/<path:path>")
def catch_all_get(path: str): def catch_all_get(path: str):
global handshake_scripts global HANDSHAKE_SCRIPTS
# If localhost, don't load handshake # If localhost, don't load handshake
if ( if (
request.host == "localhost:5000" request.host == "localhost:5000"
@@ -841,27 +811,27 @@ def catch_all_get(path: str):
or os.getenv("dev") == "true" or os.getenv("dev") == "true"
or request.host == "test.nathan.woodburn.au" or request.host == "test.nathan.woodburn.au"
): ):
handshake_scripts = "" HANDSHAKE_SCRIPTS = ""
if path.lower().replace(".html", "") in restricted: if path.lower().replace(".html", "") in RESTRICTED_ROUTES:
return render_template("404.html"), 404 return render_template("404.html"), 404
if path in redirects: if path in REDIRECT_ROUTES:
return redirect(redirects[path], code=302) return redirect(REDIRECT_ROUTES[path], code=302)
# If file exists, load it # If file exists, load it
if os.path.isfile("templates/" + path): if os.path.isfile("templates/" + path):
return render_template(path, handshake_scripts=handshake_scripts, sites=sites) return render_template(path, handshake_scripts=HANDSHAKE_SCRIPTS, sites=SITES)
# Try with .html # Try with .html
if os.path.isfile("templates/" + path + ".html"): if os.path.isfile("templates/" + path + ".html"):
return render_template( return render_template(
path + ".html", handshake_scripts=handshake_scripts, sites=sites path + ".html", handshake_scripts=HANDSHAKE_SCRIPTS, sites=SITES
) )
if os.path.isfile("templates/" + path.strip("/") + ".html"): if os.path.isfile("templates/" + path.strip("/") + ".html"):
return render_template( return render_template(
path.strip("/") + ".html", handshake_scripts=handshake_scripts, sites=sites path.strip("/") + ".html", handshake_scripts=HANDSHAKE_SCRIPTS, sites=SITES
) )
# Try to find a file matching # Try to find a file matching
@@ -871,16 +841,14 @@ def catch_all_get(path: str):
if filename: if filename:
return send_file(filename) return send_file(filename)
if request.headers: if isCurl(request):
# Check if curl return jsonify(
if "curl" in request.headers.get("User-Agent", "curl"): {
return jsonify( "status": 404,
{ "message": "Page not found",
"status": 404, "ip": getClientIP(request),
"message": "Page not found", }
"ip": getClientIP(request), ), 404
}
), 404
return render_template("404.html"), 404 return render_template("404.html"), 404
# 404 catch all # 404 catch all
@@ -888,16 +856,14 @@ def catch_all_get(path: str):
@app.errorhandler(404) @app.errorhandler(404)
def not_found(e): def not_found(e):
if request.headers: if isCurl(request):
# Check if curl return jsonify(
if "curl" in request.headers.get("User-Agent", "curl"): {
return jsonify( "status": 404,
{ "message": "Page not found",
"status": 404, "ip": getClientIP(request),
"message": "Page not found", }
"ip": getClientIP(request), ), 404
}
), 404
return render_template("404.html"), 404 return render_template("404.html"), 404

51
tools.py Normal file
View File

@@ -0,0 +1,51 @@
from flask import Request
import os
from functools import cache
def isCurl(request: Request) -> bool:
"""
Check if the request is from curl
Args:
request (Request): The Flask request object
Returns:
bool: True if the request is from curl, False otherwise
"""
if request.headers and request.headers.get("User-Agent"):
# Check if curl
if "curl" in request.headers.get("User-Agent", "curl"):
return True
return False
def isCrawler(request: Request) -> bool:
"""
Check if the request is from a web crawler (e.g., Googlebot, Bingbot)
Args:
request (Request): The Flask request object
Returns:
bool: True if the request is from a web crawler, False otherwise
"""
if request.headers and request.headers.get("User-Agent"):
# Check if Googlebot or Bingbot
if "Googlebot" in request.headers.get(
"User-Agent", ""
) or "Bingbot" in request.headers.get("User-Agent", ""):
return True
return False
@cache
def getAddress(coin: str) -> str:
address = ""
if os.path.isfile(".well-known/wallets/" + coin.upper()):
with open(".well-known/wallets/" + coin.upper()) as file:
address = file.read()
return address
def getFilePath(name, path):
for root, dirs, files in os.walk(path):
if name in files:
return os.path.join(root, name)