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.

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