Package buildbot :: Module manhole
[frames] | no frames]

Source Code for Module buildbot.manhole

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16   
 17  import os 
 18  import types 
 19  import binascii 
 20  import base64 
 21  from twisted.python import log 
 22  from twisted.application import service, strports 
 23  from twisted.cred import checkers, portal 
 24  from twisted.conch import manhole, telnet, manhole_ssh, checkers as conchc 
 25  from twisted.conch.insults import insults 
 26  from twisted.internet import protocol 
 27   
 28  from buildbot.util import ComparableMixin 
 29  from zope.interface import implements # requires Twisted-2.0 or later 
 30   
 31  # makeTelnetProtocol and _TelnetRealm are for the TelnetManhole 
 32   
33 -class makeTelnetProtocol:
34 # this curries the 'portal' argument into a later call to 35 # TelnetTransport()
36 - def __init__(self, portal):
37 self.portal = portal
38
39 - def __call__(self):
40 auth = telnet.AuthenticatingTelnetProtocol 41 return telnet.TelnetTransport(auth, self.portal)
42
43 -class _TelnetRealm:
44 implements(portal.IRealm) 45
46 - def __init__(self, namespace_maker):
47 self.namespace_maker = namespace_maker
48
49 - def requestAvatar(self, avatarId, *interfaces):
50 if telnet.ITelnetProtocol in interfaces: 51 namespace = self.namespace_maker() 52 p = telnet.TelnetBootstrapProtocol(insults.ServerProtocol, 53 manhole.ColoredManhole, 54 namespace) 55 return (telnet.ITelnetProtocol, p, lambda: None) 56 raise NotImplementedError()
57 58
59 -class chainedProtocolFactory:
60 # this curries the 'namespace' argument into a later call to 61 # chainedProtocolFactory()
62 - def __init__(self, namespace):
63 self.namespace = namespace
64
65 - def __call__(self):
66 return insults.ServerProtocol(manhole.ColoredManhole, self.namespace)
67
68 -class AuthorizedKeysChecker(conchc.SSHPublicKeyDatabase):
69 """Accept connections using SSH keys from a given file. 70 71 SSHPublicKeyDatabase takes the username that the prospective client has 72 requested and attempts to get a ~/.ssh/authorized_keys file for that 73 username. This requires root access, so it isn't as useful as you'd 74 like. 75 76 Instead, this subclass looks for keys in a single file, given as an 77 argument. This file is typically kept in the buildmaster's basedir. The 78 file should have 'ssh-dss ....' lines in it, just like authorized_keys. 79 """ 80
81 - def __init__(self, authorized_keys_file):
82 self.authorized_keys_file = os.path.expanduser(authorized_keys_file)
83
84 - def checkKey(self, credentials):
85 f = open(self.authorized_keys_file) 86 for l in f.readlines(): 87 l2 = l.split() 88 if len(l2) < 2: 89 continue 90 try: 91 if base64.decodestring(l2[1]) == credentials.blob: 92 return 1 93 except binascii.Error: 94 continue 95 return 0
96 97
98 -class _BaseManhole(service.MultiService):
99 """This provides remote access to a python interpreter (a read/exec/print 100 loop) embedded in the buildmaster via an internal SSH server. This allows 101 detailed inspection of the buildmaster state. It is of most use to 102 buildbot developers. Connect to this by running an ssh client. 103 """ 104
105 - def __init__(self, port, checker, using_ssh=True):
106 """ 107 @type port: string or int 108 @param port: what port should the Manhole listen on? This is a 109 strports specification string, like 'tcp:12345' or 110 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 111 simple tcp port. 112 113 @type checker: an object providing the 114 L{twisted.cred.checkers.ICredentialsChecker} interface 115 @param checker: if provided, this checker is used to authenticate the 116 client instead of using the username/password scheme. You must either 117 provide a username/password or a Checker. Some useful values are:: 118 import twisted.cred.checkers as credc 119 import twisted.conch.checkers as conchc 120 c = credc.AllowAnonymousAccess # completely open 121 c = credc.FilePasswordDB(passwd_filename) # file of name:passwd 122 c = conchc.UNIXPasswordDatabase # getpwnam() (probably /etc/passwd) 123 124 @type using_ssh: bool 125 @param using_ssh: If True, accept SSH connections. If False, accept 126 regular unencrypted telnet connections. 127 """ 128 129 # unfortunately, these don't work unless we're running as root 130 #c = credc.PluggableAuthenticationModulesChecker: PAM 131 #c = conchc.SSHPublicKeyDatabase() # ~/.ssh/authorized_keys 132 # and I can't get UNIXPasswordDatabase to work 133 134 service.MultiService.__init__(self) 135 if type(port) is int: 136 port = "tcp:%d" % port 137 self.port = port # for comparison later 138 self.checker = checker # to maybe compare later 139 140 def makeNamespace(): 141 master = self.master 142 namespace = { 143 'master': master, 144 'status': master.getStatus(), 145 'show': show, 146 } 147 return namespace
148 149 def makeProtocol(): 150 namespace = makeNamespace() 151 p = insults.ServerProtocol(manhole.ColoredManhole, namespace) 152 return p
153 154 self.using_ssh = using_ssh 155 if using_ssh: 156 r = manhole_ssh.TerminalRealm() 157 r.chainedProtocolFactory = makeProtocol 158 p = portal.Portal(r, [self.checker]) 159 f = manhole_ssh.ConchFactory(p) 160 else: 161 r = _TelnetRealm(makeNamespace) 162 p = portal.Portal(r, [self.checker]) 163 f = protocol.ServerFactory() 164 f.protocol = makeTelnetProtocol(p) 165 s = strports.service(self.port, f) 166 s.setServiceParent(self) 167 168
169 - def startService(self):
170 service.MultiService.startService(self) 171 if self.using_ssh: 172 via = "via SSH" 173 else: 174 via = "via telnet" 175 log.msg("Manhole listening %s on port %s" % (via, self.port))
176 177
178 -class TelnetManhole(_BaseManhole, ComparableMixin):
179 """This Manhole accepts unencrypted (telnet) connections, and requires a 180 username and password authorize access. You are encouraged to use the 181 encrypted ssh-based manhole classes instead.""" 182 183 compare_attrs = ["port", "username", "password"] 184
185 - def __init__(self, port, username, password):
186 """ 187 @type port: string or int 188 @param port: what port should the Manhole listen on? This is a 189 strports specification string, like 'tcp:12345' or 190 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 191 simple tcp port. 192 193 @param username: 194 @param password: username= and password= form a pair of strings to 195 use when authenticating the remote user. 196 """ 197 198 self.username = username 199 self.password = password 200 201 c = checkers.InMemoryUsernamePasswordDatabaseDontUse() 202 c.addUser(username, password) 203 204 _BaseManhole.__init__(self, port, c, using_ssh=False)
205
206 -class PasswordManhole(_BaseManhole, ComparableMixin):
207 """This Manhole accepts encrypted (ssh) connections, and requires a 208 username and password to authorize access. 209 """ 210 211 compare_attrs = ["port", "username", "password"] 212
213 - def __init__(self, port, username, password):
214 """ 215 @type port: string or int 216 @param port: what port should the Manhole listen on? This is a 217 strports specification string, like 'tcp:12345' or 218 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 219 simple tcp port. 220 221 @param username: 222 @param password: username= and password= form a pair of strings to 223 use when authenticating the remote user. 224 """ 225 226 self.username = username 227 self.password = password 228 229 c = checkers.InMemoryUsernamePasswordDatabaseDontUse() 230 c.addUser(username, password) 231 232 _BaseManhole.__init__(self, port, c)
233
234 -class AuthorizedKeysManhole(_BaseManhole, ComparableMixin):
235 """This Manhole accepts ssh connections, and requires that the 236 prospective client have an ssh private key that matches one of the public 237 keys in our authorized_keys file. It is created with the name of a file 238 that contains the public keys that we will accept.""" 239 240 compare_attrs = ["port", "keyfile"] 241
242 - def __init__(self, port, keyfile):
243 """ 244 @type port: string or int 245 @param port: what port should the Manhole listen on? This is a 246 strports specification string, like 'tcp:12345' or 247 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 248 simple tcp port. 249 250 @param keyfile: the name of a file (relative to the buildmaster's 251 basedir) that contains SSH public keys of authorized 252 users, one per line. This is the exact same format 253 as used by sshd in ~/.ssh/authorized_keys . 254 """ 255 256 # TODO: expanduser this, and make it relative to the buildmaster's 257 # basedir 258 self.keyfile = keyfile 259 c = AuthorizedKeysChecker(keyfile) 260 _BaseManhole.__init__(self, port, c)
261
262 -class ArbitraryCheckerManhole(_BaseManhole, ComparableMixin):
263 """This Manhole accepts ssh connections, but uses an arbitrary 264 user-supplied 'checker' object to perform authentication.""" 265 266 compare_attrs = ["port", "checker"] 267
268 - def __init__(self, port, checker):
269 """ 270 @type port: string or int 271 @param port: what port should the Manhole listen on? This is a 272 strports specification string, like 'tcp:12345' or 273 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 274 simple tcp port. 275 276 @param checker: an instance of a twisted.cred 'checker' which will 277 perform authentication 278 """ 279 280 _BaseManhole.__init__(self, port, checker)
281 282 ## utility functions for the manhole 283
284 -def show(x):
285 """Display the data attributes of an object in a readable format""" 286 print "data attributes of %r" % (x,) 287 names = dir(x) 288 maxlen = max([0] + [len(n) for n in names]) 289 for k in names: 290 v = getattr(x,k) 291 t = type(v) 292 if t == types.MethodType: continue 293 if k[:2] == '__' and k[-2:] == '__': continue 294 if t is types.StringType or t is types.UnicodeType: 295 if len(v) > 80 - maxlen - 5: 296 v = `v[:80 - maxlen - 5]` + "..." 297 elif t in (types.IntType, types.NoneType): 298 v = str(v) 299 elif v in (types.ListType, types.TupleType, types.DictType): 300 v = "%s (%d elements)" % (v, len(v)) 301 else: 302 v = str(t) 303 print "%*s : %s" % (maxlen, k, v) 304 return x
305