server/app/__init__.py (78 lines of code) (raw):
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Selfserve Portal for the Apache Software Foundation"""
import re
import secrets
import asfquart
import asfquart.generics
import quart
from .lib import config, log, middleware
import os
import hashlib
import base64
STATIC_DIR = os.path.join(
os.path.realpath(".."), "htdocs"
) # File location of static assets
TEMPLATES_DIR = os.path.join(STATIC_DIR, "templates") # HTML master templates
COMPILED_DIR = os.path.join(
STATIC_DIR, "compiled"
) # Compiled HTML (template + content)
asfquart.generics.OAUTH_URL_INIT = "https://oauth.apache.org/auth?state=%s&redirect_uri=%s"
asfquart.generics.OAUTH_URL_CALLBACK = "https://oauth.apache.org/token?code=%s"
def file_to_sri(filepath: str):
"""Generates a sub-resource integrity value for a file - https://www.w3.org/TR/SRI/"""
with open(filepath, "rb") as f:
digest = hashlib.sha384(f.read()).digest()
b64_digest = base64.b64encode(digest).decode("us-ascii")
return f"sha384-{b64_digest}"
def main():
asfquart.construct(__name__, oauth="/api/auth")
asfquart.APP.secret_key = secrets.token_hex() # For session management
asfquart.APP.config[
"MAX_CONTENT_LENGTH"
] = config.server.max_content_length # Ensure upload limits match expectations
asfquart.APP.url_map.converters[
"filename"
] = middleware.FilenameConverter # Special converter for filename-style vars
# Static files (or index.html if requesting a dir listing)
@asfquart.APP.route("/<path:path>")
@asfquart.APP.route("/")
async def static_files(path="index.html"):
if path.endswith("/"):
path += "index.html"
if path.endswith(".html"): # Serve HTML from the compiled output dir
return await quart.send_from_directory(COMPILED_DIR, path)
return await quart.send_from_directory(STATIC_DIR, path)
@asfquart.APP.before_serving
async def compile_html():
"""Compiles HTML files in htdocs/ using a master template"""
master_template = open(os.path.join(TEMPLATES_DIR, "master.html")).read()
# Add sub-resource integrity to all scripts
for script_src in re.finditer(r'(src="(.+?\.js)")', master_template):
script_name = script_src.group(2).lstrip("/")
script_path = os.path.join(STATIC_DIR, script_name)
if os.path.isfile(script_path):
sri = file_to_sri(script_path)
orig_src = script_src.group(1)
new_src = f'{orig_src} integrity="{sri}"'
master_template = master_template.replace(orig_src, new_src)
if not os.path.isdir(COMPILED_DIR):
log.log(
f"Compiled HTML directory {COMPILED_DIR} does not exist, will attempt to create it"
)
os.makedirs(COMPILED_DIR, exist_ok=True, mode=0o700)
for htmlfile in [
filename
for filename in os.listdir(STATIC_DIR)
if filename.endswith(".html")
]:
print(f"Compiling {htmlfile} into output/{htmlfile}")
htmldata = open(os.path.join(STATIC_DIR, htmlfile)).read()
output = master_template.replace("{contents}", htmldata)
open(os.path.join(COMPILED_DIR, htmlfile), "w").write(output)
@asfquart.APP.before_serving
async def load_endpoints():
"""Load all API end points. This is run before Quart starts serving requests"""
async with asfquart.APP.app_context():
from . import endpoints
from .lib import tokens
# Regularly update the list of projects from LDAP
asfquart.APP.add_background_task(config.get_projects_from_ldap)
# Reset rate limits daily
asfquart.APP.add_background_task(middleware.reset_rate_limits)
# Fetch mailing lists hourly
asfquart.APP.add_background_task(config.fetch_valid_lists)
@asfquart.APP.after_serving
async def shutdown():
"""Ensure a clean shutdown of the portal by stopping background tasks"""
log.log("Shutting down selfserve portal...")
asfquart.APP.background_tasks.clear() # Clear repo polling etc
return asfquart.APP