Commit f1f1f2e1 authored by ale's avatar ale
Browse files

remove the autovpn code (on a separate repository)

parent 4da834f5
A Certification Authority manager with a very simple API (and an HTTP
web interface).
Suitable for low-security authorities that can be used as a web
service. For a practical example, see
where it is used to provide OpenVPN short-term certificates.
# An example of how to create a new certificate request with certutil
# $Id$
import os, sys
import certutil
import optparse
import urllib
import urllib2
from OpenSSL import crypto
AUTOCA_CONF = "ca.yml"
def main():
parser = optparse.OptionParser(usage="Usage: newcert [<OPTIONS>] <CN>")
parser.add_option("-c", "--config", dest="config_file", default=AUTOCA_CONF,
help="Load CA configuration from this file")
parser.add_option("-u", "--ca-url", dest="ca_url", default=AUTOCA_URL,
help="Set AutoCA endpoint URL")
parser.add_option("-o", "--output", dest="output_base", default="newcert",
help="Set the name of the output files (minus the extension)")
opts, args = parser.parse_args()
if len(args) != 1:
parser.error("Wrong number of arguments")
cn = args[0]
ca = certutil.CA(opts.config_file, load=False)
pkey = ca.create_rsa_key_pair()
req = ca.create_cert_request(pkey, CN=cn)
key_file = "%s.key" % opts.output_base
csr_file = "%s.csr" % opts.output_base
crt_file = "%s.pem" % opts.output_base
csr = crypto.dump_certificate_request(crypto.FILETYPE_PEM, req)
open(key_file, "w").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
os.chmod(key_file, 0400)
open(csr_file, "w").write(csr)
data = urllib.urlencode({"cert": csr})
req = urllib2.urlopen(AUTOCA_URL + "/sign", data)
signed =
open(crt_file, "w").write(signed)
print "private key saved in %s" % key_file
print "public cert saved in %s" % crt_file
if __name__ == "__main__":
A simple self-service OpenVPN infrastructure (with X.509 PKI).
The 'autovpn' Python package is bundled with 'autoca'.
Once you've got the autoca/autovpn sources, just run::
$ sudo python install
from the top-level directory.
How to run the AutoVPN web application will depend on the deployment
method that you choose (mod_wsgi, FastCGI, standalone HTTP server...).
Examples of a few different drivers can be found in the apps/
In all cases though, you'll need to create a configuration file and
point the application at it (for example using the VPN_APP_CONFIG
environment variable). This file should define the following variables
(using Python syntax):
DNS name for the VPN server (use more than one A record for
crude load balancing)
The public URL for the AutoVPN web application
How many days should the generated certificates be valid for
(default: 7)
A dictionary containing X.509 attributes to use as defaults for
the subject of the generated certificates. For example::
The app will only set the 'CN' attribute.
There are two different options to connect to the CA, you can either
connect to a remote autoca web application:
URL of the remote autoca web application
(optional) the shared secret to use for authentication
Otherwise, you can instantiate a CA that is local to the AutoVPN
application itself (for simpler deployments):
Base directory for the CA storage -- keep it private!
How many bits to use for the CA RSA key (default 4096)
Dictionary with the X.509 CA subject attributes
In this latter case, the autoca web application will be available
under the /ca/ URL prefix.
The VPN web application supports authentication (to control who has
access to the certificate generation). To enable it, define the
following variables:
Set this to True to enable authentication
Set this to a function that should accept two arguments (username
and password) and return a True/Fals result.
import json
import optparse
import redis
import time
def time2day(unix_stamp):
return time.strftime('%Y%m%d', time.gmtime(unix_stamp))
class Accounting(object):
def __init__(self, local_db, aggr_db=None):
self._local_db = local_db
self._aggr_db = aggr_db or local_db
def add_connection(self, cn, conn_info):
data = json.dumps(conn_info)
local_pipe = self._local_db.pipeline()
local_pipe.sadd('in_cns', cn)
local_pipe.rpush('aggr_in:%s' % cn, data)
def get_connections(self, cn, n=0):
return (json.loads(x) for x in
self._aggr_db.lrange('connections:%s' % cn, 0, (n - 1)))
def aggregate(self, cn):
conns = []
local_pipe = self._local_db.pipeline()
while True:
key = 'aggr_in:%s' % cn
for data in local_pipe.lrange(key, 0, -1):
except redis.WatchError:
del conns[:]
# Compute daily aggregates, and copy the connection data to the master.
aggr = {}
pipe = self._aggr_db.pipeline()
pipe.sadd('all_cns', cn)
for data in conns:
pipe.lpush('connections:%s' % cn, data)
conn_info = json.loads(data)
day = time2day(conn_info['end_time'])
aggr_day = aggr.setdefault(day, {'conn_time': 0,
'bytes_sent': 0,
'bytes_recv': 0})
for attr in ('conn_time', 'bytes_recv', 'bytes_sent'):
aggr_day[attr] += conn_info[attr]
# Short return if there's nothing to do.
if not aggr:
# Aggregate values on the master server.
days = aggr.keys()
aggr_key = 'aggr:%s' % cn
pipe = self._aggr_db.pipeline()
while True:
old_aggr = {}
for day, data in zip(days, pipe.hmget(aggr_key, days)):
if data:
old_aggr[day] = json.loads(data)
for day, aggr_data in aggr.iteritems():
old_aggr_data = old_aggr.get(day, {})
for attr in aggr_data:
aggr_data[attr] += old_aggr_data.get(attr, 0)
pipe.hset(aggr_key, day, json.dumps(aggr_data))
except redis.WatchError:
def aggregate_all(self):
local_pipe = self._local_db.pipeline()
while True:
input_cns = local_pipe.smembers('in_cns')
except redis.WatchError:
for cn in input_cns:
def get_aggregate_counts(self, cn, when=None):
if not when:
when = time.time()
day = time2day(when)
data = self._aggr_db.hget('aggr:%s' % cn, day)
if data:
return json.loads(data)
return {'bytes_sent': 0, 'bytes_recv': 0, 'conn_time': 0}
def str2kv(args):
return dict(x.split('=', 1) for x in args)
def main():
parser = optparse.OptionParser()
parser.add_option('--db', dest='local_db', default='localhost',
help='endpoint of the local Redis database')
parser.add_option('--aggr-db', dest='aggr_db',
help='endpoint of the aggregate Redis database (optional)')
parser.add_option('--password', dest='password',
help='Redis password (optional)')
opts, args = parser.parse_args()
if not args:
parser.error('No command specified')
cmd, args = args[0], args[1:]
if cmd == 'help':
local_db = redis.Redis(opts.local_db, password=opts.password)
if opts.aggr_db:
aggr_db = redis.Redis(opts.aggr_db, password=opts.password)
aggr_db = None
acct = Accounting(local_db, aggr_db)
if cmd == 'connect':
if len(args) < 2:
parser.error('Syntax: connect <CN> <ATTR=VALUE>...')
cn = args[0]
conn_info = str2kv(args[1:])
for mandatory in ('bytes_sent', 'bytes_recv', 'remote_ip', 'conn_time'):
if mandatory not in conn_info:
parser.error('Missing mandatory attribute "%s"' % mandatory)
for int_attr in ('bytes_sent', 'bytes_recv', 'conn_time'):
conn_info[int_attr] = int(conn_info[int_attr])
conn_info['end_time'] = int(time.time())
conn_info['start_time'] = conn_info['end_time'] - conn_info['conn_time']
acct.add_connection(cn, conn_info)
elif cmd == 'aggregate':
if len(args) != 1:
parser.error('Syntax: aggregate <CN | "all">')
cn = args[0]
if cn == 'all':
elif cmd == 'get-aggr':
if len(args) != 1:
parser.error('Syntax: get-aggr <CN>')
cn = args[0]
result = acct.get_aggregate_counts(cn)
for key in sorted(result):
print '%s %d' % (key, result[key])
elif cmd == 'list':
if len(args) != 1:
parser.error('Syntax: list <CN>')
cn = args[0]
for conn in acct.get_connections(cn):
print '%s %s %-6d %-20s %-20s' % (
time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(conn['start_time'])),
time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(conn['end_time'])),
'%d/%d' % (conn['bytes_recv'], conn['bytes_sent']),
parser.error('Unknown command "%s"' % cmd)
if __name__ == '__main__':
This diff is collapsed.
* jQuery Backstretch
* Version 1.2.5
* Add a dynamically-resized background image to the page
* Copyright (c) 2011 Scott Robbin (
* Dual licensed under the MIT and GPL licenses.
(function(a){a.backstretch=function(l,b,j){function m(c){try{h={left:0,top:0},e=f.width(),d=e/k,d>=f.height()?(i=(d-f.height())/2,g.centeredY&&a.extend(h,{top:"-"+i+"px"})):(d=f.height(),e=d*k,i=(e-f.width())/2,g.centeredX&&a.extend(h,{left:"-"+i+"px"})),a("#backstretch, #backstretch img:not(.deleteable)").width(e).height(d).filter("img").css(h)}catch(b){}"function"==typeof c&&c()}var n={centeredX:!0,centeredY:!0,speed:0},c=a("#backstretch"),"settings")||n;"settings");var f="onorientationchange"in window?a(document):a(window),k,e,d,i,h;b&&"object"==typeof b&&a.extend(g,b);b&&"function"==typeof b&&(j=b);a(document).ready(function(){if(l){var b;0==c.length?c=a("<div />").attr("id","backstretch").css({left:0,top:0,position:"fixed",overflow:"hidden",zIndex:-999999,margin:0,padding:0,height:"100%",width:"100%"}):c.find("img").addClass("deleteable");b=a("<img />").css({position:"absolute",display:"none",margin:0,padding:0,border:"none",zIndex:-999999}).bind("load",function(b){var d=a(this),e;d.css({width:"auto",height:"auto"});e=this.width||a(;b=this.height||a(;k=e/b;m(function(){d.fadeIn(g.speed,function(){c.find(".deleteable").remove();"function"==typeof j&&j()})})}).appendTo(c);0==a("body #backstretch").length&&a("body").append(c);"settings",g);b.attr("src",l);a(window).resize(m)}});return this}})(jQuery);
\ No newline at end of file
body {
background-color: white;
font-family: helvetica, arial, sans-serif;
margin-left: 0px;
margin-right: auto;
width: 100%;
#container {
width: 100%;
margin-right: 0;
margin-left: 0;
padding: 0;
h1 {
font-family: helvetica, arial, sans-serif;
font-size: 6em;
color: red;
padding: 10px;
width: 50%;
background: url(/static/ant.jpg) white no-repeat top left;
opacity: 0.9;
h1 span {
display: block;
padding-left: 150px;
#content {
background-color: rgba(128,128,128,0.9);
font-weight: bold;
color: #FFF;
padding: 10px;
width: 50%;
font-size: 1.2em;
a {
text-decoration: underline;
color: white;
a:hover {
text-decoration: underline;
.footer {
font-size: 0.8em;
color: #AAA;
text-align: right;
padding-right: 5px;
margin-top: 20px;
.footer a {
text-decoration: none;
input {
font-size: 1.2em;
.center {
text-align: center;
<!doctype html>
<title>A/I VPN</title>
<link href="/static/favicon.ico" type="image/x-icon" rel="shortcut icon">
<link rel="stylesheet" type="text/css" href="/static/style.css">
<script type="text/javascript"
<script type="text/javascript"
$(document).ready(function() {
{% block head %}{% endblock -%}
<div id="container">
<div id="content">
{% block content %}{% endblock %}
{% if config.get('FOOTER') %}{{ config['FOOTER'] | safe }}{% endif %}
{% extends "_base.html" %}
{% set dl_url = url_for('vpn_admin.new_cert_dl', _csrf=csrf_token(), t=cn_token) %}
{% block head %}
<meta http-equiv="refresh" content="2; url={{ dl_url }}">
{% endblock %}
{% block content %}
The download of the ZIP file should start in a few seconds.
If it doesn't work, try with
<a href="{{ dl_url }}">this link</a> (note: it will only work
If you have any trouble downloading the ZIP file, try clearing
the cookies in your browser and
<a href="{{ url_for('vpn_admin.index') }}">starting over</a>.
<b>This ZIP file is VERY IMPORTANT, do not
share it with anyone, and save it only on trusted media.</b>
{% endblock %}
{% extends "_base.html" %}
{% block content %}
Sorry, you are trying to re-use an expired session. If you are
trying to download a certificate, it probably means that the
file is already on your computer, check in your Downloads
Otherwise, you should
<a href="{{ url_for('vpn_admin.index') }}">start the process
from scratch</a> (it's easy anyway!)
{% endblock %}
{% extends "_base.html" %}
{% block content %}
A reasonably anonymous (but low-traffic) VPN service, for when
you really need to get on the Internet from a nasty place!
You only need a SSL certificate to connect, click the link
below to download a ZIP file containing the certificate and
a private key.
<form action="{{ url_for('vpn_admin.new_cert') }}" method="post">
<div style="display:none;">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<p class="center">
<input type="submit" value=" Generate new SSL certificate ">
{% if config.get('HELP_MESSAGE') %}
{{ config['HELP_MESSAGE'] | safe }}
{% endif %}
{% endblock %}
{% extends "_base.html" %}
{% block content %}
{% if config.get('AUTH_MESSAGE') %}
{{ config['AUTH_MESSAGE'] | safe }}
{% else %}
Enter your authentication credentials:
{% endif %}
{% if error %}
<p class="center" style="color:red;">{{ error }}</p>
{% endif %}
<form action="{{ url_for('vpn_admin.login') }}" method="post">
<div style="display:none;">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<p class="center">
<input type="text" size="20" placeholder="Email"
name="username" value="{{ username }}"><br>
<input type="password" size="20" placeholder="Password"
<input type="submit" value="Login">
<b>NOTE:</b> the credentials are only used to limit access
to this application. They will not be saved, nor they will
ever be associated with the VPN connection.
{% endblock %}
import tempfile
import shutil
import unittest
from flask import session
from autovpn import vpn_app
class VpnAppTest(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
self.config = {
'DEBUG': 'true',
'SECRET_KEY': 'somesecret',
'SIGNER_SECRET': 'abracadabra',
'CACHE_DIR': self.tmpdir,
'VPN_CA_ROOT': self.tmpdir,
'VPN_CA_SUBJECT': {'CN': 'test CA', 'O': 'test'},
'VPN_CA_BITS': 1024,
'VPN_SITE_URL': 'http://localhost:4000/',
'FOOTER': '''
<p class="footer">
built by <a href=""></a>
'AUTH_FUNCTION': lambda x, y: (x and y and x == y),
'TLS_AUTH_KEY': self.tmpdir + '/tlsauth.key',
} = vpn_app.make_app(self.config)
def tearDown(self):