{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE QuasiQuotes #-} module Akamai.Edgegrid ( Auth(..) , mkReq , mkJsonReq ) where import Crypto.Hash.Algorithms import Crypto.MAC.HMAC import Crypto.Hash import Data.UUID.V4 (nextRandom) import Data.UUID (toASCIIBytes) import Data.CaseInsensitive (original) import Data.Aeson import Data.Aeson.TH import Data.ByteArray.Encoding (convertToBase, Base (Base64)) import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Lazy as BL import Data.Char (toLower) import Data.Text (Text) import qualified Data.Text.Encoding as T import Data.UnixTime hiding (UnixTime) import Network.HTTP.Client.Conduit (RequestBody(..)) import Network.HTTP.Simple import Network.HTTP.Types (hAuthorization, HeaderName) type Message = ByteString type AuthSign = ByteString type Nonce = ByteString type Method = ByteString type PathWithQuery = ByteString type CanonicalizedRequestHeaders = [(HeaderName,ByteString)] type Body = ByteString data Auth = Auth { authHostname :: Text , authAccessToken :: Text , authClientToken :: Text , authClientSecret :: Text } deriving (Show,Read,Eq) $(deriveJSON defaultOptions{fieldLabelModifier = (map toLower) . (drop 4)} ''Auth) -- | dataToSign -- -- >>> base_url = "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" -- >>> access_token = "akab-access-token-xxx-xxxxxxxxxxxxxxxx" -- >>> client_token = "akab-client-token-xxx-xxxxxxxxxxxxxxxx" -- >>> client_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=" -- >>> nonce = "nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -- >>> timestamp = "20140321T19:34:21+0000" -- >>> dataToSign (Auth base_url access_token client_token client_secret) "GET" "/" [] "" "20140321T19:34:21+0000" "nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -- "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/\t\t\tEG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;" -- "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=hXm4iCxtpN22m4cbZb4lVLW5rhX8Ca82vCFqXzSTPe4=" dataToSign :: Auth -> Method -> PathWithQuery -> CanonicalizedRequestHeaders -> Body -> ByteString -> Nonce -> ByteString dataToSign auth method pathWithQuery creqHeaders body timeStamp nonce = method <> "\t" <> "https" <> "\t" <> T.encodeUtf8 (authHostname auth) <> "\t" <> pathWithQuery <> "\t" <> (B.intercalate "\t" $ map (\(k,v) -> original k <> ":" <> v) creqHeaders) <> "\t" <> (if body == "" then "" else contentHash body) <> "\t" <> "EG1-HMAC-SHA256 " <> "client_token=" <> T.encodeUtf8 (authClientToken auth) <> ";" <> "access_token=" <> T.encodeUtf8 (authAccessToken auth) <> ";" <> "timestamp=" <> timeStamp <> ";" <> "nonce=" <> nonce <> ";" -- | authHeader -- -- >>> authSign "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=" "20140321T19:34:21+0000" -- "znsRMDBRqTXGJ7Ojip3/h2FGPu3LuoMYWgv9PKEnE/o=" authSign :: ByteString -> Message -> AuthSign authSign key msg = convertToBase Base64 $ (hmac key msg :: HMAC SHA256) contentHash :: Message -> ByteString contentHash msg = convertToBase Base64 $ (hash msg :: Digest SHA256) -- | authHeader -- -- >>> base_url = "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" -- >>> access_token = "akab-access-token-xxx-xxxxxxxxxxxxxxxx" -- >>> client_token = "akab-client-token-xxx-xxxxxxxxxxxxxxxx" -- >>> client_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=" -- >>> nonce = "nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -- >>> timestamp = "20140321T19:34:21+0000" -- >>> authHeader (Auth base_url access_token client_token client_secret) "GET" "/" [] "" "20140321T19:34:21+0000" "nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -- "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=tL+y4hxyHxgWVD30X3pWnGKHcPzmrIF+LThiAOhMxYU=" -- >>> authHeader (Auth base_url access_token client_token client_secret) "GET" "/testapi/v1/t1?p1=1&p2=2" [] "" "20140321T19:34:21+0000" "nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -- "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=hKDH1UlnQySSHjvIcZpDMbQHihTQ0XyVAKZaApabdeA=" -- >>> authHeader (Auth base_url access_token client_token client_secret) "POST" "/testapi/v1/t3" [] "datadatadatadatadatadatadatadata" "20140321T19:34:21+0000" "nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -- "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=hXm4iCxtpN22m4cbZb4lVLW5rhX8Ca82vCFqXzSTPe4=" authHeader :: Auth -> Method -> PathWithQuery -> CanonicalizedRequestHeaders -> Body -> ByteString -> Nonce -> ByteString authHeader auth method pathWithQuery creqHeaders body timeStamp nonce = "EG1-HMAC-SHA256 " <> "client_token=" <> T.encodeUtf8 (authClientToken auth) <> ";" <> "access_token=" <> T.encodeUtf8 (authAccessToken auth) <> ";" <> "timestamp=" <> timeStamp <> ";" <> "nonce=" <> nonce <> ";" <> "signature=" <> signature where signature = authSign signingKey (dataToSign auth method pathWithQuery creqHeaders body' timeStamp nonce) signingKey = authSign (T.encodeUtf8 (authClientSecret auth)) timeStamp body' = B.take 131072 body mkReq :: Auth -> Method -> PathWithQuery -> CanonicalizedRequestHeaders -> Body -> IO Request mkReq auth method pathWithQuery creqHeaders body = do initReq <- parseRequest $ BC.unpack $ method <> " " <> "https" <> "://" <> (T.encodeUtf8 (authHostname auth)) <> pathWithQuery timeStamp <- (getUnixTime >>= return.(formatUnixTimeGMT "%Y%m%dT%H:%M:%S%z")) uuid <- (nextRandom >>= return.toASCIIBytes) return $ setRequestBody (RequestBodyBS body) $ setRequestHeaders ((hAuthorization,authHeader auth method pathWithQuery creqHeaders body timeStamp uuid):creqHeaders) initReq mkJsonReq :: ToJSON a => Auth -> Method -> PathWithQuery -> CanonicalizedRequestHeaders -> a -> IO Request mkJsonReq auth method pathWithQuery creqHeaders body = do initReq <- parseRequest $ BC.unpack $ method <> " " <> "https" <> "://" <> (T.encodeUtf8 (authHostname auth)) <> pathWithQuery timeStamp <- (getUnixTime >>= return.(formatUnixTimeGMT "%Y%m%dT%H:%M:%S%z")) uuid <- (nextRandom >>= return.toASCIIBytes) return $ setRequestHeader "Content-Type" ["application/json"] $ setRequestBodyJSON body $ setRequestHeaders ((hAuthorization,authHeader auth method pathWithQuery creqHeaders (BL.toStrict (encode body)) timeStamp uuid):creqHeaders) initReq