Skip to content

Instantly share code, notes, and snippets.

@kennyjwilli
Created June 15, 2020 16:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kennyjwilli/aa9e99321d9443a8ae80448974850e79 to your computer and use it in GitHub Desktop.
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
(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))))))
;; 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