Commit 9e47d2e4 authored by ale's avatar ale

allow multiple downloads of the same zip file (for a short time)

parent 70c717c8
{% extends "_base.html" %}
{% set dl_url = url_for('vpn_admin.new_cert_dl', _csrf=csrf_token()) %}
{% 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 %}
......
import datetime
import functools
import itsdangerous
import logging
import os
import shutil
import subprocess
import tempfile
import threading
import time
import uuid
import zipfile
from cStringIO import StringIO
from OpenSSL import crypto
from flask import Blueprint, Flask, abort, redirect, request, make_response, \
render_template, session, g, current_app, url_for
from autoca import ca
from autoca import ca_app
from autoca import ca_stub
from flask import Blueprint, Flask, abort, redirect, request, make_response, \
render_template, session, g, current_app, url_for
vpn_admin = Blueprint('vpn_admin', __name__)
log = logging.getLogger(__name__)
......@@ -136,6 +140,97 @@ Further info:
'''
class Singleton(object):
"""Singleton with deferred instantiation."""
def __init__(self, cls):
self.lock = threading.Lock()
self.obj = None
self.cls = cls
def __call__(self, *args):
with self.lock:
if self.obj is None:
self.obj = self.cls(*args)
return self.obj
class Signer(object):
"""Generates secure download tokens that are valid for a few seconds."""
TIMEOUT = 60
def __init__(self, secret):
self.l = threading.local()
self.secret = secret
def _signer(self):
if not hasattr(self.l, 'signer'):
self.l.signer = itsdangerous.URLSafeTimedSerializer(self.secret, salt='cn')
return self.l.signer
def encode(self):
cn = str(uuid.uuid4())
return self._signer().dumps(cn)
def decode(self, token):
return self._signer().loads(token, max_age=self.TIMEOUT)
class ZipCacheCleanupThread(threading.Thread):
EXPIRE_TIME = 120
def __init__(self, root):
threading.Thread.__init__(self)
self.root = root
self.setDaemon(True)
self.start()
def purge_cached_files(self):
cutoff = self.EXPIRE_TIME
now = time.time()
for f in os.listdir(self.root):
if f.startswith('.') or not f.endswith('.zip'):
continue
fp = os.path.join(self.root, f)
mtime = os.path.getmtime(gp)
if (now - mtime) > cutoff:
os.unlink(fp)
def run(self):
while True:
time.sleep(60)
try:
self.purge_cached_files()
except:
pass
class ZipCache(object):
"""A simple on-disk cache of recent zip files."""
cleanup_thread = Singleton(ZipCacheCleanupThread)
def __init__(self, root):
self.root = root
self.cleanup = self.cleanup_thread(root)
def _path(self, cn):
return os.path.join(self.root, cn + '.zip')
def get(self, cn):
try:
with open(self._path(cn), 'r') as fd:
return fd.read()
except:
return None
def put(self, cn, contents):
with open(self._path(cn), 'w') as fd:
fd.write(contents)
def to_pkcs12(crt_pem, key_pem, ca_pem):
"""Pack credentials into a PKCS12-format buffer."""
tmpdir = tempfile.mkdtemp()
......@@ -220,66 +315,78 @@ def logout():
@auth
@csrf(methods=['GET', 'POST'])
def new_cert():
session['dl_ok'] = True
return render_template('download.html')
cn = str(uuid.uuid4())
cn_token = current_app.signer.encode(cn)
return render_template('download.html', cn_token=cn_token)
@vpn_admin.route('/newcertdl')
@auth
@csrf(methods=['GET'])
def new_cert_dl():
if not session.pop('dl_ok', None):
# Retrieve CN from the signed token.
try:
cn = current_app.signer.decode(request.args.get('t'))
except:
return render_template('download_retry.html')
# Create and sign a certificate.
cn = str(uuid.uuid4())
# Check if the certificate is new or not, so that the user can
# perform multiple downloads of the same certificate / private
# keypair until the token is valid.
zip_data = current_app.zipcache.get(cn)
if not zip_data:
validity = int(current_app.config.get('VPN_CERT_VALIDITY', 7))
expiry_date = datetime.date.today() + datetime.timedelta(validity)
# Create and sign the new certificate.
validity = int(current_app.config.get('VPN_CERT_VALIDITY', 7))
expiry_date = datetime.date.today() + datetime.timedelta(validity)
subject = current_app.config.get('VPN_DEFAULT_SUBJECT_ATTRS', {}).copy()
subject['CN'] = cn
subject = current_app.config.get('VPN_DEFAULT_SUBJECT_ATTRS', {}).copy()
subject['CN'] = cn
pkey, cert = g.ca.make_certificate(subject, days=validity)
pkey, cert = g.ca.make_certificate(subject, days=validity)
# Create the zipfile in-memory, with all the files the user needs.
vars = {'cn': cn,
'bundle_identifier': '.'.join(
# Create the zipfile in-memory, with all the files the user needs.
vars = {'cn': cn,
'bundle_identifier': '.'.join(
current_app.config['VPN_ENDPOINT'].split('.')[::-1]) + '.' + cn,
'vpn_endpoint': current_app.config['VPN_ENDPOINT'],
'vpn_site': current_app.config['VPN_SITE_URL'],
'expiry_date': expiry_date.strftime('%Y/%m/%d')}
ca_pem = g.ca.get_ca()
crt_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
pkcs12 = to_pkcs12(crt_pem, key_pem, ca_pem)
manifest = [
('ca.crt', ca_pem),
('crl.pem', g.ca.get_crl(format='pem')),
('%s.crt' % cn, crt_pem),
('%s.key' % cn, key_pem),
('%s.pfx' % cn, pkcs12),
('tlsauth.key', current_app.config['TLS_AUTH_KEY']),
('openvpn-%s.conf' % cn, OPENVPN_CONFIG_TEMPLATE % vars),
('README.txt', README_TEMPLATE % vars),
# Tunnelblick configuration for OSX
('%s.tblk/Info.plist' % cn, TBLK_PLIST_TEMPLATE % vars),
('%s.tblk/config.ovpn' % cn, OPENVPN_CONFIG_TEMPLATE % vars),
('%s.tblk/ca.crt' % cn, g.ca.get_ca()),
('%s.tblk/crl.pem' % cn, g.ca.get_crl(format='pem')),
('%s.tblk/%s.crt' % (cn, cn), crt_pem),
('%s.tblk/%s.key' % (cn, cn), key_pem),
('%s.tblk/tlsauth.key' % cn, current_app.config['TLS_AUTH_KEY']),
]
zbuf = StringIO()
zf = zipfile.ZipFile(zbuf, mode='w',
compression=zipfile.ZIP_DEFLATED)
for filename, contents in manifest:
zf.writestr(filename, contents)
zf.close()
response = make_response(zbuf.getvalue())
'vpn_endpoint': current_app.config['VPN_ENDPOINT'],
'vpn_site': current_app.config['VPN_SITE_URL'],
'expiry_date': expiry_date.strftime('%Y/%m/%d')}
ca_pem = g.ca.get_ca()
crt_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
pkcs12 = to_pkcs12(crt_pem, key_pem, ca_pem)
manifest = [
('ca.crt', ca_pem),
('crl.pem', g.ca.get_crl(format='pem')),
('%s.crt' % cn, crt_pem),
('%s.key' % cn, key_pem),
('%s.pfx' % cn, pkcs12),
('tlsauth.key', current_app.config['TLS_AUTH_KEY']),
('openvpn-%s.conf' % cn, OPENVPN_CONFIG_TEMPLATE % vars),
('README.txt', README_TEMPLATE % vars),
# Tunnelblick configuration for OSX
('%s.tblk/Info.plist' % cn, TBLK_PLIST_TEMPLATE % vars),
('%s.tblk/config.ovpn' % cn, OPENVPN_CONFIG_TEMPLATE % vars),
('%s.tblk/ca.crt' % cn, g.ca.get_ca()),
('%s.tblk/crl.pem' % cn, g.ca.get_crl(format='pem')),
('%s.tblk/%s.crt' % (cn, cn), crt_pem),
('%s.tblk/%s.key' % (cn, cn), key_pem),
('%s.tblk/tlsauth.key' % cn, current_app.config['TLS_AUTH_KEY']),
]
zbuf = StringIO()
zf = zipfile.ZipFile(zbuf, mode='w',
compression=zipfile.ZIP_DEFLATED)
for filename, contents in manifest:
zf.writestr(filename, contents)
zf.close()
zip_data = zbuf.getvalue()
current_app.zipcache.put(cn, zip_data)
response = make_response(zip_data)
response.headers['Content-Type'] = 'application/zip'
response.headers['Content-Disposition'] = (
'attachment; filename="%s.zip"' % cn)
......@@ -291,6 +398,8 @@ def make_app(config={}):
app = Flask(__name__)
app.config.update(config)
app.config.from_envvar('VPN_APP_SETTINGS', silent=True)
app.signer = Signer(app.config['SIGNER_SECRET'])
app.zipcache = ZipCache(app.config['CACHE_DIR'])
app.register_blueprint(vpn_admin)
# Figure out how to hook to the CA.
......@@ -316,6 +425,7 @@ if __name__ == '__main__':
try:
make_app({'DEBUG': 'true',
'SECRET_KEY': 'somesecret',
'SIGNER_SECRET': 'moresecrets',
'VPN_CA_ROOT': ca_dir,
'VPN_CA_SUBJECT': {'CN': 'test CA', 'O': 'test'},
'VPN_ENDPOINT': 'vpn.example.com',
......@@ -327,6 +437,7 @@ if __name__ == '__main__':
''',
'AUTH_ENABLE': True,
'AUTH_FUNCTION': lambda x, y: (x and y and x == y),
'CACHE_DIR': 'cache',
}).run(port=4000)
finally:
shutil.rmtree(ca_dir)
......@@ -12,6 +12,7 @@ setup(
license = "MIT",
packages = find_packages(),
platforms = ["any"],
install_requires = ["Flask", "itsdangerous"],
zip_safe = False,
entry_points = {
"console_scripts": [
......
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