app.py (120 lines of code) (raw):
import json
import re
from urllib.parse import ParseResult, urlencode, urlparse, urlunparse
from flask import Flask, abort, make_response, redirect, request
REDIRECT_MAP = {}
def load_redirect_map(path="foundation.mozilla.org_wagtail_redirects.json"):
"""
Load the foundation.mozilla.org redirects into memory.
Not necessary to offload to some external resource like Redis yet.
"""
global REDIRECT_MAP
try:
with open(path) as f:
REDIRECT_MAP = json.load(f)
except FileNotFoundError:
REDIRECT_MAP = {}
load_redirect_map()
def get_keyvalue_redirect(path, query_string, redirect_map, debug=False):
"""
Checks the in-memory key/value redirect map and returns a redirect response
if a match is found. Returns None otherwise.
"""
full_path = "/" + path
# Normalize: try as-is and strip trailing slash
candidates = [full_path]
if full_path.endswith("/"):
candidates.append(full_path.rstrip("/"))
else:
candidates.append(full_path + "/")
# Also try full_path + query
if query_string:
candidates = [f"{p}?{query_string}" for p in candidates] + candidates
# Try each candidate
for candidate in candidates:
redirect_entry = redirect_map.get(candidate)
if redirect_entry:
redirect_url = redirect_entry["redirect_to"]
# If candidate didn't include query string, but the request did, add it
if "?" not in candidate and query_string:
separator = "&" if "?" in redirect_url else "?"
redirect_url = f"{redirect_url}{separator}{query_string}"
status_code = 301 if redirect_entry.get("is_permanent") else 302
if debug:
print(f"[kv redirect] {candidate} → {redirect_url} ({status_code})")
return redirect(redirect_url, code=status_code)
return None
def create_app(test_config=None):
app = Flask(__name__, static_folder=None)
if test_config is None:
app.config.from_object("config.Config")
else:
app.config.update(test_config)
redirect_rules = app.config["REDIRECT_RULES"]
force_ssl = app.config["FORCE_SSL"]
debug = app.config["DEBUG"]
@app.before_request
def enforce_ssl():
if not force_ssl:
return None
proto = request.headers.get("X-Forwarded-Proto", None)
if proto == "https":
return None
url = request.url.replace("http://", "https://", 1)
return redirect(url, code=301)
@app.route("/robots.txt")
def send_robots_txt():
response = make_response("User-agent: *\n")
response.headers["Content-Type"] = "text/plain; charset=utf-8"
return response
def handle_donate_mozilla_org(path):
"""
Strips language codes from the URL path, redirecting to '/donate/' or its approved subpaths if specified.
"""
# Regex pattern to identify language codes (EX: /en-US/, /fr/)
language_code_regex = re.compile(r"^[a-z]{2}(-[A-Z]{2}|-[A-Z][a-z])?/?$")
# The default donate path
donate_path = "/donate/"
# Donate subpaths that exist on foundation.mozilla.org
donate_subpaths = ["faq", "help", "ways-to-give"]
if path:
# Strip language codes from the path and reconstruct it,
path_segments = path.strip("/").split("/")
filtered_segments = [segment for segment in path_segments if not language_code_regex.match(segment)]
cleaned_path = "/".join(filtered_segments)
if cleaned_path in donate_subpaths:
donate_path += cleaned_path
return donate_path
@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
def redirector(path):
x_forwarded_host = request.headers.get("X-Forwarded-Host", None)
if x_forwarded_host:
host = x_forwarded_host
else:
host = request.headers.get("Host", None)
if debug:
print("received request from {}".format(host))
# Special handling for donate.mozilla.org requests
if "donate.mozilla.org" in host:
path = handle_donate_mozilla_org(path)
# Prevent redirect for resources such as JS, CSS and images and return HTTP 410 Gone
if path.endswith((".js", ".css", ".png", ".svg", ".ico", ".txt")):
return abort(410)
# Use key/value redirects to short-circuit foundation.mozilla.org's redirect rule only
# Note foundation.mozilla.org for testing until domain switch is live.
if "foundation.mozilla.org" in host:
keyvalue_response = get_keyvalue_redirect(path, request.query_string.decode("utf-8"), REDIRECT_MAP, debug)
if keyvalue_response:
return keyvalue_response
if host in redirect_rules:
redirect_target, redirect_code, preserves = redirect_rules[host]
preserve_path, preserve_query = preserves
redirect_path = ""
redirect_query = ""
target_url = urlparse(redirect_target)
if preserve_path:
redirect_path = path
else:
redirect_path = target_url.path
if preserve_query:
redirect_query = urlencode(request.args, doseq=True)
else:
redirect_query = target_url.query
redirect_parse = ParseResult(
scheme=target_url.scheme,
netloc=target_url.netloc,
path=redirect_path,
query=redirect_query,
params="",
fragment="",
)
final_redirect = urlunparse(redirect_parse)
if debug:
print("redirecting to {} with a {}".format(final_redirect, redirect_code.value))
return redirect(final_redirect, code=redirect_code.value)
return abort(400)
@app.after_request
def response_headers(response):
response.headers["Server"] = "MoFo Redirector"
return response
return app
if __name__ == "__main__":
create_app().run()