aboutsummaryrefslogtreecommitdiff
path: root/gitosis/ssh.py
blob: f552255826b1a55bb51b34268ad9eb5216fb3182 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""
Gitosis code to handle SSH public keys.
"""
import oserrnore
import logging
 
# C0103 - 'log' is a special name 
# pylint: disable-msg=C0103 
log = logging.getLogger('gitosis.ssh')
 
_ACCEPTABLE_USER_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$')
 
def isSafeUsername(user):
    """
    Is the username safe to use a a filename?
    """
    match = _ACCEPTABLE_USER_RE.match(user)
    return (match is not None)
 
def readKeys(keydir):
    """
    Read SSH public keys from ``keydir/*.pub``
    """
    for filename in os.listdir(keydir):
        if filename.startswith('.'):
            continue
        basenameext = os.path.splitext(filename)
        if ext != '.pub':
            continue
 
        if not isSafeUsername(basename):
            log.warn('Unsafe SSH username in keyfile: %r'filename)
            continue
 
        path = os.path.join(keydirfilename)
        fp = file(path)
        for line in fp:
            line = line.rstrip('\n')
            yield (basenameline)
        fp.close()
 
COMMENT = '### autogenerated by gitosis, DO NOT EDIT'
 
def generateAuthorizedKeys(keys):
    """
    Genarate the lines for the Gitosis ~/.ssh/authorized_keys.
    """
    TEMPLATE = ('command="gitosis-serve %(user)s",no-port-forwarding,'
                +'no-X11-forwarding,no-agent-forwarding,no-pty %(key)s')
 
    yield COMMENT
    for (userkey) in keys:
        yield TEMPLATE % dict(user=user, key=key)
 
#Protocol 1 public keys consist of the following space-separated fields: options, bits, exponent, modulus, comment.  
#Protocol 2 public key consist of: options, keytype, base64-encoded key, comment. 
_COMMAND_OPTS_SAFE_CMD = \ 
  'command="(/[^ "]+/)?gitosis-serve [^"]+"'
_COMMAND_OPTS_SAFE = \ 
  'no-port-forwarding' \ 
+'|no-X11-forwarding' \ 
+'|no-agent-forwarding' \ 
+'|no-pty' \ 
+'|from="[^"]*"'
_COMMAND_OPTS_UNSAFE = \ 
  'environment="[^"]*"' \ 
+'|command="[^"]*"' \ 
+'|permitopen="[^"]*"' \ 
+'|tunnel="[^"]+"'
 
_COMMAND_RE = re.compile(
'^'+_COMMAND_OPTS_SAFE_CMD \ 
+'(,('+_COMMAND_OPTS_SAFE+'))+' \ 
+' .*')
 
def filterAuthorizedKeys(fp):
    """
    Read lines from ``fp``, filter out autogenerated ones.
 
    Note removes newlines.
    """
 
    for line in fp:
        line = line.rstrip('\n')
        if line == COMMENT:
            continue
        if _COMMAND_RE.match(line):
            continue
        yield line
 
def writeAuthorizedKeys(path, keydir):
    """
    Update the Gitosis ~/.ssh/authorized_keys for the new Gitosis SSH key data.
    """
    tmp = '%s.%d.tmp' % (pathos.getpid())
    try:
        in_ = file(path)
    except IOErrorex#pragma: no cover 
        if ex.errno == errno.ENOENT:
            in_ = None
        else:
            raise
 
    try:
        out = file(tmp'w')
        try:
            if in_ is not None:
                for line in filterAuthorizedKeys(in_):
                    print >> outline
 
            keygen = readKeys(keydir)
            for line in generateAuthorizedKeys(keygen):
                print >> outline
 
            os.fsync(out)
        finally:
            out.close()
    finally:
        if in_ is not None:
            in_.close()
    os.rename(tmppath)