Commit f1f1f2e1 authored by ale's avatar ale

remove the autovpn code (on a separate repository)

parent 4da834f5
++++++
autoca
++++++
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 http://vpn.autistici.org/
where it is used to provide OpenVPN short-term certificates.
#!/usr/bin/python
# 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_URL = "https://www.autistici.org/internalca"
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 = req.read()
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__":
main()
AutoVPN
=======
A simple self-service OpenVPN infrastructure (with X.509 PKI).
Installation
------------
The 'autovpn' Python package is bundled with 'autoca'.
Once you've got the autoca/autovpn sources, just run::
$ sudo python setup.py install
from the top-level directory.
Configuration
-------------
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/
directory.
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):
VPN_ENDPOINT
DNS name for the VPN server (use more than one A record for
crude load balancing)
VPN_SITE_URL
The public URL for the AutoVPN web application
VPN_CERT_VALIDITY
How many days should the generated certificates be valid for
(default: 7)
VPN_DEFAULT_SUBJECT_ATTRS
A dictionary containing X.509 attributes to use as defaults for
the subject of the generated certificates. For example::
VPN_DEFAULT_SUBJECT_ATTRS = {'O': 'My VPN', 'C': 'IT'}
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:
VPN_CA_URL
URL of the remote autoca web application
CA_SHARED_SECRET
(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):
VPN_CA_ROOT
Base directory for the CA storage -- keep it private!
VPN_CA_BITS
How many bits to use for the CA RSA key (default 4096)
VPN_CA_SUBJECT
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:
AUTH_ENABLE
Set this to True to enable authentication
AUTH_FUNCTION
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)
local_pipe.execute()
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:
try:
key = 'aggr_in:%s' % cn
local_pipe.watch(key)
for data in local_pipe.lrange(key, 0, -1):
conns.append(data)
local_pipe.delete(key)
break
except redis.WatchError:
del conns[:]
finally:
local_pipe.reset()
# 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]
pipe.execute()
# Short return if there's nothing to do.
if not aggr:
return
# Aggregate values on the master server.
days = aggr.keys()
aggr_key = 'aggr:%s' % cn
pipe = self._aggr_db.pipeline()
while True:
try:
pipe.watch(aggr_key)
old_aggr = {}
for day, data in zip(days, pipe.hmget(aggr_key, days)):
if data:
old_aggr[day] = json.loads(data)
pipe.multi()
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))
pipe.execute()
break
except redis.WatchError:
continue
finally:
pipe.reset()
def aggregate_all(self):
local_pipe = self._local_db.pipeline()
while True:
try:
local_pipe.watch('in_cns')
input_cns = local_pipe.smembers('in_cns')
local_pipe.delete('in_cns')
break
except redis.WatchError:
continue
finally:
local_pipe.reset()
for cn in input_cns:
self.aggregate(cn)
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)
else:
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':
parser.show_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)
else:
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':
acct.aggregate_all()
else:
acct.aggregate(cn)
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'])),
conn['conn_time'],
'%d/%d' % (conn['bytes_recv'], conn['bytes_sent']),
conn['remote_ip'])
else:
parser.error('Unknown command "%s"' % cmd)
if __name__ == '__main__':
main()
This diff is collapsed.
/*
* jQuery Backstretch
* Version 1.2.5
* http://srobbin.com/jquery-plugins/jquery-backstretch/
*
* Add a dynamically-resized background image to the page
*
* Copyright (c) 2011 Scott Robbin (srobbin.com)
* 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"),g=c.data("settings")||n;c.data("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.target).width();b=this.height||a(b.target).height();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);c.data("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>
<html>
<head>
<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"
src="/static/jquery-1.6.4.min.js"></script>
<script type="text/javascript"
src="/static/jquery.backstretch.min.js"></script>
<script>
$(document).ready(function() {
$.backstretch('/static/background.jpg');
});
</script>
{% block head %}{% endblock -%}
</head>
<body>
<div id="container">
<h1>
<span>VPN</span>
</h1>
<div id="content">
{% block content %}{% endblock %}
{% if config.get('FOOTER') %}{{ config['FOOTER'] | safe }}{% endif %}
</div>
</div>
</body>
</html>
{% 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 %}
<p>
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
once).
</p>
<p>
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>.
</p>
<p>
<b>This ZIP file is VERY IMPORTANT, do not
share it with anyone, and save it only on trusted media.</b>
</p>
{% endblock %}
{% extends "_base.html" %}
{% block content %}
<p>
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
folder.
</p>
<p>
Otherwise, you should
<a href="{{ url_for('vpn_admin.index') }}">start the process
from scratch</a> (it's easy anyway!)
</p>
{% endblock %}
{% extends "_base.html" %}
{% block content %}
<p>
A reasonably anonymous (but low-traffic) VPN service, for when
you really need to get on the Internet from a nasty place!
</p>
<p>
You only need a SSL certificate to connect, click the link
below to download a ZIP file containing the certificate and
a private key.
</p>
<form action="{{ url_for('vpn_admin.new_cert') }}" method="post">
<div style="display:none;">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
</div>
<p class="center">
<input type="submit" value=" Generate new SSL certificate ">
</p>
</form>
{% if config.get('HELP_MESSAGE') %}
{{ config['HELP_MESSAGE'] | safe }}
{% endif %}
{% endblock %}
{% extends "_base.html" %}
{% block content %}
<p>
{% if config.get('AUTH_MESSAGE') %}
{{ config['AUTH_MESSAGE'] | safe }}
{% else %}
Enter your authentication credentials:
{% endif %}
</p>
{% 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() }}">
</div>
<p class="center">
<input type="text" size="20" placeholder="Email"
name="username" value="{{ username }}"><br>
<input type="password" size="20" placeholder="Password"
name="password"><br>
<input type="submit" value="Login">
</p>
</form>
<p>
<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.
</p>
{% 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_ENDPOINT': 'vpn.example.com',
'VPN_SITE_URL': 'http://localhost:4000/',
'FOOTER': '''
<p class="footer">
built by <a href="http://www.autistici.org/">autistici.org</a>
</p>
''',
'AUTH_ENABLE': True,
'AUTH_FUNCTION': lambda x, y: (x and y and x == y),
'TLS_AUTH_KEY': self.tmpdir + '/tlsauth.key',
}
self.app = vpn_app.make_app(self.config)
def tearDown(self):
shutil.rmtree(self.tmpdir)
def test_login_ok(self):
with self.app.test_client() as c:
rv = c.get('/login')
csrf = session['_csrf']
rv = c.post('/login', data={
'_csrf': csrf,
'username': 'admin',
'password': 'admin'},
follow_redirects=True)
self.assertTrue('download of the ZIP file' in rv.data)
def test_login_fail(self):
with self.app.test_client() as c:
rv = c.get('/login')
csrf = session['_csrf']
rv = c.post('/login', data={
'_csrf': csrf,
'username': 'user',
'password': 'wrong password'},
follow_redirects=True)
self.assertFalse(session.get('dl_ok'))
self.assertTrue('Authentication failed' in rv.data)
def test_cert_dl(self):
cn = 'testcn1234'
t = self.app.signer.encode(cn)
with self.app.test_client() as c:
with c.session_transaction() as sess:
sess['dl_ok'] = True
sess['_csrf'] = 'csrf'
sess['logged_in'] = True
rv = c.get('/newcertdl?_csrf=csrf&t=' + t)
self.assertEquals('200 OK', rv.status)
self.assertEquals('application/zip', rv.content_type)
This diff is collapsed.
......@@ -3,25 +3,19 @@
from setuptools import setup, find_packages
setup(
name = "autoca",
version = "0.2.1",
description = "Automated CA management.",
author = "Ale",
author_email = "ale@incal.net",
url = "http://git.autistici.org/autoca/",
license = "MIT",
packages = find_packages(),
platforms = ["any"],
install_requires = ["pyOpenSSL", "Flask", "itsdangerous"],
zip_safe = False,
entry_points = {
name="autoca",
version="0.3",
description="Automated CA management.",
author="Ale",
author_email="ale@incal.net",
url="http://git.autistici.org/ai/autoca",
license="MIT",
packages=find_packages(),
platforms=["any"],
install_requires=["pyOpenSSL", "Flask"],
entry_points={
"console_scripts": [
"autoca = autoca.ca_tool:main",
"vpnacct = autovpn.acct:main",
],
},
package_data={
"autovpn": ["templates/*", "static/*"],
},
)
"autoca=autoca.ca_tool:main",
],
},
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment