]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/manhole.py
Fix a subtle bug in the overwrite algorithm.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / manhole.py
1
2 # this is adapted from my code in Buildbot  -warner
3
4 import binascii, base64
5
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
12
13 from zope.interface import implements
14
15 from allmydata.util.fileutil import precondition_abspath
16
17 # makeTelnetProtocol and _TelnetRealm are for the TelnetManhole
18
19 class makeTelnetProtocol:
20     # this curries the 'portal' argument into a later call to
21     # TelnetTransport()
22     def __init__(self, portal):
23         self.portal = portal
24
25     def __call__(self):
26         auth = telnet.AuthenticatingTelnetProtocol
27         return telnet.TelnetTransport(auth, self.portal)
28
29 class _TelnetRealm:
30     implements(portal.IRealm)
31
32     def __init__(self, namespace_maker):
33         self.namespace_maker = namespace_maker
34
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,
40                                                namespace)
41             return (telnet.ITelnetProtocol, p, lambda: None)
42         raise NotImplementedError()
43
44
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
50
51     def __call__(self):
52         return insults.ServerProtocol(manhole.ColoredManhole, self.namespace)
53
54 class AuthorizedKeysChecker(conchc.SSHPublicKeyDatabase):
55     """Accept connections using SSH keys from a given file.
56
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
60     like.
61
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.
65     """
66
67     def __init__(self, authorized_keys_file):
68         precondition_abspath(authorized_keys_file)
69         self.authorized_keys_file = authorized_keys_file
70
71     def checkKey(self, credentials):
72         f = open(self.authorized_keys_file)
73         for l in f.readlines():
74             l2 = l.split()
75             if len(l2) < 2:
76                 continue
77             try:
78                 if base64.decodestring(l2[1]) == credentials.blob:
79                     return 1
80             except binascii.Error:
81                 continue
82         return 0
83
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
96
97     def handle_KILLLINE(self):
98         self.handle_END()
99         for i in range(len(self.lineBuffer)):
100             self.handle_BACKSPACE()
101
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.
107     """
108
109     def __init__(self, port, checker, using_ssh=True):
110         """
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
115         simple tcp port.
116
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)
127
128         @type using_ssh: bool
129         @param using_ssh: If True, accept SSH connections. If False, accept
130                           regular unencrypted telnet connections.
131         """
132
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
137
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
143
144         def makeNamespace():
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'
148             namespace = {}
149             for sym in dir(debugshell):
150                 if sym.startswith('__') and sym.endswith('__'):
151                     continue
152                 namespace[sym] = getattr(debugshell, sym)
153             return namespace
154
155         def makeProtocol():
156             namespace = makeNamespace()
157             p = insults.ServerProtocol(ModifiedColoredManhole, namespace)
158             return p
159
160         self.using_ssh = using_ssh
161         if using_ssh:
162             r = manhole_ssh.TerminalRealm()
163             r.chainedProtocolFactory = makeProtocol
164             p = portal.Portal(r, [self.checker])
165             f = manhole_ssh.ConchFactory(p)
166         else:
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)
173
174
175     def startService(self):
176         service.MultiService.startService(self)
177         if self.using_ssh:
178             via = "via SSH"
179         else:
180             via = "via telnet"
181         log.msg("Manhole listening %s on port %s" % (via, self.port))
182
183
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."""
188
189     def __init__(self, port, username, password):
190         """
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
195         simple tcp port.
196
197         @param username:
198         @param password: username= and password= form a pair of strings to
199                          use when authenticating the remote user.
200         """
201
202         self.username = username
203         self.password = password
204
205         c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
206         c.addUser(username, password)
207
208         _BaseManhole.__init__(self, port, c, using_ssh=False)
209
210 class PasswordManhole(_BaseManhole):
211     """This Manhole accepts encrypted (ssh) connections, and requires a
212     username and password to authorize access.
213     """
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):
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     def __init__(self, port, keyfile):
243         """
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
248         simple tcp port.
249
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.
254         """
255
256         self.keyfile = keyfile
257         c = AuthorizedKeysChecker(keyfile)
258         _BaseManhole.__init__(self, port, c)
259
260 class ArbitraryCheckerManhole(_BaseManhole):
261     """This Manhole accepts ssh connections, but uses an arbitrary
262     user-supplied 'checker' object to perform authentication."""
263
264     def __init__(self, port, checker):
265         """
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
270         simple tcp port.
271
272         @param checker: an instance of a twisted.cred 'checker' which will
273                         perform authentication
274         """
275
276         _BaseManhole.__init__(self, port, checker)
277
278
279