beacon.py (4804B)
1 import json 2 3 from wptserve.utils import isomorphic_decode 4 5 def main(request, response): 6 """Helper handler for Beacon tests. 7 8 It handles two forms of requests: 9 10 STORE: 11 A URL with a query string of the form 'cmd=store&id=<token>'. 12 13 Stores the receipt of a sendBeacon() request along with its validation 14 result, returning HTTP 200 OK. 15 16 if "preflightExpected" exists in the query, this handler responds to 17 CORS preflights. 18 19 STAT: 20 A URL with a query string of the form 'cmd=stat&id=<token>'. 21 22 Retrieves the results of test for the given id and returns them as a 23 JSON array and HTTP 200 OK status code. Due to the eventual read-once 24 nature of the stash, results for a given test are only guaranteed to be 25 returned once, though they may be returned multiple times. 26 27 An entry may contain following members. 28 - error: An error string. null if there is no error. 29 - type: The content-type header of the request "(missing)" if there 30 is no content-type header in the request. 31 32 Example response bodies: 33 - [{error: null, type: "text/plain;charset=UTF8"}] 34 - [{error: "some validation details"}] 35 - [] 36 37 Common parameters: 38 cmd - the command, 'store' or 'stat'. 39 id - the unique identifier of the test. 40 """ 41 42 id = request.GET.first(b"id") 43 command = request.GET.first(b"cmd").lower() 44 45 # Append CORS headers if needed. 46 if b"origin" in request.GET: 47 response.headers.set(b"Access-Control-Allow-Origin", 48 request.GET.first(b"origin")) 49 if b"credentials" in request.GET: 50 response.headers.set(b"Access-Control-Allow-Credentials", 51 request.GET.first(b"credentials")) 52 53 # Handle the 'store' and 'stat' commands. 54 if command == b"store": 55 error = None 56 57 # Only store the actual POST requests, not any preflight/OPTIONS 58 # requests we may get. 59 if request.method == u"POST": 60 payload = b"" 61 contentType = request.headers[b"Content-Type"] \ 62 if b"Content-Type" in request.headers else b"(missing)" 63 if b"form-data" in contentType: 64 if b"payload" in request.POST: 65 # The payload was sent as a FormData. 66 payload = request.POST.first(b"payload") 67 else: 68 # A FormData was sent with an empty payload. 69 pass 70 else: 71 # The payload was sent as either a string, Blob, or BufferSource. 72 payload = request.body 73 74 payload_parts = list(filter(None, payload.split(b":"))) 75 if len(payload_parts) > 1: 76 payload_size = int(payload_parts[0]) 77 78 # Confirm the payload size sent matches with the number of 79 # characters sent. 80 if payload_size != len(payload): 81 error = u"expected %d characters but got %d" % ( 82 payload_size, len(payload)) 83 else: 84 # Confirm the payload contains the correct characters. 85 for i in range(len(payload)): 86 if i <= len(payload_parts[0]): 87 continue 88 c = payload[i:i+1] 89 if c != b"*": 90 error = u"expected '*' at index %d but got '%s''" % ( 91 i, isomorphic_decode(c)) 92 break 93 94 # Store the result in the stash so that it can be retrieved 95 # later with a 'stat' command. 96 request.server.stash.put(id, { 97 u"error": error, 98 u"type": isomorphic_decode(contentType) 99 }) 100 elif request.method == u"OPTIONS": 101 # If we expect a preflight, then add the cors headers we expect, 102 # otherwise log an error as we shouldn't send a preflight for all 103 # requests. 104 if b"preflightExpected" in request.GET: 105 response.headers.set(b"Access-Control-Allow-Headers", 106 b"content-type") 107 response.headers.set(b"Access-Control-Allow-Methods", b"POST") 108 else: 109 error = u"Preflight not expected." 110 request.server.stash.put(id, {u"error": error}) 111 elif command == b"stat": 112 test_data = request.server.stash.take(id) 113 results = [test_data] if test_data else [] 114 115 response.headers.set(b"Content-Type", b"text/plain") 116 response.content = json.dumps(results) 117 else: 118 response.status = 400 # BadRequest