mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-03-25 09:21:10 +00:00
...
This commit is contained in:
commit
35071d7212
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
*.pyc
|
||||
*.tiny.css
|
||||
data/*.log
|
||||
data/cherrypy_sessions
|
||||
data/store.sqlite3
|
||||
doc/*.tex
|
||||
doc/*.pdf
|
||||
doc/*.html
|
||||
doc/*.aux
|
||||
doc/*.toc
|
||||
doc/*.log
|
||||
doc/*.out
|
||||
doc/COPYING.mdwn
|
||||
doc/INSTALL.mdwn
|
||||
doc/README.mdwn
|
||||
doc/TODO.mdwn
|
||||
doc/oneline.txt
|
||||
doc/plinth.1
|
||||
templates/*.py
|
||||
TODO
|
||||
34
INSTALL
Normal file
34
INSTALL
Normal file
@ -0,0 +1,34 @@
|
||||
# Quick Start
|
||||
|
||||
## Installing Plinth
|
||||
|
||||
Unzip and untar the source into a directory. Change to the directory
|
||||
containing the program. Run `./plinth.py` and point your web
|
||||
browser at `localhost:8000`. The default username is "admin" and the
|
||||
default password is "secret".
|
||||
|
||||
## Dependencies
|
||||
|
||||
* cherrypy - python web engine
|
||||
|
||||
* python - tested with version 2.6.6
|
||||
|
||||
* *GNU Make* is used to build the templates and such.
|
||||
|
||||
The documentation has some dependencies too.
|
||||
|
||||
* *Markdown* is used to format and style docs.
|
||||
|
||||
* *Pandoc* converts the markdown to different formats.
|
||||
|
||||
* *PDFLatex* generates pdf versions of the documentation.
|
||||
|
||||
* *GNU Make* processes /doc/Makefile.
|
||||
|
||||
## Building the Documentation
|
||||
|
||||
Documentation has been collected into a pdf that can be built using
|
||||
`make doc`. It also gets built into smaller files and other formats,
|
||||
including one suitable for install as a man page.
|
||||
|
||||
<a name="installing_systemwide" />
|
||||
36
Makefile
Normal file
36
Makefile
Normal file
@ -0,0 +1,36 @@
|
||||
MAKE=make
|
||||
|
||||
#TODO: add install target
|
||||
|
||||
CSS=$(wildcard *.css)
|
||||
CSS=$(subst .tiny,,$(shell find themes -type f -name '*.css'))
|
||||
COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS))
|
||||
|
||||
## Catch-all tagets
|
||||
default: template docs css
|
||||
all: default
|
||||
|
||||
%.tiny.css: %.css
|
||||
@cat $< | python -c 'import re,sys;print re.sub("\s*([{};,:])\s*", "\\1", re.sub("/\*.*?\*/", "", re.sub("\s+", " ", sys.stdin.read())))' > $@
|
||||
css: $(COMPRESSED_CSS)
|
||||
|
||||
template:
|
||||
@$(MAKE) -s -C templates
|
||||
templates: template
|
||||
|
||||
docs:
|
||||
@$(MAKE) -s -C doc
|
||||
doc: docs
|
||||
|
||||
html:
|
||||
@$(MAKE) -s -C doc html
|
||||
|
||||
clean:
|
||||
@find themes -name "*.tiny.css" -exec rm {} \;
|
||||
@find . -name "*~" -exec rm {} \;
|
||||
@find . -name ".#*" -exec rm {} \;
|
||||
@find . -name "#*" -exec rm {} \;
|
||||
@find . -name "*.pyc" -exec rm {} \;
|
||||
@find . -name "*.bak" -exec rm {} \;
|
||||
@$(MAKE) -s -C doc clean
|
||||
@$(MAKE) -s -C templates clean
|
||||
53
README
Normal file
53
README
Normal file
@ -0,0 +1,53 @@
|
||||
% PLINTH(1) Version 0.1 | Plinth User Manual
|
||||
%
|
||||
% February 2011
|
||||
|
||||
# Introduction
|
||||
|
||||
## Name
|
||||
|
||||
plinth - a web front end for administering every aspect of a Freedom Box.
|
||||
|
||||
## Synopsis
|
||||
|
||||
plinth.py
|
||||
|
||||
## Description
|
||||
|
||||
The Freedom Box is a net appliance conceived by Eben Moglen. It
|
||||
contains free software and is designed to allow you to interface with
|
||||
the rest of the net under conditions of protected privacy and data
|
||||
security.
|
||||
|
||||
The Plinth front end is a web interface to administer the functions of
|
||||
the Freedom Box. For example, the Freedom Box is a wireless router,
|
||||
and the front end is where you can adjust its settings.
|
||||
|
||||
## Overview
|
||||
|
||||
The front end is an extensible web platform for forms and menus. It
|
||||
allows authenticated users to fill out forms. The interface saves the
|
||||
form data and from them generates configuration files for the various
|
||||
services running on the box.
|
||||
|
||||
The interface is extremely pluggable. Drop modules into place to add
|
||||
new capabilities to Plinth and your Freedom Box. Replace existing modules to get
|
||||
newer, better shinier functions. The modules will automatically
|
||||
integrate into the existing menu system so you can control all of the
|
||||
box's parts from one central location.
|
||||
|
||||
The interface has a 'basic' and an 'expert' mode. In basic mode, much
|
||||
of Plinth's configuration and capability are hidden. Sane defaults
|
||||
are chosen whenever possible. In expert mode, you can get down into
|
||||
the details and configure things the average user never thinks about.
|
||||
For example, experts can turn off ntp or switch ntp servers. Basic
|
||||
users should never even know those options exist.
|
||||
|
||||
One caveat: expert mode is not as powerful as the commandline. We can
|
||||
only do so much when translating free-form configuration files into
|
||||
web forms. And if you manually change your configuration files, the
|
||||
interface will overwrite those changes at first opportunity. This
|
||||
interface is not a tool for super admins facing complex scenarios. It
|
||||
is for home users to do a wide variety of basic tasks.
|
||||
|
||||
|
||||
16
cfg.py
Normal file
16
cfg.py
Normal file
@ -0,0 +1,16 @@
|
||||
from menu import Menu
|
||||
import os
|
||||
|
||||
file_root = os.path.dirname(os.path.realpath(__file__))
|
||||
data_dir = os.path.join(file_root, "data")
|
||||
store_file = os.path.join(data_dir, "store.sqlite3")
|
||||
status_log_file = os.path.join(data_dir, "status.log")
|
||||
access_log_file = os.path.join(data_dir, "access.log")
|
||||
users_dir = os.path.join(data_dir, "users")
|
||||
|
||||
product_name = "Plinth"
|
||||
box_name = "Freedom Plug"
|
||||
|
||||
html_root = None
|
||||
main_menu = Menu()
|
||||
|
||||
19
cherrypy.config
Normal file
19
cherrypy.config
Normal file
@ -0,0 +1,19 @@
|
||||
[global]
|
||||
server.socket_host = '0.0.0.0'
|
||||
server.socket_port = 8000
|
||||
server.thread_pool = 10
|
||||
tools.staticdir.root = "/home/vasile/src/plinth"
|
||||
tools.sessions.on = True
|
||||
tools.auth.on = True
|
||||
tools.sessions.storage_type = "file"
|
||||
tools.sessions.storage_path = "/home/vasile/src/plinth/data/cherrypy_sessions"
|
||||
tools.sessions.timeout = 90
|
||||
|
||||
[/static]
|
||||
tools.staticdir.on = True
|
||||
tools.staticdir.dir = "static"
|
||||
|
||||
[/favicon.ico]
|
||||
tools.staticfile.on = True
|
||||
tools.staticfile.filename = "/home/vasile/src/plinth/static/theme/favicon.ico"
|
||||
|
||||
1
data/users/admin
Normal file
1
data/users/admin
Normal file
@ -0,0 +1 @@
|
||||
{"username": "admin", "name": "Admin", "expert": "on", "groups": ["expert"], "passphrase": "5ebe2294ecd0e0f08eab7690d2a6ee69", "password": "", "email": "root@localhost"}
|
||||
118
doc/Makefile
Normal file
118
doc/Makefile
Normal file
@ -0,0 +1,118 @@
|
||||
DOCDIR=../dist/doc
|
||||
|
||||
PANDOC=pandoc
|
||||
PDFLATEX=pdflatex
|
||||
|
||||
|
||||
# List text files in the order in which you want them to appear in the
|
||||
# complete manual:
|
||||
SOURCES=README.mdwn INSTALL.mdwn themes.mdwn hacking.mdwn TODO.mdwn modules.mdwn scripts.mdwn design.mdwn roadmap.mdwn faq.mdwn COPYING.mdwn colophon.mdwn
|
||||
TODO_SOURCES=$(patsubst TODO.mdwn,,$(SOURCES))
|
||||
MAN_SOURCES=$(patsubst COPYING.mdwn,copyright_notice00,$(SOURCES))
|
||||
|
||||
NEWLINE_SOURCES=$(patsubst %,% oneline.txt,$(SOURCES))
|
||||
NEWLINE_MAN_SOURCES=$(patsubst %,% oneline.txt,$(MAN_SOURCES))
|
||||
|
||||
HTML=plinth.html $(patsubst %.mdwn,%.html,$(SOURCES))
|
||||
HTML_PART=$(patsubst %.html,%.part.html,$(HTML))
|
||||
LATEX=plinth.tex $(patsubst %.mdwn,%.tex,$(SOURCES))
|
||||
PDF=plinth.pdf $(patsubst %.mdwn,%.pdf,$(SOURCES))
|
||||
MAN=plinth.1
|
||||
|
||||
OUTPUTS=$(HTML) $(LATEX) $(MAN) $(PDF) $(HTML_PART)
|
||||
DIST_OUTPUT=$(patsubst %,$(DOCDIR)/%,$(OUTPUTS))
|
||||
|
||||
# Yes, do it twice. TODO created during the process requires a second run
|
||||
default:
|
||||
make all
|
||||
make all
|
||||
all: oneline.txt $(OUTPUTS) Makefile
|
||||
|
||||
$(DOCDIR)/%: %
|
||||
cp $< $@
|
||||
dist: $(DIST_OUTPUT)
|
||||
###############################################################################
|
||||
oneline.txt: Makefile
|
||||
perl -e 'print "\n"' > oneline.txt
|
||||
|
||||
$(SOURCES):
|
||||
@rm -f $@
|
||||
@ln -s ../$(patsubst %.mdwn,%,$@) $@
|
||||
|
||||
../TODO : $(TODO_SOURCES) ../*.py ../modules/*.py ../Makefile Makefile ../templates/Makefile
|
||||
grep -ro --exclude=.git* --exclude=plinth.1 --exclude=*.tex --exclude=*.html \
|
||||
--exclude=README.mdwn --exclude=INSTALL.mdwn \
|
||||
--exclude=TODO.mdwn --exclude=COPYING.mdwn \
|
||||
"TODO\:.*" ../* 2>/dev/null | \
|
||||
sed -e "s/TODO\://g" | \
|
||||
sed -e "s/^..\//* /g" | \
|
||||
sed -e 's/"""$$//g' | \
|
||||
sed -e 's/<\/p>$$//g' \
|
||||
> ../TODO
|
||||
###############################################################################
|
||||
##
|
||||
## MAN PAGES
|
||||
##
|
||||
$(MAN): $(SOURCES) ../TODO
|
||||
@csplit -s -f copyright_notice COPYING.mdwn '/##/'
|
||||
cat $(NEWLINE_MAN_SOURCES) | perl -pe 'BEGIN { $$/=undef } $$_ =~ s/\n\n#\s.*/\n/gm; $$_ =~ s/\n\n#/\n\n/gm; $$_ =~ s/(\n\n#\s.*)/uc($$1)/gme' > .make_man
|
||||
$(PANDOC) -s -t man -o $@ .make_man
|
||||
@rm -f copyright_notice0? .make_man
|
||||
manpages: $(MAN)
|
||||
###############################################################################
|
||||
##
|
||||
## LaTeX
|
||||
##
|
||||
%.tex: %.mdwn
|
||||
$(PANDOC) -s --toc -f markdown --standalone -o $@ $<
|
||||
|
||||
hacking.tex: hacking.mdwn ../TODO
|
||||
$(PANDOC) -s --toc -f markdown -o $@ hacking.mdwn ../TODO
|
||||
|
||||
plinth.tex: $(NEWLINE_SOURCES) ../TODO
|
||||
$(PANDOC) -s --toc -f markdown -o $@ $(NEWLINE_SOURCES)
|
||||
|
||||
latex: $(LATEX)
|
||||
###############################################################################
|
||||
##
|
||||
## HTML
|
||||
##
|
||||
|
||||
|
||||
# This gets us the html sections complete with TOC, but without the
|
||||
# HTML and head section boilerplate. /help/view uses the parts.
|
||||
%.part.html: %.html
|
||||
csplit -s -f $@ $< '%.*<body>%'
|
||||
sed '1d' $@00 > $@01
|
||||
csplit -s -f $@ $@01 '/<\/body>/'
|
||||
mv $@00 $@
|
||||
rm $@01
|
||||
%.html: %.mdwn header.html footer.html style.css Makefile
|
||||
$(PANDOC) -s --toc -c style.css -f markdown -o $@ header.html $< footer.html
|
||||
|
||||
hacking.html: hacking.mdwn ../TODO style.css Makefile
|
||||
$(PANDOC) -s --toc -c style.css -o $@ -f markdown hacking.mdwn ../TODO
|
||||
|
||||
#plinth.html: $(NEWLINE_SOURCES) ../TODO style.css Makefile
|
||||
# $(PANDOC) -s --toc -c style.css -o $@ -f markdown $(NEWLINE_SOURCES)
|
||||
|
||||
plinth.html: $(NEWLINE_SOURCES) ../TODO style.css Makefile
|
||||
@csplit -s -f copyright_notice COPYING.mdwn '/##/'
|
||||
$(PANDOC) -s --toc -c style.css -o $@ -f markdown $(NEWLINE_MAN_SOURCES)
|
||||
@rm -f copyright_notice0? .make_man
|
||||
|
||||
html: $(HTML) $(HTML_PART)
|
||||
###############################################################################
|
||||
%.pdf: %.tex
|
||||
$(PDFLATEX) -interaction=batchmode $< >/dev/null
|
||||
$(PDFLATEX) -interaction=batchmode $< >/dev/null # yes, do it twice so the toc works
|
||||
|
||||
pdf: $(PDF)
|
||||
###############################################################################
|
||||
|
||||
clean-latex:
|
||||
rm -f *.log *.out *.aux *.toc
|
||||
|
||||
clean: clean-latex
|
||||
rm -f $(OUTPUTS) README.mdwn INSTALL.mdwn TODO.mdwn COPYING.mdwn \
|
||||
copyright_notice0? \#*\# ../TODO oneline.txt
|
||||
8
doc/colophon.mdwn
Normal file
8
doc/colophon.mdwn
Normal file
@ -0,0 +1,8 @@
|
||||
# Colophon
|
||||
|
||||
This manual was typed in emacs, formatted using markdown and converted
|
||||
to pdf, html, troff and latex using pandoc and pdflatex.
|
||||
|
||||
The complete source code to this manual is available in the 'doc'
|
||||
directory of the Freedom Box front end source distribution.
|
||||
|
||||
196
doc/design.mdwn
Normal file
196
doc/design.mdwn
Normal file
@ -0,0 +1,196 @@
|
||||
# Freedom Box Design and Architecture
|
||||
|
||||
This article describes and documents architectural considerations for
|
||||
my vision of Freedom Box. It is not specific to the user interface
|
||||
and instead contains thoughts about the Freedom Box as a whole.
|
||||
|
||||
The major immediate design problems that I see are authentication, ip
|
||||
addressing, and backup. I'm sure there are others.
|
||||
|
||||
If the Freedom Box front end pulls together basic pieces and
|
||||
configures them properly, we'll have the start of the freedom stack.
|
||||
Then we can build things like free social networking applications on
|
||||
top.
|
||||
|
||||
## Design Goals
|
||||
|
||||
The target hardware for this project is the
|
||||
[GuruPlug](http://www.globalscaletechnologies.com/t-guruplugdetails.aspx).
|
||||
It has low power consumption, two wired network interfaces, one
|
||||
wireless hostapd-capable interface (802.11b), and two usb slots for
|
||||
connecting external drives and peripherals. The hardware target might
|
||||
change if a better unit becomes available (for example, with 802.11n
|
||||
or USB 3 capability). The GuruPlug is reputed to have availability
|
||||
problems involving shipping delays, and targetting a unit that cannot
|
||||
satsify high demand might be an issue.
|
||||
|
||||
Freedom Boxes are not giant honking servers. They are small boxes
|
||||
that do perform a lot of functions. They are not heavily resourced.
|
||||
So keep things small and light.
|
||||
|
||||
By the same token, there is no need to scale up to thousands of users.
|
||||
A box should happily serve a person and her family, maybe an extended
|
||||
group of friends. Call it 100 people at the most.
|
||||
|
||||
The target user for this project is the home consumer of network
|
||||
appliances. It ranges from the most basic user (the kind of person
|
||||
who might have spent all of three minutes setting up her LinkSys
|
||||
WRT54G) to the home enthusiast who wants a local file server for media
|
||||
and backups, print server and dns service.
|
||||
|
||||
|
||||
## Authentication
|
||||
|
||||
Authentication in the context of the Freedom Box is a diffiult
|
||||
problem. You need to be able to trust your box, and it needs to be
|
||||
able to trust you. What's more, security must withstand forgotten
|
||||
passwords, server destruction, changing email addresses and any other
|
||||
possible disaster scenario.
|
||||
|
||||
In addition, your friends (and their boxes) need to trust your box
|
||||
when it acts on your behalf, even if it does so when you're not around
|
||||
to enter passphrases or otherwise confirm agency. But even as it
|
||||
needs to operate in your absence, it can't itself have authority to
|
||||
access all your data, because you might lack exclusive physical
|
||||
control of the box (e.g. if you have a roommate). If the box can act
|
||||
as you without limits, anybody who takes control of the box takes
|
||||
control of your online identity.
|
||||
|
||||
What's more, security should be high. Freedom Boxes might contain
|
||||
emails or other sensitive documents. The box mediates network
|
||||
traffic, which means rooting it would allow an attacker to spy on or
|
||||
even change traffic. Demonstrating control of an email address should
|
||||
not be enough to gain access to the box and its admin functions.
|
||||
|
||||
### Passphrases
|
||||
|
||||
Most users habitually think of passwords. We need to force them to
|
||||
think of passphrases. It starts by using 'passphrase' in the
|
||||
literature. Second, we need to encourage users to pick phrases, and
|
||||
prompts should urge them to do so. We also need minimum password
|
||||
lengths and maybe even a requirement of at least a few spaces.
|
||||
|
||||
Even better than passphrases are passfiles. We should keep a lookout
|
||||
for ways to use files instead of phrases when securing the Freedom
|
||||
Box.
|
||||
|
||||
### A Scheme for Secure Web Login
|
||||
|
||||
Passphrases should
|
||||
[never be stored in plain text](http://www.codinghorror.com/blog/2007/09/rainbow-hash-cracking.html).
|
||||
[MD5 is too fast for passphrases.](http://chargen.matasano.com/chargen/2007/9/7/enough-with-the-rainbow-tables-what-you-need-to-know-about-s.html)
|
||||
Therefore, my current plan for secure website login involves bcrypt:
|
||||
|
||||
The server sends the client a permanent salt, PS, a random session
|
||||
salt, SS and a number of rounds, R. It brcypts the password using PS
|
||||
and stores the result, B.
|
||||
|
||||
The browser
|
||||
[bcrypts the passphrase using javascript](https://code.google.com/p/javascript-bcrypt/)
|
||||
and PS. Then, it bcrypts that result with SS. It does R rounds of
|
||||
bcrypt with S. The browser then sends the result, C, back to the
|
||||
server.
|
||||
|
||||
The server retrieves bcrypts its stored, hashed passphrase and does R
|
||||
rounds of bcrypt on it with SS. If that matches C, the passphrase is
|
||||
a match.
|
||||
|
||||
The server must be able to keep track of sessions, as it needs to
|
||||
remember SS between giving it to the client and getting it back. The
|
||||
Server cannot rely on the client to remind it of SS. This would allow
|
||||
an attacker to dictate SS and leave the server vulnerable to replay
|
||||
attacks.
|
||||
|
||||
This scheme has the dual advantage of not storing any cleartext
|
||||
passphrases and also never sending any cleartext passphrases between
|
||||
client and server.
|
||||
|
||||
TODO: consult a security expert as to the strength of this scheme
|
||||
|
||||
### Other Schemes
|
||||
|
||||
#### Monkeysphere
|
||||
|
||||
[Monkeysphere](http://web.monkeysphere.info/) is a promising project.
|
||||
Monkeysphere's major win is that it avoids the clunkiness of SSL
|
||||
certificate authorities. That's huge, except it replaces the
|
||||
certificate authority structure with the PGP web of trust. Relying on
|
||||
the web of trust is a good idea
|
||||
([much better than relying on SSL certificate authorities](http://www.crypto.com/blog/spycerts/)),
|
||||
except that new users might not have joined the web. It also requires
|
||||
a bunch of GPG infrastructure that might be difficult to setup
|
||||
automatically and with little user intervention.
|
||||
|
||||
As of this writing, it does not appear to support Windows or Macintosh
|
||||
clients, making it unsuitable for this project.
|
||||
|
||||
#### Secure Remote Password
|
||||
|
||||
[SRP](http://srp.stanford.edu/) is an interesting technique. Patents
|
||||
are a concern. What does it buy us that my scheme, above does not?
|
||||
|
||||
There is a
|
||||
[python implementation](http://members.tripod.com/professor_tom/archives/srpsocket.html),
|
||||
although it is not clear that it has been through the crucible. Even
|
||||
if SRP is solid, this implementation might be flawed.
|
||||
|
||||
|
||||
|
||||
## Finding Each Other
|
||||
|
||||
### Dynamic DNS
|
||||
|
||||
Each box might need multiple dynamic DNS pointers. Every box is going
|
||||
to require its own set of dynamic names. For example, my Freedom Box
|
||||
might be known as both `fb.hackervisions.org` and
|
||||
`james.softwarefreedom.org`. My work colleagues might know me as
|
||||
`james@james.softwarefreedom.org` while my friends might reach me at
|
||||
`james@fb.hackervisions.org`. By default, when contacts come in via
|
||||
different names, my box might be smart enough to treat those contacts
|
||||
differently.
|
||||
|
||||
If I share this box with my partner, Emily, she might be
|
||||
`emily@jv187.fboxes.columbia.edu`, in which case my box will need
|
||||
another dynamic DNS pointer.
|
||||
|
||||
### Mesh Networking
|
||||
|
||||
Freedom Boxes should be meshed. Rather than run in infrastructure
|
||||
mode, they should route for each other and create strong local meshes
|
||||
that will carry data for anybody that passes through.
|
||||
|
||||
There are some problems, though.
|
||||
|
||||
* I'm not sure how well nodes in ad-hoc mesh mode interact with
|
||||
others in infrastructure mode.
|
||||
|
||||
* The Freedom Box only has one wireless radio, which will drastically
|
||||
hurt speed.
|
||||
|
||||
* Routing can be difficult when connecting mesh network nodes to the WAN.
|
||||
|
||||
## Backup
|
||||
|
||||
Everybody should automatically and redundantly store encrypted backups
|
||||
on their friend's freedom boxes. Recovery should not assume you kept
|
||||
your key or know a password. We can recover the decryption key by
|
||||
having a bunch of friends encrypt the key in various combinations. If
|
||||
enough of my friends get together, they can decrypt it for me.
|
||||
Optionally, a passphrase can serve to slow my friends down if they
|
||||
turn against me. Maybe it takes 5 friends acting in concert to
|
||||
recover my key, but only 3 to recover a version protected by the
|
||||
passphrase.
|
||||
|
||||
## Push vs Pull
|
||||
|
||||
For a social network, real time communication is key. Asynch
|
||||
communication is good, but sometimes people just want to bash comments
|
||||
back and forth. For that, you need push, which is just a web fetch
|
||||
meaning "pull my rss feed".
|
||||
|
||||
If, however, you have a lot of friends and they have a lot of friends,
|
||||
your extended network can be large, resulting in many "pull my rss
|
||||
feed" commands. We should only process push requests for friends to
|
||||
reduce load. You really don't need real time updates of the social
|
||||
networking activity of strangers. Friends of friends is as far out as
|
||||
things should ever go.
|
||||
37
doc/faq.mdwn
Normal file
37
doc/faq.mdwn
Normal file
@ -0,0 +1,37 @@
|
||||
# Plinth and Freedom Plug FAQ
|
||||
|
||||
## General Questions
|
||||
|
||||
### What is the Freedom Plug?
|
||||
|
||||
The Freedom Plug is .... insert links...
|
||||
|
||||
The Freedom Plug is based on the GNU/Debian operating system. It is
|
||||
not a Linux distribution. It is a network appliance that depends on a
|
||||
series of Debian packages that configure a plug computer to behave as
|
||||
a Freedom Plug.
|
||||
|
||||
### What is Plinth?
|
||||
|
||||
Plinth is the web-based GUI administration front end for the Freedom Plug.
|
||||
|
||||
### On what hardware is the Freedom Plug based?
|
||||
|
||||
The current targets are the [Guru Plug](http://guruplug) and the
|
||||
[Dream Plug](http://dreamplug).
|
||||
|
||||
## Accessing the Freedom Plug
|
||||
|
||||
### Why does ssh listen on port 2222 instead of 22?
|
||||
|
||||
If ssh listens on port 2222, bots and scripts will forever attempt to
|
||||
guess your username and password. Maybe your password isn't so
|
||||
strong. Maybe the bots get lucky. Either way, if you allow ssh
|
||||
access on port 22, you're taking a chance that can be avoided quite
|
||||
easily by moving your ssh activity to a slightly more obscure port.
|
||||
|
||||
Because ssh activity on these boxes will be limited to programs
|
||||
configured to work specifically with Freedom Plugs as well relatively
|
||||
few people generally using ssh, the coordination necessary to use a
|
||||
non-standard port is easily achieved.
|
||||
|
||||
0
doc/footer.html
Normal file
0
doc/footer.html
Normal file
97
doc/hacking.mdwn
Normal file
97
doc/hacking.mdwn
Normal file
@ -0,0 +1,97 @@
|
||||
# Hacking
|
||||
|
||||
This codebase could really use a testing framework.
|
||||
|
||||
If you are interested in helping out, writing tests is a great place
|
||||
to start-- you don't need to know much about the code or python to
|
||||
write useful tests.
|
||||
|
||||
In addition to a testing framework, the code is in need of some
|
||||
general cleanup. I've been inconsistent in capitalization conventions
|
||||
as well as in my use of underscores.
|
||||
|
||||
The plugin interface could use some attention as well. Right now,
|
||||
it's a a bit of a free-for-all until I see how the plugins actually
|
||||
code up. Channeling all that into a few plugin interface grooves
|
||||
would be a help.
|
||||
|
||||
If you're feeling more ambitious than that, the best way to improve
|
||||
Plinth is to add modules. More functionality, especially in the
|
||||
router section, could convert the Freedom Box from a good idea to a
|
||||
must-have appliance.
|
||||
|
||||
Beyond that, we need to train some expert eyes on the interaction
|
||||
between Freedom Boxes. Transparent, zero-config box-to-box backup is
|
||||
possible. We just need to build an auth and dns layer. There are
|
||||
lots of theories on how to do this well. The first theory reduced to
|
||||
practice wins!
|
||||
|
||||
There is a list of TODO items below. Some of them are discrete pieces
|
||||
that can be tackled without diving too deep into the code.
|
||||
|
||||
## Repository
|
||||
|
||||
Plinth is available from github at
|
||||
`git://github.com/jvasile/plinth.git`. The [project page on
|
||||
github](https://github.com/jvasile/plinth) is at
|
||||
`https://github.com/jvasile/plinth`.
|
||||
|
||||
## Bugs
|
||||
|
||||
There are lots of bugs. We don't have a spec or tests, so a bug is
|
||||
really just any unexpected behavior. I am not easily surprised, but
|
||||
there are still lots of bugs.
|
||||
|
||||
<a name="hacking_code_practices" />
|
||||
|
||||
## Coding Practices
|
||||
|
||||
I try to stick to [PEP 8](http://www.python.org/dev/peps/pep-0008/)
|
||||
recommendations. That's not to say I don't deviate, just that
|
||||
deviations are usually bugs that should be fixed.
|
||||
|
||||
### Internationalization
|
||||
|
||||
Every module should `from gettext import gettext as _` and wrap
|
||||
displayed strings with _(). We don't have the language stuff in place
|
||||
yet (we have no translation files), but we need to put the
|
||||
infrastructure in place for it from the start. Use it like this:
|
||||
|
||||
cfg.log.error(_("Couldn't import %s: %s") % (path, e))
|
||||
|
||||
### Variables and Data Stores
|
||||
|
||||
Plinth needs to keep information for short and long term
|
||||
future use, and it can't just store all of that on the stack.
|
||||
|
||||
Global config information can be put in the `cfg` module namespace.
|
||||
Keep it thread and session safe, though, or you'll get undefined
|
||||
behavior as soon as multiple simultaneous users enter the picture.
|
||||
|
||||
Cherrpy has support for session variables. Use those for short term
|
||||
user-specific data.
|
||||
|
||||
For long term storage, the Plinth needs a back end
|
||||
storage solution. Databases are a bit opaque and can be hard for
|
||||
third party software or shell users to manipulate. For now, I've
|
||||
decided that persistent data should be placed in dicts and stored in
|
||||
json format. We'll need a file locking solution too.
|
||||
|
||||
The `user_store.py` module implements the `UserStoreModule` interface
|
||||
specified in `plugin_mount.py`. Any new system that respects that
|
||||
interface can be used. The existing `user_store.py` holds entire user
|
||||
files in memory and caches user files as it goes. This has two
|
||||
downsides: first, if you have lots of users and store big things in
|
||||
the user dict, you'll run out of memory. Second, it's not thread
|
||||
safe. Maybe a database is a good idea after all.
|
||||
|
||||
We do not yet have a means of storing module data for long terms. My
|
||||
current thinking is that modules can store data in their own
|
||||
directories. That makes removal easy.
|
||||
|
||||
## Todo
|
||||
|
||||
Plinth has a number of open todo items. Please help!
|
||||
|
||||
* Implement the functions in the submenus of router.py
|
||||
* Unify our logging and cherrypy's.
|
||||
0
doc/header.html
Normal file
0
doc/header.html
Normal file
71
doc/modules.mdwn
Normal file
71
doc/modules.mdwn
Normal file
@ -0,0 +1,71 @@
|
||||
# Modules
|
||||
|
||||
Almost all of the front end's functionality is contained in small,
|
||||
separate modules that reside in the directory tree beneath
|
||||
`/modules/installed`. Some are installed by default, some are
|
||||
required for operation, and some are entirely optional.
|
||||
|
||||
## Installing and Loading Modules
|
||||
|
||||
Eventually, the goal is for module to be separate Debian package so
|
||||
installation is as simple as `aptitude install freedombox-foo`. As an
|
||||
intermediate step, we'll start scripting module installation. But
|
||||
until then, we can install them manually.
|
||||
|
||||
Modules are installed by copying them into the tree beneath the
|
||||
`/modules/installed` directory. They are activated by linking their
|
||||
.py files into the modules directory. (Freedom Box will not load
|
||||
modules unless they are symlinks.) If the module provides other
|
||||
resources, they can be linked from wherever in the working tree they
|
||||
need to be. So if a module provides a template, that template can be
|
||||
linked from `/templates`.
|
||||
|
||||
Modules can be organized into subdirectories beneath the installed
|
||||
directory. As an initial matter, I've made separate directories for
|
||||
each major tab. Modules that extend the functionality of the Freedom
|
||||
Box code base (as opposed to being user-visible interface modules
|
||||
under specific major tabs) go in `modules/installed/lib`. This
|
||||
convention can be adjusted or ignored as needed. In fact, modules can
|
||||
be installed anywhere in the file system as long as the symlinks are
|
||||
in `/modules`.
|
||||
|
||||
The names of the symlinks in the `modules` directory can be arbitrary,
|
||||
and if a name is already taken, (e.g. if `router/info.py` and
|
||||
`apps/info.p`y both exist), append a counter to the end (e.g. link
|
||||
`info.py` to `router/info.py` and `info1.py` to `apps/info.py`).
|
||||
|
||||
If a module cannot be imported for any reason (e.g. it's a dead
|
||||
symlink), freedombox.py will log an error but will otherwise just keep
|
||||
going.
|
||||
|
||||
TODO: automatically prune dead links to clear out old module installs.
|
||||
This is something the install scripts should do.
|
||||
|
||||
## Pluggable Module Structure
|
||||
|
||||
Plugin interfaces are contained in `plugin_mount.py`. Each interface
|
||||
is a metaclass. Classes that implement a given interface should
|
||||
inherit the metaclass and then provide the indicated properties. New
|
||||
metaclasses can be created to make new classes of plugins.
|
||||
|
||||
Any place that might be affected by arbitrary addition of modules
|
||||
should have its own metaclass.
|
||||
|
||||
### FromPlugin
|
||||
|
||||
Each form displayed in the interface should inherit from FormPlugin.
|
||||
This will allow the system to display multiple forms on the page in
|
||||
the correct order. Forms will likely also want to inherit from
|
||||
PagePlugin because after the user clicks submit, the plugin usually
|
||||
displays a responsive message or an error (along with the form to fill
|
||||
out again).
|
||||
|
||||
## Coding Practices for Modules
|
||||
|
||||
All the
|
||||
[coding practices for hacking on the front end](#hacking_code_practices)
|
||||
also apply to modules. In addition, I try to stick to the other
|
||||
practices listed in this section.
|
||||
|
||||
* Every module should `from gettext import gettext as _` and wrap
|
||||
displayed strings with _().
|
||||
64
doc/roadmap.mdwn
Normal file
64
doc/roadmap.mdwn
Normal file
@ -0,0 +1,64 @@
|
||||
# Plinth Roadmap
|
||||
|
||||
## 0.1 Basic Wireless Access Point
|
||||
|
||||
* <strike>Basic/Expert mode toggle</strike>
|
||||
* SSH access
|
||||
* Connect to WAN via DHCP and static IP
|
||||
* Offer DHCP on wired and wireless interfaces
|
||||
* WEP
|
||||
* WPA2
|
||||
* Bridge WAN and LAN
|
||||
|
||||
## 0.2 Basic Wireless Router
|
||||
|
||||
* NAT
|
||||
* DMZ
|
||||
* Port forwarding and triggering
|
||||
* dhcp server (on by default, expert can disable)
|
||||
* dns server
|
||||
* MAC address filtering
|
||||
* NTP client
|
||||
|
||||
## 0.3 Advanced Wireless Router
|
||||
|
||||
* dynamic dns - This is special. See the notes in the section on
|
||||
[dynamic DNS](#dynamic-dns) for details.
|
||||
* UPnP
|
||||
* Cron
|
||||
* Tx power management
|
||||
|
||||
## 0.4 File Server, More Routing Features
|
||||
|
||||
* boxbackupd
|
||||
* NFS
|
||||
* Samba
|
||||
* Auto mount usb volumes and create samba shares for them
|
||||
* TOR
|
||||
* Ad blocking web proxy
|
||||
* HTTPS everywhere (on by default, experts can disable)
|
||||
|
||||
## 0.5 Print Server and Backups
|
||||
|
||||
* Social backup to friend's boxes via ddns and boxbackup
|
||||
* printer discovery and sharing via samba and cups
|
||||
|
||||
## 1.0 Plinth Lives!
|
||||
|
||||
* Complete user documentation for every basic and expert menu item
|
||||
* Package management complete
|
||||
|
||||
## Unscheduled Features
|
||||
|
||||
There are a variety of other features to be implemented, but they are
|
||||
of lower priority than all the tasks scheduled above.
|
||||
|
||||
* NTP Server
|
||||
* Provide virtual networks and multiple SSIDs
|
||||
* Radius Server
|
||||
* QoS
|
||||
* file explorer
|
||||
* Mesh networking
|
||||
* Chaining boxes so the furthest upstream knows to route messages down
|
||||
the chain (e.g. if you and your roommate both have your own box, you
|
||||
just plug one into the other).
|
||||
1
doc/style.css
Symbolic link
1
doc/style.css
Symbolic link
@ -0,0 +1 @@
|
||||
../static/theme/style.css
|
||||
59
doc/themes.mdwn
Normal file
59
doc/themes.mdwn
Normal file
@ -0,0 +1,59 @@
|
||||
# Themes and Templates
|
||||
|
||||
The visual look and feel of the front end is described in theme files
|
||||
while <a href="http://cheetahtemplate.org">Cheetah templates</a>
|
||||
handle layout.
|
||||
|
||||
## Themes
|
||||
|
||||
Themes are stored in `/themes`. Themes consist entirely of static
|
||||
files (e.g. css, images and javascript) and templates. The default or
|
||||
active theme is linked from `/static/default` and `templates/default`.
|
||||
If your theme needs to change anything other than these items, you'll
|
||||
need a module (perhaps you'll need both).
|
||||
|
||||
There is not currently any support for dynamically choosing a theme at
|
||||
runtime, but it is theoretically possible.
|
||||
|
||||
## Templates
|
||||
|
||||
Plinth uses the Cheetah templating system. Templates are stored in
|
||||
`/templates`. Template requirements are not specified.
|
||||
|
||||
TODO: formalize the template spec so template writers know what they need to implement and where they can deviate.
|
||||
|
||||
In this section, I'll attempt to document some of the assumptions the
|
||||
program has about templates. The goal is that if you write a tempate
|
||||
that implements the spec, it should work just fine.
|
||||
|
||||
### The Template Stack
|
||||
The template is a hierarchical stack, where some templates extend on
|
||||
others. At the base of this stack is `base.tmpl`. It should specify
|
||||
variables as blocks (rather than using the $ notation). This allows
|
||||
other templates to easily override the base template.
|
||||
|
||||
Next up is the `page.tmpl`. It extends `base.tmpl` and simply
|
||||
replaces all the blocks with interpolable variables. Most of the
|
||||
time, `page.tmpl` is what you will actually use to create pages.
|
||||
|
||||
`err.tmpl` builds on top of `page.tmpl` by adding some decoration to
|
||||
the title field.
|
||||
|
||||
### Layout
|
||||
|
||||
Plinth expects a `main` block. This is where the
|
||||
meat of the content goes. It is the center pain in the default
|
||||
layout. There is a `title` block that the program will fill with text
|
||||
describing the current page. `sidebar_left` contains the submenu
|
||||
navigation menu, and `sidebar_right` is where we put all short text
|
||||
that helps the admin fill out forms. They don't have to be sidebars,
|
||||
and they don't have to go on the left and right.
|
||||
|
||||
It is possible to override the `footer`, but I haven't yet found a
|
||||
reason to do so.
|
||||
|
||||
## Cheetah
|
||||
|
||||
This section is for Cheetah hints that are especially useful in this context.
|
||||
|
||||
TODO: add Cheetah hints
|
||||
150
filedict.py
Normal file
150
filedict.py
Normal file
@ -0,0 +1,150 @@
|
||||
"""filedict.py
|
||||
a Persistent Dictionary in Python
|
||||
|
||||
Author: Erez Shinan
|
||||
Date : 31-May-2009
|
||||
"""
|
||||
|
||||
import simplejson as json ## jlv replaced pickle with json
|
||||
|
||||
import UserDict
|
||||
##import cPickle as pickle
|
||||
|
||||
import sqlite3
|
||||
|
||||
class DefaultArg:
|
||||
pass
|
||||
|
||||
class Solutions:
|
||||
Sqlite3 = 0
|
||||
|
||||
class FileDict(UserDict.DictMixin):
|
||||
"A dictionary that stores its data persistantly in a file"
|
||||
|
||||
def __init__(self, solution=Solutions.Sqlite3, **options):
|
||||
assert solution == Solutions.Sqlite3
|
||||
try:
|
||||
self.__conn = options.pop('connection')
|
||||
except KeyError:
|
||||
filename = options.pop('filename')
|
||||
self.__conn = sqlite3.connect(filename)
|
||||
|
||||
self.__tablename = options.pop('table', 'dict')
|
||||
|
||||
self._nocommit = False
|
||||
|
||||
assert not options, "Unrecognized options: %s" % options
|
||||
|
||||
self.__conn.execute('create table if not exists %s (id integer primary key, hash integer, key blob, value blob);'%self.__tablename)
|
||||
self.__conn.execute('create index if not exists %s_index ON %s(hash);' % (self.__tablename, self.__tablename))
|
||||
self.__conn.commit()
|
||||
|
||||
def _commit(self):
|
||||
if self._nocommit:
|
||||
return
|
||||
|
||||
self.__conn.commit()
|
||||
|
||||
def __pack(self, value):
|
||||
return sqlite3.Binary(json.dumps(value))
|
||||
##return sqlite3.Binary(pickle.dumps(value, -1))
|
||||
def __unpack(self, value):
|
||||
return json.loads(str(value))
|
||||
##return pickle.loads(str(value))
|
||||
|
||||
def __get_id(self, key):
|
||||
cursor = self.__conn.execute('select key,id from %s where hash=?;'%self.__tablename, (hash(key),))
|
||||
for k,id in cursor:
|
||||
if self.__unpack(k) == key:
|
||||
return id
|
||||
|
||||
raise KeyError(key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
cursor = self.__conn.execute('select key,value from %s where hash=?;'%self.__tablename, (hash(key),))
|
||||
for k,v in cursor:
|
||||
if self.__unpack(k) == key:
|
||||
return self.__unpack(v)
|
||||
|
||||
raise KeyError(key)
|
||||
|
||||
def __setitem(self, key, value):
|
||||
value_pickle = self.__pack(value)
|
||||
|
||||
try:
|
||||
id = self.__get_id(key)
|
||||
cursor = self.__conn.execute('update %s set value=? where id=?;'%self.__tablename, (value_pickle, id) )
|
||||
except KeyError:
|
||||
key_pickle = self.__pack(key)
|
||||
cursor = self.__conn.execute('insert into %s (hash, key, value) values (?, ?, ?);'
|
||||
%self.__tablename, (hash(key), key_pickle, value_pickle) )
|
||||
|
||||
assert cursor.rowcount == 1
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.__setitem(key, value)
|
||||
self._commit()
|
||||
|
||||
def __delitem__(self, key):
|
||||
id = self.__get_id(key)
|
||||
cursor = self.__conn.execute('delete from %s where id=?;'%self.__tablename, (id,))
|
||||
if cursor.rowcount <= 0:
|
||||
raise KeyError(key)
|
||||
|
||||
self._commit()
|
||||
|
||||
def update(self, d):
|
||||
for k,v in d.iteritems():
|
||||
self.__setitem(k, v)
|
||||
self._commit()
|
||||
|
||||
def __iter__(self):
|
||||
return (self.__unpack(x[0]) for x in self.__conn.execute('select key from %s;'%self.__tablename) )
|
||||
def keys(self):
|
||||
return iter(self)
|
||||
def values(self):
|
||||
return (self.__unpack(x[0]) for x in self.__conn.execute('select value from %s;'%self.__tablename) )
|
||||
def items(self):
|
||||
return (map(self.__unpack, x) for x in self.__conn.execute('select key,value from %s;'%self.__tablename) )
|
||||
def iterkeys(self):
|
||||
return self.keys()
|
||||
def itervalues(self):
|
||||
return self.values()
|
||||
def iteritems(self):
|
||||
return self.items()
|
||||
|
||||
def __contains__(self, key):
|
||||
try:
|
||||
self.__get_id(key)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
return self.__conn.execute('select count(*) from %s;' % self.__tablename).fetchone()[0]
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.__conn
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.__conn.commit()
|
||||
|
||||
@property
|
||||
def batch(self):
|
||||
return self._Batch(self)
|
||||
|
||||
class _Batch:
|
||||
def __init__(self, d):
|
||||
self.__d = d
|
||||
|
||||
def __enter__(self):
|
||||
self.__old_nocommit = self.__d._nocommit
|
||||
self.__d._nocommit = True
|
||||
return self.__d
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.__d._nocommit = self.__old_nocommit
|
||||
self.__d._commit()
|
||||
return True
|
||||
39
logger.py
Normal file
39
logger.py
Normal file
@ -0,0 +1,39 @@
|
||||
import cherrypy
|
||||
import inspect
|
||||
import cfg
|
||||
|
||||
cherrypy.log.error_file = cfg.status_log_file
|
||||
cherrypy.log.access_file = cfg.access_log_file
|
||||
cherrypy.log.screen = False
|
||||
|
||||
class Logger():
|
||||
"""By convention, log levels are DEBUG, INFO, WARNING, ERROR and CRITICAL."""
|
||||
def log(self, msg, level="DEBUG"):
|
||||
try:
|
||||
username = cherrypy.session.get(cfg.session_key)
|
||||
except AttributeError:
|
||||
username = ''
|
||||
cherrypy.log.error("%s %s %s" % (username, level, msg), inspect.stack()[2][3], 20)
|
||||
def __call__(self, *args):
|
||||
self.log(*args)
|
||||
|
||||
def debug(self, msg):
|
||||
self.log(msg)
|
||||
|
||||
def info(self, msg):
|
||||
self.log(msg, "INFO")
|
||||
|
||||
def warn(self, msg):
|
||||
self.log(msg, "WARNING")
|
||||
|
||||
def warning(self, msg):
|
||||
self.log(msg, "WARNING")
|
||||
|
||||
def error(self, msg):
|
||||
self.log(msg, "ERROR")
|
||||
|
||||
def err(self, msg):
|
||||
self.error(msg)
|
||||
|
||||
def critical(self, msg):
|
||||
self.log(msg, "CRITICAL")
|
||||
74
menu.py
Normal file
74
menu.py
Normal file
@ -0,0 +1,74 @@
|
||||
import simplejson as json
|
||||
from urlparse import urlparse
|
||||
import cherrypy
|
||||
import cfg
|
||||
|
||||
class Menu():
|
||||
"""One menu item."""
|
||||
def __init__(self, label="", url="#", order=50):
|
||||
"""label is the text that is displayed on the menu.
|
||||
|
||||
url is the url location that will be activated when the menu
|
||||
item is selected
|
||||
|
||||
order is the numerical rank of this item within the menu.
|
||||
Lower order items appear closest to the top/left of the menu.
|
||||
By convention, we use the spectrum between 0 and 100 to rank
|
||||
orders, but feel free to disregard that. If you need more
|
||||
granularity, don't bother renumbering things. Feel free to
|
||||
use fractional orders.
|
||||
"""
|
||||
|
||||
self.label = label
|
||||
self.order = order
|
||||
self.url = url
|
||||
self.items = []
|
||||
|
||||
def sort_items(self):
|
||||
"""Sort the items in self.items by order."""
|
||||
self.items = sorted(self.items, key=lambda x: x.order, reverse=False)
|
||||
def add_item(self, label, url, order=50):
|
||||
"""This method creates a menu item with the parameters, adds
|
||||
that menu item to this menu, and returns the item."""
|
||||
item = Menu(label=label, url=url, order=order)
|
||||
self.items.append(item)
|
||||
self.sort_items()
|
||||
return item
|
||||
|
||||
def active_p(self):
|
||||
"""Returns True if this menu item is active, otherwise False.
|
||||
|
||||
We can tell if a menu is active if the menu item points
|
||||
anywhere above url we are visiting in the url tree."""
|
||||
return urlparse(cherrypy.url()).path.startswith(self.url)
|
||||
|
||||
def active_item(self):
|
||||
"""Return item list (e.g. submenu) of active menu item."""
|
||||
path = urlparse(cherrypy.url()).path
|
||||
for item in self.items:
|
||||
if path.startswith(item.url):
|
||||
return item
|
||||
|
||||
def serializable(self, render_subs=False):
|
||||
"""Return the items in self.items as a serializable object we can pass to json.
|
||||
Note: this doesn't serialize all the data in this object."""
|
||||
|
||||
so = []
|
||||
for item in self.items:
|
||||
i = { 'label':item.label, 'url':item.url}
|
||||
if item.active_p():
|
||||
i['active']=True
|
||||
if item.items and render_subs:
|
||||
i['subs'] = item.serializable()
|
||||
so.append(i)
|
||||
return so
|
||||
def encode(self, name="", render_subs=False):
|
||||
"""return a string containing a javascript data structure
|
||||
assigned to the menu name
|
||||
|
||||
if render_subs is True, we render submenus too"""
|
||||
|
||||
return ('<SCRIPT LANGUAGE="JavaScript">\n <!--\n var %s_items=' % name
|
||||
#+ json.dumps(self.serializable(render_subs=render_subs), separators=(',',':')) # compact
|
||||
+ "\n"+ json.dumps(self.serializable(render_subs=render_subs), sort_keys=True, indent=4) # pretty print
|
||||
+ ';\n // -->\n </SCRIPT>')
|
||||
14
model.py
Normal file
14
model.py
Normal file
@ -0,0 +1,14 @@
|
||||
class User(dict):
|
||||
""" Every user must have keys for a username, name, password (this
|
||||
is a md5 hash of the password), groups, and an email address. They can be
|
||||
blank or None, but the keys must exist. """
|
||||
def __init__(self, dict=None):
|
||||
for key in ['username', 'name', 'password', 'email']:
|
||||
self[key] = ''
|
||||
for key in ['groups']:
|
||||
self[key] = []
|
||||
for key in dict:
|
||||
self[key] = dict[key]
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return None
|
||||
0
modules/__init__.py
Normal file
0
modules/__init__.py
Normal file
1
modules/apps.py
Symbolic link
1
modules/apps.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/apps/apps.py
|
||||
1
modules/auth.py
Symbolic link
1
modules/auth.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/lib/auth.py
|
||||
1
modules/auth_page.py
Symbolic link
1
modules/auth_page.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/lib/auth_page.py
|
||||
1
modules/config.py
Symbolic link
1
modules/config.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/system/config.py
|
||||
1
modules/expert_mode.py
Symbolic link
1
modules/expert_mode.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/system/expert_mode.py
|
||||
1
modules/file_explorer.py
Symbolic link
1
modules/file_explorer.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/sharing/file_explorer.py
|
||||
1
modules/forms.py
Symbolic link
1
modules/forms.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/lib/forms.py
|
||||
1
modules/help.py
Symbolic link
1
modules/help.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/help/help.py
|
||||
1
modules/info.py
Symbolic link
1
modules/info.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/router/info.py
|
||||
35
modules/installed/apps/apps.py
Normal file
35
modules/installed/apps/apps.py
Normal file
@ -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>
|
||||
""")
|
||||
91
modules/installed/help/help.py
Normal file
91
modules/installed/help/help.py
Normal file
@ -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)
|
||||
118
modules/installed/lib/auth.py
Normal file
118
modules/installed/lib/auth.py
Normal file
@ -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
|
||||
|
||||
|
||||
49
modules/installed/lib/auth_page.py
Normal file
49
modules/installed/lib/auth_page.py
Normal file
@ -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 "/")
|
||||
123
modules/installed/lib/forms.py
Normal file
123
modules/installed/lib/forms.py
Normal file
@ -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
|
||||
112
modules/installed/lib/user_store.py
Normal file
112
modules/installed/lib/user_store.py
Normal file
@ -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))
|
||||
41
modules/installed/privacy/privacy.py
Normal file
41
modules/installed/privacy/privacy.py
Normal file
@ -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)
|
||||
18
modules/installed/router/info.py
Normal file
18
modules/installed/router/info.py
Normal file
@ -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>
|
||||
""")
|
||||
160
modules/installed/router/router.py
Normal file
160
modules/installed/router/router.py
Normal file
@ -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()
|
||||
|
||||
25
modules/installed/services/services.py
Normal file
25
modules/installed/services/services.py
Normal file
@ -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)
|
||||
35
modules/installed/sharing/file_explorer.py
Normal file
35
modules/installed/sharing/file_explorer.py
Normal file
@ -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='')
|
||||
51
modules/installed/sharing/sharing.py
Normal file
51
modules/installed/sharing/sharing.py
Normal file
@ -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)
|
||||
|
||||
|
||||
135
modules/installed/system/config.py
Normal file
135
modules/installed/system/config.py
Normal file
@ -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."
|
||||
|
||||
70
modules/installed/system/expert_mode.py
Normal file
70
modules/installed/system/expert_mode.py
Normal file
@ -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
|
||||
50
modules/installed/system/system.py
Normal file
50
modules/installed/system/system.py
Normal file
@ -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)
|
||||
|
||||
572
modules/installed/system/time_zones
Normal file
572
modules/installed/system/time_zones
Normal file
@ -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"
|
||||
]
|
||||
127
modules/installed/system/users.py
Normal file
127
modules/installed/system/users.py
Normal file
@ -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)
|
||||
73
modules/installed/system/wan.py
Normal file
73
modules/installed/system/wan.py
Normal file
@ -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."
|
||||
|
||||
1
modules/privacy.py
Symbolic link
1
modules/privacy.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/privacy/privacy.py
|
||||
1
modules/router.py
Symbolic link
1
modules/router.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/router/router.py
|
||||
1
modules/services.py
Symbolic link
1
modules/services.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/services/services.py
|
||||
1
modules/sharing.py
Symbolic link
1
modules/sharing.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/sharing/sharing.py
|
||||
1
modules/system.py
Symbolic link
1
modules/system.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/system/system.py
|
||||
1
modules/user_store.py
Symbolic link
1
modules/user_store.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/lib/user_store.py
|
||||
1
modules/users.py
Symbolic link
1
modules/users.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/system/users.py
|
||||
1
modules/wan.py
Symbolic link
1
modules/wan.py
Symbolic link
@ -0,0 +1 @@
|
||||
installed/system/wan.py
|
||||
71
plinth.py
Executable file
71
plinth.py
Executable file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Start listener, just for testing
|
||||
import os, sys
|
||||
#import logging
|
||||
from gettext import gettext as _
|
||||
import cherrypy
|
||||
import cfg, plugin_mount
|
||||
from util import *
|
||||
from logger import Logger
|
||||
#from modules.auth import AuthController, require, member_of, name_is
|
||||
|
||||
def error_page(status, dynamic_msg, stock_msg):
|
||||
return page_template(template="err", title=status, main="<p>%s</p>%s" % (dynamic_msg, stock_msg))
|
||||
|
||||
def error_page_404(status, message, traceback, version):
|
||||
return error_page(status, message, """<p>If you believe this missing page should exist, please file a
|
||||
bug with either the Plinth project or the people responsible for
|
||||
the module you are trying to access.</p>
|
||||
|
||||
<p>Sorry for the mistake.</p>
|
||||
""")
|
||||
|
||||
def error_page_500(status, message, traceback, version):
|
||||
cfg.log.error("500 Internal Server Error. Trackback is above.")
|
||||
more="""<p>This is an internal error and not something you caused or can fix. You are welcome to report it.
|
||||
TODO: add link to bug tracker.</p>"""
|
||||
return error_page(status, message, "<p>%s</p><pre>%s</pre>" % (more, "\n".join(traceback.split("\n"))))
|
||||
|
||||
class Root(plugin_mount.PagePlugin):
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
if cherrypy.session.get(cfg.session_key, None):
|
||||
raise cherrypy.InternalRedirect('/router')
|
||||
else:
|
||||
raise cherrypy.InternalRedirect('/help/about')
|
||||
|
||||
def load_modules():
|
||||
"""Import all the symlinked .py files in the modules directory and
|
||||
all the .py files in directories linked in the modules directory
|
||||
(but don't dive deeper than that). Also, ignore the installed
|
||||
directory."""
|
||||
for name in os.listdir("modules"):
|
||||
if name.endswith(".py") and not name.startswith('.'):
|
||||
cfg.log.info("importing modules/%s" % name)
|
||||
try:
|
||||
exec("import modules.%s" % (name[:-3]))
|
||||
except ImportError, e:
|
||||
cfg.log.error(_("Couldn't import modules/%s: %s") % (name, e))
|
||||
else:
|
||||
cfg.log("skipping %s" % name)
|
||||
|
||||
def setup():
|
||||
os.chdir(cfg.file_root)
|
||||
cherrypy.config.update({'error_page.404': error_page_404})
|
||||
cherrypy.config.update({'error_page.500': error_page_500})
|
||||
cfg.log = Logger()
|
||||
load_modules()
|
||||
cfg.html_root = Root()
|
||||
cfg.page_plugins = plugin_mount.PagePlugin.get_plugins()
|
||||
cfg.log("Loaded %d page plugins" % len(cfg.page_plugins))
|
||||
cfg.users = plugin_mount.UserStoreModule.get_plugins()[0]
|
||||
cfg.forms = plugin_mount.FormPlugin.get_plugins()
|
||||
def main():
|
||||
setup()
|
||||
cherrypy.quickstart(cfg.html_root, script_name='/', config="cherrypy.config")
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
169
plugin_mount.py
Normal file
169
plugin_mount.py
Normal file
@ -0,0 +1,169 @@
|
||||
import cherrypy
|
||||
from modules.auth import require
|
||||
import cfg
|
||||
from util import *
|
||||
|
||||
class PluginMount(type):
|
||||
"""See http://martyalchin.com/2008/jan/10/simple-plugin-framework/ for documentation"""
|
||||
def __init__(cls, name, bases, attrs):
|
||||
if not hasattr(cls, 'plugins'):
|
||||
cls.plugins = []
|
||||
else:
|
||||
cls.plugins.append(cls)
|
||||
|
||||
def init_plugins(cls, *args, **kwargs):
|
||||
try:
|
||||
cls.plugins = sorted(cls.plugins, key=lambda x: x.order, reverse=False)
|
||||
except AttributeError:
|
||||
pass
|
||||
return [p(*args, **kwargs) for p in cls.plugins]
|
||||
def get_plugins(cls, *args, **kwargs):
|
||||
return cls.init_plugins(*args, **kwargs)
|
||||
|
||||
class MultiplePluginViolation:
|
||||
pass
|
||||
|
||||
class PluginMountSingular(PluginMount):
|
||||
def __init__(cls, name, bases, attrs):
|
||||
if not hasattr(cls, 'plugins'):
|
||||
cls.plugins = []
|
||||
else:
|
||||
if len(cls.plugins) > 0:
|
||||
raise MultiplePluginViolation
|
||||
cls.plugins.append(cls)
|
||||
|
||||
|
||||
def get_parts(obj, parts=None, *args, **kwargs):
|
||||
if parts == None:
|
||||
parts={}
|
||||
|
||||
fields = ['sidebar_left', 'sidebar_right', 'main', 'js', 'onload', 'nav', 'css', 'title']
|
||||
for v in fields:
|
||||
if not v in parts:
|
||||
parts[v] = ''
|
||||
exec("""
|
||||
try:
|
||||
if str(type(obj.%(v)s))=="<type 'instancemethod'>":
|
||||
parts[v] += obj.%(v)s(*args, **kwargs)
|
||||
else:
|
||||
parts[v] += obj.%(v)s
|
||||
except AttributeError:
|
||||
pass""" % {'v':v})
|
||||
|
||||
return parts
|
||||
|
||||
class PagePlugin:
|
||||
"""
|
||||
Mount point for page plugins. Page plugins provide display pages
|
||||
in the interface (one menu item, for example).
|
||||
|
||||
order - How early should this plugin be loaded? Lower order is earlier.
|
||||
"""
|
||||
|
||||
order = 50
|
||||
|
||||
__metaclass__ = PluginMount
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""If cfg.html_root is none, then this is the html_root."""
|
||||
if not cfg.html_root:
|
||||
cfg.html_root = self
|
||||
|
||||
def register_page(self, url):
|
||||
cfg.log.info("Registering page: %s" % url)
|
||||
exec "cfg.html_root.%s = self" % (url)
|
||||
def fill_template(self, *args, **kwargs):
|
||||
return page_template(*args, **kwargs)
|
||||
|
||||
def forms(self, url, *args, **kwargs):
|
||||
for form in cfg.forms:
|
||||
if url in form.url:
|
||||
cfg.log('Pulling together form for url %s (which matches %s)' % (url, form.url))
|
||||
|
||||
parts = get_parts(form, None, *args, **kwargs)
|
||||
|
||||
return parts
|
||||
return {'sidebar_left':left, 'sidebar_right':right, 'main':main}
|
||||
|
||||
class FormPlugin():
|
||||
"""
|
||||
Mount point for plugins that provide forms at specific URLs.
|
||||
|
||||
Form plugin classes should also inherit from PagePlugin so they
|
||||
can implement the page that handles the results of the form. The
|
||||
name of the class will be appended to each url where the form is
|
||||
displayed to get the urls of the results pages.
|
||||
|
||||
Plugins implementing this reference should provide the following attributes:
|
||||
|
||||
url - list of URL path strings of pages on which to display this
|
||||
form, not including the url path that *only* displays this form
|
||||
(that's handled by the index method)
|
||||
|
||||
order - How high up on the page should this content be displayed?
|
||||
Lower order is higher up.
|
||||
|
||||
The following attributes are optional (though without at least one
|
||||
of them, this plugin won't do much):
|
||||
|
||||
sidebar_right - text to be displayed in the right column (can be attribute or method) (optional)
|
||||
|
||||
sidebar_left - text to be displayed in the left column (can be attribute or method) (optional)
|
||||
|
||||
main - text to be displayed in the center column (i.e. the form) (can be attribute or method)
|
||||
|
||||
js - attribute containing a string that will be placed in the
|
||||
template head, just below the javascript loads. Use it to load
|
||||
more javascript files (optional)
|
||||
|
||||
Although this plugin is intended for forms, it could just display
|
||||
some html and skip the form.
|
||||
"""
|
||||
__metaclass__ = PluginMount
|
||||
|
||||
order = 50
|
||||
url = []
|
||||
js = ''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
for u in self.url:
|
||||
exec "cfg.html_root.%s = self" % "%s.%s" % ('.'.join(u.split("/")[1:]), self.__class__.__name__)
|
||||
cfg.log("Registered page: %s.%s" % ('.'.join(u.split("/")[1:]), self.__class__.__name__))
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
return "<p>Override this method and replace it with a form.</p>"
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def index(self, **kwargs):
|
||||
"""If the user has tried to fill in the form, process it, otherwise, just display a default form."""
|
||||
if kwargs:
|
||||
kwargs['message'] = self.process_form(**kwargs)
|
||||
parts = get_parts(self)
|
||||
return self.fill_template(**parts)
|
||||
|
||||
def process_form(self, **kwargs):
|
||||
"""Process the form. Return any message as a result of processing."""
|
||||
pass
|
||||
|
||||
def fill_template(self, *args, **kwargs):
|
||||
if not 'js' in kwargs:
|
||||
try:
|
||||
kwargs['js'] = self.js
|
||||
except AttributeError:
|
||||
pass
|
||||
cfg.log("%%%%%%%%%%% %s" % kwargs)
|
||||
return page_template(*args, **kwargs)
|
||||
|
||||
class UserStoreModule:
|
||||
"""
|
||||
Mount Point for plugins that will manage the user backend storage,
|
||||
where we keep a hash for each user.
|
||||
|
||||
Plugins implementing this reference should provide the following
|
||||
methods, as described in the doc strings of the default
|
||||
user_store.py: get, get_all, set, exists, remove, attr, expert.
|
||||
See source code for doc strings.
|
||||
|
||||
"""
|
||||
__metaclass__ = PluginMountSingular
|
||||
|
||||
1
static/doc
Symbolic link
1
static/doc
Symbolic link
@ -0,0 +1 @@
|
||||
../doc
|
||||
409
static/js/md5.js
Normal file
409
static/js/md5.js
Normal file
@ -0,0 +1,409 @@
|
||||
/*
|
||||
* md5.js 1.0b 27/06/96
|
||||
*
|
||||
* Javascript implementation of the RSA Data Security, Inc. MD5
|
||||
* Message-Digest Algorithm.
|
||||
*
|
||||
* Copyright (c) 1996 Henri Torgemane. All Rights Reserved.
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software
|
||||
* and its documentation for any purposes and without
|
||||
* fee is hereby granted provided that this copyright notice
|
||||
* appears in all copies.
|
||||
*
|
||||
* Of course, this soft is provided "as is" without express or implied
|
||||
* warranty of any kind.
|
||||
*
|
||||
*
|
||||
* Modified with german comments and some information about collisions.
|
||||
* (Ralf Mieke, ralf@miekenet.de, http://mieke.home.pages.de)
|
||||
*/
|
||||
|
||||
function array(n) {
|
||||
for(i=0;i<n;i++) this[i]=0;
|
||||
this.length=n;
|
||||
}
|
||||
|
||||
/* Einige grundlegenden Funktionen müssen wegen
|
||||
* Javascript Fehlern umgeschrieben werden.
|
||||
* Man versuche z.B. 0xffffffff >> 4 zu berechnen..
|
||||
* Die nun verwendeten Funktionen sind zwar langsamer als die Originale,
|
||||
* aber sie funktionieren.
|
||||
*/
|
||||
|
||||
function integer(n) { return n%(0xffffffff+1); }
|
||||
|
||||
function shr(a,b) {
|
||||
a=integer(a);
|
||||
b=integer(b);
|
||||
if (a-0x80000000>=0) {
|
||||
a=a%0x80000000;
|
||||
a>>=b;
|
||||
a+=0x40000000>>(b-1);
|
||||
} else
|
||||
a>>=b;
|
||||
return a;
|
||||
}
|
||||
|
||||
function shl1(a) {
|
||||
a=a%0x80000000;
|
||||
if (a&0x40000000==0x40000000)
|
||||
{
|
||||
a-=0x40000000;
|
||||
a*=2;
|
||||
a+=0x80000000;
|
||||
} else
|
||||
a*=2;
|
||||
return a;
|
||||
}
|
||||
|
||||
function shl(a,b) {
|
||||
a=integer(a);
|
||||
b=integer(b);
|
||||
for (var i=0;i<b;i++) a=shl1(a);
|
||||
return a;
|
||||
}
|
||||
|
||||
function and(a,b) {
|
||||
a=integer(a);
|
||||
b=integer(b);
|
||||
var t1=(a-0x80000000);
|
||||
var t2=(b-0x80000000);
|
||||
if (t1>=0)
|
||||
if (t2>=0)
|
||||
return ((t1&t2)+0x80000000);
|
||||
else
|
||||
return (t1&b);
|
||||
else
|
||||
if (t2>=0)
|
||||
return (a&t2);
|
||||
else
|
||||
return (a&b);
|
||||
}
|
||||
|
||||
function or(a,b) {
|
||||
a=integer(a);
|
||||
b=integer(b);
|
||||
var t1=(a-0x80000000);
|
||||
var t2=(b-0x80000000);
|
||||
if (t1>=0)
|
||||
if (t2>=0)
|
||||
return ((t1|t2)+0x80000000);
|
||||
else
|
||||
return ((t1|b)+0x80000000);
|
||||
else
|
||||
if (t2>=0)
|
||||
return ((a|t2)+0x80000000);
|
||||
else
|
||||
return (a|b);
|
||||
}
|
||||
|
||||
function xor(a,b) {
|
||||
a=integer(a);
|
||||
b=integer(b);
|
||||
var t1=(a-0x80000000);
|
||||
var t2=(b-0x80000000);
|
||||
if (t1>=0)
|
||||
if (t2>=0)
|
||||
return (t1^t2);
|
||||
else
|
||||
return ((t1^b)+0x80000000);
|
||||
else
|
||||
if (t2>=0)
|
||||
return ((a^t2)+0x80000000);
|
||||
else
|
||||
return (a^b);
|
||||
}
|
||||
|
||||
function not(a) {
|
||||
a=integer(a);
|
||||
return (0xffffffff-a);
|
||||
}
|
||||
|
||||
/* Beginn des Algorithmus */
|
||||
|
||||
var state = new array(4);
|
||||
var count = new array(2);
|
||||
count[0] = 0;
|
||||
count[1] = 0;
|
||||
var buffer = new array(64);
|
||||
var transformBuffer = new array(16);
|
||||
var digestBits = new array(16);
|
||||
|
||||
var S11 = 7;
|
||||
var S12 = 12;
|
||||
var S13 = 17;
|
||||
var S14 = 22;
|
||||
var S21 = 5;
|
||||
var S22 = 9;
|
||||
var S23 = 14;
|
||||
var S24 = 20;
|
||||
var S31 = 4;
|
||||
var S32 = 11;
|
||||
var S33 = 16;
|
||||
var S34 = 23;
|
||||
var S41 = 6;
|
||||
var S42 = 10;
|
||||
var S43 = 15;
|
||||
var S44 = 21;
|
||||
|
||||
function F(x,y,z) {
|
||||
return or(and(x,y),and(not(x),z));
|
||||
}
|
||||
|
||||
function G(x,y,z) {
|
||||
return or(and(x,z),and(y,not(z)));
|
||||
}
|
||||
|
||||
function H(x,y,z) {
|
||||
return xor(xor(x,y),z);
|
||||
}
|
||||
|
||||
function I(x,y,z) {
|
||||
return xor(y ,or(x , not(z)));
|
||||
}
|
||||
|
||||
function rotateLeft(a,n) {
|
||||
return or(shl(a, n),(shr(a,(32 - n))));
|
||||
}
|
||||
|
||||
function FF(a,b,c,d,x,s,ac) {
|
||||
a = a+F(b, c, d) + x + ac;
|
||||
a = rotateLeft(a, s);
|
||||
a = a+b;
|
||||
return a;
|
||||
}
|
||||
|
||||
function GG(a,b,c,d,x,s,ac) {
|
||||
a = a+G(b, c, d) +x + ac;
|
||||
a = rotateLeft(a, s);
|
||||
a = a+b;
|
||||
return a;
|
||||
}
|
||||
|
||||
function HH(a,b,c,d,x,s,ac) {
|
||||
a = a+H(b, c, d) + x + ac;
|
||||
a = rotateLeft(a, s);
|
||||
a = a+b;
|
||||
return a;
|
||||
}
|
||||
|
||||
function II(a,b,c,d,x,s,ac) {
|
||||
a = a+I(b, c, d) + x + ac;
|
||||
a = rotateLeft(a, s);
|
||||
a = a+b;
|
||||
return a;
|
||||
}
|
||||
|
||||
function transform(buf,offset) {
|
||||
var a=0, b=0, c=0, d=0;
|
||||
var x = transformBuffer;
|
||||
|
||||
a = state[0];
|
||||
b = state[1];
|
||||
c = state[2];
|
||||
d = state[3];
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
x[i] = and(buf[i*4+offset],0xff);
|
||||
for (j = 1; j < 4; j++) {
|
||||
x[i]+=shl(and(buf[i*4+j+offset] ,0xff), j * 8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Runde 1 */
|
||||
a = FF ( a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */
|
||||
d = FF ( d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */
|
||||
c = FF ( c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */
|
||||
b = FF ( b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */
|
||||
a = FF ( a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */
|
||||
d = FF ( d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */
|
||||
c = FF ( c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */
|
||||
b = FF ( b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */
|
||||
a = FF ( a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */
|
||||
d = FF ( d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */
|
||||
c = FF ( c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
|
||||
b = FF ( b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
|
||||
a = FF ( a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
|
||||
d = FF ( d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
|
||||
c = FF ( c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
|
||||
b = FF ( b, c, d, a, x[15], S14, 0x49b40821); /* 16 */
|
||||
|
||||
/* Runde 2 */
|
||||
a = GG ( a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */
|
||||
d = GG ( d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */
|
||||
c = GG ( c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
|
||||
b = GG ( b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */
|
||||
a = GG ( a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */
|
||||
d = GG ( d, a, b, c, x[10], S22, 0x2441453); /* 22 */
|
||||
c = GG ( c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
|
||||
b = GG ( b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */
|
||||
a = GG ( a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */
|
||||
d = GG ( d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
|
||||
c = GG ( c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */
|
||||
b = GG ( b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */
|
||||
a = GG ( a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
|
||||
d = GG ( d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */
|
||||
c = GG ( c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */
|
||||
b = GG ( b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */
|
||||
|
||||
/* Runde 3 */
|
||||
a = HH ( a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */
|
||||
d = HH ( d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */
|
||||
c = HH ( c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
|
||||
b = HH ( b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
|
||||
a = HH ( a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */
|
||||
d = HH ( d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */
|
||||
c = HH ( c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */
|
||||
b = HH ( b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
|
||||
a = HH ( a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
|
||||
d = HH ( d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */
|
||||
c = HH ( c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */
|
||||
b = HH ( b, c, d, a, x[ 6], S34, 0x4881d05); /* 44 */
|
||||
a = HH ( a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */
|
||||
d = HH ( d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
|
||||
c = HH ( c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
|
||||
b = HH ( b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */
|
||||
|
||||
/* Runde 4 */
|
||||
a = II ( a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */
|
||||
d = II ( d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */
|
||||
c = II ( c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
|
||||
b = II ( b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */
|
||||
a = II ( a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
|
||||
d = II ( d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */
|
||||
c = II ( c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
|
||||
b = II ( b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */
|
||||
a = II ( a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */
|
||||
d = II ( d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
|
||||
c = II ( c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */
|
||||
b = II ( b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
|
||||
a = II ( a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */
|
||||
d = II ( d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
|
||||
c = II ( c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */
|
||||
b = II ( b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */
|
||||
|
||||
state[0] +=a;
|
||||
state[1] +=b;
|
||||
state[2] +=c;
|
||||
state[3] +=d;
|
||||
|
||||
}
|
||||
/* Mit der Initialisierung von Dobbertin:
|
||||
state[0] = 0x12ac2375;
|
||||
state[1] = 0x3b341042;
|
||||
state[2] = 0x5f62b97c;
|
||||
state[3] = 0x4ba763ed;
|
||||
gibt es eine Kollision:
|
||||
|
||||
begin 644 Message1
|
||||
M7MH=JO6_>MG!X?!51$)W,CXV!A"=(!AR71,<X`Y-IIT9^Z&8L$2N'Y*Y:R.;
|
||||
39GIK9>TF$W()/MEHR%C4:G1R:Q"=
|
||||
`
|
||||
end
|
||||
|
||||
begin 644 Message2
|
||||
M7MH=JO6_>MG!X?!51$)W,CXV!A"=(!AR71,<X`Y-IIT9^Z&8L$2N'Y*Y:R.;
|
||||
39GIK9>TF$W()/MEHREC4:G1R:Q"=
|
||||
`
|
||||
end
|
||||
*/
|
||||
function init() {
|
||||
count[0]=count[1] = 0;
|
||||
state[0] = 0x67452301;
|
||||
state[1] = 0xefcdab89;
|
||||
state[2] = 0x98badcfe;
|
||||
state[3] = 0x10325476;
|
||||
for (i = 0; i < digestBits.length; i++)
|
||||
digestBits[i] = 0;
|
||||
}
|
||||
|
||||
function update(b) {
|
||||
var index,i;
|
||||
|
||||
index = and(shr(count[0],3) , 0x3f);
|
||||
if (count[0]<0xffffffff-7)
|
||||
count[0] += 8;
|
||||
else {
|
||||
count[1]++;
|
||||
count[0]-=0xffffffff+1;
|
||||
count[0]+=8;
|
||||
}
|
||||
buffer[index] = and(b,0xff);
|
||||
if (index >= 63) {
|
||||
transform(buffer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function finish() {
|
||||
var bits = new array(8);
|
||||
var padding;
|
||||
var i=0, index=0, padLen=0;
|
||||
|
||||
for (i = 0; i < 4; i++) {
|
||||
bits[i] = and(shr(count[0],(i * 8)), 0xff);
|
||||
}
|
||||
for (i = 0; i < 4; i++) {
|
||||
bits[i+4]=and(shr(count[1],(i * 8)), 0xff);
|
||||
}
|
||||
index = and(shr(count[0], 3) ,0x3f);
|
||||
padLen = (index < 56) ? (56 - index) : (120 - index);
|
||||
padding = new array(64);
|
||||
padding[0] = 0x80;
|
||||
for (i=0;i<padLen;i++)
|
||||
update(padding[i]);
|
||||
for (i=0;i<8;i++)
|
||||
update(bits[i]);
|
||||
|
||||
for (i = 0; i < 4; i++) {
|
||||
for (j = 0; j < 4; j++) {
|
||||
digestBits[i*4+j] = and(shr(state[i], (j * 8)) , 0xff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Ende des MD5 Algorithmus */
|
||||
|
||||
function hexa(n) {
|
||||
var hexa_h = "0123456789abcdef";
|
||||
var hexa_c="";
|
||||
var hexa_m=n;
|
||||
for (hexa_i=0;hexa_i<8;hexa_i++) {
|
||||
hexa_c=hexa_h.charAt(Math.abs(hexa_m)%16)+hexa_c;
|
||||
hexa_m=Math.floor(hexa_m/16);
|
||||
}
|
||||
return hexa_c;
|
||||
}
|
||||
|
||||
var ascii="01234567890123456789012345678901" +
|
||||
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
|
||||
"[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
|
||||
|
||||
function MD5(nachricht)
|
||||
{
|
||||
var l,s,k,ka,kb,kc,kd;
|
||||
|
||||
init();
|
||||
for (k=0;k<nachricht.length;k++) {
|
||||
l=nachricht.charAt(k);
|
||||
update(ascii.lastIndexOf(l));
|
||||
}
|
||||
finish();
|
||||
ka=kb=kc=kd=0;
|
||||
for (i=0;i<4;i++) ka+=shl(digestBits[15-i], (i*8));
|
||||
for (i=4;i<8;i++) kb+=shl(digestBits[15-i], ((i-4)*8));
|
||||
for (i=8;i<12;i++) kc+=shl(digestBits[15-i], ((i-8)*8));
|
||||
for (i=12;i<16;i++) kd+=shl(digestBits[15-i], ((i-12)*8));
|
||||
s=hexa(kd)+hexa(kc)+hexa(kb)+hexa(ka);
|
||||
return s;
|
||||
}
|
||||
|
||||
function md5ify(form, field) {
|
||||
/* replace the field in the form with the md5 version of the contents of that field
|
||||
|
||||
The form must already have a field (preferably hidden) called md5_[field name]*/
|
||||
|
||||
var x=document.forms[form][field].value;
|
||||
document.forms[form]["md5_"+field].value = MD5(x);
|
||||
document.forms[form][field].value = '';
|
||||
}
|
||||
1
static/theme
Symbolic link
1
static/theme
Symbolic link
@ -0,0 +1 @@
|
||||
../themes/default
|
||||
15
templates/Makefile
Normal file
15
templates/Makefile
Normal file
@ -0,0 +1,15 @@
|
||||
CHEETAH=cheetah
|
||||
TEMPLATES=$(wildcard *.tmpl)
|
||||
TEMPLATE_PYFILES := $(patsubst %.tmpl,%.py,$(TEMPLATES))
|
||||
|
||||
## Catch-all tagets
|
||||
default: $(TEMPLATE_PYFILES)
|
||||
|
||||
%.py: %.tmpl
|
||||
$(CHEETAH) c $<
|
||||
templates: $(TEMPLATE_PYFILES)
|
||||
template: templates
|
||||
|
||||
clean:
|
||||
rm -rf .\#* \#* *.pyz *.bak
|
||||
@rm -f $(TEMPLATE_PYFILES)
|
||||
0
templates/__init__.py
Normal file
0
templates/__init__.py
Normal file
BIN
templates/__init__.pyc
Normal file
BIN
templates/__init__.pyc
Normal file
Binary file not shown.
112
templates/base.tmpl
Normal file
112
templates/base.tmpl
Normal file
@ -0,0 +1,112 @@
|
||||
#def default($text, $default)
|
||||
#if $text
|
||||
$text
|
||||
#else
|
||||
$default
|
||||
#end if
|
||||
#end def
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB">
|
||||
<head>
|
||||
<title>$default($title, "Plinth Front End to the Freedom Box")
|
||||
</title>
|
||||
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
|
||||
<meta name="description" content="Plint admin frontend for Freedom Box" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/theme/style.tiny.css" media="screen" />
|
||||
$css
|
||||
<script type="text/javascript" src="/static/theme/menu.js"></script>
|
||||
<script type="text/javascript" src="/static/theme/plinth.js"></script>
|
||||
$js
|
||||
$main_menu_js
|
||||
$sub_menu_js
|
||||
<script LANGUAGE="JavaScript">
|
||||
<!--
|
||||
$onload
|
||||
// -->
|
||||
</script>
|
||||
</head>
|
||||
<body onLoad="javascript:onload_handler();">
|
||||
<div id="header">
|
||||
<div id="headerleft">
|
||||
<a href="/"><img src="/static/theme/freedombox.png" /></a>
|
||||
</div>
|
||||
<div id="headerright">
|
||||
<br /><br /><br />
|
||||
<h1><a href="/">Freedom Box</a></h1>
|
||||
<h2><a href="/">Plinth Administration Control Panel</a></h2>
|
||||
<SCRIPT LANGUAGE="JavaScript">
|
||||
<!--
|
||||
main_menu(main_menu_items);
|
||||
// -->
|
||||
</SCRIPT>
|
||||
</div>
|
||||
#if $username
|
||||
<p id="layoutdims">Logged in as $username. <a href="/auth/logout" title="Log out">Logout.</a></p>
|
||||
#else
|
||||
<p id="layoutdims">Not logged in. <a href="/auth/login" title="Log in">Log in.</a></p>
|
||||
#end if
|
||||
|
||||
</div>
|
||||
<div class="colmask threecol">
|
||||
<div class="colmid">
|
||||
<div class="colleft">
|
||||
<div class="col1">
|
||||
<!-- Column 1 start -->
|
||||
<h2>
|
||||
#block title_block
|
||||
$title
|
||||
#end block title_block
|
||||
</h2>
|
||||
#block main_block
|
||||
$main
|
||||
#end block main_block
|
||||
<!-- Column 1 end -->
|
||||
</div>
|
||||
<div class="col2">
|
||||
<!-- Column 2 start -->
|
||||
#block nav_block
|
||||
$nav
|
||||
#end block nav_block
|
||||
#block sidebar_left_block
|
||||
$sidebar_left
|
||||
#end block sidebar_left_block
|
||||
<!-- Column 2 end -->
|
||||
</div>
|
||||
<div class="col3">
|
||||
<!-- Column 3 start -->
|
||||
<div id="ads">
|
||||
<!--<a href="http://matthewjamestaylor.com">
|
||||
<img src="mjt-125x125.gif" width="125" border="0" height="125" alt="Art and Design by Matthew James Taylor" />
|
||||
</a>
|
||||
-->
|
||||
</div>
|
||||
#block sidebar_right_block
|
||||
$sidebar_right
|
||||
#end block sidebar_right_block
|
||||
<!-- Column 3 end -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
#block footer_block
|
||||
<p>
|
||||
Plinth is copyright 2011 <a href="http://hackervisions.org">James Vasile</a>. It is
|
||||
free software offered to you under the terms of
|
||||
the <a href="http://www.gnu.org/licenses/agpl.html">GNU Affero General Public
|
||||
License</a>, Version 3 or later.
|
||||
</p>
|
||||
<!--<p>Current page: $current_url</p>-->
|
||||
<p>
|
||||
This page uses
|
||||
the <a href="http://matthewjamestaylor.com/blog/perfect-3-column.htm">Perfect
|
||||
'Holy Grail' 3 Column Liquid Layout</a>
|
||||
by <a href="http://matthewjamestaylor.com">Matthew James Taylor</a>.
|
||||
</p>
|
||||
#end block footer_block
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
23
templates/err.tmpl
Normal file
23
templates/err.tmpl
Normal file
@ -0,0 +1,23 @@
|
||||
#extends templates.two_col
|
||||
|
||||
#def title_block
|
||||
<span class="err">Error: $title</span>
|
||||
<br />
|
||||
<br />
|
||||
#end def
|
||||
|
||||
#def sidebar_left_block
|
||||
$sidebar_left
|
||||
#end def
|
||||
|
||||
#def main_block
|
||||
$main
|
||||
#end def
|
||||
|
||||
#def sidebar_right_block
|
||||
$sidebar_right
|
||||
#end def
|
||||
|
||||
#def nav_block
|
||||
$nav
|
||||
#end def
|
||||
5
templates/two_col.tmpl
Normal file
5
templates/two_col.tmpl
Normal file
@ -0,0 +1,5 @@
|
||||
#extends templates.base
|
||||
|
||||
#def css
|
||||
<link rel="stylesheet" type="text/css" href="/static/theme/2col.tiny.css" media="screen" />
|
||||
#end def
|
||||
30
themes/default/2col.css
Normal file
30
themes/default/2col.css
Normal file
@ -0,0 +1,30 @@
|
||||
/* 2 Column settings */
|
||||
|
||||
.colright {
|
||||
float:left;
|
||||
width:0%; /* width of page */
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.threecol .colmid {
|
||||
right:5%; /* width of the right column */
|
||||
}
|
||||
.threecol .colleft {
|
||||
right:70%; /* width of the middle column */
|
||||
}
|
||||
.threecol .col1 {
|
||||
width:66%; /* width of center column content (column width minus padding on either side) */
|
||||
left:102%; /* 100% plus left padding of center column */
|
||||
}
|
||||
.threecol .col2 {
|
||||
width:21%; /* Width of left column content (column width minus padding on either side) */
|
||||
left:11%; /* width of (right column) plus (center column left and right padding) plus (left column left padding) */
|
||||
}
|
||||
.threecol .col3 {
|
||||
width:21%; /* Width of right column content (column width minus padding on either side) */
|
||||
|
||||
/* Note this used to be 85%, but I subtracted 1% for padding to pull stuff closer to the margin */
|
||||
left:84%; /* Please make note of the brackets here:
|
||||
(100% - left column width) plus (center column left and right padding) plus (left column left and right padding) plus (right column left padding) */
|
||||
}
|
||||
|
||||
BIN
themes/default/favicon.ico
Normal file
BIN
themes/default/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
themes/default/freedombox.png
Normal file
BIN
themes/default/freedombox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
themes/default/images/freedombox.png
Normal file
BIN
themes/default/images/freedombox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
54
themes/default/menu.js
Normal file
54
themes/default/menu.js
Normal file
@ -0,0 +1,54 @@
|
||||
function main_menu(items) {
|
||||
output = "<ul>"
|
||||
for (item in items) {
|
||||
i = items[item];
|
||||
|
||||
// Handle active page
|
||||
if (i["active"]) {
|
||||
active = 'class = "active"';
|
||||
} else {
|
||||
active = '';
|
||||
}
|
||||
|
||||
// Line break labels
|
||||
label = i["label"];
|
||||
if (label.search(" ") != -1) {
|
||||
label = label.replace(" ", "<br />");
|
||||
} else {
|
||||
label = " <br />" + label;
|
||||
}
|
||||
|
||||
output = output +'<li><a href="' + i["url"] + '" ' + active + '>' + label + "</a></li>";
|
||||
}
|
||||
output = output + "</ul>";
|
||||
document.write(output);
|
||||
}
|
||||
|
||||
function render_items(items) {
|
||||
output = "<ul>";
|
||||
for (item in items) {
|
||||
i = items[item];
|
||||
|
||||
// Handle active page
|
||||
if (i["active"]) {
|
||||
active = 'class = "active"';
|
||||
} else {
|
||||
active = '';
|
||||
}
|
||||
|
||||
output = output +'<li><a href="' + i["url"] + '" ' + active + '>' + i['label'] + "</a></li>";
|
||||
if (i['subs']) {
|
||||
output += render_items(i['subs']);
|
||||
}
|
||||
}
|
||||
output = output + "</ul>";
|
||||
return output
|
||||
}
|
||||
|
||||
function side_menu(items) {
|
||||
if (items.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
output = "<h2>Menu</h2>" + render_items(items);
|
||||
document.write(output);
|
||||
}
|
||||
16
themes/default/plinth.js
Normal file
16
themes/default/plinth.js
Normal file
@ -0,0 +1,16 @@
|
||||
function toggle_visibility(id) {
|
||||
var d = document.getElementById(id);
|
||||
if(d.style.display == 'block')
|
||||
d.style.display = 'none';
|
||||
else
|
||||
d.style.display = 'block';
|
||||
}
|
||||
|
||||
function show(id) {
|
||||
var d = document.getElementById(id);
|
||||
d.style.display = 'block';
|
||||
}
|
||||
|
||||
function hide(id) {
|
||||
document.getElementById(id).style.display = 'none';
|
||||
}
|
||||
328
themes/default/style.css
Normal file
328
themes/default/style.css
Normal file
@ -0,0 +1,328 @@
|
||||
body {
|
||||
margin:0;
|
||||
padding:0;
|
||||
border:0; /* This removes the border around the viewport in old versions of IE */
|
||||
width:100%;
|
||||
background:lightgoldenrodyellow;
|
||||
min-width:600px; /* Minimum width of layout - remove line if not required */
|
||||
/* The min-width property does not work in old versions of Internet Explorer */
|
||||
font-size:90%;
|
||||
font-family:Arial, Helvetica, sans-serif;
|
||||
}
|
||||
a {
|
||||
color:#369;
|
||||
text-decoration:none;
|
||||
}
|
||||
a:hover {
|
||||
color:#fff;
|
||||
background:GoldenRod;
|
||||
text-decoration:none;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
margin:.8em 0 .2em 0;
|
||||
padding:0;
|
||||
}
|
||||
p {
|
||||
margin:.4em 0 .8em 0;
|
||||
padding:0;
|
||||
}
|
||||
img {
|
||||
margin:10px 0 5px;
|
||||
}
|
||||
#ads img {
|
||||
display:block;
|
||||
padding-top:10px;
|
||||
}
|
||||
|
||||
/* Header styles */
|
||||
#header {
|
||||
clear:both;
|
||||
float:left;
|
||||
width:100%;
|
||||
border-bottom:1px solid #000;
|
||||
}
|
||||
|
||||
#headerleft {
|
||||
float: left;
|
||||
padding-left: 1em;
|
||||
}
|
||||
#headerleft a { color:lightgoldenrodyellow; text-decoration:none; outline: none;}
|
||||
|
||||
#headerright {
|
||||
float:left;
|
||||
}
|
||||
#headerright p,
|
||||
#headerright h1,
|
||||
#headerright h2 {
|
||||
padding:.4em 15px 0 15px;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
#headerright h1 a,
|
||||
#headerright h2 a {color:black; text-decoration:none; outline: none;}
|
||||
#headerright ul {
|
||||
clear:left;
|
||||
float:left;
|
||||
width:100%;
|
||||
list-style:none;
|
||||
margin:10px 0 0 0;
|
||||
padding:0;
|
||||
}
|
||||
#headerright ul li {
|
||||
display:inline;
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
#headerright ul li a {
|
||||
display:block;
|
||||
float:left;
|
||||
margin:0 0 0 1px;
|
||||
padding:3px 10px;
|
||||
text-align:center;
|
||||
background:Khaki;
|
||||
color:#000;
|
||||
text-decoration:none;
|
||||
position:relative;
|
||||
left:15px;
|
||||
line-height:1.3em;
|
||||
}
|
||||
#headerright ul li a:hover {
|
||||
background:GoldenRod;
|
||||
color:#fff;
|
||||
}
|
||||
#headerright ul li a.active,
|
||||
#headerright ul li a.active:hover {
|
||||
color:#fff;
|
||||
background:#000;
|
||||
font-weight:bold;
|
||||
text-decoration:none;
|
||||
}
|
||||
#headerright ul li a span {
|
||||
display:block;
|
||||
}
|
||||
|
||||
/* 'widths' sub menu */
|
||||
#layoutdims {
|
||||
clear:both;
|
||||
background:#eee;
|
||||
background:GoldenRod;
|
||||
border-top:4px solid #000;
|
||||
margin:0;
|
||||
padding:6px 15px !important;
|
||||
text-align:right;
|
||||
}
|
||||
/* column container */
|
||||
.colmask {
|
||||
position:relative; /* This fixes the IE7 overflow hidden bug */
|
||||
clear:both;
|
||||
float:left;
|
||||
width:100%; /* width of whole page */
|
||||
overflow:hidden; /* This chops off any overhanging divs */
|
||||
}
|
||||
/* common column settings */
|
||||
.colright,
|
||||
.colmid,
|
||||
.colleft {
|
||||
float:left;
|
||||
width:100%; /* width of page */
|
||||
position:relative;
|
||||
}
|
||||
.col1,
|
||||
.col2,
|
||||
.col3 {
|
||||
float:left;
|
||||
position:relative;
|
||||
padding:0 0em 1em 0; /* no left and right padding on columns, we just make them narrower instead
|
||||
only padding top and bottom is included here, make it whatever value you need */
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.col2 h2,
|
||||
.col3 h2 {color:black;
|
||||
font-weight:bold;
|
||||
font-size:100%;
|
||||
text-align:center;
|
||||
background:Goldenrod;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.4em;}
|
||||
.col2 h3,
|
||||
.col3 h3 {color:black;
|
||||
font-weight:bold;
|
||||
font-size:100%;
|
||||
text-align:left;
|
||||
padding-top: 0em;
|
||||
margin-top: 0em;
|
||||
margin-bottom: 0em;}
|
||||
.col3 p {
|
||||
margin: 0.4em 0.5em .8em 0.5em;
|
||||
padding:0;
|
||||
}
|
||||
.col2 li {padding-bottom: 0.2em;}
|
||||
.col2 a {padding-left: 0.2em;}
|
||||
.col2 a.active,
|
||||
.col2 a.active:hover {
|
||||
color:#fff;
|
||||
background:#000;
|
||||
font-weight:bold;
|
||||
text-decoration:none;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
/* 3 Column settings */
|
||||
.threecol {
|
||||
background:#eee; /* right column background colour */
|
||||
background:Khaki;
|
||||
}
|
||||
.threecol .colmid {
|
||||
right:25%; /* width of the right column */
|
||||
background:lightgoldenrodyellow; /* center column background colour */
|
||||
}
|
||||
.threecol .colleft {
|
||||
right:50%; /* width of the middle column */
|
||||
background:#f4f4f4; /* left column background colour */
|
||||
background:Khaki;
|
||||
}
|
||||
.threecol .col1 {
|
||||
width:46%; /* width of center column content (column width minus padding on either side) */
|
||||
left:102%; /* 100% plus left padding of center column */
|
||||
}
|
||||
.threecol .col2 {
|
||||
width:21%; /* Width of left column content (column width minus padding on either side) */
|
||||
left:31%; /* width of (right column) plus (center column left and right padding) plus (left column left padding) */
|
||||
}
|
||||
.threecol .col3 {
|
||||
width:21%; /* Width of right column content (column width minus padding on either side) */
|
||||
|
||||
/* Note this used to be 85%, but I subtracted 1% for padding to pull stuff closer to the margin */
|
||||
left:84%; /* Please make note of the brackets here:
|
||||
(100% - left column width) plus (center column left and right padding) plus (left column left and right padding) plus (right column left padding) */
|
||||
}
|
||||
/* Footer styles */
|
||||
#footer {
|
||||
clear:both;
|
||||
float:left;
|
||||
width:100%;
|
||||
border-top:1px solid #000;
|
||||
padding: 1em;
|
||||
font-size:75%;
|
||||
background:GoldenRod;
|
||||
}
|
||||
#footer p {
|
||||
margin:0;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
form.form {
|
||||
margin:0 auto;
|
||||
width:100%;
|
||||
background:khaki;
|
||||
position:relative;
|
||||
margin-top:2em;
|
||||
}
|
||||
|
||||
form.form h2 {
|
||||
color:#ffffff;
|
||||
font-size:1.2em;
|
||||
background:goldenrod;
|
||||
text-transform:uppercase;
|
||||
padding:0.5em 0em 0.5em 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
form.form h3 {
|
||||
color:red;
|
||||
font-weight:bold;
|
||||
font-size:1em;
|
||||
padding:0.5em 0 0.5em 0.5em;
|
||||
text-align:center;
|
||||
background:khaki;
|
||||
}
|
||||
|
||||
form.form .line,
|
||||
form.form p {
|
||||
width:94%;
|
||||
display: block;
|
||||
background:khaki;
|
||||
padding:0.5em 0em 1em 1em;
|
||||
|
||||
padding-right:0.5em;
|
||||
margin-left:0.5em;
|
||||
background:lightgoldenrodyellow;
|
||||
margin-top: 0em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
form.form label {
|
||||
width:100%;
|
||||
display: block;
|
||||
background:khaki;
|
||||
padding:1em 0 0.5em 0em;
|
||||
}
|
||||
|
||||
form.form .submit {
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
form.form .indent {
|
||||
padding: 0px;
|
||||
padding-left: 0.75em;
|
||||
}
|
||||
|
||||
form.form label span {
|
||||
display: block;
|
||||
color:black;
|
||||
font-size:12px;
|
||||
float:left;
|
||||
width:30%;
|
||||
text-align:right;
|
||||
padding:0.5em 2em 0 0;
|
||||
}
|
||||
|
||||
form.form .inputtext {
|
||||
padding:0.2em 0.3em 0.2em 0.3em;
|
||||
background:lightgoldenrodyellow;
|
||||
border-bottom: 1px double #171717;
|
||||
border-top: 1px double #171717;
|
||||
border-left:1px double #333333;
|
||||
border-right:1px double #333333;
|
||||
}
|
||||
|
||||
form.form .checkbox { text-align: left;}
|
||||
|
||||
form.form .inputtextnowidth {
|
||||
padding:0.5em 0.5em 0em 0em;
|
||||
background:lightgoldenrodyellow;
|
||||
border-bottom: 1px double #171717;
|
||||
border-top: 1px double #171717;
|
||||
border-left:1px double #333333;
|
||||
border-right:1px double #333333;
|
||||
}
|
||||
|
||||
form.form .textbox{
|
||||
padding:7px 7px;
|
||||
width:60%;
|
||||
background:lightgoldenrodyellow;
|
||||
border-bottom: 1px double #171717;
|
||||
border-top: 1px double #171717;
|
||||
border-left:1px double #333333;
|
||||
border-right:1px double #333333;
|
||||
overflow:hidden;
|
||||
height:150px;
|
||||
}
|
||||
|
||||
form.form .button
|
||||
{
|
||||
margin:0 0 10px 0;
|
||||
padding:4px 7px;
|
||||
background:goldenrod;
|
||||
border:0px;
|
||||
position: relative;
|
||||
top:10px;
|
||||
width:100px;
|
||||
border-bottom: 1px double Goldenrod;
|
||||
border-top: 1px double Gold;
|
||||
border-left:1px double Goldenrod;
|
||||
border-right:1px double Gold;
|
||||
}
|
||||
|
||||
.err {color:red;}
|
||||
76
util.py
Normal file
76
util.py
Normal file
@ -0,0 +1,76 @@
|
||||
import sys
|
||||
import cherrypy
|
||||
import cfg
|
||||
import sqlite3
|
||||
from filedict import FileDict
|
||||
|
||||
def is_string(obj):
|
||||
isinstance(obj, basestring)
|
||||
def is_ascii(s):
|
||||
return all(ord(c) < 128 for c in s)
|
||||
def is_alphanumeric(string):
|
||||
for c in string:
|
||||
o = ord(c)
|
||||
if not o in range(48, 58) + range(41, 91) + [95] + range(97, 123):
|
||||
return False
|
||||
return True
|
||||
|
||||
def slurp(filespec):
|
||||
with open(filespec) as x: f = x.read()
|
||||
return f
|
||||
|
||||
def unslurp(filespec, msg):
|
||||
with open(filespec, 'w') as x:
|
||||
x.write(msg)
|
||||
|
||||
def find_in_seq(func, seq):
|
||||
"Return first item in seq for which func(item) returns True."
|
||||
for i in seq:
|
||||
if func(i):
|
||||
return i
|
||||
|
||||
def find_keys(dic, val):
|
||||
"""return the key of dictionary dic given the value"""
|
||||
return [k for k, v in dic.iteritems() if v == val]
|
||||
|
||||
class Message():
|
||||
def __init__(self, msg=''):
|
||||
self.text = msg
|
||||
def add(self, text):
|
||||
self.text += "<br />%s" % text
|
||||
|
||||
def page_template(template='base', **kwargs):
|
||||
for k in ['sidebar_left', 'sidebar_right', 'main', 'js', 'onload', 'nav', 'css']:
|
||||
if not k in kwargs:
|
||||
kwargs[k] = ''
|
||||
|
||||
if template=='base' and kwargs['sidebar_right']=='':
|
||||
template='two_col'
|
||||
if isinstance(template, basestring):
|
||||
exec ("from templates.%s import %s as template" % (template, template))
|
||||
try:
|
||||
submenu = cfg.main_menu.active_item().encode("sub_menu", render_subs=True)
|
||||
except AttributeError:
|
||||
submenu = ""
|
||||
|
||||
kwargs['template'] = template
|
||||
kwargs['main_menu_js'] = cfg.main_menu.encode("main_menu")
|
||||
kwargs['sub_menu_js'] = submenu
|
||||
kwargs['current_url'] = cherrypy.url()
|
||||
kwargs['username'] = cherrypy.session.get(cfg.session_key)
|
||||
|
||||
if not kwargs['nav']: kwargs['nav'] = """ <SCRIPT LANGUAGE="JavaScript">
|
||||
<!--
|
||||
side_menu(sub_menu_items);
|
||||
// -->
|
||||
</SCRIPT>"""
|
||||
|
||||
return str(template(searchList=[kwargs]))
|
||||
|
||||
def filedict_con(filespec=None, table='dict'):
|
||||
"""TODO: better error handling in filedict_con"""
|
||||
try:
|
||||
return FileDict(connection=sqlite3.connect(filespec), table=table)
|
||||
except IOError as (errno, strerror):
|
||||
cfg.log.critical("I/O error({0}): {1}".format(errno, strerror))
|
||||
sys.exit(-1)
|
||||
Loading…
x
Reference in New Issue
Block a user