Browse Source

Initial commit

master
Alice 6 years ago
commit
2f7deaae2d
100 changed files with 10044 additions and 0 deletions
  1. +91
    -0
      .gitignore
  2. +21
    -0
      LICENSE
  3. +40
    -0
      README.md
  4. +0
    -0
      ccvpn/__init__.py
  5. +39
    -0
      ccvpn/ca.crt
  6. +9
    -0
      ccvpn/context_processors.py
  7. +0
    -0
      ccvpn/forms.py
  8. +48
    -0
      ccvpn/passwords.py
  9. +173
    -0
      ccvpn/settings.py
  10. +38
    -0
      ccvpn/urls.py
  11. +82
    -0
      ccvpn/views.py
  12. +16
    -0
      ccvpn/wsgi.py
  13. +0
    -0
      lambdainst/__init__.py
  14. +132
    -0
      lambdainst/admin.py
  15. +99
    -0
      lambdainst/core.py
  16. +56
    -0
      lambdainst/forms.py
  17. +126
    -0
      lambdainst/graphs.py
  18. +0
    -0
      lambdainst/management/__init__.py
  19. +0
    -0
      lambdainst/management/commands/__init__.py
  20. +11
    -0
      lambdainst/management/commands/core_info.py
  21. +56
    -0
      lambdainst/management/commands/expire_notify.py
  22. +113
    -0
      lambdainst/management/commands/send_report.py
  23. +42
    -0
      lambdainst/middleware.py
  24. +73
    -0
      lambdainst/migrations/0001_initial.py
  25. +0
    -0
      lambdainst/migrations/__init__.py
  26. +147
    -0
      lambdainst/models.py
  27. +98
    -0
      lambdainst/openvpn.py
  28. +19
    -0
      lambdainst/templatetags/active.py
  29. +38
    -0
      lambdainst/templatetags/bw.py
  30. +274
    -0
      lambdainst/tests.py
  31. +18
    -0
      lambdainst/urls.py
  32. +360
    -0
      lambdainst/views.py
  33. +1141
    -0
      locale/fr/LC_MESSAGES/django.po
  34. +10
    -0
      manage.py
  35. +126
    -0
      pages/faq.en.md
  36. +131
    -0
      pages/faq.fr.md
  37. +17
    -0
      pages/help.en.md
  38. +16
    -0
      pages/help.fr.md
  39. +21
    -0
      pages/install-android.en.md
  40. +21
    -0
      pages/install-android.fr.md
  41. +107
    -0
      pages/install-gnulinux.en.md
  42. +252
    -0
      pages/install-gnulinux.fr.md
  43. +11
    -0
      pages/install-osx.en.md
  44. +11
    -0
      pages/install-osx.fr.md
  45. +47
    -0
      pages/install-windows.en.md
  46. +48
    -0
      pages/install-windows.fr.md
  47. +91
    -0
      pages/self-diagnosis.en.md
  48. +87
    -0
      pages/self-diagnosis.fr.md
  49. +68
    -0
      pages/tos.en.md
  50. +0
    -0
      payments/__init__.py
  51. +60
    -0
      payments/admin.py
  52. +456
    -0
      payments/backends.py
  53. +15
    -0
      payments/forms.py
  54. +0
    -0
      payments/management/__init__.py
  55. +0
    -0
      payments/management/commands/__init__.py
  56. +15
    -0
      payments/management/commands/bitcoin_info.py
  57. +28
    -0
      payments/management/commands/check_btc_payments.py
  58. +52
    -0
      payments/management/commands/confirm_payment.py
  59. +31
    -0
      payments/management/commands/expire_payments.py
  60. +60
    -0
      payments/migrations/0001_initial.py
  61. +19
    -0
      payments/migrations/0002_auto_20151204_0341.py
  62. +20
    -0
      payments/migrations/0003_auto_20151209_0440.py
  63. +0
    -0
      payments/migrations/__init__.py
  64. +114
    -0
      payments/models.py
  65. +323
    -0
      payments/tests.py
  66. +14
    -0
      payments/urls.py
  67. +116
    -0
      payments/views.py
  68. +10
    -0
      requirements.txt
  69. BIN
      static/affimg/banner.png
  70. BIN
      static/affimg/leaderboard.png
  71. +23
    -0
      static/css/admin_status.css
  72. +2086
    -0
      static/css/font-awesome.css
  73. +4
    -0
      static/css/font-awesome.min.css
  74. +7
    -0
      static/css/grids-responsive-min.css
  75. +11
    -0
      static/css/pure-min.css
  76. +49
    -0
      static/css/reset.css
  77. +688
    -0
      static/css/style.css
  78. BIN
      static/fonts/OpenSans.woff2
  79. BIN
      static/fonts/OpenSansB.woff2
  80. BIN
      static/fonts/fontawesome-webfont.eot
  81. +655
    -0
      static/fonts/fontawesome-webfont.svg
  82. BIN
      static/fonts/fontawesome-webfont.ttf
  83. BIN
      static/fonts/fontawesome-webfont.woff
  84. BIN
      static/fonts/fontawesome-webfont.woff2
  85. BIN
      static/img/7proxies.png
  86. +65
    -0
      static/img/anon.svg
  87. +65
    -0
      static/img/bg-logo.svg
  88. BIN
      static/img/bg-title.png
  89. BIN
      static/img/bg.png
  90. +54
    -0
      static/img/bolt.svg
  91. BIN
      static/img/chat.png
  92. +57
    -0
      static/img/chat.svg
  93. +65
    -0
      static/img/cheap.svg
  94. +54
    -0
      static/img/fast.svg
  95. +71
    -0
      static/img/openvpn.svg
  96. +57
    -0
      static/img/unlimited.svg
  97. +139
    -0
      static/ping.js
  98. +22
    -0
      templates/account_layout.html
  99. +98
    -0
      templates/admin/index.html
  100. +17
    -0
      templates/admin/tickets/ticket/change_list.html

