From 22bbb173b56c9f7718b8fb8390f912d378fa3029 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Tue, 4 Feb 2020 12:27:50 +0000 Subject: [PATCH] zeiterfassung: Validate API responses Signed-off-by: Michael Tremer --- src/backend/zeiterfassung.py | 42 +++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/backend/zeiterfassung.py b/src/backend/zeiterfassung.py index fef5e00b..c999a0ca 100644 --- a/src/backend/zeiterfassung.py +++ b/src/backend/zeiterfassung.py @@ -1,8 +1,9 @@ -#!/usr/bin/python +#!/usr/bin/python3 import hashlib import hmac import json +import logging import tornado.httpclient import urllib.parse @@ -22,7 +23,7 @@ class ZeiterfassungClient(Object): if not all((self.url, self.api_key, self.api_secret)): raise RuntimeError("%s is not configured" % self.__class__.__name__) - def _make_signature(self, method, path, body): + def _sign_request(self, method, path, body): # Empty since we only support POST canonical_query = "" @@ -36,25 +37,50 @@ class ZeiterfassungClient(Object): return h.hexdigest() + def _sign_response(self, body): + h = hmac.new(self.api_secret.encode("utf-8"), body, hashlib.sha512) + + return h.hexdigest() + async def send_request(self, path, **kwargs): url = urllib.parse.urljoin(self.url, path) - # Query arguments are all keyword arguments - arguments = kwargs - request = tornado.httpclient.HTTPRequest(url, method="POST") - request.body = urllib.parse.urlencode(arguments) + request.body = urllib.parse.urlencode(kwargs) # Compose the signature - signature = self._make_signature("POST", path, request.body) + signature = self._sign_request("POST", path, request.body) # Add authorization header request.headers["Authorization"] = " ".join( (self.algorithm, self.api_key, signature) ) + # Log request + logging.debug("Sending request to %s:" % request.url) + for header in sorted(request.headers): + logging.debug(" %s: %s" % (header, request.headers[header])) + # Send the request response = await self.backend.http_client.fetch(request) + # Log response + logging.debug("Got response %s from %s in %.2fms:" % \ + (response.code, response.effective_url, response.request_time * 1000)) + for header in response.headers: + logging.debug(" %s: %s" % (header, response.headers[header])) + + # Fetch the whole body + body = response.body + + # Fetch the signature + signature = response.headers.get("Hash") + if not signature: + raise RuntimeError("Could not find signature on response") + + expected_signature = self._sign_response(body) + if not hmac.compare_digest(expected_signature, signature): + raise RuntimeError("Invalid signature: %s" % signature) + # Decode the JSON response - return json.loads(response.body.decode()) + return json.loads(body) -- 2.47.2