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.

347 lines
9.5 KiB

10 years ago
8 years ago
10 years ago
10 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. from datetime import timedelta as td
  2. import time
  3. import uuid
  4. from django.conf import settings
  5. from django.db import connection
  6. from django.http import (
  7. HttpResponse,
  8. HttpResponseForbidden,
  9. HttpResponseNotFound,
  10. JsonResponse,
  11. )
  12. from django.shortcuts import get_object_or_404
  13. from django.utils import timezone
  14. from django.views.decorators.cache import never_cache
  15. from django.views.decorators.csrf import csrf_exempt
  16. from django.views.decorators.http import require_POST
  17. from hc.api import schemas
  18. from hc.api.decorators import authorize, authorize_read, cors, validate_json
  19. from hc.api.models import Flip, Channel, Check, Notification, Ping
  20. from hc.lib.badges import check_signature, get_badge_svg
  21. class BadChannelException(Exception):
  22. pass
  23. @csrf_exempt
  24. @never_cache
  25. def ping(request, code, action="success"):
  26. check = get_object_or_404(Check, code=code)
  27. headers = request.META
  28. remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"])
  29. remote_addr = remote_addr.split(",")[0]
  30. scheme = headers.get("HTTP_X_FORWARDED_PROTO", "http")
  31. method = headers["REQUEST_METHOD"]
  32. ua = headers.get("HTTP_USER_AGENT", "")
  33. body = request.body.decode()
  34. if check.methods == "POST" and method != "POST":
  35. action = "ign"
  36. check.ping(remote_addr, scheme, method, ua, body, action)
  37. response = HttpResponse("OK")
  38. response["Access-Control-Allow-Origin"] = "*"
  39. return response
  40. def _lookup(project, spec):
  41. unique_fields = spec.get("unique", [])
  42. if unique_fields:
  43. existing_checks = Check.objects.filter(project=project)
  44. if "name" in unique_fields:
  45. existing_checks = existing_checks.filter(name=spec.get("name"))
  46. if "tags" in unique_fields:
  47. existing_checks = existing_checks.filter(tags=spec.get("tags"))
  48. if "timeout" in unique_fields:
  49. timeout = td(seconds=spec["timeout"])
  50. existing_checks = existing_checks.filter(timeout=timeout)
  51. if "grace" in unique_fields:
  52. grace = td(seconds=spec["grace"])
  53. existing_checks = existing_checks.filter(grace=grace)
  54. return existing_checks.first()
  55. def _update(check, spec):
  56. channels = set()
  57. # First, validate the supplied channel codes
  58. if "channels" in spec and spec["channels"] not in ("*", ""):
  59. q = Channel.objects.filter(project=check.project)
  60. for s in spec["channels"].split(","):
  61. try:
  62. code = uuid.UUID(s)
  63. except ValueError:
  64. raise BadChannelException("invalid channel identifier: %s" % s)
  65. try:
  66. channels.add(q.get(code=code))
  67. except Channel.DoesNotExist:
  68. raise BadChannelException("invalid channel identifier: %s" % s)
  69. if "name" in spec:
  70. check.name = spec["name"]
  71. if "tags" in spec:
  72. check.tags = spec["tags"]
  73. if "desc" in spec:
  74. check.desc = spec["desc"]
  75. if "timeout" in spec and "schedule" not in spec:
  76. check.kind = "simple"
  77. check.timeout = td(seconds=spec["timeout"])
  78. if "grace" in spec:
  79. check.grace = td(seconds=spec["grace"])
  80. if "schedule" in spec:
  81. check.kind = "cron"
  82. check.schedule = spec["schedule"]
  83. if "tz" in spec:
  84. check.tz = spec["tz"]
  85. check.alert_after = check.going_down_after()
  86. check.save()
  87. # This needs to be done after saving the check, because of
  88. # the M2M relation between checks and channels:
  89. if spec.get("channels") == "*":
  90. check.assign_all_channels()
  91. elif spec.get("channels") == "":
  92. check.channel_set.clear()
  93. elif channels:
  94. check.channel_set.set(channels)
  95. return check
  96. @validate_json()
  97. @authorize_read
  98. def get_checks(request):
  99. q = Check.objects.filter(project=request.project)
  100. q = q.prefetch_related("channel_set")
  101. tags = set(request.GET.getlist("tag"))
  102. for tag in tags:
  103. # approximate filtering by tags
  104. q = q.filter(tags__contains=tag)
  105. checks = []
  106. for check in q:
  107. # precise, final filtering
  108. if not tags or check.matches_tag_set(tags):
  109. checks.append(check.to_dict(readonly=request.readonly))
  110. return JsonResponse({"checks": checks})
  111. @validate_json(schemas.check)
  112. @authorize
  113. def create_check(request):
  114. created = False
  115. check = _lookup(request.project, request.json)
  116. if check is None:
  117. if request.project.num_checks_available() <= 0:
  118. return HttpResponseForbidden()
  119. check = Check(project=request.project)
  120. created = True
  121. try:
  122. _update(check, request.json)
  123. except BadChannelException as e:
  124. return JsonResponse({"error": str(e)}, status=400)
  125. return JsonResponse(check.to_dict(), status=201 if created else 200)
  126. @csrf_exempt
  127. @cors("GET", "POST")
  128. def checks(request):
  129. if request.method == "POST":
  130. return create_check(request)
  131. return get_checks(request)
  132. @cors("GET")
  133. @validate_json()
  134. @authorize
  135. def channels(request):
  136. q = Channel.objects.filter(project=request.project)
  137. channels = [ch.to_dict() for ch in q]
  138. return JsonResponse({"channels": channels})
  139. @validate_json()
  140. @authorize_read
  141. def get_check(request, code):
  142. check = get_object_or_404(Check, code=code)
  143. if check.project != request.project:
  144. return HttpResponseForbidden()
  145. return JsonResponse(check.to_dict(readonly=request.readonly))
  146. @validate_json(schemas.check)
  147. @authorize
  148. def update_check(request, code):
  149. check = get_object_or_404(Check, code=code)
  150. if check.project != request.project:
  151. return HttpResponseForbidden()
  152. try:
  153. _update(check, request.json)
  154. except BadChannelException as e:
  155. return JsonResponse({"error": str(e)}, status=400)
  156. return JsonResponse(check.to_dict())
  157. @validate_json()
  158. @authorize
  159. def delete_check(request, code):
  160. check = get_object_or_404(Check, code=code)
  161. if check.project != request.project:
  162. return HttpResponseForbidden()
  163. response = check.to_dict()
  164. check.delete()
  165. return JsonResponse(response)
  166. @csrf_exempt
  167. @cors("POST", "DELETE", "GET")
  168. def single(request, code):
  169. if request.method == "POST":
  170. return update_check(request, code)
  171. if request.method == "DELETE":
  172. return delete_check(request, code)
  173. return get_check(request, code)
  174. @cors("POST")
  175. @csrf_exempt
  176. @validate_json()
  177. @authorize
  178. def pause(request, code):
  179. check = get_object_or_404(Check, code=code)
  180. if check.project != request.project:
  181. return HttpResponseForbidden()
  182. check.status = "paused"
  183. check.last_start = None
  184. check.alert_after = None
  185. check.save()
  186. return JsonResponse(check.to_dict())
  187. @never_cache
  188. @cors("GET")
  189. def badge(request, badge_key, signature, tag, fmt="svg"):
  190. if not check_signature(badge_key, tag, signature):
  191. return HttpResponseNotFound()
  192. if fmt not in ("svg", "json", "shields"):
  193. return HttpResponseNotFound()
  194. q = Check.objects.filter(project__badge_key=badge_key)
  195. if tag != "*":
  196. q = q.filter(tags__contains=tag)
  197. label = tag
  198. else:
  199. label = settings.MASTER_BADGE_LABEL
  200. status, total, grace, down = "up", 0, 0, 0
  201. for check in q:
  202. if tag != "*" and tag not in check.tags_list():
  203. continue
  204. total += 1
  205. check_status = check.get_status(with_started=False)
  206. if check_status == "down":
  207. down += 1
  208. status = "down"
  209. if fmt == "svg":
  210. # For SVG badges, we can leave the loop as soon as we
  211. # find the first "down"
  212. break
  213. elif check_status == "grace":
  214. grace += 1
  215. if status == "up":
  216. status = "late"
  217. if fmt == "shields":
  218. color = "success"
  219. if status == "down":
  220. color = "critical"
  221. elif status == "late":
  222. color = "important"
  223. return JsonResponse({"label": label, "message": status, "color": color})
  224. if fmt == "json":
  225. return JsonResponse(
  226. {"status": status, "total": total, "grace": grace, "down": down}
  227. )
  228. svg = get_badge_svg(label, status)
  229. return HttpResponse(svg, content_type="image/svg+xml")
  230. @csrf_exempt
  231. @require_POST
  232. def bounce(request, code):
  233. notification = get_object_or_404(Notification, code=code)
  234. # If webhook is more than 10 minutes late, don't accept it:
  235. td = timezone.now() - notification.created
  236. if td.total_seconds() > 600:
  237. return HttpResponseForbidden()
  238. notification.error = request.body.decode()[:200]
  239. notification.save()
  240. notification.channel.last_error = notification.error
  241. if request.GET.get("type") in (None, "Permanent"):
  242. # For permanent bounces, mark the channel as not verified, so we
  243. # will not try to deliver to it again.
  244. notification.channel.email_verified = False
  245. notification.channel.save()
  246. return HttpResponse()
  247. def metrics(request):
  248. if not settings.METRICS_KEY:
  249. return HttpResponseForbidden()
  250. key = request.META.get("HTTP_X_METRICS_KEY")
  251. if key != settings.METRICS_KEY:
  252. return HttpResponseForbidden()
  253. doc = {
  254. "ts": int(time.time()),
  255. "max_ping_id": Ping.objects.values_list("id", flat=True).last(),
  256. "max_notification_id": Notification.objects.values_list("id", flat=True).last(),
  257. "num_unprocessed_flips": Flip.objects.filter(processed__isnull=True).count(),
  258. }
  259. return JsonResponse(doc)
  260. def status(request):
  261. with connection.cursor() as c:
  262. c.execute("SELECT 1")
  263. c.fetchone()
  264. return HttpResponse("OK")