Skip to content

Commit

Permalink
redirect/handle old-style index and SmartHTTP requests
Browse files Browse the repository at this point in the history
  • Loading branch information
s-ol committed Aug 11, 2022
1 parent 1befb64 commit a30105b
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 21 deletions.
85 changes: 65 additions & 20 deletions klaus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 + '/<path:path>',
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"],
Expand Down Expand Up @@ -55,6 +99,8 @@ def create_jinja_environment(self):
return env

def setup_routes(self):
redirects = {}

# fmt: off
for endpoint, rule in [
('repo_list', '/'),
Expand Down Expand Up @@ -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
Expand All @@ -206,30 +244,37 @@ 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:
# No .htdigest file given. Disable push-ing. Semantically we should
# 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
Expand Down
46 changes: 46 additions & 0 deletions klaus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import binascii
import os
import re
import sys
import time
import datetime
import mimetypes
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion klaus/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,4 @@ def get_response(self):


def smarthttp(*args, **kwargs):
raise ValueError("this endpoint shouldn't be reachable")
raise NotFound()

0 comments on commit a30105b

Please sign in to comment.