]>
Commit | Line | Data |
---|---|---|
22bbb173 | 1 | #!/usr/bin/python3 |
2c361abc MT |
2 | |
3 | import hashlib | |
4 | import hmac | |
5 | import json | |
22bbb173 | 6 | import logging |
2c361abc | 7 | import tornado.httpclient |
11347e46 | 8 | import urllib.parse |
2c361abc | 9 | |
11347e46 | 10 | from .misc import Object |
2c361abc MT |
11 | |
12 | class ZeiterfassungClient(Object): | |
13 | algorithm = "Zeiterfassung-HMAC-SHA512" | |
14 | ||
15 | def init(self): | |
16 | self.url = self.settings.get("zeiterfassung_url") | |
17 | ||
18 | # API credentials | |
19 | self.api_key = self.settings.get("zeiterfassung_api_key") | |
20 | self.api_secret = self.settings.get("zeiterfassung_api_secret") | |
21 | ||
22 | # Check if all configuration values are set | |
23 | if not all((self.url, self.api_key, self.api_secret)): | |
24 | raise RuntimeError("%s is not configured" % self.__class__.__name__) | |
25 | ||
22bbb173 | 26 | def _sign_request(self, method, path, body): |
2c361abc MT |
27 | # Empty since we only support POST |
28 | canonical_query = "" | |
29 | ||
30 | # Put everything together | |
31 | string_to_sign = "\n".join(( | |
32 | method, path, canonical_query, | |
33 | )).encode("utf-8") + body | |
34 | ||
35 | # Compute HMAC | |
36 | h = hmac.new(self.api_secret.encode("utf-8"), string_to_sign, hashlib.sha512) | |
37 | ||
38 | return h.hexdigest() | |
39 | ||
22bbb173 MT |
40 | def _sign_response(self, body): |
41 | h = hmac.new(self.api_secret.encode("utf-8"), body, hashlib.sha512) | |
42 | ||
43 | return h.hexdigest() | |
44 | ||
9fdf4fb7 | 45 | async def send_request(self, path, **kwargs): |
11347e46 | 46 | url = urllib.parse.urljoin(self.url, path) |
2c361abc | 47 | |
2c361abc | 48 | request = tornado.httpclient.HTTPRequest(url, method="POST") |
22bbb173 | 49 | request.body = urllib.parse.urlencode(kwargs) |
2c361abc MT |
50 | |
51 | # Compose the signature | |
22bbb173 | 52 | signature = self._sign_request("POST", path, request.body) |
2c361abc MT |
53 | |
54 | # Add authorization header | |
55 | request.headers["Authorization"] = " ".join( | |
56 | (self.algorithm, self.api_key, signature) | |
57 | ) | |
58 | ||
22bbb173 MT |
59 | # Log request |
60 | logging.debug("Sending request to %s:" % request.url) | |
61 | for header in sorted(request.headers): | |
62 | logging.debug(" %s: %s" % (header, request.headers[header])) | |
f38c875a MT |
63 | if request.body: |
64 | logging.debug("%s" % json.dumps(kwargs, indent=4, sort_keys=True)) | |
22bbb173 | 65 | |
2c361abc | 66 | # Send the request |
9fdf4fb7 | 67 | response = await self.backend.http_client.fetch(request) |
2c361abc | 68 | |
22bbb173 MT |
69 | # Log response |
70 | logging.debug("Got response %s from %s in %.2fms:" % \ | |
71 | (response.code, response.effective_url, response.request_time * 1000)) | |
72 | for header in response.headers: | |
73 | logging.debug(" %s: %s" % (header, response.headers[header])) | |
74 | ||
75 | # Fetch the whole body | |
76 | body = response.body | |
f38c875a MT |
77 | if body: |
78 | # Decode the JSON response | |
79 | body = json.loads(body) | |
80 | ||
81 | # Log what we have received in a human-readable way | |
82 | logging.debug("%s" % json.dumps(body, indent=4, sort_keys=True)) | |
22bbb173 MT |
83 | |
84 | # Fetch the signature | |
85 | signature = response.headers.get("Hash") | |
86 | if not signature: | |
87 | raise RuntimeError("Could not find signature on response") | |
88 | ||
f38c875a | 89 | expected_signature = self._sign_response(response.body) |
22bbb173 MT |
90 | if not hmac.compare_digest(expected_signature, signature): |
91 | raise RuntimeError("Invalid signature: %s" % signature) | |
92 | ||
f38c875a MT |
93 | # Return the body |
94 | return body |