Merged with upstream.

This commit is contained in:
Nick Daly 2013-10-06 21:21:43 -05:00
commit a9c853e5bd
16 changed files with 186 additions and 77 deletions

View File

@ -4,7 +4,6 @@ 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,9 +28,9 @@ install: default
mkdir -p $(DESTDIR)$(PYDIR) $(DESTDIR)$(DATADIR) $(DESTDIR)/usr/bin \
$(DESTDIR)/usr/share/doc/plinth $(DESTDIR)/usr/share/man/man1
cp -a static themes $(DESTDIR)$(DATADIR)/
cp -a actions $(DESTDIR)$(DATADIR)/
cp -a sudoers.d $(DESTDIR)/etc/sudoers.d
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
@ -81,7 +80,7 @@ clean:
@find . -name "*.bak" -exec rm {} \;
@$(MAKE) -s -C doc clean
@$(MAKE) -s -C templates clean
rm -rf $(BUILDDIR) $(DESTDIR)
rm -f plinth.config
rm -f predepend
hosting:

3
README
View File

@ -43,9 +43,6 @@ get down into the details and configure things the average user never
thinks about. For example, experts can turn off ntp or switch ntp
servers. Basic users should never even know those options exist.
See comments in exmachina/exmachina.py for more details about the
configuration management process seperation scheme.
## Getting Started
See the INSTALL file for additional details. Run:

10
actions/hostname-change Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
hostname="$1"
echo "$hostname" > /etc/hostname
if [ -x /etc/init.d/hostname.sh ] ; then
service hostname.sh start
else
service hostname start
fi

48
actions/owncloud-setup Executable file
View File

@ -0,0 +1,48 @@
#!/bin/sh
if [ -e /etc/apache2/conf-enabled/owncloud.conf ] ; then
owncloud_enable=true
else
owncloud_enable=false
fi
owncloud_enable_cur=$owncloud_enable
export owncloud_enable
while [ "$1" ] ; do
arg="$1"
shift
case "$arg" in
enable|noenable) # Not using disable for consistency with other options
if [ 'enable' = "$arg" ] ; then
owncloud_enable=true
else
owncloud_enable=false
fi
export owncloud_enable
;;
status)
printstatus() {
if $2 ; then
echo $1
else
echo no$1
fi
}
printstatus enable $owncloud_enable_cur
exit 0
;;
*)
;;
esac
done
if [ "$owncloud_enable" != "$owncloud_enable_cur" ] ; then
if $owncloud_enable ; then
DEBIAN_FRONTEND=noninteractive apt-get install -y owncloud 2>&1 | logger -t owncloud-setup
a2enconf owncloud 2>&1 | logger -t owncloud-setup
else
a2disconf owncloud 2>&1 | logger -t owncloud-setup
fi
service apache2 restart 2>&1 | logger -t owncloud-setup
fi

14
actions/timezone-change Executable file
View File

@ -0,0 +1,14 @@
#!/bin/sh
zonename="$1"
tzpath="/usr/share/zoneinfo/$zonename"
if [ -e "$tzpath" ] ; then
cp "$tzpath" /etc/localtime
echo "$zonename" > /etc/timezone
exit 0
else
echo "timezone not valid" 1>&2
exit 1
fi

View File

