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