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