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.

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