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 gettext 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 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 except VPNUser.DoesNotExist: user.vpnuser = VPNUser.objects.create(user=user) user.vpnuser.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), "trial") 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) 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, subscription=request.user.vpnuser.get_subscription(), 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", 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 not project_settings.HCAPTCHA_SITE_KEY: return True 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 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.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 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) @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)