Files
linux-cloud-router/debian-package/src/usr/lib/vpn-router/editor-file
T

83 lines
3.0 KiB
Python
Executable File

#!/usr/bin/python3
"""Read or write a file of the editor of the Router Manager's Shell page, as the user it runs as.
The web service runs it with systemd-run --uid=<console account> --pipe, outside its sandbox, so the
file is read and written with the rights of the shell's user. The version of a content is the
hexadecimal SHA-256 of its bytes; a missing file has the version of the empty content.
editor-file read PATH print the content of the file; nothing for a missing file
editor-file write PATH VERSION replace the content with standard input when the file still has
VERSION, writing in place, so its owner, mode and links stay;
a missing file is created
Only regular files of at most MAX_BYTES bytes of UTF-8 text are read and written. The exit status
is 0 on success, 2 for a file that is refused, with the reason on standard error, 3 when the file
does not have VERSION any more, and 1 for any other failure.
"""
import hashlib
import os
import sys
#: The largest file the editor opens, in bytes.
MAX_BYTES = 1024 * 1024
class Refused(Exception):
"""A file the editor does not open or write; status is the exit status."""
def __init__(self, message, status=2):
super().__init__(message)
self.status = status
def text(data, path):
"""The bytes when they are UTF-8 text of at most MAX_BYTES; Refused otherwise."""
if len(data) > MAX_BYTES:
raise Refused(f'{path} is larger than {MAX_BYTES // 1024} KiB')
try:
data.decode('utf-8')
except UnicodeDecodeError:
raise Refused(f'{path} is not UTF-8 text')
return data
def current(path):
"""The content of the file, b'' when it does not exist; Refused for a file that is not a regular one."""
if not os.path.lexists(path):
return b''
if not os.path.isfile(path):
raise Refused(f'{path} is not a regular file')
with open(path, 'rb') as handle:
return text(handle.read(MAX_BYTES + 1), path)
def main(argv):
if len(argv) < 2 or (argv[0], len(argv)) not in (('read', 2), ('write', 3)):
print('usage: editor-file read PATH | editor-file write PATH VERSION', file=sys.stderr)
return 2
path = argv[1]
try:
if argv[0] == 'read':
sys.stdout.buffer.write(current(path))
return 0
content = text(sys.stdin.buffer.read(MAX_BYTES + 1), path)
if hashlib.sha256(current(path)).hexdigest() != argv[2]:
raise Refused(f'{path} changed since it was read', 3)
with open(path, 'r+b' if os.path.lexists(path) else 'xb') as handle:
handle.write(content)
handle.truncate()
handle.flush()
os.fsync(handle.fileno())
except Refused as refused:
print(refused, file=sys.stderr)
return refused.status
except OSError as exc:
print(f'{exc.filename or path}: {exc.strerror}', file=sys.stderr)
return 1
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))