To restrict what a remote bup can do on a server, e.g. in
'bup on' scenarios, add a --mode argument to the server,
adding the following modes:
- read,
- append,
- read-append and
- unrestricted.
To make this reasonably safe in the face of future additions,
this employs an allowlist of permitted commands, and is based
on a filter put into the base protocol class.
For 'bup on', even restrict when a server is started/connected,
and only do that if the remote command is explicitly listed as
requiring append, read, or both (unrestricted is not used used
right now as remote 'gc', 'rm' or 'prune' etc. aren't supported
at all over the server).
Right now, append is almost equivalent to unrestricted, except
it doesn't allow making ref updates that would drop commits or
writing to the config file.
Note that the mode 'read-append' is not used by 'bup on', it
may however be useful in conjunction with SSH forced commands.
Documentation/
bup-server.1.md | 19 ++++++++++++++++
lib/bup/client.py | 4 ++--
lib/bup/cmd/on.py | 28 ++++++++++++++++++++----
lib/bup/cmd/server.py | 13 ++++++++++-
lib/bup/protocol.py | 41 +++++++++++++++++++++++++++++------
5 files changed, 91 insertions(+), 14 deletions(-)
diff --git a/Documentation/
bup-server.1.md b/Documentation/
bup-server.1.md
index da3bdec3c172..a1875e788798 100644
--- a/Documentation/
bup-server.1.md
+++ b/Documentation/
bup-server.1.md
@@ -37,6 +37,25 @@ in `bup-config(5)` for additional information.
connection to use a given bup repository; it cannot read from
or write to any other location on the filesystem.
+\--mode=*mode*
+: Set the server mode, the following modes are accepted:
+
+ *unrestricted*: No restrictions, this is the default if this option
+ is not given.
+
+ *append*: Data can only be written to this repository, and refs can
+ be updated. (Obviously, object existence can be proven since indexes
+ are needed to save data to a repository.)
+
+ *read-append*: Data can be written to and read back.
+
+ *read*: Data can only be read.
+
+ **NOTE**: Currently, the server doesn't support any destructive
+ operations, so *unrestricted* is really identical to *read-append*,
+ but as this may change in the future there's a difference already
+ to avoid breaking setups.
+
# FILES
$BUP_DIR/bup-dumb-server
diff --git a/lib/bup/client.py b/lib/bup/client.py
index d6358a0d9529..8509255e1075 100644
--- a/lib/bup/client.py
+++ b/lib/bup/client.py
@@ -285,13 +285,13 @@ class Client:
ctx.enter_context(self._transport)
self.conn = self._transport.conn
self._available_commands = self._get_available_commands()
- self._require_command(b'init-dir')
- self._require_command(b'set-dir')
if self.dir:
self.dir = re.sub(br'[\r\n]', b' ', self.dir)
if create:
+ self._require_command(b'init-dir')
self.conn.write(b'init-dir %s\n' % self.dir)
else:
+ self._require_command(b'set-dir')
self.conn.write(b'set-dir %s\n' % self.dir)
self.check_ok()
self.cachedir = self._prep_cache(self.host, self.port, self.dir)
diff --git a/lib/bup/cmd/on.py b/lib/bup/cmd/on.py
index f3f502201c8f..0a645176be48 100644
--- a/lib/bup/cmd/on.py
+++ b/lib/bup/cmd/on.py
@@ -48,10 +48,30 @@ def main(argv):
argvs = b'\0'.join([b'bup'] + argv)
p.stdin.write(struct.pack('!I', len(argvs)) + argvs)
p.stdin.flush()
- # we already put BUP_DIR into the environment, which
- # is inherited here
- sp = subprocess.Popen([path.exe(), b'server', b'--force-repo'],
- stdin=p.stdout, stdout=p.stdin)
+
+ # for commands not listed here don't even execute the server
+ # (e.g. bup on <host> index ...)
+ cmdmodes = {
+ b'get': b'unrestricted',
+ b'save': b'append',
+ b'split': b'append',
+ b'tag': b'append',
+ b'join': b'read',
+ b'cat-file': b'read',
+ b'ftp': b'read',
+ b'ls': b'read',
+ b'margin': b'read',
+ b'meta': b'read',
+ b'config': b'unrestricted',
+ }
+ mode = cmdmodes.get(argv[0], None)
+
+ if mode is not None:
+ # we already put BUP_DIR into the environment, which
+ # is inherited here
+ sp = subprocess.Popen([path.exe(), b'server', b'--force-repo',
+ b'--mode=' + mode],
+ stdin=p.stdout, stdout=p.stdin)
p.stdin.close()
p.stdout.close()
# Demultiplex remote client's stderr (back to stdout/stderr).
diff --git a/lib/bup/cmd/server.py b/lib/bup/cmd/server.py
index 40f517d21377..1574e40f7fd6 100644
--- a/lib/bup/cmd/server.py
+++ b/lib/bup/cmd/server.py
@@ -14,6 +14,7 @@ bup server
--
Options:
force-repo force the configured (environment, --bup-dir) repository to be used
+mode= server mode (unrestricted, append, read-append, read)
"""
def main(argv):
@@ -54,6 +55,16 @@ def main(argv):
return self._packwriter.just_write_encoded(sha, content)
+ def _restrict(server, commands):
+ for fn in dir(server):
+ if getattr(fn, 'bup_server_command', False):
+ if not fn in commands:
+ del server.fn
+
+ modes = ['unrestricted', 'append', 'read-append', 'read']
+ if opt.mode is not None and opt.mode not in modes:
+ o.fatal("server: invalid mode")
+
with Conn(byte_stream(sys.stdin), byte_stream(sys.stdout)) as conn, \
- protocol.Server(conn, ServerRepo) as server:
+ protocol.Server(conn, ServerRepo, mode=opt.mode) as server:
server.handle()
diff --git a/lib/bup/protocol.py b/lib/bup/protocol.py
index 99a3cedc9542..705c970399f2 100644
--- a/lib/bup/protocol.py
+++ b/lib/bup/protocol.py
@@ -157,21 +157,46 @@ def _command(fn):
return fn
class Server:
- def __init__(self, conn, backend):
+ def __init__(self, conn, backend, mode=None):
self.conn = conn
self._backend = backend
- self._commands = self._get_commands()
+ self._only_ff_updates = mode is not None and mode != 'unrestricted'
+ self._commands = self._get_commands(mode or 'unrestricted')
self.suspended = False
self.repo = None
self._deduplicate_writes = True
- def _get_commands(self):
+ def _get_commands(self, mode):
+ # always allow these - even if set-dir may actually be
+ # a no-op (if --force-repo is given)
+ permitted = set([b'quit', b'help', b'set-dir', b'list-indexes',
+ b'send-index', b'config-get', b'config-list'])
+
+ read_cmds = set([b'read-ref', b'join', b'cat-batch',
+ b'refs', b'rev-list', b'resolve'])
+ append_cmds = set([b'receive-objects-v2', b'read-ref', b'update-ref',
+ b'init-dir'])
+
+ if mode == 'unrestricted':
+ permitted = None # all commands permitted
+ elif mode == 'append':
+ permitted.update(append_cmds)
+ elif mode == 'read-append':
+ permitted.update(read_cmds)
+ permitted.update(append_cmds)
+ elif mode == 'read':
+ permitted.update(read_cmds)
+ else:
+ assert False # should be caught elsewhere
+
commands = []
for name in dir(self):
fn = getattr(self, name)
if getattr(fn, 'bup_server_command', False):
- commands.append(name.replace('_', '-').encode('ascii'))
+ cmdname = name.replace('_', '-').encode('ascii')
+ if permitted is None or cmdname in permitted:
+ commands.append(cmdname)
return commands
@@ -308,9 +333,11 @@ class Server:
@_command
def update_ref(self, refname):
self.init_session()
- newval = self.conn.readline().strip()
- oldval = self.conn.readline().strip()
- self.repo.update_ref(refname, unhexlify(newval), unhexlify(oldval))
+ newval = unhexlify(self.conn.readline().strip())
+ oldval = unhexlify(self.conn.readline().strip())
+ if self._only_ff_updates:
+ assert (self.repo.read_ref(refname) or b'') == oldval
+ self.repo.update_ref(refname, newval, oldval)
self.conn.ok()
@_command
--
2.53.0