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.
464 lines
14 KiB
Python
464 lines
14 KiB
Python
import requests
|
|
import io
|
|
import zipfile
|
|
import hmac
|
|
import base64
|
|
from hashlib import sha256
|
|
from urllib.parse import urlencode, parse_qsl
|
|
from datetime import timedelta, datetime
|
|
|
|
from django.http import (
|
|
HttpResponse, JsonResponse,
|
|
HttpResponseRedirect,
|
|
HttpResponseNotFound, HttpResponseForbidden
|
|
)
|
|
from django.shortcuts import render, redirect
|
|
from django.contrib.auth import authenticate
|
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
from django.contrib.admin.sites import site
|
|
from django.contrib import messages
|
|
from django.utils.translation import ugettext as _
|
|
from django.utils import timezone
|
|
from django.conf import settings as project_settings
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.db.models import Count
|
|
from django.contrib import auth
|
|
from django.contrib.auth.models import User
|
|
from django_countries import countries
|
|
import lcoreapi
|
|
|
|
from ccvpn.common import get_client_ip
|
|
from payments.models import ACTIVE_BACKENDS
|
|
from .forms import SignupForm, ReqEmailForm
|
|
from .models import GiftCode, VPNUser
|
|
from .core import core_api
|
|
from . import core
|
|
from . import graphs
|
|
from . import openvpn
|
|
|
|
|
|
def get_locations():
|
|
""" Pretty bad thing that returns get_locations() with translated stuff
|
|
that depends on the request
|
|
"""
|
|
countries_d = dict(countries)
|
|
locations = core.get_locations()
|
|
for k, v in locations:
|
|
cc = v['country_code'].upper()
|
|
v['country_name'] = countries_d.get(cc, cc)
|
|
return locations
|
|
|
|
|
|
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()
|
|
|
|
if core.VPN_AUTH_STORAGE == 'core':
|
|
core.create_user(form.cleaned_data['username'], form.cleaned_data['password'])
|
|
|
|
try:
|
|
user.vpnuser.referrer = User.objects.get(id=request.session.get('referrer'))
|
|
except User.DoesNotExist:
|
|
pass
|
|
|
|
user.vpnuser.save()
|
|
|
|
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) * project_settings.PAYMENTS_MONTHLY_PRICE / 100
|
|
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_id),
|
|
subscr_backends=sorted((b for b in ACTIVE_BACKENDS.values()
|
|
if b.backend_has_recurring),
|
|
key=lambda x: x.backend_id),
|
|
default_backend='paypal',
|
|
recaptcha_site_key=project_settings.RECAPTCHA_SITE_KEY,
|
|
price=price_fn(),
|
|
)
|
|
return render(request, 'lambdainst/account.html', context)
|
|
|
|
|
|
def captcha_test(grr, request):
|
|
api_url = project_settings.RECAPTCHA_API
|
|
|
|
if api_url == 'TEST' and grr == 'TEST-TOKEN':
|
|
# FIXME: i'm sorry.
|
|
return True
|
|
|
|
data = dict(secret=project_settings.RECAPTCHA_SECRET_KEY,
|
|
remoteip=get_client_ip(request),
|
|
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')
|
|
|
|
|
|
@login_required
|
|
def settings(request):
|
|
if request.method != 'POST':
|
|
return render(request, 'lambdainst/settings.html')
|
|
|
|
pw = request.POST.get('password')
|
|
pw2 = request.POST.get('password2')
|
|
if pw and pw2:
|
|
if pw != pw2:
|
|
messages.error(request, _("Passwords do not match"))
|
|
else:
|
|
request.user.set_password(pw)
|
|
|
|
if core.VPN_AUTH_STORAGE == 'core':
|
|
core.update_user_password(request.user, pw)
|
|
|
|
messages.success(request, _("OK!"))
|
|
|
|
email = request.POST.get('email')
|
|
if email:
|
|
request.user.email = email
|
|
else:
|
|
request.user.email = ''
|
|
|
|
request.user.save()
|
|
|
|
return render(request, 'lambdainst/settings.html', dict(title=_("Settings")))
|
|
|
|
|
|
@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 logs(request):
|
|
page_size = 20
|
|
page = int(request.GET.get('page', 0))
|
|
offset = page * page_size
|
|
|
|
base = core_api.info['current_instance']
|
|
path = '/users/' + request.user.username + '/sessions/'
|
|
try:
|
|
l = core_api.get(base + path, offset=offset, limit=page_size)
|
|
total_count = l['total_count']
|
|
items = l['items']
|
|
except lcoreapi.APINotFoundError:
|
|
total_count = 0
|
|
items = []
|
|
return render(request, 'lambdainst/logs.html', {
|
|
'sessions': items,
|
|
'page': page,
|
|
'prev': page - 1 if page > 0 else None,
|
|
'next': page + 1 if offset + page_size < total_count else None,
|
|
'last_page': total_count // page_size,
|
|
'title': _("Logs"),
|
|
})
|
|
|
|
|
|
@login_required
|
|
def config(request):
|
|
return render(request, 'lambdainst/config.html', dict(
|
|
title=_("Config"),
|
|
config_os=openvpn.CONFIG_OS,
|
|
config_countries=(c for _, c in get_locations()),
|
|
config_protocols=openvpn.PROTOCOLS,
|
|
))
|
|
|
|
|
|
@login_required
|
|
def config_dl(request):
|
|
allowed_cc = [cc for (cc, _) in get_locations()]
|
|
|
|
common_options = {
|
|
'protocol': request.GET.get('protocol'),
|
|
'os': request.GET.get('client_os'),
|
|
'http_proxy': request.GET.get('http_proxy'),
|
|
'ipv6': 'enable_ipv6' in request.GET,
|
|
}
|
|
|
|
# Should be validated since it's used in the filename
|
|
# other common options are only put in the config file
|
|
protocol = common_options['protocol']
|
|
if protocol not in ('udp', 'udpl', 'tcp'):
|
|
return HttpResponseNotFound()
|
|
|
|
location = request.GET.get('gateway')
|
|
|
|
if location == 'all':
|
|
# Multiple gateways in a zip archive
|
|
|
|
f = io.BytesIO()
|
|
z = zipfile.ZipFile(f, mode='w')
|
|
|
|
for gw_name in allowed_cc + ['random']:
|
|
filename = 'ccrypto-%s-%s.ovpn' % (gw_name, protocol)
|
|
config = openvpn.make_config(gw_name=gw_name, **common_options)
|
|
z.writestr(filename, config.encode('utf-8'))
|
|
|
|
z.close()
|
|
|
|
r = HttpResponse(content=f.getvalue(), content_type='application/zip')
|
|
r['Content-Disposition'] = 'attachment; filename="%s.zip"' % filename
|
|
return r
|
|
else:
|
|
# Single gateway
|
|
if location[3:] in allowed_cc:
|
|
gw_name = location[3:]
|
|
else:
|
|
gw_name = 'random'
|
|
filename = 'ccrypto-%s-%s.ovpn' % (gw_name, protocol)
|
|
|
|
config = openvpn.make_config(gw_name=gw_name, **common_options)
|
|
|
|
if 'plain' in request.GET:
|
|
return HttpResponse(content=config, content_type='text/plain')
|
|
else:
|
|
r = HttpResponse(content=config, content_type='application/x-openvpn-profile')
|
|
r['Content-Disposition'] = 'attachment; filename="%s.ovpn"' % filename
|
|
return r
|
|
|
|
|
|
@csrf_exempt
|
|
def api_auth(request):
|
|
if request.method != 'POST':
|
|
return HttpResponseNotFound()
|
|
|
|
if core.VPN_AUTH_STORAGE != 'inst':
|
|
return HttpResponseNotFound()
|
|
|
|
username = request.POST.get('username')
|
|
password = request.POST.get('password')
|
|
secret = request.POST.get('secret')
|
|
|
|
if secret != core.LCORE_INST_SECRET:
|
|
return HttpResponseForbidden(content="Invalid secret")
|
|
|
|
user = authenticate(username=username, password=password)
|
|
if not user or not user.is_active:
|
|
return JsonResponse(dict(status='fail', message="Invalid credentials"))
|
|
|
|
if not user.vpnuser.is_paid:
|
|
return JsonResponse(dict(status='fail', message="Not allowed to connect"))
|
|
|
|
user.vpnuser.last_vpn_auth = timezone.now()
|
|
user.vpnuser.save()
|
|
|
|
return JsonResponse(dict(status='ok'))
|
|
|
|
|
|
def api_locations(request):
|
|
def format_loc(cc, l):
|
|
return {
|
|
'country_name': l['country_name'],
|
|
'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': core.current_active_sessions(),
|
|
'n_gws': sum(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)
|
|
|
|
ctx = {
|
|
'api_status': {k: str(v) for k, v in core_api.info.items()},
|
|
'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)
|
|
|
|
|
|
|
|
|
|
|