reports.py (4672B)
1 """Methods for the report-event-attribution and report-aggregate-attribution endpoints""" 2 import json 3 from typing import List, Optional, Tuple 4 import urllib.parse 5 6 from wptserve.request import Request 7 from wptserve.stash import Stash 8 from wptserve.utils import isomorphic_decode, isomorphic_encode 9 10 # Key used to access the reports in the stash. 11 REPORTS = "4691a2d7fca5430fb0f33b1bd8a9d388" 12 REDIRECT = "9250f93f-2c05-4aae-83b9-2817b0e18b4e" 13 14 CLEAR_STASH = isomorphic_encode("clear_stash") 15 CONFIG_REDIRECT = isomorphic_encode("redirect_to") 16 17 Header = Tuple[str, str] 18 Status = Tuple[int, str] 19 Response = Tuple[Status, List[Header], str] 20 21 def decode_headers(headers: dict) -> dict: 22 """Decodes the headers from wptserve. 23 24 wptserve headers are encoded like 25 { 26 encoded(key): [encoded(value1), encoded(value2),...] 27 } 28 This method decodes the above using the wptserve.utils.isomorphic_decode 29 method 30 """ 31 return { 32 isomorphic_decode(key): [isomorphic_decode(el) for el in value 33 ] for key, value in headers.items() 34 } 35 36 def get_request_origin(request: Request) -> str: 37 return "%s://%s" % (request.url_parts.scheme, 38 request.url_parts.netloc) 39 40 def configure_redirect(request, origin) -> None: 41 with request.server.stash.lock: 42 request.server.stash.put(REDIRECT, origin) 43 return None 44 45 def get_report_redirect_url(request): 46 with request.server.stash.lock: 47 origin = request.server.stash.take(REDIRECT) 48 if origin is None: 49 return None 50 origin_parts = urllib.parse.urlsplit(origin) 51 parts = request.url_parts 52 new_parts = origin_parts._replace(path=bytes(parts.path, 'utf-8')) 53 return urllib.parse.urlunsplit(new_parts) 54 55 def handle_post_report(request: Request, headers: List[Header]) -> Response: 56 """Handles POST request for reports. 57 58 Retrieves the report from the request body and stores the report in the 59 stash. If clear_stash is specified in the query params, clears the stash. 60 """ 61 if request.GET.get(CLEAR_STASH): 62 clear_stash(request.server.stash) 63 return (200, "OK"), headers, json.dumps({ 64 "code": 200, 65 "message": "Stash successfully cleared.", 66 }) 67 68 redirect_origin = request.GET.get(CONFIG_REDIRECT) 69 if redirect_origin: 70 configure_redirect(request, redirect_origin) 71 return (200, "OK"), headers, json.dumps({ 72 "code": 200, 73 "message": "Redirect successfully configured.", 74 }) 75 76 redirect_url = get_report_redirect_url(request) 77 if redirect_url is not None: 78 headers.append(("Location", redirect_url)) 79 return (308, "Permanent Redirect"), headers, json.dumps({ 80 "code": 308 81 }) 82 83 store_report( 84 request.server.stash, get_request_origin(request), { 85 "body": request.body.decode("utf-8"), 86 "headers": decode_headers(request.headers) 87 }) 88 return (201, "OK"), headers, json.dumps({ 89 "code": 201, 90 "message": "Report successfully stored." 91 }) 92 93 94 def handle_get_reports(request: Request, headers: List[Header]) -> Response: 95 """Handles GET request for reports. 96 97 Retrieves and returns all reports from the stash. 98 """ 99 reports = take_reports(request.server.stash, get_request_origin(request)) 100 headers.append(("Access-Control-Allow-Origin", "*")) 101 return (200, "OK"), headers, json.dumps({ 102 "code": 200, 103 "reports": reports, 104 }) 105 106 107 def store_report(stash: Stash, origin: str, report: str) -> None: 108 """Stores the report in the stash. Report here is a JSON.""" 109 with stash.lock: 110 reports_dict = stash.take(REPORTS) 111 if not reports_dict: 112 reports_dict = {} 113 reports = reports_dict.get(origin, []) 114 reports.append(report) 115 reports_dict[origin] = reports 116 stash.put(REPORTS, reports_dict) 117 return None 118 119 def clear_stash(stash: Stash) -> None: 120 "Clears the stash." 121 stash.take(REPORTS) 122 stash.take(REDIRECT) 123 return None 124 125 def take_reports(stash: Stash, origin: str) -> List[str]: 126 """Takes all the reports from the stash and returns them.""" 127 with stash.lock: 128 reports_dict = stash.take(REPORTS) 129 if not reports_dict: 130 reports_dict = {} 131 132 reports = reports_dict.pop(origin, []) 133 stash.put(REPORTS, reports_dict) 134 return reports 135 136 137 def handle_reports(request: Request) -> Response: 138 """Handles request to get or store reports.""" 139 headers = [("Content-Type", "application/json")] 140 if request.method == "POST": 141 return handle_post_report(request, headers) 142 if request.method == "GET": 143 return handle_get_reports(request, headers) 144 return (405, "Method Not Allowed"), headers, json.dumps({ 145 "code": 405, 146 "message": "Only GET or POST methods are supported." 147 })