@ -1,6 +1,9 @@
import cherrypy
from gettext import gettext as _
from modules.auth import require
from plugin_mount import PagePlugin
from forms import Form
from privilegedactions import privilegedaction_run
import cfg
class Apps(PagePlugin):
@ -9,6 +12,7 @@ class Apps(PagePlugin):
self.register_page("apps")
self.menu = cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80)
self.menu.add_item("Photo Gallery", "icon-picture", "/apps/photos", 35)
self.menu.add_item("Owncloud", "icon-picture", "/apps/owncloud", 35)
@cherrypy.expose
def index(self):
@ -33,3 +37,47 @@ investment in the sentimental value of your family snaps? Keep those
photos local, backed up, easily accessed and free from the whims of
some other websites business model.</p>
""")
@cherrypy.expose
@require()
def owncloud(self, submitted=False, **kwargs):
checkedinfo = {
'enable' : False,
}
if submitted:
opts = []
for k in kwargs.keys():
if 'on' == kwargs[k]:
shortk = k.split("owncloud_").pop()
checkedinfo[shortk] = True
for key in checkedinfo.keys():
if checkedinfo[key]:
opts.append(key)
else:
opts.append('no'+key)
privilegedaction_run("owncloud-setup", opts)
output, error = privilegedaction_run("owncloud-setup", ['status'])
if error:
raise Exception("something is wrong: " + error)
for option in output.split():
checkedinfo[option] = True
main="""
"""
form = Form(title="Configuration",
action="/apps/owncloud",
name="configure_owncloud",
message='')
form.checkbox(_("Enable Owncloud"), name="owncloud_enable", id="owncloud_enable", checked=checkedinfo['enable'])
form.hidden(name="submitted", value="True")
form.html(_("<p>When enabled, the owncloud installation will be available from /owncloud/ on the web server.</p>"))
form.submit(_("Update setup"))
main += form.render()
sidebar_right="""
<strong>Owncloud</strong><p>gives you universal access to your files through a web interface or WebDAV. It also provides a platform to easily view & sync your contacts, calendars and bookmarks across all your devices and enables basic editing right on the web. Installation has minimal server requirements, doesn't need special permissions and is quick. ownCloud is extendable via a simple but powerful API for applications and plugins.
</p>
"""
return self.fill_template(title="Owncloud", main=main, sidebar_right=sidebar_right)

View File

@ -8,6 +8,7 @@ import util as u
from withsqlite.withsqlite import sqlite_db
import cfg
import config
from model import User
class FirstBoot(PagePlugin):
def __init__(self, *args, **kwargs):
@ -25,7 +26,7 @@ class FirstBoot(PagePlugin):
return "fake key"
@cherrypy.expose
def state0(self, message="", hostname="", box_key="", submitted=False):
def state0(self, message="", hostname="", box_key="", submitted=False, username="", md5_password="", **kwargs):
"""
In this state, we do time config over HTTP, name the box and
server key selection.
@ -62,9 +63,21 @@ class FirstBoot(PagePlugin):
elif submitted and not box_key:
box_key = self.generate_box_key()
db['box_key'] = box_key
if username and md5_password:
di = {
'username':username,
'name':'First user - please change',
'expert':'on',
"groups": ["expert"],
'passphrase':md5_password,
}
new_user = User(di)
cfg.users.set(username,new_user)
validuser = True
else:
validuser = False
if hostname and box_key and '' == config.valid_hostname(hostname) 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) and validuser:
## Update state to 1 and head there
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
db['state']=1
@ -75,9 +88,14 @@ class FirstBoot(PagePlugin):
action="firstboot",
name="whats_my_name",
message=message)
form.text = '<script type="text/javascript" src="/static/js/md5.js"></script>\n'+form.text
form.html("<p>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.</p>")
form.text_input('Name your FreedomBox', id="hostname", value=hostname)
form.html("<p>%(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!</p>" % {'hostname':cfg.box_name})
form.html("<p><strong>Initial user and password.</strong> Access to this web interface is protected by knowing a username and password. Provide one here to register the initial privileged user. The password can be changed and other users added later.</p>")
form.text_input('Username:', id="username", value=username)
form.text_input('Password:', id="password", type='password')
form.text_input(name="md5_password", type="hidden")
form.html("<p>%(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!</p>" % {'box_name':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!")

View File

@ -27,7 +27,10 @@ def check_credentials(username, passphrase):
cfg.log(error)
return error
u = cfg.users[username]
if username in cfg.users:
u = cfg.users[username]
else:
u = None
# hash the password whether the user exists, to foil timing
# side-channel attacks
pass_hash = hashlib.md5(passphrase).hexdigest()

View File

@ -142,6 +142,16 @@ class Form():
<span>%s</span>
<input type=checkbox name="%s" id="%s" %s/>
</label></div>\n""" % (label, name, id, checked)
def radiobutton(self, label='', name='', id='', value='', checked=''):
name, id = self.name_or_id(name, id)
if checked:
checked = 'checked="on"'
self.text += """
<div class="radio">
<label>
<span>%s</span>
<input type="radio" name="%s" id="%s" value="%s" %s/>
</label></div>\n""" % (label, name, id, value, checked)
def get_checkbox(self, name='', id=''):
return '<input type=checkbox name="%s" id="%s" />\n' % self.name_or_id(name, id)
def render(self):

View File

@ -9,6 +9,7 @@ from gettext import gettext as _
from filedict import FileDict
from modules.auth import require
from plugin_mount import PagePlugin, FormPlugin
from privilegedactions import privilegedaction_run
import cfg
from forms import Form
from model import User
@ -48,20 +49,14 @@ def get_hostname():
def set_hostname(hostname):
"Sets machine hostname to hostname"
cfg.log.info("Writing '%s' to /etc/hostname with exmachina" % hostname)
cfg.log.info("Changing hostname to '%s'" % hostname)
try:
cfg.exmachina.augeas.set("/files/etc/hostname/*", hostname)
cfg.exmachina.augeas.save()
privilegedaction_run("hostname-change", [hostname])
# don't persist/cache change unless it was saved successfuly
sys_store = filedict_con(cfg.store_file, 'sys')
sys_store['hostname'] = hostname
if platform.linux_distribution()[0]=="Ubuntu" :
cfg.exmachina.service.start("hostname")
else:
cfg.exmachina.initd.start("hostname.sh") # is hostname.sh debian-only?
except OSError, e:
raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e)
raise cherrypy.HTTPError(500, "Updating hostname failed: %s" % e)
class general(FormPlugin, PagePlugin):
url = ["/sys/config"]
@ -79,7 +74,7 @@ class general(FormPlugin, PagePlugin):
return '<p>' + _('Only members of the expert group are allowed to see and modify the system setup.') + '</p>'
sys_store = filedict_con(cfg.store_file, 'sys')
hostname = cfg.exmachina.augeas.get("/files/etc/hostname/*")
hostname = get_hostname()
# this layer of persisting configuration in sys_store could/should be
# removed -BLN
defaults = {'time_zone': "slurp('/etc/timezone').rstrip()",
@ -139,10 +134,10 @@ class general(FormPlugin, PagePlugin):
raise
else:
message += msg
time_zone = time_zone.strip()
if time_zone != sys_store['time_zone']:
src = os.path.join("/usr/share/zoneinfo", time_zone)
cfg.log.info("Setting timezone to %s" % time_zone)
cfg.exmachina.misc.set_timezone(time_zone)
privilegedaction_run("timezone-change", [time_zone])
sys_store['time_zone'] = time_zone
return message or "Settings updated."

View File

@ -18,7 +18,6 @@ from logger import Logger
#from modules.auth import AuthController, require, member_of, name_is
from withsqlite.withsqlite import sqlite_db
from exmachina.exmachina import ExMachinaClient
import socket
__version__ = "0.2.14"
@ -86,8 +85,6 @@ def parse_arguments():
parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.')
parser.add_argument('--pidfile', default="",
help='specify a file in which the server may write its pid')
parser.add_argument('--listen-exmachina-key', default=False, action='store_true',
help='listen for JSON-RPC shared secret key on stdin at startup')
parser.add_argument('--directory', default="/",
help='specify a subdirectory to host the server.')
@ -101,18 +98,6 @@ def parse_arguments():
except AttributeError:
cfg.pidfile = "plinth.pid"
if args.listen_exmachina_key:
# this is where we optionally try to read in a shared secret key to
# authenticate connections to exmachina
cfg.exmachina_secret_key = sys.stdin.readline().strip()
else:
cfg.exmachina_secret_key = None
if not args.directory.startswith("/"):
args.directory = "/" + args.directory
return args
def setup():
args = parse_arguments()
@ -123,19 +108,6 @@ def setup():
except AttributeError:
pass
try:
from vendor.exmachina.exmachina import ExMachinaClient
except ImportError:
cfg.exmachina = None
print "unable to import exmachina client library, but continuing anyways..."
else:
try:
cfg.exmachina = ExMachinaClient(
secret_key=cfg.exmachina_secret_key or None)
except socket.error:
cfg.exmachina = None
print "couldn't connect to exmachina daemon, but continuing anyways..."
os.chdir(cfg.python_root)
cherrypy.config.update({'error_page.404': error_page_404})
cherrypy.config.update({'error_page.500': error_page_500})

View File

@ -4,6 +4,7 @@ box_name = FreedomBox
[Path]
file_root = %(root)s
python_root = %(root)s
data_dir = %(file_root)s/data
store_file = %(data_dir)s/store.sqlite3
user_db = %(data_dir)s/users

15
privilegedactions.py Normal file
View File

@ -0,0 +1,15 @@
import sys
import subprocess
import cfg
def privilegedaction_run(action, options):
cmd = ['sudo', '-n', "/usr/share/plinth/actions/%s" % action]
if options:
cmd.extend(options)
cfg.log.info('running: %s ' % ' '.join(cmd))
output, error = \
subprocess.Popen(cmd,
stdout = subprocess.PIPE,
stderr= subprocess.PIPE).communicate()
return output, error

View File

@ -7,21 +7,17 @@
# Default-Stop: 0 1 6
# Short-Description: plinth web frontend
# Description:
# Control the exmachina privileged execution daemon and the plinth
# web frontend.
# Control the plinth web frontend.
### END INIT INFO
# This file is /etc/init.d/plinth
DAEMON=/usr/local/bin/plinth.py
EXMACHINA_DAEMON=/usr/local/bin/exmachina.py
PID_FILE=/var/run/plinth.pid
EXMACHINA_PID_FILE=/var/run/exmachina.pid
PLINTH_USER=www-data
PLINTH_GROUP=www-data
test -x $DAEMON || exit 0
test -x $EXMACHINA_DAEMON || exit 0
set -e
@ -31,17 +27,9 @@ start_plinth (){
if [ -f $PID_FILE ]; then
echo Already running with a pid of `cat $PID_FILE`.
else
if [ -f $EXMACHINA_PID_FILE ]; then
echo exmachina was already running with a pid of `cat $EXMACHINA_PID_FILE`.
kill -15 `cat $EXMACHINA_PID_FILE`
rm -rf $EXMACHINA_PID_FILE
fi
SHAREDKEY=`$EXMACHINA_DAEMON --random-key`
touch $PID_FILE
chown $PLINTH_USER:$PLINTH_GROUP $PID_FILE
echo $SHAREDKEY | $EXMACHINA_DAEMON --pidfile=$EXMACHINA_PID_FILE --group=$PLINTH_GROUP || rm $PID_FILE
sleep 0.5
echo $SHAREDKEY | sudo -u $PLINTH_USER -g $PLINTH_GROUP $DAEMON --pidfile=$PID_FILE
sudo -u $PLINTH_USER -g $PLINTH_GROUP $DAEMON --pidfile=$PID_FILE
fi
}
@ -53,13 +41,6 @@ stop_plinth () {
else
echo "No pid file at $PID_FILE suggests plinth is not running."
fi
if [ -f $EXMACHINA_PID_FILE ]; then
kill -15 `cat $EXMACHINA_PID_FILE` || true
rm -rf $EXMACHINA_PID_FILE
echo "killed exmachina"
else
echo "No pid file at $EXMACHINA_PID_FILE suggests exmachina is not running."
fi
}
test -x $DAEMON || exit 0

View File

@ -1,10 +1,7 @@
#! /bin/sh
#PYTHONPATH=vendor/exmachina:$PYTHONPATH
#PYTHONPATH=$PYTHONPATH
export PYTHONPATH
sudo killall exmachina.py
sudo vendor/exmachina/exmachina.py -v &
python plinth.py
sudo killall exmachina.py

1
sudoers.d/plinth Normal file
View File

@ -0,0 +1 @@
plinth ALL=(ALL:ALL) NOPASSWD:/usr/share/plinth/actions/*