Created
June 15, 2020 16:26
-
-
Save kennyjwilli/aa9e99321d9443a8ae80448974850e79 to your computer and use it in GitHub Desktop.
Code to call API Gateway using utilities copied from Cognitect's aws-api
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cs.aws-api.api-gateway | |
(:require | |
[cognitect.aws.credentials :as aws.credentials] | |
[cognitect.aws.client.shared :as aws.shared] | |
[cs.aws-api.signers :as signers] | |
[java-http-clj.core :as http]) | |
(:import (java.net URI) | |
(java.util Date) | |
(java.text SimpleDateFormat))) | |
(defn- format-date | |
([fmt] | |
(format-date fmt (Date.))) | |
([^ThreadLocal fmt inst] | |
(.format ^SimpleDateFormat (.get fmt) inst))) | |
(defn send-request | |
[request-map] | |
(let [uri (URI. (:uri request-map)) | |
signed-req (signers/v4-sign-http-request | |
"execute-api" | |
"us-west-2" | |
(aws.credentials/fetch (aws.shared/credentials-provider)) | |
(-> request-map | |
(assoc | |
:request-method (:method request-map) | |
:uri (.getPath uri)) | |
(assoc-in [:headers "host"] (.getHost uri)) | |
(assoc-in [:headers "x-amz-date"] | |
(format-date signers/x-amz-date-format (Date.)))) | |
:content-sha256-header? true)] | |
(http/send (-> signed-req | |
(update :headers dissoc "host") | |
(assoc :uri (:uri request-map)))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; Copyright (c) Cognitect, Inc. | |
;; All rights reserved. | |
(ns cs.aws-api.signers | |
(:require [clojure.string :as str]) | |
(:import (java.text SimpleDateFormat) | |
(java.util Date TimeZone) | |
(java.nio ByteBuffer) | |
(java.security MessageDigest) | |
(javax.crypto Mac) | |
(javax.crypto.spec SecretKeySpec) | |
(java.net URI URLDecoder))) | |
(defn ^ThreadLocal date-format | |
"Return a thread-safe GMT date format that can be used with `format-date` and `parse-date`. | |
See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4228335" | |
[^String fmt] | |
(proxy [ThreadLocal] [] | |
(initialValue [] | |
(doto (SimpleDateFormat. fmt) | |
(.setTimeZone (TimeZone/getTimeZone "GMT")))))) | |
(def ^ThreadLocal x-amz-date-format | |
(date-format "yyyyMMdd'T'HHmmss'Z'")) | |
(def ^ThreadLocal x-amz-date-only-format | |
(date-format "yyyyMMdd")) | |
(defn parse-date | |
[^ThreadLocal fmt s] | |
(.parse ^SimpleDateFormat (.get fmt) s)) | |
(defn format-date | |
([fmt] | |
(format-date fmt (Date.))) | |
([^ThreadLocal fmt inst] | |
(.format ^SimpleDateFormat (.get fmt) inst))) | |
(defn credential-scope | |
[{:keys [region service] :as auth-info} request] | |
(str/join "/" [(->> (get-in request [:headers "x-amz-date"]) | |
(parse-date x-amz-date-format) | |
(format-date x-amz-date-only-format)) | |
region | |
service | |
"aws4_request"])) | |
(defn sha-256 | |
"Returns the sha-256 digest (bytes) of data, which can be a | |
byte-array, an input-stream, or nil, in which case returns the | |
sha-256 of the empty string." | |
[data] | |
(cond (string? data) | |
(sha-256 (.getBytes ^String data "UTF-8")) | |
(instance? ByteBuffer data) | |
(sha-256 (.array ^ByteBuffer data)) | |
:else | |
(let [digest (MessageDigest/getInstance "SHA-256")] | |
(when data | |
(.update digest ^bytes data)) | |
(.digest digest)))) | |
(let [hex-chars (char-array [\0 \1 \2 \3 \4 \5 \6 \7 \8 \9 \a \b \c \d \e \f])] | |
(defn hex-encode | |
[^bytes bytes] | |
(let [bl (alength bytes) | |
ca (char-array (* 2 bl))] | |
(loop [i (int 0) | |
c (int 0)] | |
(if (< i bl) | |
(let [b (long (bit-and (long (aget bytes i)) 255))] | |
(aset ca c ^char (aget hex-chars (unsigned-bit-shift-right b 4))) | |
(aset ca (unchecked-inc-int c) (aget hex-chars (bit-and b 15))) | |
(recur (unchecked-inc-int i) (unchecked-add-int c 2))) | |
(String. ca)))))) | |
(defn hashed-body | |
[body] | |
(hex-encode (sha-256 body))) | |
(defn- canonical-headers | |
[headers] | |
(reduce-kv (fn [m k v] | |
(assoc m (str/lower-case k) (-> v str/trim (str/replace #"\s+" " ")))) | |
(sorted-map) | |
headers)) | |
(defn signed-headers | |
[headers] | |
(->> (canonical-headers headers) | |
keys | |
(str/join ";"))) | |
(defn hmac-sha-256 | |
[key ^String data] | |
(let [mac (Mac/getInstance "HmacSHA256")] | |
(.init mac (SecretKeySpec. key "HmacSHA256")) | |
(.doFinal mac (.getBytes data "UTF-8")))) | |
(defn signing-key | |
[request {:keys [secret-access-key region service] :as auth-info}] | |
(-> (.getBytes (str "AWS4" secret-access-key) "UTF-8") | |
(hmac-sha-256 (->> (get-in request [:headers "x-amz-date"]) | |
(parse-date x-amz-date-format) | |
(format-date x-amz-date-only-format))) | |
(hmac-sha-256 region) | |
(hmac-sha-256 service) | |
(hmac-sha-256 "aws4_request"))) | |
(defn- canonical-method | |
[{:keys [request-method]}] | |
(-> request-method name str/upper-case)) | |
(defn uri-encode | |
"Escape (%XX) special characters in the string `s`. | |
Letters, digits, and the characters `_-~.` are never encoded. | |
The optional `extra-chars` specifies extra characters to not encode." | |
([^String s] | |
(when s | |
(uri-encode s ""))) | |
([^String s extra-chars] | |
(when s | |
(let [safe-chars (->> extra-chars | |
(into #{\_ \- \~ \.}) | |
(into #{} (map int))) | |
builder (StringBuilder.)] | |
(doseq [b (.getBytes s "UTF-8")] | |
(.append builder | |
(if (or (Character/isLetterOrDigit ^int b) | |
(contains? safe-chars b)) | |
(char b) | |
(format "%%%02X" b)))) | |
(.toString builder))))) | |
(defn- canonical-uri | |
[{:keys [uri]}] | |
(let [encoded-path (-> uri | |
(str/replace #"//+" "/") ; (URI.) throws Exception on '//'. | |
(str/replace #"\s" "%20"); (URI.) throws Exception on space. | |
(URI.) | |
(.normalize) | |
(.getPath) | |
(uri-encode "/"))] | |
(if (.isEmpty ^String encoded-path) | |
"/" | |
encoded-path))) | |
(defn- canonical-query-string | |
[{:keys [uri query-string]}] | |
(let [qs (or query-string (second (str/split uri #"\?")))] | |
(when-not (str/blank? qs) | |
(->> (str/split qs #"&") | |
(map #(str/split % #"=" 2)) | |
;; TODO (dchelimsky 2019-01-30) decoding first because sometimes | |
;; it's already been encoding. Look into avoiding that! | |
(map (fn [kv] (map #(uri-encode (URLDecoder/decode %)) kv))) | |
(sort (fn [[k1 v1] [k2 v2]] | |
(if (= k1 k2) | |
(compare v1 v2) | |
(compare k1 k2)))) | |
(map (fn [[k v]] (str k "=" v))) | |
(str/join "&"))))) | |
(defn- canonical-headers-string | |
[request] | |
(->> (canonical-headers (:headers request)) | |
(map (fn [[k v]] (str k ":" v "\n"))) | |
(str/join ""))) | |
(defn canonical-request | |
[{:keys [headers body content-length] :as request}] | |
(str/join "\n" [(canonical-method request) | |
(canonical-uri request) | |
(canonical-query-string request) | |
(canonical-headers-string request) | |
(signed-headers (:headers request)) | |
(or (get headers "x-amz-content-sha256") | |
(hashed-body request))])) | |
(defn string-to-sign | |
[request auth-info] | |
(let [bytes (.getBytes ^String (canonical-request request))] | |
(str/join "\n" ["AWS4-HMAC-SHA256" | |
(get-in request [:headers "x-amz-date"]) | |
(credential-scope auth-info request) | |
(hex-encode (sha-256 bytes))]))) | |
(defn signature | |
[auth-info request] | |
(hex-encode | |
(hmac-sha-256 (signing-key request auth-info) | |
(string-to-sign request auth-info)))) | |
(defn v4-sign-http-request | |
[signing-name region credentials http-request & {:keys [content-sha256-header?]}] | |
(let [{:keys [:aws/access-key-id :aws/secret-access-key :aws/session-token]} credentials | |
auth-info {:access-key-id access-key-id | |
:secret-access-key secret-access-key | |
:service signing-name | |
:region region} | |
req (cond-> http-request | |
session-token (assoc-in [:headers "x-amz-security-token"] session-token) | |
content-sha256-header? (assoc-in [:headers "x-amz-content-sha256"] (hashed-body (:body http-request))))] | |
(assoc-in req | |
[:headers "authorization"] | |
(format "AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s" | |
(:access-key-id auth-info) | |
(credential-scope auth-info req) | |
(signed-headers (:headers req)) | |
(signature auth-info req))))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment