add wireguard, update lcore api

master
Alice 4 years ago
parent d51f3232a0
commit e67a61a315

@ -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):

@ -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:

@ -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),

@ -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 '<a href="%s">%s</a>' % (change_url, user.username)
return format_html('<a href="{}">{}</a>', 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 '<a href="%s">%s</a>' % (change_url, object.code.code)
code_link.allow_tags = True
return format_html('<a href="{}">{}</a>', 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 = '<a href="%s?user__id__exact=%d">%s</a>'
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 = '<a href="{}?user__id__exact={}">{}</a>'
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)

@ -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)

@ -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)

@ -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))

@ -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),
),
]

@ -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):

@ -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),

@ -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)

@ -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"

210
poetry.lock generated

@ -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"},

@ -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"]

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -0,0 +1,614 @@
/**
* @fileoverview
* - Using the 'QRCode for Javascript library'
* - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
* - this library has no dependencies.
*
* @author davidshimjs
* @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
* @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
*/
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<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=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<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
this.modules[r][6]=(r%2==0);}
for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>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<data.length){dark=(((data[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;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
if(buffer.getLengthInBits()>totalDataCount*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<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=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;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
if(r==0&&c==0){continue;}
if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
while(n>=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<<i;}
for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(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 = ['<table style="border:0;border-collapse:collapse;">'];
for (var row = 0; row < nCount; row++) {
aHTML.push('<tr>');
for (var col = 0; col < nCount; col++) {
aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
}
aHTML.push('</tr>');
}
aHTML.push('</table>');
_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;
})();

File diff suppressed because one or more lines are too long

@ -15,8 +15,14 @@
</a></li>
<li><a href="/account/config">
<i class="fa fa-download fa-fw" aria-hidden="true"></i>&nbsp;
{% trans 'Config Download' %}
{% trans 'OpenVPN Config' %}
</a></li>
{% if config.WIREGUARD %}
<li><a href="/account/wireguard">
<i class="icon icon-wireguard fa-fw" aria-hidden="true"></i>&nbsp;
{% trans 'WireGuard' %}
</a></li>
{% endif %}
<li><a href="/account/settings">
<i class="fa fa-gears fa-fw" aria-hidden="true"></i>&nbsp;
{% trans 'Settings' %}

@ -48,7 +48,7 @@
<td class="host_line">
<span class="host_name">{{ d.hostname }}</span>
</td>
<td>{{ d.servers }}</td>
<td>{{ d.servers|length }}</td>
<td>{{ d.bandwidth|bwformat }}</td>
</tr>
{% endfor %}

@ -0,0 +1,168 @@
{% extends 'account_layout.html' %}
{% load static %}
{% load i18n %}
{% block title %}WireGuard{% endblock %}
{% block account_content %}
<h2>WireGuard</h2>
<p>
{% blocktrans trimmed %}
This page lets you manage WireGuard peers.
Each can only have one concurrent connection.
{% endblocktrans %}
</p>
<div class="page-wg-title">
<h3>
{% trans "Your Devices" %} <span>{{ keys|length }}/{{ config.WIREGUARD_MAX_PEERS }}</span>
</h3>
<p>
<a href="{% url 'lambdainst:wireguard_new' %}" class="pure-button pure-button-primary"
{% if not can_create_key %}disabled{% endif %}
>{% trans "New Device" %}</a>
</p>
</div>
{% if keys %}
<table class="pure-table pure-table-bordered page-wg-table">
<thead>
<tr>
<td>{% trans "Key" %}</td>
<td>{% trans "Name" %}</td>
<td colspan="4">{% trans "Actions" %}</td>
</tr>
</thead>
{% for peer in keys %}
<tr>
<td>
<span class="wireguard-key">{{ peer.public_key }}</span>
</td>
<td class="page-wg-table__name">
{{peer.name}}
</td>
<td class="page-wg-table__action">
<a href="{% url 'django_lcore:wireguard_dl' %}?id={{peer.id}}" title="{% trans 'Download' %}">
<i class="fa fa-download"></i>
</a>
</td>
<td class="page-wg-table__action page-wg-table__action--more">
<a href="#" onclick="showWgCode(event, '{% url 'django_lcore:wireguard_dl' %}?id={{peer.id}}');" title="{% trans 'Show QR Code' %}">
<i class="fa fa-qrcode"></i>
</a>
</td>
<td class="page-wg-table__action page-wg-table__action--more">
<a href="#" onclick="toggleWgEdit(event, {{ peer.id }});" title="{% trans 'Edit' %}">
<i class="fa fa-pencil"></i>
</a>
</td>
<td class="page-wg-table__action page-wg-table__action--more">
<form action="?" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="delete_key" />
<input type="hidden" name="peer_id" value="{{ peer.id }}" />
<a href="#" onclick="deleteWgKey(event);" title="{% trans 'Delete' %}">
<i class="fa fa-trash"></i>
</a>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
<script src="{% static 'js/qrcode.min.js' %}"></script>
<script>
function showWgCode(event, url) {
event.preventDefault();
var req = new XMLHttpRequest();
var bg = document.createElement("div");
bg.classList.add("page-wg-shadow");
bg.onclick = function() {
req.abort();
bg.remove();
};
document.body.appendChild(bg);
var spinner = document.createElement("div");
spinner.classList.add("page-wg-shadow__loading");
bg.appendChild(spinner);
var el = document.getElementById("qrcode");
req.addEventListener("load", function(e) {
spinner.remove();
console.log(req.response);
new QRCode(bg, {
text: req.response,
width: 256,
height: 256,
colorDark: "#000000",
colorLight: "#ffffff",
});
});
req.open("GET", url);
req.send();
}
function deleteWgKey(event) {
event.preventDefault();
event.target.parentNode.parentNode.submit();
}
function toggleWgEdit(event, peer_id) {
event.preventDefault();
const edit_class = 'page-wg-table__name--edit';
const tr = event.target.parentNode.parentNode.parentNode;
const nameTd = tr.querySelector(".page-wg-table__name");
// press again to save (trigger form submit)
if (tr.classList.contains(edit_class)) {
const nameForm = nameTd.querySelector("form");
if (nameForm) {
nameForm.submit();
}
return;
}
function addField(form, key, value) {
const f = document.createElement("input");
f.type = "hidden";
f.name = key;
f.value = value;
form.appendChild(f);
}
const currentName = nameTd.innerText;
nameTd.innerHTML = "";
const nameForm = document.createElement("form");
nameForm.method = "post";
addField(nameForm, "peer_id", peer_id);
addField(nameForm, "action", "set_name");
addField(nameForm, "csrfmiddlewaretoken", "{{ csrf_token }}");
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.name = "name";
nameInput.value = currentName;
nameInput.maxLength = 21;
nameForm.appendChild(nameInput);
const nameSubmit = document.createElement("input");
nameSubmit.type = "submit";
nameSubmit.value = "{% trans "Save" %}";
nameForm.appendChild(nameSubmit);
nameTd.appendChild(nameForm);
}
function saveWgName(tr, name) {
}
</script>
{% endblock %}

@ -0,0 +1,47 @@
{% extends 'account_layout.html' %}
{% load static %}
{% load i18n %}
{% block title %}WireGuard - {% trans "New Device" %}{% endblock %}
{% block account_content %}
<h2>WireGuard - {% trans "New Device" %}</h2>
<form method="post" class="pure-form pure-form-aligned">
{% csrf_token %}
<input type="hidden" name="action" value="add_key" />
<fieldset>
<div class="pure-control-group">
<label for="name">{% trans 'Name' %}</label>
<input type="text" name="name" id="name"
placeholder="{% trans "Name" %}"
class="pure-input-1-2" />
<p class="inputinfo">
{% trans 'Used to identify the device in your account.' %}
</p>
</div>
<div class="pure-control-group">
<label for="public_key">{% trans 'Public Key' %}</label>
<input type="text" name="public_key" id="public_key"
placeholder="{% trans "Public key (base64)" %}"
class="pure-input-1-2" />
<p class="inputinfo">
<b>{% trans 'Optional.' %}</b>
{% trans 'Use an existing public key. Leave empty to generate a new one.' %}
</p>
</div>
<div class="pure-controls">
<label for="p_ipv6" class="pure-checkbox">
<input type="checkbox" name="enable_ipv6" id="p_ipv6" checked />
{% trans 'Enable IPv6?' %}
</label>
<input type="submit" class="pure-button pure-button-primary"
value="{% trans 'Create Key' %}" />
</div>
</fieldset>
</form>
{% endblock %}
Loading…
Cancel
Save