3232from sentry .attachments import CachedAttachment
3333from sentry .coreapi import (
3434 Auth , APIError , APIForbidden , APIRateLimited , ClientApiHelper , ClientAuthHelper ,
35- SecurityAuthHelper , MinidumpAuthHelper , safely_load_json_string , logger as api_logger
35+ SecurityAuthHelper , MinidumpAuthHelper , safely_load_json_string , logger as api_logger ,
3636)
3737from sentry .event_manager import EventManager
3838from sentry .interfaces import schemas
3939from sentry .interfaces .base import get_interface
40- from sentry .lang .native .unreal import process_unreal_crash , merge_apple_crash_report , \
41- unreal_attachment_type , merge_unreal_context_event , merge_unreal_logs_event
42- from sentry .lang .native .minidump import merge_attached_event , merge_attached_breadcrumbs , write_minidump_placeholder , \
43- MINIDUMP_ATTACHMENT_TYPE
40+ from sentry .lang .native .unreal import (
41+ process_unreal_crash , merge_apple_crash_report ,
42+ unreal_attachment_type , merge_unreal_context_event , merge_unreal_logs_event ,
43+ )
44+ from sentry .lang .native .minidump import (
45+ merge_attached_event , merge_attached_breadcrumbs , write_minidump_placeholder ,
46+ MINIDUMP_ATTACHMENT_TYPE ,
47+ )
4448from sentry .models import Project , OrganizationOption , Organization , File , EventAttachment , Event
4549from sentry .signals import (
46- event_accepted , event_dropped , event_filtered , event_received )
50+ event_accepted , event_dropped , event_filtered , event_received ,
51+ )
4752from sentry .quotas .base import RateLimit
4853from sentry .utils import json , metrics
4954from sentry .utils .data_filters import FilterStatKeys
5257 is_valid_origin ,
5358 get_origins ,
5459 is_same_domain ,
60+ origin_from_request ,
5561)
5662from sentry .utils .outcomes import Outcome , track_outcome
5763from sentry .utils .pubsub import QueuedPublisherService , KafkaPublisher
7783) if getattr (settings , 'KAFKA_RAW_EVENTS_PUBLISHER_ENABLED' , False ) else None
7884
7985
86+ def allow_cors_options (func ):
87+ """
88+ Decorator that adds automatic handling of OPTIONS requests for CORS
89+
90+ If the request is OPTIONS (i.e. pre flight CORS) construct a NO Content (204) response
91+ in which we explicitly enable the caller and add the custom headers that we support
92+ :param func: the original request handler
93+ :return: a request handler that shortcuts OPTIONS requests and just returns an OK (CORS allowed)
94+ """
95+
96+ @wraps (func )
97+ def allow_cors_options_wrapper (self , request , * args , ** kwargs ):
98+
99+ if request .method == 'OPTIONS' :
100+ response = HttpResponse (status = 200 )
101+ response ['Access-Control-Max-Age' ] = '3600' # don't ask for options again for 1 hour
102+ else :
103+ response = func (self , request , * args , ** kwargs )
104+
105+ allow = ', ' .join (self ._allowed_methods ())
106+ response ['Allow' ] = allow
107+ response ['Access-Control-Allow-Methods' ] = allow
108+ response ['Access-Control-Allow-Headers' ] = 'X-Sentry-Auth, X-Requested-With, Origin, Accept, ' \
109+ 'Content-Type, Authentication'
110+ response ['Access-Control-Expose-Headers' ] = 'X-Sentry-Error, Retry-After'
111+
112+ if request .META .get ('HTTP_ORIGIN' ) == 'null' :
113+ origin = 'null' # if ORIGIN header is explicitly specified as 'null' leave it alone
114+ else :
115+ origin = origin_from_request (request )
116+
117+ if origin is None or origin == 'null' :
118+ response ['Access-Control-Allow-Origin' ] = '*'
119+ else :
120+ response ['Access-Control-Allow-Origin' ] = origin
121+
122+ return response
123+
124+ return allow_cors_options_wrapper
125+
126+
80127def api (func ):
81128 @wraps (func )
82129 def wrapped (request , * args , ** kwargs ):
@@ -167,7 +214,7 @@ def process_event(event_manager, project, key, remote_addr, helper, attachments)
167214
168215 # TODO(dcramer): ideally we'd only validate this if the event_id was
169216 # supplied by the user
170- cache_key = 'ev:%s:%s' % (project .id , event_id , )
217+ cache_key = 'ev:%s:%s' % (project .id , event_id ,)
171218
172219 if cache .get (cache_key ) is not None :
173220 track_outcome (
@@ -179,7 +226,7 @@ def process_event(event_manager, project, key, remote_addr, helper, attachments)
179226 event_id = event_id
180227 )
181228 raise APIForbidden (
182- 'An event with the same ID already exists (%s)' % (event_id , ))
229+ 'An event with the same ID already exists (%s)' % (event_id ,))
183230
184231 scrub_ip_address = (org_options .get ('sentry:require_scrub_ip_address' , False ) or
185232 project .get_option ('sentry:scrub_ip_address' , False ))
@@ -301,13 +348,13 @@ def _publish_to_kafka(self, request):
301348
302349 @csrf_exempt
303350 @never_cache
351+ @allow_cors_options
304352 def dispatch (self , request , project_id = None , * args , ** kwargs ):
305353 helper = ClientApiHelper (
306354 agent = request .META .get ('HTTP_USER_AGENT' ),
307355 project_id = project_id ,
308356 ip_address = request .META ['REMOTE_ADDR' ],
309357 )
310- origin = None
311358
312359 if kafka_publisher is not None :
313360 self ._publish_to_kafka (request )
@@ -349,15 +396,15 @@ def dispatch(self, request, project_id=None, *args, **kwargs):
349396 # tsdb could optimize this
350397 metrics .incr ('client-api.all-versions.requests' , skip_internal = False )
351398 metrics .incr ('client-api.all-versions.responses.%s' %
352- (response .status_code , ), skip_internal = False )
399+ (response .status_code ,), skip_internal = False )
353400 metrics .incr (
354401 'client-api.all-versions.responses.%sxx' % (six .text_type (response .status_code )[0 ],),
355402 skip_internal = False ,
356403 )
357404
358405 if helper .context .version :
359406 metrics .incr (
360- 'client-api.v%s.requests' % (helper .context .version , ),
407+ 'client-api.v%s.requests' % (helper .context .version ,),
361408 skip_internal = False ,
362409 )
363410 metrics .incr (
@@ -370,19 +417,6 @@ def dispatch(self, request, project_id=None, *args, **kwargs):
370417 skip_internal = False ,
371418 )
372419
373- if response .status_code != 200 and origin :
374- # We allow all origins on errors
375- response ['Access-Control-Allow-Origin' ] = '*'
376-
377- if origin :
378- response ['Access-Control-Allow-Headers' ] = \
379- 'X-Sentry-Auth, X-Requested-With, Origin, Accept, ' \
380- 'Content-Type, Authentication'
381- response ['Access-Control-Allow-Methods' ] = \
382- ', ' .join (self ._allowed_methods ())
383- response ['Access-Control-Expose-Headers' ] = \
384- 'X-Sentry-Error, Retry-After'
385-
386420 return response
387421
388422 def _dispatch (self , request , helper , project_id = None , origin = None , * args , ** kwargs ):
@@ -403,47 +437,31 @@ def _dispatch(self, request, helper, project_id=None, origin=None, *args, **kwar
403437 None ,
404438 Outcome .INVALID ,
405439 FilterStatKeys .CORS )
406- raise APIForbidden ('Invalid origin: %s' % (origin , ))
440+ raise APIForbidden ('Invalid origin: %s' % (origin ,))
407441
408- # XXX: It seems that the OPTIONS call does not always include custom headers
409- if request .method == 'OPTIONS' :
410- response = self .options (request , project )
411- else :
412- auth = self ._parse_header (request , helper , project )
442+ auth = self ._parse_header (request , helper , project )
413443
414- key = helper .project_key_from_auth (auth )
444+ key = helper .project_key_from_auth (auth )
415445
416- # Legacy API was /api/store/ and the project ID was only available elsewhere
417- if not project :
418- project = Project .objects .get_from_cache (id = key .project_id )
419- helper .context .bind_project (project )
420- elif key .project_id != project .id :
421- raise APIError ('Two different projects were specified' )
422-
423- helper .context .bind_auth (auth )
424-
425- # Explicitly bind Organization so we don't implicitly query it later
426- # this just allows us to comfortably assure that `project.organization` is safe.
427- # This also allows us to pull the object from cache, instead of being
428- # implicitly fetched from database.
429- project .organization = Organization .objects .get_from_cache (
430- id = project .organization_id )
431-
432- response = super (APIView , self ).dispatch (
433- request = request , project = project , auth = auth , helper = helper , key = key , ** kwargs
434- )
446+ # Legacy API was /api/store/ and the project ID was only available elsewhere
447+ if not project :
448+ project = Project .objects .get_from_cache (id = key .project_id )
449+ helper .context .bind_project (project )
450+ elif key .project_id != project .id :
451+ raise APIError ('Two different projects were specified' )
435452
436- if origin :
437- if origin == 'null' :
438- # If an Origin is `null`, but we got this far, that means
439- # we've gotten past our CORS check for some reason. But the
440- # problem is that we can't return "null" as a valid response
441- # to `Access-Control-Allow-Origin` and we don't have another
442- # value to work with, so just allow '*' since they've gotten
443- # this far.
444- response ['Access-Control-Allow-Origin' ] = '*'
445- else :
446- response ['Access-Control-Allow-Origin' ] = origin
453+ helper .context .bind_auth (auth )
454+
455+ # Explicitly bind Organization so we don't implicitly query it later
456+ # this just allows us to comfortably assure that `project.organization` is safe.
457+ # This also allows us to pull the object from cache, instead of being
458+ # implicitly fetched from database.
459+ project .organization = Organization .objects .get_from_cache (
460+ id = project .organization_id )
461+
462+ response = super (APIView , self ).dispatch (
463+ request = request , project = project , auth = auth , helper = helper , key = key , ** kwargs
464+ )
447465
448466 return response
449467
@@ -452,10 +470,15 @@ def _allowed_methods(self):
452470 return [m .upper () for m in self .http_method_names if hasattr (self , m )]
453471
454472 def options (self , request , * args , ** kwargs ):
455- response = HttpResponse ()
456- response ['Allow' ] = ', ' .join (self ._allowed_methods ())
457- response ['Content-Length' ] = '0'
458- return response
473+ """
474+ Serves requests for OPTIONS
475+
476+ NOTE: This function is not called since it is shortcut by the @allow_cors_options descriptor.
477+ It is nevertheless used to construct the allowed http methods and it should not be removed.
478+ """
479+ raise NotImplementedError ("Options request should have been handled by @allow_cors_options.\n "
480+ "If dispatch was overridden either decorate it with @allow_cors_options or provide "
481+ "a valid implementation for options." )
459482
460483
461484class StoreView (APIView ):
@@ -832,7 +855,7 @@ def post(self, request, project, **kwargs):
832855
833856# Endpoint used by the Unreal Engine 4 (UE4) Crash Reporter.
834857class UnrealView (StoreView ):
835- content_types = ('application/octet-stream' , )
858+ content_types = ('application/octet-stream' ,)
836859
837860 def _dispatch (self , request , helper , sentry_key , project_id = None , origin = None , * args , ** kwargs ):
838861 if request .method != 'POST' :
0 commit comments