From 20d4f961b68c2848b40b8f8b7061b4d1bc9082bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20O=C5=BCarowski?=
Date: Tue, 10 Sep 2013 23:12:46 +0200
Subject: [PATCH 01/13] use stdlib's json module if simplejson is missing
json is available in Python >= 2.6
---
fabfile.py | 5 ++++-
filedict.py | 5 ++++-
menu.py | 5 ++++-
modules/installed/lib/user_store.py | 5 ++++-
modules/installed/santiago/santiago.py | 5 ++++-
modules/installed/system/config.py | 5 ++++-
modules/installed/system/expert_mode.py | 5 ++++-
modules/installed/system/system.py | 5 ++++-
modules/installed/system/wan.py | 5 ++++-
9 files changed, 36 insertions(+), 9 deletions(-)
diff --git a/fabfile.py b/fabfile.py
index 4b4d25410..a9a95d09c 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -7,7 +7,10 @@
# plinth box
import os,sys, subprocess
-import simplejson as json
+try:
+ import simplejson as json
+except ImportError:
+ import json
import fabric.api
from fabric.api import local, env, cd, put, get, task
diff --git a/filedict.py b/filedict.py
index 1e76d67da..d4f9a35ad 100644
--- a/filedict.py
+++ b/filedict.py
@@ -5,7 +5,10 @@ Author: Erez Shinan
Date : 31-May-2009
"""
-import simplejson as json ## jlv replaced pickle with json
+try:
+ import simplejson as json ## jlv replaced pickle with json
+except ImportError:
+ import json
import UserDict
##import cPickle as pickle
diff --git a/menu.py b/menu.py
index 79f220537..90b0af896 100644
--- a/menu.py
+++ b/menu.py
@@ -1,5 +1,8 @@
-import simplejson as json
from urlparse import urlparse
+try:
+ import simplejson as json
+except ImportError:
+ import json
import cherrypy
import cfg
diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py
index 236b73a8f..f8ece84e7 100644
--- a/modules/installed/lib/user_store.py
+++ b/modules/installed/lib/user_store.py
@@ -1,5 +1,8 @@
import os
-import simplejson as json
+try:
+ import simplejson as json
+except ImportError:
+ import json
import cherrypy
import cfg
from model import User
diff --git a/modules/installed/santiago/santiago.py b/modules/installed/santiago/santiago.py
index 7cf11e538..6d179c50b 100644
--- a/modules/installed/santiago/santiago.py
+++ b/modules/installed/santiago/santiago.py
@@ -7,7 +7,10 @@ haven't figured that one all the way through yet.
import os, sys
import cherrypy
-import simplejson as json
+try:
+ import simplejson as json
+except ImportError:
+ import json
from gettext import gettext as _
from plugin_mount import PagePlugin
from modules.auth import require
diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py
index b671fcc12..a987a0394 100644
--- a/modules/installed/system/config.py
+++ b/modules/installed/system/config.py
@@ -1,7 +1,10 @@
import os, subprocess
from socket import gethostname
import cherrypy
-import simplejson as json
+try:
+ import simplejson as json
+except ImportError:
+ import json
from gettext import gettext as _
from filedict import FileDict
from modules.auth import require
diff --git a/modules/installed/system/expert_mode.py b/modules/installed/system/expert_mode.py
index da8dbc5aa..e69e08bcf 100644
--- a/modules/installed/system/expert_mode.py
+++ b/modules/installed/system/expert_mode.py
@@ -1,6 +1,9 @@
import os
import cherrypy
-import simplejson as json
+try:
+ import simplejson as json
+except ImportError:
+ import json
from gettext import gettext as _
from filedict import FileDict
from modules.auth import require
diff --git a/modules/installed/system/system.py b/modules/installed/system/system.py
index 68a7a50ff..eba279a90 100644
--- a/modules/installed/system/system.py
+++ b/modules/installed/system/system.py
@@ -1,6 +1,9 @@
import os
import cherrypy
-import simplejson as json
+try:
+ import simplejson as json
+except ImportError:
+ import json
from gettext import gettext as _
from filedict import FileDict
from auth import require
diff --git a/modules/installed/system/wan.py b/modules/installed/system/wan.py
index 0817f62c4..394f67d6e 100644
--- a/modules/installed/system/wan.py
+++ b/modules/installed/system/wan.py
@@ -1,6 +1,9 @@
import os
import cherrypy
-import simplejson as json
+try:
+ import simplejson as json
+except ImportError:
+ import json
from gettext import gettext as _
from filedict import FileDict
from modules.auth import require
From bad89393891761334e37b611856449ede3a99470 Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Wed, 11 Sep 2013 13:46:37 +0200
Subject: [PATCH 02/13] Fix typo blocking access to exmachina, and report the
problem if unable to load the exmachina client library.
---
plinth.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/plinth.py b/plinth.py
index b3ada9d71..3dfbe0c31 100755
--- a/plinth.py
+++ b/plinth.py
@@ -114,9 +114,10 @@ def setup():
pass
try:
- from exmachina import ExMachinaClient
+ from exmachina.exmachina import ExMachinaClient
except ImportError:
cfg.exmachina = None
+ print "unable to import exmachina client library, but continuing anyways..."
else:
try:
cfg.exmachina = ExMachinaClient(
From 71873b6702dafcb184961eebec54a0ac0772cdaa Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Wed, 11 Sep 2013 14:04:34 +0200
Subject: [PATCH 03/13] Change first_boot module to show and update current
hostname, instead of showing the box_name value and failing to set anything
when the user specify a host name value.
---
modules/installed/first_boot.py | 30 +++++++++++-------------------
modules/installed/system/config.py | 5 ++++-
2 files changed, 15 insertions(+), 20 deletions(-)
diff --git a/modules/installed/first_boot.py b/modules/installed/first_boot.py
index ff3a27e63..24c422246 100644
--- a/modules/installed/first_boot.py
+++ b/modules/installed/first_boot.py
@@ -7,6 +7,7 @@ from forms import Form
import util as u
from withsqlite.withsqlite import sqlite_db
import cfg
+import config
class FirstBoot(PagePlugin):
def __init__(self, *args, **kwargs):
@@ -18,18 +19,13 @@ class FirstBoot(PagePlugin):
return self.state0(*args, **kwargs)
## TODO: flesh out these tests values
- def valid_box_name_p(self, name):
- name = name.strip()
- if re.search("\W", name):
- return False
- return True
def valid_box_key_p(self, key):
return True
def generate_box_key(self):
return "fake key"
@cherrypy.expose
- def state0(self, message="", box_name="", box_key="", submitted=False):
+ def state0(self, message="", hostname="", box_key="", submitted=False):
"""
In this state, we do time config over HTTP, name the box and
server key selection.
@@ -49,15 +45,13 @@ class FirstBoot(PagePlugin):
## Must resist the sick temptation to write an LDAP interface to the sqlite file
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as db:
db['about'] = "This table is for information about this FreedomBox"
- if box_name:
- if self.valid_box_name_p(box_name):
- db['box_name'] = box_name
+ if hostname:
+ if '' == config.valid_hostname(hostname):
+ config.set_hostname(hostname)
else:
- message += _("Invalid box name.")
- elif 'box_name' in db and db['box_name']:
- box_name = db['box_name']
- #TODO: set /etc/hostname to box name via ex machina
-
+ message += _("Invalid box name: %s") % config.valid_hostname(hostname)
+ else:
+ hostname = config.get_hostname()
if box_key:
if self.valid_box_key_p(box_key):
db['box_key'] = box_key
@@ -70,7 +64,7 @@ class FirstBoot(PagePlugin):
db['box_key'] = box_key
- if box_name and box_key and self.valid_box_name_p(box_name) and self.valid_box_key_p(box_key):
+ if hostname and box_key and '' == config.valid_hostname(hostname) and self.valid_box_key_p(box_key):
## Update state to 1 and head there
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
db['state']=1
@@ -81,11 +75,9 @@ class FirstBoot(PagePlugin):
action="/firstboot",
name="whats_my_name",
message=message)
- if not box_name:
- box_name = cfg.box_name
form.html("For convenience, your FreedomBox needs a name. It should be something short that doesn't contain spaces or punctuation. 'Willard' would be a good name. 'Freestyle McFreedomBox!!!' would not.
")
- form.text_input('Name your FreedomBox', id="box_name", value=box_name)
- form.html("%(box_name)s uses cryptographic keys so it can prove its identity when talking to you. %(box_name)s can make a key for itself, but if one already exists (from a prior FreedomBox, for example), you can paste it below. This key should not be the same as your key because you are not your FreedomBox!
" % {'box_name':cfg.box_name})
+ form.text_input('Name your FreedomBox', id="hostname", value=hostname)
+ form.html("%(hostname)s uses cryptographic keys so it can prove its identity when talking to you. %(hostname)s can make a key for itself, but if one already exists (from a prior FreedomBox, for example), you can paste it below. This key should not be the same as your key because you are not your FreedomBox!
" % {'hostname':cfg.box_name})
form.text_box("If you want, paste your box's key here.", id="box_key", value=box_key)
form.hidden(name="submitted", value="True")
form.submit("Box it up!")
diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py
index b671fcc12..e51c6d8c8 100644
--- a/modules/installed/system/config.py
+++ b/modules/installed/system/config.py
@@ -40,6 +40,9 @@ def valid_hostname(name):
return message
+def get_hostname():
+ return gethostname()
+
def set_hostname(hostname):
"Sets machine hostname to hostname"
cfg.log.info("Writing '%s' to /etc/hostname with exmachina" % hostname)
@@ -53,7 +56,7 @@ def set_hostname(hostname):
if platform.linux_distribution()[0]=="Ubuntu" :
cfg.exmachina.service.start("hostname")
else:
- cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only?
+ cfg.exmachina.initd.start("hostname.sh") # is hostname.sh debian-only?
except OSError, e:
raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e)
From 6effc94349b0947c0f0638dcaab23a33a56eb869 Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Wed, 11 Sep 2013 20:11:45 +0200
Subject: [PATCH 04/13] Fix expert user access checks. Several places in the
code, cfg.users.expert() is used as a boolean test to see if the current user
is an expert user. But this do not work. Change the implementation of
expert() to assume the current user if no argument is given, to get the code
working.
---
modules/installed/lib/user_store.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py
index 236b73a8f..1512472be 100644
--- a/modules/installed/lib/user_store.py
+++ b/modules/installed/lib/user_store.py
@@ -16,7 +16,12 @@ class UserStore(UserStoreModule, sqlite_db):
def close(self):
self.__exit__(None,None,None)
+ def currentuser(self):
+ return cherrypy.session.get(cfg.session_key)
+
def expert(self, username=None):
+ if username is None:
+ username = self.currentuser()
groups = self.attr(username,"groups")
if not groups:
return False
From 1615b7818b5b6d8936702c2d8d397efcffe1c5ea Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Wed, 11 Sep 2013 20:13:46 +0200
Subject: [PATCH 05/13] Give sensible feedback for non-expert users visiting
system/configure. Explain that only expert users get access, instead of only
showing an title.
---
modules/installed/system/config.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py
index b671fcc12..bc27c878c 100644
--- a/modules/installed/system/config.py
+++ b/modules/installed/system/config.py
@@ -62,10 +62,6 @@ class general(FormPlugin, PagePlugin):
order = 30
def help(self, *args, **kwargs):
-
- ## only expert users are going to get deep enough to see any timestamps
- if not cfg.users.expert():
- return ''
return _(#"""Time Zone
"""Set your timezone to get accurate
timestamps. %(product)s will use this information to set your
@@ -73,6 +69,9 @@ class general(FormPlugin, PagePlugin):
""" % {'product':cfg.product_name, 'box':cfg.box_name})
def main(self, message='', **kwargs):
+ if not cfg.users.expert():
+ return '
' + _('Only members of the expert group are allowed to see and modify the system setup.') + '
'
+
sys_store = filedict_con(cfg.store_file, 'sys')
hostname = cfg.exmachina.augeas.get("/files/etc/hostname/*")
# this layer of persisting configuration in sys_store could/should be
From 7ff6ea14e2f3ec8607c5c3556c9385927bb2cb06 Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Wed, 11 Sep 2013 20:31:12 +0200
Subject: [PATCH 06/13] Rewrite fix for UserStore.expert() to be more like
UserStoreOld.expert().
---
modules/installed/lib/user_store.py | 19 ++++++++++++++-----
1 file changed, 14 insertions(+), 5 deletions(-)
diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py
index 1512472be..0e7c5ecab 100644
--- a/modules/installed/lib/user_store.py
+++ b/modules/installed/lib/user_store.py
@@ -16,12 +16,21 @@ class UserStore(UserStoreModule, sqlite_db):
def close(self):
self.__exit__(None,None,None)
- def currentuser(self):
- return cherrypy.session.get(cfg.session_key)
-
+ def current(self, name=False):
+ """Return current user, if there is one, else None.
+ If name = True, return the username instead of the user."""
+ try:
+ username = cherrypy.session.get(cfg.session_key)
+ if name:
+ return username
+ else:
+ return self.get(username)
+ except AttributeError:
+ return None
+
def expert(self, username=None):
- if username is None:
- username = self.currentuser()
+ if not username:
+ username = self.current(name=True)
groups = self.attr(username,"groups")
if not groups:
return False
From 657068b0263ce15afcf63055adec3a771b8bdb5d Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Thu, 12 Sep 2013 11:05:08 +0200
Subject: [PATCH 07/13] Import exmachina into plint, using source from Tom
Galloway.
---
Makefile | 2 +
exmachina/README | 105 +++++++
exmachina/exmachina.py | 557 ++++++++++++++++++++++++++++++++++++
exmachina/init_test.sh | 13 +
exmachina/test_exmachina.py | 85 ++++++
plinth.py | 2 +-
start.sh | 4 +-
test.sh | 2 +-
8 files changed, 766 insertions(+), 4 deletions(-)
create mode 100644 exmachina/README
create mode 100755 exmachina/exmachina.py
create mode 100755 exmachina/init_test.sh
create mode 100755 exmachina/test_exmachina.py
diff --git a/Makefile b/Makefile
index 4273b87d2..65622287b 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,8 @@ install: default
$(DESTDIR)/usr/share/doc/plinth $(DESTDIR)/usr/share/man/man1
cp -a static themes $(DESTDIR)$(DATADIR)/
cp -a *.py modules templates $(DESTDIR)$(PYDIR)/
+ mkdir -p $(DESTDIR)$(PYDIR)/exmachina
+ cp -a exmachina/exmachina.py $(DESTDIR)$(PYDIR)/exmachina/.
cp share/init.d/plinth $(DESTDIR)/etc/init.d
install plinth $(DESTDIR)/usr/bin/
mkdir -p $(DESTDIR)/var/lib/plinth/cherrypy_sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run
diff --git a/exmachina/README b/exmachina/README
new file mode 100644
index 000000000..6216029ad
--- /dev/null
+++ b/exmachina/README
@@ -0,0 +1,105 @@
+ _ _
+ _____ ___ __ ___ __ _ ___| |__ (_)_ __ __ _
+ / _ \ \/ / '_ ` _ \ / _` |/ __| '_ \| | '_ \ / _` |
+ | __/> <| | | | | | (_| | (__| | | | | | | | (_| |
+ \___/_/\_\_| |_| |_|\__,_|\___|_| |_|_|_| |_|\__,_|
+
+
+
+### DISCLAIMER
+
+ ----- ACHTUNG! WARNING! DANGER! ----
+
+This code is hackish and not "production quality. It represents a potential
+approach to a specific problem (privilege separation for system configuration).
+It has not been extensively reviewed or tested and does not represent a known
+best practice.
+
+### What is this?
+
+exmachina is a small system configuration system which runs as separate but
+coupled client/server UNIX processes for the purpose of privilege separation:
+the "server" process runs with root privileges and a python program using the
+"client" library runs as any unprivileged user. The commands and parameters
+that the client can send to the server are limited, though in this particular
+case can of course be used to deny service (reboot or shutdown the machine) or
+probably escalate privileges one way or another (install arbitrary packages,
+reconfigure networks, enable callback scripts, edit system configuration
+files).
+
+The server and client processes should be one-to-one: only one client should
+ever connect to the server. The init_test.sh script shows how this could be
+achieved in a SysV-style /etc/init.d script.
+
+The intended use case is writing a user-friendly web control panel for a Debian
+server or router: the web designer creating the user interface should not be
+overly concerned with writing secure code, and the web application itself
+(possibly including lots of third party framework code, javascript libraries,
+etc) should not run with strong system permissions, but core components of the
+system (such as hostname, wireless access point configuration, network
+settings, package installation, locale, timezone, etc) need to be modified.
+
+See the comments in exmachina.py for more information.
+
+### Alternatives
+
+The most simple alternative to exmachina that has been recommended to me is to
+create simple setuid/setgid programs or scripts to execute privileged system
+changes, and to only allow execute permissions to those programs for the
+user/group of the less-trusted user interface program. This seems to be the
+current best practice. For the more complicated case of generalized system
+configuration, the setuid/setgid program becomes complicated, or you need to
+write and install many of them, but this is no worse that the situation with
+exmachina.
+
+Another approach is the Assuan protocol used by GPG, which has been generalized
+as libassuan:
+
+ "Assuan permits the servers, which do the actual work, e.g. encryption and
+ decryption of data using a secret key, to be developed independently of the
+ user interfaces, e.g. mail clients and other encryption front ends."
+
+ http://www.gnupg.org/related_software/libassuan/index.en.html
+
+### Status
+
+Basic server and client functionality implemented. Crude, and far more simple
+than it may appear or the length of code would imply.
+
+This was code was written in a weekend "sprint" for the FreedomBox project and
+their Plinth web user interface in 2012.
+
+I may or may not maintain this code. I have hesitation even publishing it
+because i'm almost certain there are implementation bugs and that the entire
+concept is problematic.
+
+Features:
+* shared secret key process/privilege separation
+* call augeas API: match, set, setm, get, save, move, insert, remove
+* call init.d service scripts: status, start, stop, restart
+
+In late 2012 Nick Daly (of the FreedomBox project) wrote up a brief audit of
+this code and concept on his blog (https://www.betweennowhere.net/). Link is
+frequantly broken.
+
+### Dependencies (server)
+
+* augeas configuration editing library
+* python-augeas wrapper for augeas
+* bjsonrpc python library
+
+On debian (wheezy) try:
+
+ $ sudo apt-get install augeas-tools python-bjsonrpc python-augeas
+
+### Dependencies (client)
+
+* bjsonrpc
+
+On debian (wheezy) try:
+
+ $ sudo apt-get install bjsonrpc
+
+### License
+
+exmachina.py is GPLv3 or later
diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py
new file mode 100755
index 000000000..4fe6e75aa
--- /dev/null
+++ b/exmachina/exmachina.py
@@ -0,0 +1,557 @@
+#!/usr/bin/env python
+
+"""
+Author: bnewbold
+Date: July 2012
+License: GPLv3 or later (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.
+
+Note that the authentication mechanism only tells the server that the client
+seems to be legitimate, it doesn't prevent a rapid "man in the middle" style
+attack on the client, which could feed back malicious information.
+
+Alternatively, an optional user or group can be specified and the socket file
+will have it's ownership and permissions changed appropriately.
+
+Note that the socket path would need to be changed on a per-application basis
+so that competing daemons don't block/clobber each other.
+"""
+
+import os
+import sys
+import grp
+import shutil
+import argparse
+import logging
+import socket
+import subprocess
+import time
+import base64
+import functools
+import hashlib
+import atexit
+import stat
+import pwd
+
+import bjsonrpc
+import bjsonrpc.handlers
+import bjsonrpc.server
+import augeas
+
+log = logging.getLogger(__name__)
+
+# hackish way to enforce single client connection
+allow_connect = True
+
+def execute_service(servicename, action, timeout=10):
+ """This function mostly ripped from StackOverflow:
+ http://stackoverflow.com/questions/1556348/python-run-a-process-with-timeout-and-capture-stdout-stderr-and-exit-status
+ """
+ # ensure service name isn't tricky trick
+ script = "/etc/init.d/" + os.path.split(servicename)[1]
+
+ if not os.path.exists(script):
+ raise ValueError("so such service: %s" % servicename)
+
+ command_list = [script, action]
+ log.info("executing: %s" % command_list)
+ proc = subprocess.Popen(command_list,
+ bufsize=0,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ poll_seconds = .250
+ deadline = time.time() + timeout
+ while time.time() < deadline and proc.poll() is None:
+ time.sleep(poll_seconds)
+
+ if proc.poll() is None:
+ if float(sys.version[:3]) >= 2.6:
+ proc.terminate()
+ raise Exception("execution timed out (>%d seconds): %s" %
+ (timeout, command_list))
+
+ stdout, stderr = proc.communicate()
+ # TBD: should raise exception here if proc.returncode != 0?
+ return stdout, stderr, proc.returncode
+
+def execute_apt(packagename, action, timeout=120, aptargs=['-q', '-y']):
+ # ensure package name isn't tricky trick
+ if action != "update" \
+ and (packagename != packagename.strip().split()[0] \
+ or packagename.startswith('-')):
+ raise ValueError("Not a good apt package name: %s" % packagename)
+
+ if action == "update":
+ command_list = ['apt-get', action]
+ else:
+ command_list = ['apt-get', action, packagename]
+ command_list.extend(aptargs)
+ log.info("executing: %s" % command_list)
+ proc = subprocess.Popen(command_list,
+ bufsize=0,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ poll_seconds = .250
+ deadline = time.time() + timeout
+ while time.time() < deadline and proc.poll() is None:
+ time.sleep(poll_seconds)
+
+ if proc.poll() is None:
+ if float(sys.version[:3]) >= 2.6:
+ proc.terminate()
+ raise Exception("execution timed out (>%d seconds): %s" %
+ (timeout, command_list))
+
+ stdout, stderr = proc.communicate()
+ return stdout, stderr, proc.returncode
+
+def authreq(fn):
+ """
+ Decorator to force authentication before allowing calls to a method
+ """
+ @functools.wraps(fn)
+ def wrappedfunc(self, *args, **kwargs):
+ if not self.secret_key:
+ return fn(self, *args, **kwargs)
+ else:
+ log.error("Unauthorized function call attempt; bailing")
+ sys.exit(-1)
+ return wrappedfunc
+
+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):
+ global allow_connect
+ if not allow_connect:
+ log.error("second client tried to connect, exiting")
+ sys.exit(-1)
+ allow_connect = False
+ self.augeas = augeas.Augeas()
+
+ def _shutdown(self):
+ # Server shuts down after a single client connection closes
+ log.info("connection closing, server exiting")
+ sys.exit(-1)
+
+ def authenticate(self, secret_key):
+ if not self.secret_key:
+ log.warn("Unecessary authentication attempt")
+ return
+ if not hashlib.sha256(secret_key.strip()).hexdigest() == \
+ hashlib.sha256(self.secret_key.strip()).hexdigest():
+ # key doesn't match, fail hard
+ log.error("Authentication failed!")
+ sys.exit()
+ self.secret_key = None
+
+ def need_to_auth(self):
+ """
+ Helper for clients to learn whether they still need to authenticate
+ """
+ return self.secret_key != None
+
+ # ------------- Augeas API Passthrough -----------------
+ @authreq
+ def augeas_save(self):
+ log.info("augeas: saving config")
+ return self.augeas.save()
+
+ @authreq
+ def augeas_set(self, path, value):
+ log.info("augeas: set %s=%s" % (path, value))
+ return self.augeas.set(path.encode('utf-8'),
+ value.encode('utf-8'))
+
+ @authreq
+ def augeas_setm(self, base, sub, value):
+ log.info("augeas: setm %s %s = %s" % (base, sub, value))
+ return self.augeas.setm(base.encode('utf-8'),
+ sub.encode('utf-8'),
+ value.encode('utf-8'))
+
+ @authreq
+ def augeas_get(self, path):
+ # reduce verbosity
+ log.debug("augeas: get %s" % path)
+ return self.augeas.get(path.encode('utf-8'))
+
+ @authreq
+ def augeas_match(self, path):
+ # reduce verbosity
+ log.debug("augeas: match %s" % path)
+ return self.augeas.match("%s" % path.encode('utf-8'))
+
+ @authreq
+ def augeas_insert(self, path, label, before=True):
+ log.info("augeas: insert %s=%s" % (path, value))
+ return self.augeas.insert(path.encode('utf-8'),
+ label.encode('utf-8'),
+ before=before)
+
+ @authreq
+ def augeas_move(self, src, dst):
+ log.info("augeas: move %s -> %s" % (src, dst))
+ return self.augeas.move(src.encode('utf-8'), dst.encode('utf-8'))
+
+ @authreq
+ def augeas_remove(self, path):
+ log.info("augeas: remove %s" % path)
+ return self.augeas.remove(path.encode('utf-8'))
+
+ # ------------- Misc. non-Augeas Helpers -----------------
+ @authreq
+ def set_timezone(self, tzname):
+ log.info("reset timezone to %s" % tzname)
+ tzname = tzname.strip()
+ tzpath = os.path.join("/usr/share/zoneinfo", tzname)
+ try:
+ os.stat(tzpath)
+ except OSError:
+ # file not found
+ raise ValueError("timezone not valid: %s" % tzname)
+ shutil.copy(
+ os.path.join("/usr/share/zoneinfo", tzname),
+ "/etc/localtime")
+ with open("/etc/timezone", "w") as tzfile:
+ tzfile.write(tzname + "\n")
+ return "timezone changed to %s" % tzname
+
+ # ------------- init.d Service Control -----------------
+ @authreq
+ def initd_status(self, servicename):
+ return execute_service(servicename, "status")
+
+ @authreq
+ def initd_start(self, servicename):
+ return execute_service(servicename, "start")
+
+ @authreq
+ def initd_stop(self, servicename):
+ return execute_service(servicename, "stop")
+
+ @authreq
+ def initd_restart(self, servicename):
+ return execute_service(servicename, "restart")
+
+ # ------------- apt-get Package Control -----------------
+ @authreq
+ def apt_install(self, packagename):
+ return execute_apt(packagename, "install")
+
+ @authreq
+ def apt_update(self):
+ return execute_apt("", "update")
+
+ @authreq
+ def apt_remove(self, packagename):
+ return execute_apt(packagename, "remove")
+
+
+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",
+ secret_key=None):
+
+ if secret_key:
+ secret_key = hashlib.sha256(secret_key.strip() + "|exmachina")\
+ .hexdigest()
+
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.sock.connect(socket_path)
+ self.conn = bjsonrpc.connection.Connection(self.sock)
+
+ if self.conn.call.need_to_auth():
+ if secret_key:
+ self.conn.call.authenticate(secret_key)
+ else:
+ self.conn.close()
+ raise Exception(
+ "authentication required but no secret_key passed")
+ elif secret_key:
+ print "secret_key passed but no authentication required; ignoring"
+
+ self.augeas = EmptyClass()
+ self.initd = EmptyClass()
+ self.apt = EmptyClass()
+ self.misc = EmptyClass()
+
+ self.augeas.save = self.conn.call.augeas_save
+ self.augeas.set = self.conn.call.augeas_set
+ self.augeas.setm = self.conn.call.augeas_setm
+ self.augeas.get = self.conn.call.augeas_get
+ self.augeas.match = self.conn.call.augeas_match
+ self.augeas.insert = self.conn.call.augeas_insert
+ self.augeas.move = self.conn.call.augeas_move
+ self.augeas.remove = self.conn.call.augeas_remove
+ self.initd.status = self.conn.call.initd_status
+ self.initd.start = self.conn.call.initd_start
+ self.initd.stop = self.conn.call.initd_stop
+ self.initd.restart = self.conn.call.initd_restart
+ self.apt.install = self.conn.call.apt_install
+ self.apt.update = self.conn.call.apt_update
+ self.apt.remove = self.conn.call.apt_remove
+ self.misc.set_timezone = self.conn.call.set_timezone
+
+ def close(self):
+ self.sock.close()
+
+
+def run_server(socket_path, secret_key=None, socket_group=None,
+ socket_user=None):
+
+ if secret_key:
+ secret_key = hashlib.sha256(secret_key.strip() + "|exmachina")\
+ .hexdigest()
+
+ if not 0 == os.geteuid():
+ log.warn("Expected to be running as root!")
+ if socket_group or socket_user:
+ log.error("Can't change socket permissions if non-root, exiting")
+ sys.exit(-1)
+
+ # check if the socket was left open after a previous run, overwrite it
+ if os.path.exists(socket_path):
+ if not stat.S_ISSOCK(os.stat(socket_path).st_mode):
+ log.error("socket_path exists and isn't a stale socket: %s" %
+ socket_path)
+ sys.exit(-1)
+ # socket file exists, need to check if it's stale from a previous
+ # session
+ test_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ test_sock.connect(socket_path)
+ log.error("socket_path already exists and seems to be active: %s" %
+ socket_path)
+ test_sock.close()
+ sys.exit(-1)
+ except Exception, e:
+ print e
+ # if we got this far it's probably a stale socket and should be
+ # destroyed
+ log.warn("Clobbering pre-existing socket: %s" % socket_path)
+ os.unlink(socket_path)
+
+ # open and bind to unix socket
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.bind(socket_path)
+
+ # we just created the socket file, so now let's register an atexit callback
+ # to clean up after ourselves if we get Ctrl-C'd (or exit for any other
+ # reason, including normal cleanup)
+ def delete_socket():
+ os.unlink(socket_path)
+ atexit.register(delete_socket)
+
+ if socket_group is not None:
+ # optionally set group-only permissions on socket file before we start
+ # accepting connections
+ socket_uid = os.stat(socket_path).st_uid
+ socket_gid = grp.getgrnam(socket_group).gr_gid
+ os.chmod(socket_path, 0660)
+ os.chown(socket_path, socket_uid, socket_gid)
+ elif socket_user is not None:
+ # optionally set user-only permissions on socket file before we start
+ # accepting connections
+ pwn = pwd.getpwnam(socket_user)
+ socket_uid = pwn.pw_uid
+ socket_gid = pwn.pw_gid
+ os.chmod(socket_path, 0660)
+ os.chown(socket_path, socket_uid, socket_gid)
+ else:
+ os.chmod(socket_path, 0666)
+
+ # only going to allow a single client, so don't allow queued connections
+ sock.listen(0)
+
+ if secret_key:
+ # key already got hashed above
+ ExMachinaHandler.secret_key = secret_key
+
+ # get bjsonrpc server started. it would make more sense to just listen for
+ # a single client connection and pass that off to the bjsonrpc handler,
+ # then close the socket when that's done, but I don't see an easy way to do
+ # that with the bjsonrpc API, so instead we let it wait indefinately for
+ # connections, but actual only allow one and bail when that one closes.
+ 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.
+ These arguments are optional and default to /dev/null. Note that stderr is
+ opened unbuffered, so if it shares a file with stdout then interleaved
+ output may not appear in the order that you expect. """
+
+ # Do first fork.
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0) # Exit first parent.
+ except OSError, e:
+ sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ # Decouple from parent environment.
+ os.chdir("/")
+ os.umask(0)
+ os.setsid()
+
+ # Do second fork.
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0) # Exit second parent.
+ except OSError, e:
+ sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ # Now I am a daemon!
+
+ # Redirect standard file descriptors.
+ si = open(stdin, 'r')
+ so = open(stdout, 'a+')
+ se = open(stderr, 'a+', 0)
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
+ return pid
+
+# =============================================================================
+# Command line handling
+def main():
+
+ global log
+ parser = argparse.ArgumentParser(usage=
+ "usage: exmachina.py [options]\n"
+ "exmachina.py --help for more info."
+ )
+ parser.add_argument("-v", "--verbose",
+ default=False,
+ help="Show more debugging statements",
+ action="store_true")
+ parser.add_argument("-q", "--quiet",
+ default=False,
+ help="Show fewer informational statements",
+ action="store_true")
+ parser.add_argument("-k", "--key",
+ default=False,
+ help="Wait for Secret Access Key on stdin before starting",
+ action="store_true")
+ parser.add_argument("--random-key",
+ default=False,
+ help="Just dump a random base64 key and exit",
+ action="store_true")
+ parser.add_argument("-s", "--socket-path",
+ default="/tmp/exmachina.sock",
+ help="UNIX Domain socket file path to listen on",
+ metavar="FILE")
+ parser.add_argument("--pidfile",
+ default=None,
+ help="Daemonize and write pid to this file",
+ metavar="FILE")
+ parser.add_argument("-g", "--group",
+ default=None,
+ help="chgrp socket file to this group and set 0660 permissions")
+ parser.add_argument("-u", "--user",
+ default=None,
+ help="chown socket file to this user/group and set 0600 permissions")
+
+ args = parser.parse_args()
+
+ if args.user and args.group:
+ parser.error("set user or group option, but not both")
+
+ #if len(args) != 0:
+ #parser.error("Incorrect number of arguments")
+
+ if args.random_key:
+ sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128)))
+ sys.exit(0)
+
+ log = logging.getLogger()
+ hdlr = logging.StreamHandler()
+ formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+ hdlr.setFormatter(formatter)
+ log.addHandler(hdlr)
+
+ if args.verbose:
+ log.setLevel(logging.DEBUG)
+ elif args.quiet:
+ log.setLevel(logging.ERROR)
+ else:
+ log.setLevel(logging.INFO)
+
+ secret_key = None
+ if args.key:
+ log.debug("Waiting for secret key on stdin...")
+ secret_key = sys.stdin.readline().strip()
+ log.debug("Got it!")
+
+ if args.pidfile:
+ with open(args.pidfile, 'w') as pfile:
+ # ensure file is available/writable
+ pass
+ os.unlink(args.pidfile)
+ daemonize()
+ pid = os.getpid()
+ with open(args.pidfile, 'w') as pfile:
+ pfile.write("%s" % pid)
+ log.info("Daemonized, pid is %s" % pid)
+
+ run_server(secret_key=secret_key,
+ socket_path=args.socket_path,
+ socket_group=args.group,
+ socket_user=args.user)
+
+if __name__ == '__main__':
+ main()
diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh
new file mode 100755
index 000000000..941285d36
--- /dev/null
+++ b/exmachina/init_test.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+# Test init.d-style initialization; run this script as root (or sudo it)
+
+export key=`./exmachina.py --random-key`
+
+echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid -g www-data
+sleep 1
+echo $key | sudo -u www-data -g www-data ./test_exmachina.py -k
+
+kill `cat /tmp/exmachina_test.pid` && rm /tmp/exmachina_test.pid
+sleep 1
+jobs
diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py
new file mode 100755
index 000000000..510d30d3a
--- /dev/null
+++ b/exmachina/test_exmachina.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+
+"""
+This file tests the "client side" of the exmachina layer.
+
+To use with secret keys, do the following in seperate terminals:
+
+ $ echo "" | sudo ./exmachina.py -vk
+ $ echo "" | ./test_exmachina.py -k
+
+To use without, do the following in seperate terminals:
+
+ $ sudo ./exmachina.py -v
+ $ ./test_exmachina.py
+
+Use the init_test.sh script to test shared key passing and privilage seperation
+at the same time:
+
+ $ sudo ./init_test.sh
+"""
+
+import sys
+import socket
+
+import bjsonrpc
+import bjsonrpc.connection
+from bjsonrpc.exceptions import ServerError
+
+from exmachina.exmachina import ExMachinaClient
+
+# =============================================================================
+# Command line handling
+def main():
+
+ secret_key = None
+ if sys.argv[-1] == "-k":
+ print "waiting for key on stdin..."
+ secret_key = sys.stdin.readline()
+ print "got it!"
+
+ """
+ # both tests together won't work now that server exits after single client
+ socket_path = "/tmp/exmachina.sock"
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.connect(socket_path)
+
+ 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")
+ try:
+ print client.initd.status("greentooth")
+ print "ERROR: should have failed above!"
+ except ServerError:
+ print "(got expected error, good!)"
+ print "(expect Error on the above line)"
+ print client.initd.status("bluetooth")
+ print client.apt.install("pkg_which_does_not_exist")
+ print client.apt.remove("pkg_which_does_not_exist")
+ #print client.apt.update() # can be slow...
+ #print client.misc.set_timezone("UTC") # don't clobber system...
+ try:
+ print client.misc.set_timezone("whoopie") # should be an error
+ print "ERROR: should have failed above!"
+ except ServerError:
+ print "(got expected error, good!)"
+ client.close()
+
+if __name__ == '__main__':
+ main()
diff --git a/plinth.py b/plinth.py
index b3ada9d71..6243fb7d5 100755
--- a/plinth.py
+++ b/plinth.py
@@ -114,7 +114,7 @@ def setup():
pass
try:
- from exmachina import ExMachinaClient
+ from exmachina.exmachina import ExMachinaClient
except ImportError:
cfg.exmachina = None
else:
diff --git a/start.sh b/start.sh
index 8e141fcf8..79baa1268 100755
--- a/start.sh
+++ b/start.sh
@@ -1,10 +1,10 @@
#! /bin/sh
-#PYTHONPATH=vendor/exmachina:$PYTHONPATH
+#PYTHONPATH=exmachina:$PYTHONPATH
export PYTHONPATH
sudo killall exmachina.py
-sudo /usr/share/pyshared/exmachina/exmachina.py -v &
+sudo exmachina/exmachina.py -v &
python plinth.py
sudo killall exmachina.py
diff --git a/test.sh b/test.sh
index 4b6f701d4..54ec30292 100755
--- a/test.sh
+++ b/test.sh
@@ -1,6 +1,6 @@
#! /bin/sh
-PYTHONPATH=build/exmachina:$PYTHONPATH
+PYTHONPATH=exmachina:$PYTHONPATH
PYTHONPATH=modules/installed/lib:$PYTHONPATH
PYTHONPATH=vendor:$PYTHONPATH
From 6409552e706846c08e34bbde4f4b141fedd8ffd0 Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Thu, 12 Sep 2013 12:53:10 +0200
Subject: [PATCH 08/13] Fix exmachine import.
---
Makefile | 2 +-
exmachina/__init__.py | 0
2 files changed, 1 insertion(+), 1 deletion(-)
create mode 100644 exmachina/__init__.py
diff --git a/Makefile b/Makefile
index 65622287b..6a6a323e0 100644
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,7 @@ install: default
cp -a static themes $(DESTDIR)$(DATADIR)/
cp -a *.py modules templates $(DESTDIR)$(PYDIR)/
mkdir -p $(DESTDIR)$(PYDIR)/exmachina
- cp -a exmachina/exmachina.py $(DESTDIR)$(PYDIR)/exmachina/.
+ cp -a exmachina/exmachina.py exmachina/__init__.py $(DESTDIR)$(PYDIR)/exmachina/.
cp share/init.d/plinth $(DESTDIR)/etc/init.d
install plinth $(DESTDIR)/usr/bin/
mkdir -p $(DESTDIR)/var/lib/plinth/cherrypy_sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run
diff --git a/exmachina/__init__.py b/exmachina/__init__.py
new file mode 100644
index 000000000..e69de29bb
From 023325fdde74f77631f277423b7048b650fadc8e Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Fri, 13 Sep 2013 00:00:17 +0200
Subject: [PATCH 09/13] Provide a way to get out of the first-boot screen, even
if it isn't ready yet.
---
modules/installed/first_boot.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/modules/installed/first_boot.py b/modules/installed/first_boot.py
index ff3a27e63..d20b0a76c 100644
--- a/modules/installed/first_boot.py
+++ b/modules/installed/first_boot.py
@@ -109,6 +109,15 @@ TODO: explain all this cert stuff to the user.
TODO: add instrux for installing certificate.
After you have installed
"""
+ # TODO complete first_boot handling
+ # Make sure the user is not stuck on a dead end for now.
+ with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
+ db['state']=5
+ main = main + """
+
Welcome screen not done. Press continue to
+see the rest of the web interface.
"
+"""
+
if False:
## Update state to 2 and head there
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
From 5be8a552ab7875270d5fe381a0bb2d4bfa545c30 Mon Sep 17 00:00:00 2001
From: James Valleroy
Date: Thu, 12 Sep 2013 23:14:07 -0400
Subject: [PATCH 10/13] Enable multithread for UserStore DB.
---
modules/installed/lib/user_store.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py
index 236b73a8f..8c9343a67 100644
--- a/modules/installed/lib/user_store.py
+++ b/modules/installed/lib/user_store.py
@@ -10,7 +10,7 @@ class UserStore(UserStoreModule, sqlite_db):
def __init__(self):
self.data_dir = cfg.users_dir
self.db_file = cfg.user_db
- sqlite_db.__init__(self, self.db_file, autocommit=True)
+ sqlite_db.__init__(self, self.db_file, autocommit=True, check_same_thread=False)
self.__enter__()
def close(self):
From 8cd1d23e1e0e1957139f627007b0a37e9ca145c5 Mon Sep 17 00:00:00 2001
From: Petter Reinholdtsen
Date: Fri, 13 Sep 2013 10:04:50 +0200
Subject: [PATCH 11/13] Improve message shown to the users.
---
modules/installed/first_boot.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/installed/first_boot.py b/modules/installed/first_boot.py
index d20b0a76c..36c32f114 100644
--- a/modules/installed/first_boot.py
+++ b/modules/installed/first_boot.py
@@ -114,7 +114,7 @@ TODO: explain all this cert stuff to the user.
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
db['state']=5
main = main + """
-Welcome screen not done. Press continue to
+
Welcome screen not completely implemented yet. Press continue to
see the rest of the web interface.
"
"""
From 87753531d27ae30b406f2812dd5eacac38fea71f Mon Sep 17 00:00:00 2001
From: Nick Daly
Date: Sun, 15 Sep 2013 21:47:18 -0500
Subject: [PATCH 12/13] Reverted change 657068b0.
---
Makefile | 2 -
exmachina/README | 105 -------
exmachina/exmachina.py | 557 ------------------------------------
exmachina/init_test.sh | 13 -
exmachina/test_exmachina.py | 85 ------
plinth.py | 2 +-
start.sh | 4 +-
test.sh | 2 +-
8 files changed, 4 insertions(+), 766 deletions(-)
delete mode 100644 exmachina/README
delete mode 100755 exmachina/exmachina.py
delete mode 100755 exmachina/init_test.sh
delete mode 100755 exmachina/test_exmachina.py
diff --git a/Makefile b/Makefile
index 6a6a323e0..4273b87d2 100644
--- a/Makefile
+++ b/Makefile
@@ -29,8 +29,6 @@ install: default
$(DESTDIR)/usr/share/doc/plinth $(DESTDIR)/usr/share/man/man1
cp -a static themes $(DESTDIR)$(DATADIR)/
cp -a *.py modules templates $(DESTDIR)$(PYDIR)/
- mkdir -p $(DESTDIR)$(PYDIR)/exmachina
- cp -a exmachina/exmachina.py exmachina/__init__.py $(DESTDIR)$(PYDIR)/exmachina/.
cp share/init.d/plinth $(DESTDIR)/etc/init.d
install plinth $(DESTDIR)/usr/bin/
mkdir -p $(DESTDIR)/var/lib/plinth/cherrypy_sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run
diff --git a/exmachina/README b/exmachina/README
deleted file mode 100644
index 6216029ad..000000000
--- a/exmachina/README
+++ /dev/null
@@ -1,105 +0,0 @@
- _ _
- _____ ___ __ ___ __ _ ___| |__ (_)_ __ __ _
- / _ \ \/ / '_ ` _ \ / _` |/ __| '_ \| | '_ \ / _` |
- | __/> <| | | | | | (_| | (__| | | | | | | | (_| |
- \___/_/\_\_| |_| |_|\__,_|\___|_| |_|_|_| |_|\__,_|
-
-
-
-### DISCLAIMER
-
- ----- ACHTUNG! WARNING! DANGER! ----
-
-This code is hackish and not "production quality. It represents a potential
-approach to a specific problem (privilege separation for system configuration).
-It has not been extensively reviewed or tested and does not represent a known
-best practice.
-
-### What is this?
-
-exmachina is a small system configuration system which runs as separate but
-coupled client/server UNIX processes for the purpose of privilege separation:
-the "server" process runs with root privileges and a python program using the
-"client" library runs as any unprivileged user. The commands and parameters
-that the client can send to the server are limited, though in this particular
-case can of course be used to deny service (reboot or shutdown the machine) or
-probably escalate privileges one way or another (install arbitrary packages,
-reconfigure networks, enable callback scripts, edit system configuration
-files).
-
-The server and client processes should be one-to-one: only one client should
-ever connect to the server. The init_test.sh script shows how this could be
-achieved in a SysV-style /etc/init.d script.
-
-The intended use case is writing a user-friendly web control panel for a Debian
-server or router: the web designer creating the user interface should not be
-overly concerned with writing secure code, and the web application itself
-(possibly including lots of third party framework code, javascript libraries,
-etc) should not run with strong system permissions, but core components of the
-system (such as hostname, wireless access point configuration, network
-settings, package installation, locale, timezone, etc) need to be modified.
-
-See the comments in exmachina.py for more information.
-
-### Alternatives
-
-The most simple alternative to exmachina that has been recommended to me is to
-create simple setuid/setgid programs or scripts to execute privileged system
-changes, and to only allow execute permissions to those programs for the
-user/group of the less-trusted user interface program. This seems to be the
-current best practice. For the more complicated case of generalized system
-configuration, the setuid/setgid program becomes complicated, or you need to
-write and install many of them, but this is no worse that the situation with
-exmachina.
-
-Another approach is the Assuan protocol used by GPG, which has been generalized
-as libassuan:
-
- "Assuan permits the servers, which do the actual work, e.g. encryption and
- decryption of data using a secret key, to be developed independently of the
- user interfaces, e.g. mail clients and other encryption front ends."
-
- http://www.gnupg.org/related_software/libassuan/index.en.html
-
-### Status
-
-Basic server and client functionality implemented. Crude, and far more simple
-than it may appear or the length of code would imply.
-
-This was code was written in a weekend "sprint" for the FreedomBox project and
-their Plinth web user interface in 2012.
-
-I may or may not maintain this code. I have hesitation even publishing it
-because i'm almost certain there are implementation bugs and that the entire
-concept is problematic.
-
-Features:
-* shared secret key process/privilege separation
-* call augeas API: match, set, setm, get, save, move, insert, remove
-* call init.d service scripts: status, start, stop, restart
-
-In late 2012 Nick Daly (of the FreedomBox project) wrote up a brief audit of
-this code and concept on his blog (https://www.betweennowhere.net/). Link is
-frequantly broken.
-
-### Dependencies (server)
-
-* augeas configuration editing library
-* python-augeas wrapper for augeas
-* bjsonrpc python library
-
-On debian (wheezy) try:
-
- $ sudo apt-get install augeas-tools python-bjsonrpc python-augeas
-
-### Dependencies (client)
-
-* bjsonrpc
-
-On debian (wheezy) try:
-
- $ sudo apt-get install bjsonrpc
-
-### License
-
-exmachina.py is GPLv3 or later
diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py
deleted file mode 100755
index 4fe6e75aa..000000000
--- a/exmachina/exmachina.py
+++ /dev/null
@@ -1,557 +0,0 @@
-#!/usr/bin/env python
-
-"""
-Author: bnewbold
-Date: July 2012
-License: GPLv3 or later (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.
-
-Note that the authentication mechanism only tells the server that the client
-seems to be legitimate, it doesn't prevent a rapid "man in the middle" style
-attack on the client, which could feed back malicious information.
-
-Alternatively, an optional user or group can be specified and the socket file
-will have it's ownership and permissions changed appropriately.
-
-Note that the socket path would need to be changed on a per-application basis
-so that competing daemons don't block/clobber each other.
-"""
-
-import os
-import sys
-import grp
-import shutil
-import argparse
-import logging
-import socket
-import subprocess
-import time
-import base64
-import functools
-import hashlib
-import atexit
-import stat
-import pwd
-
-import bjsonrpc
-import bjsonrpc.handlers
-import bjsonrpc.server
-import augeas
-
-log = logging.getLogger(__name__)
-
-# hackish way to enforce single client connection
-allow_connect = True
-
-def execute_service(servicename, action, timeout=10):
- """This function mostly ripped from StackOverflow:
- http://stackoverflow.com/questions/1556348/python-run-a-process-with-timeout-and-capture-stdout-stderr-and-exit-status
- """
- # ensure service name isn't tricky trick
- script = "/etc/init.d/" + os.path.split(servicename)[1]
-
- if not os.path.exists(script):
- raise ValueError("so such service: %s" % servicename)
-
- command_list = [script, action]
- log.info("executing: %s" % command_list)
- proc = subprocess.Popen(command_list,
- bufsize=0,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- poll_seconds = .250
- deadline = time.time() + timeout
- while time.time() < deadline and proc.poll() is None:
- time.sleep(poll_seconds)
-
- if proc.poll() is None:
- if float(sys.version[:3]) >= 2.6:
- proc.terminate()
- raise Exception("execution timed out (>%d seconds): %s" %
- (timeout, command_list))
-
- stdout, stderr = proc.communicate()
- # TBD: should raise exception here if proc.returncode != 0?
- return stdout, stderr, proc.returncode
-
-def execute_apt(packagename, action, timeout=120, aptargs=['-q', '-y']):
- # ensure package name isn't tricky trick
- if action != "update" \
- and (packagename != packagename.strip().split()[0] \
- or packagename.startswith('-')):
- raise ValueError("Not a good apt package name: %s" % packagename)
-
- if action == "update":
- command_list = ['apt-get', action]
- else:
- command_list = ['apt-get', action, packagename]
- command_list.extend(aptargs)
- log.info("executing: %s" % command_list)
- proc = subprocess.Popen(command_list,
- bufsize=0,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- poll_seconds = .250
- deadline = time.time() + timeout
- while time.time() < deadline and proc.poll() is None:
- time.sleep(poll_seconds)
-
- if proc.poll() is None:
- if float(sys.version[:3]) >= 2.6:
- proc.terminate()
- raise Exception("execution timed out (>%d seconds): %s" %
- (timeout, command_list))
-
- stdout, stderr = proc.communicate()
- return stdout, stderr, proc.returncode
-
-def authreq(fn):
- """
- Decorator to force authentication before allowing calls to a method
- """
- @functools.wraps(fn)
- def wrappedfunc(self, *args, **kwargs):
- if not self.secret_key:
- return fn(self, *args, **kwargs)
- else:
- log.error("Unauthorized function call attempt; bailing")
- sys.exit(-1)
- return wrappedfunc
-
-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):
- global allow_connect
- if not allow_connect:
- log.error("second client tried to connect, exiting")
- sys.exit(-1)
- allow_connect = False
- self.augeas = augeas.Augeas()
-
- def _shutdown(self):
- # Server shuts down after a single client connection closes
- log.info("connection closing, server exiting")
- sys.exit(-1)
-
- def authenticate(self, secret_key):
- if not self.secret_key:
- log.warn("Unecessary authentication attempt")
- return
- if not hashlib.sha256(secret_key.strip()).hexdigest() == \
- hashlib.sha256(self.secret_key.strip()).hexdigest():
- # key doesn't match, fail hard
- log.error("Authentication failed!")
- sys.exit()
- self.secret_key = None
-
- def need_to_auth(self):
- """
- Helper for clients to learn whether they still need to authenticate
- """
- return self.secret_key != None
-
- # ------------- Augeas API Passthrough -----------------
- @authreq
- def augeas_save(self):
- log.info("augeas: saving config")
- return self.augeas.save()
-
- @authreq
- def augeas_set(self, path, value):
- log.info("augeas: set %s=%s" % (path, value))
- return self.augeas.set(path.encode('utf-8'),
- value.encode('utf-8'))
-
- @authreq
- def augeas_setm(self, base, sub, value):
- log.info("augeas: setm %s %s = %s" % (base, sub, value))
- return self.augeas.setm(base.encode('utf-8'),
- sub.encode('utf-8'),
- value.encode('utf-8'))
-
- @authreq
- def augeas_get(self, path):
- # reduce verbosity
- log.debug("augeas: get %s" % path)
- return self.augeas.get(path.encode('utf-8'))
-
- @authreq
- def augeas_match(self, path):
- # reduce verbosity
- log.debug("augeas: match %s" % path)
- return self.augeas.match("%s" % path.encode('utf-8'))
-
- @authreq
- def augeas_insert(self, path, label, before=True):
- log.info("augeas: insert %s=%s" % (path, value))
- return self.augeas.insert(path.encode('utf-8'),
- label.encode('utf-8'),
- before=before)
-
- @authreq
- def augeas_move(self, src, dst):
- log.info("augeas: move %s -> %s" % (src, dst))
- return self.augeas.move(src.encode('utf-8'), dst.encode('utf-8'))
-
- @authreq
- def augeas_remove(self, path):
- log.info("augeas: remove %s" % path)
- return self.augeas.remove(path.encode('utf-8'))
-
- # ------------- Misc. non-Augeas Helpers -----------------
- @authreq
- def set_timezone(self, tzname):
- log.info("reset timezone to %s" % tzname)
- tzname = tzname.strip()
- tzpath = os.path.join("/usr/share/zoneinfo", tzname)
- try:
- os.stat(tzpath)
- except OSError:
- # file not found
- raise ValueError("timezone not valid: %s" % tzname)
- shutil.copy(
- os.path.join("/usr/share/zoneinfo", tzname),
- "/etc/localtime")
- with open("/etc/timezone", "w") as tzfile:
- tzfile.write(tzname + "\n")
- return "timezone changed to %s" % tzname
-
- # ------------- init.d Service Control -----------------
- @authreq
- def initd_status(self, servicename):
- return execute_service(servicename, "status")
-
- @authreq
- def initd_start(self, servicename):
- return execute_service(servicename, "start")
-
- @authreq
- def initd_stop(self, servicename):
- return execute_service(servicename, "stop")
-
- @authreq
- def initd_restart(self, servicename):
- return execute_service(servicename, "restart")
-
- # ------------- apt-get Package Control -----------------
- @authreq
- def apt_install(self, packagename):
- return execute_apt(packagename, "install")
-
- @authreq
- def apt_update(self):
- return execute_apt("", "update")
-
- @authreq
- def apt_remove(self, packagename):
- return execute_apt(packagename, "remove")
-
-
-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",
- secret_key=None):
-
- if secret_key:
- secret_key = hashlib.sha256(secret_key.strip() + "|exmachina")\
- .hexdigest()
-
- self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- self.sock.connect(socket_path)
- self.conn = bjsonrpc.connection.Connection(self.sock)
-
- if self.conn.call.need_to_auth():
- if secret_key:
- self.conn.call.authenticate(secret_key)
- else:
- self.conn.close()
- raise Exception(
- "authentication required but no secret_key passed")
- elif secret_key:
- print "secret_key passed but no authentication required; ignoring"
-
- self.augeas = EmptyClass()
- self.initd = EmptyClass()
- self.apt = EmptyClass()
- self.misc = EmptyClass()
-
- self.augeas.save = self.conn.call.augeas_save
- self.augeas.set = self.conn.call.augeas_set
- self.augeas.setm = self.conn.call.augeas_setm
- self.augeas.get = self.conn.call.augeas_get
- self.augeas.match = self.conn.call.augeas_match
- self.augeas.insert = self.conn.call.augeas_insert
- self.augeas.move = self.conn.call.augeas_move
- self.augeas.remove = self.conn.call.augeas_remove
- self.initd.status = self.conn.call.initd_status
- self.initd.start = self.conn.call.initd_start
- self.initd.stop = self.conn.call.initd_stop
- self.initd.restart = self.conn.call.initd_restart
- self.apt.install = self.conn.call.apt_install
- self.apt.update = self.conn.call.apt_update
- self.apt.remove = self.conn.call.apt_remove
- self.misc.set_timezone = self.conn.call.set_timezone
-
- def close(self):
- self.sock.close()
-
-
-def run_server(socket_path, secret_key=None, socket_group=None,
- socket_user=None):
-
- if secret_key:
- secret_key = hashlib.sha256(secret_key.strip() + "|exmachina")\
- .hexdigest()
-
- if not 0 == os.geteuid():
- log.warn("Expected to be running as root!")
- if socket_group or socket_user:
- log.error("Can't change socket permissions if non-root, exiting")
- sys.exit(-1)
-
- # check if the socket was left open after a previous run, overwrite it
- if os.path.exists(socket_path):
- if not stat.S_ISSOCK(os.stat(socket_path).st_mode):
- log.error("socket_path exists and isn't a stale socket: %s" %
- socket_path)
- sys.exit(-1)
- # socket file exists, need to check if it's stale from a previous
- # session
- test_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- try:
- test_sock.connect(socket_path)
- log.error("socket_path already exists and seems to be active: %s" %
- socket_path)
- test_sock.close()
- sys.exit(-1)
- except Exception, e:
- print e
- # if we got this far it's probably a stale socket and should be
- # destroyed
- log.warn("Clobbering pre-existing socket: %s" % socket_path)
- os.unlink(socket_path)
-
- # open and bind to unix socket
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- sock.bind(socket_path)
-
- # we just created the socket file, so now let's register an atexit callback
- # to clean up after ourselves if we get Ctrl-C'd (or exit for any other
- # reason, including normal cleanup)
- def delete_socket():
- os.unlink(socket_path)
- atexit.register(delete_socket)
-
- if socket_group is not None:
- # optionally set group-only permissions on socket file before we start
- # accepting connections
- socket_uid = os.stat(socket_path).st_uid
- socket_gid = grp.getgrnam(socket_group).gr_gid
- os.chmod(socket_path, 0660)
- os.chown(socket_path, socket_uid, socket_gid)
- elif socket_user is not None:
- # optionally set user-only permissions on socket file before we start
- # accepting connections
- pwn = pwd.getpwnam(socket_user)
- socket_uid = pwn.pw_uid
- socket_gid = pwn.pw_gid
- os.chmod(socket_path, 0660)
- os.chown(socket_path, socket_uid, socket_gid)
- else:
- os.chmod(socket_path, 0666)
-
- # only going to allow a single client, so don't allow queued connections
- sock.listen(0)
-
- if secret_key:
- # key already got hashed above
- ExMachinaHandler.secret_key = secret_key
-
- # get bjsonrpc server started. it would make more sense to just listen for
- # a single client connection and pass that off to the bjsonrpc handler,
- # then close the socket when that's done, but I don't see an easy way to do
- # that with the bjsonrpc API, so instead we let it wait indefinately for
- # connections, but actual only allow one and bail when that one closes.
- 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.
- These arguments are optional and default to /dev/null. Note that stderr is
- opened unbuffered, so if it shares a file with stdout then interleaved
- output may not appear in the order that you expect. """
-
- # Do first fork.
- try:
- pid = os.fork()
- if pid > 0:
- sys.exit(0) # Exit first parent.
- except OSError, e:
- sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
- sys.exit(1)
-
- # Decouple from parent environment.
- os.chdir("/")
- os.umask(0)
- os.setsid()
-
- # Do second fork.
- try:
- pid = os.fork()
- if pid > 0:
- sys.exit(0) # Exit second parent.
- except OSError, e:
- sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
- sys.exit(1)
-
- # Now I am a daemon!
-
- # Redirect standard file descriptors.
- si = open(stdin, 'r')
- so = open(stdout, 'a+')
- se = open(stderr, 'a+', 0)
- os.dup2(si.fileno(), sys.stdin.fileno())
- os.dup2(so.fileno(), sys.stdout.fileno())
- os.dup2(se.fileno(), sys.stderr.fileno())
- return pid
-
-# =============================================================================
-# Command line handling
-def main():
-
- global log
- parser = argparse.ArgumentParser(usage=
- "usage: exmachina.py [options]\n"
- "exmachina.py --help for more info."
- )
- parser.add_argument("-v", "--verbose",
- default=False,
- help="Show more debugging statements",
- action="store_true")
- parser.add_argument("-q", "--quiet",
- default=False,
- help="Show fewer informational statements",
- action="store_true")
- parser.add_argument("-k", "--key",
- default=False,
- help="Wait for Secret Access Key on stdin before starting",
- action="store_true")
- parser.add_argument("--random-key",
- default=False,
- help="Just dump a random base64 key and exit",
- action="store_true")
- parser.add_argument("-s", "--socket-path",
- default="/tmp/exmachina.sock",
- help="UNIX Domain socket file path to listen on",
- metavar="FILE")
- parser.add_argument("--pidfile",
- default=None,
- help="Daemonize and write pid to this file",
- metavar="FILE")
- parser.add_argument("-g", "--group",
- default=None,
- help="chgrp socket file to this group and set 0660 permissions")
- parser.add_argument("-u", "--user",
- default=None,
- help="chown socket file to this user/group and set 0600 permissions")
-
- args = parser.parse_args()
-
- if args.user and args.group:
- parser.error("set user or group option, but not both")
-
- #if len(args) != 0:
- #parser.error("Incorrect number of arguments")
-
- if args.random_key:
- sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128)))
- sys.exit(0)
-
- log = logging.getLogger()
- hdlr = logging.StreamHandler()
- formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
- hdlr.setFormatter(formatter)
- log.addHandler(hdlr)
-
- if args.verbose:
- log.setLevel(logging.DEBUG)
- elif args.quiet:
- log.setLevel(logging.ERROR)
- else:
- log.setLevel(logging.INFO)
-
- secret_key = None
- if args.key:
- log.debug("Waiting for secret key on stdin...")
- secret_key = sys.stdin.readline().strip()
- log.debug("Got it!")
-
- if args.pidfile:
- with open(args.pidfile, 'w') as pfile:
- # ensure file is available/writable
- pass
- os.unlink(args.pidfile)
- daemonize()
- pid = os.getpid()
- with open(args.pidfile, 'w') as pfile:
- pfile.write("%s" % pid)
- log.info("Daemonized, pid is %s" % pid)
-
- run_server(secret_key=secret_key,
- socket_path=args.socket_path,
- socket_group=args.group,
- socket_user=args.user)
-
-if __name__ == '__main__':
- main()
diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh
deleted file mode 100755
index 941285d36..000000000
--- a/exmachina/init_test.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env bash
-
-# Test init.d-style initialization; run this script as root (or sudo it)
-
-export key=`./exmachina.py --random-key`
-
-echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid -g www-data
-sleep 1
-echo $key | sudo -u www-data -g www-data ./test_exmachina.py -k
-
-kill `cat /tmp/exmachina_test.pid` && rm /tmp/exmachina_test.pid
-sleep 1
-jobs
diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py
deleted file mode 100755
index 510d30d3a..000000000
--- a/exmachina/test_exmachina.py
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/env python
-
-"""
-This file tests the "client side" of the exmachina layer.
-
-To use with secret keys, do the following in seperate terminals:
-
- $ echo "" | sudo ./exmachina.py -vk
- $ echo "" | ./test_exmachina.py -k
-
-To use without, do the following in seperate terminals:
-
- $ sudo ./exmachina.py -v
- $ ./test_exmachina.py
-
-Use the init_test.sh script to test shared key passing and privilage seperation
-at the same time:
-
- $ sudo ./init_test.sh
-"""
-
-import sys
-import socket
-
-import bjsonrpc
-import bjsonrpc.connection
-from bjsonrpc.exceptions import ServerError
-
-from exmachina.exmachina import ExMachinaClient
-
-# =============================================================================
-# Command line handling
-def main():
-
- secret_key = None
- if sys.argv[-1] == "-k":
- print "waiting for key on stdin..."
- secret_key = sys.stdin.readline()
- print "got it!"
-
- """
- # both tests together won't work now that server exits after single client
- socket_path = "/tmp/exmachina.sock"
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- sock.connect(socket_path)
-
- 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")
- try:
- print client.initd.status("greentooth")
- print "ERROR: should have failed above!"
- except ServerError:
- print "(got expected error, good!)"
- print "(expect Error on the above line)"
- print client.initd.status("bluetooth")
- print client.apt.install("pkg_which_does_not_exist")
- print client.apt.remove("pkg_which_does_not_exist")
- #print client.apt.update() # can be slow...
- #print client.misc.set_timezone("UTC") # don't clobber system...
- try:
- print client.misc.set_timezone("whoopie") # should be an error
- print "ERROR: should have failed above!"
- except ServerError:
- print "(got expected error, good!)"
- client.close()
-
-if __name__ == '__main__':
- main()
diff --git a/plinth.py b/plinth.py
index 3dfbe0c31..b140dbcb0 100755
--- a/plinth.py
+++ b/plinth.py
@@ -114,7 +114,7 @@ def setup():
pass
try:
- from exmachina.exmachina import ExMachinaClient
+ from exmachina import ExMachinaClient
except ImportError:
cfg.exmachina = None
print "unable to import exmachina client library, but continuing anyways..."
diff --git a/start.sh b/start.sh
index 79baa1268..8e141fcf8 100755
--- a/start.sh
+++ b/start.sh
@@ -1,10 +1,10 @@
#! /bin/sh
-#PYTHONPATH=exmachina:$PYTHONPATH
+#PYTHONPATH=vendor/exmachina:$PYTHONPATH
export PYTHONPATH
sudo killall exmachina.py
-sudo exmachina/exmachina.py -v &
+sudo /usr/share/pyshared/exmachina/exmachina.py -v &
python plinth.py
sudo killall exmachina.py
diff --git a/test.sh b/test.sh
index 54ec30292..4b6f701d4 100755
--- a/test.sh
+++ b/test.sh
@@ -1,6 +1,6 @@
#! /bin/sh
-PYTHONPATH=exmachina:$PYTHONPATH
+PYTHONPATH=build/exmachina:$PYTHONPATH
PYTHONPATH=modules/installed/lib:$PYTHONPATH
PYTHONPATH=vendor:$PYTHONPATH
From c99f074f22c8b7b8132257a754c313b934e902ea Mon Sep 17 00:00:00 2001
From: Nick Daly
Date: Sun, 15 Sep 2013 21:55:23 -0500
Subject: [PATCH 13/13] Use ExMachina already in Plinth/vendor.
---
Makefile | 3 +++
plinth.py | 2 +-
start.sh | 2 +-
test.sh | 1 -
4 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/Makefile b/Makefile
index 4273b87d2..c9a6b4cdc 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,7 @@ CSS=$(wildcard *.css)
CSS=$(subst .tiny,,$(shell find themes -type f -name '*.css'))
COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS))
PWD=`pwd`
+BUILDDIR=vendor
# hosting variables
SLEEP_TIME=300
@@ -29,6 +30,8 @@ install: default
$(DESTDIR)/usr/share/doc/plinth $(DESTDIR)/usr/share/man/man1
cp -a static themes $(DESTDIR)$(DATADIR)/
cp -a *.py modules templates $(DESTDIR)$(PYDIR)/
+ mkdir -p $(DESTDIR)$(PYDIR)/exmachina
+ cp -a vendor/exmachina/exmachina.py $(DESTDIR)$(PYDIR)/exmachina/.
cp share/init.d/plinth $(DESTDIR)/etc/init.d
install plinth $(DESTDIR)/usr/bin/
mkdir -p $(DESTDIR)/var/lib/plinth/cherrypy_sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run
diff --git a/plinth.py b/plinth.py
index b140dbcb0..e2bd80818 100755
--- a/plinth.py
+++ b/plinth.py
@@ -114,7 +114,7 @@ def setup():
pass
try:
- from exmachina import ExMachinaClient
+ from vendor.exmachina.exmachina import ExMachinaClient
except ImportError:
cfg.exmachina = None
print "unable to import exmachina client library, but continuing anyways..."
diff --git a/start.sh b/start.sh
index 8e141fcf8..8b97c0bcd 100755
--- a/start.sh
+++ b/start.sh
@@ -5,6 +5,6 @@
export PYTHONPATH
sudo killall exmachina.py
-sudo /usr/share/pyshared/exmachina/exmachina.py -v &
+sudo vendor/exmachina/exmachina.py -v &
python plinth.py
sudo killall exmachina.py
diff --git a/test.sh b/test.sh
index 4b6f701d4..535f6d408 100755
--- a/test.sh
+++ b/test.sh
@@ -1,6 +1,5 @@
#! /bin/sh
-PYTHONPATH=build/exmachina:$PYTHONPATH
PYTHONPATH=modules/installed/lib:$PYTHONPATH
PYTHONPATH=vendor:$PYTHONPATH