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.
 
 
 
 
 
 

231 lines
8.0 KiB

  1. import logging
  2. import itertools
  3. import time
  4. from datetime import timedelta, datetime
  5. from celery import shared_task
  6. from django.db.models import Q, F
  7. from django.db import transaction
  8. from django.conf import settings
  9. from django.utils import timezone
  10. from django.template.loader import get_template
  11. from django.core.mail import send_mass_mail, send_mail
  12. from constance import config as site_config
  13. import django_lcore
  14. import lcoreapi
  15. from ccvpn.common import parse_integer_list
  16. from lambdainst.models import User, VPNUser
  17. ROOT_URL = settings.ROOT_URL
  18. SITE_NAME = settings.TICKETS_SITE_NAME
  19. logger = logging.getLogger(__name__)
  20. @shared_task(autoretry_for=(Exception,), default_retry_delay=60 * 60)
  21. def push_all_users():
  22. count = 0
  23. for u in User.objects.all():
  24. # skip 'empty' accounts
  25. if u.vpnuser.expiration is None:
  26. continue
  27. logger.debug("pushing user #%d %r", user.id, user)
  28. django_lcore.sync_user(u.vpnuser, fail_silently=False)
  29. count += 1
  30. logger.info("pushed %d users", count)
  31. @shared_task(autoretry_for=(Exception,), max_retries=10, retry_backoff=True)
  32. def push_user(user_id):
  33. user = User.objects.get(id=user_id)
  34. logger.info("pushing user #%d %r", user.id, user)
  35. django_lcore.sync_user(user.vpnuser, fail_silently=False)
  36. @shared_task
  37. def notify_vpn_auth_fails():
  38. """
  39. due to the high number of regular authentication fails, we'll start sending
  40. a periodic reminder to accounts with high rates.
  41. it could be a misconfigured client or a payment failure
  42. 3 years, >0 payments: 7 marked, 883 skipped
  43. 6 years, >0 payments: 11 marked, 1582 skipped
  44. 3 years, incl 0 paym: 9 marked, 3590 skipped
  45. 6 years, incl 0 paym: 14 marked, 6313 skipped [12m25s]
  46. """
  47. logger.info("searching for recent openvpn auth failures")
  48. api = django_lcore.api
  49. from_email = settings.DEFAULT_FROM_EMAIL
  50. FETCH_EVENTS_PER_USER = 90
  51. MIN_EXP = timezone.now() - timedelta(days=6 * 365)
  52. MIN_EVENTS = 5 # that threshold is automatically negative
  53. MAX_FAILS = 15 # that threshold is automatically positive
  54. MIN_DT_SINCE_LAST = timedelta(days=1)
  55. NOTIFY_EVERY = timedelta(days=180) # users notified recently should be ignored
  56. def find_fails_for_user(u):
  57. """returns true if there are 'many' recent fails"""
  58. path = api.info["current_instance"] + "/users/" + u.username + "/auth-events/"
  59. try:
  60. r = api.get(path)
  61. except lcoreapi.APINotFoundError:
  62. return False
  63. assert r["object"] == "list", repr(r)
  64. items = list(itertools.islice(r.list_iter(), FETCH_EVENTS_PER_USER))
  65. # split the dataset, errors are ignored
  66. fails = [i for i in items if i["auth_status"] == "fail"]
  67. ok = [i for i in items if i["auth_status"] == "ok"]
  68. # do we have enough failures
  69. if len(fails) == 0:
  70. return False
  71. if len(fails) < MIN_EVENTS:
  72. logger.debug(
  73. f"#{u.id} '{u.username}': too few fails ({len(fails)} < {MIN_EVENTS})"
  74. )
  75. return False
  76. # are they recent
  77. dates = [e["event_date"] for e in fails]
  78. first = min(dates)
  79. last = max(dates)
  80. dt_fails = last - first
  81. dt_last_fail = datetime.utcnow() - last
  82. if dt_last_fail > MIN_DT_SINCE_LAST:
  83. logger.debug(
  84. f"#{u.id} '{u.username}': no recent enough failure ({dt_last_fail} ago > {MIN_DT_SINCE_LAST} ago)"
  85. )
  86. return False
  87. # ~ end of checks. now we filter positive matches ~
  88. # do we have only fails in the recent events?
  89. if len(fails) > MAX_FAILS and len(ok) == 0:
  90. logger.info(f"#{u.id} '{u.username}': {len(fails)} fails and 0 ok")
  91. return True
  92. # do we have many fails per hour?
  93. fails_per_hour = len(fails) / (dt_fails.total_seconds() / 3600.0)
  94. if fails_per_hour > 1:
  95. logger.info(
  96. f"#{u.id} '{u.username}': fail/hour is high ({fails_per_hour:.2f} > 1)"
  97. )
  98. return True
  99. # otherwise there are some of both. let's check the ratio
  100. fail_ratio = len(fails) / len(ok)
  101. if fail_ratio > 1: # more fails than oks
  102. logger.info(
  103. f"#{u.id} '{u.username}': fail ratio is high ({fail_ratio:.2f} > 1)"
  104. )
  105. return True
  106. else:
  107. logger.debug(
  108. f"#{u.id} '{u.username}': fail ratio is low ({fail_ratio:.2f} < 1)"
  109. )
  110. return False
  111. def notify_user(user):
  112. if user.vpnuser.notify_vpn_auth_fail is not True:
  113. logger.warning("this user should not be sent a notice")
  114. return
  115. ctx = dict(user=user, url=ROOT_URL)
  116. text = get_template("lambdainst/mail_vpn_auth_fails.txt").render(ctx)
  117. send_mail("CCrypto VPN Authentication Errors", text, from_email, [user.email])
  118. logger.debug("sending vpn auth fails notify to %s", user.email)
  119. user.vpnuser.last_vpn_auth_fail_notice = timezone.now()
  120. user.vpnuser.save()
  121. def examine_users():
  122. skipped = 0
  123. marked = 0
  124. users = User.objects
  125. # skip accounts that opt out of this
  126. users = users.exclude(vpnuser__notify_vpn_auth_fail=False)
  127. # skip accounts with no email address
  128. users = users.exclude(email__exact="")
  129. # skip accounts where exp was never set
  130. users = users.exclude(vpnuser__expiration=None)
  131. # skip accounts that expired a long time ago
  132. users = users.exclude(vpnuser__expiration__lt=MIN_EXP)
  133. # skip accounts that already received a notice recently
  134. users = users.exclude(
  135. vpnuser__last_vpn_auth_fail_notice__gt=timezone.now() - NOTIFY_EVERY
  136. )
  137. # require at least one payment
  138. # users = users.filter(payment__status='confirmed').annotate(Count('payment')).filter(payment__count__gt=0)
  139. users = users.order_by("id")
  140. for u in users:
  141. if find_fails_for_user(u):
  142. notify_user(u)
  143. marked += 1
  144. else:
  145. skipped += 1
  146. time.sleep(0.100)
  147. logger.debug("auth fails: notified %d, reviewed %d", marked, skipped)
  148. examine_users()
  149. @shared_task
  150. def notify_account_expiration():
  151. """Notify users near the end of their subscription"""
  152. from_email = settings.DEFAULT_FROM_EMAIL
  153. for v in parse_integer_list(site_config.NOTIFY_DAYS_BEFORE):
  154. emails = []
  155. users = list(get_next_expirations(v))
  156. logger.info("sending -%d day notification to %d users", v, len(users))
  157. with transaction.atomic():
  158. for u in users:
  159. # Ignore users with active subscriptions
  160. # They will get notified only if it gets cancelled (payments
  161. # processors will cancel after a few failed payments)
  162. if u.get_subscription():
  163. continue
  164. ctx = dict(
  165. site_name=SITE_NAME, user=u.user, exp=u.expiration, url=ROOT_URL
  166. )
  167. text = get_template("lambdainst/mail_expire_soon.txt").render(ctx)
  168. emails.append(
  169. ("CCrypto VPN Expiration", text, from_email, [u.user.email])
  170. )
  171. logger.debug("sending -%d days notify to %s" % (v, u.user.email))
  172. u.last_expiry_notice = timezone.now()
  173. u.save()
  174. send_mass_mail(emails)
  175. def get_next_expirations(days=3):
  176. """Gets users whose subscription will expire in some days"""
  177. limit_date = timezone.now() + timedelta(days=days)
  178. users = VPNUser.objects.exclude(user__email__exact="")
  179. users = users.filter(expiration__gt=timezone.now()) # Not expired
  180. users = users.filter(expiration__lt=limit_date) # Expire in a few days
  181. # Make sure we dont send the notice twice
  182. users = users.filter(
  183. Q(last_expiry_notice__isnull=True)
  184. | Q(expiration__gt=F("last_expiry_notice") + timedelta(days=days))
  185. )
  186. return users