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.
 
 
 
 
 
 

545 lines
16 KiB

  1. from math import ceil
  2. import base64
  3. import hmac
  4. from datetime import datetime, timedelta
  5. from hashlib import sha256
  6. from urllib.parse import parse_qsl, urlencode
  7. import requests
  8. from constance import config as site_config
  9. from django.conf import settings as project_settings
  10. from django.contrib import auth, messages
  11. from django.contrib.admin.sites import site
  12. from django.contrib.auth.decorators import login_required, user_passes_test
  13. from django.contrib.auth.models import User
  14. from django.db.models import Count
  15. from django.db import transaction
  16. from django.http import (
  17. HttpResponse,
  18. HttpResponseNotFound,
  19. HttpResponseRedirect,
  20. JsonResponse,
  21. )
  22. from django.shortcuts import redirect, render
  23. from django.utils import timezone
  24. from django.utils.translation import ugettext as _
  25. from django.template.loader import render_to_string
  26. from django_countries import countries
  27. import django_lcore
  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, WgPeerForm
  32. from .models import VPNUser
  33. from . import graphs
  34. def get_locations():
  35. """Pretty bad thing that returns get_locations() with translated stuff
  36. that depends on the request
  37. """
  38. countries_d = dict(countries)
  39. locations = django_lcore.get_clusters()
  40. for (_), loc in locations:
  41. code = loc["country_code"].upper()
  42. loc["country_name"] = countries_d.get(code, code)
  43. loc["load_percent"] = ceil(
  44. (loc["usage"].get("net", 0) / (loc["bandwidth"] / 1e6)) * 100
  45. )
  46. locations = list(sorted(locations, key=lambda l: l[0]))
  47. return locations
  48. def log_errors(request, form):
  49. errors = []
  50. for field in form:
  51. for e in field.errors:
  52. errors.append(e)
  53. messages.add_message(request, messages.ERROR, ", ".join(errors))
  54. def ca_crt(request):
  55. return HttpResponse(
  56. content=project_settings.OPENVPN_CA, content_type="application/x-x509-ca-cert"
  57. )
  58. def logout(request):
  59. auth.logout(request)
  60. return redirect("index")
  61. def signup(request):
  62. if request.user.is_authenticated:
  63. return redirect("account:index")
  64. if request.method != "POST":
  65. form = SignupForm()
  66. return render(request, "ccvpn/signup.html", dict(form=form))
  67. form = SignupForm(request.POST)
  68. grr = request.POST.get("g-recaptcha-response", "")
  69. if captcha_test(grr, request):
  70. request.session["signup_captcha_pass"] = True
  71. elif not request.session.get("signup_captcha_pass"):
  72. messages.error(request, _("Invalid captcha. Please try again"))
  73. return render(request, "ccvpn/signup.html", dict(form=form))
  74. if not form.is_valid():
  75. return render(request, "ccvpn/signup.html", dict(form=form))
  76. user = User.objects.create_user(
  77. form.cleaned_data["username"],
  78. form.cleaned_data["email"],
  79. form.cleaned_data["password"],
  80. )
  81. user.save()
  82. try:
  83. user.vpnuser
  84. except VPNUser.DoesNotExist:
  85. user.vpnuser = VPNUser.objects.create(user=user)
  86. user.vpnuser.save()
  87. try:
  88. user.vpnuser.referrer = User.objects.get(id=request.session.get("referrer"))
  89. except User.DoesNotExist:
  90. pass
  91. user.vpnuser.campaign = request.session.get("campaign")
  92. user.vpnuser.add_paid_time(timedelta(days=7), "trial")
  93. user.vpnuser.save()
  94. user.vpnuser.lcore_sync()
  95. user.backend = "django.contrib.auth.backends.ModelBackend"
  96. auth.login(request, user)
  97. # invalidate that captcha
  98. request.session["signup_captcha_pass"] = False
  99. return redirect("account:index")
  100. @login_required
  101. def discourse_login(request):
  102. sso_secret = project_settings.DISCOURSE_SECRET
  103. discourse_url = project_settings.DISCOURSE_URL
  104. if project_settings.DISCOURSE_SSO is not True:
  105. return HttpResponseNotFound()
  106. payload = request.GET.get("sso", "")
  107. signature = request.GET.get("sig", "")
  108. expected_signature = hmac.new(
  109. sso_secret.encode("utf-8"), payload.encode("utf-8"), sha256
  110. ).hexdigest()
  111. if signature != expected_signature:
  112. return HttpResponseNotFound()
  113. if request.method == "POST" and "email" in request.POST:
  114. form = ReqEmailForm(request.POST)
  115. if not form.is_valid():
  116. return render(request, "ccvpn/require_email.html", dict(form=form))
  117. request.user.email = form.cleaned_data["email"]
  118. request.user.save()
  119. if not request.user.email:
  120. form = ReqEmailForm()
  121. return render(request, "ccvpn/require_email.html", dict(form=form))
  122. try:
  123. payload = base64.b64decode(payload).decode("utf-8")
  124. payload_data = dict(parse_qsl(payload))
  125. except (TypeError, ValueError):
  126. return HttpResponseNotFound()
  127. payload_data.update(
  128. {
  129. "external_id": request.user.id,
  130. "username": request.user.username,
  131. "email": request.user.email,
  132. "require_activation": "true",
  133. }
  134. )
  135. payload = urlencode(payload_data)
  136. payload = base64.b64encode(payload.encode("utf-8"))
  137. signature = hmac.new(sso_secret.encode("utf-8"), payload, sha256).hexdigest()
  138. redirect_query = urlencode(dict(sso=payload, sig=signature))
  139. redirect_path = "/session/sso_login?" + redirect_query
  140. return HttpResponseRedirect(discourse_url + redirect_path)
  141. @login_required
  142. def index(request):
  143. ref_url = project_settings.ROOT_URL + "?ref=" + str(request.user.id)
  144. class price_fn:
  145. """Clever hack to get the price in templates with {{price.3}} with
  146. 3 an arbitrary number of months
  147. """
  148. def __getitem__(self, months):
  149. n = int(months) * get_price_float()
  150. c = project_settings.PAYMENTS_CURRENCY[1]
  151. return "%.2f %s" % (n, c)
  152. context = dict(
  153. title=_("Account"),
  154. ref_url=ref_url,
  155. subscription=request.user.vpnuser.get_subscription(),
  156. backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_display_name),
  157. subscr_backends=sorted(
  158. (b for b in ACTIVE_BACKENDS.values() if b.backend_has_recurring),
  159. key=lambda x: x.backend_id,
  160. ),
  161. default_backend="paypal",
  162. price=price_fn(),
  163. user_motd=site_config.MOTD_USER,
  164. )
  165. return render(request, "lambdainst/account.html", context)
  166. def captcha_test(grr, request):
  167. api_url = project_settings.HCAPTCHA_API
  168. if not project_settings.HCAPTCHA_SITE_KEY:
  169. return True
  170. if api_url == "TEST" and grr == "TEST-TOKEN":
  171. # FIXME: i'm sorry.
  172. return True
  173. data = dict(secret=project_settings.HCAPTCHA_SECRET_KEY, response=grr)
  174. try:
  175. r = requests.post(api_url, data=data)
  176. r.raise_for_status()
  177. d = r.json()
  178. return d.get("success")
  179. except (requests.ConnectionError, requests.HTTPError, ValueError):
  180. return False
  181. def make_export_zip(user, name):
  182. import io
  183. import zipfile
  184. import json
  185. f = io.BytesIO()
  186. z = zipfile.ZipFile(f, mode="w")
  187. gw_cache = {}
  188. def process_wg_peer(item):
  189. keys = {
  190. "gateway_port",
  191. "id",
  192. "local_ipv4",
  193. "local_ipv6",
  194. "name",
  195. "object",
  196. "private_key",
  197. "public_key",
  198. }
  199. return {k: v for (k, v) in item.items() if k in keys}
  200. def process_ovpn_sess(item):
  201. keys = {
  202. "connect_date",
  203. "disconnect_date",
  204. "remote",
  205. "object",
  206. "protocol",
  207. "id",
  208. "stats",
  209. "tunnel",
  210. }
  211. def convert(v):
  212. if isinstance(v, datetime):
  213. return v.isoformat()
  214. return v
  215. obj = {k: convert(v) for (k, v) in item.items() if k in keys}
  216. gw_url = item["gateway"]["href"]
  217. if gw_url not in gw_cache:
  218. gw = django_lcore.api.get(gw_url)
  219. gw_cache[gw_url] = {
  220. "name": gw["name"],
  221. "ipv4": gw["main_addr"]["ipv4"],
  222. "ipv6": gw["main_addr"]["ipv6"],
  223. }
  224. obj["gateway"] = gw_cache[gw_url]
  225. return obj
  226. def process_payments(item):
  227. obj = {
  228. "id": item.id,
  229. "backend": item.backend_id,
  230. "status": item.status,
  231. "created": item.created.isoformat(),
  232. "confirmed": item.confirmed_on.isoformat() if item.confirmed_on else None,
  233. "amount": item.amount / 100,
  234. "paid_amount": item.amount / 100,
  235. "time": item.time.total_seconds(),
  236. "external_id": item.backend_extid,
  237. }
  238. if item.subscription:
  239. obj["subscription"] = item.subscription.backend_extid
  240. else:
  241. obj["subscription"] = None
  242. return obj
  243. with z.open(name + "/account.json", "w") as jf:
  244. jf.write(
  245. json.dumps(
  246. {
  247. "username": user.username,
  248. "email": user.email,
  249. "date_joined": user.date_joined.isoformat(),
  250. "expiration": user.vpnuser.expiration.isoformat()
  251. if user.vpnuser.expiration
  252. else None,
  253. },
  254. indent=2,
  255. ).encode("ascii")
  256. )
  257. with z.open(name + "/wireguard_peers.json", "w") as jf:
  258. try:
  259. keys = list(
  260. map(process_wg_peer, django_lcore.api.get_wg_peers(user.username))
  261. )
  262. except lcoreapi.APINotFoundError:
  263. keys = []
  264. jf.write(json.dumps(keys, indent=2).encode("ascii"))
  265. with z.open(name + "/openvpn_logs.json", "w") as jf:
  266. base = django_lcore.api.info["current_instance"]
  267. next_page = "/users/" + user.username + "/sessions/"
  268. try:
  269. items = django_lcore.api.get(base + next_page).list_iter()
  270. except lcoreapi.APINotFoundError:
  271. items = []
  272. items = list(map(process_ovpn_sess, items))
  273. jf.write(json.dumps(items, indent=2).encode("ascii"))
  274. with z.open(name + "/payments.json", "w") as jf:
  275. items = user.payment_set.all()
  276. items = list(map(process_payments, items))
  277. jf.write(json.dumps(items, indent=2).encode("ascii"))
  278. z.close()
  279. return f.getvalue()
  280. def deactivate_user(user):
  281. """clear most information from a user, keeping the username and id"""
  282. user.vpnuser.clear_fields()
  283. user.vpnuser.save()
  284. user.is_active = False
  285. user.email = ""
  286. user.password = ""
  287. user.save()
  288. user.payment_set.update(backend_data="null")
  289. user.subscription_set.update(backend_data="null")
  290. django_lcore.sync_user(user.vpnuser)
  291. @login_required
  292. def settings(request):
  293. can_delete = request.user.vpnuser.get_subscription() is None
  294. if request.method != "POST":
  295. return render(
  296. request,
  297. "lambdainst/settings.html",
  298. dict(
  299. can_delete=can_delete,
  300. ),
  301. )
  302. action = request.POST.get("action")
  303. current_pw = request.POST.get("current_password")
  304. if not request.user.check_password(current_pw):
  305. messages.error(request, _("Invalid password"))
  306. return redirect("lambdainst:account_settings")
  307. if action == "email":
  308. email = request.POST.get("email")
  309. if email:
  310. request.user.email = email
  311. messages.success(request, _("OK! Email address changed."))
  312. else:
  313. request.user.email = ""
  314. messages.success(request, _("OK! Email address unset."))
  315. request.user.save()
  316. return redirect("lambdainst:account_settings")
  317. elif action == "password":
  318. pw = request.POST.get("password")
  319. pw2 = request.POST.get("password2")
  320. if pw != pw2 or not pw:
  321. messages.error(request, _("Password and confirmation do not match"))
  322. else:
  323. request.user.set_password(pw)
  324. messages.success(request, _("OK! Password changed."))
  325. request.user.save()
  326. django_lcore.sync_user(request.user.vpnuser)
  327. return redirect("lambdainst:account_settings")
  328. elif action == "export":
  329. timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
  330. data = make_export_zip(request.user, timestamp)
  331. r = HttpResponse(content=data, content_type="application/zip")
  332. r[
  333. "Content-Disposition"
  334. ] = 'attachment; filename="ccvpn-export-{}-{}.zip"'.format(
  335. request.user.username, timestamp
  336. )
  337. return r
  338. elif action == "delete" and can_delete:
  339. with transaction.atomic():
  340. deactivate_user(request.user)
  341. logout(request)
  342. messages.success(request, _("OK! Your account has been deactivated."))
  343. return redirect("/")
  344. return render(
  345. request,
  346. "lambdainst/settings.html",
  347. dict(
  348. title=_("Settings"),
  349. user=request.user,
  350. can_delete=can_delete,
  351. ),
  352. )
  353. @login_required
  354. def config(request):
  355. return render(
  356. request,
  357. "lambdainst/config.html",
  358. dict(
  359. title=_("Config"),
  360. config_os=django_lcore.openvpn.CONFIG_OS,
  361. config_countries=(c for _, c in get_locations()),
  362. config_protocols=django_lcore.openvpn.PROTOCOLS,
  363. ),
  364. )
  365. def api_locations(request):
  366. def format_loc(cc, l):
  367. msg = ""
  368. tags = l.get("tags", {})
  369. message = tags.get("message")
  370. if message:
  371. msg = " [%s]" % message
  372. return {
  373. "country_name": l["country_name"] + msg,
  374. "country_code": cc,
  375. "hostname": l["hostname"],
  376. "bandwidth": l["bandwidth"],
  377. "servers": l["servers"],
  378. }
  379. return JsonResponse(
  380. dict(locations=[format_loc(cc, l) for cc, l in get_locations()])
  381. )
  382. def status(request):
  383. locations = get_locations()
  384. ctx = {
  385. "title": _("Servers"),
  386. "locations": locations,
  387. }
  388. return render(request, "lambdainst/status.html", ctx)
  389. @login_required
  390. def wireguard(request):
  391. api = django_lcore.api
  392. if request.method == "POST":
  393. action = request.POST.get("action")
  394. if action == "delete_key":
  395. key = api.get_wg_peer(request.user.username, request.POST.get("peer_id"))
  396. if key:
  397. key.delete()
  398. elif action == "set_name":
  399. key = api.get_wg_peer(request.user.username, request.POST.get("peer_id"))
  400. if key:
  401. name = request.POST.get("name")
  402. if name:
  403. name = name[:21]
  404. key.rename(name)
  405. return redirect(request.path)
  406. try:
  407. keys = api.get_wg_peers(request.user.username)
  408. except lcoreapi.APINotFoundError:
  409. django_lcore.sync_user(request.user.vpnuser)
  410. keys = []
  411. context = dict(
  412. can_create_key=len(keys) < int(site_config.WIREGUARD_MAX_PEERS),
  413. menu_item="wireguard",
  414. enabled=request.user.vpnuser.is_paid,
  415. config_countries=[(k, v) for (k, v) in django_lcore.get_clusters()],
  416. keys=keys,
  417. locations=get_locations(),
  418. )
  419. return render(request, "lambdainst/wireguard.html", context)
  420. @login_required
  421. def wireguard_new(request):
  422. if not request.user.vpnuser.is_paid:
  423. return redirect("account:index")
  424. try:
  425. keys = django_lcore.api.get_wg_peers(request.user.username)
  426. except lcoreapi.APINotFoundError:
  427. django_lcore.sync_user(request.user.vpnuser)
  428. keys = []
  429. if len(keys) >= int(site_config.WIREGUARD_MAX_PEERS):
  430. return redirect("/account/wireguard")
  431. api = django_lcore.api
  432. if request.method == "POST":
  433. action = request.POST.get("action")
  434. form = WgPeerForm(request.POST)
  435. if action == "add_key":
  436. if form.is_valid():
  437. api.create_wg_peer(
  438. request.user.username,
  439. public_key=form.cleaned_data["public_key"],
  440. name=form.cleaned_data["name"],
  441. )
  442. else:
  443. log_errors(request, form)
  444. return redirect("/account/wireguard")
  445. context = dict(
  446. menu_item="wireguard",
  447. locations=get_locations(),
  448. )
  449. return render(request, "lambdainst/wireguard_new.html", context)