diff options
author | James Vasile <james@hackervisions.org> | 2011-02-22 13:32:45 -0500 |
---|---|---|
committer | James Vasile <james@hackervisions.org> | 2011-02-22 13:32:45 -0500 |
commit | 35071d7212cec1fc23e8204bfd392a116a5313ed (patch) | |
tree | 1c75a525227769fc94f303b5c0233882d90ef2a8 /modules |
...
Diffstat (limited to 'modules')
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 += " " + 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"> %s ' % + 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 |