@@ -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) | |||
def __str__(self): | |||
return "%s (%s)" % (self.user.username, self.code.code) | |||
@@ -0,0 +1,98 @@ | |||
from django.utils.translation import ugettext as _ | |||
from django.conf import settings | |||
CA_CERT = settings.OPENVPN_CA | |||
CONFIG_OS = ( | |||
('windows', _("Windows")), | |||
('android', _("Android")), | |||
('ubuntu', _("Ubuntu")), | |||
('osx', _("OS X")), | |||
('ios', _("iOS")), | |||
('freebox', _("Freebox")), | |||
('other', _("Other / GNU/Linux")), | |||
) | |||
PROTOCOLS = ( | |||
('udp', _("UDP (default)")), | |||
('tcp', _("TCP")), | |||
('udpl', _("UDP (low MTU)")), | |||
) | |||
def make_config(gw_name, os, protocol, http_proxy=None, ipv6=True): | |||
use_frag = protocol == 'udpl' and os != 'ios' | |||
ipv6 = ipv6 and (os != 'freebox') | |||
http_proxy = http_proxy if protocol == 'tcp' else None | |||
resolvconf = os in ('ubuntu', 'other') | |||
openvpn_proto = {'udp': 'udp', 'udpl': 'udp', 'tcp': 'tcp'} | |||
openvpn_ports = {'udp': 1196, 'udpl': 1194, 'tcp': 443} | |||
hostname = 'gw.%s.204vpn.net' % gw_name | |||
remote = str(hostname) | |||
remote += ' ' + str(openvpn_ports[protocol]) | |||
remote += ' ' + openvpn_proto[protocol] | |||
config = """\ | |||
# +----------------------------+ | |||
# | Cognitive Cryptography VPN | | |||
# | https://vpn.ccrypto.org/ | | |||
# +----------------------------+ | |||
verb 4 | |||
client | |||
tls-client | |||
script-security 2 | |||
remote-cert-tls server | |||
dev tun | |||
nobind | |||
persist-key | |||
persist-tun | |||
comp-lzo yes | |||
remote {remote} | |||
auth-user-pass | |||
""".format(remote=remote) | |||
if os == 'ios': | |||
# i'd like to note here how much i hate OpenVPN | |||
config += "redirect-gateway ipv6\n" | |||
config += 'push "route 0.0.0.0 128.0.0.0"\n' | |||
config += 'push "route 128.0.0.0 128.0.0.0"\n' | |||
else: | |||
config += "redirect-gateway def1\n" | |||
if ipv6: | |||
config += "tun-ipv6\n" | |||
config += "route-ipv6 2000::/3\n" | |||
config += "\n" | |||
if use_frag: | |||
config += "fragment 1300\n" | |||
config += "mssfix 1300\n" | |||
config += "\n" | |||
if http_proxy: | |||
config += "http-proxy %s\n\n" % http_proxy | |||
if resolvconf: | |||
config += "up /etc/openvpn/update-resolv-conf\n" | |||
config += "down /etc/openvpn/update-resolv-conf\n" | |||
config += "\n" | |||
if os == 'windows': | |||
config += "register-dns\n" | |||
config += "\n" | |||
config += "<ca>\n%s\n</ca>" % CA_CERT | |||
if os == 'windows': | |||
config = config.replace('\n', '\r\n') | |||
return config | |||
@@ -0,0 +1,19 @@ | |||
import re | |||
from django import template | |||
from django.core.urlresolvers import reverse, NoReverseMatch | |||
register = template.Library() | |||
@register.simple_tag(takes_context=True) | |||
def active(context, pattern_or_urlname): | |||
try: | |||
pattern = '^' + reverse(pattern_or_urlname) | |||
except NoReverseMatch: | |||
pattern = pattern_or_urlname | |||
path = context['request'].path | |||
if re.search(pattern, path): | |||
return 'active' | |||
return '' | |||
@@ -0,0 +1,38 @@ | |||
from django.utils.translation import ugettext, ungettext | |||
from django.template import Library | |||
from django.utils.html import avoid_wrapping | |||
from django.utils import formats | |||
register = Library() | |||
@register.filter(is_safe=True) | |||
def bwformat(bps): | |||
try: | |||
bps = float(bps) | |||
except (TypeError, ValueError, UnicodeDecodeError): | |||
value = ungettext("%(bw)d bps", "%(bw)d bps", 0) % {'bw': 0} | |||
return avoid_wrapping(value) | |||
filesize_number_format = lambda value: formats.number_format(round(value, 1), -1) | |||
K = 1 * 10 ** 3 | |||
M = 1 * 10 ** 6 | |||
G = 1 * 10 ** 9 | |||
T = 1 * 10 ** 12 | |||
P = 1 * 10 ** 15 | |||
if bps < K: | |||
value = ungettext("%(size)d bps", "%(size)d bps", bps) % {'size': bps} | |||
elif bps < M: | |||
value = ugettext("%s Kbps") % filesize_number_format(bps / K) | |||
elif bps < G: | |||
value = ugettext("%s Mbps") % filesize_number_format(bps / M) | |||
elif bps < T: | |||
value = ugettext("%s Gbps") % filesize_number_format(bps / G) | |||
elif bps < P: | |||
value = ugettext("%s Tbps") % filesize_number_format(bps / T) | |||
else: | |||
value = ugettext("%s Pbps") % filesize_number_format(bps / P) | |||
return avoid_wrapping(value) |
@@ -0,0 +1,274 @@ | |||
from datetime import timedelta, datetime | |||
from django.test import TestCase | |||
from django.utils import timezone | |||
from .forms import SignupForm | |||
from .models import VPNUser, User, random_gift_code, GiftCode, GiftCodeUser | |||
from payments.models import Payment | |||
class UserTestMixin: | |||
def assertRemaining(self, vpnuser, time): | |||
""" Check that the vpnuser will expire in time (+/- 5 seconds) """ | |||
exp = vpnuser.expiration or timezone.now() | |||
seconds = (exp - timezone.now() - time).total_seconds() | |||
self.assertAlmostEqual(seconds, 0, delta=5) | |||
class UserModelTest(TestCase, UserTestMixin): | |||
def setUp(self): | |||
User.objects.create_user('aaa') | |||
def test_add_time(self): | |||
u = User.objects.get(username='aaa') | |||
vu = u.vpnuser | |||
p = timedelta(days=1) | |||
self.assertFalse(vu.is_paid) | |||
vu.expiration = timezone.now() | |||
vu.add_paid_time(p) | |||
final = vu.expiration | |||
self.assertRemaining(vu, p) | |||
self.assertGreater(final, timezone.now()) | |||
self.assertTrue(vu.is_paid) | |||
def test_add_time_past(self): | |||
u = User.objects.get(username='aaa') | |||
vu = u.vpnuser | |||
p = timedelta(days=1) | |||
self.assertFalse(vu.is_paid) | |||
vu.expiration = timezone.now() - timedelta(days=135) | |||
vu.add_paid_time(p) | |||
final = vu.expiration | |||
self.assertRemaining(vu, p) | |||
self.assertGreater(final, timezone.now()) | |||
self.assertTrue(vu.is_paid) | |||
def test_add_time_initial(self): | |||
u = User.objects.get(username='aaa') | |||
vu = u.vpnuser | |||
p = timedelta(days=1) | |||
self.assertFalse(vu.is_paid) | |||
vu.add_paid_time(p) | |||
self.assertTrue(vu.is_paid) | |||
def test_grant_trial(self): | |||
p = timedelta(days=1) | |||
u = User.objects.get(username='aaa') | |||
vu = u.vpnuser | |||
with self.settings(TRIAL_PERIOD=p, TRIAL_PERIOD_LIMIT=2): | |||
self.assertEqual(vu.remaining_trial_periods, 2) | |||
self.assertTrue(vu.can_have_trial) | |||
vu.give_trial_period() | |||
self.assertRemaining(vu, p) | |||
self.assertEqual(vu.remaining_trial_periods, 1) | |||
self.assertTrue(vu.can_have_trial) | |||
vu.give_trial_period() | |||
self.assertRemaining(vu, p * 2) | |||
self.assertEqual(vu.remaining_trial_periods, 0) | |||
self.assertFalse(vu.can_have_trial) | |||
def test_trial_refused(self): | |||
p = timedelta(days=1) | |||
u = User.objects.get(username='aaa') | |||
payment = Payment.objects.create(user=u, status='confirmed', amount=300, | |||
time=timedelta(days=30)) | |||
payment.save() | |||
vu = u.vpnuser | |||
with self.settings(TRIAL_PERIOD=p, TRIAL_PERIOD_LIMIT=2): | |||
self.assertEqual(vu.remaining_trial_periods, 2) | |||
self.assertFalse(vu.can_have_trial) | |||
class UserModelReferrerTest(TestCase, UserTestMixin): | |||
def setUp(self): | |||
self.referrer = User.objects.create_user('ref') | |||
self.without_ref = User.objects.create_user('aaaa') | |||
self.with_ref = User.objects.create_user('bbbb') | |||
self.with_ref.vpnuser.referrer = self.referrer | |||
self.payment = Payment.objects.create( | |||
user=self.with_ref, status='confirmed', amount=300, time=timedelta(days=30)) | |||
def test_no_ref(self): | |||
self.without_ref.vpnuser.on_payment_confirmed(self.payment) | |||
def test_ref(self): | |||
self.with_ref.vpnuser.on_payment_confirmed(self.payment) | |||
self.assertTrue(self.with_ref.vpnuser.referrer_used) | |||
self.assertEqual(self.with_ref.vpnuser.referrer, self.referrer) | |||
self.assertRemaining(self.referrer.vpnuser, timedelta(days=14)) | |||
class GCModelTest(TestCase): | |||
def test_generator(self): | |||
c = random_gift_code() | |||
self.assertEqual(len(c), 10) | |||
self.assertNotEqual(c, random_gift_code()) | |||
class SignupViewTest(TestCase): | |||
def test_form(self): | |||
response = self.client.get('/account/signup') | |||
self.assertEqual(response.status_code, 200) | |||
self.assertIsInstance(response.context['form'], SignupForm) | |||
def test_post(self): | |||
response = self.client.post('/account/signup', { | |||
'username': 'test_un', 'password': 'test_pw', 'password2': 'test_pw'}) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test_un') | |||
self.assertTrue(user.check_password('test_pw')) | |||
def test_post_error(self): | |||
response = self.client.post('/account/signup', { | |||
'username': 'test_un', 'password': 'test_pw', 'password2': 'qsdf'}) | |||
self.assertEqual(response.status_code, 200) | |||
self.assertIsInstance(response.context['form'], SignupForm) | |||
self.assertFormError(response, 'form', 'password', | |||
'Passwords are not the same') | |||
def test_post_referrer(self): | |||
ref = User.objects.create_user('referrer') | |||
response = self.client.post('/account/signup?ref=%d' % ref.id, { | |||
'username': 'test_un', 'password': 'test_pw', 'password2': 'test_pw'}) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test_un') | |||
self.assertTrue(user.check_password('test_pw')) | |||
self.assertEqual(user.vpnuser.referrer, ref) | |||
class AccountViewsTest(TestCase, UserTestMixin): | |||
def setUp(self): | |||
User.objects.create_user('test', None, 'testpw') | |||
self.client.login(username='test', password='testpw') | |||
def test_account(self): | |||
response = self.client.get('/account/') | |||
self.assertEqual(response.status_code, 200) | |||
def test_trial_get(self): | |||
response = self.client.get('/account/trial') | |||
self.assertRedirects(response, '/account/') | |||
def test_trial(self): | |||
p = timedelta(days=1) | |||
with self.settings(RECAPTCHA_API='TEST', TRIAL_PERIOD=p): | |||
good_data = {'g-recaptcha-response': 'TEST-TOKEN'} | |||
response = self.client.post('/account/trial', good_data) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test') | |||
self.assertRemaining(user.vpnuser, p) | |||
def test_trial_fail(self): | |||
p = timedelta(days=1) | |||
with self.settings(RECAPTCHA_API='TEST', TRIAL_PERIOD=p): | |||
bad_data = {'g-recaptcha-response': 'TOTALLY-NOT-TEST-TOKEN'} | |||
response = self.client.post('/account/trial', bad_data) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test') | |||
self.assertRemaining(user.vpnuser, timedelta()) | |||
def test_settings_form(self): | |||
response = self.client.get('/account/settings') | |||
self.assertEqual(response.status_code, 200) | |||
def test_settings_post(self): | |||
response = self.client.post('/account/settings', { | |||
'password': 'new_test_pw', 'password2': 'new_test_pw', | |||
'email': 'new_email@example.com'}) | |||
self.assertEqual(response.status_code, 200) | |||
user = User.objects.get(username='test') | |||
self.assertTrue(user.check_password('new_test_pw')) | |||
self.assertEqual(user.email, 'new_email@example.com') | |||
def test_settings_post_fail(self): | |||
response = self.client.post('/account/settings', { | |||
'password': 'new_test_pw', 'password2': 'new_test_pw_qsdfg', | |||
'email': 'new_email@example.com'}) | |||
self.assertEqual(response.status_code, 200) | |||
user = User.objects.get(username='test') | |||
self.assertFalse(user.check_password('new_test_pw')) | |||
self.assertEqual(user.email, 'new_email@example.com') | |||
def test_giftcode_use_single(self): | |||
gc = GiftCode.objects.create(time=timedelta(days=42), single_use=True) | |||
response = self.client.post('/account/gift_code', {'code': gc.code}) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test') | |||
self.assertRemaining(user.vpnuser, timedelta(days=42)) | |||
response = self.client.post('/account/gift_code', {'code': gc.code}) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test') | |||
self.assertRemaining(user.vpnuser, timedelta(days=42)) # same expiration | |||
def test_giftcode_use_free_only(self): | |||
gc = GiftCode.objects.create(time=timedelta(days=42), free_only=True) | |||
response = self.client.post('/account/gift_code', {'code': gc.code}) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test') | |||
self.assertRemaining(user.vpnuser, timedelta(days=42)) | |||
def test_giftcode_use_free_only_fail(self): | |||
gc = GiftCode.objects.create(time=timedelta(days=42), free_only=True) | |||
user = User.objects.get(username='test') | |||
user.vpnuser.add_paid_time(timedelta(days=1)) | |||
user.vpnuser.save() | |||
response = self.client.post('/account/gift_code', {'code': gc.code}) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test') | |||
self.assertRemaining(user.vpnuser, timedelta(days=1)) | |||
def test_giftcode_create_gcu(self): | |||
gc = GiftCode.objects.create(time=timedelta(days=42)) | |||
response = self.client.post('/account/gift_code', {'code': gc.code}) | |||
self.assertRedirects(response, '/account/') | |||
user = User.objects.get(username='test') | |||
gcu = GiftCodeUser.objects.get(user=user, code=gc) | |||
self.assertRemaining(user.vpnuser, timedelta(days=42)) | |||
self.assertIn(gcu, user.giftcodeuser_set.all()) | |||
class CACrtViewTest(TestCase): | |||
def test_ca_crt(self): | |||
with self.settings(OPENVPN_CA='test ca'): | |||
response = self.client.get('/ca.crt') | |||
self.assertEqual(response.status_code, 200) | |||
self.assertEqual(response['Content-Type'], 'application/x-x509-ca-cert') | |||
self.assertEqual(response.content, b'test ca') | |||
@@ -0,0 +1,18 @@ | |||
from django.conf.urls import url | |||
from django.contrib.auth import views as auth_views | |||
from . import views | |||
urlpatterns = [ | |||
url(r'^login$', auth_views.login, name='login'), | |||
url(r'^logout$', views.logout, name='logout'), | |||
url(r'^signup$', views.signup, name='signup'), | |||
url(r'^settings', views.settings), | |||
url(r'^config_dl', views.config_dl), | |||
url(r'^config', views.config), | |||
url(r'^logs', views.logs), | |||
url(r'^gift_code', views.gift_code), | |||
url(r'^trial', views.trial), | |||
url(r'^', views.index, name='index'), | |||
] |
@@ -0,0 +1,360 @@ | |||
import requests | |||
import io | |||
import zipfile | |||
from urllib.parse import urlencode | |||
from datetime import timedelta, datetime | |||
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseForbidden | |||
from django.http import JsonResponse | |||
from django.shortcuts import render, redirect | |||
from django.contrib.auth import authenticate | |||
from django.contrib.auth.decorators import login_required, user_passes_test | |||
from django.contrib.admin.sites import site | |||
from django.contrib import messages | |||
from django.utils.translation import ugettext as _ | |||
from django.utils import timezone | |||
from django.conf import settings as project_settings | |||
from django.views.decorators.csrf import csrf_exempt | |||
from django.db.models import Count | |||
from django.contrib import auth | |||
from django.contrib.auth.models import User | |||
from django_countries import countries | |||
from payments.models import ACTIVE_BACKENDS | |||
from .forms import SignupForm | |||
from .models import GiftCode, VPNUser | |||
from .core import core_api, current_active_sessions, get_locations as core_get_locations | |||
from .core import LCORE_INST_SECRET, LCORE_SOURCE_ADDR | |||
from . import graphs | |||
from . import openvpn | |||
def get_locations(): | |||
""" Pretty bad thing that returns get_locations() with translated stuff | |||
that depends on the request | |||
""" | |||
countries_d = dict(countries) | |||
locations = core_get_locations() | |||
for k, v in locations: | |||
cc = v['country_code'].upper() | |||
v['country_name'] = countries_d.get(cc, cc) | |||
return locations | |||
def ca_crt(request): | |||
return HttpResponse(content=project_settings.OPENVPN_CA, | |||
content_type='application/x-x509-ca-cert') | |||
def logout(request): | |||
auth.logout(request) | |||
return redirect('index') | |||
def signup(request): | |||
if request.user.is_authenticated(): | |||
return redirect('account:index') | |||
if request.method != 'POST': | |||
form = SignupForm() | |||
return render(request, 'ccvpn/signup.html', dict(form=form)) | |||
form = SignupForm(request.POST) | |||
if not form.is_valid(): | |||
return render(request, 'ccvpn/signup.html', dict(form=form)) | |||
user = User.objects.create_user(form.cleaned_data['username'], | |||
form.cleaned_data['email'], | |||
form.cleaned_data['password']) | |||
user.save() | |||
try: | |||
user.vpnuser.referrer = User.objects.get(id=request.session.get('referrer')) | |||
except User.DoesNotExist: | |||
pass | |||
user.vpnuser.save() | |||
user.backend = 'django.contrib.auth.backends.ModelBackend' | |||
auth.login(request, user) | |||
return redirect('account:index') | |||
@login_required | |||
def index(request): | |||
ref_url = project_settings.ROOT_URL + '?ref=' + str(request.user.id) | |||
twitter_url = 'https://twitter.com/intent/tweet?' | |||
twitter_args = { | |||
'text': _("Awesome VPN! 3€ per month, with a free 7 days trial!"), | |||
'via': 'CCrypto_VPN', | |||
'url': ref_url, | |||
'related': 'CCrypto_VPN,CCrypto_org' | |||
} | |||
context = dict( | |||
title=_("Account"), | |||
ref_url=ref_url, | |||
twitter_link=twitter_url + urlencode(twitter_args), | |||
backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_id), | |||
default_backend='paypal', | |||
recaptcha_site_key=project_settings.RECAPTCHA_SITE_KEY, | |||
) | |||
return render(request, 'lambdainst/account.html', context) | |||
def captcha_test(grr, request): | |||
api_url = project_settings.RECAPTCHA_API | |||
if api_url == 'TEST' and grr == 'TEST-TOKEN': | |||
# FIXME: i'm sorry. | |||
return True | |||
data = dict(secret=project_settings.RECAPTCHA_SECRET_KEY, | |||
remoteip=request.META['REMOTE_ADDR'], | |||
response=grr) | |||
try: | |||
r = requests.post(api_url, data=data) | |||
r.raise_for_status() | |||
d = r.json() | |||
return d.get('success') | |||
except (requ |