mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-01-18 18:56:28 +00:00
remove "manhole" (ssh-accessible REPL)
This little-used debugging feature allowed you to SSH or Telnet "into" a Tahoe node, and get an interactive Read-Eval-Print-Loop (REPL) that executed inside the context of the running process. The SSH authentication code used a deprecated feature of Twisted, this code had no unit-test coverage, and I haven't personally used it in at least 6 years (despite writing it in the first place). Time to go. Also experiment with a Twisted-style "topfiles/" directory of NEWS fragments. The idea is that we require all user-visible changes to include a file or two (named as $TICKETNUM.$TYPE), and then run a script to generate NEWS during the release process, instead of having a human scan the commit logs and summarize the changes long after they landed. Closes ticket:2367
This commit is contained in:
parent
a8161028d6
commit
8279d919f3
@ -272,19 +272,6 @@ set the ``tub.location`` option described below.
|
||||
|
||||
.. _`#521`: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/521
|
||||
|
||||
``ssh.port = (strports string, optional)``
|
||||
|
||||
``ssh.authorized_keys_file = (filename, optional)``
|
||||
|
||||
This enables an SSH-based interactive Python shell, which can be used to
|
||||
inspect the internal state of the node, for debugging. To cause the node
|
||||
to accept SSH connections on port 8022 from the same keys as the rest of
|
||||
your account, use::
|
||||
|
||||
[tub]
|
||||
ssh.port = 8022
|
||||
ssh.authorized_keys_file = ~/.ssh/authorized_keys
|
||||
|
||||
``tempdir = (string, optional)``
|
||||
|
||||
This specifies a temporary directory for the web-API server to use, for
|
||||
@ -705,8 +692,6 @@ a legal one.
|
||||
log_gatherer.furl = pb://soklj4y7eok5c3xkmjeqpw@192.168.69.247:44801/eqpwqtzm
|
||||
timeout.keepalive = 240
|
||||
timeout.disconnect = 1800
|
||||
ssh.port = 8022
|
||||
ssh.authorized_keys_file = ~/.ssh/authorized_keys
|
||||
|
||||
[client]
|
||||
introducer.furl = pb://ok45ssoklj4y7eok5c3xkmj@tahoe.example:44801/ii3uumo
|
||||
|
@ -1,5 +0,0 @@
|
||||
|
||||
# 'app' is overwritten by manhole when the connection is established. We set
|
||||
# it to None now to keep pyflakes from complaining.
|
||||
app = None
|
||||
|
@ -1,279 +0,0 @@
|
||||
|
||||
# this is adapted from my code in Buildbot -warner
|
||||
|
||||
import binascii, base64
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.application import service, strports
|
||||
from twisted.cred import checkers, portal
|
||||
from twisted.conch import manhole, telnet, manhole_ssh, checkers as conchc
|
||||
from twisted.conch.insults import insults
|
||||
from twisted.internet import protocol
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
from allmydata.util.fileutil import precondition_abspath
|
||||
|
||||
# makeTelnetProtocol and _TelnetRealm are for the TelnetManhole
|
||||
|
||||
class makeTelnetProtocol:
|
||||
# this curries the 'portal' argument into a later call to
|
||||
# TelnetTransport()
|
||||
def __init__(self, portal):
|
||||
self.portal = portal
|
||||
|
||||
def __call__(self):
|
||||
auth = telnet.AuthenticatingTelnetProtocol
|
||||
return telnet.TelnetTransport(auth, self.portal)
|
||||
|
||||
class _TelnetRealm:
|
||||
implements(portal.IRealm)
|
||||
|
||||
def __init__(self, namespace_maker):
|
||||
self.namespace_maker = namespace_maker
|
||||
|
||||
def requestAvatar(self, avatarId, *interfaces):
|
||||
if telnet.ITelnetProtocol in interfaces:
|
||||
namespace = self.namespace_maker()
|
||||
p = telnet.TelnetBootstrapProtocol(insults.ServerProtocol,
|
||||
manhole.ColoredManhole,
|
||||
namespace)
|
||||
return (telnet.ITelnetProtocol, p, lambda: None)
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class chainedProtocolFactory:
|
||||
# this curries the 'namespace' argument into a later call to
|
||||
# chainedProtocolFactory()
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
def __call__(self):
|
||||
return insults.ServerProtocol(manhole.ColoredManhole, self.namespace)
|
||||
|
||||
class AuthorizedKeysChecker(conchc.SSHPublicKeyDatabase):
|
||||
"""Accept connections using SSH keys from a given file.
|
||||
|
||||
SSHPublicKeyDatabase takes the username that the prospective client has
|
||||
requested and attempts to get a ~/.ssh/authorized_keys file for that
|
||||
username. This requires root access, so it isn't as useful as you'd
|
||||
like.
|
||||
|
||||
Instead, this subclass looks for keys in a single file, given as an
|
||||
argument. This file is typically kept in the buildmaster's basedir. The
|
||||
file should have 'ssh-dss ....' lines in it, just like authorized_keys.
|
||||
"""
|
||||
|
||||
def __init__(self, authorized_keys_file):
|
||||
precondition_abspath(authorized_keys_file)
|
||||
self.authorized_keys_file = authorized_keys_file
|
||||
|
||||
def checkKey(self, credentials):
|
||||
f = open(self.authorized_keys_file)
|
||||
for l in f.readlines():
|
||||
l2 = l.split()
|
||||
if len(l2) < 2:
|
||||
continue
|
||||
try:
|
||||
if base64.decodestring(l2[1]) == credentials.blob:
|
||||
return 1
|
||||
except binascii.Error:
|
||||
continue
|
||||
return 0
|
||||
|
||||
class ModifiedColoredManhole(manhole.ColoredManhole):
|
||||
def connectionMade(self):
|
||||
manhole.ColoredManhole.connectionMade(self)
|
||||
# look in twisted.conch.recvline.RecvLine for hints
|
||||
self.keyHandlers["\x08"] = self.handle_BACKSPACE
|
||||
self.keyHandlers["\x15"] = self.handle_KILLLINE
|
||||
self.keyHandlers["\x01"] = self.handle_HOME
|
||||
self.keyHandlers["\x04"] = self.handle_DELETE
|
||||
self.keyHandlers["\x05"] = self.handle_END
|
||||
self.keyHandlers["\x0b"] = self.handle_KILLLINE # really kill-to-end
|
||||
#self.keyHandlers["\xe2"] = self.handle_BACKWARDS_WORD # M-b
|
||||
#self.keyHandlers["\xe6"] = self.handle_FORWARDS_WORD # M-f
|
||||
|
||||
def handle_KILLLINE(self):
|
||||
self.handle_END()
|
||||
for i in range(len(self.lineBuffer)):
|
||||
self.handle_BACKSPACE()
|
||||
|
||||
class _BaseManhole(service.MultiService):
|
||||
"""This provides remote access to a python interpreter (a read/exec/print
|
||||
loop) embedded in the buildmaster via an internal SSH server. This allows
|
||||
detailed inspection of the buildmaster state. It is of most use to
|
||||
buildbot developers. Connect to this by running an ssh client.
|
||||
"""
|
||||
|
||||
def __init__(self, port, checker, using_ssh=True):
|
||||
"""
|
||||
@type port: string or int
|
||||
@param port: what port should the Manhole listen on? This is a
|
||||
strports specification string, like 'tcp:12345' or
|
||||
'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
|
||||
simple tcp port.
|
||||
|
||||
@type checker: an object providing the
|
||||
L{twisted.cred.checkers.ICredentialsChecker} interface
|
||||
@param checker: if provided, this checker is used to authenticate the
|
||||
client instead of using the username/password scheme. You must either
|
||||
provide a username/password or a Checker. Some useful values are::
|
||||
import twisted.cred.checkers as credc
|
||||
import twisted.conch.checkers as conchc
|
||||
c = credc.AllowAnonymousAccess # completely open
|
||||
c = credc.FilePasswordDB(passwd_filename) # file of name:passwd
|
||||
c = conchc.UNIXPasswordDatabase # getpwnam() (probably /etc/passwd)
|
||||
|
||||
@type using_ssh: bool
|
||||
@param using_ssh: If True, accept SSH connections. If False, accept
|
||||
regular unencrypted telnet connections.
|
||||
"""
|
||||
|
||||
# unfortunately, these don't work unless we're running as root
|
||||
#c = credc.PluggableAuthenticationModulesChecker: PAM
|
||||
#c = conchc.SSHPublicKeyDatabase() # ~/.ssh/authorized_keys
|
||||
# and I can't get UNIXPasswordDatabase to work
|
||||
|
||||
service.MultiService.__init__(self)
|
||||
if type(port) is int:
|
||||
port = "tcp:%d" % port
|
||||
self.port = port # for comparison later
|
||||
self.checker = checker # to maybe compare later
|
||||
|
||||
def makeNamespace():
|
||||
# close over 'self' so we can get access to .parent later
|
||||
from allmydata import debugshell
|
||||
debugshell.app = self.parent # make node accessible via 'app'
|
||||
namespace = {}
|
||||
for sym in dir(debugshell):
|
||||
if sym.startswith('__') and sym.endswith('__'):
|
||||
continue
|
||||
namespace[sym] = getattr(debugshell, sym)
|
||||
return namespace
|
||||
|
||||
def makeProtocol():
|
||||
namespace = makeNamespace()
|
||||
p = insults.ServerProtocol(ModifiedColoredManhole, namespace)
|
||||
return p
|
||||
|
||||
self.using_ssh = using_ssh
|
||||
if using_ssh:
|
||||
r = manhole_ssh.TerminalRealm()
|
||||
r.chainedProtocolFactory = makeProtocol
|
||||
p = portal.Portal(r, [self.checker])
|
||||
f = manhole_ssh.ConchFactory(p)
|
||||
else:
|
||||
r = _TelnetRealm(makeNamespace)
|
||||
p = portal.Portal(r, [self.checker])
|
||||
f = protocol.ServerFactory()
|
||||
f.protocol = makeTelnetProtocol(p)
|
||||
s = strports.service(self.port, f)
|
||||
s.setServiceParent(self)
|
||||
|
||||
|
||||
def startService(self):
|
||||
service.MultiService.startService(self)
|
||||
if self.using_ssh:
|
||||
via = "via SSH"
|
||||
else:
|
||||
via = "via telnet"
|
||||
log.msg("Manhole listening %s on port %s" % (via, self.port))
|
||||
|
||||
|
||||
class TelnetManhole(_BaseManhole):
|
||||
"""This Manhole accepts unencrypted (telnet) connections, and requires a
|
||||
username and password authorize access. You are encouraged to use the
|
||||
encrypted ssh-based manhole classes instead."""
|
||||
|
||||
def __init__(self, port, username, password):
|
||||
"""
|
||||
@type port: string or int
|
||||
@param port: what port should the Manhole listen on? This is a
|
||||
strports specification string, like 'tcp:12345' or
|
||||
'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
|
||||
simple tcp port.
|
||||
|
||||
@param username:
|
||||
@param password: username= and password= form a pair of strings to
|
||||
use when authenticating the remote user.
|
||||
"""
|
||||
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
|
||||
c.addUser(username, password)
|
||||
|
||||
_BaseManhole.__init__(self, port, c, using_ssh=False)
|
||||
|
||||
class PasswordManhole(_BaseManhole):
|
||||
"""This Manhole accepts encrypted (ssh) connections, and requires a
|
||||
username and password to authorize access.
|
||||
"""
|
||||
|
||||
def __init__(self, port, username, password):
|
||||
"""
|
||||
@type port: string or int
|
||||
@param port: what port should the Manhole listen on? This is a
|
||||
strports specification string, like 'tcp:12345' or
|
||||
'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
|
||||
simple tcp port.
|
||||
|
||||
@param username:
|
||||
@param password: username= and password= form a pair of strings to
|
||||
use when authenticating the remote user.
|
||||
"""
|
||||
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
|
||||
c.addUser(username, password)
|
||||
|
||||
_BaseManhole.__init__(self, port, c)
|
||||
|
||||
class AuthorizedKeysManhole(_BaseManhole):
|
||||
"""This Manhole accepts ssh connections, and requires that the
|
||||
prospective client have an ssh private key that matches one of the public
|
||||
keys in our authorized_keys file. It is created with the name of a file
|
||||
that contains the public keys that we will accept."""
|
||||
|
||||
def __init__(self, port, keyfile):
|
||||
"""
|
||||
@type port: string or int
|
||||
@param port: what port should the Manhole listen on? This is a
|
||||
strports specification string, like 'tcp:12345' or
|
||||
'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
|
||||
simple tcp port.
|
||||
|
||||
@param keyfile: the path of a file that contains SSH public keys of
|
||||
authorized users, one per line. This is the exact
|
||||
same format as used by sshd in ~/.ssh/authorized_keys .
|
||||
The path should be absolute.
|
||||
"""
|
||||
|
||||
self.keyfile = keyfile
|
||||
c = AuthorizedKeysChecker(keyfile)
|
||||
_BaseManhole.__init__(self, port, c)
|
||||
|
||||
class ArbitraryCheckerManhole(_BaseManhole):
|
||||
"""This Manhole accepts ssh connections, but uses an arbitrary
|
||||
user-supplied 'checker' object to perform authentication."""
|
||||
|
||||
def __init__(self, port, checker):
|
||||
"""
|
||||
@type port: string or int
|
||||
@param port: what port should the Manhole listen on? This is a
|
||||
strports specification string, like 'tcp:12345' or
|
||||
'tcp:12345:interface=127.0.0.1'. Bare integers are treated as a
|
||||
simple tcp port.
|
||||
|
||||
@param checker: an instance of a twisted.cred 'checker' which will
|
||||
perform authentication
|
||||
"""
|
||||
|
||||
_BaseManhole.__init__(self, port, checker)
|
||||
|
||||
|
||||
|
@ -88,7 +88,6 @@ class Node(service.MultiService):
|
||||
self.create_tub()
|
||||
self.logSource="Node"
|
||||
|
||||
self.setup_ssh()
|
||||
self.setup_logging()
|
||||
self.log("Node constructed. " + get_package_versions_string())
|
||||
iputil.increase_rlimits()
|
||||
@ -203,16 +202,6 @@ class Node(service.MultiService):
|
||||
# any services with the Tub until after that point
|
||||
self.tub.setServiceParent(self)
|
||||
|
||||
def setup_ssh(self):
|
||||
ssh_port = self.get_config("node", "ssh.port", "")
|
||||
if ssh_port:
|
||||
ssh_keyfile_config = self.get_config("node", "ssh.authorized_keys_file").decode('utf-8')
|
||||
ssh_keyfile = abspath_expanduser_unicode(ssh_keyfile_config, base=self.basedir)
|
||||
from allmydata import manhole
|
||||
m = manhole.AuthorizedKeysManhole(ssh_port, ssh_keyfile)
|
||||
m.setServiceParent(self)
|
||||
self.log("AuthorizedKeysManhole listening on %s" % (ssh_port,))
|
||||
|
||||
def get_app_versions(self):
|
||||
# TODO: merge this with allmydata.get_package_versions
|
||||
return dict(app_versions.versions)
|
||||
|
@ -11,7 +11,6 @@ from allmydata.node import Node, OldConfigError, OldConfigOptionError, MissingCo
|
||||
from allmydata.frontends.auth import NeedRootcapLookupScheme
|
||||
from allmydata import client
|
||||
from allmydata.storage_client import StorageFarmBroker
|
||||
from allmydata.manhole import AuthorizedKeysManhole
|
||||
from allmydata.util import base32, fileutil
|
||||
from allmydata.interfaces import IFilesystemNode, IFileNode, \
|
||||
IImmutableFileNode, IMutableFileNode, IDirectoryNode
|
||||
@ -195,20 +194,6 @@ class Basic(testutil.ReallyEqualMixin, unittest.TestCase):
|
||||
expected = fileutil.abspath_expanduser_unicode(u"relative", abs_basedir)
|
||||
self.failUnlessReallyEqual(w.staticdir, expected)
|
||||
|
||||
def test_manhole_keyfile(self):
|
||||
basedir = u"client.Basic.test_manhole_keyfile"
|
||||
os.mkdir(basedir)
|
||||
fileutil.write(os.path.join(basedir, "tahoe.cfg"),
|
||||
BASECONFIG +
|
||||
"[node]\n" +
|
||||
"ssh.port = tcp:0:interface=127.0.0.1\n" +
|
||||
"ssh.authorized_keys_file = relative\n")
|
||||
c = client.Client(basedir)
|
||||
m = [s for s in c if isinstance(s, AuthorizedKeysManhole)][0]
|
||||
abs_basedir = fileutil.abspath_expanduser_unicode(basedir)
|
||||
expected = fileutil.abspath_expanduser_unicode(u"relative", abs_basedir)
|
||||
self.failUnlessReallyEqual(m.keyfile, expected)
|
||||
|
||||
# TODO: also test config options for SFTP.
|
||||
|
||||
def test_ftp_auth_keyfile(self):
|
||||
|
6
topfiles/2367.removal
Normal file
6
topfiles/2367.removal
Normal file
@ -0,0 +1,6 @@
|
||||
The little-used "manhole" debugging feature has been removed. This allowed
|
||||
you to SSH or Telnet "into" a Tahoe node, providing an interactive
|
||||
Read-Eval-Print-Loop (REPL) that executed inside the context of the running
|
||||
process. The SSH authentication code used a deprecated feature of Twisted,
|
||||
this code had no unit-test coverage, and I haven't personally used it in at
|
||||
least 6 years (despite writing it in the first place). Time to go.
|
Loading…
Reference in New Issue
Block a user