You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

546 lines
16 KiB
Python

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
8 years ago
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
8 years ago
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
8 years ago
from django_countries import countries
import django_lcore
import lcoreapi
8 years ago
from ccvpn.common import get_client_ip, get_price_float
8 years ago
from payments.models import ACTIVE_BACKENDS
from .forms import SignupForm, ReqEmailForm, WgPeerForm
from .models import VPNUser
8 years ago
from . import graphs
def get_locations():
"""Pretty bad thing that returns get_locations() with translated stuff
8 years ago
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)
4 years ago
loc["load_percent"] = ceil(
(loc["usage"].get("net", 0) / (loc["bandwidth"] / 1e6)) * 100
)
locations = list(sorted(locations, key=lambda l: l[0]))
8 years ago
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))
8 years ago
def ca_crt(request):
return HttpResponse(
content=project_settings.OPENVPN_CA, content_type="application/x-x509-ca-cert"
)
8 years ago
def logout(request):
auth.logout(request)
return redirect("index")
8 years ago
def signup(request):
if request.user.is_authenticated:
return redirect("account:index")
8 years ago
if request.method != "POST":
8 years ago
form = SignupForm()
return render(request, "ccvpn/signup.html", dict(form=form))
8 years ago
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))
8 years ago
if not form.is_valid():
return render(request, "ccvpn/signup.html", dict(form=form))
8 years ago
user = User.objects.create_user(
form.cleaned_data["username"],
form.cleaned_data["email"],
form.cleaned_data["password"],
)
8 years ago
user.save()
try:
user.vpnuser
except VPNUser.DoesNotExist:
user.vpnuser = VPNUser.objects.create(user=user)
user.vpnuser.save()
8 years ago
try:
user.vpnuser.referrer = User.objects.get(id=request.session.get("referrer"))
8 years ago
except User.DoesNotExist:
pass
user.vpnuser.campaign = request.session.get("campaign")
user.vpnuser.add_paid_time(timedelta(days=7), "trial")
4 years ago
8 years ago
user.vpnuser.save()
user.vpnuser.lcore_sync()
8 years ago
user.backend = "django.contrib.auth.backends.ModelBackend"
8 years ago
auth.login(request, user)
# invalidate that captcha
request.session["signup_captcha_pass"] = False
return redirect("account:index")
8 years ago
@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)
8 years ago
@login_required
def index(request):
ref_url = project_settings.ROOT_URL + "?ref=" + str(request.user.id)
8 years ago
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)
8 years ago
context = dict(
title=_("Account"),
ref_url=ref_url,
active_subscription=request.user.vpnuser.get_active_subscription(),
last_subscription=request.user.vpnuser.get_last_subscription(),
5 years ago
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,
8 years ago
)
return render(request, "lambdainst/account.html", context)
8 years ago
def captcha_test(grr, request):
api_url = project_settings.HCAPTCHA_API
8 years ago
if not project_settings.HCAPTCHA_SITE_KEY:
return True
if api_url == "TEST" and grr == "TEST-TOKEN":
8 years ago
# FIXME: i'm sorry.
return True
data = dict(secret=project_settings.HCAPTCHA_SECRET_KEY, response=grr)
8 years ago
try:
r = requests.post(api_url, data=data)
r.raise_for_status()
d = r.json()
return d.get("success")
8 years ago
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)
8 years ago
@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")
8 years ago
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"))
8 years ago
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,
),
)
8 years ago
@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,
),
)
8 years ago
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()])
)
8 years ago
def status(request):
locations = get_locations()
ctx = {
"title": _("Servers"),
"locations": locations,
8 years ago
}
return render(request, "lambdainst/status.html", ctx)
8 years ago
@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)
8 years ago
try:
keys = api.get_wg_peers(request.user.username)
except lcoreapi.APINotFoundError:
django_lcore.sync_user(request.user.vpnuser)
keys = []
8 years ago
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)