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.
569 lines
19 KiB
Python
569 lines
19 KiB
Python
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)
|
|
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)
|
|
|
|
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.save()
|
|
django_lcore.sync_user(user.vpnuser, True)
|
|
|
|
user.backend = 'django.contrib.auth.backends.ModelBackend'
|
|
auth.login(request, user)
|
|
|
|
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_ipv4"],
|
|
"ipv6": gw["main_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 = 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)
|
|
django_lcore.sync_user(request.user.vpnuser)
|
|
messages.success(request, _("OK! Password changed."))
|
|
request.user.save()
|
|
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': _("Status"),
|
|
'n_users': VPNUser.objects.filter(expiration__gte=timezone.now()).count(),
|
|
'n_sess': django_lcore.count_active_sessions(),
|
|
'n_gws': sum(len(l['servers']) for cc, l in locations),
|
|
'n_countries': len(set(cc for cc, l in locations)),
|
|
'total_bw': sum(l['bandwidth'] for cc, l in locations),
|
|
'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)
|
|
|