summaryrefslogtreecommitdiffstats
path: root/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell
diff options
context:
space:
mode:
Diffstat (limited to 'config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell')
-rwxr-xr-xconfig/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell238
1 files changed, 175 insertions, 63 deletions
diff --git a/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell b/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell
index 3dd2384..8660cd6 100755
--- a/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell
+++ b/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell
@@ -4,76 +4,188 @@
# adversary with access to you *physical* serial port, which means
# that you are screwed any way.
-from subprocess import Popen, PIPE
-from sys import argv
-from json import dumps, loads
-from pwd import getpwnam
-from os import setgid, setuid, environ
+import base64
+import fcntl
+import io
+import json
+import os
+import pwd
import serial
-from systemd.daemon import notify as sd_notify
+import signal
+import subprocess
+import sys
+import systemd.daemon
+import textwrap
+import traceback
-def mk_switch_user_fn(uid, gid):
+REMOTE_SHELL_DEV = '/dev/ttyS0'
+
+
+def mk_switch_user_fn(user):
+ pwd_user = pwd.getpwnam(user)
def switch_user():
- setgid(gid)
- setuid(uid)
+ os.initgroups(user, pwd_user.pw_gid)
+ os.setgid(pwd_user.pw_gid)
+ os.setuid(pwd_user.pw_uid)
return switch_user
+
+def get_user_env(user):
+ # We try to create an environment identical to what's expected
+ # inside Tails for the user by logging in (via `su`) as the user,
+ # setting up the GNOME shell environment, and extracting the
+ # environment via `env`; not that we will run `env` unconditionally
+ # since the former command could fail, e.g. if GNOME is not running.
+ env_cmd = '. /usr/local/lib/tails-shell-library/gnome.sh && ' + \
+ 'export_gnome_env ; ' + \
+ 'env'
+ wrapped_env_cmd = "su -c '{}' {}".format(env_cmd, user)
+ pipe = subprocess.Popen(wrapped_env_cmd, stdout=subprocess.PIPE, shell=True)
+ env_data = pipe.communicate()[0].decode('utf-8')
+ return dict((line.split('=', 1) for line in env_data.splitlines()))
+
+
+# Dogtail does not seem to support the root user interacting with
+# other users' applications, and it does not support Python 3 (which
+# this script is written in) so let's wrap around an interactive
+# Python shell started as a subprocess.
+class PythonSession:
+ def __init__(self, user = None):
+ interactive_shell_code = '; '.join([
+ "import sys",
+ "import code",
+ "sys.ps1 = ''",
+ "sys.ps2 = ''",
+ "code.interact(banner='')",
+ ])
+ if not user:
+ user = pwd.getpwuid(os.getuid()).pw_name
+ env = get_user_env(user)
+ cwd = env['HOME']
+ self.process = subprocess.Popen(
+ ["python2", "-u", "-c", interactive_shell_code],
+ bufsize = 0,
+ shell=False,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=env,
+ cwd=cwd,
+ preexec_fn=mk_switch_user_fn(user)
+ )
+ init_code = """
+ import cStringIO
+ import json
+ import sys
+ orig_stdout = sys.stdout
+ orig_stderr = sys.stderr
+ """.replace(' ', '').lstrip()
+ self.process.stdin.write(init_code.encode())
+ self.process.stdin.flush()
+
+ def execute(self, code):
+ # This wrapping ensures that we can run almost any reasonable
+ # code and capture what it does.
+ wrapper = """
+ fake_stdout = cStringIO.StringIO()
+ fake_stderr = cStringIO.StringIO()
+ sys.stdout = fake_stdout
+ sys.stderr = fake_stderr
+ exc = None
+ try:
+ {code}
+ except Exception as e:
+ exc = '%s: %s' % (type(e).__name__, str(e))
+ # This newline is needed to end the `try` statement.
+
+ sys.stdout = orig_stdout
+ sys.stderr = orig_stderr
+ out_data = fake_stdout.getvalue()
+ err_data = fake_stderr.getvalue()
+ fake_stdout.close()
+ fake_stderr.close()
+ print(json.dumps([exc, out_data, err_data]))
+ """.replace(' ', '').lstrip()
+ indented_code = textwrap.indent(code, prefix=' '*4)
+ wrapped_code = wrapper.format(code=indented_code)
+ self.process.stdin.write(wrapped_code.encode())
+ self.process.stdin.flush()
+ return str(self.process.stdout.readline().strip(), 'utf-8')
+
+
def run_cmd_as_user(cmd, user):
- pwd_user = getpwnam(user)
- switch_user_fn = mk_switch_user_fn(pwd_user.pw_uid,
- pwd_user.pw_gid)
- # We try to create an environment identical to what's expected
- # inside Tails for the user by logging in (via `su`) as the user,
- # setting up the GNOME shell environment, and extracting the
- # environment via `env`; not that we will run `env` unconditionally
- # since the former command could fail, e.g. if GNOME is not running.
- env_cmd = '. /usr/local/lib/tails-shell-library/gnome.sh && ' + \
- 'export_gnome_env ; ' + \
- 'env'
- wrapped_env_cmd = "su -c '{}' {}".format(env_cmd, user)
- pipe = Popen(wrapped_env_cmd, stdout=PIPE, shell=True)
- env_data = pipe.communicate()[0].decode('utf-8')
- env = dict((line.split('=', 1) for line in env_data.splitlines()))
- cwd = env['HOME']
- return Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True, env=env, cwd=cwd,
- preexec_fn=switch_user_fn)
+ switch_user_fn = mk_switch_user_fn(user)
+ env = get_user_env(user)
+ cwd = env['HOME']
+ return subprocess.Popen(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ shell=True, env=env, cwd=cwd, preexec_fn=switch_user_fn
+ )
+
def main():
- dev = argv[1]
- port = serial.Serial(port = dev, baudrate = 4000000)
- if not port.isOpen():
- port.open()
-
- # Notify systemd that we're ready
- sd_notify('READY=1')
- sd_notify('STATUS=Processing requests...\n')
-
- while True:
- try:
- line = port.readline().decode('utf-8')
- except Exception as e:
- # port must be opened wrong, so we restart everything and pray
- # that it works.
- print(str(e))
- port.close()
- return main()
- try:
- id, cmd_type, user, cmd = loads(line)
- except Exception as e:
- # We had a parse/pack error, so we just send a \0 as an ACK,
- # releasing the client from blocking.
- print(str(e))
- port.write(b"\0")
- continue
- p = run_cmd_as_user(cmd, user)
- if cmd_type == "spawn":
- returncode, stdout, stderr = 0, "", ""
- else:
- stdout_b, stderr_b = p.communicate()
- stdout = stdout_b.decode('utf-8')
- stderr = stderr_b.decode('utf-8')
- returncode = p.returncode
- port.write(dumps([id, returncode, stdout, stderr]).encode('utf-8') + b"\0")
+ port = serial.Serial(port = REMOTE_SHELL_DEV, baudrate = 4000000)
+ python_sessions = dict()
+
+ # Notify systemd that we're ready
+ systemd.daemon.notify('READY=1')
+ systemd.daemon.notify('STATUS=Processing requests...\n')
+
+ while True:
+ line = port.readline().decode('utf-8')
+ try:
+ id, cmd_type, *rest = json.loads(line)
+ ret = ""
+ if cmd_type in ['sh_call', 'sh_spawn']:
+ user, cmd = rest
+ p = run_cmd_as_user(cmd, user)
+ if cmd_type == "sh_spawn":
+ returncode, stdout, stderr = 0, "", ""
+ else:
+ stdout_b, stderr_b = p.communicate()
+ stdout = stdout_b.decode('utf-8')
+ stderr = stderr_b.decode('utf-8')
+ returncode = p.returncode
+ ret = json.dumps([id, 'success', returncode, stdout, stderr])
+ elif cmd_type == 'python_execute':
+ user, code = rest
+ if user not in python_sessions:
+ python_sessions[user] = PythonSession(user)
+ session = python_sessions[user]
+ result_str = session.execute(code)
+ result = json.loads(result_str)
+ ret = json.dumps([id, 'success'] + result)
+ elif cmd_type in ['file_read', 'file_write', 'file_append']:
+ path, *rest = rest
+ open_mode = cmd_type[5] + 'b'
+ with open(path, open_mode) as f:
+ if cmd_type == 'file_read':
+ assert(rest == [])
+ ret = str(base64.b64encode(f.read()), 'utf-8')
+ elif cmd_type in ['file_write', 'file_append']:
+ assert(len(rest) == 1)
+ data = base64.b64decode(rest[0])
+ ret = f.write(data)
+ if ret != len(data):
+ raise IOError("we only wrote {} bytes out of {}"
+ .format(ret, len(data)))
+ ret = json.dumps([id, 'success'] + [ret])
+ else:
+ raise ValueError("unknown command type")
+ response = (ret + "\n").encode('utf-8')
+ port.write(response)
+ port.flush()
+ except Exception as e:
+ print("Error caught while processing line:", file=sys.stderr)
+ print(" " + line, file=sys.stderr)
+ print("The error was:", file=sys.stderr)
+ traceback.print_exc(file=sys.stdout)
+ print("-----", file=sys.stderr)
+ sys.stderr.flush()
+ exc_str = '{}: {}'.format(type(e).__name__, str(e))
+ port.write(json.dumps([id, 'error', exc_str]).encode('utf-8') + b"\n")
+ port.flush()
+ continue
if __name__ == "__main__":
- main()
+ main()