Initial commit

master
Alice 9 years ago
commit 2f7deaae2d

91
.gitignore vendored

@ -0,0 +1,91 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
ccvpn/local_settings.py

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 PacketImpact
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,40 @@
CCrypto VPN
===========
CCVPN is the software we use at CCrypto to provide our VPN.
You can see it live at https://vpn.ccrypto.org/
It handles user management, support tickets, billing and is used as a backend
for VPN authentication.
It communicates with an external service, lambdacore, that manages VPN servers
and sessions.
CCrypto's commercial support *does not* include this product and will not help you set it up.
Feel free to contact us about ccvpn, but with no guarantee.
[PacketImpact](https://packetimpact.net/) however may provide you commercial support
and more services about ccvpn and lambdacore.
Getting Started
---------------
```bash
git clone https://github.com/CCrypto/ccvpn3.git
cd ccvpn3/
./manage.py createsuperuser
./manage.py runserver
```
CRON
----
For bitcoin payments, you will need to run a script regularly to check for
verified transaction. Another to delete old cancelled payments.
And another to send expiration emails.
*/5 * * * * /home/vpn/ccvpn3/manage.py check_btc_payments
0 0 * * * /home/vpn/ccvpn3/manage.py expire_payments
0 */6 * * * /home/vpn/ccvpn3/manage.py expire_notify

@ -0,0 +1,39 @@
-----BEGIN CERTIFICATE-----
MIIG0zCCBLugAwIBAgIJAOOv2BdszSOVMA0GCSqGSIb3DQEBBQUAMIGhMQswCQYD
VQQGEwJGUjERMA8GA1UEBxMIU29tZUNpdHkxHzAdBgNVBAoTFkNvZ25pdGl2ZSBD
cnlwdG9ncmFwaHkxEzARBgNVBAsTCkNDcnlwdG9WUE4xEzARBgNVBAMTCkNDcnlw
dG9WUE4xEzARBgNVBCkTCkNDcnlwdG9WUE4xHzAdBgkqhkiG9w0BCQEWEGNlcnRA
Y2NyeXB0by5vcmcwHhcNMTMwODEzMTgxOTQ4WhcNMjMwODExMTgxOTQ4WjCBoTEL
MAkGA1UEBhMCRlIxETAPBgNVBAcTCFNvbWVDaXR5MR8wHQYDVQQKExZDb2duaXRp
dmUgQ3J5cHRvZ3JhcGh5MRMwEQYDVQQLEwpDQ3J5cHRvVlBOMRMwEQYDVQQDEwpD
Q3J5cHRvVlBOMRMwEQYDVQQpEwpDQ3J5cHRvVlBOMR8wHQYJKoZIhvcNAQkBFhBj
ZXJ0QGNjcnlwdG8ub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
xvkZj62nUvSjEPs1qBokLd8bBpBlLj6RGgJpfPqS/kKF0s1HpcYZynIcqP6Dw/Pi
LFcTE1STzgFfcEdKLmZAH+JCFVpc9mRTXEifouBk+2j3MG9+j2GTXHCK5FMkcJWQ
o4YihO2UOLz8qz4yn3dmy0zP1UmqxB2SayYXhwT2+pDSTkBCP6YtRURVVNIVRM7A
72hBUJ2dUgKHMTsBJSQj/11rRJ6wW6yUt0NtEcDUbdgrq0BibHq8zzCkl3vGl20M
2UCXPNKavP1aapoDGWLSxZgJ9nkFUfWbjWjHuuBw8cjQ7OV7SLiBXqLNJdcCshDA
OlwaPS4atao73rliE8bWAsGiwJZ+WXnGNJAr/6BwEtOizc6hr92S8lHOrlEWz3/F
h2+L/GI97KMM+pfxlTd8j4dbBpDIXv2vlpYOQ97EbYSbWv7fmYZ2BxxgljATBPfA
hA/y7GEfTodC/mkyZO8R2joBxcbQRu7AjsL30AOiE0GepUQNqhlbhEePvm28C9Rn
OHm4zaqQLo3BJzlP23N9sn4cMZYMiPnx+eCDv+UW7Y+xHVz0GSjlO1IkZ5lTu2fR
IbONfLYDGcByOBaLmo3oD60grw552COTAYDMlu2h8zQV0gPhJziO/txDapoBuNeX
XUdzzp1l6GFTsqIKs6ATleJ4n/S4OQe/kicZkCdYZ3kCAwEAAaOCAQowggEGMB0G
A1UdDgQWBBTWsE0h2/8fw8SbrhvXny8aBL/KTTCB1gYDVR0jBIHOMIHLgBTWsE0h
2/8fw8SbrhvXny8aBL/KTaGBp6SBpDCBoTELMAkGA1UEBhMCRlIxETAPBgNVBAcT
CFNvbWVDaXR5MR8wHQYDVQQKExZDb2duaXRpdmUgQ3J5cHRvZ3JhcGh5MRMwEQYD
VQQLEwpDQ3J5cHRvVlBOMRMwEQYDVQQDEwpDQ3J5cHRvVlBOMRMwEQYDVQQpEwpD
Q3J5cHRvVlBOMR8wHQYJKoZIhvcNAQkBFhBjZXJ0QGNjcnlwdG8ub3JnggkA46/Y
F2zNI5UwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAgEAMQsLBf8TPFkh
/cnsh+E2N5YCqoeiYwLfPlqo3DwVaDoD8w5NovbIj+O6Z31FkTbt2Zotky6BQMCz
5wfuPa1WPuU2kARqS+4LA8FOd1tH4+xqGE6lgkqQuKGJEhi4J4wBg0yKvS4GxZ0+
ZXw+fWZSGDPR8WlKVfAR6DEuPIgJqIo0hIWbMgKsmqDzxevWg+Y8C9X0VjLwaSX+
dcC9N+ztYFKkzP4yOba8vFjYBmAYzaxwloUmm+iaTD/jrik9kqjd6grNh8bQa/0N
hzoa0UL8qgnorusvzo85B/I+cavS37djAeoZ1Oilerp39HS5yjvtbI/ijKW6Wp0l
aYCQ6e9f6Ka9cMfVwDJ+aow3rp0MJG0ilP+mS+ouE/R4tnAfVXrlh5X7tsGanByu
LtXIbRrLGcCmmNLtdZ2lRVi3BRRKQu5G/6WkvjslBU27CcvKnTtT7FmtKg9564rB
NCRawVf3veelwmVdnmkKd6Ka05ymJrm3ZU03+41ilSiqecFRFqz/+urdYkmynJtq
E4hNy013h6rooKnNXrMDMuTJqPTa0xJTM+74+JQwEZETSW7TdAGVrqRnJXdXn8Zi
17Gr351BJNOBWvyk8MPBKLM24e0qX+4NykQCT8Whhj4YQ/wzvwx2ayKAfoJYd74s
9ZCg4dyZMObBjkw08am0H6W6pe9umaY=
-----END CERTIFICATE-----

@ -0,0 +1,9 @@
from django.conf import settings
def some_settings(request):
return {
'ROOT_URL': settings.ROOT_URL,
'ADDITIONAL_HTML': settings.ADDITIONAL_HTML,
'ADDITIONAL_HEADER_HTML': settings.ADDITIONAL_HEADER_HTML,
}

@ -0,0 +1,48 @@
import hashlib
import binascii
from collections import OrderedDict
from django.contrib.auth.hashers import BasePasswordHasher
from django.utils.translation import ugettext as _
class LegacyPasswordHasher(BasePasswordHasher):
""" Legacy password hasher.
Single SHA512 iteration with a 32 bytes salt.
It's wrong and should not be used except for backward compatibility.
CCVPN2 had it in a binary form, it must be base64-encoded and appened
to "legacy_sha512$" during the migration.
"""
algorithm = "legacy_sha512"
def encode(self, password, salt):
assert password is not None
if isinstance(password, str):
password = bytes(password, 'utf-8')
if isinstance(salt, str):
salt = bytes(salt, 'utf-8')
hash = hashlib.sha512(salt + password)
return "%s$%s%s" % (self.algorithm, binascii.b2a_hex(salt).decode('utf-8'),
hash.hexdigest())
def verify(self, password, encoded):
algorithm, rest = encoded.split('$', 1)
assert algorithm == self.algorithm
binary = binascii.a2b_hex(rest)
encoded_2 = self.encode(password, binary[:32])
return encoded == encoded_2
def safe_summary(self, encoded):
algorithm, hash = encoded.split('$', 1)
assert algorithm == self.algorithm
return OrderedDict([
(_('algorithm'), algorithm),
(_('salt'), hash[0:8]),
(_('hash'), hash[64:72]),
])
def must_update(self, encoded):
return True # "legacy"

@ -0,0 +1,173 @@
"""
Django settings for ccvpn project.
Generated by 'django-admin startproject' using Django 1.8.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '@4+zlzju0(wymvatr%8uguuc-aeap8yaz$269ftloqhd&vm%c4'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_countries',
'lambdainst',
'payments',
'tickets',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.common.BrokenLinkEmailsMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.middleware.locale.LocaleMiddleware',
'lambdainst.middleware.ReferrerMiddleware',
)
ROOT_URLCONF = 'ccvpn.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates/'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.template.context_processors.i18n',
'django.template.context_processors.static',
'django.template.context_processors.csrf',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'ccvpn.context_processors.some_settings',
],
},
},
]
WSGI_APPLICATION = 'ccvpn.wsgi.application'
LOGIN_URL = 'account:login'
LOGOUT_URL = 'account:logout'
LOGIN_REDIRECT_URL = 'ccvpn.views.index'
LOGOUT_REDIRECT_URL = 'ccvpn.views.index'
PAGES_DIR = BASE_DIR + '/pages/'
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'ccvpn.passwords.LegacyPasswordHasher',
]
# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'en'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = (
('fr', "French"),
('en', "English"),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale/'),
)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
# Security
X_FRAME_OPTIONS = 'SAMEORIGIN'
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
CSRF_COOKIE_HTTPONLY = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_SSL_REDIRECT = False
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
# OpenVPN CA Certificate
with open(BASE_DIR + '/ccvpn/ca.crt') as ca_file:
OPENVPN_CA = ca_file.read()
ADDITIONAL_HEADER_HTML = ''
ADDITIONAL_HTML = ''
LAMBDAINST_CLUSTER_MESSAGES = {}
# Local settings
try:
from .local_settings import * # noqa
except ImportError:
pass

@ -0,0 +1,38 @@
from django.conf.urls import include, url
from django.contrib import admin
from django.contrib.auth import views as auth_views
from . import views
from lambdainst import urls as account_urls, views as account_views
from payments import urls as payments_urls
from tickets import urls as tickets_urls
urlpatterns = [
url(r'^admin/status$', account_views.admin_status, name='admin_status'),
url(r'^admin/referrers$', account_views.admin_ref, name='admin_ref'),
url(r'^admin/', include(admin.site.urls)),
url(r'^api/auth$', account_views.api_auth),
url(r'^$', views.index, name='index'),
url(r'^ca.crt$', account_views.ca_crt),
url(r'^setlang$', views.set_lang, name='set_lang'),
url(r'^chat$', views.chat, name='chat'),
url(r'^page/(?P<name>[a-zA-Z0-9_-]+)$', views.page, name='page'),
url(r'^status$', account_views.status),
url(r'^account/forgot$', auth_views.password_reset,
{}, name='password_reset'),
url(r'^account/forgot_done$', auth_views.password_reset_done,
name='password_reset_done'),
url(r'^account/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
auth_views.password_reset_confirm, name='password_reset_confirm'),
url(r'^account/reset/done/$', auth_views.password_reset_complete,
name='password_reset_complete'),
url(r'^account/', include(account_urls, namespace='account')),
url(r'^payments/', include(payments_urls, namespace='payments')),
url(r'^tickets/', include(tickets_urls, namespace='tickets')),
]

@ -0,0 +1,82 @@
import os.path
import markdown
from django.http import HttpResponseNotFound
from django.shortcuts import render
from django.conf import settings
from django.utils.translation import ugettext as _, get_language
from django import http
from django.utils.http import is_safe_url
from django.utils.translation import (
LANGUAGE_SESSION_KEY, check_for_language,
)
md = markdown.Markdown(extensions=['toc', 'meta', 'codehilite(noclasses=True)'])
def index(request):
eur = '%.2f' % (settings.PAYMENTS_MONTHLY_PRICE / 100)
return render(request, 'ccvpn/index.html', dict(eur_price=eur))
def chat(request):
if request.user.is_authenticated():
username = request.user.username + '|cc'
else:
username = "cc?"
return render(request, 'ccvpn/chat.html', dict(username=username))
def set_lang(request):
""" django.views.i18n.set_language() with GET """
next = request.GET.get('next', request.GET.get('next'))
if not is_safe_url(url=next, host=request.get_host()):
next = request.META.get('HTTP_REFERER')
if not is_safe_url(url=next, host=request.get_host()):
next = '/'
response = http.HttpResponseRedirect(next)
lang_code = request.GET.get('lang', None)
if lang_code and check_for_language(lang_code):
if hasattr(request, 'session'):
request.session[LANGUAGE_SESSION_KEY] = lang_code
else:
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code,
max_age=settings.LANGUAGE_COOKIE_AGE,
path=settings.LANGUAGE_COOKIE_PATH,
domain=settings.LANGUAGE_COOKIE_DOMAIN)
return response
def page(request, name):
basename = settings.PAGES_DIR + '/' + name
username = request.user.username
page_replace = {
'USERNAME': username or '[username]',
}
files = [
basename + '.' + get_language() + '.md',
basename + '.en.md',
basename + '.md',
]
for file in files:
if not os.path.isfile(file):
continue
with open(file, encoding='utf8') as fh:
page = fh.read()
for s, r in page_replace.items():
page = page.replace('{' + s + '}', r)
page = md.convert(page)
title = md.Meta.get('title', [None])[0]
ctx = dict(content=page, title=title)
return render(request, 'ccvpn/page.html', ctx)
return HttpResponseNotFound()

@ -0,0 +1,16 @@
"""
WSGI config for ccvpn project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ccvpn.settings")
application = get_wsgi_application()

@ -0,0 +1,132 @@
import string
from django.shortcuts import resolve_url
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
from lambdainst.models import VPNUser, GiftCode, GiftCodeUser
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)
class GiftCodeAdminForm(forms.ModelForm):
def clean(self):
input_code = self.cleaned_data.get('code', '')
code_charset = string.ascii_letters + string.digits
if any(c not in code_charset for c in input_code):
raise forms.ValidationError(_("Code must be [a-zA-Z0-9]"))
if not 1 <= len(input_code) <= 32:
raise forms.ValidationError(_("Code must be between 1 and 32 characters"))
return self.cleaned_data
class VPNUserInline(admin.StackedInline):
model = VPNUser
can_delete = False
fk_name = 'user'
fields = ('notes', 'expiration', 'last_expiry_notice', 'notify_expiration',
'trial_periods_given', 'referrer_a', 'last_vpn_auth')
readonly_fields = ('referrer_a', 'last_vpn_auth')
def referrer_a(self, object):
if not object.referrer:
return "-"
s = make_user_link(object.referrer) + " "
if object.referrer_used:
s += _("(rewarded)")
else:
s += _("(not rewarded)")
return s
referrer_a.allow_tags = True
referrer_a.short_description = _("Referrer")
def is_paid(self, object):
return object.is_paid
is_paid.boolean = True
is_paid.short_description = _("Is paid?")
class GiftCodeUserAdmin(admin.TabularInline):
model = GiftCodeUser
fields = ('user_link', 'code_link', 'date')
readonly_fields = ('user_link', 'code_link', 'date')
list_display = ('user', )
original = False
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
code_link.short_description = 'Code'
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
class UserAdmin(UserAdmin):
inlines = (VPNUserInline, GiftCodeUserAdmin)
list_display = ('username', 'email', 'is_staff', 'date_joined', 'is_paid')
ordering = ('-date_joined', )
fieldsets = (
(None, {'fields': ('username', 'password', 'email', 'links')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
)
readonly_fields = ('last_login', 'date_joined', 'links')
def is_paid(self, object):
return object.vpnuser.is_paid
is_paid.boolean = True
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
class GiftCodeAdmin(admin.ModelAdmin):
fields = ('code', 'time', 'created', 'created_by', 'single_use', 'free_only',
'available', 'comment')
readonly_fields = ('created', 'created_by')
list_display = ('code', 'time', 'comment_head', 'available')
search_fields = ('code', 'comment', 'users__username')
inlines = (GiftCodeUserAdmin,)
list_filter = ('available', 'time')
form = GiftCodeAdminForm
def comment_head(self, object):
return object.comment_head
comment_head.short_description = _("Comment")
def save_model(self, request, obj, form, change):
if not change:
obj.created_by = request.user
obj.save()
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
admin.site.register(GiftCode, GiftCodeAdmin)

@ -0,0 +1,99 @@
from datetime import timedelta, datetime
import lcoreapi
from django.conf import settings
from django.core.mail import mail_admins
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']
# 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)
core_api = lcoreapi.API(LCORE_API_KEY, LCORE_API_SECRET, LCORE_BASE_URL)
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/')
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']
locations = sorted(locations.items(), key=lambda x: x[1]['country_code'])
return locations

@ -0,0 +1,56 @@
from django import forms
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe
class FormPureRender:
def as_pure_aligned(self):
html = ''
for f in self:
html += '<div class="pure-control-group">\n'
html += str(f.label_tag()) + '\n'
html += str(f) + '\n'
if f.errors:
html += str(f.errors) + '\n'
html += '</div>\n'
return mark_safe(html)
class UserField(forms.RegexField):
def clean(self, value):
super(UserField, self).clean(value)
try:
User.objects.get(username=value)
raise forms.ValidationError(_("Username taken."))
except User.DoesNotExist:
return value
class SignupForm(forms.Form, FormPureRender):
username = UserField(
label=_("Username"), min_length=2, max_length=16, regex='^[a-zA-Z0-9_-]+$',
widget=forms.TextInput(attrs={'required': 'true',
'pattern': '[a-zA-Z0-9_-]{2,32}',
'placeholder': _("Username"),
'autofocus': 'true'})
)
password = forms.CharField(
label=_("Password"),
widget=forms.PasswordInput(attrs={'placeholder': _("Anything")})
)
password2 = forms.CharField(
label=_("Repeat"),
widget=forms.PasswordInput(attrs={'placeholder': _("Same Anything")})
)
email = forms.EmailField(
label=_("E-Mail"),
widget=forms.EmailInput(attrs={'placeholder': _("E-Mail")}),
required=False,
)
def clean_password(self):
if self.data['password'] != self.data['password2']:
raise forms.ValidationError(_("Passwords are not the same"))
return self.data['password']

@ -0,0 +1,126 @@
from datetime import timedelta, date
import pygal
from .models import User
from payments.models import BACKENDS
from payments.models import Payment
PERIOD_VERBOSE_NAME = {
'y': "per month",
'm': "per day",
}
def monthdelta(date, delta):
m = (date.month + delta) % 12
y = date.year + (date.month + delta - 1) // 12
if not m:
m = 12
d = min(date.day, [31, 29 if y % 4 == 0 and not y % 400 == 0
else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
][m - 1])
return date.replace(day=d, month=m, year=y)
def last_days(n=30):
now = date.today()
for i in range(n - 1, -1, -1):
yield now - timedelta(days=i)
def last_months(n=12):
now = date.today().replace(day=1)
for i in range(n - 1, -1, -1):
yield monthdelta(now, -i)
def time_filter_future(period, m, df):
def _filter(o):
if period == 'm':
return df(o).date() <= m
if period == 'y':
return df(o).date().replace(day=1) <= m
return _filter
def time_filter_between(period, m, df):
def _filter(o):
if period == 'm':
return df(o).year == m.year and df(o).month == m.month and df(o).day == m.day
return df(o).date() <= m and df(o).date() > (m - timedelta(days=1))
if period == 'y':
return df(o).year == m.year and df(o).month == m.month
return (df(o).date().replace(day=1) <= m and
df(o).date().replace(day=1) > (m - timedelta(days=30)))
return _filter
def users_graph(period):
chart = pygal.Line(fill=True, x_label_rotation=75, show_legend=False)
chart.title = 'Users %s' % PERIOD_VERBOSE_NAME[period]
chart.x_labels = []
values = []
gen = last_days(30) if period == 'm' else last_months(12)
users = User.objects.all()
for m in gen:
filter_ = time_filter_future(period, m, lambda o: o.date_joined)
users_filtered = filter(filter_, users)
values.append(len(list(users_filtered)))
chart.x_labels.append('%02d/%02d' % (m.month, m.day))
chart.add('Users', values)
return chart.render()
def payments_paid_graph(period):
chart = pygal.StackedBar(x_label_rotation=75, show_legend=True)
chart.x_labels = []
gen = list(last_days(30) if period == 'm' else last_months(12))
chart.title = 'Payments %s in €' % (PERIOD_VERBOSE_NAME[period])
for m in gen:
chart.x_labels.append('%02d/%02d' % (m.month, m.day))
values = dict()
for backend_id, backend in BACKENDS.items():
values = []
payments = list(Payment.objects.filter(status='confirmed', backend_id=backend_id))
for m in gen:
filter_ = time_filter_between(period, m, lambda o: o.created)
filtered = filter(filter_, payments)
values.append(sum(u.paid_amount for u in filtered) / 100)
chart.add(backend_id, values)
return chart.render()
def payments_success_graph(period):
chart = pygal.StackedBar(x_label_rotation=75, show_legend=True)
chart.x_labels = []
gen = list(last_days(30) if period == 'm' else last_months(12))
chart.title = 'Successful payments %s' % (PERIOD_VERBOSE_NAME[period])
for m in gen:
chart.x_labels.append('%02d/%02d' % (m.month, m.day))
values = dict()
for backend_id, backend in BACKENDS.items():
values = []
payments = list(Payment.objects.filter(status='confirmed', backend_id=backend_id))
for m in gen:
filter_ = time_filter_between(period, m, lambda o: o.created)
filtered = filter(filter_, payments)
values.append(sum(1 for u in filtered))
chart.add(backend_id, values)
return chart.render()

@ -0,0 +1,11 @@
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,56 @@
from django.core.management.base import BaseCommand
from datetime import timedelta
from django.db.models import Q, F
from django.conf import settings
from django.utils import timezone
from django.template.loader import get_template
from django.template import Context
from django.core.mail import send_mass_mail
from lambdainst.models import VPNUser
ROOT_URL = settings.ROOT_URL
SITE_NAME = settings.TICKETS_SITE_NAME
NOTIFY_DAYS_BEFORE = settings.NOTIFY_DAYS_BEFORE
assert isinstance(NOTIFY_DAYS_BEFORE, (list, tuple, set))
def get_next_expirations(days=3):
""" Gets users whose subscription will expire in some days """
limit_date = timezone.now() + timedelta(days=days)
users = VPNUser.objects.exclude(user__email__exact='')
users = users.filter(expiration__gt=timezone.now()) # Not expired
users = users.filter(expiration__lt=limit_date) # Expire in a few days
# Make sure we dont send the notice twice
users = users.filter(Q(last_expiry_notice__isnull=True)
| Q(expiration__gt=F('last_expiry_notice')
+ timedelta(days=days)))
return users
class Command(BaseCommand):
help = "Notify users near the end of their subscription"
def handle(self, *args, **options):
from_email = settings.DEFAULT_FROM_EMAIL
for v in NOTIFY_DAYS_BEFORE:
emails = []
qs = get_next_expirations(v)
users = list(qs)
for u in users:
ctx = Context(dict(site_name=SITE_NAME, user=u.user,
exp=u.expiration, url=ROOT_URL))
text = get_template('lambdainst/mail_expire_soon.txt').render(ctx)
emails.append(("CCVPN Expiration", text, from_email, [u.user.email]))
print("sending -%d days notify to %s ..." % (v, u.user.email))
send_mass_mail(emails)
qs.update(last_expiry_notice=timezone.now())

@ -0,0 +1,113 @@
from datetime import timedelta
from io import StringIO
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.core.mail import EmailMessage
from django.db.models import Count
from django.utils import timezone
def get_prev_month(d):
if d.month == 1:
year = d.year - 1
month = 12
else:
year = d.year
month = d.month - 1
return d.replace(month=month, year=year)
def should_bill(report, user, time_limit):
""" Determines if one user has actually paid for the current month """
# Here for consistency, should be filtered in the query
if not user.vpnuser.expiration or user.vpnuser.expiration < time_limit:
return False
# Replay payments
payments = list(user.payment_set.order_by('id').filter(status='confirmed'))
paid_expiration = None
for p in payments:
d = p.confirmed_on or p.created
paid_expiration = max(paid_expiration or d, d) + p.time
# Numbre of days paid after the start of the month
# If negative and not filtered with vpnuser.expiration, user was given time.
# If positive, user has paid for this time.
delta = paid_expiration - time_limit
if delta < timedelta():
report.write("- %s (#%d): %s\n" % (user.username, user.id, -delta))
return delta > timedelta()
class Command(BaseCommand):
help = "Generate and send a monthly usage report to ADMINS"
def handle(self, *args, **options):
addresses = settings.USAGE_REPORT_DESTINATION
def format_e(n):
return '%.2f%s' % (n / 100, settings.PAYMENTS_CURRENCY[1])
# Dates
end = timezone.now().replace(microsecond=0, second=0, minute=0, hour=0, day=5)
start = get_prev_month(end)
# Filter users
filtering_report = StringIO()
all_users = User.objects.order_by('id')
active_users = all_users.filter(vpnuser__expiration__gt=start)
paying_users = active_users.filter(payment__status='confirmed').annotate(Count('payment')).filter(payment__count__gt=0)
users = [u for u in paying_users if should_bill(filtering_report, u, start)]
# Generate report
report = "CCVPN Usage Report\n"
report += "==================\n\n"
report += "From: %s\nTo : %s\n\n" % (start, end)
keys = ('Users', 'Active', 'W/Payment', 'Selected')
values = (all_users.count(), active_users.count(), paying_users.count(), len(users))
report += " | ".join("%-10s" % s for s in keys) + "\n"
report += " | ".join("%-10s" % s for s in values) + "\n"
report += "\n"
user_cost = settings.VPN_USER_COST
total_cost = settings.VPN_USER_COST * len(users)
report += "Billed: %d * %s = %s\n" % (len(users), format_e(user_cost), format_e(total_cost))
report += "\n"
if filtering_report.getvalue():
report += "Ignored users:\n"
report += filtering_report.getvalue()
report += "\n"
users_text = "\n".join("%s (#%d)" % (u.username, u.id) for u in users)
subject = "[CCVPN] Usage Report: %s to %s" % (
start.strftime('%m/%Y'), end.strftime('%m/%Y'))
# Send
print(report)
print("-------")
print("Send to: " + ", ".join(a for a in addresses))
print("Confirm? [y/n] ", end='')
i = input()
if i.lower().strip() != 'y':
return
for dest in addresses:
mail = EmailMessage(subject=subject, body=report,
from_email=settings.DEFAULT_FROM_EMAIL, to=[dest])
mail.attach('users.txt', users_text, 'text/plain')
mail.send()
print("Sent.")

@ -0,0 +1,42 @@
from datetime import datetime, timedelta
from django.conf import settings
from .models import User
class ReferrerMiddleware():
def process_request(self, request):
if 'ref' in request.GET:
id = request.GET['ref']
elif 'referrer' in request.COOKIES:
id = request.COOKIES['referrer']
else:
return
try:
id = int(id.strip())
except (ValueError, TypeError):
return
try:
u = User.objects.get(id=id)
except User.DoesNotExist:
return
request.session['referrer'] = u.id
def process_response(self, request, response):
id = request.session.get('referrer')
if not id:
return response
max_age = 365 * 24 * 60 * 60
expires = (datetime.utcnow() + timedelta(seconds=max_age))
expires = expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
response.set_cookie('referrer', id,
max_age=max_age,
expires=expires,
domain=settings.SESSION_COOKIE_DOMAIN,
secure=settings.SESSION_COOKIE_SECURE or None)
return response

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import lambdainst.models
from django.conf import settings
import datetime
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='GiftCode',
fields=[
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
('code', models.CharField(default=lambdainst.models.random_gift_code, max_length=32)),
('time', models.DurationField(default=datetime.timedelta(30))),
('created', models.DateTimeField(null=True, auto_now_add=True)),
('single_use', models.BooleanField(default=True)),
('free_only', models.BooleanField(default=True)),
('available', models.BooleanField(default=True)),
('comment', models.TextField(blank=True)),
('created_by', models.ForeignKey(related_name='created_giftcode_set', null=True, blank=True, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Gift Codes',
'verbose_name': 'Gift Code',
},
),
migrations.CreateModel(
name='GiftCodeUser',
fields=[
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
('date', models.DateTimeField(null=True, auto_now_add=True)),
('code', models.ForeignKey(to='lambdainst.GiftCode')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Gift Code Users',
'verbose_name': 'Gift Code User',
},
),
migrations.CreateModel(
name='VPNUser',
fields=[
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
('notes', models.TextField(blank=True)),
('expiration', models.DateTimeField(null=True, blank=True)),
('last_expiry_notice', models.DateTimeField(null=True, blank=True)),
('notify_expiration', models.BooleanField(default=True)),
('trial_periods_given', models.IntegerField(default=0)),
('last_vpn_auth', models.DateTimeField(null=True, blank=True)),
('referrer_used', models.BooleanField(default=False)),
('referrer', models.ForeignKey(related_name='referrals', null=True, on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'VPN Users',
'verbose_name': 'VPN User',
},
),
migrations.AddField(
model_name='giftcode',
name='users',
field=models.ManyToManyField(through='lambdainst.GiftCodeUser', to=settings.AUTH_USER_MODEL),
),
]

@ -0,0 +1,147 @@
import random
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.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
assert isinstance(settings.TRIAL_PERIOD, timedelta)
assert isinstance(settings.TRIAL_PERIOD_LIMIT, int)
prng = random.SystemRandom()
def random_gift_code():
charset = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"
return ''.join([prng.choice(charset) for n in range(10)])
class VPNUser(models.Model):
class Meta:
verbose_name = _("VPN User")
verbose_name_plural = _("VPN Users")
user = models.OneToOneField(User, on_delete=models.CASCADE)
notes = models.TextField(blank=True)
expiration = models.DateTimeField(blank=True, null=True)
last_expiry_notice = models.DateTimeField(blank=True, null=True)
notify_expiration = models.BooleanField(default=True)
trial_periods_given = models.IntegerField(default=0)
last_vpn_auth = 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)
@property
def is_paid(self):
if not self.expiration:
return False
return self.expiration > timezone.now()
@property
def time_left(self):
return timezone.now() - self.expiration
def add_paid_time(self, time):
now = timezone.now()
if not self.expiration or self.expiration < now:
self.expiration = now
self.expiration += time
def give_trial_period(self):
self.add_paid_time(settings.TRIAL_PERIOD)
self.trial_periods_given += 1
@property
def can_have_trial(self):
if self.trial_periods_given >= settings.TRIAL_PERIOD_LIMIT:
return False
if self.user.payment_set.filter(status='confirmed').count() > 0:
return False
return True
@property
def remaining_trial_periods(self):
return settings.TRIAL_PERIOD_LIMIT - self.trial_periods_given
def on_payment_confirmed(self, payment):
if self.referrer and not self.referrer_used:
self.referrer.vpnuser.add_paid_time(timedelta(days=14))
self.referrer.vpnuser.save()
self.referrer_used = True
def __str__(self):
return self.user.username
@receiver(post_save, sender=User)
def create_vpnuser(sender, instance, created, **kwargs):
if created:
VPNUser.objects.create(user=instance)
class GiftCode(models.Model):
class Meta:
verbose_name = _("Gift Code")
verbose_name_plural = _("Gift Codes")
code = models.CharField(max_length=32, default=random_gift_code)
time = models.DurationField(default=timedelta(days=30))
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
created_by = models.ForeignKey(User, related_name='created_giftcode_set',
on_delete=models.CASCADE, null=True, blank=True)
single_use = models.BooleanField(default=True)
free_only = models.BooleanField(default=True)
available = models.BooleanField(default=True)
comment = models.TextField(blank=True)
users = models.ManyToManyField(User, through='GiftCodeUser')
def use_on(self, user):
if not self.available:
return False
if self.free_only and user.vpnuser.is_paid:
return False
link = GiftCodeUser(user=user, code=self)
link.save()
user.vpnuser.add_paid_time(self.time)
user.vpnuser.save()
if self.single_use:
self.available = False
self.save()
return True
@property
def comment_head(self):
head = self.comment.split('\n', 1)[0]
if len(head) > 80:
head = head[:80] + "..."
return head
def __str__(self):
return self.code
class GiftCodeUser(models.Model):
class Meta:
verbose_name = _("Gift Code User")
verbose_name_plural = _("Gift Code Users")
user = models.ForeignKey(User)
code = models.ForeignKey(GiftCode)
date = models.DateTimeField(auto_now_add=True, null=True, blank=True)