From e67a61a3155441d391403d9c3bf8c98510c3346f Mon Sep 17 00:00:00 2001 From: Alice Date: Sat, 28 Mar 2020 03:24:47 +0100 Subject: [PATCH] add wireguard, update lcore api --- ccvpn/context_processors.py | 2 +- ccvpn/settings.py | 18 + ccvpn/urls.py | 4 +- lambdainst/admin.py | 36 +- lambdainst/core.py | 177 ----- lambdainst/forms.py | 14 + lambdainst/management/commands/core_info.py | 11 - .../migrations/0003_vpnuser_last_core_sync.py | 18 + lambdainst/models.py | 40 +- lambdainst/urls.py | 5 +- lambdainst/views.py | 234 +++---- locale/fr/LC_MESSAGES/django.po | 137 +++- poetry.lock | 210 +++--- pyproject.toml | 4 +- static/css/style.css | 103 +++ static/img/icon-wireguard.png | Bin 0 -> 9653 bytes static/js/qrcode.js | 614 ++++++++++++++++++ static/js/qrcode.min.js | 1 + templates/account_layout.html | 8 +- templates/lambdainst/status.html | 2 +- templates/lambdainst/wireguard.html | 168 +++++ templates/lambdainst/wireguard_new.html | 47 ++ 22 files changed, 1360 insertions(+), 493 deletions(-) delete mode 100644 lambdainst/core.py delete mode 100644 lambdainst/management/commands/core_info.py create mode 100644 lambdainst/migrations/0003_vpnuser_last_core_sync.py create mode 100644 static/img/icon-wireguard.png create mode 100644 static/js/qrcode.js create mode 100644 static/js/qrcode.min.js create mode 100644 templates/lambdainst/wireguard.html create mode 100644 templates/lambdainst/wireguard_new.html diff --git a/ccvpn/context_processors.py b/ccvpn/context_processors.py index 27b4a9e..365bf54 100644 --- a/ccvpn/context_processors.py +++ b/ccvpn/context_processors.py @@ -1,6 +1,6 @@ from django.conf import settings from ccvpn.common import get_client_ip -from lambdainst.core import is_vpn_gateway +from django_lcore import is_vpn_gateway def some_settings(request): diff --git a/ccvpn/settings.py b/ccvpn/settings.py index a45affb..2132ea1 100644 --- a/ccvpn/settings.py +++ b/ccvpn/settings.py @@ -42,6 +42,7 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', 'django.contrib.humanize', 'django_countries', + 'django_lcore', 'lambdainst', 'payments', 'tickets', @@ -85,6 +86,7 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'ccvpn.context_processors.some_settings', + 'constance.context_processors.config', ], }, }, @@ -278,6 +280,12 @@ CONSTANCE_CONFIG = { 'NOTIFY_DAYS_BEFORE': ("3, 1", "When to send account expiration notifications. In number of days before, separated y commas", 'integer_list'), 'KB_MAX_TOP_ENTRIES': (10, "Maximum number of categories to show in the short list"), + + 'WIREGUARD': (False, "Enable WireGuard support"), + 'WIREGUARD_MAX_PEERS': (10, "Maximum number of WireGuard peers registered per user.", int), + 'WIREGUARD_PUBKEY': ("", "WireGuard server public key"), + 'WIREGUARD_PSK': ("", "WireGuard pre-shared key (optional)"), + 'WIREGUARD_DNS': ("10.128.0.1", "WireGuard DNS server IP address"), } CONSTANCE_ADDITIONAL_FIELDS = { @@ -302,6 +310,16 @@ TINYMCE_DEFAULT_CONFIG = { 'browser_spellcheck': True, } +VPN_DOMAIN = "204vpn.net" +VPN_FULL_NAME = "CCrypto VPN" +VPN_SHORT_NAME = "ccvpn" + +OPENVPN_CONFIG_HEADER = """\ +# +----------------------------+ +# | Cognitive Cryptography VPN | +# | https://vpn.ccrypto.org/ | +# +----------------------------+ +""" # Local settings try: diff --git a/ccvpn/urls.py b/ccvpn/urls.py index ae1ab24..fb052b7 100644 --- a/ccvpn/urls.py +++ b/ccvpn/urls.py @@ -2,6 +2,7 @@ from django.urls import path, include from django.contrib import admin from django.contrib.auth import views as auth_views from django.views.generic.base import RedirectView +import django_lcore from lambdainst import views as account_views @@ -11,9 +12,10 @@ urlpatterns = [ path('admin/status', account_views.admin_status, name='admin_status'), path('admin/referrers', account_views.admin_ref, name='admin_ref'), path('admin/', admin.site.urls), + path('vpn/', include(django_lcore.urls)), path('api/locations', account_views.api_locations), - path('api/auth', account_views.api_auth), + path('api/auth', django_lcore.views.api_auth), path('', views.index, name='index'), path('ca.crt', account_views.ca_crt), diff --git a/lambdainst/admin.py b/lambdainst/admin.py index d885033..be091d0 100644 --- a/lambdainst/admin.py +++ b/lambdainst/admin.py @@ -2,17 +2,20 @@ import string from django.shortcuts import resolve_url from django import forms from django.contrib import admin +from django.contrib import messages from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from django.utils.translation import ugettext as _ +from django.utils.html import format_html +import django_lcore +import lcoreapi 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) - return '%s' % (change_url, user.username) + return format_html('{}', change_url, user.username) class GiftCodeAdminForm(forms.ModelForm): @@ -32,8 +35,8 @@ class VPNUserInline(admin.StackedInline): fk_name = 'user' fields = ('notes', 'expiration', 'last_expiry_notice', 'notify_expiration', - 'trial_periods_given', 'referrer_a', 'campaign', 'last_vpn_auth') - readonly_fields = ('referrer_a', 'last_vpn_auth', 'campaign') + 'trial_periods_given', 'referrer_a', 'campaign', 'last_vpn_auth', 'last_core_sync') + readonly_fields = ('referrer_a', 'last_vpn_auth', 'last_core_sync', 'campaign') def referrer_a(self, object): if not object.referrer: @@ -63,13 +66,11 @@ class GiftCodeUserAdmin(admin.TabularInline): def user_link(self, object): return make_user_link(object.user) - user_link.allow_tags = True user_link.short_description = 'User' def code_link(self, object): change_url = resolve_url('admin:lambdainst_giftcode_change', object.code.id) - return '%s' % (change_url, object.code.code) - code_link.allow_tags = True + return format_html('{}', change_url, object.code.code) code_link.short_description = 'Code' def has_add_permission(self, request): @@ -90,6 +91,7 @@ class UserAdmin(UserAdmin): 'groups', 'user_permissions')}), ) readonly_fields = ('last_login', 'date_joined', 'links') + actions = (django_lcore.core_sync_action, ) def is_paid(self, object): return object.vpnuser.is_paid @@ -97,24 +99,26 @@ class UserAdmin(UserAdmin): is_paid.short_description = _("Is paid?") def links(self, object): - fmt = '%s' payments_url = resolve_url('admin:payments_payment_changelist') tickets_url = resolve_url('admin:tickets_ticket_changelist') - s = fmt % (payments_url, object.id, "Payments") - s += ' - ' + fmt % (tickets_url, object.id, "Tickets") - return s - links.allow_tags = True + fmt = '{}' + return format_html(fmt + " - " + fmt, + payments_url, object.id, "Payments", + tickets_url, object.id, "Tickets", + ) def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) # Notify core - if change and core.VPN_AUTH_STORAGE == 'core': - core.update_user_expiration(obj) + if change: + django_lcore.sync_user(obj.vpnuser) def delete_model(self, request, obj): - if core.VPN_AUTH_STORAGE == 'core': - core.delete_user(obj.username) + try: + django_lcore.api.get_user(obj.username).delete() + except lcoreapi.APIError as e: + messages.error(request, "failed to delete vpn user: %r" % e) super().delete_model(request, obj) diff --git a/lambdainst/core.py b/lambdainst/core.py deleted file mode 100644 index ae07a35..0000000 --- a/lambdainst/core.py +++ /dev/null @@ -1,177 +0,0 @@ -from datetime import timedelta, datetime -import lcoreapi -from django.conf import settings -import logging - -cluster_messages = settings.LAMBDAINST_CLUSTER_MESSAGES - -lcore_settings = settings.LCORE - -LCORE_BASE_URL = lcore_settings.get('BASE_URL') -LCORE_API_KEY = lcore_settings['API_KEY'] -LCORE_API_SECRET = lcore_settings['API_SECRET'] -LCORE_SOURCE_ADDR = lcore_settings.get('SOURCE_ADDRESS') -LCORE_INST_SECRET = lcore_settings['INST_SECRET'] -LCORE_TIMEOUT = lcore_settings.get('TIMEOUT', 10) - -# The default is to log the exception and only raise it if we cannot show -# the previous value or a default value instead. -LCORE_RAISE_ERRORS = bool(lcore_settings.get('RAISE_ERRORS', False)) - -LCORE_CACHE_TTL = lcore_settings.get('CACHE_TTL', 60) -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, - timeout=LCORE_TIMEOUT) - - -class APICache: - """ Cache data for a time, try to update and silence errors. - Outdated data is not a problem. - """ - def __init__(self, ttl=None, initial=None): - self.cache_date = datetime.fromtimestamp(0) - self.ttl = ttl or LCORE_CACHE_TTL - - self.has_cached_value = initial is not None - self.cached = initial() if initial else None - - def query(self, wrapped, *args, **kwargs): - try: - return wrapped(*args, **kwargs) - except lcoreapi.APIError: - logger = logging.getLogger('django.request') - logger.exception("core api error") - - if LCORE_RAISE_ERRORS: - raise - - if not self.has_cached_value: - # We only return a default value if we were given one. - # Prevents returning an unexpected None. - raise - - # Return previous value - return self.cached - - def __call__(self, wrapped): - def wrapper(*args, **kwargs): - if self.cache_date > (datetime.now() - self.ttl): - return self.cached - - self.cached = self.query(wrapped, *args, **kwargs) - - # New results *and* errors are cached - self.cache_date = datetime.now() - return self.cached - return wrapper - - -@APICache(initial=lambda: 0) -def current_active_sessions(): - return core_api.get(core_api.info['current_instance'] + '/sessions', active=True)['total_count'] - - -@APICache(initial=lambda: []) -def get_locations(): - gateways = core_api.get('gateways/', enabled=True) - locations = {} - - for gw in gateways.list_iter(): - cc = gw['cluster_name'] - - if cc not in locations: - locations[cc] = dict( - servers=0, - bandwidth=0, - hostname='gw.' + cc + '.204vpn.net', - country_code=cc, - message=cluster_messages.get(cc), - ) - - locations[cc]['servers'] += 1 - locations[cc]['bandwidth'] += gw['bandwidth'] or 0 - - locations = sorted(locations.items(), key=lambda x: x[1]['country_code']) - return locations - - -@APICache(initial=lambda: []) -def get_gateway_exit_ips(): - gateways = core_api.get('gateways/', enabled=True) - ipv4_list = [] - ipv6_list = [] - - for gw in gateways.list_iter(): - ma = gw['main_addr'] - if ma.get('ipv4'): - ipv4_list.append(ma['ipv4']) - if ma.get('ipv6'): - ipv6_list.append(ma['ipv6']) - - # TODO: IPv6 support - - return ipv4_list - - -def is_vpn_gateway(ip): - addresses = get_gateway_exit_ips() - return ip in addresses - - -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/forms.py b/lambdainst/forms.py index e2aae14..c29c14e 100644 --- a/lambdainst/forms.py +++ b/lambdainst/forms.py @@ -62,3 +62,17 @@ class ReqEmailForm(forms.Form, FormPureRender): ) +def wg_pk_validator(s): + try: + data = base64.b64decode(s, validate=True) + except: + raise forms.ValidationError("Invalid public key format") + if len(data) != 32: + raise forms.ValidationError("Invalid public key length") + + +class WgPeerForm(forms.Form): + public_key = forms.CharField(min_length=3, max_length=100, strip=True, required=False, validators=[ + wg_pk_validator + ]) + name = forms.CharField(max_length=21, required=False) diff --git a/lambdainst/management/commands/core_info.py b/lambdainst/management/commands/core_info.py deleted file mode 100644 index ed4f627..0000000 --- a/lambdainst/management/commands/core_info.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.core.management.base import BaseCommand - -from lambdainst.core import core_api - - -class Command(BaseCommand): - help = "Get informations about core API" - - def handle(self, *args, **options): - for k, v in core_api.info.items(): - print("%s: %s" % (k, v)) diff --git a/lambdainst/migrations/0003_vpnuser_last_core_sync.py b/lambdainst/migrations/0003_vpnuser_last_core_sync.py new file mode 100644 index 0000000..69d05d6 --- /dev/null +++ b/lambdainst/migrations/0003_vpnuser_last_core_sync.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.8 on 2020-03-24 04:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lambdainst', '0002_vpnuser_campaign'), + ] + + operations = [ + migrations.AddField( + model_name='vpnuser', + name='last_core_sync', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/lambdainst/models.py b/lambdainst/models.py index 1cf6dde..8472110 100644 --- a/lambdainst/models.py +++ b/lambdainst/models.py @@ -3,12 +3,11 @@ from datetime import timedelta from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext as _ -from django.utils import timezone from django.db.models.signals import post_save from django.dispatch import receiver from constance import config as site_config +from django_lcore.core import LcoreUserProfileMethods, setup_sync_hooks -from . import core from ccvpn.common import get_trial_period_duration from payments.models import Subscription @@ -20,7 +19,7 @@ def random_gift_code(): return ''.join([prng.choice(charset) for n in range(10)]) -class VPNUser(models.Model): +class VPNUser(models.Model, LcoreUserProfileMethods): class Meta: verbose_name = _("VPN User") verbose_name_plural = _("VPN Users") @@ -35,45 +34,13 @@ class VPNUser(models.Model): trial_periods_given = models.IntegerField(default=0) last_vpn_auth = models.DateTimeField(blank=True, null=True) + last_core_sync = models.DateTimeField(blank=True, null=True) referrer = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL, related_name='referrals') referrer_used = models.BooleanField(default=False) campaign = models.CharField(blank=True, null=True, max_length=64) - @property - def is_paid(self): - if self.get_subscription(): - return True - if not self.expiration: - return False - return self.expiration > timezone.now() - - @property - def time_left(self): - return self.expiration - timezone.now() - - def add_paid_time(self, time): - now = timezone.now() - if not self.expiration or self.expiration < now: - self.expiration = now - self.expiration += time - - # Propagate update to core - if core.VPN_AUTH_STORAGE == 'core': - core.update_user_expiration(self.user) - - def remove_paid_time(self, time): - now = timezone.now() - - if self.expiration < now: - return - - self.expiration -= time - - if core.VPN_AUTH_STORAGE == 'core': - core.update_user_expiration(self.user) - def give_trial_period(self): self.add_paid_time(get_trial_period_duration()) self.trial_periods_given += 1 @@ -111,6 +78,7 @@ class VPNUser(models.Model): def __str__(self): return self.user.username +setup_sync_hooks(User, VPNUser) @receiver(post_save, sender=User) def create_vpnuser(sender, instance, created, **kwargs): diff --git a/lambdainst/urls.py b/lambdainst/urls.py index 700735a..1fe8162 100644 --- a/lambdainst/urls.py +++ b/lambdainst/urls.py @@ -1,5 +1,6 @@ from django.urls import path from django.contrib.auth import views as auth_views +import django_lcore from . import views @@ -12,8 +13,10 @@ urlpatterns = [ path('signup', views.signup, name='signup'), path('settings', views.settings), - path('config_dl', views.config_dl), path('config', views.config), + path('config_dl', django_lcore.views.openvpn_dl), + path('wireguard', views.wireguard), + path('wireguard/new', views.wireguard_new, name='wireguard_new'), path('logs', views.logs), path('gift_code', views.gift_code), path('trial', views.trial), diff --git a/lambdainst/views.py b/lambdainst/views.py index 49b723b..c294c40 100644 --- a/lambdainst/views.py +++ b/lambdainst/views.py @@ -1,41 +1,34 @@ -import requests -import io -import zipfile -import hmac import base64 +import hmac +from datetime import datetime, timedelta 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 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.views.decorators.csrf import csrf_exempt -from django.db.models import Count -from django.contrib import auth +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.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 -from constance import config as site_config +import qrcode +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 +from .forms import SignupForm, ReqEmailForm, WgPeerForm from .models import GiftCode, VPNUser -from .core import core_api -from . import core from . import graphs -from . import openvpn def get_locations(): @@ -43,13 +36,21 @@ def get_locations(): 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) + 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') @@ -78,9 +79,6 @@ 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: @@ -89,6 +87,7 @@ def signup(request): 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) @@ -234,10 +233,7 @@ def settings(request): 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) - + django_lcore.sync_user(request.user.vpnuser) messages.success(request, _("OK!")) email = request.POST.get('email') @@ -274,10 +270,10 @@ def logs(request): page = int(request.GET.get('page', 0)) offset = page * page_size - base = core_api.info['current_instance'] + base = django_lcore.api.info['current_instance'] path = '/users/' + request.user.username + '/sessions/' try: - l = core_api.get(base + path, offset=offset, limit=page_size) + l = django_lcore.api.get(base + path, offset=offset, limit=page_size) total_count = l['total_count'] items = l['items'] except lcoreapi.APINotFoundError: @@ -297,105 +293,12 @@ def logs(request): def config(request): return render(request, 'lambdainst/config.html', dict( title=_("Config"), - config_os=openvpn.CONFIG_OS, + config_os=django_lcore.openvpn.CONFIG_OS, config_countries=(c for _, c in get_locations()), - config_protocols=openvpn.PROTOCOLS, + config_protocols=django_lcore.openvpn.PROTOCOLS, )) -@login_required -def config_dl(request): - allowed_cc = [cc for (cc, _) in get_locations()] - - os = request.GET.get('client_os') - - common_options = { - 'username': request.user.username, - 'protocol': request.GET.get('protocol'), - 'os': 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']: - if os == 'chromeos': - filename = 'ccrypto-%s-%s.onc' % (gw_name, protocol) - else: - 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' - if os == 'chromeos': - filename = 'ccrypto-%s-%s.onc' % (gw_name, protocol) - else: - 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: - if os == 'chromeos': - r = HttpResponse(content=config, content_type='application/x-onc') - else: - r = HttpResponse(content=config, content_type='application/x-openvpn-profile') - r['Content-Disposition'] = 'attachment; filename="%s"' % 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): msg = ' [%s]' % l['message'] if l['message'] else '' @@ -415,8 +318,8 @@ def status(request): 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_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, @@ -444,8 +347,10 @@ def admin_status(request): 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 core_api.info.items()}, + '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, } @@ -475,6 +380,59 @@ def admin_ref(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, + ) + return render(request, 'lambdainst/wireguard.html', context) + + +@login_required +def wireguard_new(request): + 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', + enabled=request.user.vpnuser.is_paid, + ) + return render(request, 'lambdainst/wireguard_new.html', context) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index c483b5b..d8be94c 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-10-23 11:55+0000\n" +"POT-Creation-Date: 2020-03-27 18:32+0000\n" "PO-Revision-Date: 2016-04-07 01:32+0000\n" "Last-Translator: \n" "Language-Team: \n" @@ -35,39 +35,39 @@ msgstr "Chat Live" msgid "Download {} v{}" msgstr "Télécharger {} v{}" -#: lambdainst/admin.py:23 +#: lambdainst/admin.py:25 msgid "Code must be [a-zA-Z0-9]" msgstr "" -#: lambdainst/admin.py:25 +#: lambdainst/admin.py:27 msgid "Code must be between 1 and 32 characters" msgstr "" -#: lambdainst/admin.py:44 +#: lambdainst/admin.py:46 msgid "(rewarded)" msgstr "" -#: lambdainst/admin.py:46 +#: lambdainst/admin.py:48 msgid "(not rewarded)" msgstr "" -#: lambdainst/admin.py:49 +#: lambdainst/admin.py:51 msgid "Referrer" msgstr "" -#: lambdainst/admin.py:54 lambdainst/admin.py:97 +#: lambdainst/admin.py:56 lambdainst/admin.py:100 msgid "Is paid?" msgstr "Est payé?" -#: lambdainst/admin.py:88 +#: lambdainst/admin.py:90 msgid "Important dates" msgstr "Dates importantes" -#: lambdainst/admin.py:89 +#: lambdainst/admin.py:91 msgid "Permissions" msgstr "Permissions" -#: lambdainst/admin.py:134 tickets/admin.py:44 +#: lambdainst/admin.py:139 tickets/admin.py:44 msgid "Comment" msgstr "Notes" @@ -106,27 +106,27 @@ msgstr "E-Mail" msgid "Passwords are not the same" msgstr "Les mots de passe de correspondent pas" -#: lambdainst/models.py:25 +#: lambdainst/models.py:24 msgid "VPN User" msgstr "VPN User" -#: lambdainst/models.py:26 +#: lambdainst/models.py:25 msgid "VPN Users" msgstr "VPN Users" -#: lambdainst/models.py:123 +#: lambdainst/models.py:91 msgid "Gift Code" msgstr "Code cadeau" -#: lambdainst/models.py:124 +#: lambdainst/models.py:92 msgid "Gift Codes" msgstr "Codes cadeau" -#: lambdainst/models.py:169 +#: lambdainst/models.py:137 msgid "Gift Code User" msgstr "Utilisateur de codes" -#: lambdainst/models.py:170 +#: lambdainst/models.py:138 msgid "Gift Code Users" msgstr "Utilisateurs de codes" @@ -213,50 +213,50 @@ msgstr "" msgid "%s Pbps" msgstr "" -#: lambdainst/views.py:157 +#: lambdainst/views.py:156 msgid "Awesome VPN! 3€ per month, with a free 7 days trial!" msgstr "" -#: lambdainst/views.py:173 templates/account_layout.html:10 +#: lambdainst/views.py:172 templates/account_layout.html:10 #: templates/lambdainst/account.html:17 msgid "Account" msgstr "Compte" -#: lambdainst/views.py:218 lambdainst/views.py:241 lambdainst/views.py:266 +#: lambdainst/views.py:217 lambdainst/views.py:237 lambdainst/views.py:262 msgid "OK!" msgstr "OK!" -#: lambdainst/views.py:220 +#: lambdainst/views.py:219 msgid "Invalid captcha" msgstr "Captcha invalide" -#: lambdainst/views.py:234 +#: lambdainst/views.py:233 msgid "Passwords do not match" msgstr "Les mots de passe ne correspondent pas" -#: lambdainst/views.py:251 templates/account_layout.html:22 +#: lambdainst/views.py:247 templates/account_layout.html:28 #: templates/lambdainst/settings.html:6 msgid "Settings" msgstr "Options" -#: lambdainst/views.py:262 +#: lambdainst/views.py:258 msgid "Gift code not found or already used." msgstr "Code inconnu ou déjà utilisé." -#: lambdainst/views.py:264 +#: lambdainst/views.py:260 msgid "Gift code only available to free accounts." msgstr "Code uniquement disponible pour les nouveaux comptes." -#: lambdainst/views.py:292 templates/account_layout.html:30 +#: lambdainst/views.py:288 templates/account_layout.html:36 #: templates/lambdainst/logs.html:6 msgid "Logs" msgstr "Logs" -#: lambdainst/views.py:299 templates/lambdainst/config.html:7 +#: lambdainst/views.py:295 templates/lambdainst/config.html:7 msgid "Config" msgstr "Config" -#: lambdainst/views.py:416 payments/backends/bitcoin.py:90 +#: lambdainst/views.py:319 payments/backends/bitcoin.py:90 #: payments/backends/bitcoin.py:92 templates/admin/index.html:53 #: templates/admin/index.html:56 templates/lambdainst/admin_ref.html:16 #: templates/lambdainst/admin_status.html:16 templates/payments/list.html:13 @@ -426,10 +426,14 @@ msgid "Overview" msgstr "Vue d'ensemble" #: templates/account_layout.html:18 -msgid "Config Download" -msgstr "Configuration" +msgid "OpenVPN Config" +msgstr "Config OpenVPN" -#: templates/account_layout.html:26 templates/payments/list.html:6 +#: templates/account_layout.html:23 +msgid "WireGuard" +msgstr "" + +#: templates/account_layout.html:32 templates/payments/list.html:6 msgid "Payments" msgstr "Paiements" @@ -657,7 +661,7 @@ msgstr "N'importe quoi entre 1 et 256 caractères." msgid "Same password." msgstr "Le même mot de passe." -#: templates/ccvpn/signup.html:33 +#: templates/ccvpn/signup.html:33 templates/lambdainst/wireguard_new.html:30 msgid "Optional." msgstr "Optionnel." @@ -845,6 +849,7 @@ msgid "Gateway" msgstr "Serveur" #: templates/lambdainst/config.html:22 +#: templates/lambdainst/wireguard_peer.html:26 msgid "Random" msgstr "Aléatoire" @@ -881,6 +886,7 @@ msgid "Requires TCP." msgstr "Nécéssite TCP." #: templates/lambdainst/config.html:56 +#: templates/lambdainst/wireguard_new.html:38 msgid "Enable IPv6?" msgstr "Activer l'IPv6?" @@ -941,6 +947,7 @@ msgid "repeat" msgstr "répétez" #: templates/lambdainst/settings.html:27 +#: templates/lambdainst/wireguard.html:158 msgid "Save" msgstr "Enregistrer" @@ -994,6 +1001,74 @@ msgstr "Nom" msgid "Servers" msgstr "Serveurs" +#: templates/lambdainst/wireguard.html:10 +msgid "" +"This page lets you manage WireGuard peers. Each can only have one concurrent " +"connection." +msgstr "" +"Gérez vos appareils et clés WireGuard. Chaque clé permet une connexion " +"simultanée." + +#: templates/lambdainst/wireguard.html:18 +msgid "Your Devices" +msgstr "Vos appareils" + +#: templates/lambdainst/wireguard.html:23 +#: templates/lambdainst/wireguard_new.html:5 +#: templates/lambdainst/wireguard_new.html:7 +msgid "New Device" +msgstr "Nouvel appareil" + +#: templates/lambdainst/wireguard.html:32 +msgid "Key" +msgstr "Clé" + +#: templates/lambdainst/wireguard.html:33 +#: templates/lambdainst/wireguard_new.html:15 +#: templates/lambdainst/wireguard_new.html:17 +msgid "Name" +msgstr "Nom" + +#: templates/lambdainst/wireguard.html:34 +msgid "Actions" +msgstr "Actions" + +#: templates/lambdainst/wireguard.html:46 +msgid "Download" +msgstr "Télécharger" + +#: templates/lambdainst/wireguard.html:51 +msgid "Show QR Code" +msgstr "Afficher le code QR" + +#: templates/lambdainst/wireguard.html:56 +msgid "Edit" +msgstr "Éditer" + +#: templates/lambdainst/wireguard.html:65 +msgid "Delete" +msgstr "Supprimer" + +#: templates/lambdainst/wireguard_new.html:20 +msgid "Used to identify the device in your account." +msgstr "Utilisé pour l'identifier dans votre compte." + +#: templates/lambdainst/wireguard_new.html:25 +msgid "Public Key" +msgstr "Clé publique" + +#: templates/lambdainst/wireguard_new.html:27 +msgid "Public key (base64)" +msgstr "Clé publique (base64)" + +#: templates/lambdainst/wireguard_new.html:31 +msgid "Use an existing public key. Leave empty to generate a new one." +msgstr "Pour générer votre propre clé privée. Laissez vide sinon." + +#: templates/lambdainst/wireguard_new.html:42 +msgid "Create Key" +msgstr "Ajouter" + #: templates/layout.html:26 msgid "Service Status" msgstr "État des services" diff --git a/poetry.lock b/poetry.lock index 7111b15..fe818a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -37,8 +37,8 @@ description = "Cross-platform colored terminal text." marker = "sys_platform == \"win32\"" name = "colorama" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.4.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" [[package]] category = "main" @@ -46,7 +46,7 @@ description = "A high-level Python Web framework that encourages rapid developme name = "django" optional = false python-versions = ">=3.5" -version = "2.2.8" +version = "2.2.11" [package.dependencies] pytz = "*" @@ -100,18 +100,35 @@ description = "JSONField for django models" name = "django-jsonfield" optional = false python-versions = "*" -version = "1.3.1" +version = "1.4.0" [package.dependencies] -Django = ">=1.9" +Django = ">=1.11" +six = "*" +[[package]] +category = "main" +description = "a reusable django app for common lambdacore instance code" +name = "django-lcore" +optional = false +python-versions = "*" +version = "1.1.0" + +[package.dependencies] +django = ">=2.2" +lcoreapi = "*" + +[package.source] +reference = "94ef8c9467d1d71c86d95439404b7c40202fa1ec" +type = "git" +url = "https://git.packetimpact.net/lvpn/django-lcore.git" [[package]] category = "main" description = "Pickled object field for Django" name = "django-picklefield" optional = false python-versions = "*" -version = "2.0" +version = "2.1.1" [package.dependencies] Django = ">=1.11" @@ -137,7 +154,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8" +version = "2.9" [[package]] category = "dev" @@ -175,13 +192,13 @@ description = "lambdacore client api" name = "lcoreapi" optional = false python-versions = "*" -version = "1.0" +version = "1.1.1" [package.dependencies] requests = "*" [package.source] -reference = "d72f38a6c3658ac856358d53f22fbddae6503ff7" +reference = "df771a7dbd1a2166a9a873539f976865f6f6a630" type = "git" url = "https://git.packetimpact.net/lvpn/lcoreapi.git" [[package]] @@ -233,8 +250,8 @@ category = "main" description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.5.2" +python-versions = ">=3.5" +version = "2.6.1" [[package]] category = "dev" @@ -250,6 +267,33 @@ colorama = "*" isort = ">=4.2.5,<5" mccabe = ">=0.6,<0.7" +[[package]] +category = "dev" +description = "A Pylint plugin to help Pylint understand the Django web framework" +name = "pylint-django" +optional = false +python-versions = "*" +version = "2.0.14" + +[package.dependencies] +pylint = ">=2.0" +pylint-plugin-utils = ">=0.5" + +[package.extras] +for_tests = ["coverage", "django-tables2", "factory-boy", "pytest"] +with_django = ["django"] + +[[package]] +category = "dev" +description = "Utilities and helpers for writing Pylint plugins" +name = "pylint-plugin-utils" +optional = false +python-versions = "*" +version = "0.6" + +[package.dependencies] +pylint = ">=1.7" + [[package]] category = "main" description = "The Swiss Army Knife of the Bitcoin protocol." @@ -283,8 +327,8 @@ category = "main" description = "YAML parser and emitter for Python" name = "pyyaml" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "5.2" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" [[package]] category = "main" @@ -292,16 +336,16 @@ description = "Python HTTP for Humans." name = "requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.22.0" +version = "2.23.0" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<3.1.0" -idna = ">=2.5,<2.9" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] [[package]] @@ -309,8 +353,8 @@ category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.13.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" [[package]] category = "main" @@ -318,7 +362,7 @@ description = "Non-validating SQL parser" name = "sqlparse" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.3.0" +version = "0.3.1" [[package]] category = "main" @@ -326,7 +370,7 @@ description = "Python bindings for the Stripe API" name = "stripe" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.41.0" +version = "2.44.0" [package.dependencies] [package.dependencies.requests] @@ -340,15 +384,15 @@ marker = "implementation_name == \"cpython\" and python_version < \"3.8\"" name = "typed-ast" optional = false python-versions = "*" -version = "1.4.0" +version = "1.4.1" [[package]] category = "main" description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -version = "1.25.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.8" [package.extras] brotli = ["brotlipy (>=0.6.0)"] @@ -364,7 +408,7 @@ python-versions = "*" version = "1.11.2" [metadata] -content-hash = "c00450bd56aa5a669d833686086c1839e5f67dd1ed271fa7a1898d88d6a9ae10" +content-hash = "6484d8b6029f1cbbeaa6013676535b59801aef7f1e9bd03bf3f06a7dd8acd37a" python-versions = "^3.5" [metadata.files] @@ -381,12 +425,12 @@ chardet = [ {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] colorama = [ - {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, - {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] django = [ - {file = "Django-2.2.8-py3-none-any.whl", hash = "sha256:fa98ec9cc9bf5d72a08ebf3654a9452e761fbb8566e3f80de199cbc15477e891"}, - {file = "Django-2.2.8.tar.gz", hash = "sha256:a4ad4f6f9c6a4b7af7e2deec8d0cbff28501852e5010d6c2dc695d3d1fae7ca0"}, + {file = "Django-2.2.11-py3-none-any.whl", hash = "sha256:b51c9c548d5c3b3ccbb133d0bebc992e8ec3f14899bce8936e6fdda6b23a1881"}, + {file = "Django-2.2.11.tar.gz", hash = "sha256:65e2387e6bde531d3bb803244a2b74e0253550a9612c64a60c8c5be267b30f50"}, ] django-admin-list-filter-dropdown = [ {file = "django-admin-list-filter-dropdown-1.0.3.tar.gz", hash = "sha256:07cd37b6a9be1b08f11d4a92957c69b67bc70b1f87a2a7d4ae886c93ea51eb53"}, @@ -401,20 +445,21 @@ django-countries = [ {file = "django_countries-5.3.3-py2.py3-none-any.whl", hash = "sha256:e4eaaec9bddb9365365109f833d1fd0ecc0cfee3348bf5441c0ccefb2d6917cd"}, ] django-jsonfield = [ - {file = "django-jsonfield-1.3.1.tar.gz", hash = "sha256:df2a0cefa4daeb56b074cf178b59ced0d1b4f31e6bbfdfb488755507eabfbf93"}, - {file = "django_jsonfield-1.3.1-py2.py3-none-any.whl", hash = "sha256:431caef3d6a93ad2b1844d26520cd104c474c7dd9dacf3dcb2f72888bf17e284"}, + {file = "django-jsonfield-1.4.0.tar.gz", hash = "sha256:9b3dac9f7352a6d37e9cfe0126c5b58ac7abf1cb20c0da294a00269a725223f1"}, + {file = "django_jsonfield-1.4.0-py2.py3-none-any.whl", hash = "sha256:7bdd0ea75ad842b9e33decdf343398c91fbb7bd664fde0648ef83e78b0453b6e"}, ] +django-lcore = [] django-picklefield = [ - {file = "django-picklefield-2.0.tar.gz", hash = "sha256:f1733a8db1b6046c0d7d738e785f9875aa3c198215de11993463a9339aa4ea24"}, - {file = "django_picklefield-2.0-py2.py3-none-any.whl", hash = "sha256:9052f2dcf4882c683ce87b4356f29b4d014c0dad645b6906baf9f09571f52bc8"}, + {file = "django-picklefield-2.1.1.tar.gz", hash = "sha256:67a5e156343e3b032cac2f65565f0faa81635a99c7da74b0f07a0f5db467b646"}, + {file = "django_picklefield-2.1.1-py2.py3-none-any.whl", hash = "sha256:e03cb181b7161af38ad6b573af127e4fe9b7cc2c455b42c1ec43eaad525ade0a"}, ] django-tinymce4-lite = [ {file = "django-tinymce4-lite-1.7.5.tar.gz", hash = "sha256:f0958117ddacc72596e80746729e02a727264413ab54b799f3b697a44e054e87"}, {file = "django_tinymce4_lite-1.7.5-py2.py3-none-any.whl", hash = "sha256:e6776bc5b2c7237705fea18668574bc1c4dff36babc90c99a2bb7b5d636eb5e8"}, ] idna = [ - {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, - {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, + {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, + {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, ] isort = [ {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, @@ -494,13 +539,21 @@ pygal = [ {file = "pygal-2.4.0.tar.gz", hash = "sha256:9204f05380b02a8a32f9bf99d310b51aa2a932cba5b369f7a4dc3705f0a4ce83"}, ] pygments = [ - {file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, - {file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, + {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, ] pylint = [ {file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"}, {file = "pylint-2.4.4.tar.gz", hash = "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd"}, ] +pylint-django = [ + {file = "pylint-django-2.0.14.tar.gz", hash = "sha256:c9bbcff6b87ee8466fae274fd7aae3d2d3d4c4d1ea20c48cbce673e837e36048"}, + {file = "pylint_django-2.0.14-py3-none-any.whl", hash = "sha256:3a4cc19dd6301fc2d36c9fb6e15163001a6d12723c1f7f8c2249223c2a8c68f0"}, +] +pylint-plugin-utils = [ + {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, + {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, +] python-bitcoinlib = [ {file = "python-bitcoinlib-0.10.2.tar.gz", hash = "sha256:bdb270ded594b8dead58fd6830ad14f880c25ec1fd2ca1be24e9e85decefce04"}, {file = "python_bitcoinlib-0.10.2-py2-none-any.whl", hash = "sha256:5df2777f4b47dc4f3e63ee02fd92ac3cc06d79a9fd6a2284c8d345cd8e750d25"}, @@ -515,59 +568,60 @@ pytz = [ {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, ] pyyaml = [ - {file = "PyYAML-5.2-cp27-cp27m-win32.whl", hash = "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc"}, - {file = "PyYAML-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"}, - {file = "PyYAML-5.2-cp35-cp35m-win32.whl", hash = "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15"}, - {file = "PyYAML-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075"}, - {file = "PyYAML-5.2-cp36-cp36m-win32.whl", hash = "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31"}, - {file = "PyYAML-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc"}, - {file = "PyYAML-5.2-cp37-cp37m-win32.whl", hash = "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04"}, - {file = "PyYAML-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd"}, - {file = "PyYAML-5.2-cp38-cp38-win32.whl", hash = "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f"}, - {file = "PyYAML-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803"}, - {file = "PyYAML-5.2.tar.gz", hash = "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] requests = [ - {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, - {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, + {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, + {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, ] six = [ - {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, - {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, ] sqlparse = [ - {file = "sqlparse-0.3.0-py2.py3-none-any.whl", hash = "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177"}, - {file = "sqlparse-0.3.0.tar.gz", hash = "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"}, + {file = "sqlparse-0.3.1-py2.py3-none-any.whl", hash = "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e"}, + {file = "sqlparse-0.3.1.tar.gz", hash = "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"}, ] stripe = [ - {file = "stripe-2.41.0-py2.py3-none-any.whl", hash = "sha256:50900b01a6db4f4f8db0f02a5125db0b2d732252c7683f73187abd08b7d238f3"}, - {file = "stripe-2.41.0.tar.gz", hash = "sha256:2f0ec677136985ece9cca232f106c2a87193261cac1fe58d4e959215310a0da8"}, + {file = "stripe-2.44.0-py2.py3-none-any.whl", hash = "sha256:c16f92ae001e415340fffd73c97ec29b2e1f6591b905e8741c4ff02eab35e104"}, + {file = "stripe-2.44.0.tar.gz", hash = "sha256:b15ce50cac5961ce63873906eac1492a12ed01a6ba3606aca831a1741b724a29"}, ] typed-ast = [ - {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e"}, - {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b"}, - {file = "typed_ast-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4"}, - {file = "typed_ast-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"}, - {file = "typed_ast-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631"}, - {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233"}, - {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1"}, - {file = "typed_ast-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a"}, - {file = "typed_ast-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c"}, - {file = "typed_ast-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a"}, - {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e"}, - {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d"}, - {file = "typed_ast-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36"}, - {file = "typed_ast-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0"}, - {file = "typed_ast-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66"}, - {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2"}, - {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47"}, - {file = "typed_ast-1.4.0-cp38-cp38-win32.whl", hash = "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161"}, - {file = "typed_ast-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e"}, - {file = "typed_ast-1.4.0.tar.gz", hash = "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] urllib3 = [ - {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, - {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, + {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, + {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, ] wrapt = [ {file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"}, diff --git a/pyproject.toml b/pyproject.toml index 440aac0..e203c75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,15 +16,17 @@ pytz = "^2019.1" python-bitcoinlib = "^0.10.1" stripe = "^2.24" django-constance = {version = "=2.4",extras = ["database"]} -lcoreapi = {git = "https://git.packetimpact.net/lvpn/lcoreapi.git"} +lcoreapi = {git = "https://git.packetimpact.net/lvpn/lcoreapi.git", tag = "v1.1.1"} pygments = "^2.3" psycopg2-binary = "^2.8" python-frontmatter = "^0.4.5" django-tinymce4-lite = "^1.7" django-admin-list-filter-dropdown = "^1.0" +django-lcore = {git = "https://git.packetimpact.net/lvpn/django-lcore.git", tag = "v1.1.0"} [tool.poetry.dev-dependencies] pylint = "^2.3" +pylint-django = "^2.0.14" [build-system] requires = ["poetry>=0.12"] diff --git a/static/css/style.css b/static/css/style.css index dc8b741..8d03622 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -725,6 +725,87 @@ a.home-signup-button { } +/***************************************************/ +/********************* WireGuard */ + +.wireguard-peer-actions a { + margin: 0 0.25em; +} + +.wireguard-qr { + text-align: center; +} +.wireguard-qr img { + width: 305px; +} + +.wireguard-peer-info td { + padding: 0.25em 0.5em; +} + +.wireguard-key { + font-family: monospace; + font-size: 1.1em; + background: #ddd; + padding: 5px 3px 0 3px; +} + +.wireguard-list-header h3 { + display: inline-block; +} +.wireguard-list-header a { + float: right; +} + + +.page-wg-title { + display: flex; + flex-direction: row; +} +.page-wg-title h3 { + flex-grow: 1; +} +.page-wg-title p { + margin: 0; +} +.page-wg-table { + width: 100%; + margin-top: 0.5em; +} +.page-wg-table__action { + text-align: center; +} + +.page-wg-shadow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.75); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} +.page-wg-shadow__loading { + width: 100%; + height: 100%; + background: url("../img/spinner.gif") center no-repeat; +} +.page-wg-shadow canvas, .page-wg-shadow img { + border: 3px solid white; +} + +td.page-wg-table__action { + padding: 0.5em 0.27em; +} +td.page-wg-table__action--more { + border-left: 0; +} + + + /***************************************************/ /********************* Tickets */ @@ -879,6 +960,28 @@ div.ticket-message-private { +/***************************************************/ +/********************* Icons */ + +.icon { + display: inline-block; + background-position: center; + background-size: contain; + background-repeat: no-repeat; +} +.icon-wireguard { background-image: url(../img/icon-wireguard.png); } + +.icon-wireguard { + /* fix for wireguard at least */ + height: 0.9em; + position: relative; + bottom: -1px; +} + + + + + @media screen and (max-width: 64em) { .content { padding: 0em 1em 3em 1em; diff --git a/static/img/icon-wireguard.png b/static/img/icon-wireguard.png new file mode 100644 index 0000000000000000000000000000000000000000..8557fccb638a4045720e38d57e87cb8e4a241ebb GIT binary patch literal 9653 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://www.d-project.com/ + * @see http://jeromeetienne.github.com/jquery-qrcode/ + */ +var QRCode; + +(function () { + //--------------------------------------------------------------------- + // QRCode for JavaScript + // + // Copyright (c) 2009 Kazuhiko Arase + // + // URL: http://www.d-project.com/ + // + // Licensed under the MIT license: + // http://www.opensource.org/licenses/mit-license.php + // + // The word "QR Code" is registered trademark of + // DENSO WAVE INCORPORATED + // http://www.denso-wave.com/qrcode/faqpatent-e.html + // + //--------------------------------------------------------------------- + function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE; + this.data = data; + this.parsedData = []; + + // Added to support UTF-8 Characters + for (var i = 0, l = this.data.length; i < l; i++) { + var byteArray = []; + var code = this.data.charCodeAt(i); + + if (code > 0x10000) { + byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); + byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); + byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[3] = 0x80 | (code & 0x3F); + } else if (code > 0x800) { + byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); + byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[2] = 0x80 | (code & 0x3F); + } else if (code > 0x80) { + byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); + byteArray[1] = 0x80 | (code & 0x3F); + } else { + byteArray[0] = code; + } + + this.parsedData.push(byteArray); + } + + this.parsedData = Array.prototype.concat.apply([], this.parsedData); + + if (this.parsedData.length != this.data.length) { + this.parsedData.unshift(191); + this.parsedData.unshift(187); + this.parsedData.unshift(239); + } + } + + QR8bitByte.prototype = { + getLength: function (buffer) { + return this.parsedData.length; + }, + write: function (buffer) { + for (var i = 0, l = this.parsedData.length; i < l; i++) { + buffer.put(this.parsedData[i], 8); + } + } + }; + + function QRCodeModel(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = []; + } + + QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} + return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} + if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} + this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} + return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} + for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} + for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} + this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} + var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} + this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} + row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" + +buffer.getLengthInBits() + +">" + +totalDataCount*8 + +")");} + if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} + while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} + while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD1,8);} + return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} + var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} + return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} + return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} + return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} + for(var row=0;row=256){n-=255;} + return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} + if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} + this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; + + function _isSupportCanvas() { + return typeof CanvasRenderingContext2D != "undefined"; + } + + // android 2.x doesn't support Data-URI spec + function _getAndroid() { + var android = false; + var sAgent = navigator.userAgent; + + if (/android/i.test(sAgent)) { // android + android = true; + var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); + + if (aMat && aMat[1]) { + android = parseFloat(aMat[1]); + } + } + + return android; + } + + var svgDrawer = (function() { + + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + + this.clear(); + + function makeSVG(tag, attrs) { + var el = document.createElementNS('http://www.w3.org/2000/svg', tag); + for (var k in attrs) + if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); + return el; + } + + var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); + svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); + _el.appendChild(svg); + + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})); + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + if (oQRCode.isDark(row, col)) { + var child = makeSVG("use", {"x": String(col), "y": String(row)}); + child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") + svg.appendChild(child); + } + } + } + }; + Drawing.prototype.clear = function () { + while (this._el.hasChildNodes()) + this._el.removeChild(this._el.lastChild); + }; + return Drawing; + })(); + + var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; + + // Drawing in DOM by using Table tag + var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + var aHTML = ['']; + + for (var row = 0; row < nCount; row++) { + aHTML.push(''); + + for (var col = 0; col < nCount; col++) { + aHTML.push(''); + } + + aHTML.push(''); + } + + aHTML.push('
'); + _el.innerHTML = aHTML.join(''); + + // Fix the margin values as real size. + var elTable = _el.childNodes[0]; + var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; + var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; + + if (nLeftMarginTable > 0 && nTopMarginTable > 0) { + elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; + } + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._el.innerHTML = ''; + }; + + return Drawing; + })() : (function () { // Drawing in Canvas + function _onMakeImage() { + this._elImage.src = this._elCanvas.toDataURL("image/png"); + this._elImage.style.display = "block"; + this._elCanvas.style.display = "none"; + } + + // Android 2.1 bug workaround + // http://code.google.com/p/android/issues/detail?id=5141 + if (this._android && this._android <= 2.1) { + var factor = 1 / window.devicePixelRatio; + var drawImage = CanvasRenderingContext2D.prototype.drawImage; + CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { + if (("nodeName" in image) && /img/i.test(image.nodeName)) { + for (var i = arguments.length - 1; i >= 1; i--) { + arguments[i] = arguments[i] * factor; + } + } else if (typeof dw == "undefined") { + arguments[1] *= factor; + arguments[2] *= factor; + arguments[3] *= factor; + arguments[4] *= factor; + } + + drawImage.apply(this, arguments); + }; + } + + /** + * Check whether the user's browser supports Data URI or not + * + * @private + * @param {Function} fSuccess Occurs if it supports Data URI + * @param {Function} fFail Occurs if it doesn't support Data URI + */ + function _safeSetDataURI(fSuccess, fFail) { + var self = this; + self._fFail = fFail; + self._fSuccess = fSuccess; + + // Check it just once + if (self._bSupportDataURI === null) { + var el = document.createElement("img"); + var fOnError = function() { + self._bSupportDataURI = false; + + if (self._fFail) { + self._fFail.call(self); + } + }; + var fOnSuccess = function() { + self._bSupportDataURI = true; + + if (self._fSuccess) { + self._fSuccess.call(self); + } + }; + + el.onabort = fOnError; + el.onerror = fOnError; + el.onload = fOnSuccess; + el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. + return; + } else if (self._bSupportDataURI === true && self._fSuccess) { + self._fSuccess.call(self); + } else if (self._bSupportDataURI === false && self._fFail) { + self._fFail.call(self); + } + }; + + /** + * Drawing QRCode by using canvas + * + * @constructor + * @param {HTMLElement} el + * @param {Object} htOption QRCode Options + */ + var Drawing = function (el, htOption) { + this._bIsPainted = false; + this._android = _getAndroid(); + + this._htOption = htOption; + this._elCanvas = document.createElement("canvas"); + this._elCanvas.width = htOption.width; + this._elCanvas.height = htOption.height; + el.appendChild(this._elCanvas); + this._el = el; + this._oContext = this._elCanvas.getContext("2d"); + this._bIsPainted = false; + this._elImage = document.createElement("img"); + this._elImage.alt = "Scan me!"; + this._elImage.style.display = "none"; + this._el.appendChild(this._elImage); + this._bSupportDataURI = null; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _elImage = this._elImage; + var _oContext = this._oContext; + var _htOption = this._htOption; + + var nCount = oQRCode.getModuleCount(); + var nWidth = _htOption.width / nCount; + var nHeight = _htOption.height / nCount; + var nRoundedWidth = Math.round(nWidth); + var nRoundedHeight = Math.round(nHeight); + + _elImage.style.display = "none"; + this.clear(); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + var bIsDark = oQRCode.isDark(row, col); + var nLeft = col * nWidth; + var nTop = row * nHeight; + _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.lineWidth = 1; + _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.fillRect(nLeft, nTop, nWidth, nHeight); + + // 안티 앨리어싱 방지 처리 + _oContext.strokeRect( + Math.floor(nLeft) + 0.5, + Math.floor(nTop) + 0.5, + nRoundedWidth, + nRoundedHeight + ); + + _oContext.strokeRect( + Math.ceil(nLeft) - 0.5, + Math.ceil(nTop) - 0.5, + nRoundedWidth, + nRoundedHeight + ); + } + } + + this._bIsPainted = true; + }; + + /** + * Make the image from Canvas if the browser supports Data URI. + */ + Drawing.prototype.makeImage = function () { + if (this._bIsPainted) { + _safeSetDataURI.call(this, _onMakeImage); + } + }; + + /** + * Return whether the QRCode is painted or not + * + * @return {Boolean} + */ + Drawing.prototype.isPainted = function () { + return this._bIsPainted; + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); + this._bIsPainted = false; + }; + + /** + * @private + * @param {Number} nNumber + */ + Drawing.prototype.round = function (nNumber) { + if (!nNumber) { + return nNumber; + } + + return Math.floor(nNumber * 1000) / 1000; + }; + + return Drawing; + })(); + + /** + * Get the type by string length + * + * @private + * @param {String} sText + * @param {Number} nCorrectLevel + * @return {Number} type + */ + function _getTypeNumber(sText, nCorrectLevel) { + var nType = 1; + var length = _getUTF8Length(sText); + + for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { + var nLimit = 0; + + switch (nCorrectLevel) { + case QRErrorCorrectLevel.L : + nLimit = QRCodeLimitLength[i][0]; + break; + case QRErrorCorrectLevel.M : + nLimit = QRCodeLimitLength[i][1]; + break; + case QRErrorCorrectLevel.Q : + nLimit = QRCodeLimitLength[i][2]; + break; + case QRErrorCorrectLevel.H : + nLimit = QRCodeLimitLength[i][3]; + break; + } + + if (length <= nLimit) { + break; + } else { + nType++; + } + } + + if (nType > QRCodeLimitLength.length) { + throw new Error("Too long data"); + } + + return nType; + } + + function _getUTF8Length(sText) { + var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); + return replacedText.length + (replacedText.length != sText ? 3 : 0); + } + + /** + * @class QRCode + * @constructor + * @example + * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); + * + * @example + * var oQRCode = new QRCode("test", { + * text : "http://naver.com", + * width : 128, + * height : 128 + * }); + * + * oQRCode.clear(); // Clear the QRCode. + * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. + * + * @param {HTMLElement|String} el target element or 'id' attribute of element. + * @param {Object|String} vOption + * @param {String} vOption.text QRCode link data + * @param {Number} [vOption.width=256] + * @param {Number} [vOption.height=256] + * @param {String} [vOption.colorDark="#000000"] + * @param {String} [vOption.colorLight="#ffffff"] + * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] + */ + QRCode = function (el, vOption) { + this._htOption = { + width : 256, + height : 256, + typeNumber : 4, + colorDark : "#000000", + colorLight : "#ffffff", + correctLevel : QRErrorCorrectLevel.H + }; + + if (typeof vOption === 'string') { + vOption = { + text : vOption + }; + } + + // Overwrites options + if (vOption) { + for (var i in vOption) { + this._htOption[i] = vOption[i]; + } + } + + if (typeof el == "string") { + el = document.getElementById(el); + } + + if (this._htOption.useSVG) { + Drawing = svgDrawer; + } + + this._android = _getAndroid(); + this._el = el; + this._oQRCode = null; + this._oDrawing = new Drawing(this._el, this._htOption); + + if (this._htOption.text) { + this.makeCode(this._htOption.text); + } + }; + + /** + * Make the QRCode + * + * @param {String} sText link data + */ + QRCode.prototype.makeCode = function (sText) { + this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); + this._oQRCode.addData(sText); + this._oQRCode.make(); + this._el.title = sText; + this._oDrawing.draw(this._oQRCode); + this.makeImage(); + }; + + /** + * Make the Image from Canvas element + * - It occurs automatically + * - Android below 3 doesn't support Data-URI spec. + * + * @private + */ + QRCode.prototype.makeImage = function () { + if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { + this._oDrawing.makeImage(); + } + }; + + /** + * Clear the QRCode + */ + QRCode.prototype.clear = function () { + this._oDrawing.clear(); + }; + + /** + * @name QRCode.CorrectLevel + */ + QRCode.CorrectLevel = QRErrorCorrectLevel; +})(); diff --git a/static/js/qrcode.min.js b/static/js/qrcode.min.js new file mode 100644 index 0000000..993e88f --- /dev/null +++ b/static/js/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/templates/account_layout.html b/templates/account_layout.html index a4e219d..0e3a93c 100644 --- a/templates/account_layout.html +++ b/templates/account_layout.html @@ -15,8 +15,14 @@
  •   - {% trans 'Config Download' %} + {% trans 'OpenVPN Config' %}
  • + {% if config.WIREGUARD %} +
  • +   + {% trans 'WireGuard' %} +
  • + {% endif %}
  •   {% trans 'Settings' %} diff --git a/templates/lambdainst/status.html b/templates/lambdainst/status.html index ca7fba0..d0608cb 100644 --- a/templates/lambdainst/status.html +++ b/templates/lambdainst/status.html @@ -48,7 +48,7 @@ {{ d.hostname }} - {{ d.servers }} + {{ d.servers|length }} {{ d.bandwidth|bwformat }} {% endfor %} diff --git a/templates/lambdainst/wireguard.html b/templates/lambdainst/wireguard.html new file mode 100644 index 0000000..5004c8a --- /dev/null +++ b/templates/lambdainst/wireguard.html @@ -0,0 +1,168 @@ +{% extends 'account_layout.html' %} +{% load static %} +{% load i18n %} + +{% block title %}WireGuard{% endblock %} +{% block account_content %} +

    WireGuard

    + +

    + {% blocktrans trimmed %} + This page lets you manage WireGuard peers. + Each can only have one concurrent connection. + {% endblocktrans %} +

    + +
    + + {% if keys %} + + + + + + + + + {% for peer in keys %} + + + + + + + + + {% endfor %} +
    {% trans "Key" %}{% trans "Name" %}{% trans "Actions" %}
    + {{ peer.public_key }} + + {{peer.name}} + + + + + + + + + + + + + +
    + {% csrf_token %} + + + + + +
    +
    + {% endif %} + + + + +{% endblock %} diff --git a/templates/lambdainst/wireguard_new.html b/templates/lambdainst/wireguard_new.html new file mode 100644 index 0000000..ca84d24 --- /dev/null +++ b/templates/lambdainst/wireguard_new.html @@ -0,0 +1,47 @@ +{% extends 'account_layout.html' %} +{% load static %} +{% load i18n %} + +{% block title %}WireGuard - {% trans "New Device" %}{% endblock %} +{% block account_content %} +

    WireGuard - {% trans "New Device" %}

    + +
    + {% csrf_token %} + + +
    +
    + + +

    + {% trans 'Used to identify the device in your account.' %} +

    +
    + +
    + + +

    + {% trans 'Optional.' %} + {% trans 'Use an existing public key. Leave empty to generate a new one.' %} +

    +
    + +
    + + + +
    +
    +
    +{% endblock %} +