Merge branch 'master' of git://github.com/bbangert/buildbot
[buildbot.git] / buildbot / manhole.py
blobe5479b3813b2facd9b37404cfe0cba891cbe5a9f
2 import os.path
3 import binascii, base64
4 from twisted.python import log
5 from twisted.application import service, strports
6 from twisted.cred import checkers, portal
7 from twisted.conch import manhole, telnet, manhole_ssh, checkers as conchc
8 from twisted.conch.insults import insults
9 from twisted.internet import protocol
11 from buildbot.util import ComparableMixin
12 from zope.interface import implements # requires Twisted-2.0 or later
14 # makeTelnetProtocol and _TelnetRealm are for the TelnetManhole
16 class makeTelnetProtocol:
17 # this curries the 'portal' argument into a later call to
18 # TelnetTransport()
19 def __init__(self, portal):
20 self.portal = portal
22 def __call__(self):
23 auth = telnet.AuthenticatingTelnetProtocol
24 return telnet.TelnetTransport(auth, self.portal)
26 class _TelnetRealm:
27 implements(portal.IRealm)
29 def __init__(self, namespace_maker):
30 self.namespace_maker = namespace_maker
32 def requestAvatar(self, avatarId, *interfaces):
33 if telnet.ITelnetProtocol in interfaces:
34 namespace = self.namespace_maker()
35 p = telnet.TelnetBootstrapProtocol(insults.ServerProtocol,
36 manhole.ColoredManhole,
37 namespace)
38 return (telnet.ITelnetProtocol, p, lambda: None)
39 raise NotImplementedError()
42 class chainedProtocolFactory:
43 # this curries the 'namespace' argument into a later call to
44 # chainedProtocolFactory()
45 def __init__(self, namespace):
46 self.namespace = namespace
48 def __call__(self):
49 return insults.ServerProtocol(manhole.ColoredManhole, self.namespace)
51 class AuthorizedKeysChecker(conchc.SSHPublicKeyDatabase):
52 """Accept connections using SSH keys from a given file.
54 SSHPublicKeyDatabase takes the username that the prospective client has
55 requested and attempts to get a ~/.ssh/authorized_keys file for that
56 username. This requires root access, so it isn't as useful as you'd
57 like.
59 Instead, this subclass looks for keys in a single file, given as an
60 argument. This file is typically kept in the buildmaster's basedir. The
61 file should have 'ssh-dss ....' lines in it, just like authorized_keys.
62 """
64 def __init__(self, authorized_keys_file):
65 self.authorized_keys_file = os.path.expanduser(authorized_keys_file)
67 def checkKey(self, credentials):
68 f = open(self.authorized_keys_file)
69 for l in f.readlines():
70 l2 = l.split()
71 if len(l2) < 2:
72 continue
73 try:
74 if base64.decodestring(l2[1]) == credentials.blob:
75 return 1
76 except binascii.Error:
77 continue
78 return 0
81 class _BaseManhole(service.MultiService):
82 """This provides remote access to a python interpreter (a read/exec/print
83 loop) embedded in the buildmaster via an internal SSH server. This allows
84 detailed inspection of the buildmaster state. It is of most use to
85 buildbot developers. Connect to this by running an ssh client.
86 """
88 def __init__(self, port, checker, using_ssh=True):
89 """
90 @type port: string or int
91 @param port: what port should the Manhole listen on? This is a
92 strports specification string, like 'tcp:12345' or
93 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
94 simple tcp port.
96 @type checker: an object providing the
97 L{twisted.cred.checkers.ICredentialsChecker} interface
98 @param checker: if provided, this checker is used to authenticate the
99 client instead of using the username/password scheme. You must either
100 provide a username/password or a Checker. Some useful values are::
101 import twisted.cred.checkers as credc
102 import twisted.conch.checkers as conchc
103 c = credc.AllowAnonymousAccess # completely open
104 c = credc.FilePasswordDB(passwd_filename) # file of name:passwd
105 c = conchc.UNIXPasswordDatabase # getpwnam() (probably /etc/passwd)
107 @type using_ssh: bool
108 @param using_ssh: If True, accept SSH connections. If False, accept
109 regular unencrypted telnet connections.
112 # unfortunately, these don't work unless we're running as root
113 #c = credc.PluggableAuthenticationModulesChecker: PAM
114 #c = conchc.SSHPublicKeyDatabase() # ~/.ssh/authorized_keys
115 # and I can't get UNIXPasswordDatabase to work
117 service.MultiService.__init__(self)
118 if type(port) is int:
119 port = "tcp:%d" % port
120 self.port = port # for comparison later
121 self.checker = checker # to maybe compare later
123 def makeNamespace():
124 # close over 'self' so we can get access to .parent later
125 master = self.parent
126 namespace = {
127 'master': master,
128 'status': master.getStatus(),
130 return namespace
132 def makeProtocol():
133 namespace = makeNamespace()
134 p = insults.ServerProtocol(manhole.ColoredManhole, namespace)
135 return p
137 self.using_ssh = using_ssh
138 if using_ssh:
139 r = manhole_ssh.TerminalRealm()
140 r.chainedProtocolFactory = makeProtocol
141 p = portal.Portal(r, [self.checker])
142 f = manhole_ssh.ConchFactory(p)
143 else:
144 r = _TelnetRealm(makeNamespace)
145 p = portal.Portal(r, [self.checker])
146 f = protocol.ServerFactory()
147 f.protocol = makeTelnetProtocol(p)
148 s = strports.service(self.port, f)
149 s.setServiceParent(self)
152 def startService(self):
153 service.MultiService.startService(self)
154 if self.using_ssh:
155 via = "via SSH"
156 else:
157 via = "via telnet"
158 log.msg("Manhole listening %s on port %s" % (via, self.port))
161 class TelnetManhole(_BaseManhole, ComparableMixin):
162 """This Manhole accepts unencrypted (telnet) connections, and requires a
163 username and password authorize access. You are encouraged to use the
164 encrypted ssh-based manhole classes instead."""
166 compare_attrs = ["port", "username", "password"]
168 def __init__(self, port, username, password):
170 @type port: string or int
171 @param port: what port should the Manhole listen on? This is a
172 strports specification string, like 'tcp:12345' or
173 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
174 simple tcp port.
176 @param username:
177 @param password: username= and password= form a pair of strings to
178 use when authenticating the remote user.
181 self.username = username
182 self.password = password
184 c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
185 c.addUser(username, password)
187 _BaseManhole.__init__(self, port, c, using_ssh=False)
189 class PasswordManhole(_BaseManhole, ComparableMixin):
190 """This Manhole accepts encrypted (ssh) connections, and requires a
191 username and password to authorize access.
194 compare_attrs = ["port", "username", "password"]
196 def __init__(self, port, username, password):
198 @type port: string or int
199 @param port: what port should the Manhole listen on? This is a
200 strports specification string, like 'tcp:12345' or
201 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
202 simple tcp port.
204 @param username:
205 @param password: username= and password= form a pair of strings to
206 use when authenticating the remote user.
209 self.username = username
210 self.password = password
212 c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
213 c.addUser(username, password)
215 _BaseManhole.__init__(self, port, c)
217 class AuthorizedKeysManhole(_BaseManhole, ComparableMixin):
218 """This Manhole accepts ssh connections, and requires that the
219 prospective client have an ssh private key that matches one of the public
220 keys in our authorized_keys file. It is created with the name of a file
221 that contains the public keys that we will accept."""
223 compare_attrs = ["port", "keyfile"]
225 def __init__(self, port, keyfile):
227 @type port: string or int
228 @param port: what port should the Manhole listen on? This is a
229 strports specification string, like 'tcp:12345' or
230 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
231 simple tcp port.
233 @param keyfile: the name of a file (relative to the buildmaster's
234 basedir) that contains SSH public keys of authorized
235 users, one per line. This is the exact same format
236 as used by sshd in ~/.ssh/authorized_keys .
239 # TODO: expanduser this, and make it relative to the buildmaster's
240 # basedir
241 self.keyfile = keyfile
242 c = AuthorizedKeysChecker(keyfile)
243 _BaseManhole.__init__(self, port, c)
245 class ArbitraryCheckerManhole(_BaseManhole, ComparableMixin):
246 """This Manhole accepts ssh connections, but uses an arbitrary
247 user-supplied 'checker' object to perform authentication."""
249 compare_attrs = ["port", "checker"]
251 def __init__(self, port, checker):
253 @type port: string or int
254 @param port: what port should the Manhole listen on? This is a
255 strports specification string, like 'tcp:12345' or
256 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
257 simple tcp port.
259 @param checker: an instance of a twisted.cred 'checker' which will
260 perform authentication
263 _BaseManhole.__init__(self, port, checker)