Permalink
Browse files

initial import of version 1.0.0

  • Loading branch information...
jwr committed Oct 24, 2011
0 parents commit 131ec1060425af3ebd34defaf588dd5b196bd76c
Showing with 159 additions and 0 deletions.
  1. +22 −0 LICENSE
  2. +9 −0 project.clj
  3. +48 −0 src/fablo/api_client.clj
  4. +80 −0 src/fablo/auth.clj
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (C) 2011 Fablo Sp. z o.o.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,9 @@
+(defproject fablo/api-client "1.0.0"
+ :description "Fablo API client"
+ :dependencies [[org.clojure/clojure "1.3.0"]
+ [clj-json "0.4.2"]
+ [commons-io/commons-io "2.0.1"]
+ [commons-codec "1.4"]
+ [clj-http "0.2.1"]
+ [ring/ring-core "0.3.11"]]
+ :dev-dependencies [[swank-clojure "1.3.2"]])
@@ -0,0 +1,48 @@
+(ns fablo.api-client
+ (:require [clj-http.client :as http]
+ [clj-json.core :as json]
+ [fablo.auth :as auth]
+ [clojure.string :as string]))
+
+;;; Bind those dynamically before using API functions
+(def ^:dynamic *api-server* "localhost:8080")
+(def ^:dynamic *api-customer* "dev")
+(def ^:dynamic *api-auth-info* {:key-id "default" :key "example-api-key"})
+
+;;; Our api-request is an http request wrapped in signature processing
+(def api-request (auth/wrap-sign-request #'http/request))
+
+(defmacro def-api-fn [name url-template & {:keys [request-method required-args optional-args url-template-args signature-required]}]
+ (let [url-parameters (set url-template-args)]
+ `(defn ~name [~@required-args & {:keys ~(vec (conj optional-args 'api-server 'api-customer 'api-auth-info))}]
+ (let [request-map# (merge
+ {:url (str "http://"
+ (string/join "/"
+ [(or ~'api-server ~'*api-server*) "api/2" (or ~'api-customer ~'*api-customer*)
+ (format ~url-template ~@url-template-args)]))
+ :method ~(or request-method :get)
+ :query-params (merge ~(into {} (map #(vector (str %) %) (remove url-parameters required-args)))
+ ~@(map (fn [x] `(if ~x {~(str x) (if (string? ~x) ~x (json/generate-string ~x))} {}))
+ optional-args))}
+ ~(when signature-required
+ `(when-let [auth-info# (or ~'api-auth-info ~'*api-auth-info*)]
+ {:amazon-aws-auth [(or (:key-id auth-info#) "default") (:key auth-info#)]})))
+ response# (api-request request-map#)
+ result# (:status response#)]
+ (when (and (>= result# 200) (< result# 300))
+ (json/parse-string (:body response#)))))))
+
+(def-api-fn products-query "products/query" :optional-args [search-string start results category prefilter attributes return])
+(def-api-fn product "products/id/%s" :required-args [id] :optional-args [return] :url-template-args [id])
+(def-api-fn product-variants-query "products/id/%s/variants/query" :required-args [id] :optional-args [options return] :url-template-args [id])
+(def-api-fn product-categories "products/categories" :optional-args [level])
+
+(def-api-fn vendors "vendors")
+(def-api-fn vendors-query "vendors/query" :optional-args [category])
+
+(def-api-fn vendor-by-id "vendors/id/%s" :required-args [id] :url-template-args [id])
+
+(def-api-fn vendor-categories "vendors/categories" :optional-args [level])
+
+(def-api-fn promotions "promotions")
+(def-api-fn promotions-random "promotions/random" :optional-args [number])
@@ -0,0 +1,80 @@
+(ns fablo.auth
+ (:require [clojure.string :as string]
+ [ring.util.codec :as codec])
+ (:import [javax.crypto Mac]
+ [javax.crypto.spec SecretKeySpec]))
+
+(defn- hmac [algorithm data key]
+ (when key
+ (let [signing-key (SecretKeySpec. (.getBytes key) algorithm)
+ mac (Mac/getInstance algorithm)
+ encoder (sun.misc.BASE64Encoder.)]
+ (.init mac signing-key)
+ (string/trim-newline (.encodeBuffer encoder (.doFinal mac (.getBytes data)))))))
+
+(def ^:private hmac-sha1 (partial hmac "HmacSHA1"))
+
+(def ^:private hmac-sha256 (partial hmac "HmacSHA256"))
+
+(defmacro ^:private replace-all
+ ([str] str)
+ ([str k v] `(string/replace ~str ~k ~v))
+ ([str k v & args] `(replace-all (string/replace ~str ~k ~v) ~@args)))
+
+;; Java's url-encoder has a bit different semantics than the one
+;; mandated by AWS, so we provide a translation layer:
+(defn url-encode-with-aws-semantics [s]
+ (replace-all (codec/url-encode s)
+ "*" "%2A"
+ "%7E" "~"
+ "+" "%20"))
+
+(defn canonicalize-query-string [req]
+ (let [names-and-values (remove #(= (first %) "Signature") (:params req))
+ sorted-nv-pairs (sort-by first names-and-values)
+ canonicalized-pairs (map (fn [[k v]] (str k "=" (url-encode-with-aws-semantics v))) sorted-nv-pairs)]
+ (string/join "&" canonicalized-pairs)))
+
+(defn string-to-sign [req]
+ (string/join
+ "\n"
+ [(-> (or (:request-method req) (:method req)) name .toUpperCase)
+ (or ((or (:headers req) {}) "host") "")
+ (:uri req)
+ (canonicalize-query-string req)]))
+
+(defn check-signature
+ "Check signature according to Amazon AWS specs. Takes a ring request and a key (string) as parameters, returns a
+ vector with true/false indicating whether authentication succeeded and a string with the reason for failure."
+ [{:keys [params] :as req} key]
+ (let [version (params "SignatureVersion")
+ method (params "SignatureMethod")
+ key-id (params "AWSAccessKeyId")
+ signature (params "Signature")]
+ (assert key)
+ (cond
+ (not (and version method key-id signature))
+ [false "Missing SignatureVersion, SignatureMethod, AWSAccessKeyId or Signature"]
+
+ (not (#{"HmacSHA1" "HmacSHA256"} method))
+ [false "Unknown signature method, accepted methods are HmacSHA1 and HmacSHA256"]
+
+ (not= version "2")
+ [false "Unsupported signature version, only version 2 is supported"]
+
+ (not= signature (hmac method (string-to-sign req) key))
+ [false (pr-str "Signature verification failed, got " signature)]
+
+ true
+ [true "Signature verification succesful"])))
+
+(defn wrap-sign-request [client]
+ (fn [req]
+ (if-let [[key-id key] (:amazon-aws-auth req)]
+ (let [headers (merge {"SignatureVersion" "2"
+ "SignatureMethod" "HmacSHA256"
+ "AWSAccessKeyId" key-id}
+ (:headers req))
+ signature (hmac-sha256 (string-to-sign (assoc req :headers headers)) key)]
+ (client (assoc (dissoc req :amazon-aws-auth) :headers (assoc headers "Signature" signature))))
+ (client req))))

0 comments on commit 131ec10

Please sign in to comment.