1#!/usr/bin/env python3
2#
3# Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
4#
5# Author(s):
6# Thomas Arendsen Hein <thomas@intevation.de>
7#
8# This software may be used and distributed according to the terms of the
9# GNU General Public License version 2 or any later version.
10
11"""
12hg-ssh - a wrapper for ssh access to a limited set of mercurial repos
13
14To be used in ~/.ssh/authorized_keys with the "command" option, see sshd(8):
15command="hg-ssh path/to/repo1 /path/to/repo2 ~/repo3 ~user/repo4" ssh-dss ...
16(probably together with these other useful options:
17 no-port-forwarding,no-X11-forwarding,no-agent-forwarding)
18
19This allows pull/push over ssh from/to the repositories given as arguments.
20
21If all your repositories are subdirectories of a common directory, you can
22allow shorter paths with:
23command="cd path/to/my/repositories && hg-ssh repo1 subdir/repo2"
24
25You can use pattern matching of your normal shell, e.g.:
26command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}"
27
28You can also add a --read-only flag to allow read-only access to a key, e.g.:
29command="hg-ssh --read-only repos/*"
30"""
31from __future__ import absolute_import
32
33import os
34import re
35import shlex
36import sys
37
38# enable importing on demand to reduce startup time
39import hgdemandimport
40
41hgdemandimport.enable()
42
43from mercurial import (
44    dispatch,
45    pycompat,
46    ui as uimod,
47)
48
49
50def main():
51    # Prevent insertion/deletion of CRs
52    dispatch.initstdio()
53
54    cwd = os.getcwd()
55    if os.name == 'nt':
56        # os.getcwd() is inconsistent on the capitalization of the drive
57        # letter, so adjust it. see https://bugs.python.org/issue40368
58        if re.match('^[a-z]:', cwd):
59            cwd = cwd[0:1].upper() + cwd[1:]
60
61    readonly = False
62    args = sys.argv[1:]
63    while len(args):
64        if args[0] == '--read-only':
65            readonly = True
66            args.pop(0)
67        else:
68            break
69    allowed_paths = [
70        os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
71        for path in args
72    ]
73    orig_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '?')
74    try:
75        cmdargv = shlex.split(orig_cmd)
76    except ValueError as e:
77        sys.stderr.write('Illegal command "%s": %s\n' % (orig_cmd, e))
78        sys.exit(255)
79
80    if cmdargv[:2] == ['hg', '-R'] and cmdargv[3:] == ['serve', '--stdio']:
81        path = cmdargv[2]
82        repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
83        if repo in allowed_paths:
84            cmd = [b'-R', pycompat.fsencode(repo), b'serve', b'--stdio']
85            req = dispatch.request(cmd)
86            if readonly:
87                if not req.ui:
88                    req.ui = uimod.ui.load()
89                req.ui.setconfig(
90                    b'hooks',
91                    b'pretxnopen.hg-ssh',
92                    b'python:__main__.rejectpush',
93                    b'hg-ssh',
94                )
95                req.ui.setconfig(
96                    b'hooks',
97                    b'prepushkey.hg-ssh',
98                    b'python:__main__.rejectpush',
99                    b'hg-ssh',
100                )
101            dispatch.dispatch(req)
102        else:
103            sys.stderr.write('Illegal repository "%s"\n' % repo)
104            sys.exit(255)
105    else:
106        sys.stderr.write('Illegal command "%s"\n' % orig_cmd)
107        sys.exit(255)
108
109
110def rejectpush(ui, **kwargs):
111    ui.warn((b"Permission denied\n"))
112    # mercurial hooks use unix process conventions for hook return values
113    # so a truthy return means failure
114    return True
115
116
117if __name__ == '__main__':
118    main()
119