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 |