from math import ceil import base64 import hmac from datetime import datetime, timedelta from hashlib import sha256 from urllib.parse import parse_qsl, urlencode import requests from constance import config as site_config from django.conf import settings as project_settings from django.contrib import auth, messages from django.contrib.admin.sites import site from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import User from django.db.models import Count from django.db import transaction from django.http import (HttpResponse, HttpResponseNotFound, HttpResponseRedirect, JsonResponse) from django.shortcuts import redirect, render from django.utils import timezone from django.utils.translation import ugettext as _ from django.template.loader import render_to_string from django_countries import countries import django_lcore import lcoreapi from ccvpn.common import get_client_ip, get_price_float from payments.models import ACTIVE_BACKENDS from .forms import SignupForm, ReqEmailForm, WgPeerForm from .models import GiftCode, VPNUser from . import graphs def get_locations(): """ Pretty bad thing that returns get_locations() with translated stuff that depends on the request """ countries_d = dict(countries) locations = django_lcore.get_clusters() for (_), loc in locations: code = loc['country_code'].upper() loc['country_name'] = countries_d.get(code, code) loc['load_percent'] = ceil((loc['usage'].get('net', 0) / (loc['bandwidth'] / 1e6)) * 100) locations = list(sorted(locations, key=lambda l: l[0])) return locations def log_errors(request, form): errors = [] for field in form: for e in field.errors: errors.append(e) messages.add_message(request, messages.ERROR, ", ".join(errors)) def ca_crt(request): return HttpResponse(content=project_settings.OPENVPN_CA, content_type='application/x-x509-ca-cert') def logout(request): auth.logout(request) return redirect('index') def signup(request): if request.user.is_authenticated: return redirect('account:index') if request.method != 'POST': form = SignupForm() return render(request, 'ccvpn/signup.html', dict(form=form)) form = SignupForm(request.POST) grr = request.POST.get('g-recaptcha-response', '') if captcha_test(grr, request): request.session['signup_captcha_pass'] = True elif not request.session.get('signup_captcha_pass'): messages.error(request, _("Invalid captcha. Please try again")) return render(request, 'ccvpn/signup.html', dict(form=form)) if not form.is_valid(): return render(request, 'ccvpn/signup.html', dict(form=form)) user = User.objects.create_user(form.cleaned_data['username'], form.cleaned_data['email'], form.cleaned_data['password']) user.save() try: user.vpnuser.referrer = User.objects.get(id=request.session.get('referrer')) except User.DoesNotExist: pass user.vpnuser.campaign = request.session.get('campaign') user.vpnuser.add_paid_time(timedelta(days=7)) if site_config.TRIAL_ON_SIGNUP: trial_time = timedelta(hours=site_config.TRIAL_ON_SIGNUP) user.vpnuser.add_paid_time(trial_time) user.vpnuser.save() user.vpnuser.lcore_sync() user.backend = 'django.contrib.auth.backends.ModelBackend' auth.login(request, user) # invalidate that captcha request.session['signup_captcha_pass'] = False return redirect('account:index') @login_required def discourse_login(request): sso_secret = project_settings.DISCOURSE_SECRET discourse_url = project_settings.DISCOURSE_URL if project_settings.DISCOURSE_SSO is not True: return HttpResponseNotFound() payload = request.GET.get('sso', '') signature = request.GET.get('sig', '') expected_signature = hmac.new(sso_secret.encode('utf-8'), payload.encode('utf-8'), sha256).hexdigest() if signature != expected_signature: return HttpResponseNotFound() if request.method == 'POST' and 'email' in request.POST: form = ReqEmailForm(request.POST) if not form.is_valid(): return render(request, 'ccvpn/require_email.html', dict(form=form)) request.user.email = form.cleaned_data['email'] request.user.save() if not request.user.email: form = ReqEmailForm() return render(request, 'ccvpn/require_email.html', dict(form=form)) try: payload = base64.b64decode(payload).decode('utf-8') payload_data = dict(parse_qsl(payload)) except (TypeError, ValueError): return HttpResponseNotFound() payload_data.update({ 'external_id': request.user.id, 'username': request.user.username, 'email': request.user.email, 'require_activation': 'true', }) payload = urlencode(payload_data) payload = base64.b64encode(payload.encode('utf-8')) signature = hmac.new(sso_secret.encode('utf-8'), payload, sha256).hexdigest() redirect_query = urlencode(dict(sso=payload, sig=signature)) redirect_path = '/session/sso_login?' + redirect_query return HttpResponseRedirect(discourse_url + redirect_path) @login_required def index(request): ref_url = project_settings.ROOT_URL + '?ref=' + str(request.user.id) twitter_url = 'https://twitter.com/intent/tweet?' twitter_args = { 'text': _("Awesome VPN! 3€ per month, with a free 7 days trial!"), 'via': 'CCrypto_VPN', 'url': ref_url, 'related': 'CCrypto_VPN,CCrypto_org' } class price_fn: """ Clever hack to get the price in templates with {{price.3}} with 3 an arbitrary number of months """ def __getitem__(self, months): n = int(months) * get_price_float() c = project_settings.PAYMENTS_CURRENCY[1] return '%.2f %s' % (n, c) context = dict( title=_("Account"), ref_url=ref_url, twitter_link=twitter_url + urlencode(twitter_args), subscription=request.user.vpnuser.get_subscription(include_unconfirmed=True), backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_display_name), subscr_backends=sorted((b for b in ACTIVE_BACKENDS.values() if b.backend_has_recurring), key=lambda x: x.backend_id), default_backend='paypal', hcaptcha_site_key=project_settings.HCAPTCHA_SITE_KEY, price=price_fn(), user_motd=site_config.MOTD_USER, ) return render(request, 'lambdainst/account.html', context) def captcha_test(grr, request): api_url = project_settings.HCAPTCHA_API if api_url == 'TEST' and grr == 'TEST-TOKEN': # FIXME: i'm sorry. return True data = dict(secret=project_settings.HCAPTCHA_SECRET_KEY, response=grr) try: r = requests.post(api_url, data=data) r.raise_for_status() d = r.json() return d.get('success') except (requests.ConnectionError, requests.HTTPError, ValueError): return False @login_required def trial(request): if request.method != 'POST' or not request.user.vpnuser.can_have_trial: return redirect('account:index') grr = request.POST.get('g-recaptcha-response', '') if captcha_test(grr, request): request.user.vpnuser.give_trial_period() request.user.vpnuser.save() messages.success(request, _("OK!")) else: messages.error(request, _("Invalid captcha")) return redirect('account:index') def make_export_zip(user, name): import io import zipfile import json f = io.BytesIO() z = zipfile.ZipFile(f, mode="w") gw_cache = {} def process_wg_peer(item): keys = {"gateway_port", "id", "local_ipv4", "local_ipv6", "name", "object", "private_key", "public_key"} return {k: v for (k, v) in item.items() if k in keys} def process_ovpn_sess(item): keys = {"connect_date", "disconnect_date", "remote", "object", "protocol", "id", "stats", "tunnel"} def convert(v): if isinstance(v, datetime): return v.isoformat() return v obj = {k: convert(v) for (k, v) in item.items() if k in keys} gw_url = item["gateway"]["href"] if gw_url not in gw_cache: gw = django_lcore.api.get(gw_url) gw_cache[gw_url] = { "name": gw["name"], "ipv4": gw["main_addr"]["ipv4"], "ipv6": gw["main_addr"]["ipv6"], } obj["gateway"] = gw_cache[gw_url] return obj def process_payments(item): obj = { "id": item.id, "backend": item.backend_id, "status": item.status, "created": item.created.isoformat(), "confirmed": item.confirmed_on.isoformat() if item.confirmed_on else None, "amount": item.amount / 100, "paid_amount": item.amount / 100, "time": item.time.total_seconds(), "external_id": item.backend_extid, } if item.subscription: obj["subscription"] = item.subscription.backend_extid else: obj["subscription"] = None return obj with z.open(name + "/account.json", "w") as jf: jf.write(json.dumps({ "username": user.username, "email": user.email, "date_joined": user.date_joined.isoformat(), "expiration": user.vpnuser.expiration.isoformat() if user.vpnuser.expiration else None, }, indent=2).encode('ascii')) with z.open(name + "/wireguard_peers.json", "w") as jf: try: keys = list(map(process_wg_peer, django_lcore.api.get_wg_peers(user.username))) except lcoreapi.APINotFoundError: keys = [] jf.write(json.dumps(keys, indent=2).encode('ascii')) with z.open(name + "/openvpn_logs.json", "w") as jf: base = django_lcore.api.info['current_instance'] next_page = '/users/' + user.username + '/sessions/' try: items = django_lcore.api.get(base + next_page).list_iter() except lcoreapi.APINotFoundError: items = [] items = list(map(process_ovpn_sess, items)) jf.write(json.dumps(items, indent=2).encode('ascii')) with z.open(name + "/payments.json", "w") as jf: items = user.payment_set.all() items = list(map(process_payments, items)) jf.write(json.dumps(items, indent=2).encode('ascii')) z.close() return f.getvalue() def deactivate_user(user): """ clear most information from a user, keeping the username and id """ user.vpnuser.clear_fields() user.vpnuser.save() user.is_active = False user.email = "" user.password = "" user.save() user.giftcodeuser_set.all().delete() user.payment_set.update(backend_data="null") user.subscription_set.update(backend_data="null") django_lcore.sync_user(user.vpnuser) @login_required def settings(request): can_delete = request.user.vpnuser.get_subscription() is None if request.method != 'POST': return render(request, 'lambdainst/settings.html', dict( can_delete=can_delete, )) action = request.POST.get('action') current_pw = request.POST.get('current_password') if not request.user.check_password(current_pw): messages.error(request, _("Invalid password")) return redirect('lambdainst:account_settings') if action == 'email': email = request.POST.get('email') if email: request.user.email = email messages.success(request, _("OK! Email address changed.")) else: request.user.email = '' messages.success(request, _("OK! Email address unset.")) request.user.save() return redirect('lambdainst:account_settings') elif action == 'password': pw = request.POST.get('password') pw2 = request.POST.get('password2') if pw != pw2 or not pw: messages.error(request, _("Password and confirmation do not match")) else: request.user.set_password(pw) messages.success(request, _("OK! Password changed.")) request.user.save() django_lcore.sync_user(request.user.vpnuser) return redirect('lambdainst:account_settings') elif action == 'export': timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") data = make_export_zip(request.user, timestamp) r = HttpResponse(content=data, content_type="application/zip") r["Content-Disposition"] = 'attachment; filename="ccvpn-export-{}-{}.zip"'.format(request.user.username, timestamp) return r elif action == 'delete' and can_delete: with transaction.atomic(): deactivate_user(request.user) logout(request) messages.success(request, _("OK! Your account has been deactivated.")) return redirect('/') return render(request, 'lambdainst/settings.html', dict( title=_("Settings"), user=request.user, can_delete=can_delete, )) @login_required def gift_code(request): try: code = GiftCode.objects.get(code=request.POST.get('code', '').strip(), available=True) except GiftCode.DoesNotExist: code = None if code is None: messages.error(request, _("Gift code not found or already used.")) elif not code.use_on(request.user): messages.error(request, _("Gift code only available to free accounts.")) else: messages.success(request, _("OK!")) return redirect('account:index') @login_required def config(request): return render(request, 'lambdainst/config.html', dict( title=_("Config"), config_os=django_lcore.openvpn.CONFIG_OS, config_countries=(c for _, c in get_locations()), config_protocols=django_lcore.openvpn.PROTOCOLS, )) def api_locations(request): def format_loc(cc, l): msg = "" tags = l.get('tags', {}) message = tags.get('message') if message: msg = " [%s]" % message return { 'country_name': l['country_name'] + msg, 'country_code': cc, 'hostname': l['hostname'], 'bandwidth': l['bandwidth'], 'servers': l['servers'], } return JsonResponse(dict(locations=[format_loc(cc, l) for cc, l in get_locations()])) def status(request): locations = get_locations() ctx = { 'title': _("Servers"), 'locations': locations, } return render(request, 'lambdainst/status.html', ctx) @user_passes_test(lambda user: user.is_staff) def admin_status(request): graph_name = request.GET.get('graph_name') graph_period = request.GET.get('period') if graph_period not in ('y', 'm'): graph_period = 'm' if graph_name: if graph_name == 'users': content = graphs.users_graph(graph_period) elif graph_name == 'payments_paid': content = graphs.payments_paid_graph(graph_period) elif graph_name == 'payments_success': content = graphs.payments_success_graph(graph_period) else: return HttpResponseNotFound() return HttpResponse(content=content, content_type='image/svg+xml') payment_status = ((b, b.get_info()) for b in ACTIVE_BACKENDS.values()) payment_status = ((b, i) for (b, i) in payment_status if i) lcore_keys = {'core_name', 'core_now', 'core_version', 'current_instance', 'key_public'} ctx = { 'api_status': {k: str(v) for k, v in django_lcore.api.info.items() if k in lcore_keys}, 'payment_backends': sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_id), 'payment_status': payment_status, } ctx.update(site.each_context(request)) return render(request, 'lambdainst/admin_status.html', ctx) @user_passes_test(lambda user: user.is_staff) def admin_ref(request): last_week = datetime.now() - timedelta(days=7) last_month = datetime.now() - timedelta(days=30) top_ref = User.objects.annotate(n_ref=Count('referrals')).order_by('-n_ref')[:10] top_ref_week = User.objects.filter(referrals__user__date_joined__gt=last_week) \ .annotate(n_ref=Count('referrals')) \ .order_by('-n_ref')[:10] top_ref_month = User.objects.filter(referrals__user__date_joined__gt=last_month) \ .annotate(n_ref=Count('referrals')) \ .order_by('-n_ref')[:10] ctx = { 'top_ref': top_ref, 'top_ref_week': top_ref_week, 'top_ref_month': top_ref_month, } ctx.update(site.each_context(request)) return render(request, 'lambdainst/admin_ref.html', ctx) @login_required def wireguard(request): api = django_lcore.api if request.method == 'POST': action = request.POST.get('action') if action == 'delete_key': key = api.get_wg_peer(request.user.username, request.POST.get('peer_id')) if key: key.delete() elif action == 'set_name': key = api.get_wg_peer(request.user.username, request.POST.get('peer_id')) if key: name = request.POST.get('name') if name: name = name[:21] key.rename(name) return redirect(request.path) try: keys = api.get_wg_peers(request.user.username) except lcoreapi.APINotFoundError: django_lcore.sync_user(request.user.vpnuser) keys = [] context = dict( can_create_key=len(keys) < int(site_config.WIREGUARD_MAX_PEERS), menu_item='wireguard', enabled=request.user.vpnuser.is_paid, config_countries=[(k, v) for (k, v) in django_lcore.get_clusters()], keys=keys, locations=get_locations(), ) return render(request, 'lambdainst/wireguard.html', context) @login_required def wireguard_new(request): if not request.user.vpnuser.is_paid: return redirect('account:index') try: keys = django_lcore.api.get_wg_peers(request.user.username) except lcoreapi.APINotFoundError: django_lcore.sync_user(request.user.vpnuser) keys = [] if len(keys) >= int(site_config.WIREGUARD_MAX_PEERS): return redirect('/account/wireguard') api = django_lcore.api if request.method == 'POST': action = request.POST.get('action') form = WgPeerForm(request.POST) if action == 'add_key': if form.is_valid(): api.create_wg_peer( request.user.username, public_key=form.cleaned_data['public_key'], name=form.cleaned_data['name'], ) else: log_errors(request, form) return redirect('/account/wireguard') context = dict( menu_item='wireguard', locations=get_locations(), ) return render(request, 'lambdainst/wireguard_new.html', context)