From 47543de6fa707b8f82d9b7c9c8af0fda02971f0e Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 12 Jul 2012 16:28:06 -0400 Subject: documentation, cleanup --- exmachina.py | 94 +++++++++++++++++++++++++++++++++++-------------------- init_test.sh | 2 +- test.py | 60 ----------------------------------- test_exmachina.py | 59 ++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 95 deletions(-) delete mode 100755 test.py create mode 100755 test_exmachina.py diff --git a/exmachina.py b/exmachina.py index 1a90c3b..35559da 100755 --- a/exmachina.py +++ b/exmachina.py @@ -1,8 +1,32 @@ #!/usr/bin/env python -# Author: bnewbold -# Date: July 2012 -# License: GPLv3 (see http://www.gnu.org/licenses/gpl-3.0.html) +""" +Author: bnewbold +Date: July 2012 +License: GPLv3 (see http://www.gnu.org/licenses/gpl-3.0.html) + (two helper functions copied from web, as cited below) +Package Requirements: python-augeas, bjsonrpc + +This file implements both ends (privilaged daemon and unprivilaged python +client library) of a crude system configuration message bus, intended for use +(initially) with the Plinth web interface to the FreedomBox operating system. + +The goal is to provide partially-untrusted processes (such as the web interface +running as the www-data user) access to core system configuration files +(through the Augeas library) and daemon control (through the init.d scripts). + +The daemon process (started in the same startup script as Plinth) runs as root +and accepts JSON-RPC method calls through a unix domain socket +(/tmp/exmachina.sock by default). Because file access control may not be +sufficiently flexible for access control, a somewhat-elaborate secret key +mechanism can be used to control access to the RPC mechanism. + +The (optional) shared secret-key mechanism requires clients to first call the +"authenticate" RPC method before any other methods. The secret key is passed to +the server process through stdin at startup (command line arguments could be +snooped by unprivilaged processes), and would presumably be passed on to the +client in the same way. The init_test.sh script demonstrates this mechanism. +""" import os import sys @@ -33,7 +57,7 @@ def execute_service(servicename, action, timeout=10): return "ERROR: so such service" command_list = [script, action] - log.info("running: %s" % command_list) + log.info("executing: %s" % command_list) proc = subprocess.Popen(command_list, bufsize=0, stdout=subprocess.PIPE, @@ -46,13 +70,18 @@ def execute_service(servicename, action, timeout=10): if proc.poll() == None: if float(sys.version[:3]) >= 2.6: proc.terminate() - raise Exception("Timeout: %s" % command_list) + raise Exception("execution timed out (>%d seconds): %s" % + (timeout, command_list)) stdout, stderr = proc.communicate() return stdout, stderr, proc.returncode class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): + # authentication state variable. If not None, still need to authenticate; + # if None then authentication not require or was already successful for + # this instantiation of the Handler. This class variable gets optionally + # overridden on a per-process basis secret_key = None def _setup(self): @@ -60,18 +89,11 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): def authenticate(self, secret_key): if not secret_key.strip() == self.secret_key.strip(): + # fail hard log.error("Authentication failed!") sys.exit() self.secret_key = None - def test_whattime(self): - log.debug("whattime") - return time.time() - - def test_listfiles(self): - log.debug("listfiles") - return self.augeas.match("/files/etc/*") - # ------------- Augeas API Passthrough ----------------- def augeas_save(self): if not self.secret_key: @@ -120,7 +142,7 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): log.info("augeas: remove %s" % path) return self.augeas.remove(path.encode('utf-8')) - # ------------- Service Control ----------------- + # ------------- init.d Service Control ----------------- def initd_status(self, servicename): if not self.secret_key: return execute_service(servicename, "status") @@ -138,9 +160,23 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): return execute_service(servicename, "restart") class EmptyClass(): + # Used by ExMachinaClient below pass class ExMachinaClient(): + """Simple client wrapper library to expose augeas and init.d methods. + + In brief, use augeas.get/set/insert to modify system configuration files + under the /files/etc/* namespace. augeas.match with a wildcard can be used + to find variables to edit. + + After making any changes, use augeas.save to commit to disk, then + initd.restart to restart the appropriate system daemons. In many cases, + this would be the 'networking' meta-daemon. + + See test_exmachina.py for some simple examples; see the augeas docs for + more in depth guidance. + """ def __init__(self, socket_path="/tmp/exmachina.sock", @@ -171,8 +207,7 @@ class ExMachinaClient(): def close(self): self.sock.close() -def run_server(socket_path="/tmp/exmachina.sock", secret_key=None): - # TODO: check for root permissions, warn if not root +def run_server(socket_path, secret_key=None): if not 0 == os.geteuid(): log.warn("Expected to be running as root!") @@ -183,31 +218,18 @@ def run_server(socket_path="/tmp/exmachina.sock", secret_key=None): sock.bind(socket_path) sock.listen(1) + # TODO: www-data group permissions only? os.chmod(socket_path, 0666) if secret_key: ExMachinaHandler.secret_key = secret_key - """ - (conn, addr) = sock.accept() - print addr - msg = conn.recv(1024).strip() - if not msg == secret_key: - print "|%s| != |%s|" % (msg, secret_key) - log.error("Didn't receive secret key at socket initialization!") - conn.close() - sys.exit() - # now that connection is established, lock the pipe - os.chmod(socket_path, 0600) - log.info("Auth!") - """ - serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler) - # TODO: www-data group permissions only? - #os.chmod(socket_path, 0666) + serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler) serv.serve() def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): """ From: http://www.noah.org/wiki/Daemonize_Python + This forks the current process into a daemon. The stdin, stdout, and stderr arguments are file names that will be opened and be used to replace the standard file descriptors in sys.stdin, sys.stdout, and sys.stderr. @@ -274,6 +296,10 @@ def main(): default=False, help="Just dump a random base64 key and exit", action="store_true") + parser.add_option("-s", "--socket-path", + default="/tmp/exmachina.sock", + help="UNIX Domain socket file path to listen on", + metavar="FILE") parser.add_option("--pid-file", default=None, help="Daemonize and write pid to this file", @@ -309,7 +335,7 @@ def main(): if options.pid_file: with open(options.pid_file, 'w') as pfile: - # ensure file is available + # ensure file is available/writable pass os.unlink(options.pid_file) daemonize() @@ -318,7 +344,7 @@ def main(): pfile.write("%s" % pid) log.info("Daemonized, pid is %s" % pid) - run_server(secret_key=secret_key) + run_server(secret_key=secret_key, socket_path=options.socket_path) if __name__ == '__main__': main() diff --git a/init_test.sh b/init_test.sh index d1e6f7d..4a00734 100755 --- a/init_test.sh +++ b/init_test.sh @@ -6,7 +6,7 @@ export key=`./exmachina.py --random-key` echo $key | ./exmachina.py -vk --pid-file /tmp/exmachina_test.pid sleep 1 -echo $key | ./test.py -k +echo $key | sudo -u www-data ./test_exmachina.py -k kill `cat /tmp/exmachina_test.pid` && rm /tmp/exmachina_test.pid sleep 1 diff --git a/test.py b/test.py deleted file mode 100755 index 240b47d..0000000 --- a/test.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python - -""" -To use with secret keys, do: - - $ echo "" | ./test.py -k - -""" - -import sys -import optparse -import logging -import socket - -import bjsonrpc -import bjsonrpc.connection -import augeas - -from exmachina import ExMachinaClient - -# ============================================================================= -# Command line handling -def main(): - - socket_path="/tmp/exmachina.sock" - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(socket_path) - - secret_key = None - if sys.argv[-1] == "-k": - print "waiting for key on stdin..." - secret_key = sys.stdin.readline() - print "sent!" - - print "========= Testing low level connection" - c = bjsonrpc.connection.Connection(sock) - if secret_key: - c.call.authenticate(secret_key) - print "time: %s" % c.call.test_whattime() - print "/*: %s" % c.call.augeas_match("/*") - print "/augeas/*: %s" % c.call.augeas_match("/augeas/*") - print "/etc/* files:" - for name in c.call.augeas_match("/files/etc/*"): - print "\t%s" % name - print c.call.initd_status("bluetooth") - print "hostname: %s" % c.call.augeas_get("/files/etc/hostname/*") - print "localhost: %s" % c.call.augeas_get("/files/etc/hosts/1/canonical") - sock.close() - - print "========= Testing user client library" - client = ExMachinaClient(secret_key=secret_key) - print client.augeas.match("/files/etc/*") - print client.initd.restart("bluetooth") - print client.initd.status("greentooth") - print "(expect Error on the above line)" - print client.initd.status("bluetooth") - client.close() - -if __name__ == '__main__': - main() diff --git a/test_exmachina.py b/test_exmachina.py new file mode 100755 index 0000000..6c2a97d --- /dev/null +++ b/test_exmachina.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +""" +To use with secret keys, do: + + $ echo "" | ./test.py -k + +""" + +import sys +import optparse +import logging +import socket + +import bjsonrpc +import bjsonrpc.connection +import augeas + +from exmachina import ExMachinaClient + +# ============================================================================= +# Command line handling +def main(): + + socket_path="/tmp/exmachina.sock" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(socket_path) + + secret_key = None + if sys.argv[-1] == "-k": + print "waiting for key on stdin..." + secret_key = sys.stdin.readline() + print "sent!" + + print "========= Testing JSON-RPC connection" + c = bjsonrpc.connection.Connection(sock) + if secret_key: + c.call.authenticate(secret_key) + print "/*: %s" % c.call.augeas_match("/*") + print "/augeas/*: %s" % c.call.augeas_match("/augeas/*") + print "/etc/* files:" + for name in c.call.augeas_match("/files/etc/*"): + print "\t%s" % name + print c.call.initd_status("bluetooth") + print "hostname: %s" % c.call.augeas_get("/files/etc/hostname/*") + print "localhost: %s" % c.call.augeas_get("/files/etc/hosts/1/canonical") + sock.close() + + print "========= Testing user client library" + client = ExMachinaClient(secret_key=secret_key) + print client.augeas.match("/files/etc/*") + #print client.initd.restart("bluetooth") + print client.initd.status("greentooth") + print "(expect Error on the above line)" + print client.initd.status("bluetooth") + client.close() + +if __name__ == '__main__': + main() -- cgit v1.2.3