shell

Git shell for sbi.re
Log | Files | Refs | README

shell.py (5656B)


1#!/bin/python3
2
3import sys
4import os
5import subprocess
6import shlex
7import cmd
8from glob import glob
9from os.path import basename, splitext, isfile, isdir, join
10
11import policy
12from policy import BASEDIR
13
14USERS    = os.path.expanduser('~/users')
15TEMPLATE = os.path.expanduser('~/template')
16REPOS    = os.path.expanduser('~/repositories')
17
18# STAGIT CONFIG
19HTMLDIR = '/var/lib/www/git'
20CACHEFILE = '.htmlcache'
21COMMITS = 100
22BASEURL = 'https://git.sbi.re'
23
24def die(msg, status=1):
25    sys.stderr.write(msg)
26    sys.stderr.write('\n')
27    sys.stderr.flush()
28    sys.stdout.flush()
29    sys.exit(status)
30
31def git(*args):
32    return (subprocess
33             .run(['git', *args], capture_output=True, encoding='utf-8')
34             .stdout)
35
36def stagit(repo):
37    path = join(REPOS, repo)
38
39    outdir = join(HTMLDIR, repo)
40    os.makedirs(outdir, exist_ok=True)
41
42    # change current dir and generate html
43    os.chdir(outdir)
44    subprocess.run(['stagit', '-l', str(COMMITS), '-u', BASEURL , path])
45
46    # render index again
47    with open(join(HTMLDIR, 'index.html'), 'w') as index:
48        files = sorted(glob(join(REPOS, '*')))
49        subprocess.run(['stagit-index', *files], stdout=index)
50
51
52def do_git_receive_pack(user, cmd, args):
53    if len(args) < 1:
54        die('error: git-receive-pack must be given an argument')
55    repo = args[-1]
56    path = os.path.join(policy.BASEDIR, repo)
57    perm = policy.get_perm(user, repo)
58
59    if not policy.WRITE & perm:
60        die('error: sorry %s, you are not allowed to push to %s' % (user, repo))
61
62    if not os.path.isdir(path):
63        if not policy.CREATE & perm:
64            die('error: sorry %s, %s does not exist and you are not allowed to create it' % (user, repo))
65        subprocess.run(
66            [ 'git', 'init', '--bare', 
67              '--template=%s' % TEMPLATE,
68              path,
69            ],
70            stdout=sys.stderr.buffer,
71        )
72        sys.stderr.write('info: created new repo %s' % repo)
73
74    os.execvp('git-receive-pack', ['git-receive-pack', *args[:-1], path])
75
76
77def do_git_upload_pack(user, cmd, args):
78    if len(args) < 1:
79        die('error: git-upload-pack must be given an argument')
80    repo = args[-1]
81    path = os.path.join(policy.BASEDIR, repo)
82    perm = policy.get_perm(user, repo)
83
84    if not policy.READ & perm:
85        die('error: sorry %s, you are not allowed to pull from %s' % (user, repo))
86
87    if not os.path.isdir(path):
88        die('error: sorry %s, %s does not exist' % (user, repo))
89
90    os.execvp('git-upload-pack', ['git-upload-pack', *args[:-1], path])
91
92
93def do_git_upload_archive(user, cmd, args):
94    if len(args) < 1:
95        die('error: git-upload-archive must be given an argument')
96    repo = args[-1]
97    path = os.path.join(policy.BASEDIR, repo)
98    perm = policy.get_perm(user, repo)
99
100    if not policy.READ & perm:
101        die('error: sorry %s, you are not allowed to retrieve archive from %s' % (user, repo))
102
103    if not os.path.isdir(path):
104        die('error: sorry %s, %s does not exist' % (user, repo))
105
106    os.execvp('git-upload-archive', ['git-upload-archive', *args[:-1], path])
107
108def do_keys(user, cmd, args):
109    """List authorized SSH keys for current user"""
110    sys.stdout.write('You currently allow the following SSH keys:\n')
111    for path in os.listdir(os.path.join(USERS, user)):
112        abs_path = join(USERS, user, path)
113        if not isfile(abs_path): continue
114        with open(abs_path) as key:
115            key_name, _ = splitext(path)
116            sys.stdout.write('%s: %s\n' % (key_name, key.read()))
117
118def do_desc(user, cmd, args):
119    """Get/set description of repository"""
120    if len(args) < 1:
121        do_help(user, 'help', [])
122        return
123    repo = args[0]
124    path = os.path.join(policy.BASEDIR, repo)
125    perm = policy.get_perm(user, repo)
126
127    if not policy.READ & perm:
128        sys.stderr.write('Sorry %s, you are not allowed to access %s' % (user, repo))
129        return
130
131    if len(args) == 2:
132        with open(join(path, 'description'), 'w') as desc:
133            desc.write('%s\n' % args[1])
134        stagit(repo)
135    else:
136      with open(join(path, 'description'), 'r') as desc:
137          sys.stdout.write(desc.read())
138
139def do_help(user, cmd, args):
140    """Print all available commands"""
141    for key, func in SHELL_COMMANDS.items():
142        sys.stdout.write('%s:\n  %s\n' % (key, func.__doc__))
143
144def do_unknown(user, cmd, args):
145    die('Sorry %s, I don\'t know the command `%s` :(.' % (user, cmd), status=1)
146
147
148SHELL_COMMANDS = {
149    'keys': do_keys,
150    'desc': do_desc,
151    'help': do_help,
152}
153
154COMMANDS = {
155    **SHELL_COMMANDS,
156    'git-receive-pack':   do_git_receive_pack,
157    'git-upload-pack':    do_git_upload_pack,
158    'git-upload-archive': do_git_upload_archive,
159}
160
161class SbireShell(cmd.Cmd):
162    intro  = 'Hello %s, and welcome to the SBIRE shell.\nType `help` to list available commands.\n'
163    prompt = 'λ '
164    file   = None
165
166    def __init__(self, user):
167        super().__init__()
168        self.user  = user
169        self.intro = SbireShell.intro % user
170
171    def onecmd(self, str):
172        cmd, *args = shlex.split(str)
173        return SHELL_COMMANDS.get(cmd, do_unknown)(self.user, cmd, args)
174
175def main():
176    if len(sys.argv) != 2:
177        die('usage: shell.py USER')
178    user = sys.argv[1]
179
180    ssh_orig = os.getenv('SSH_ORIGINAL_COMMAND')
181
182    # no original command provided, launching interactive shell
183    if not ssh_orig:
184        SbireShell(user).cmdloop()
185        # die('error: SSH_ORIGINAL_COMMAND is empty')
186    cmd, *args = shlex.split(ssh_orig)
187    COMMANDS.get(cmd, do_unknown)(user, cmd, args)
188
189    sys.stdout.flush()
190    sys.exit(0)
191
192
193if __name__ == '__main__':
194    main()