mirror of
https://github.com/bugsink/bugsink.git
synced 2025-12-30 09:50:11 -06:00
Support for CORS
Tested in-browser with:
```
function main() {
$.ajax({
type: "POST",
url: "http://bugsink:8000/api/1/store/",
headers: {
"Content-Type": "application/json",
"X-Sentry-Auth": "Sentry sentry_key=a2df4cd647dc4b7a8a81b78a3601eba1, sentry_version=7, sentry_client=bugsink/0.0.1",
},
data: JSON.stringify({foo: "Bar"}),
success: function(data) {
console.log(data);
}
});
}
```
This commit is contained in:
@@ -17,6 +17,7 @@ from django.views.defaults import permission_denied as django_permission_denied,
|
||||
from django.http import FileResponse, HttpRequest, HttpResponse
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.shortcuts import render
|
||||
from django.views import debug
|
||||
|
||||
from snappea.settings import get_settings as get_snappea_settings
|
||||
|
||||
@@ -28,6 +29,24 @@ from bugsink.decorators import atomic_for_request_method
|
||||
from phonehome.tasks import send_if_due
|
||||
from phonehome.models import Installation
|
||||
|
||||
from ingest.views import BaseIngestAPIView
|
||||
|
||||
|
||||
def cors_for_api_view(view):
|
||||
def inner(request, *args, **kwargs):
|
||||
response = view(request, *args, **kwargs)
|
||||
if request.path.startswith("/api/"):
|
||||
return BaseIngestAPIView._set_cors_headers(response)
|
||||
return response
|
||||
return inner
|
||||
|
||||
|
||||
# The below lines monkey-patch the debug views to add CORS headers for API views in DEBUG=True mode; a small convenience
|
||||
# to not get distracted by CORS errors in the browser console when there's a bug in the API view. Though I generally
|
||||
# avoid monkey-patching, this will only affect me (DEBUG=True) so it should be OK.
|
||||
debug.technical_404_response = cors_for_api_view(debug.technical_404_response)
|
||||
debug.technical_500_response = cors_for_api_view(debug.technical_500_response)
|
||||
|
||||
|
||||
def _phone_home():
|
||||
# I need a way to cron-like run tasks that works for the setup with and without snappea. With snappea it's straight-
|
||||
@@ -137,6 +156,7 @@ def silence_email_system_warning(request):
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
@cors_for_api_view
|
||||
def bad_request(request, exception, template_name=ERROR_400_TEMPLATE_NAME):
|
||||
# verbatim copy of Django's default bad_request view, but with "exception" in the context
|
||||
# doing this for any-old-Django-site is probably a bad idea, but here the security/convenience tradeoff is fine,
|
||||
@@ -158,6 +178,7 @@ def bad_request(request, exception, template_name=ERROR_400_TEMPLATE_NAME):
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
@cors_for_api_view
|
||||
def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
|
||||
# verbatim copy of Django's default server_error view, but with "exception" in the context
|
||||
# doing this for any-old-Django-site is probably a bad idea, but here the security/convenience tradeoff is fine,
|
||||
@@ -181,6 +202,7 @@ def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
@cors_for_api_view
|
||||
def permission_denied(request, exception, template_name=ERROR_403_TEMPLATE_NAME):
|
||||
# (this remark applies 4 times, for 400, 403, 404 and 500):
|
||||
# the check for /api/ is a UX improvement such that we get slightly easier-to-read errors on screen in our own
|
||||
@@ -193,6 +215,7 @@ def permission_denied(request, exception, template_name=ERROR_403_TEMPLATE_NAME)
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
@cors_for_api_view
|
||||
def page_not_found(request, exception, template_name=ERROR_404_TEMPLATE_NAME):
|
||||
if request.path.startswith("/api/"):
|
||||
template_name = "4xx_5xx_api.txt"
|
||||
|
||||
@@ -55,13 +55,48 @@ performance_logger = logging.getLogger("bugsink.performance.ingest")
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BaseIngestAPIView(View):
|
||||
|
||||
@staticmethod
|
||||
def _set_cors_headers(response):
|
||||
# For in-browser SDKs, we need to set the CORS headers, because if we don't, the browser will block the response
|
||||
# from being read and print an error in the console; the longer version is:
|
||||
#
|
||||
# CORS protects the user of a browser from some random website "A" sending requests to the API of some site "B";
|
||||
# implemented by the the browser enforcing the "Access-Control-Allow-Origin" response header as set by server B.
|
||||
# In this case, the "other website B" is the Bugsink server, and the "random website A" is the application that
|
||||
# is being monitored. The user has no relationship with the Bugsink API (there's nothing to protect), so we need
|
||||
# to tell the browser to not protect against Bugsink data from reaching the monitored application, i.e.
|
||||
# Access-Control-Allow-Origin: *.
|
||||
|
||||
response["Access-Control-Allow-Origin"] = "*"
|
||||
response["Access-Control-Allow-Methods"] = "POST, OPTIONS"
|
||||
|
||||
response["Access-Control-Allow-Headers"] = (
|
||||
# The following 2 headers are actually understood by us:
|
||||
"Content-Type, X-Sentry-Auth, "
|
||||
|
||||
# The following list of headers may be sent by Sentry SDKs. Even if we don't use them, we list them, because
|
||||
# any not-listed header is not allowed by the browser and would trip the CORS protection:
|
||||
"X-Requested-With, Origin, Accept, Authentication, Authorization, Content-Encoding, sentry-trace, "
|
||||
"baggage, X-CSRFToken"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def options(self, request, project_pk=None):
|
||||
# This is a CORS preflight request; we just return the headers that the browser expects. (we _could_ check for
|
||||
# the Origin, Request-Method, etc. headers, but we don't need to)
|
||||
result = HttpResponse()
|
||||
self._set_cors_headers(result)
|
||||
result["Access-Control-Max-Age"] = "3600" # tell browser to cache to avoid repeated preflight requests
|
||||
return result
|
||||
|
||||
def post(self, request, project_pk=None):
|
||||
try:
|
||||
return self._post(request, project_pk)
|
||||
return self._set_cors_headers(self._post(request, project_pk))
|
||||
except MaxLengthExceeded as e:
|
||||
return JsonResponse({"message": str(e)}, status=HTTP_400_BAD_REQUEST)
|
||||
return self._set_cors_headers(JsonResponse({"message": str(e)}, status=HTTP_400_BAD_REQUEST))
|
||||
except exceptions.ValidationError as e:
|
||||
return JsonResponse({"message": str(e)}, status=HTTP_400_BAD_REQUEST)
|
||||
return self._set_cors_headers(JsonResponse({"message": str(e)}, status=HTTP_400_BAD_REQUEST))
|
||||
|
||||
@classmethod
|
||||
def get_sentry_key_for_request(cls, request):
|
||||
|
||||
Reference in New Issue
Block a user