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 9cd5398
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 20 deletions.
97 changes: 78 additions & 19 deletions klaus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,55 @@
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 + '.git',
endpoint=pattern + '_git2root',
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 +104,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 @@ -84,6 +135,17 @@ def setup_routes(self):
view_func=getattr(views, endpoint)
)
# fmt: on
if self.use_smarthttp:
self.add_url_rule(
'/<repo>.git/<path:path>',
view_func=views.smarthttp,
methods=['GET', 'POST'],
)
self.add_url_rule(
'/<path:namespace>/<repo>.git/<path:path>',
view_func=views.smarthttp,
methods=['GET', 'POST'],
)

def should_use_ctags(self, git_repo, git_commit):
if self.ctags_policy == "none":
Expand Down Expand Up @@ -169,23 +231,20 @@ def make_app(
use_smarthttp,
ctags_policy,
)
app.wsgi_app = utils.ChainedApps(
app,
KlausRedirects(app.valid_repos),
)
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,27 +265,27 @@ def make_app(
)
if unauthenticated_push:
# DANGER ZONE: Don't require authentication for push'ing
app.wsgi_app = dulwich_wrapped_app
app.smarthttp = dulwich_app
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
app.smarthttp = dulwich_app
else:
# Web interface isn't already secured. Require authentication for push'ing.
app.wsgi_app = httpauth.DigestFileHttpAuthMiddleware(
app.smarthttp = 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,
app.smarthttp = httpauth.AlwaysFailingAuthMiddleware(
wsgi_app=dulwich_app,
routes=[PATTERN],
)

Expand Down
49 changes: 49 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,54 @@ 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):
self.apps = apps

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
first_response = 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 first_response is None:
first_response = response

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 first_response:
return first_response(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
5 changes: 4 additions & 1 deletion klaus/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,7 @@ def get_response(self):


def smarthttp(*args, **kwargs):
raise ValueError("this endpoint shouldn't be reachable")
if not current_app.use_smarthttp or not current_app.smarthttp:
raise NotFound()

return current_app.smarthttp

0 comments on commit 9cd5398

Please sign in to comment.