diff options
-rw-r--r-- | .dir-locals.el | 10 | ||||
-rw-r--r-- | .flake8 | 2 | ||||
-rw-r--r-- | .mypy.ini | 2 | ||||
-rw-r--r-- | bozo4/__init__.py | 88 | ||||
-rw-r--r-- | bozo4/py.typed | 0 |
5 files changed, 102 insertions, 0 deletions
diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..042b2ea --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,10 @@ +((python-ts-mode . ((eval . (flycheck-mode 1)) + (eval . (flycheck-select-checker 'python-mypy)) + (eval . (flycheck-add-next-checker 'python-mypy (cons t 'python-flake8))) + (eval . (add-hook 'before-save-hook 'fmt-current-buffer nil t)) + (eval . (setq-local fmt-executable "black" + fmt-args '("-"))))) + (meson-mode + . ((fmt-executable . "meson") + (fmt-args . ("format" "-")) + (eval . (add-hook 'before-save-hook 'fmt-current-buffer nil t))))) @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120
\ No newline at end of file diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..1d781df --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,2 @@ +[mypy] +strict = true
\ No newline at end of file diff --git a/bozo4/__init__.py b/bozo4/__init__.py new file mode 100644 index 0000000..0f2e2fa --- /dev/null +++ b/bozo4/__init__.py @@ -0,0 +1,88 @@ +import hmac +from urllib.parse import quote as quote_url +from hashlib import sha256 +from datetime import datetime +from typing import Dict + + +def _s3v4_sign(msg: str, key: bytes) -> bytes: + return hmac.new(key, msg.encode(), sha256).digest() + + +def s3v4_datetime_string(dt: datetime) -> str: + return dt.strftime("%Y%m%dT%H%M%SZ") + + +def s3v4_sign_request( + endpoint: str, + region: str, + access_key: str, + secret_key: str, + request_method: str, + date: datetime, + payload_hash: str, + uri: str, + parameters: Dict[str, str], + headers: Dict[str, str], + service: str, +) -> str: + algo = "AWS4-HMAC-SHA256" + datestamp = date.strftime("%Y%m%d") + amzdate = s3v4_datetime_string(date) + + canon_uri = quote_url(uri) + + signed_headers = ";".join( + key for key in sorted(key.lower() for key in headers.keys()) + ) + + quoted_parameters = { + quote_url(key): quote_url(value) for key, value in parameters.items() + } + + canon_parameters = "&".join( + f"{key}={val}" + for key, val in sorted(quoted_parameters.items(), key=lambda p: p[0]) + ) + + def fixup_header_value(val: str) -> str: + return " ".join(w for w in val.strip().split()) + + canon_headers = "".join( + f"{key.lower()}:{fixup_header_value(val)}\n" + for key, val in sorted(headers.items(), key=lambda h: h[0]) + ) + + canon_request = "\n".join( + x + for x in [ + request_method, + canon_uri, + canon_parameters, + canon_headers, + signed_headers, + payload_hash, + ] + ) + + hashed_request = sha256(canon_request.encode()).hexdigest().lower() + + credential_scope = f"{datestamp}/{region}/{service}/aws4_request" + + s = f"{algo}\n{amzdate}\n{credential_scope}\n{hashed_request}" + + k = f"AWS4{secret_key}".encode() + k = _s3v4_sign(msg=datestamp, key=k) + k = _s3v4_sign(msg=region, key=k) + k = _s3v4_sign(msg=service, key=k) + k = _s3v4_sign(msg="aws4_request", key=k) + sig = _s3v4_sign(msg=s, key=k).hex().lower() + + return ", ".join( + x + for x in [ + f"{algo} Credential={access_key}/{credential_scope}", + f"SignedHeaders={signed_headers}", + f"Signature={sig}", + ] + ) diff --git a/bozo4/py.typed b/bozo4/py.typed new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/bozo4/py.typed |