Eclectic Media Git klaus / 59e755b
add HTTP Basic Auth for Git Smart HTTP > klaus --help [...] Git Smart HTTP: -s, --smarthttp enable smart HTTP serving --htpasswd=FILE use credentials from FILE If you don't specify a htpasswd FILE Git Smart HTTP is read-only. posativ 8 years ago
3 changed file(s) with 93 addition(s) and 20 deletion(s). Raw diff Collapse all Expand all
00 #!/usr/bin/env python
11
22 from sys import exit
3 from os.path import isdir
4 from optparse import make_option, OptionParser, SUPPRESS_HELP
3 from os.path import isfile
4 from optparse import make_option, OptionParser, SUPPRESS_HELP, OptionGroup
55
66 from dulwich.repo import Repo
77 from dulwich.errors import NotGitRepository
1919 options = [
2020 make_option("-i", default="127.0.0.1", dest="interface",
2121 help="bind to host/address (default: 127.0.0.1)"),
22 make_option("-p", "--port", type=int, default=8080, dest="port",
22 make_option("-p", type=int, default=8080, dest="port",
2323 help="webserver port (default: 8080)"),
2424 make_option("--prefix", type=str, default='/', dest="prefix",
2525 help="serve on given sub uri"),
26 make_option("-s", "--smarthttp", action="store_true", dest="smarthttp",
27 help="serve git repositories the smart way", default=False),
2826 make_option("-r", "--use-reloader", action="store_true", dest="reloader",
2927 help=SUPPRESS_HELP, default=False),
3028 make_option("--debug", action="store_true", dest="debug",
3230 ]
3331
3432 parser = OptionParser(option_list=options, usage=usage, epilog="klaus, a simple Git viewer")
33
34 # smart HTTP
35 grp = OptionGroup(parser, "Git Smart HTTP")
36 grp.add_option("-s", "--smarthttp", action="store_true", dest="smarthttp",
37 help="enable smart HTTP serving", default=False)
38 grp.add_option("--htpasswd", dest="htpasswd", metavar="FILE",
39 help="use credentials from FILE")
40
41 parser.add_option_group(grp)
3542 (options, args) = parser.parse_args()
3643
3744 if len(args) == 0:
4552 print '%r: Not a git repository' % path
4653 args.remove(path)
4754
48 app = make_app(args, options.prefix, options.smarthttp)
55 if options.htpasswd and not isfile(options.htpasswd):
56 print '%r: No such file' % options.htpasswd
57 exit(1)
58
59 app = make_app(args, options.prefix, options.smarthttp, options.htpasswd)
4960
5061 if options.debug:
5162 from werkzeug.debug import DebuggedApplication
104104 return self.wsgi_app(environ, start_response)
105105
106106
107 def make_app(repos, prefix='/', smartgit=False):
107 def make_app(repos, prefix='/', smartgit=False, htpasswd=None):
108108
109109 repos = dict(
110110 (repo.rstrip(os.sep).split(os.sep)[-1].replace('.git', ''), repo)
123123 app.jinja_env.filters['shorten_author'] = extract_author_name
124124
125125 if smartgit:
126 app = http.make_app(app, repos)
126 app = http.make_app(app, repos, htpasswd=htpasswd)
127127
128128 app.wsgi_app = SubUri(app.wsgi_app, prefix=prefix)
129129 app = SharedDataMiddleware(app, {
00 # -*- encoding: utf-8 -*-
1
2 import re
3 from crypt import crypt
4
5 from werkzeug.wrappers import Request, Response
16
27 from dulwich.repo import Repo
38 from dulwich.server import DictBackend
4 from dulwich.web import HTTPGitRequest
5 from dulwich.web import HTTPGitApplication, GunzipFilter
9 from dulwich.web import handle_service_request
10 from dulwich.web import HTTPGitRequest, HTTPGitApplication
11 from dulwich.web import LimitedInputFilter, GunzipFilter
12
13
14 def authenticated(func, auth={}):
15 """This wraps a function around HTTP Basic Authentication using
16 a dictionary of {username: lambda passwd: True or False} to verify
17 the password using the common htpasswd file.
18
19 Unfortunately we can not use werkzeug's Response object since it
20 would result in a major rewrite of the dulwich/web.py module."""
21
22 def dec(req, backend, mat, *args, **kwargs):
23 """This decorater function will send an authenticate header, if none
24 is present and denies access, if HTTP Basic Auth failed."""
25
26 service = mat.group().lstrip('/')
27 if not req.authorization:
28 req.respond(
29 status='401 Unauthorized',
30 content_type='application/x-%s-result' % service,
31 headers=[('WWW-Authenticate', 'Basic realm="Git Smart HTTP"')]
32 )
33 return ''
34 else:
35 user, passwd = req.authorization.username, req.authorization.password
36 if not auth.get(user, lambda x: False)(passwd):
37 req.respond(
38 status='403 Forbidden',
39 content_type='application/x-%s-result' % service
40 )
41 return ''
42 return func(req, backend, mat, *args, **kwargs)
43 return dec
44
45
46 class SmartGitRequest(HTTPGitRequest, Request):
47 """We use werkzeug's Request object to parse the authorization headers. Due
48 the design of Dulwich's we can not use a native Request object."""
49
50 def __init__(self, environ, start_response, dumb=False, handlers=None):
51 Request.__init__(self, environ)
52 HTTPGitRequest.__init__(self, environ, start_response, dumb, handlers)
653
754
855 class AuthenticatedGitApplication(HTTPGitApplication):
9 """Add basic HTTP authentication to ``git push``."""
56 """Add basic HTTP authentication to ``git push``; pathced to pass
57 unknown urls to Klaus.
1058
11 def __init__(self, app, backend, dumb=False, handlers=None):
12 super(AuthenticatedGitApplication, self).__init__(backend, dumb, handlers)
59 Instead of the common URL scheme http://foo.bar/repo.git we can
60 safely run klaus and git-http-backend in parallel. Thus to clone
61 or push a repository, just use http://foo.bar/repo/ as URL.
62 """
63 def __init__(self, app, backend, htpasswd):
64 super(AuthenticatedGitApplication, self).__init__(backend)
65
1366 self.app = app
67 self.auth = {}
68
69 if htpasswd:
70 for line in open(htpasswd, 'r').readlines():
71 username, pwhash = line.rstrip().split(':')
72 self.auth[username] = lambda passwd: crypt(passwd, pwhash) == pwhash
73
74 key = ('POST', re.compile('/git-receive-pack$'))
75 self.services[key] = authenticated(handle_service_request, self.auth)
1476
1577 def __call__(self, environ, start_response):
1678 path = environ['PATH_INFO']
1779 method = environ['REQUEST_METHOD']
18 req = HTTPGitRequest(environ, start_response, dumb=self.dumb,
19 handlers=self.handlers)
80 req = SmartGitRequest(environ, start_response, dumb=self.dumb,
81 handlers=self.handlers)
2082 # environ['QUERY_STRING'] has qs args
21 handler = None
2283 for smethod, spath in self.services.iterkeys():
2384 if smethod != method:
2485 continue
2687 if mat:
2788 handler = self.services[smethod, spath]
2889 break
29 if handler is None:
90 else:
3091 return self.app(environ, start_response)
3192 return handler(req, self.backend, mat)
3293
3394
34 def make_app(app, repos):
95 def make_app(app, repos, htpasswd):
3596
3697 # DictBackend uses keys with a leading slash
3798 backend = DictBackend(dict(('/'+k, Repo(v)) for k, v in repos.iteritems()))
3899 wsgi_app = app.wsgi_app
39100
40 app = AuthenticatedGitApplication(app, backend, dumb=False, handlers=None)
41 app = GunzipFilter(app)
101 app = AuthenticatedGitApplication(app, backend, htpasswd)
102 app.wsgi_app = wsgi_app
103 app = GunzipFilter(LimitedInputFilter(app))
42104 app.wsgi_app = wsgi_app
43105
44106 return app