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 # close over 'self' so we can get access to .parent later 142 master = self.parent 143 namespace = { 144 'master': master, 145 'status': master.getStatus(), 146 'show': show, 147 } 148 return namespace
149 150 def makeProtocol(): 151 namespace = makeNamespace() 152 p = insults.ServerProtocol(manhole.ColoredManhole, namespace) 153 return p
154 155 self.using_ssh = using_ssh 156 if using_ssh: 157 r = manhole_ssh.TerminalRealm() 158 r.chainedProtocolFactory = makeProtocol 159 p = portal.Portal(r, [self.checker]) 160 f = manhole_ssh.ConchFactory(p) 161 else: 162 r = _TelnetRealm(makeNamespace) 163 p = portal.Portal(r, [self.checker]) 164 f = protocol.ServerFactory() 165 f.protocol = makeTelnetProtocol(p) 166 s = strports.service(self.port, f) 167 s.setServiceParent(self) 168 169
170 - def startService(self):
171 service.MultiService.startService(self) 172 if self.using_ssh: 173 via = "via SSH" 174 else: 175 via = "via telnet" 176 log.msg("Manhole listening %s on port %s" % (via, self.port))
177 178
179 -class TelnetManhole(_BaseManhole, ComparableMixin):
180 """This Manhole accepts unencrypted (telnet) connections, and requires a 181 username and password authorize access. You are encouraged to use the 182 encrypted ssh-based manhole classes instead.""" 183 184 compare_attrs = ["port", "username", "password"] 185
186 - def __init__(self, port, username, password):
187 """ 188 @type port: string or int 189 @param port: what port should the Manhole listen on? This is a 190 strports specification string, like 'tcp:12345' or 191 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 192 simple tcp port. 193 194 @param username: 195 @param password: username= and password= form a pair of strings to 196 use when authenticating the remote user. 197 """ 198 199 self.username = username 200 self.password = password 201 202 c = checkers.InMemoryUsernamePasswordDatabaseDontUse() 203 c.addUser(username, password) 204 205 _BaseManhole.__init__(self, port, c, using_ssh=False)
206
207 -class PasswordManhole(_BaseManhole, ComparableMixin):
208 """This Manhole accepts encrypted (ssh) connections, and requires a 209 username and password to authorize access. 210 """ 211 212 compare_attrs = ["port", "username", "password"] 213
214 - def __init__(self, port, username, password):
215 """ 216 @type port: string or int 217 @param port: what port should the Manhole listen on? This is a 218 strports specification string, like 'tcp:12345' or 219 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 220 simple tcp port. 221 222 @param username: 223 @param password: username= and password= form a pair of strings to 224 use when authenticating the remote user. 225 """ 226 227 self.username = username 228 self.password = password 229 230 c = checkers.InMemoryUsernamePasswordDatabaseDontUse() 231 c.addUser(username, password) 232 233 _BaseManhole.__init__(self, port, c)
234
235 -class AuthorizedKeysManhole(_BaseManhole, ComparableMixin):
236 """This Manhole accepts ssh connections, and requires that the 237 prospective client have an ssh private key that matches one of the public 238 keys in our authorized_keys file. It is created with the name of a file 239 that contains the public keys that we will accept.""" 240 241 compare_attrs = ["port", "keyfile"] 242
243 - def __init__(self, port, keyfile):
244 """ 245 @type port: string or int 246 @param port: what port should the Manhole listen on? This is a 247 strports specification string, like 'tcp:12345' or 248 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 249 simple tcp port. 250 251 @param keyfile: the name of a file (relative to the buildmaster's 252 basedir) that contains SSH public keys of authorized 253 users, one per line. This is the exact same format 254 as used by sshd in ~/.ssh/authorized_keys . 255 """ 256 257 # TODO: expanduser this, and make it relative to the buildmaster's 258 # basedir 259 self.keyfile = keyfile 260 c = AuthorizedKeysChecker(keyfile) 261 _BaseManhole.__init__(self, port, c)
262
263 -class ArbitraryCheckerManhole(_BaseManhole, ComparableMixin):
264 """This Manhole accepts ssh connections, but uses an arbitrary 265 user-supplied 'checker' object to perform authentication.""" 266 267 compare_attrs = ["port", "checker"] 268
269 - def __init__(self, port, checker):
270 """ 271 @type port: string or int 272 @param port: what port should the Manhole listen on? This is a 273 strports specification string, like 'tcp:12345' or 274 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a 275 simple tcp port. 276 277 @param checker: an instance of a twisted.cred 'checker' which will 278 perform authentication 279 """ 280 281 _BaseManhole.__init__(self, port, checker)
282 283 ## utility functions for the manhole 284
285 -def show(x):
286 """Display the data attributes of an object in a readable format""" 287 print "data attributes of %r" % (x,) 288 names = dir(x) 289 maxlen = max([0] + [len(n) for n in names]) 290 for k in names: 291 v = getattr(x,k) 292 t = type(v) 293 if t == types.MethodType: continue 294 if k[:2] == '__' and k[-2:] == '__': continue 295 if t is types.StringType or t is types.UnicodeType: 296 if len(v) > 80 - maxlen - 5: 297 v = `v[:80 - maxlen - 5]` + "..." 298 elif t in (types.IntType, types.NoneType): 299 v = str(v) 300 elif v in (types.ListType, types.TupleType, types.DictType): 301 v = "%s (%d elements)" % (v, len(v)) 302 else: 303 v = str(t) 304 print "%*s : %s" % (maxlen, k, v) 305 return x
306