CCrypto VPN public website https://vpn.ccrypto.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

480 lines
15 KiB

  1. import requests
  2. import io
  3. import zipfile
  4. import hmac
  5. import base64
  6. from hashlib import sha256
  7. from urllib.parse import urlencode, parse_qsl
  8. from datetime import timedelta, datetime
  9. from django.http import (
  10. HttpResponse, JsonResponse,
  11. HttpResponseRedirect,
  12. HttpResponseNotFound, HttpResponseForbidden
  13. )
  14. from django.shortcuts import render, redirect
  15. from django.contrib.auth import authenticate
  16. from django.contrib.auth.decorators import login_required, user_passes_test
  17. from django.contrib.admin.sites import site
  18. from django.contrib import messages
  19. from django.utils.translation import ugettext as _
  20. from django.utils import timezone
  21. from django.conf import settings as project_settings
  22. from django.views.decorators.csrf import csrf_exempt
  23. from django.db.models import Count
  24. from django.contrib import auth
  25. from django.contrib.auth.models import User
  26. from django_countries import countries
  27. from constance import config as site_config
  28. import lcoreapi
  29. from ccvpn.common import get_client_ip, get_price_float
  30. from payments.models import ACTIVE_BACKENDS
  31. from .forms import SignupForm, ReqEmailForm
  32. from .models import GiftCode, VPNUser
  33. from .core import core_api
  34. from . import core
  35. from . import graphs
  36. from . import openvpn
  37. def get_locations():
  38. """ Pretty bad thing that returns get_locations() with translated stuff
  39. that depends on the request
  40. """
  41. countries_d = dict(countries)
  42. locations = core.get_locations()
  43. for k, v in locations:
  44. cc = v['country_code'].upper()
  45. v['country_name'] = countries_d.get(cc, cc)
  46. return locations
  47. def ca_crt(request):
  48. return HttpResponse(content=project_settings.OPENVPN_CA,
  49. content_type='application/x-x509-ca-cert')
  50. def logout(request):
  51. auth.logout(request)
  52. return redirect('index')
  53. def signup(request):
  54. if request.user.is_authenticated():
  55. return redirect('account:index')
  56. if request.method != 'POST':
  57. form = SignupForm()
  58. return render(request, 'ccvpn/signup.html', dict(form=form))
  59. form = SignupForm(request.POST)
  60. if not form.is_valid():
  61. return render(request, 'ccvpn/signup.html', dict(form=form))
  62. user = User.objects.create_user(form.cleaned_data['username'],
  63. form.cleaned_data['email'],
  64. form.cleaned_data['password'])
  65. user.save()
  66. if core.VPN_AUTH_STORAGE == 'core':
  67. core.create_user(form.cleaned_data['username'], form.cleaned_data['password'])
  68. try:
  69. user.vpnuser.referrer = User.objects.get(id=request.session.get('referrer'))
  70. except User.DoesNotExist:
  71. pass
  72. user.vpnuser.campaign = request.session.get('campaign')
  73. user.vpnuser.save()
  74. user.backend = 'django.contrib.auth.backends.ModelBackend'
  75. auth.login(request, user)
  76. return redirect('account:index')
  77. @login_required
  78. def discourse_login(request):
  79. sso_secret = project_settings.DISCOURSE_SECRET
  80. discourse_url = project_settings.DISCOURSE_URL
  81. if project_settings.DISCOURSE_SSO is not True:
  82. return HttpResponseNotFound()
  83. payload = request.GET.get('sso', '')
  84. signature = request.GET.get('sig', '')
  85. expected_signature = hmac.new(sso_secret.encode('utf-8'),
  86. payload.encode('utf-8'),
  87. sha256).hexdigest()
  88. if signature != expected_signature:
  89. return HttpResponseNotFound()
  90. if request.method == 'POST' and 'email' in request.POST:
  91. form = ReqEmailForm(request.POST)
  92. if not form.is_valid():
  93. return render(request, 'ccvpn/require_email.html', dict(form=form))
  94. request.user.email = form.cleaned_data['email']
  95. request.user.save()
  96. if not request.user.email:
  97. form = ReqEmailForm()
  98. return render(request, 'ccvpn/require_email.html', dict(form=form))
  99. try:
  100. payload = base64.b64decode(payload).decode('utf-8')
  101. payload_data = dict(parse_qsl(payload))
  102. except (TypeError, ValueError):
  103. return HttpResponseNotFound()
  104. payload_data.update({
  105. 'external_id': request.user.id,
  106. 'username': request.user.username,
  107. 'email': request.user.email,
  108. 'require_activation': 'true',
  109. })
  110. payload = urlencode(payload_data)
  111. payload = base64.b64encode(payload.encode('utf-8'))
  112. signature = hmac.new(sso_secret.encode('utf-8'), payload, sha256).hexdigest()
  113. redirect_query = urlencode(dict(sso=payload, sig=signature))
  114. redirect_path = '/session/sso_login?' + redirect_query
  115. return HttpResponseRedirect(discourse_url + redirect_path)
  116. @login_required
  117. def index(request):
  118. ref_url = project_settings.ROOT_URL + '?ref=' + str(request.user.id)
  119. twitter_url = 'https://twitter.com/intent/tweet?'
  120. twitter_args = {
  121. 'text': _("Awesome VPN! 3€ per month, with a free 7 days trial!"),
  122. 'via': 'CCrypto_VPN',
  123. 'url': ref_url,
  124. 'related': 'CCrypto_VPN,CCrypto_org'
  125. }
  126. class price_fn:
  127. """ Clever hack to get the price in templates with {{price.3}} with
  128. 3 an arbitrary number of months
  129. """
  130. def __getitem__(self, months):
  131. n = int(months) * get_price_float()
  132. c = project_settings.PAYMENTS_CURRENCY[1]
  133. return '%.2f %s' % (n, c)
  134. context = dict(
  135. title=_("Account"),
  136. ref_url=ref_url,
  137. twitter_link=twitter_url + urlencode(twitter_args),
  138. subscription=request.user.vpnuser.get_subscription(include_unconfirmed=True),
  139. backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_id),
  140. subscr_backends=sorted((b for b in ACTIVE_BACKENDS.values()
  141. if b.backend_has_recurring),
  142. key=lambda x: x.backend_id),
  143. default_backend='paypal',
  144. recaptcha_site_key=project_settings.RECAPTCHA_SITE_KEY,
  145. price=price_fn(),
  146. user_motd=site_config.MOTD_USER,
  147. )
  148. return render(request, 'lambdainst/account.html', context)
  149. def captcha_test(grr, request):
  150. api_url = project_settings.RECAPTCHA_API
  151. if api_url == 'TEST' and grr == 'TEST-TOKEN':
  152. # FIXME: i'm sorry.
  153. return True
  154. data = dict(secret=project_settings.RECAPTCHA_SECRET_KEY,
  155. remoteip=get_client_ip(request),
  156. response=grr)
  157. try:
  158. r = requests.post(api_url, data=data)
  159. r.raise_for_status()
  160. d = r.json()
  161. return d.get('success')
  162. except (requests.ConnectionError, requests.HTTPError, ValueError):
  163. return False
  164. @login_required
  165. def trial(request):
  166. if request.method != 'POST' or not request.user.vpnuser.can_have_trial:
  167. return redirect('account:index')
  168. grr = request.POST.get('g-recaptcha-response', '')
  169. if captcha_test(grr, request):
  170. request.user.vpnuser.give_trial_period()
  171. request.user.vpnuser.save()
  172. messages.success(request, _("OK!"))
  173. else:
  174. messages.error(request, _("Invalid captcha"))
  175. return redirect('account:index')
  176. @login_required
  177. def settings(request):
  178. if request.method != 'POST':
  179. return render(request, 'lambdainst/settings.html')
  180. pw = request.POST.get('password')
  181. pw2 = request.POST.get('password2')
  182. if pw and pw2:
  183. if pw != pw2:
  184. messages.error(request, _("Passwords do not match"))
  185. else:
  186. request.user.set_password(pw)
  187. if core.VPN_AUTH_STORAGE == 'core':
  188. core.update_user_password(request.user, pw)
  189. messages.success(request, _("OK!"))
  190. email = request.POST.get('email')
  191. if email:
  192. request.user.email = email
  193. else:
  194. request.user.email = ''
  195. request.user.save()
  196. return render(request, 'lambdainst/settings.html', dict(title=_("Settings")))
  197. @login_required
  198. def gift_code(request):
  199. try:
  200. code = GiftCode.objects.get(code=request.POST.get('code', '').strip(), available=True)
  201. except GiftCode.DoesNotExist:
  202. code = None
  203. if code is None:
  204. messages.error(request, _("Gift code not found or already used."))
  205. elif not code.use_on(request.user):
  206. messages.error(request, _("Gift code only available to free accounts."))
  207. else:
  208. messages.success(request, _("OK!"))
  209. return redirect('account:index')
  210. @login_required
  211. def logs(request):
  212. page_size = 20
  213. page = int(request.GET.get('page', 0))
  214. offset = page * page_size
  215. base = core_api.info['current_instance']
  216. path = '/users/' + request.user.username + '/sessions/'
  217. try:
  218. l = core_api.get(base + path, offset=offset, limit=page_size)
  219. total_count = l['total_count']
  220. items = l['items']
  221. except lcoreapi.APINotFoundError:
  222. total_count = 0
  223. items = []
  224. return render(request, 'lambdainst/logs.html', {
  225. 'sessions': items,
  226. 'page': page,
  227. 'prev': page - 1 if page > 0 else None,
  228. 'next': page + 1 if offset + page_size < total_count else None,
  229. 'last_page': total_count // page_size,
  230. 'title': _("Logs"),
  231. })
  232. @login_required
  233. def config(request):
  234. return render(request, 'lambdainst/config.html', dict(
  235. title=_("Config"),
  236. config_os=openvpn.CONFIG_OS,
  237. config_countries=(c for _, c in get_locations()),
  238. config_protocols=openvpn.PROTOCOLS,
  239. ))
  240. @login_required
  241. def config_dl(request):
  242. allowed_cc = [cc for (cc, _) in get_locations()]
  243. os = request.GET.get('client_os')
  244. common_options = {
  245. 'username': request.user.username,
  246. 'protocol': request.GET.get('protocol'),
  247. 'os': os,
  248. 'http_proxy': request.GET.get('http_proxy'),
  249. 'ipv6': 'enable_ipv6' in request.GET,
  250. }
  251. # Should be validated since it's used in the filename
  252. # other common options are only put in the config file
  253. protocol = common_options['protocol']
  254. if protocol not in ('udp', 'udpl', 'tcp'):
  255. return HttpResponseNotFound()
  256. location = request.GET.get('gateway')
  257. if location == 'all':
  258. # Multiple gateways in a zip archive
  259. f = io.BytesIO()
  260. z = zipfile.ZipFile(f, mode='w')
  261. for gw_name in allowed_cc + ['random']:
  262. if os == 'chromeos':
  263. filename = 'ccrypto-%s-%s.onc' % (gw_name, protocol)
  264. else:
  265. filename = 'ccrypto-%s-%s.ovpn' % (gw_name, protocol)
  266. config = openvpn.make_config(gw_name=gw_name, **common_options)
  267. z.writestr(filename, config.encode('utf-8'))
  268. z.close()
  269. r = HttpResponse(content=f.getvalue(), content_type='application/zip')
  270. r['Content-Disposition'] = 'attachment; filename="%s.zip"' % filename
  271. return r
  272. else:
  273. # Single gateway
  274. if location[3:] in allowed_cc:
  275. gw_name = location[3:]
  276. else:
  277. gw_name = 'random'
  278. if os == 'chromeos':
  279. filename = 'ccrypto-%s-%s.onc' % (gw_name, protocol)
  280. else:
  281. filename = 'ccrypto-%s-%s.ovpn' % (gw_name, protocol)
  282. config = openvpn.make_config(gw_name=gw_name, **common_options)
  283. if 'plain' in request.GET:
  284. return HttpResponse(content=config, content_type='text/plain')
  285. else:
  286. if os == 'chromeos':
  287. r = HttpResponse(content=config, content_type='application/x-onc')
  288. else:
  289. r = HttpResponse(content=config, content_type='application/x-openvpn-profile')
  290. r['Content-Disposition'] = 'attachment; filename="%s"' % filename
  291. return r
  292. @csrf_exempt
  293. def api_auth(request):
  294. if request.method != 'POST':
  295. return HttpResponseNotFound()
  296. if core.VPN_AUTH_STORAGE != 'inst':
  297. return HttpResponseNotFound()
  298. username = request.POST.get('username')
  299. password = request.POST.get('password')
  300. secret = request.POST.get('secret')
  301. if secret != core.LCORE_INST_SECRET:
  302. return HttpResponseForbidden(content="Invalid secret")
  303. user = authenticate(username=username, password=password)
  304. if not user or not user.is_active:
  305. return JsonResponse(dict(status='fail', message="Invalid credentials"))
  306. if not user.vpnuser.is_paid:
  307. return JsonResponse(dict(status='fail', message="Not allowed to connect"))
  308. user.vpnuser.last_vpn_auth = timezone.now()
  309. user.vpnuser.save()
  310. return JsonResponse(dict(status='ok'))
  311. def api_locations(request):
  312. def format_loc(cc, l):
  313. return {
  314. 'country_name': l['country_name'],
  315. 'country_code': cc,
  316. 'hostname': l['hostname'],
  317. 'bandwidth': l['bandwidth'],
  318. 'servers': l['servers'],
  319. }
  320. return JsonResponse(dict(locations=[format_loc(cc, l) for cc, l in get_locations()]))
  321. def status(request):
  322. locations = get_locations()
  323. ctx = {
  324. 'title': _("Status"),
  325. 'n_users': VPNUser.objects.filter(expiration__gte=timezone.now()).count(),
  326. 'n_sess': core.current_active_sessions(),
  327. 'n_gws': sum(l['servers'] for cc, l in locations),
  328. 'n_countries': len(set(cc for cc, l in locations)),
  329. 'total_bw': sum(l['bandwidth'] for cc, l in locations),
  330. 'locations': locations,
  331. }
  332. return render(request, 'lambdainst/status.html', ctx)
  333. @user_passes_test(lambda user: user.is_staff)
  334. def admin_status(request):
  335. graph_name = request.GET.get('graph_name')
  336. graph_period = request.GET.get('period')
  337. if graph_period not in ('y', 'm'):
  338. graph_period = 'm'
  339. if graph_name:
  340. if graph_name == 'users':
  341. content = graphs.users_graph(graph_period)
  342. elif graph_name == 'payments_paid':
  343. content = graphs.payments_paid_graph(graph_period)
  344. elif graph_name == 'payments_success':
  345. content = graphs.payments_success_graph(graph_period)
  346. else:
  347. return HttpResponseNotFound()
  348. return HttpResponse(content=content, content_type='image/svg+xml')
  349. payment_status = ((b, b.get_info()) for b in ACTIVE_BACKENDS.values())
  350. payment_status = ((b, i) for (b, i) in payment_status if i)
  351. ctx = {
  352. 'api_status': {k: str(v) for k, v in core_api.info.items()},
  353. 'payment_backends': sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_id),
  354. 'payment_status': payment_status,
  355. }
  356. ctx.update(site.each_context(request))
  357. return render(request, 'lambdainst/admin_status.html', ctx)
  358. @user_passes_test(lambda user: user.is_staff)
  359. def admin_ref(request):
  360. last_week = datetime.now() - timedelta(days=7)
  361. last_month = datetime.now() - timedelta(days=30)
  362. top_ref = User.objects.annotate(n_ref=Count('referrals')).order_by('-n_ref')[:10]
  363. top_ref_week = User.objects.filter(referrals__user__date_joined__gt=last_week) \
  364. .annotate(n_ref=Count('referrals')) \
  365. .order_by('-n_ref')[:10]
  366. top_ref_month = User.objects.filter(referrals__user__date_joined__gt=last_month) \
  367. .annotate(n_ref=Count('referrals')) \
  368. .order_by('-n_ref')[:10]
  369. ctx = {
  370. 'top_ref': top_ref,
  371. 'top_ref_week': top_ref_week,
  372. 'top_ref_month': top_ref_month,
  373. }
  374. ctx.update(site.each_context(request))
  375. return render(request, 'lambdainst/admin_ref.html', ctx)