report.py (5475B)
1 import time 2 import json 3 import re 4 import uuid 5 6 from wptserve.utils import isomorphic_decode 7 8 9 def retrieve_from_stash(request, key, timeout, default_value, min_count=None, retain=False): 10 """Retrieve the set of reports for a given report ID. 11 12 This will extract either the set of reports, credentials, or request count 13 from the stash (depending on the key passed in) and return it encoded as JSON. 14 15 When retrieving reports, this will not return any reports until min_count 16 reports have been received. 17 18 If timeout seconds elapse before the requested data can be found in the stash, 19 or before at least min_count reports are received, default_value will be 20 returned instead.""" 21 t0 = time.time() 22 while time.time() - t0 < timeout: 23 time.sleep(0.5) 24 with request.server.stash.lock: 25 value = request.server.stash.take(key=key) 26 if value is not None: 27 have_sufficient_reports = ( 28 min_count is None or len(value) >= min_count) 29 if retain or not have_sufficient_reports: 30 request.server.stash.put(key=key, value=value) 31 if have_sufficient_reports: 32 return json.dumps(value) 33 34 return default_value 35 36 37 def main(request, response): 38 # Handle CORS preflight requests 39 if request.method == u'OPTIONS': 40 # Always reject preflights for one subdomain 41 if b"www2" in request.headers[b"Origin"]: 42 return (400, [], u"CORS preflight rejected for www2") 43 return [ 44 (b"Content-Type", b"text/plain"), 45 (b"Access-Control-Allow-Origin", b"*"), 46 (b"Access-Control-Allow-Methods", b"post"), 47 (b"Access-Control-Allow-Headers", b"Content-Type"), 48 ], u"CORS allowed" 49 50 # Delete reports as requested 51 if request.method == u'POST': 52 body = json.loads(request.body) 53 if (isinstance(body, dict) and "op" in body): 54 if body["op"] == "DELETE" and "reportIDs" in body: 55 with request.server.stash.lock: 56 for key in body["reportIDs"]: 57 request.server.stash.take(key=key) 58 return "reports cleared" 59 response.status = 400 60 return "op parameter value not recognized" 61 62 if b"reportID" in request.GET: 63 key = request.GET.first(b"reportID") 64 elif b"endpoint" in request.GET: 65 key = uuid.uuid5(uuid.NAMESPACE_OID, isomorphic_decode( 66 request.GET[b'endpoint'])).urn.encode('ascii')[9:] 67 else: 68 response.status = 400 69 return "Either reportID or endpoint parameter is required." 70 71 # Cookie and count keys are derived from the report ID. 72 cookie_key = re.sub(b'^....', b'cccc', key) 73 count_key = re.sub(b'^....', b'dddd', key) 74 75 if request.method == u'GET': 76 try: 77 timeout = float(request.GET.first(b"timeout")) 78 except: 79 timeout = 0.5 80 try: 81 min_count = int(request.GET.first(b"min_count")) 82 except: 83 min_count = 1 84 retain = (b"retain" in request.GET) 85 86 op = request.GET.first(b"op", b"") 87 if op in (b"retrieve_report", b""): 88 return [(b"Content-Type", b"application/json")], retrieve_from_stash(request, key, timeout, u'[]', min_count, retain) 89 90 if op == b"retrieve_cookies": 91 return [(b"Content-Type", b"application/json")], u"{ \"reportCookies\" : " + str(retrieve_from_stash(request, cookie_key, timeout, u"\"None\"")) + u"}" 92 93 if op == b"retrieve_count": 94 return [(b"Content-Type", b"application/json")], u"{ \"report_count\": %s }" % retrieve_from_stash(request, count_key, timeout, 0) 95 96 response.status = 400 97 return "op parameter value not recognized." 98 99 # Save cookies. 100 if len(request.cookies.keys()) > 0: 101 # Convert everything into strings and dump it into a dict. 102 temp_cookies_dict = {} 103 for dict_key in request.cookies.keys(): 104 temp_cookies_dict[isomorphic_decode(dict_key)] = str( 105 request.cookies.get_list(dict_key)) 106 with request.server.stash.lock: 107 # Clear any existing cookie data for this request before storing new data. 108 request.server.stash.take(key=cookie_key) 109 request.server.stash.put(key=cookie_key, value=temp_cookies_dict) 110 111 # Append new report(s). 112 new_reports = json.loads(request.body) 113 114 # If the incoming report is a CSP report-uri report, then it will be a single 115 # dictionary rather than a list of reports. To handle this case, ensure that 116 # any non-list request bodies are wrapped in a list. 117 if not isinstance(new_reports, list): 118 new_reports = [new_reports] 119 120 for report in new_reports: 121 report[u"metadata"] = { 122 u"content_type": isomorphic_decode(request.headers[b"Content-Type"]), 123 } 124 125 with request.server.stash.lock: 126 reports = request.server.stash.take(key=key) 127 if reports is None: 128 reports = [] 129 reports.extend(new_reports) 130 request.server.stash.put(key=key, value=reports) 131 132 # Increment report submission count. This tracks the number of times this 133 # reporting endpoint was contacted, rather than the total number of reports 134 # submitted, which can be seen from the length of the report list. 135 with request.server.stash.lock: 136 count = request.server.stash.take(key=count_key) 137 if count is None: 138 count = 0 139 count += 1 140 request.server.stash.put(key=count_key, value=count) 141 142 # Return acknowledgement report. 143 response_headers = [(b"Content-Type", b"text/plain")] 144 # Keep the same as preflight to not send CORS header for www2 145 if b"www2" not in request.headers[b"Origin"]: 146 response_headers.append((b"Access-Control-Allow-Origin", b"*")) 147 return response_headers, b"Recorded report " + request.body