Implement fastAPI prefix handling

Admins just need to set `galaxy_url_prefix`, the rest should just work.
This commit is contained in:
mvdbeek
2022-01-05 13:31:54 +01:00
parent b74f9bf038
commit 8ce2690ea9
9 changed files with 132 additions and 127 deletions
+12 -1
View File
@@ -1885,6 +1885,17 @@
:Type: str
~~~~~~~~~~~~~~~~~~~~~
``galaxy_url_prefix``
~~~~~~~~~~~~~~~~~~~~~
:Description:
URL prefix for Galaxy application. If Galaxy should be served
under a prefix set this to the desired prefix value.
:Default: ``/``
:Type: str
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``galaxy_infrastructure_url``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1946,7 +1957,7 @@
:Description:
The brand image source.
:Default: ``/static/favicon.png``
:Default: ``static/favicon.png``
:Type: str
+5 -1
View File
@@ -993,6 +993,10 @@ galaxy:
# user_preferences_extra_conf.yml
#default_locale: auto
# URL prefix for Galaxy application. If Galaxy should be served under
# a prefix set this to the desired prefix value.
#galaxy_url_prefix: /
# URL (with schema http/https) of the Galaxy instance as accessible
# within your local network - if specified used as a default by pulsar
# file staging and Jupyter Docker container for communicating back
@@ -1020,7 +1024,7 @@ galaxy:
#logo_url: /
# The brand image source.
#logo_src: /static/favicon.png
#logo_src: static/favicon.png
# The custom brand image source.
#logo_src_secondary: null
@@ -1,64 +0,0 @@
import os
from typing import List, Tuple
from paste import request
from paste.fileapp import FileApp
from paste.httpheaders import ETAG
from paste.urlparser import StaticURLParser
class CacheableStaticURLParser(StaticURLParser):
def __init__(self, directory, cache_seconds=None, directory_per_host=None):
StaticURLParser.__init__(self, directory)
self.cache_seconds = cache_seconds
self.directory_per_host = directory_per_host
def __call__(self, environ, start_response):
path_info = environ.get('PATH_INFO', '')
if not path_info:
# See if this is a static file hackishly mapped.
if os.path.exists(self.directory) and os.path.isfile(self.directory):
app = FileApp(self.directory)
if self.cache_seconds:
app.cache_control(max_age=int(self.cache_seconds))
return app(environ, start_response)
return self.add_slash(environ, start_response)
if path_info == '/':
# @@: This should obviously be configurable
filename = 'index.html'
else:
filename = request.path_info_pop(environ)
directory = self.directory
host = environ.get('HTTP_HOST')
if self.directory_per_host and host:
for host_key, host_val in self.directory_per_host.items():
if host_key in host:
directory = host_val
break
full = os.path.join(directory, filename)
if not os.path.exists(full):
return self.not_found(environ, start_response)
if os.path.isdir(full):
# @@: Cache?
return self.__class__(full)(environ, start_response)
if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/':
return self.error_extra_path(environ, start_response)
if_none_match = environ.get('HTTP_IF_NONE_MATCH')
if if_none_match:
mytime = os.stat(full).st_mtime
if str(mytime) == if_none_match:
headers: List[Tuple[str, str]] = []
ETAG.update(headers, mytime)
start_response('304 Not Modified', headers)
return [''] # empty body
app = FileApp(full)
if self.cache_seconds:
app.cache_control(max_age=int(self.cache_seconds))
return app(environ, start_response)
def make_static(global_conf, document_root, cache_seconds=None):
return CacheableStaticURLParser(document_root, cache_seconds)
-33
View File
@@ -976,36 +976,3 @@ def build_native_uwsgi_app(paste_factory, config_section):
app_kwds = get_app_kwds(config_section)
uwsgi_app = paste_factory({}, load_app_kwds=app_kwds)
return uwsgi_app
def build_url_map(app, global_conf, local_conf):
from paste.urlmap import URLMap
from galaxy.web.framework.middleware.static import CacheableStaticURLParser as Static
urlmap = URLMap()
# Merge the global and local configurations
conf = global_conf.copy()
conf.update(local_conf)
# Get cache time in seconds
cache_time = conf.get("static_cache_time", None)
if cache_time is not None:
cache_time = int(cache_time)
# Send to dynamic app by default
urlmap["/"] = app
def get_static_from_config(option_name, default_path):
config_val = conf.get(option_name, default_url_path(default_path))
per_host_config_option = f"{option_name}_by_host"
per_host_config = conf.get(per_host_config_option)
return Static(config_val, cache_time, directory_per_host=per_host_config)
# Define static mappings from config
urlmap["/static"] = get_static_from_config("static_dir", "static/")
urlmap["/images"] = get_static_from_config("static_images_dir", "static/images")
urlmap["/static/scripts"] = get_static_from_config("static_scripts_dir", "static/scripts/")
urlmap["/static/welcome.html"] = get_static_from_config("static_welcome_html", "static/welcome.html")
urlmap["/favicon.ico"] = get_static_from_config("static_favicon_dir", "static/favicon.ico")
urlmap["/robots.txt"] = get_static_from_config("static_robots_txt", "static/robots.txt")
if 'static_local_dir' in conf:
urlmap["/static_local"] = Static(conf["static_local_dir"], cache_time)
return urlmap, cache_time
-9
View File
@@ -207,10 +207,6 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs):
# Wrap the webapp in some useful middleware
if kwargs.get('middleware', True):
webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs)
if asbool(kwargs.get('static_enabled', True)):
webapp = wrap_if_allowed(webapp, app.application_stack, wrap_in_static,
args=(global_conf,),
kwargs=dict(plugin_frameworks=[app.visualizations_registry], **kwargs))
app.application_stack.register_postfork_function(postfork_setup)
for th in threading.enumerate():
@@ -1399,8 +1395,3 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf):
from galaxy.web.framework.middleware.sqldebug import SQLDebugMiddleware
app = wrap_if_allowed(app, stack, SQLDebugMiddleware, args=(webapp, {}))
return app
def wrap_in_static(app, global_conf, plugin_frameworks=None, **local_conf):
urlmap, cache_time = galaxy.webapps.base.webapp.build_url_map(app, global_conf, local_conf)
return urlmap
+9 -1
View File
@@ -1361,6 +1361,14 @@ mapping:
Users can override this settings in their user preferences if the localization
settings are enabled in user_preferences_extra_conf.yml
galaxy_url_prefix:
type: str
default: '/'
required: false
desc: |
URL prefix for Galaxy application. If Galaxy should be served under a prefix set this to
the desired prefix value.
galaxy_infrastructure_url:
type: str
default: http://localhost:8080
@@ -1409,7 +1417,7 @@ mapping:
logo_src:
type: str
default: /static/favicon.png
default: static/favicon.png
required: false
per_host: true
desc: |
+106
View File
@@ -1,13 +1,23 @@
import os
import stat
import typing
from pathlib import Path
from typing import cast
import anyio
from fastapi import FastAPI, Request
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.staticfiles import StaticFiles
from starlette.datastructures import Headers
from starlette.exceptions import HTTPException
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import (
FileResponse,
RedirectResponse,
Response,
)
from starlette.staticfiles import PathLike
from starlette.types import Scope
from galaxy.webapps.base.api import (
add_exception_handler,
@@ -16,6 +26,8 @@ from galaxy.webapps.base.api import (
)
from galaxy.webapps.base.webapp import config_allows_origin
if typing.TYPE_CHECKING:
from galaxy.config import Configuration
# https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-tags
api_tags_metadata = [
@@ -58,6 +70,90 @@ api_tags_metadata = [
]
def get_static_from_config(conf, option_name, default_path):
config_val = conf.get(option_name, default_path)
per_host_config_option = f"{option_name}_by_host"
per_host_config = conf.get(per_host_config_option)
return (config_val, per_host_config or {})
class GalaxyStaticFiles(StaticFiles):
def __init__(
self, *, config: "Configuration", html: bool = False, check_dir: bool = True
) -> None:
self.config = config
self.directory, self.directory_per_host = get_static_from_config(
config, "static_dir", "static/"
)
super().__init__(
directory=self.directory, packages=None, html=html, check_dir=check_dir
)
def get_directories(
self, directory: PathLike = None, packages: typing.List[str] = None
) -> typing.List[PathLike]:
pass
async def check_config(self) -> None:
"""
Perform a one-off configuration check that GalaxyStaticFiles is actually
pointed at directories, so that we can raise loud errors rather than
just returning 404 responses.
"""
for directory in (self.directory, *self.directory_per_host.values()):
try:
stat_result = await anyio.to_thread.run_sync(os.stat, directory)
except FileNotFoundError:
raise RuntimeError(
f"StaticFiles directory '{directory}' does not exist."
)
if not (
stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)
):
raise RuntimeError(
f"StaticFiles path '{directory}' is not a directory."
)
async def get_response(self, path: str, scope: Scope) -> Response:
"""
Returns an HTTP response, given the incoming path, method and request headers.
"""
if scope["method"] not in ("GET", "HEAD"):
raise HTTPException(status_code=405)
headers = Headers(scope=scope)
try:
full_path, stat_result = await anyio.to_thread.run_sync(
self.lookup_path, path, headers.get("host")
)
except PermissionError:
raise HTTPException(status_code=401)
except OSError:
raise
if stat_result and stat.S_ISREG(stat_result.st_mode):
# We have a static file to serve.
return self.file_response(full_path, stat_result, scope)
raise HTTPException(status_code=404)
def lookup_path(
self, path: str, host: typing.Optional[str] = None
) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
directory = self.directory_per_host.get(host, self.directory)
full_path = os.path.realpath(os.path.join(directory, path))
directory = os.path.realpath(directory)
if os.path.commonprefix([full_path, directory]) != directory:
# Don't allow misbehaving clients to break out of the static files
# directory.
return "", None
try:
return full_path, os.stat(full_path)
except (FileNotFoundError, NotADirectoryError):
return "", None
class GalaxyCORSMiddleware(CORSMiddleware):
def __init__(self, *args, **kwds):
@@ -129,6 +225,16 @@ def initialize_fast_app(gx_wsgi_webapp, gx_app):
add_galaxy_middleware(app, gx_app)
add_request_id_middleware(app)
include_all_package_routers(app, 'galaxy.webapps.galaxy.api')
app.mount("/static", GalaxyStaticFiles(config=gx_app.config), name="static")
@app.get("/favicon.ico")
async def favicon():
return RedirectResponse(url="static/favicon.ico")
wsgi_handler = WSGIMiddleware(gx_wsgi_webapp)
app.mount('/', wsgi_handler)
if gx_app.config.galaxy_url_prefix != '/':
parent_app = FastAPI()
parent_app.mount(gx_app.config.galaxy_url_prefix, app=app)
return parent_app
return app
-9
View File
@@ -68,10 +68,6 @@ def app_factory(global_conf, load_app_kwds=None, **kwargs):
# Wrap the webapp in some useful middleware
if kwargs.get('middleware', True):
webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs)
if asbool(kwargs.get('static_enabled', True)):
webapp = wrap_if_allowed(webapp, app.application_stack, wrap_in_static,
args=(global_conf,),
kwargs=kwargs)
return webapp
@@ -121,10 +117,5 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf):
return app
def wrap_in_static(app, global_conf, **local_conf):
urlmap, _ = galaxy.webapps.base.webapp.build_url_map(app, global_conf, local_conf)
return urlmap
def uwsgi_app():
return galaxy.webapps.base.webapp.build_native_uwsgi_app(app_factory, "reports")
-9
View File
@@ -190,10 +190,6 @@ def app_factory(global_conf, load_app_kwds=None, **kwargs):
# Wrap the webapp in some useful middleware
if kwargs.get('middleware', True):
webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs)
if asbool(kwargs.get('static_enabled', True)):
webapp = wrap_if_allowed(webapp, app.application_stack, wrap_in_static,
args=(global_conf,),
kwargs=kwargs)
return webapp
@@ -263,11 +259,6 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf):
return app
def wrap_in_static(app, global_conf, **local_conf):
urlmap, _ = galaxy.webapps.base.webapp.build_url_map(app, global_conf, local_conf)
return urlmap
def _map_redirects(mapper):
"""
Add redirect to the Routes mapper and forward the received query string.