diff --git a/ccvpn/settings.py b/ccvpn/settings.py index c81e65e..f499cba 100644 --- a/ccvpn/settings.py +++ b/ccvpn/settings.py @@ -157,26 +157,31 @@ SECURE_HSTS_INCLUDE_SUBDOMAINS = True # OpenVPN CA Certificate - with open(BASE_DIR + '/ccvpn/ca.crt') as ca_file: OPENVPN_CA = ca_file.read() - +# HTML added before ADDITIONAL_HEADER_HTML = '' + +# HTML added before ADDITIONAL_HTML = '' -LAMBDAINST_CLUSTER_MESSAGES = {} # 'cluster_name': "No P2P" +# Custom per cluster message displayed config page +# 'cluster_name': "No P2P" +LAMBDAINST_CLUSTER_MESSAGES = {} +# Name used in ticket emails TICKETS_SITE_NAME = 'CCrypto VPN Support' -ROOT_URL = "" # Full URL to the site root +# Full URL to the site root +ROOT_URL = '' +# reCAPTCHA API details. If empty, no captcha is displayed. RECAPTCHA_API = 'https://www.google.com/recaptcha/api/siteverify' RECAPTCHA_SITE_KEY = '' RECAPTCHA_SECRET_KEY = '' -# lcore API - +# lcore API settings LCORE = dict( BASE_URL='https://core.test.lambdavpn.net/v1/', API_KEY='', @@ -185,13 +190,24 @@ LCORE = dict( CACHE_TTL=10, ) +# VPN auth credentials and expiration time storage +# - if 'core', password and expiration_date will be replicated to core and +# auth will be done from there. +# - if 'inst', both will be kept here and core should call the API here to +# authenticate users. +# 'core' is faster and doesn't depend on the instance's stability, 'inst' +# lets you generate client_config dynamically. +# /!\ don't use 'core' with unit tests for now. +VPN_AUTH_STORAGE = 'inst' + # Payment & Trial +# Payment backends. See payments/backends.py for more infos. PAYMENTS_BACKENDS = { 'paypal': { - 'TEST': True, - 'ADDRESS': 'paypal@xomg.net', # Your PayPal primary address + 'TEST': True, # Sandbox + 'ADDRESS': 'paypal@ccrypto.org', # Your PayPal primary address }, # Remove the leading '_' to enable these backends. '_stripe': { @@ -206,9 +222,9 @@ PAYMENTS_BACKENDS = { PAYMENTS_CURRENCY = ('eur', '€') PAYMENTS_MONTHLY_PRICE = 300 # in currency*100 -TRIAL_PERIOD = timedelta(hours=2) +TRIAL_PERIOD = timedelta(hours=2) # Time added on each trial awarded TRIAL_PERIOD_LIMIT = 2 # 2 * 2h = 4h, still has to push the button twice -NOTIFY_DAYS_BEFORE = (3, 1) # notify twice 3 and 1 days before expiration +NOTIFY_DAYS_BEFORE = (3, 1) # notify twice: 3 and 1 days before expiration # Local settings diff --git a/lambdainst/admin.py b/lambdainst/admin.py index c3bfdc6..faa9a04 100644 --- a/lambdainst/admin.py +++ b/lambdainst/admin.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import User from django.utils.translation import ugettext as _ from lambdainst.models import VPNUser, GiftCode, GiftCodeUser - +from . import core def make_user_link(user): change_url = resolve_url('admin:auth_user_change', user.id) @@ -52,6 +52,11 @@ class VPNUserInline(admin.StackedInline): is_paid.boolean = True is_paid.short_description = _("Is paid?") + def save_model(self, request, obj, form, change): + obj.save() + if change and 'expiration' in form.changed_data: + core.update_user_expiration(obj.user) + class GiftCodeUserAdmin(admin.TabularInline): model = GiftCodeUser @@ -104,6 +109,20 @@ class UserAdmin(UserAdmin): return s links.allow_tags = True + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + + # Notify core + if core.VPN_AUTH_STORAGE == 'core': + if change and 'is_active' in form.changed_data: + core.update_user_expiration(obj) + + def delete_model(self, request, obj): + if core.VPN_AUTH_STORAGE == 'core': + core.delete_user(obj.username) + + super().delete_model(request, obj) + class GiftCodeAdmin(admin.ModelAdmin): fields = ('code', 'time', 'created', 'created_by', 'single_use', 'free_only', diff --git a/lambdainst/core.py b/lambdainst/core.py index dab98de..4cb9678 100644 --- a/lambdainst/core.py +++ b/lambdainst/core.py @@ -23,6 +23,9 @@ if isinstance(LCORE_CACHE_TTL, int): LCORE_CACHE_TTL = timedelta(seconds=LCORE_CACHE_TTL) assert isinstance(LCORE_CACHE_TTL, timedelta) +VPN_AUTH_STORAGE = settings.VPN_AUTH_STORAGE +assert VPN_AUTH_STORAGE in ('core', 'inst') + core_api = lcoreapi.API(LCORE_API_KEY, LCORE_API_SECRET, LCORE_BASE_URL) @@ -97,3 +100,54 @@ def get_locations(): return locations +def create_user(username, cleartext_password): + """ The password will be hashed and stored safely on the core, + so we have to send it clearly here. + """ + path = core_api.info['current_instance'] + '/users/' + core_api.post(path, data={ + 'username': username, + 'password': cleartext_password, + 'expiration_date': datetime(1, 1, 1).isoformat(), # Expired. + }) + + +def update_user_expiration(user): + path = core_api.info['current_instance'] + '/users/' + user.username + + try: + if not user.is_active: + core_api.patch(path, data={ + 'expiration_date': datetime(1, 1, 1).isoformat(), # Expired. + }) + return + + core_api.patch(path, data={ + 'expiration_date': user.vpnuser.expiration, + }) + except lcoreapi.APIError: + # User can't do anything to this, we should just report it + logger = logging.getLogger('django.request') + logger.exception("core api error, missing user (exp update)") + + +def update_user_password(user, cleartext_password): + path = core_api.info['current_instance'] + '/users/' + user.username + + try: + core_api.patch(path, data={ + 'password': cleartext_password, + }) + except lcoreapi.APINotFoundError: + # This time we can try fix it! + create_user(user.username, cleartext_password) + except lcoreapi.APIError: + # and maybe fail. + logger = logging.getLogger('django.request') + logger.exception("core api error (password update)") + + +def delete_user(username): + path = core_api.info['current_instance'] + '/users/' + username + core_api.delete(path) + diff --git a/lambdainst/models.py b/lambdainst/models.py index cad186e..a50405a 100644 --- a/lambdainst/models.py +++ b/lambdainst/models.py @@ -7,6 +7,7 @@ from django.utils import timezone from django.conf import settings from django.db.models.signals import post_save from django.dispatch import receiver +from . import core assert isinstance(settings.TRIAL_PERIOD, timedelta) assert isinstance(settings.TRIAL_PERIOD_LIMIT, int) @@ -55,6 +56,10 @@ class VPNUser(models.Model): self.expiration = now self.expiration += time + # Propagate update to core + if core.VPN_AUTH_STORAGE == 'core': + core.update_user_expiration(self.user) + def give_trial_period(self): self.add_paid_time(settings.TRIAL_PERIOD) self.trial_periods_given += 1 diff --git a/lambdainst/views.py b/lambdainst/views.py index 8d31bb5..eb5226b 100644 --- a/lambdainst/views.py +++ b/lambdainst/views.py @@ -23,8 +23,8 @@ from django_countries import countries from payments.models import ACTIVE_BACKENDS from .forms import SignupForm from .models import GiftCode, VPNUser -from .core import core_api, current_active_sessions, get_locations as core_get_locations -from .core import LCORE_INST_SECRET, LCORE_SOURCE_ADDR +from .core import core_api +from . import core from . import graphs from . import openvpn @@ -34,7 +34,7 @@ def get_locations(): that depends on the request """ countries_d = dict(countries) - locations = core_get_locations() + locations = core.get_locations() for k, v in locations: cc = v['country_code'].upper() v['country_name'] = countries_d.get(cc, cc) @@ -69,6 +69,9 @@ def signup(request): 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: @@ -154,6 +157,11 @@ def settings(request): 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 @@ -269,11 +277,14 @@ 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 != LCORE_INST_SECRET: + if secret != core.LCORE_INST_SECRET: return HttpResponseForbidden(content="Invalid secret") user = authenticate(username=username, password=password) @@ -295,7 +306,7 @@ def status(request): ctx = { 'title': _("Status"), 'n_users': VPNUser.objects.filter(expiration__gte=timezone.now()).count(), - 'n_sess': current_active_sessions(), + '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),