2 # this is adapted from my code in Buildbot -warner
4 import binascii, base64
6 from twisted.python import log
7 from twisted.application import service, strports
8 from twisted.cred import checkers, portal
9 from twisted.conch import manhole, telnet, manhole_ssh, checkers as conchc
10 from twisted.conch.insults import insults
11 from twisted.internet import protocol
13 from zope.interface import implements
15 from allmydata.util.fileutil import precondition_abspath
17 # makeTelnetProtocol and _TelnetRealm are for the TelnetManhole
19 class makeTelnetProtocol:
20 # this curries the 'portal' argument into a later call to
22 def __init__(self, portal):
26 auth = telnet.AuthenticatingTelnetProtocol
27 return telnet.TelnetTransport(auth, self.portal)
30 implements(portal.IRealm)
32 def __init__(self, namespace_maker):
33 self.namespace_maker = namespace_maker
35 def requestAvatar(self, avatarId, *interfaces):
36 if telnet.ITelnetProtocol in interfaces:
37 namespace = self.namespace_maker()
38 p = telnet.TelnetBootstrapProtocol(insults.ServerProtocol,
39 manhole.ColoredManhole,
41 return (telnet.ITelnetProtocol, p, lambda: None)
42 raise NotImplementedError()
45 class chainedProtocolFactory:
46 # this curries the 'namespace' argument into a later call to
47 # chainedProtocolFactory()
48 def __init__(self, namespace):
49 self.namespace = namespace
52 return insults.ServerProtocol(manhole.ColoredManhole, self.namespace)
54 class AuthorizedKeysChecker(conchc.SSHPublicKeyDatabase):
55 """Accept connections using SSH keys from a given file.
57 SSHPublicKeyDatabase takes the username that the prospective client has
58 requested and attempts to get a ~/.ssh/authorized_keys file for that
59 username. This requires root access, so it isn't as useful as you'd
62 Instead, this subclass looks for keys in a single file, given as an
63 argument. This file is typically kept in the buildmaster's basedir. The
64 file should have 'ssh-dss ....' lines in it, just like authorized_keys.
67 def __init__(self, authorized_keys_file):
68 precondition_abspath(authorized_keys_file)
69 self.authorized_keys_file = authorized_keys_file
71 def checkKey(self, credentials):
72 f = open(self.authorized_keys_file)
73 for l in f.readlines():
78 if base64.decodestring(l2[1]) == credentials.blob:
80 except binascii.Error:
84 class ModifiedColoredManhole(manhole.ColoredManhole):
85 def connectionMade(self):
86 manhole.ColoredManhole.connectionMade(self)
87 # look in twisted.conch.recvline.RecvLine for hints
88 self.keyHandlers["\x08"] = self.handle_BACKSPACE
89 self.keyHandlers["\x15"] = self.handle_KILLLINE
90 self.keyHandlers["\x01"] = self.handle_HOME
91 self.keyHandlers["\x04"] = self.handle_DELETE
92 self.keyHandlers["\x05"] = self.handle_END
93 self.keyHandlers["\x0b"] = self.handle_KILLLINE # really kill-to-end
94 #self.keyHandlers["\xe2"] = self.handle_BACKWARDS_WORD # M-b
95 #self.keyHandlers["\xe6"] = self.handle_FORWARDS_WORD # M-f
97 def handle_KILLLINE(self):
99 for i in range(len(self.lineBuffer)):
100 self.handle_BACKSPACE()
102 class _BaseManhole(service.MultiService):
103 """This provides remote access to a python interpreter (a read/exec/print
104 loop) embedded in the buildmaster via an internal SSH server. This allows
105 detailed inspection of the buildmaster state. It is of most use to
106 buildbot developers. Connect to this by running an ssh client.
109 def __init__(self, port, checker, using_ssh=True):
111 @type port: string or int
112 @param port: what port should the Manhole listen on? This is a
113 strports specification string, like 'tcp:12345' or
114 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
117 @type checker: an object providing the
118 L{twisted.cred.checkers.ICredentialsChecker} interface
119 @param checker: if provided, this checker is used to authenticate the
120 client instead of using the username/password scheme. You must either
121 provide a username/password or a Checker. Some useful values are::
122 import twisted.cred.checkers as credc
123 import twisted.conch.checkers as conchc
124 c = credc.AllowAnonymousAccess # completely open
125 c = credc.FilePasswordDB(passwd_filename) # file of name:passwd
126 c = conchc.UNIXPasswordDatabase # getpwnam() (probably /etc/passwd)
128 @type using_ssh: bool
129 @param using_ssh: If True, accept SSH connections. If False, accept
130 regular unencrypted telnet connections.
133 # unfortunately, these don't work unless we're running as root
134 #c = credc.PluggableAuthenticationModulesChecker: PAM
135 #c = conchc.SSHPublicKeyDatabase() # ~/.ssh/authorized_keys
136 # and I can't get UNIXPasswordDatabase to work
138 service.MultiService.__init__(self)
139 if type(port) is int:
140 port = "tcp:%d" % port
141 self.port = port # for comparison later
142 self.checker = checker # to maybe compare later
145 # close over 'self' so we can get access to .parent later
146 from allmydata import debugshell
147 debugshell.app = self.parent # make node accessible via 'app'
149 for sym in dir(debugshell):
150 if sym.startswith('__') and sym.endswith('__'):
152 namespace[sym] = getattr(debugshell, sym)
156 namespace = makeNamespace()
157 p = insults.ServerProtocol(ModifiedColoredManhole, namespace)
160 self.using_ssh = using_ssh
162 r = manhole_ssh.TerminalRealm()
163 r.chainedProtocolFactory = makeProtocol
164 p = portal.Portal(r, [self.checker])
165 f = manhole_ssh.ConchFactory(p)
167 r = _TelnetRealm(makeNamespace)
168 p = portal.Portal(r, [self.checker])
169 f = protocol.ServerFactory()
170 f.protocol = makeTelnetProtocol(p)
171 s = strports.service(self.port, f)
172 s.setServiceParent(self)
175 def startService(self):
176 service.MultiService.startService(self)
181 log.msg("Manhole listening %s on port %s" % (via, self.port))
184 class TelnetManhole(_BaseManhole):
185 """This Manhole accepts unencrypted (telnet) connections, and requires a
186 username and password authorize access. You are encouraged to use the
187 encrypted ssh-based manhole classes instead."""
189 def __init__(self, port, username, password):
191 @type port: string or int
192 @param port: what port should the Manhole listen on? This is a
193 strports specification string, like 'tcp:12345' or
194 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
198 @param password: username= and password= form a pair of strings to
199 use when authenticating the remote user.
202 self.username = username
203 self.password = password
205 c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
206 c.addUser(username, password)
208 _BaseManhole.__init__(self, port, c, using_ssh=False)
210 class PasswordManhole(_BaseManhole):
211 """This Manhole accepts encrypted (ssh) connections, and requires a
212 username and password to authorize access.
215 def __init__(self, port, username, password):
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
224 @param password: username= and password= form a pair of strings to
225 use when authenticating the remote user.
228 self.username = username
229 self.password = password
231 c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
232 c.addUser(username, password)
234 _BaseManhole.__init__(self, port, c)
236 class AuthorizedKeysManhole(_BaseManhole):
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."""
242 def __init__(self, port, keyfile):
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
250 @param keyfile: the path of a file that contains SSH public keys of
251 authorized users, one per line. This is the exact
252 same format as used by sshd in ~/.ssh/authorized_keys .
253 The path should be absolute.
256 self.keyfile = keyfile
257 c = AuthorizedKeysChecker(keyfile)
258 _BaseManhole.__init__(self, port, c)
260 class ArbitraryCheckerManhole(_BaseManhole):
261 """This Manhole accepts ssh connections, but uses an arbitrary
262 user-supplied 'checker' object to perform authentication."""
264 def __init__(self, port, checker):
266 @type port: string or int
267 @param port: what port should the Manhole listen on? This is a
268 strports specification string, like 'tcp:12345' or
269 'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
272 @param checker: an instance of a twisted.cred 'checker' which will
273 perform authentication
276 _BaseManhole.__init__(self, port, checker)