| OLD | NEW |
| (Empty) |
| 1 | |
| 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 | |
| 10 | |
| 11 from buildbot.util import ComparableMixin | |
| 12 from zope.interface import implements # requires Twisted-2.0 or later | |
| 13 | |
| 14 # makeTelnetProtocol and _TelnetRealm are for the TelnetManhole | |
| 15 | |
| 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 | |
| 21 | |
| 22 def __call__(self): | |
| 23 auth = telnet.AuthenticatingTelnetProtocol | |
| 24 return telnet.TelnetTransport(auth, self.portal) | |
| 25 | |
| 26 class _TelnetRealm: | |
| 27 implements(portal.IRealm) | |
| 28 | |
| 29 def __init__(self, namespace_maker): | |
| 30 self.namespace_maker = namespace_maker | |
| 31 | |
| 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() | |
| 40 | |
| 41 | |
| 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 | |
| 47 | |
| 48 def __call__(self): | |
| 49 return insults.ServerProtocol(manhole.ColoredManhole, self.namespace) | |
| 50 | |
| 51 class AuthorizedKeysChecker(conchc.SSHPublicKeyDatabase): | |
| 52 """Accept connections using SSH keys from a given file. | |
| 53 | |
| 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. | |
| 58 | |
| 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 """ | |
| 63 | |
| 64 def __init__(self, authorized_keys_file): | |
| 65 self.authorized_keys_file = os.path.expanduser(authorized_keys_file) | |
| 66 | |
| 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 | |
| 79 | |
| 80 | |
| 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 """ | |
| 87 | |
| 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. | |
| 95 | |
| 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) | |
| 106 | |
| 107 @type using_ssh: bool | |
| 108 @param using_ssh: If True, accept SSH connections. If False, accept | |
| 109 regular unencrypted telnet connections. | |
| 110 """ | |
| 111 | |
| 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 | |
| 116 | |
| 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 | |
| 122 | |
| 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(), | |
| 129 } | |
| 130 return namespace | |
| 131 | |
| 132 def makeProtocol(): | |
| 133 namespace = makeNamespace() | |
| 134 p = insults.ServerProtocol(manhole.ColoredManhole, namespace) | |
| 135 return p | |
| 136 | |
| 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) | |
| 150 | |
| 151 | |
| 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)) | |
| 159 | |
| 160 | |
| 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.""" | |
| 165 | |
| 166 compare_attrs = ["port", "username", "password"] | |
| 167 | |
| 168 def __init__(self, port, username, password): | |
| 169 """ | |
| 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. | |
| 175 | |
| 176 @param username: | |
| 177 @param password: username= and password= form a pair of strings to | |
| 178 use when authenticating the remote user. | |
| 179 """ | |
| 180 | |
| 181 self.username = username | |
| 182 self.password = password | |
| 183 | |
| 184 c = checkers.InMemoryUsernamePasswordDatabaseDontUse() | |
| 185 c.addUser(username, password) | |
| 186 | |
| 187 _BaseManhole.__init__(self, port, c, using_ssh=False) | |
| 188 | |
| 189 class PasswordManhole(_BaseManhole, ComparableMixin): | |
| 190 """This Manhole accepts encrypted (ssh) connections, and requires a | |
| 191 username and password to authorize access. | |
| 192 """ | |
| 193 | |
| 194 compare_attrs = ["port", "username", "password"] | |
| 195 | |
| 196 def __init__(self, port, username, password): | |
| 197 """ | |
| 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. | |
| 203 | |
| 204 @param username: | |
| 205 @param password: username= and password= form a pair of strings to | |
| 206 use when authenticating the remote user. | |
| 207 """ | |
| 208 | |
| 209 self.username = username | |
| 210 self.password = password | |
| 211 | |
| 212 c = checkers.InMemoryUsernamePasswordDatabaseDontUse() | |
| 213 c.addUser(username, password) | |
| 214 | |
| 215 _BaseManhole.__init__(self, port, c) | |
| 216 | |
| 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.""" | |
| 222 | |
| 223 compare_attrs = ["port", "keyfile"] | |
| 224 | |
| 225 def __init__(self, port, keyfile): | |
| 226 """ | |
| 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. | |
| 232 | |
| 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 . | |
| 237 """ | |
| 238 | |
| 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) | |
| 244 | |
| 245 class ArbitraryCheckerManhole(_BaseManhole, ComparableMixin): | |
| 246 """This Manhole accepts ssh connections, but uses an arbitrary | |
| 247 user-supplied 'checker' object to perform authentication.""" | |
| 248 | |
| 249 compare_attrs = ["port", "checker"] | |
| 250 | |
| 251 def __init__(self, port, checker): | |
| 252 """ | |
| 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. | |
| 258 | |
| 259 @param checker: an instance of a twisted.cred 'checker' which will | |
| 260 perform authentication | |
| 261 """ | |
| 262 | |
| 263 _BaseManhole.__init__(self, port, checker) | |
| 264 | |
| 265 | |
| OLD | NEW |