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