|
|
|
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)
|