+ 91
- 0
.gitignore View File

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

+ 21
- 0
LICENSE View File

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

+ 40
- 0
README.md View File

@@ -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
ccvpn/__init__.py View File


+ 39
- 0
ccvpn/ca.crt View File

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

+ 9
- 0
ccvpn/context_processors.py View File

@@ -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
ccvpn/forms.py View File


+ 48
- 0
ccvpn/passwords.py View File

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


+ 173
- 0
ccvpn/settings.py View File

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


+ 38
- 0
ccvpn/urls.py View File

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

]

+ 82
- 0
ccvpn/views.py View File

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


+ 16
- 0
ccvpn/wsgi.py View File

@@ -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
lambdainst/__init__.py View File


+ 132
- 0
lambdainst/admin.py View File

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


+ 99
- 0
lambdainst/core.py View File

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



+ 56
- 0
lambdainst/forms.py View File

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


+ 126
- 0
lambdainst/graphs.py View File

@@ -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
lambdainst/management/__init__.py View File


+ 0
- 0
lambdainst/management/commands/__init__.py View File


+ 11
- 0
lambdainst/management/commands/core_info.py View File

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

+ 56
- 0
lambdainst/management/commands/expire_notify.py View File

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

+ 113
- 0
lambdainst/management/commands/send_report.py View File

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



+ 42
- 0
lambdainst/middleware.py View File

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


+ 73
- 0
lambdainst/migrations/0001_initial.py View File

@@ -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
lambdainst/migrations/__init__.py View File


+ 147
- 0
lambdainst/models.py View File

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


+ 98
- 0
lambdainst/openvpn.py View File

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




+ 19
- 0
lambdainst/templatetags/active.py View File

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


+ 38
- 0
lambdainst/templatetags/bw.py View File

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

+ 274
- 0
lambdainst/tests.py View File

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



+ 18
- 0
lambdainst/urls.py View File

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

+ 360
- 0
lambdainst/views.py View File

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