summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorJames Vasile <james@hackervisions.org>2011-02-22 13:32:45 -0500
committerJames Vasile <james@hackervisions.org>2011-02-22 13:32:45 -0500
commit35071d7212cec1fc23e8204bfd392a116a5313ed (patch)
tree1c75a525227769fc94f303b5c0233882d90ef2a8 /modules
...
Diffstat (limited to 'modules')
-rw-r--r--modules/__init__.py0
l---------modules/apps.py1
l---------modules/auth.py1
l---------modules/auth_page.py1
l---------modules/config.py1
l---------modules/expert_mode.py1
l---------modules/file_explorer.py1
l---------modules/forms.py1
l---------modules/help.py1
l---------modules/info.py1
-rw-r--r--modules/installed/apps/apps.py35
-rw-r--r--modules/installed/help/help.py91
-rw-r--r--modules/installed/lib/auth.py118
-rw-r--r--modules/installed/lib/auth_page.py49
-rw-r--r--modules/installed/lib/forms.py123
-rw-r--r--modules/installed/lib/user_store.py112
-rw-r--r--modules/installed/privacy/privacy.py41
-rw-r--r--modules/installed/router/info.py18
-rw-r--r--modules/installed/router/router.py160
-rw-r--r--modules/installed/services/services.py25
-rw-r--r--modules/installed/sharing/file_explorer.py35
-rw-r--r--modules/installed/sharing/sharing.py51
-rw-r--r--modules/installed/system/config.py135
-rw-r--r--modules/installed/system/expert_mode.py70
-rw-r--r--modules/installed/system/system.py50
-rw-r--r--modules/installed/system/time_zones572
-rw-r--r--modules/installed/system/users.py127
-rw-r--r--modules/installed/system/wan.py73
l---------modules/privacy.py1
l---------modules/router.py1
l---------modules/services.py1
l---------modules/sharing.py1
l---------modules/system.py1
l---------modules/user_store.py1
l---------modules/users.py1
l---------modules/wan.py1
36 files changed, 1902 insertions, 0 deletions
diff --git a/modules/__init__.py b/modules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/modules/__init__.py
diff --git a/modules/apps.py b/modules/apps.py
new file mode 120000
index 0000000..d69ec80
--- /dev/null
+++ b/modules/apps.py
@@ -0,0 +1 @@
+installed/apps/apps.py \ No newline at end of file
diff --git a/modules/auth.py b/modules/auth.py
new file mode 120000
index 0000000..7a4c6ec
--- /dev/null
+++ b/modules/auth.py
@@ -0,0 +1 @@
+installed/lib/auth.py \ No newline at end of file
diff --git a/modules/auth_page.py b/modules/auth_page.py
new file mode 120000
index 0000000..7ce3ca2
--- /dev/null
+++ b/modules/auth_page.py
@@ -0,0 +1 @@
+installed/lib/auth_page.py \ No newline at end of file
diff --git a/modules/config.py b/modules/config.py
new file mode 120000
index 0000000..2e37164
--- /dev/null
+++ b/modules/config.py
@@ -0,0 +1 @@
+installed/system/config.py \ No newline at end of file
diff --git a/modules/expert_mode.py b/modules/expert_mode.py
new file mode 120000
index 0000000..aaa140e
--- /dev/null
+++ b/modules/expert_mode.py
@@ -0,0 +1 @@
+installed/system/expert_mode.py \ No newline at end of file
diff --git a/modules/file_explorer.py b/modules/file_explorer.py
new file mode 120000
index 0000000..f191916
--- /dev/null
+++ b/modules/file_explorer.py
@@ -0,0 +1 @@
+installed/sharing/file_explorer.py \ No newline at end of file
diff --git a/modules/forms.py b/modules/forms.py
new file mode 120000
index 0000000..4b0a507
--- /dev/null
+++ b/modules/forms.py
@@ -0,0 +1 @@
+installed/lib/forms.py \ No newline at end of file
diff --git a/modules/help.py b/modules/help.py
new file mode 120000
index 0000000..3fc0799
--- /dev/null
+++ b/modules/help.py
@@ -0,0 +1 @@
+installed/help/help.py \ No newline at end of file
diff --git a/modules/info.py b/modules/info.py
new file mode 120000
index 0000000..0cc4052
--- /dev/null
+++ b/modules/info.py
@@ -0,0 +1 @@
+installed/router/info.py \ No newline at end of file
diff --git a/modules/installed/apps/apps.py b/modules/installed/apps/apps.py
new file mode 100644
index 0000000..1de7617
--- /dev/null
+++ b/modules/installed/apps/apps.py
@@ -0,0 +1,35 @@
+import cherrypy
+from modules.auth import require
+from plugin_mount import PagePlugin
+import cfg
+
+class Apps(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("apps")
+ self.menu = cfg.main_menu.add_item("User Apps", "/apps", 80)
+ self.menu.add_item("Photo Gallery", "/apps/photos", 35)
+
+ @cherrypy.expose
+ def index(self):
+ main = """
+ <p>User Applications are web apps hosted on your %s.</p>
+
+ <p>Eventually this box could be your photo sharing site, your
+ instant messaging site, your social networking site, your news
+ site. Remember web portals? We can be one of those too.
+ Many of the services you use on the web could soon be on site
+ and under your control!</p>
+ """ % (cfg.product_name)
+ return self.fill_template(title="User Applications", main=main, sidebar_right='')
+
+ @cherrypy.expose
+ @require()
+ def photos(self):
+ return self.fill_template(title="Open ID", main='', sidebar_right="""
+<h2>Photo Gallery</h2><p>Your photos might well be the most valuable
+digital property you have, so why trust it to companies that have no
+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>
+""")
diff --git a/modules/installed/help/help.py b/modules/installed/help/help.py
new file mode 100644
index 0000000..28229a3
--- /dev/null
+++ b/modules/installed/help/help.py
@@ -0,0 +1,91 @@
+import os
+import cherrypy
+from gettext import gettext as _
+from plugin_mount import PagePlugin
+import cfg
+class Help(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("help")
+ self.menu = cfg.main_menu.add_item(_("Documentation and FAQ"), "/help", 101)
+ self.menu.add_item(_("Where to Get Help"), "/help/index", 5)
+ self.menu.add_item(_("Developer's Manual"), "/help/view/plinth", 10)
+ self.menu.add_item(_("FAQ"), "/help/view/faq", 20)
+ self.menu.add_item(_("%s Wiki" % cfg.box_name), "http://wiki.debian.org/FreedomBox", 30)
+ self.menu.add_item(_("Design and Architecture"), "/help/view/design", 40)
+ self.menu.add_item(_("About"), "/help/about", 100)
+
+ @cherrypy.expose
+ def index(self):
+ main="""
+ <p>There are a variety of places to go for help with Plinth
+ and the box it runs on.</p>
+
+ <p>This front end has a <a
+ href="/help/view/plinth">developer's manual</a>. It isn't
+ complete, but it is the first place to look. Feel free to
+ offer suggestions, edits, and screenshots for completing
+ it!</p>
+
+ <p><a href="http://wiki.debian.org/FreedomBox">A section of
+ the Debian wiki</a> is devoted to the %(box)s. At some
+ point the documentation in the wiki and the documentation in
+ the manual should dovetail.</p>
+
+ <p>I have collected some of my thoughts about the %(box)s
+ in a <a href="/help/view/design">document focused on its
+ design and architecture</a>.</p>
+
+ <p>There
+ are Debian gurus in the \#debian channels of both
+ irc.freenode.net and irc.oftc.net. They probably don't know
+ much about the %(box)s and almost surely know nothing of
+ this front end, but they are an incredible resource for
+ general Debian issues.</p>
+
+ <p>There is no <a href="/help/view/faq">FAQ</a> because
+ the question frequency is currently zero for all
+ questions.</p>
+ """ % {'box':cfg.box_name}
+ return self.fill_template(title="Documentation and FAQ", main=main)
+
+ @cherrypy.expose
+ def about(self):
+ return self.fill_template(title=_("About the %s" % cfg.box_name), main="""
+ <p> We live in a world where our use of the network is
+ mediated by organizations that often do not have our best
+ interests at heart. By building software that does not rely on
+ a central service, we can regain control and privacy. By
+ keeping our data in our homes, we gain useful legal
+ protections over it. By giving back power to the users over
+ their networks and machines, we are returning the Internet to
+ its intended peer-to-peer architecture.</p>
+
+ <p>In order to bring about the new network order, it is
+ paramount that it is easy to convert to it. The hardware it
+ runs on must be cheap. The software it runs on must be easy to
+ install and administrate by anybody. It must be easy to
+ transition from existing services.</p>
+
+ <p>There are a number of projects working to realize a future
+ of distributed services; we aim to bring them all together in
+ a convenient package.</p>
+
+ <p>For more information about the Freedom Box project, see the
+ <a href="http://wiki.debian.org/FreedomBox">Debian
+ Wiki</a>.</p>""")
+
+class View(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("help.view")
+
+ @cherrypy.expose
+ def default(self, page=''):
+ if page not in ['design', 'plinth', 'hacking', 'faq']:
+ raise cherrypy.HTTPError(404, "The path '/help/view/%s' was not found." % page)
+ return self.fill_template(template="err", main="<p>Sorry, as much as I would like to show you that page, I don't seem to have a page named %s!</p>" % page)
+ IF = open(os.path.join("doc", "%s.part.html" % page), 'r')
+ main = IF.read()
+ IF.close()
+ return self.fill_template(template="two_col", title=_("%s Documentation" % cfg.product_name), main=main)
diff --git a/modules/installed/lib/auth.py b/modules/installed/lib/auth.py
new file mode 100644
index 0000000..4b0f229
--- /dev/null
+++ b/modules/installed/lib/auth.py
@@ -0,0 +1,118 @@
+# Form based authentication for CherryPy. Requires the
+# Session tool to be loaded.
+#
+# Thanks for this code is owed to Arnar Birgisson -at - gmail.com. It
+# is based on code he wrote that was retrieved from
+# http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
+# on 1 February 2011.
+
+# TODO: DeprecationWarning: the md5 module is deprecated; use hashlib instead import md5
+
+import cherrypy
+import urllib, hashlib
+import cfg
+
+cfg.session_key = '_cp_username'
+
+def check_credentials(username, passphrase):
+ """Verifies credentials for username and passphrase.
+ Returns None on success or a string describing the error on failure"""
+
+ u = cfg.users.get(username)
+ if u is None:
+ cfg.log("Unknown user: %s" % username)
+ return u"Username %s is unknown to me." % username
+ if u['passphrase'] != hashlib.md5(passphrase).hexdigest():
+ return u"Incorrect passphrase."
+
+
+def check_auth(*args, **kwargs):
+ """A tool that looks in config for 'auth.require'. If found and it
+ is not None, a login is required and the entry is evaluated as a
+ list of conditions that the user must fulfill"""
+ conditions = cherrypy.request.config.get('auth.require', None)
+ if conditions is not None:
+ username = cherrypy.session.get(cfg.session_key)
+ if username:
+ cherrypy.request.login = username
+ for condition in conditions:
+ # A condition is just a callable that returns true or false
+ if not condition():
+ raise cherrypy.HTTPRedirect("/auth/login")
+ else:
+ raise cherrypy.HTTPRedirect("/auth/login")
+
+def check_auth(*args, **kwargs):
+ """A tool that looks in config for 'auth.require'. If found and it
+ is not None, a login is required and the entry is evaluated as a
+ list of conditions that the user must fulfill"""
+ conditions = cherrypy.request.config.get('auth.require', None)
+ # format GET params
+ get_params = urllib.quote(cherrypy.request.request_line.split()[1])
+ if conditions is not None:
+ username = cherrypy.session.get(cfg.session_key)
+ if username:
+ cherrypy.request.login = username
+ for condition in conditions:
+ # A condition is just a callable that returns true or false
+ if not condition():
+ # Send old page as from_page parameter
+ raise cherrypy.HTTPRedirect("/auth/login?from_page=%s" % get_params)
+ else:
+ # Send old page as from_page parameter
+ raise cherrypy.HTTPRedirect("/auth/login?from_page=%s" % get_params)
+
+cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)
+
+def require(*conditions):
+ """A decorator that appends conditions to the auth.require config
+ variable."""
+ def decorate(f):
+ if not hasattr(f, '_cp_config'):
+ f._cp_config = dict()
+ if 'auth.require' not in f._cp_config:
+ f._cp_config['auth.require'] = []
+ f._cp_config['auth.require'].extend(conditions)
+ return f
+ return decorate
+
+
+# Conditions are callables that return True
+# if the user fulfills the conditions they define, False otherwise
+#
+# They can access the current username as cherrypy.request.login
+#
+# Define those at will however suits the application.
+
+def member_of(groupname):
+ def check():
+ # replace with actual check if <username> is in <groupname>
+ return cherrypy.request.login == 'joe' and groupname == 'admin'
+ return check
+
+def name_is(reqd_username):
+ return lambda: reqd_username == cherrypy.request.login
+
+# These might be handy
+
+def any_of(*conditions):
+ """Returns True if any of the conditions match"""
+ def check():
+ for c in conditions:
+ if c():
+ return True
+ return False
+ return check
+
+# By default all conditions are required, but this might still be
+# needed if you want to use it inside of an any_of(...) condition
+def all_of(*conditions):
+ """Returns True if all of the conditions match"""
+ def check():
+ for c in conditions:
+ if not c():
+ return False
+ return True
+ return check
+
+
diff --git a/modules/installed/lib/auth_page.py b/modules/installed/lib/auth_page.py
new file mode 100644
index 0000000..6525dc6
--- /dev/null
+++ b/modules/installed/lib/auth_page.py
@@ -0,0 +1,49 @@
+import cherrypy
+import cfg
+from plugin_mount import PagePlugin
+from modules.forms import Form
+from auth import *
+# Controller to provide login and logout actions
+
+class AuthController(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("auth")
+
+ def on_login(self, username):
+ """Called on successful login"""
+
+ def on_logout(self, username):
+ """Called on logout"""
+
+ def get_loginform(self, username, msg='', from_page="/"):
+ form = Form(title="Login", action="/auth/login", message=msg)
+ form.text_input(name="from_page", value=from_page, type="hidden")
+ form.text_input("Username", name="username", value=username)
+ form.text_input("Passphrase", name="passphrase", type="password")
+ form.submit(label="Login")
+
+ return self.fill_template(main=form.render(), sidebar_right=" ")
+
+ @cherrypy.expose
+ def login(self, username=None, passphrase=None, from_page="/", **kwargs):
+ if username is None or passphrase is None:
+ return self.get_loginform("", from_page=from_page)
+
+ error_msg = check_credentials(username, passphrase)
+ if error_msg:
+ return self.get_loginform(username, error_msg, from_page)
+ else:
+ cherrypy.session[cfg.session_key] = cherrypy.request.login = username
+ self.on_login(username)
+ raise cherrypy.HTTPRedirect(from_page or "/")
+
+ @cherrypy.expose
+ def logout(self, from_page="/"):
+ sess = cherrypy.session
+ username = sess.get(cfg.session_key, None)
+ sess[cfg.session_key] = None
+ if username:
+ cherrypy.request.login = None
+ self.on_logout(username)
+ raise cherrypy.HTTPRedirect(from_page or "/")
diff --git a/modules/installed/lib/forms.py b/modules/installed/lib/forms.py
new file mode 100644
index 0000000..aac4cd4
--- /dev/null
+++ b/modules/installed/lib/forms.py
@@ -0,0 +1,123 @@
+class Form():
+ def __init__(self, action=None, cls='form', title=None, onsubmit=None, name=None, message='', method="get"):
+ """Note that there appears to be a bug in cherrypy whereby
+ forms submitted via post don't have their fields included in
+ kwargs for the default index method. So we use get by
+ default, though it's not as neat."""
+
+ action = self.get_form_attrib_text('action', action)
+ onsubmit = self.get_form_attrib_text('onsubmit', onsubmit)
+ name = self.get_form_attrib_text('name', name)
+
+ self.pretext = ' <form class="%s" method="%s" %s%s%s>\n' % (cls, method, action, onsubmit, name)
+ if title:
+ self.pretext += ' <h2>%s</h2>\n' % title
+
+ if message:
+ self.message = "<h3>%s</h3>" % message
+ else:
+ self.message = ''
+ self.text = ''
+ self.end_text = "</form>\n"
+ def get_form_attrib_text(self, field, val):
+ if val:
+ return ' %s="%s"' % (field, val)
+ else:
+ return ''
+ def html(self, html):
+ self.text += html
+ def dropdown(self, label='', name=None, id=None, vals=None, select=None, onchange=''):
+ """vals is a list of values.
+ select is the index in vals of the selected item. None means no item is selected yet."""
+ name, id = self.name_or_id(name, id)
+ self.text += (""" <label>
+ <span>%(label)s</span>
+ <select name="%(name)s" id="%(id)s" onchange="%(onchange)s">\n"""
+ % {'label':label, 'name':name, 'id':id, 'onchange':onchange})
+ for i in range(len(vals)):
+ v = vals[i]
+ if i == select:
+ selected = "SELECTED"
+ else:
+ selected = ''
+ self.text +=" <option value=\"%s\" %s>%s</option>\n" % (v, selected, v)
+ self.text += """ </select>
+ </label>\n"""
+
+ def dotted_quad(self, label='', name=None, id=None, quad=None):
+ name, id = self.name_or_id(name, id)
+ if not quad:
+ quad = [0,0,0,0]
+ self.text += """ <label>
+ <span>%(label)s</span>
+ <input type="text" class="inputtextnowidth" name="%(name)s0" id="%(id)s0" value="%(q0)s" maxlength="3" size="1"/><b>.</b>
+ <input type="text" class="inputtextnowidth" name="%(name)s1" id="%(id)s1" value="%(q1)s" maxlength="3" size="1"/><b>.</b>
+ <input type="text" class="inputtextnowidth" name="%(name)s2" id="%(id)s2" value="%(q2)s" maxlength="3" size="1"/><b>.</b>
+ <input type="text" class="inputtextnowidth" name="%(name)s3" id="%(id)s3" value="%(q3)s" maxlength="3" size="1"/>
+ </label>""" % {'label':label, 'name':name, 'id':id, 'q0':quad[0], 'q1':quad[1], 'q2':quad[2], 'q3':quad[3]}
+
+ def text_input(self, label='', name=None, id=None, type='text', value='', size=20):
+ name, id = self.name_or_id(name, id)
+ if type=="hidden":
+ self.text += '<input type="%s" class="inputtext" name="%s" id="%s" value="%s"/>' % (type, name, id, value)
+ else:
+ self.text += """ <label>
+ <span>%s</span>
+ <input type="%s" class="inputtext" name="%s" id="%s" value="%s" size="%s"/>
+ </label>""" % (label, type, name, id, value, size)
+ def text_box(self, label='', name=None, id=None):
+ name, id = self.name_or_id(name, id)
+ self.text += """
+ <label>
+ <span>%s</span>
+ <textarea class="textbox" name="%s" id="%s"></textarea>
+ </label>""" % (label, name, id)
+ def submit(self, label='', name=None, id=None):
+ name, id = self.name_or_id(name, id)
+ self.text += """
+ <div class="submit">
+ <label><span></span>
+ <input type="submit" class="button" value="%s" name="%s" id="%s" />
+ </label></div>\n""" % (label, name, id)
+ def submit_row(self, buttons):
+ """buttons is a list of tuples, each containing label, name, id. Name and id are optional."""
+ self.text += '<div class="submit"><label>'
+ button_text = ''
+ for button in buttons:
+ label = button[0]
+ try:
+ name = button[1]
+ except:
+ name = None
+
+ try:
+ id = button[2]
+ except:
+ id = None
+
+ name, id = self.name_or_id(name, id)
+
+ if button_text != '':
+ button_text += "&nbsp;"
+ button_text += '<input type="submit" class="button" value="%s" name="%s" id="%s" />\n' % (label, name, id)
+ self.text += '%s</div></label>' % button_text
+ def name_or_id(self, name, id):
+ if not name: name = id
+ if not id: id = name
+ if not name: name = ''
+ if not id: id = ''
+ return name, id
+ def checkbox(self, label='', name='', id='', checked=''):
+ name, id = self.name_or_id(name, id)
+ if checked:
+ checked = 'checked="on"'
+ self.text += """
+ <div class="checkbox">
+ <label>
+ <span>%s</span>
+ <input type=checkbox name="%s" id="%s" %s/>
+ </label></div>\n""" % (label, name, id, 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):
+ return self.pretext+self.message+self.text+self.end_text
diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py
new file mode 100644
index 0000000..f5a7f48
--- /dev/null
+++ b/modules/installed/lib/user_store.py
@@ -0,0 +1,112 @@
+import os
+import simplejson as json
+import cherrypy
+import cfg
+from model import User
+from plugin_mount import UserStoreModule
+
+class UserStore(UserStoreModule):
+ """The user storage is on disk. Rather than slurp the entire
+ thing, we read from the disk as needed. Writes are immediate,
+ though.
+
+ TODO: file locking"""
+ def __init__(self):
+ self.data_dir = cfg.users_dir
+ self.users = {}
+ def sanitize(username):
+ """Return username with nonalphanumeric/underscore chars
+ removed.
+
+ TODO: allow international chars in usernames."""
+ pass
+ def attr(self, key, username):
+ """Return field from username's record. If key does not
+ exist, don't raise attribute error, just return None.
+
+ User defaults to current. If no current user and none
+ specified, return none."""
+ try:
+ return self.get(username)[key]
+ except AttributeError:
+ return None
+
+ 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):
+ """Return True if user username is an expert, else False.
+
+ If username is None, use the current user. If no such user exists, return False."""
+ user = self.get(username)
+ if not user:
+ return False
+ return 'expert' in user['groups']
+
+ def get(self, username=None, reload=False):
+ """Returns a user instance with the user's info or else None if the user couldn't be found.
+
+ If reload is true, reload from disk, regardless of dict's contents
+
+ If username is None, try current user. If no current user, return None.
+
+ TODO: implement user_store.get reload"""
+
+ if not username:
+ username = self.current(name=True)
+
+ try:
+ return self.users[username]
+ except KeyError:
+ try:
+ IF = open(os.path.join(self.data_dir, username), "r")
+ except IOError:
+ return None
+ data = IF.read()
+ IF.close()
+
+ # We cache the result, and since we assume a system with
+ # relatively few users and small user data files, we never
+ # expire those results. If we revisit that assumption, we
+ # might need some cache expiry.
+ self.users[username] = User(json.loads(data))
+
+ return self.users[username]
+ def exists(self, username):
+ """Return True if username exists, else False."""
+ return username in self.users or os.path.exists(os.path.join(cfg.users_dir, username))
+ def get_all(self):
+ "Returns a list of all the user objects"
+ usernames = os.listdir(self.data_dir)
+ for name in usernames:
+ self.get(name)
+ return self.users
+ def set(self, user):
+ """Set the user data, both in memory and as needed in storage."""
+ OF = open(os.path.join(self.data_dir, user['username']), 'w')
+ OF.write(json.dumps(user))
+ OF.close()
+ def remove(self, user):
+ """Delete the user from storage and RAM. User can be a user instance or a username."""
+ try:
+ name = user['name']
+ except TypeError:
+ if isinstance(user, basestring):
+ name = user
+ else:
+ raise TypeError
+ os.unlink(os.path.join(cfg.users_dir, name))
+ try:
+ del self.users[name]
+ except KeyError:
+ pass
+ cfg.log.info("%s deleted %s" % (cherrypy.session.get(cfg.session_key), name))
diff --git a/modules/installed/privacy/privacy.py b/modules/installed/privacy/privacy.py
new file mode 100644
index 0000000..b62ba5d
--- /dev/null
+++ b/modules/installed/privacy/privacy.py
@@ -0,0 +1,41 @@
+import cherrypy
+from gettext import gettext as _
+from plugin_mount import PagePlugin
+from modules.auth import require
+import cfg
+
+class Privacy(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("privacy")
+ self.menu = cfg.main_menu.add_item("Privacy Controls", "/privacy", 12)
+ self.menu.add_item("General Config", "/privacy/config", 10)
+ self.menu.add_item("Ad Blocking", "/privacy/adblock", 20)
+ self.menu.add_item("TOR", "/privacy/TOR", 30)
+ self.menu.add_item("HTTPS Everywhere", "/privacy/https_everywhere", 30)
+
+ @cherrypy.expose
+ def index(self):
+ #raise cherrypy.InternalRedirect('/privacy/config')
+ return self.config()
+
+ @cherrypy.expose
+ @require()
+ def config(self):
+ main="""
+ <p>Privacy controls are not yet implemented. This page is a
+ placeholder and a promise: privacy is important enough that it
+ is a founding consideration, not an afterthought.</p>
+ """
+ return self.fill_template(title=_("Privacy Control Panel"), main=main,
+sidebar_right=_("""<h2>Statement of Principles</h2><p>When we say your
+privacy is important, it's not just an empty pleasantry. We really
+mean it. Your privacy control panel should give you fine-grained
+control over exactly who can access your %s and the
+information on it.</p>
+
+<p>Your personal information should not leave this box without your
+knowledge and direction. And if companies or government wants this
+information, they have to ask <b>you</b> for it. This gives you a
+change to refuse and also tells you who wants your data.</p>
+""") % cfg.product_name)
diff --git a/modules/installed/router/info.py b/modules/installed/router/info.py
new file mode 100644
index 0000000..1f4585a
--- /dev/null
+++ b/modules/installed/router/info.py
@@ -0,0 +1,18 @@
+import cherrypy
+from plugin_mount import PagePlugin
+from modules.auth import require
+
+class Info(PagePlugin):
+ title = 'Info'
+ order = 10
+ url = 'info'
+
+ def __init__(self, *args, **kwargs):
+ self.register_page("router.info")
+
+ @cherrypy.expose
+ @require()
+ def index(self):
+ return self.fill_template(title="Router Information", main="""
+<p> Eventually we will display a bunch of info, graphs and logs about the routing functions here.</p>
+""")
diff --git a/modules/installed/router/router.py b/modules/installed/router/router.py
new file mode 100644
index 0000000..8d9ac1d
--- /dev/null
+++ b/modules/installed/router/router.py
@@ -0,0 +1,160 @@
+from urlparse import urlparse
+import os, cherrypy
+from gettext import gettext as _
+from plugin_mount import PagePlugin, PluginMount, FormPlugin
+from modules.auth import require
+from forms import Form
+from util import *
+import cfg
+
+class router(PagePlugin):
+ order = 10 # order of running init in PagePlugins
+ def __init__(self, *args, **kwargs):
+ self.register_page("router")
+ self.menu = cfg.main_menu.add_item("Router Admin", "/router", 10)
+ self.menu.add_item("Wireless", "/router/wireless", 12)
+ self.menu.add_item("Firewall", "/router/firewall", 18)
+ self.menu.add_item("Hotspot and Mesh", "/router/hotspot")
+ self.menu.add_item("Info", "/router/info", 100)
+
+ @cherrypy.expose
+ def index(self):
+ """This isn't an internal redirect, because we need the url to
+ reflect that we've moved down into the submenu hierarchy.
+ Otherwise, it's hard to know which menu portion to make active
+ or expand or contract."""
+ raise cherrypy.HTTPRedirect('/router/setup')
+
+ @cherrypy.expose
+ @require()
+ def wireless(self):
+ return self.fill_template(title="Wireless", main="<p>wireless setup: essid, etc.</p>")
+
+ @cherrypy.expose
+ @require()
+ def firewall(self):
+ return self.fill_template(title="Firewall", main="<p>Iptables twiddling.</p>")
+
+ @cherrypy.expose
+ @require()
+ def hotspot(self):
+ return self.fill_template(title="Hotspot and Mesh", main="<p>connection sharing setup.</p>")
+
+
+
+class setup(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ self.register_page("router.setup")
+ self.menu = cfg.html_root.router.menu.add_item("General Setup", "/router/setup", 10)
+ self.menu.add_item("Dynamic DNS", "/router/setup/ddns", 20)
+ self.menu.add_item("MAC Address Clone", "/router/setup/mac_address", 30)
+
+ @cherrypy.expose
+ @require()
+ def index(self):
+ parts = self.forms('/router/setup')
+ parts['title'] = "General Router Setup"
+ parts['sidebar_right']="""<h2>Introduction</h2><p>Your %s is a replacement for your
+wireless router. By default, it should do everything your usual
+router does. With the addition of some extra modules, its abilities
+can rival those of high-end routers costing hundreds of dollars.</p>
+""" % cfg.box_name + parts['sidebar_right']
+ if not cfg.users.expert():
+ main += """<p>In basic mode, you don't need to do any
+ router setup before you can go online. Just plug your
+ %(product)s in to your cable or DSL modem and the router
+ will try to get you on the internet using DHCP.</p>
+
+ <p>If that fails, you might need to resort to the expert
+ options. Enable expert mode in the "%(product)s System /
+ Configure" menu.</p>""" % {'product':cfg.box_name}
+ else:
+ parts['main'] += "<p>router name, domain name, router IP, dhcp</p>"
+ return self.fill_template(**parts)
+
+ @cherrypy.expose
+ @require()
+ def ddns(self):
+ return self.fill_template(title="Dynamic DNS", main="<p>Masquerade setup</p>")
+
+ @cherrypy.expose
+ @require()
+ def mac_address(self):
+ return self.fill_template(title="MAC Address Cloning",
+ main="<p>Your router can pretend to have a different MAC address on any interface.</p>")
+
+
+class wan(FormPlugin, PagePlugin):
+ url = ["/router/setup"]
+ order = 10
+
+ js = """<script LANGUAGE="JavaScript">
+ <!--
+ function hideshow_static() {
+ var d = document.getElementById('connect_type');
+ connect_type = d.value;
+ if (connect_type != 'Static IP') {
+ hide("static_ip_form");
+ } else {
+ show("static_ip_form");
+ }
+ }
+ // -->
+ </script>"""
+
+ def sidebar_right(self, *args, **kwargs):
+ side=''
+ if cfg.users.expert():
+ side += """<h2>WAN Connection Type</h2>
+ <h3>DHCP</h3><p>DHCP allows your router to automatically
+ connect with the upstream network. If you are unsure what
+ option to choose, stick with DHCP. It is usually
+ correct.
+
+ <h3>Static IP</h3><p>If you want to setup your connection
+ manually, you can enter static IP information. This option is
+ for those who know what they're doing. As such, it is only
+ available in expert mode.</p>"""
+ return side
+
+ def main(self, wan_ip0=0, wan_ip1=0, wan_ip2=0, wan_ip3=0,
+ subnet0=0, subnet1=0, subnet2=0, subnet3=0,
+ gateway0=0, gateway1=0, gateway2=0, gateway3=0,
+ dns10=0, dns11=0, dns12=0, dns13=0,
+ dns20=0, dns21=0, dns22=0, dns23=0,
+ dns30=0, dns31=0, dns32=0, dns33=0,
+ message=None, **kwargs):
+ if not cfg.users.expert():
+ return ''
+
+ store = filedict_con(cfg.store_file, 'router')
+ defaults = {'connect_type': "'DHCP'",
+ }
+ for k,c in defaults.items():
+ if not k in kwargs:
+ try:
+ kwargs[k] = store[k]
+ except KeyError:
+ exec("if not '%(k)s' in kwargs: store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c})
+
+ form = Form(title="WAN Connection",
+ action="/router/setup/wan/index",
+ name="wan_connection_form",
+ message=message)
+ form.dropdown('Connection Type', vals=["DHCP", "Static IP"], id="connect_type", onchange="hideshow_static()")
+ form.html('<div id="static_ip_form">')
+ form.dotted_quad("WAN IP Address", name="wan_ip", quad=[wan_ip0, wan_ip1, wan_ip2, wan_ip3])
+ form.dotted_quad("Subnet Mask", name="subnet", quad=[subnet0, subnet1, subnet2, subnet3])
+ form.dotted_quad("Gateway", name="gateway", quad=[gateway0, gateway1, gateway2, gateway3])
+ form.dotted_quad("Static DNS 1", name="dns1", quad=[dns10, dns11, dns12, dns13])
+ form.dotted_quad("Static DNS 2", name="dns2", quad=[dns20, dns21, dns22, dns23])
+ form.dotted_quad("Static DNS 3", name="dns3", quad=[dns30, dns31, dns32, dns33])
+ form.html('</div>')
+ form.html(""" <script LANGUAGE="JavaScript">
+ <!--
+ hideshow_static();
+ // -->
+ </script>""")
+ form.submit("Set Wan")
+ return form.render()
+
diff --git a/modules/installed/services/services.py b/modules/installed/services/services.py
new file mode 100644
index 0000000..c99da08
--- /dev/null
+++ b/modules/installed/services/services.py
@@ -0,0 +1,25 @@
+import cherrypy
+from modules.auth import require
+from plugin_mount import PagePlugin
+import cfg
+
+class Services(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("services")
+ self.menu = cfg.main_menu.add_item("Other Services", "/services", 90)
+ self.menu.add_item("Open ID", "/services/openid", 35)
+
+ @cherrypy.expose
+ def index(self):
+ return self.openid()
+
+ @cherrypy.expose
+ @require()
+ def openid(self):
+ return self.fill_template(title="Open ID", main='', sidebar_right="""
+<h2>One Login for Every Site</h2><p>Your %s is also an OpenID
+machine. It can generate credentials that allow you to log in to many
+websites without the need to remember or enter a separate username and
+password at each one.</p>
+""" % cfg.box_name)
diff --git a/modules/installed/sharing/file_explorer.py b/modules/installed/sharing/file_explorer.py
new file mode 100644
index 0000000..1e7ce6f
--- /dev/null
+++ b/modules/installed/sharing/file_explorer.py
@@ -0,0 +1,35 @@
+import cherrypy
+from modules.auth import require
+from plugin_mount import PagePlugin
+import cfg
+
+class FileExplorer(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("sharing.explorer")
+ cfg.html_root.sharing.menu.add_item("File Explorer", "/sharing/explorer", 30)
+
+ @cherrypy.expose
+ @require()
+ def index(self):
+ main = """
+<p>File explorer for users that also have shell accounts.</p> <p>Until
+that is written (and it will be a while), we should install <a
+href="http://www.mollify.org/demo.php">mollify</a> or <a
+href="http://www.ajaxplorer.info/wordpress/demo/">ajaxplorer</a>, but
+of which seem to have some support for playing media files in the
+browser (as opposed to forcing users to download and play them
+locally). The downsides to third-party explorers are: they're don't
+fit within our theme system, they require separate login, and they're
+written in php, which will make integrating them hard.</p>
+
+<p>There are, of course, many other options for php-based file
+explorers. These were the ones I saw that might do built-in media
+players.</p>
+
+<p>For python-friendly options, check out <a
+href="http://blogfreakz.com/jquery/web-based-filemanager/">FileManager</a>.
+It appears to be mostly javascript with some bindings to make it
+python-friendly.</p>
+"""
+ return self.fill_template(title="File Explorer", main=main, sidebar_right='')
diff --git a/modules/installed/sharing/sharing.py b/modules/installed/sharing/sharing.py
new file mode 100644
index 0000000..fcba993
--- /dev/null
+++ b/modules/installed/sharing/sharing.py
@@ -0,0 +1,51 @@
+import cherrypy
+from gettext import gettext as _
+from modules.auth import require
+from plugin_mount import PagePlugin
+import cfg
+
+class Sharing(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("sharing")
+ self.menu = cfg.main_menu.add_item("Resource Sharing", "/sharing", 35)
+ self.menu.add_item("File Server", "/sharing/files", 10)
+
+ @cherrypy.expose
+ def index(self):
+ """This isn't an internal redirect, because we need the url to
+ reflect that we've moved down into the submenu hierarchy.
+ Otherwise, it's hard to know which menu portion to make active
+ or expand or contract."""
+ raise cherrypy.HTTPRedirect('/sharing/files')
+
+ @cherrypy.expose
+ @require()
+ def files(self):
+ return self.fill_template(title="File Server", main='', sidebar_right=_("""
+<h2>Freedom NAS</h2><p> The %s can make your spare hard drives accessible to your
+local network, thus acting as a NAS server. We currently support
+sharing files via NFS and SMB.
+
+TODO: this is not true. We currently support no sharing.</p>
+""" % cfg.box_name))
+
+#TODO: move PrinterSharing to another file, as it should be an optional module (most people don't care about printer sharing)
+class PrinterSharing(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("sharing.printer")
+ cfg.html_root.sharing.menu.add_item("Printer Sharing", "/sharing/printer", 35)
+
+ @cherrypy.expose
+ @require()
+ def index(self):
+ main = """
+<p>TODO: Setup and install SAMBA</p>
+<p>TODO: Setup and install CUPS</p>
+"""
+ return self.fill_template(title="Printer Sharing", main=main, sidebar_right="""
+<h2>Share Your Printer</h2><p> The %s can share your printer via Samba and CUPS.</p>
+""" % cfg.box_name)
+
+
diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py
new file mode 100644
index 0000000..151b710
--- /dev/null
+++ b/modules/installed/system/config.py
@@ -0,0 +1,135 @@
+import os, shutil, subprocess
+from socket import gethostname
+import cherrypy
+import simplejson as json
+from gettext import gettext as _
+from filedict import FileDict
+from modules.auth import require
+from plugin_mount import PagePlugin, FormPlugin
+import cfg
+from forms import Form
+from model import User
+from util import *
+
+class Config(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ self.register_page("sys.config")
+
+ @cherrypy.expose
+ @require()
+ def index(self):
+ parts = self.forms('/sys/config')
+ parts['title']=_("Configure this %s" % cfg.box_name)
+ return self.fill_template(**parts)
+
+def valid_hostname(name):
+ """Return '' if name is a valid hostname by our standards (not
+ just by RFC 952 and RFC 1123. We're more conservative than the
+ standard. If hostname isn't valid, return message explaining why."""
+
+ message = ''
+ if len(name) > 63:
+ message += "<br />Hostname too long (max is 63 characters)"
+
+ if not is_alphanumeric(name):
+ message += "<br />Hostname must be alphanumeric"
+
+ if not name[0] in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
+ message += "<br />Hostname must start with a letter"
+
+ return message
+
+def set_hostname(hostname):
+ "Sets machine hostname to hostname"
+ cfg.log.info("Writing '%s' to /etc/hostname" % hostname)
+ unslurp("/etc/hostname", hostname+"\n")
+ try:
+ retcode = subprocess.call("/etc/init.d/hostname.sh start", shell=True)
+ if retcode < 0:
+ cfg.log.error("Hostname restart terminated by signal: return code is %s" % retcode)
+ else:
+ cfg.log.debug("Hostname restart returned %s" % retcode)
+ except OSError, e:
+ raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e)
+
+ sys_store = filedict_con(cfg.store_file, 'sys')
+ sys_store['hostname'] = hostname
+
+class general(FormPlugin, PagePlugin):
+ url = ["/sys/config"]
+ 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 _(#"""<h2>Time Zone</h2>
+ """<p>Set your timezone to get accurate
+ timestamps. %(product)s will use this information to set your
+ %(box)s's systemwide timezone.</p>
+ """ % {'product':cfg.product_name, 'box':cfg.box_name})
+
+ def main(self, message='', **kwargs):
+ sys_store = filedict_con(cfg.store_file, 'sys')
+ defaults = {'time_zone': "slurp('/etc/timezone').rstrip()",
+ 'hostname': "gethostname()",
+ }
+ for k,c in defaults.items():
+ if not k in kwargs:
+ try:
+ kwargs[k] = sys_store[k]
+ except KeyError:
+ exec("if not '%(k)s' in kwargs: sys_store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c})
+
+ ## Get the list of supported timezones and the index in that list of the current one
+ module_file = __file__
+ if module_file.endswith(".pyc"):
+ module_file = module_file[:-1]
+ time_zones = json.loads(slurp(os.path.join(os.path.dirname(os.path.realpath(module_file)), "time_zones")))
+ for i in range(len(time_zones)):
+ if kwargs['time_zone'] == time_zones[i]:
+ time_zone_id = i
+ break
+
+ ## A little sanity checking. Make sure the current timezone is in the list.
+ try:
+ cfg.log('kwargs tz: %s, from_table: %s' % (kwargs['time_zone'], time_zones[time_zone_id]))
+ except NameError:
+ cfg.log.critical("Unknown Time Zone: %s" % kwargs['time_zone'])
+ raise cherrypy.HTTPError(500, "Unknown Time Zone: %s" % kwargs['time_zone'])
+
+ ## And now, the form.
+ form = Form(title=_("General Config"),
+ action="/sys/config/general/index",
+ name="config_general_form",
+ message=message )
+ form.html(self.help())
+ form.dropdown(_("Time Zone"), name="time_zone", vals=time_zones, select=time_zone_id)
+ form.html("<p>Your hostname is the local name by which other machines on your LAN can reach you.</p>")
+ form.text_input('Hostname', name='hostname', value=kwargs['hostname'])
+ form.submit(_("Submit"))
+ return form.render()
+
+ def process_form(self, time_zone='', hostname='', *args, **kwargs):
+ sys_store = filedict_con(cfg.store_file, 'sys')
+ message = ''
+ if hostname != sys_store['hostname']:
+ msg = valid_hostname(hostname)
+ if msg == '':
+ old_val = sys_store['hostname']
+ try:
+ set_hostname(hostname)
+ except:
+ cfg.log.info("Trying to restore old hostname value.")
+ set_hostname(old_val)
+ raise
+ else:
+ message += msg
+ if time_zone != sys_store['time_zone']:
+ src = os.path.join("/usr/share/zoneinfo", time_zone)
+ cfg.log.info("Copying %s to /etc/localtime" % src)
+ shutil.copy(src, "/etc/localtime")
+ sys_store['time_zone'] = time_zone
+ return message or "Settings updated."
+
diff --git a/modules/installed/system/expert_mode.py b/modules/installed/system/expert_mode.py
new file mode 100644
index 0000000..16420a5
--- /dev/null
+++ b/modules/installed/system/expert_mode.py
@@ -0,0 +1,70 @@
+import os
+import cherrypy
+import simplejson as json
+from gettext import gettext as _
+from filedict import FileDict
+from modules.auth import require
+from plugin_mount import PagePlugin, FormPlugin
+import cfg
+from forms import Form
+from model import User
+from util import *
+
+class experts(FormPlugin, PagePlugin):
+ url = ["/sys/config"]
+ order = 10
+
+ def help(self, *args, **kwargs):
+ side = _(#"""<h2>Expert Mode</h2>
+ """
+ <p>The %(box)s can be administered in two modes, 'basic'
+ and 'expert'. Basic mode hides a lot of features and
+ configuration options that most users will never need to think
+ about. Expert mode allows you to get into the details.</p>
+
+ <p>Most users can operate the %(box)s by configuring the
+ limited number of options visible in Basic mode. For the sake
+ of simplicity and ease of use, we hid most of %(product)s's
+ less frequently used options. But if you want more
+ sophisticated features, you can enable Expert mode, and
+ %(product)s will present more advanced menu options.</p>
+
+ <p>You should be aware that it might be possible to render
+ your %(box)s inaccessible via Expert mode options.</p>
+ """ % {'box':cfg.box_name, 'product':cfg.product_name})
+
+ return side
+
+ def main(self, expert=None, message='', **kwargs):
+ """Note that kwargs contains '':"submit" if this is coming
+ from a submitted form. If kwargs is empty, it's a fresh form
+ with no user input, which means it should just reflect the
+ state of the stored data."""
+ if not kwargs and expert == None:
+ expert = cfg.users.expert()
+ cfg.log("Expert mode is %s" % expert)
+ form = Form(title=_("Expert Mode"),
+ action="/sys/config/experts",
+ name="expert_mode_form",
+ message=message )
+ form.html(self.help())
+ form.checkbox(_("Expert Mode"), name="expert", checked=expert)
+ form.submit(_("Submit"))
+ return form.render()
+
+ def process_form(self, expert='', *args, **kwargs):
+ user = cfg.users.get()
+
+ message = 'settings unchanged'
+
+ if expert:
+ if not 'expert' in user['groups']:
+ user['groups'].append('expert')
+ message = "enabled"
+ else:
+ if 'expert' in user['groups']:
+ user['groups'].remove('expert')
+ message = "disabled"
+
+ cfg.users.set(user)
+ return "Expert mode %s." % message
diff --git a/modules/installed/system/system.py b/modules/installed/system/system.py
new file mode 100644
index 0000000..7f04f64
--- /dev/null
+++ b/modules/installed/system/system.py
@@ -0,0 +1,50 @@
+import os
+import cherrypy
+import simplejson as json
+from gettext import gettext as _
+from filedict import FileDict
+from auth import require
+from plugin_mount import PagePlugin, FormPlugin
+import cfg
+from forms import Form
+from model import User
+from util import *
+
+sys_dir = "modules/installed/sys"
+
+
+class Sys(PagePlugin):
+ order = 10
+ def __init__(self, *args, **kwargs):
+ cfg.log("!!!!!!!!!!!!!!!!!!!!")
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("sys")
+ self.menu = cfg.main_menu.add_item(_("%s System" % cfg.product_name), "/sys", 100)
+ self.menu.add_item(_("Configure"), "/sys/config", 10)
+ self.menu.add_item(_("Package Manager"), "/sys/packages", 20)
+ self.menu.add_item(_("Users and Groups"), "/sys/users", 15)
+
+ @cherrypy.expose
+ def index(self):
+ return self.fill_template(title=_("System Configuration"), main=_("""
+ <p>In this section, you can control the %(product)s's
+ underlying system, as opposed to its various applications and
+ services. These options affect the %(product)s at its most
+ general level. This is where you add/remove users, install
+ applications, reboot, etc.</p>
+ """ % {'product':cfg.product_name}))
+
+ @cherrypy.expose
+ @require()
+ def packages(self):
+ side=_("""
+<h2>Help</h2>
+<p>On this page, you can add or remove %s plugins to your %s.</p>
+<p>Plugins are just Debian packages, so Debian's usual package management features should make this job fairly easy.</p>
+""" % (cfg.product_name, cfg.box_name))
+ return self.fill_template(title=_("Add/Remove Plugins"), main=_("""
+ <p>aptitude purge modules</p>
+ <p>aptitude install modules</p>
+ <p>The modules should depend on the appropriate Debian packages.</p>"""),
+ sidebar_right=side)
+
diff --git a/modules/installed/system/time_zones b/modules/installed/system/time_zones
new file mode 100644
index 0000000..58c8b7a
--- /dev/null
+++ b/modules/installed/system/time_zones
@@ -0,0 +1,572 @@
+[
+ "Africa/Abidjan",
+ "Africa/Accra",
+ "Africa/Addis_Ababa",
+ "Africa/Algiers",
+ "Africa/Asmara",
+ "Africa/Asmera",
+ "Africa/Bamako",
+ "Africa/Bangui",
+ "Africa/Banjul",
+ "Africa/Bissau",
+ "Africa/Blantyre",
+ "Africa/Brazzaville",
+ "Africa/Bujumbura",
+ "Africa/Cairo",
+ "Africa/Casablanca",
+ "Africa/Ceuta",
+ "Africa/Conakry",
+ "Africa/Dakar",
+ "Africa/Dar_es_Salaam",
+ "Africa/Djibouti",
+ "Africa/Douala",
+ "Africa/El_Aaiun",
+ "Africa/Freetown",
+ "Africa/Gaborone",
+ "Africa/Harare",
+ "Africa/Johannesburg",
+ "Africa/Kampala",
+ "Africa/Khartoum",
+ "Africa/Kigali",
+ "Africa/Kinshasa",
+ "Africa/Lagos",
+ "Africa/Libreville",
+ "Africa/Lome",
+ "Africa/Luanda",
+ "Africa/Lubumbashi",
+ "Africa/Lusaka",
+ "Africa/Malabo",
+ "Africa/Maputo",
+ "Africa/Maseru",
+ "Africa/Mbabane",
+ "Africa/Mogadishu",
+ "Africa/Monrovia",
+ "Africa/Nairobi",
+ "Africa/Ndjamena",
+ "Africa/Niamey",
+ "Africa/Nouakchott",
+ "Africa/Ouagadougou",
+ "Africa/Porto-Novo",
+ "Africa/Sao_Tome",
+ "Africa/Timbuktu",
+ "Africa/Tripoli",
+ "Africa/Tunis",
+ "Africa/Windhoek",
+ "America/Adak",
+ "America/Anchorage",
+ "America/Anguilla",
+ "America/Antigua",
+ "America/Araguaina",
+ "America/Argentina/Buenos_Aires",
+ "America/Argentina/Catamarca",
+ "America/Argentina/ComodRivadavia",
+ "America/Argentina/Cordoba",
+ "America/Argentina/Jujuy",
+ "America/Argentina/La_Rioja",
+ "America/Argentina/Mendoza",
+ "America/Argentina/Rio_Gallegos",
+ "America/Argentina/Salta",
+ "America/Argentina/San_Juan",
+ "America/Argentina/San_Luis",
+ "America/Argentina/Tucuman",
+ "America/Argentina/Ushuaia",
+ "America/Aruba",
+ "America/Asuncion",
+ "America/Atikokan",
+ "America/Atka",
+ "America/Bahia",
+ "America/Bahia_Banderas",
+ "America/Barbados",
+ "America/Belem",
+ "America/Belize",
+ "America/Blanc-Sablon",
+ "America/Boa_Vista",
+ "America/Bogota",
+ "America/Boise",
+ "America/Buenos_Aires",
+ "America/Cambridge_Bay",
+ "America/Campo_Grande",
+ "America/Cancun",
+ "America/Caracas",
+ "America/Catamarca",
+ "America/Cayenne",
+ "America/Cayman",
+ "America/Chicago",
+ "America/Chihuahua",
+ "America/Coral_Harbour",
+ "America/Cordoba",
+ "America/Costa_Rica",
+ "America/Cuiaba",
+ "America/Curacao",
+ "America/Danmarkshavn",
+ "America/Dawson",
+ "America/Dawson_Creek",
+ "America/Denver",
+ "America/Detroit",
+ "America/Dominica",
+ "America/Edmonton",
+ "America/Eirunepe",
+ "America/El_Salvador",
+ "America/Ensenada",
+ "America/Fort_Wayne",
+ "America/Fortaleza",
+ "America/Glace_Bay",
+ "America/Godthab",
+ "America/Goose_Bay",
+ "America/Grand_Turk",
+ "America/Grenada",
+ "America/Guadeloupe",
+ "America/Guatemala",
+ "America/Guayaquil",
+ "America/Guyana",
+ "America/Halifax",
+ "America/Havana",
+ "America/Hermosillo",
+ "America/Indiana/Indianapolis",
+ "America/Indiana/Knox",
+ "America/Indiana/Marengo",
+ "America/Indiana/Petersburg",
+ "America/Indiana/Tell_City",
+ "America/Indiana/Vevay",
+ "America/Indiana/Vincennes",
+ "America/Indiana/Winamac",
+ "America/Indianapolis",
+ "America/Inuvik",
+ "America/Iqaluit",
+ "America/Jamaica",
+ "America/Jujuy",
+ "America/Juneau",
+ "America/Kentucky/Louisville",
+ "America/Kentucky/Monticello",
+ "America/Knox_IN",
+ "America/La_Paz",
+ "America/Lima",
+ "America/Los_Angeles",
+ "America/Louisville",
+ "America/Maceio",
+ "America/Managua",
+ "America/Manaus",
+ "America/Marigot",
+ "America/Martinique",
+ "America/Matamoros",
+ "America/Mazatlan",
+ "America/Mendoza",
+ "America/Menominee",
+ "America/Merida",
+ "America/Mexico_City",
+ "America/Miquelon",
+ "America/Moncton",
+ "America/Monterrey",
+ "America/Montevideo",
+ "America/Montreal",
+ "America/Montserrat",
+ "America/Nassau",
+ "America/New_York",
+ "America/Nipigon",
+ "America/Nome",
+ "America/Noronha",
+ "America/North_Dakota/Center",
+ "America/North_Dakota/New_Salem",
+ "America/Ojinaga",
+ "America/Panama",
+ "America/Pangnirtung",
+ "America/Paramaribo",
+ "America/Phoenix",
+ "America/Port-au-Prince",
+ "America/Port_of_Spain",
+ "America/Porto_Acre",
+ "America/Porto_Velho",
+ "America/Puerto_Rico",
+ "America/Rainy_River",
+ "America/Rankin_Inlet",
+ "America/Recife",
+ "America/Regina",
+ "America/Resolute",
+ "America/Rio_Branco",
+ "America/Rosario",
+ "America/Santa_Isabel",
+ "America/Santarem",
+ "America/Santiago",
+ "America/Santo_Domingo",
+ "America/Sao_Paulo",
+ "America/Scoresbysund",
+ "America/Shiprock",
+ "America/St_Barthelemy",
+ "America/St_Johns",
+ "America/St_Kitts",
+ "America/St_Lucia",
+ "America/St_Thomas",
+ "America/St_Vincent",
+ "America/Swift_Current",
+ "America/Tegucigalpa",
+ "America/Thule",
+ "America/Thunder_Bay",
+ "America/Tijuana",
+ "America/Toronto",
+ "America/Tortola",
+ "America/Vancouver",
+ "America/Virgin",
+ "America/Whitehorse",
+ "America/Winnipeg",
+ "America/Yakutat",
+ "America/Yellowknife",
+ "Antarctica/Casey",
+ "Antarctica/Davis",
+ "Antarctica/DumontDUrville",
+ "Antarctica/Macquarie",
+ "Antarctica/Mawson",
+ "Antarctica/McMurdo",
+ "Antarctica/Palmer",
+ "Antarctica/Rothera",
+ "Antarctica/South_Pole",
+ "Antarctica/Syowa",
+ "Antarctica/Vostok",
+ "Arctic/Longyearbyen",
+ "Asia/Aden",
+ "Asia/Almaty",
+ "Asia/Amman",
+ "Asia/Anadyr",
+ "Asia/Aqtau",
+ "Asia/Aqtobe",
+ "Asia/Ashgabat",
+ "Asia/Ashkhabad",
+ "Asia/Baghdad",
+ "Asia/Bahrain",
+ "Asia/Baku",
+ "Asia/Bangkok",
+ "Asia/Beirut",
+ "Asia/Bishkek",
+ "Asia/Brunei",
+ "Asia/Calcutta",
+ "Asia/Choibalsan",
+ "Asia/Chongqing",
+ "Asia/Chungking",
+ "Asia/Colombo",
+ "Asia/Dacca",
+ "Asia/Damascus",
+ "Asia/Dhaka",
+ "Asia/Dili",
+ "Asia/Dubai",
+ "Asia/Dushanbe",
+ "Asia/Gaza",
+ "Asia/Harbin",
+ "Asia/Ho_Chi_Minh",
+ "Asia/Hong_Kong",
+ "Asia/Hovd",
+ "Asia/Irkutsk",
+ "Asia/Istanbul",
+ "Asia/Jakarta",
+ "Asia/Jayapura",
+ "Asia/Jerusalem",
+ "Asia/Kabul",
+ "Asia/Kamchatka",
+ "Asia/Karachi",
+ "Asia/Kashgar",
+ "Asia/Kathmandu",
+ "Asia/Katmandu",
+ "Asia/Kolkata",
+ "Asia/Krasnoyarsk",
+ "Asia/Kuala_Lumpur",
+ "Asia/Kuching",
+ "Asia/Kuwait",
+ "Asia/Macao",
+ "Asia/Macau",
+ "Asia/Magadan",
+ "Asia/Makassar",
+ "Asia/Manila",
+ "Asia/Muscat",
+ "Asia/Nicosia",
+ "Asia/Novokuznetsk",
+ "Asia/Novosibirsk",
+ "Asia/Omsk",
+ "Asia/Oral",
+ "Asia/Phnom_Penh",
+ "Asia/Pontianak",
+ "Asia/Pyongyang",
+ "Asia/Qatar",
+ "Asia/Qyzylorda",
+ "Asia/Rangoon",
+ "Asia/Riyadh",
+ "Asia/Riyadh87",
+ "Asia/Riyadh88",
+ "Asia/Riyadh89",
+ "Asia/Saigon",
+ "Asia/Sakhalin",
+ "Asia/Samarkand",
+ "Asia/Seoul",
+ "Asia/Shanghai",
+ "Asia/Singapore",
+ "Asia/Taipei",
+ "Asia/Tashkent",
+ "Asia/Tbilisi",
+ "Asia/Tehran",
+ "Asia/Tel_Aviv",
+ "Asia/Thimbu",
+ "Asia/Thimphu",
+ "Asia/Tokyo",
+ "Asia/Ujung_Pandang",
+ "Asia/Ulaanbaatar",
+ "Asia/Ulan_Bator",
+ "Asia/Urumqi",
+ "Asia/Vientiane",
+ "Asia/Vladivostok",
+ "Asia/Yakutsk",
+ "Asia/Yekaterinburg",
+ "Asia/Yerevan",
+ "Atlantic/Azores",
+ "Atlantic/Bermuda",
+ "Atlantic/Canary",
+ "Atlantic/Cape_Verde",
+ "Atlantic/Faeroe",
+ "Atlantic/Faroe",
+ "Atlantic/Jan_Mayen",
+ "Atlantic/Madeira",
+ "Atlantic/Reykjavik",
+ "Atlantic/South_Georgia",
+ "Atlantic/St_Helena",
+ "Atlantic/Stanley",
+ "Australia/ACT",
+ "Australia/Adelaide",
+ "Australia/Brisbane",
+ "Australia/Broken_Hill",
+ "Australia/Canberra",
+ "Australia/Currie",
+ "Australia/Darwin",
+ "Australia/Eucla",
+ "Australia/Hobart",
+ "Australia/LHI",
+ "Australia/Lindeman",
+ "Australia/Lord_Howe",
+ "Australia/Melbourne",
+ "Australia/NSW",
+ "Australia/North",
+ "Australia/Perth",
+ "Australia/Queensland",
+ "Australia/South",
+ "Australia/Sydney",
+ "Australia/Tasmania",
+ "Australia/Victoria",
+ "Australia/West",
+ "Australia/Yancowinna",
+ "Brazil/Acre",
+ "Brazil/DeNoronha",
+ "Brazil/East",
+ "Brazil/West",
+ "CET",
+ "CST6CDT",
+ "Canada/Atlantic",
+ "Canada/Central",
+ "Canada/East-Saskatchewan",
+ "Canada/Eastern",
+ "Canada/Mountain",
+ "Canada/Newfoundland",
+ "Canada/Pacific",
+ "Canada/Saskatchewan",
+ "Canada/Yukon",
+ "Chile/Continental",
+ "Chile/EasterIsland",
+ "Cuba",
+ "EET",
+ "EST",
+ "EST5EDT",
+ "Egypt",
+ "Eire",
+ "Etc/GMT",
+ "Etc/GMT+0",
+ "Etc/GMT+1",
+ "Etc/GMT+10",
+ "Etc/GMT+11",
+ "Etc/GMT+12",
+ "Etc/GMT+2",
+ "Etc/GMT+3",
+ "Etc/GMT+4",
+ "Etc/GMT+5",
+ "Etc/GMT+6",
+ "Etc/GMT+7",
+ "Etc/GMT+8",
+ "Etc/GMT+9",
+ "Etc/GMT-0",
+ "Etc/GMT-1",
+ "Etc/GMT-10",
+ "Etc/GMT-11",
+ "Etc/GMT-12",
+ "Etc/GMT-13",
+ "Etc/GMT-14",
+ "Etc/GMT-2",
+ "Etc/GMT-3",
+ "Etc/GMT-4",
+ "Etc/GMT-5",
+ "Etc/GMT-6",
+ "Etc/GMT-7",
+ "Etc/GMT-8",
+ "Etc/GMT-9",
+ "Etc/GMT0",
+ "Etc/Greenwich",
+ "Etc/UCT",
+ "Etc/UTC",
+ "Etc/Universal",
+ "Etc/Zulu",
+ "Europe/Amsterdam",
+ "Europe/Andorra",
+ "Europe/Athens",
+ "Europe/Belfast",
+ "Europe/Belgrade",
+ "Europe/Berlin",
+ "Europe/Bratislava",
+ "Europe/Brussels",
+ "Europe/Bucharest",
+ "Europe/Budapest",
+ "Europe/Chisinau",
+ "Europe/Copenhagen",
+ "Europe/Dublin",
+ "Europe/Gibraltar",
+ "Europe/Guernsey",
+ "Europe/Helsinki",
+ "Europe/Isle_of_Man",
+ "Europe/Istanbul",
+ "Europe/Jersey",
+ "Europe/Kaliningrad",
+ "Europe/Kiev",
+ "Europe/Lisbon",
+ "Europe/Ljubljana",
+ "Europe/London",
+ "Europe/Luxembourg",
+ "Europe/Madrid",
+ "Europe/Malta",
+ "Europe/Mariehamn",
+ "Europe/Minsk",
+ "Europe/Monaco",
+ "Europe/Moscow",
+ "Europe/Nicosia",
+ "Europe/Oslo",
+ "Europe/Paris",
+ "Europe/Podgorica",
+ "Europe/Prague",
+ "Europe/Riga",
+ "Europe/Rome",
+ "Europe/Samara",
+ "Europe/San_Marino",
+ "Europe/Sarajevo",
+ "Europe/Simferopol",
+ "Europe/Skopje",
+ "Europe/Sofia",
+ "Europe/Stockholm",
+ "Europe/Tallinn",
+ "Europe/Tirane",
+ "Europe/Tiraspol",
+ "Europe/Uzhgorod",
+ "Europe/Vaduz",
+ "Europe/Vatican",
+ "Europe/Vienna",
+ "Europe/Vilnius",
+ "Europe/Volgograd",
+ "Europe/Warsaw",
+ "Europe/Zagreb",
+ "Europe/Zaporozhye",
+ "Europe/Zurich",
+ "GB",
+ "GB-Eire",
+ "GMT",
+ "GMT+0",
+ "Greenwich",
+ "HST",
+ "Hongkong",
+ "Iceland",
+ "Indian/Antananarivo",
+ "Indian/Chagos",
+ "Indian/Christmas",
+ "Indian/Cocos",
+ "Indian/Comoro",
+ "Indian/Kerguelen",
+ "Indian/Mahe",
+ "Indian/Maldives",
+ "Indian/Mauritius",
+ "Indian/Mayotte",
+ "Indian/Reunion",
+ "Iran",
+ "Israel",
+ "Jamaica",
+ "Japan",
+ "Kwajalein",
+ "Libya",
+ "MET",
+ "MST",
+ "MST7MDT",
+ "Mexico/BajaNorte",
+ "Mexico/BajaSur",
+ "Mexico/General",
+ "Mideast/Riyadh87",
+ "Mideast/Riyadh88",
+ "Mideast/Riyadh89",
+ "NZ",
+ "NZ-CHAT",
+ "Navajo",
+ "PRC",
+ "PST8PDT",
+ "Pacific/Apia",
+ "Pacific/Auckland",
+ "Pacific/Chatham",
+ "Pacific/Chuuk",
+ "Pacific/Easter",
+ "Pacific/Efate",
+ "Pacific/Enderbury",
+ "Pacific/Fakaofo",
+ "Pacific/Fiji",
+ "Pacific/Funafuti",
+ "Pacific/Galapagos",
+ "Pacific/Gambier",
+ "Pacific/Guadalcanal",
+ "Pacific/Guam",
+ "Pacific/Honolulu",
+ "Pacific/Johnston",
+ "Pacific/Kiritimati",
+ "Pacific/Kosrae",
+ "Pacific/Kwajalein",
+ "Pacific/Majuro",
+ "Pacific/Marquesas",
+ "Pacific/Midway",
+ "Pacific/Nauru",
+ "Pacific/Niue",
+ "Pacific/Norfolk",
+ "Pacific/Noumea",
+ "Pacific/Pago_Pago",
+ "Pacific/Palau",
+ "Pacific/Pitcairn",
+ "Pacific/Pohnpei",
+ "Pacific/Ponape",
+ "Pacific/Port_Moresby",
+ "Pacific/Rarotonga",
+ "Pacific/Saipan",
+ "Pacific/Samoa",
+ "Pacific/Tahiti",
+ "Pacific/Tarawa",
+ "Pacific/Tongatapu",
+ "Pacific/Truk",
+ "Pacific/Wake",
+ "Pacific/Wallis",
+ "Pacific/Yap",
+ "Poland",
+ "Portugal",
+ "ROC",
+ "ROK",
+ "Singapore",
+ "Turkey",
+ "UCT",
+ "US/Alaska",
+ "US/Aleutian",
+ "US/Arizona",
+ "US/Central",
+ "US/East-Indiana",
+ "US/Eastern",
+ "US/Hawaii",
+ "US/Indiana-Starke",
+ "US/Michigan",
+ "US/Mountain",
+ "US/Pacific",
+ "US/Pacific-New",
+ "US/Samoa",
+ "UTC",
+ "Universal",
+ "W-SU",
+ "Zulu"
+] \ No newline at end of file
diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py
new file mode 100644
index 0000000..73fbb04
--- /dev/null
+++ b/modules/installed/system/users.py
@@ -0,0 +1,127 @@
+import os, cherrypy
+from gettext import gettext as _
+from auth import require
+from plugin_mount import PagePlugin, FormPlugin
+import cfg
+from forms import Form
+from util import *
+
+class users(PagePlugin):
+ def __init__(self, *args, **kwargs):
+ PagePlugin.__init__(self, *args, **kwargs)
+ self.register_page("sys.users")
+
+ @cherrypy.expose
+ @require()
+ def index(self):
+ parts = self.forms('/sys/config')
+ parts['title']=_("Manage Users and Groups")
+ return self.fill_template(**parts)
+
+class add(FormPlugin, PagePlugin):
+ url = ["/sys/users"]
+ order = 30
+
+ sidebar_left = ''
+ sidebar_right = _("""<h2>Add User</h2><p>Adding a user via this
+ administrative interface <b>might</b> create a system user.
+ For example, if you provide a user with ssh access, she will
+ need a system account. If you don't know what that means,
+ don't worry about it.</p>""")
+
+ def main(self, username='', name='', email='', message=None, *args, **kwargs):
+ form = Form(title="Add User",
+ action="/sys/users/add/index",
+ onsubmit="return md5ify('add_user_form', 'password')",
+ name="add_user_form",
+ message=message)
+ form.text = '<script type="text/javascript" src="/static/js/md5.js"></script>\n'+form.text
+ form.text_input(_("Username"), name="username", value=username)
+ form.text_input(_("Full name"), name="name", value=name)
+ form.text_input(_("Email"), name="email", value=email)
+ form.text_input(_("Password"), name="password")
+ form.text_input(name="md5_password", type="hidden")
+ form.submit(label=_("Create User"), name="create")
+ return form.render()
+
+ def process_form(self, username=None, name=None, email=None, md5_password=None, **kwargs):
+ msg = ''
+
+ if not username: msg = add_message(msg, _("Must specify a username!"))
+ if not md5_password: msg = add_message(msg, _("Must specify a password!"))
+
+ if cfg.users.get(username):
+ msg = add_message(msg, _("User already exists!"))
+ else:
+ try:
+ cfg.users.set(User(dict={'username':username, 'name':name, 'email':email, 'password':md5_password}))
+ except:
+ msg = add_message(msg, _("Error storing user!"))
+
+ if not msg:
+ msg = add_message(msg, "%s saved." % username)
+
+ main = self.make_form(username, name, email, message=msg)
+ return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
+
+class edit(FormPlugin, PagePlugin):
+ url = ["/sys/users"]
+ order = 35
+
+ sidebar_left = ''
+ sidebar_right = _("""<h2>Edit Users</h2><p>Click on a user's name to
+ go to a screen for editing that user's account.</p><h2>Delete
+ Users</h2><p>Check the box next to a users' names and then click
+ "Delete User" to remove users from %s and the %s
+ system.</p><p>Deleting users is permanent!</p>""" % (cfg.product_name, cfg.box_name))
+
+ def main(self, msg=''):
+ users = cfg.users.get_all()
+ add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg)
+ add_form.html('<span class="indent"><b>Delete</b><br /></span>')
+ for uname in sorted(users.keys()):
+ add_form.html('<span class="indent">&nbsp;&nbsp;%s&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' %
+ add_form.get_checkbox(name=uname) +
+ '<a href="/sys/users/edit?username=%s">%s (%s)</a><br /></span>' %
+ (uname, users[uname]['name'], uname))
+ add_form.submit(label=_("Delete User"), name="delete")
+ return add_form.render()
+
+ def process_form(self, **kwargs):
+ if 'delete' in kwargs:
+ msg = Message()
+ usernames = find_keys(kwargs, 'on')
+ cfg.log.info("%s asked to delete %s" % (cherrypy.session.get(cfg.session_key), usernames))
+ if usernames:
+ for username in usernames:
+ if cfg.users.exists(username):
+ try:
+ cfg.users.remove(username)
+ msg.add(_("Deleted user %s." % username))
+ except IOError, e:
+ if cfg.users.get('username', reload=True):
+ m = _("Error on deletion, user %s not fully deleted: %s" % (username, e))
+ cfg.log.error(m)
+ msg.add(m)
+ else:
+ m = _('Deletion failed on %s: %s' % (username, e))
+ cfg.log.error(m)
+ msg.add(m)
+ else:
+ cfg.log.warning(_("Can't delete %s. User does not exist." % username))
+ msg.add(_("User %s does not exist." % username))
+ else:
+ msg.add = _("Must specify at least one valid, existing user.")
+ main = self.make_form(msg=msg.text)
+ return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
+
+ sidebar_right = ''
+ u = cfg.users.get(kwargs['username'])
+ if not u:
+ main = _("<p>Could not find a user with username of %s!</p>" % kwargs['username'])
+ return self.fill_template(template="err", title=_("Unnown User"), main=main,
+ sidebar_left=self.sidebar_left, sidebar_right=sidebar_right)
+
+ main = _("""<h2>Edit User '%s'</h2>""" % u['username'])
+ sidebar_right = ''
+ return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right)
diff --git a/modules/installed/system/wan.py b/modules/installed/system/wan.py
new file mode 100644
index 0000000..52b0cf0
--- /dev/null
+++ b/modules/installed/system/wan.py
@@ -0,0 +1,73 @@
+import os
+import cherrypy
+import simplejson as json
+from gettext import gettext as _
+from filedict import FileDict
+from modules.auth import require
+from plugin_mount import PagePlugin, FormPlugin
+import cfg
+from forms import Form
+from model import User
+from util import *
+
+class wan(FormPlugin, PagePlugin):
+ url = ["/sys/config"]
+ order = 20
+
+ def help(self, *args, **kwargs):
+ if not cfg.users.expert():
+ return ''
+ return _(#"""<h4>Admin from WAN</h4>
+ """<p>If you check this box, this front
+ end will be reachable from the WAN. If your %(box)s
+ connects you to the internet, that means you'll be able to log
+ in to the front end from the internet. This might be
+ convenient, but it is also <b>dangerous</b>, since it can
+ enable attackers to gain access to your %(box)s from the
+ outside world. All they'll need is your username and
+ passphrase, which they might guess or they might simply try
+ every posible combination of letters and numbers until they
+ get in. If you enable the WAN administration option, you
+ <b>must</b> use long and complex passphrases.</p>
+
+ <p>For security reasons, neither WAN Administration nor WAN
+ SSH is available to the `admin` user account.</p>
+
+ <p>TODO: in expert mode, tell user they can ssh in to enable
+ admin from WAN, do their business, then disable it. It would
+ be good to enable the option and autodisable it when the ssh
+ connection dies.</p>
+ """ % {'product':cfg.product_name, 'box':cfg.box_name})
+
+ def main(self, message='', **kwargs):
+ store = filedict_con(cfg.store_file, 'sys')
+
+ defaults = {'wan_admin': "''",
+ 'wan_ssh': "''",
+ 'lan_ssh': "''",
+ }
+ for k,c in defaults.items():
+ if not k in kwargs:
+ try:
+ kwargs[k] = store[k]
+ except KeyError:
+ exec("if not '%(k)s' in kwargs: store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c})
+
+ form = Form(title=_("Accessing the %s" % cfg.box_name),
+ action="/sys/config/wan",
+ name="admin_wan_form",
+ message=message )
+ form.html(self.help())
+ if cfg.users.expert():
+ form.checkbox(_("Allow access to Plinth from WAN"), name="wan_admin", checked=kwargs['wan_admin'])
+ form.checkbox(_("Allow SSH access from LAN"), name="lan_ssh", checked=kwargs['lan_ssh'])
+ form.checkbox(_("Allow SSH access from WAN"), name="wan_ssh", checked=kwargs['wan_ssh'])
+ form.submit(_("Submit"))
+ return form.render()
+
+ def process_form(self, wan_admin='', wan_ssh='', lan_ssh='', *args, **kwargs):
+ store = filedict_con(cfg.store_file, 'sys')
+ for field in ['wan_admin', 'wan_ssh', 'lan_ssh']:
+ exec("store['%s'] = %s" % (field, field))
+ return "Settings updated."
+
diff --git a/modules/privacy.py b/modules/privacy.py
new file mode 120000
index 0000000..07f303e
--- /dev/null
+++ b/modules/privacy.py
@@ -0,0 +1 @@
+installed/privacy/privacy.py \ No newline at end of file
diff --git a/modules/router.py b/modules/router.py
new file mode 120000
index 0000000..7239c9f
--- /dev/null
+++ b/modules/router.py
@@ -0,0 +1 @@
+installed/router/router.py \ No newline at end of file
diff --git a/modules/services.py b/modules/services.py
new file mode 120000
index 0000000..806ae4f
--- /dev/null
+++ b/modules/services.py
@@ -0,0 +1 @@
+installed/services/services.py \ No newline at end of file
diff --git a/modules/sharing.py b/modules/sharing.py
new file mode 120000
index 0000000..e2323fb
--- /dev/null
+++ b/modules/sharing.py
@@ -0,0 +1 @@
+installed/sharing/sharing.py \ No newline at end of file
diff --git a/modules/system.py b/modules/system.py
new file mode 120000
index 0000000..6eddf3e
--- /dev/null
+++ b/modules/system.py
@@ -0,0 +1 @@
+installed/system/system.py \ No newline at end of file
diff --git a/modules/user_store.py b/modules/user_store.py
new file mode 120000
index 0000000..09e8f90
--- /dev/null
+++ b/modules/user_store.py
@@ -0,0 +1 @@
+installed/lib/user_store.py \ No newline at end of file
diff --git a/modules/users.py b/modules/users.py
new file mode 120000
index 0000000..4b7ce20
--- /dev/null
+++ b/modules/users.py
@@ -0,0 +1 @@
+installed/system/users.py \ No newline at end of file
diff --git a/modules/wan.py b/modules/wan.py
new file mode 120000
index 0000000..5a68efd
--- /dev/null
+++ b/modules/wan.py
@@ -0,0 +1 @@
+installed/system/wan.py \ No newline at end of file