From a30105b2cd2760c81cf3f51688add1ea5b360057 Mon Sep 17 00:00:00 2001 From: s-ol Date: Sun, 26 Jun 2022 18:54:36 +0200 Subject: [PATCH] redirect/handle old-style index and SmartHTTP requests --- klaus/__init__.py | 85 ++++++++++++++++++++++++++++++++++++----------- klaus/utils.py | 46 +++++++++++++++++++++++++ klaus/views.py | 2 +- 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/klaus/__init__.py b/klaus/__init__.py index e85c9e3d..6ec1a6cd 100644 --- a/klaus/__init__.py +++ b/klaus/__init__.py @@ -15,6 +15,50 @@ KLAUS_VERSION = utils.guess_git_revision() or "1.5.2" +class KlausRedirects(flask.Flask): + def __init__(self, repos): + flask.Flask.__init__(self, __name__) + + for namespaced_name in repos: + self.setup_redirects('/' + namespaced_name) + if namespaced_name.count('/') == 1: + self.setup_redirects('/' + namespaced_name, '/~' + namespaced_name) + + def query_str(self): + query = flask.request.query_string.decode() + if len(query) > 0: + return '?' + query + + return '' + + def setup_redirects(self, route, pattern=None): + if not pattern: + pattern = route + + def redirect_root(): + return flask.redirect(route + '/-/' + self.query_str(), 301) + def redirect_rest(path): + return flask.redirect(route + '/-/' + path + self.query_str(), 301) + def redirect_git(): + return flask.redirect(route + '.git/info/refs' + self.query_str(), 301) + + self.add_url_rule( + pattern + '/', + endpoint=pattern + '_root', + view_func=redirect_root, + ) + self.add_url_rule( + pattern + '/', + endpoint=pattern + '_rest', + view_func=redirect_rest, + ) + self.add_url_rule( + pattern + '/info/refs', + endpoint=pattern + '_git', + view_func=redirect_git, + ) + + class Klaus(flask.Flask): jinja_options = { "extensions": [] if jinja2_autoescape_builtin else ["jinja2.ext.autoescape"], @@ -55,6 +99,8 @@ def create_jinja_environment(self): return env def setup_routes(self): + redirects = {} + # fmt: off for endpoint, rule in [ ('repo_list', '/'), @@ -169,23 +215,15 @@ def make_app( use_smarthttp, ctags_policy, ) - app.wsgi_app = utils.ProxyFix(app.wsgi_app) if use_smarthttp: # `path -> Repo` mapping for Dulwich's web support - dulwich_backend = dulwich.server.DictBackend( - { - "/" + namespaced_name + '.git': repo - for namespaced_name, repo in app.valid_repos.items() - } - ) - # Dulwich takes care of all Git related requests/URLs - # and passes through everything else to klaus - dulwich_wrapped_app = dulwich.web.make_wsgi_chain( - backend=dulwich_backend, - fallback_app=app.wsgi_app, - ) - dulwich_wrapped_app = utils.ProxyFix(dulwich_wrapped_app) + dulwich_repos = {} + for namespaced_name, repo in app.valid_repos.items(): + dulwich_repos["/" + namespaced_name + '.git'] = repo + + dulwich_backend = dulwich.server.DictBackend(dulwich_repos) + dulwich_app = dulwich.web.make_wsgi_chain(backend=dulwich_backend) # `receive-pack` is requested by the "client" on a push # (the "server" is asked to *receive* packs), i.e. we need to secure @@ -206,18 +244,18 @@ def make_app( ) if unauthenticated_push: # DANGER ZONE: Don't require authentication for push'ing - app.wsgi_app = dulwich_wrapped_app + pass elif htdigest_file and not disable_push: # .htdigest file given. Use it to read the push-er credentials from. if require_browser_auth: # No need to secure push'ing if we already require HTTP auth # for all of the Web interface. - app.wsgi_app = dulwich_wrapped_app + dulwich_app = dulwich_app else: # Web interface isn't already secured. Require authentication for push'ing. - app.wsgi_app = httpauth.DigestFileHttpAuthMiddleware( + dulwich_app = httpauth.DigestFileHttpAuthMiddleware( htdigest_file, - wsgi_app=dulwich_wrapped_app, + wsgi_app=dulwich_app, routes=[PATTERN], ) else: @@ -225,11 +263,18 @@ def make_app( # use HTTP 403 here but since that results in freaky error messages # (see above) we keep asking for authentication (401) instead. # Git will print a nice error message after a few tries. - app.wsgi_app = httpauth.AlwaysFailingAuthMiddleware( - wsgi_app=dulwich_wrapped_app, + dulwich_app = httpauth.AlwaysFailingAuthMiddleware( + wsgi_app=dulwich_app, routes=[PATTERN], ) + app.wsgi_app = utils.ChainedApps( + app, + KlausRedirects(app.valid_repos), + fallback=dulwich_app, + ) + app.wsgi_app = utils.ProxyFix(app.wsgi_app) + if require_browser_auth: app.wsgi_app = httpauth.DigestFileHttpAuthMiddleware( htdigest_file, wsgi_app=app.wsgi_app diff --git a/klaus/utils.py b/klaus/utils.py index 4eae6c52..b73ee5a3 100644 --- a/klaus/utils.py +++ b/klaus/utils.py @@ -2,6 +2,7 @@ import binascii import os import re +import sys import time import datetime import mimetypes @@ -103,6 +104,51 @@ def __call__(self, environ, start_response): return self.app(environ, start_response) +class ChainedApps(object): + """WSGI middleware to chain two or more Flask apps. + + The request is passed to the next app if a response has a 404 status.""" + + def __init__(self, *apps, fallback=None): + self.apps = apps + self.fallback = fallback + + def __call__(self, environ, start_response): + # this method is almost verbatim flask.Flask.wsgi_app(), + # except for the for/continue statements. + for app in self.apps: + ctx = app.request_context(environ) + error = None + try: + try: + ctx.push() + response = app.full_dispatch_request() + except Exception as e: + error = e + response = app.handle_exception(e) + except: # noqa: B001 + error = sys.exc_info()[1] + raise + + if response.status_code == 404: + # pass through 404 codes + continue + + return response(environ, start_response) + finally: + if "werkzeug.debug.preserve_context" in environ: + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_request.get()) + + if error is not None and app.should_ignore_error(error): + error = None + + ctx.pop(error) + + if self.fallback: + return self.fallback(environ, start_response) + + def timesince(when, now=time.time): """Return the difference between `when` and `now` in human readable form.""" return naturaltime(now() - when) diff --git a/klaus/views.py b/klaus/views.py index a94d40e4..32091d71 100644 --- a/klaus/views.py +++ b/klaus/views.py @@ -539,4 +539,4 @@ def get_response(self): def smarthttp(*args, **kwargs): - raise ValueError("this endpoint shouldn't be reachable") + raise NotFound()