{-# LANGUAGE CPP #-}
module Aws.Core
( 
  Loggable(..)
  
  
, Response(..)
, readResponse
, readResponseIO
, tellMetadata
, tellMetadataRef
, mapMetadata
  
, HTTPResponseConsumer
, ResponseConsumer(..)
  
, AsMemoryResponse(..)
  
, ListResponse(..)
  
, XmlException(..)
, HeaderException(..)
, FormException(..)
, NoCredentialsException(..)
, throwStatusCodeException
  
, readHex2
  
, elContent
, elCont
, force
, forceM
, textReadBool
, textReadInt
, readInt
, xmlCursorConsumer
  
, SignedQuery(..)
, NormalQuery
, UriOnlyQuery
, queryToHttpRequest
, queryToUri
  
, TimeInfo(..)
, AbsoluteTimeInfo(..)
, fromAbsoluteTimeInfo
, makeAbsoluteTimeInfo
 
, SignatureData(..)
, signatureData
, SignQuery(..)
, AuthorizationHash(..)
, amzHash
, signature
, credentialV4
, authorizationV4
, authorizationV4'
, signatureV4
  
, queryList
, awsBool
, awsTrue
, awsFalse
, fmtTime
, fmtRfc822Time
, rfc822Time
, fmtAmzTime
, fmtTimeEpochSeconds
, parseHttpDate
, httpDate1
, textHttpDate
, iso8601UtcDate
  
, Transaction
, IteratedTransaction(..)
  
, Credentials(..)
, makeCredentials
, credentialsDefaultFile
, credentialsDefaultKey
, loadCredentialsFromFile
, loadCredentialsFromEnv
, loadCredentialsFromInstanceMetadata
, loadCredentialsFromEnvOrFile
, loadCredentialsFromEnvOrFileOrInstanceMetadata
, loadCredentialsDefault
  
, DefaultServiceConfiguration(..)
  
, Protocol(..)
, defaultPort
, Method(..)
, httpMethod
)
where
import           Aws.Ec2.InstanceMetadata
import           Aws.Network
import qualified Blaze.ByteString.Builder as Blaze
import           Control.Applicative
import           Control.Arrow
import qualified Control.Exception        as E
import           Control.Monad
import           Control.Monad.IO.Class
import           Control.Monad.Trans.Resource (ResourceT, MonadThrow (throwM))
import qualified Crypto.Hash              as CH
import qualified Crypto.MAC.HMAC          as CMH
import qualified Data.Aeson               as A
import qualified Data.ByteArray           as ByteArray
import           Data.ByteString          (ByteString)
import qualified Data.ByteString          as B
import qualified Data.ByteString.Base16   as Base16
import qualified Data.ByteString.Base64   as Base64
import           Data.ByteString.Char8    ()
import qualified Data.ByteString.Lazy     as L
import qualified Data.ByteString.UTF8     as BU
import           Data.Char
import           Data.Conduit             ((.|))
import qualified Data.Conduit             as C
#if MIN_VERSION_http_conduit(2,2,0)
import qualified Data.Conduit.Binary      as CB
#endif
import qualified Data.Conduit.List        as CL
import           Data.IORef
import           Data.List
import qualified Data.Map                 as M
import           Data.Maybe
import           Data.Monoid
import qualified Data.Text                as T
import qualified Data.Text.Encoding       as T
import qualified Data.Text.IO             as T
import           Data.Time
import qualified Data.Traversable         as Traversable
import           Data.Typeable
import           Data.Word
import qualified Network.HTTP.Conduit     as HTTP
import qualified Network.HTTP.Types       as HTTP
import           System.Directory
import           System.Environment
import           System.FilePath          ((</>))
#if !MIN_VERSION_time(1,5,0)
import           System.Locale
#endif
import qualified Text.XML                 as XML
import qualified Text.XML.Cursor          as Cu
import           Text.XML.Cursor          hiding (force, forceM)
import           Prelude
class Loggable a where
    toLogText :: a -> T.Text
data Response m a = Response { responseMetadata :: m
                             , responseResult :: Either E.SomeException a }
    deriving (Show, Functor)
readResponse :: MonadThrow n => Response m a -> n a
readResponse = either throwM return . responseResult
readResponseIO :: MonadIO io => Response m a -> io a
readResponseIO = liftIO . readResponse
tellMetadata :: m -> Response m ()
tellMetadata m = Response m (return ())
mapMetadata :: (m -> n) -> Response m a -> Response n a
mapMetadata f (Response m a) = Response (f m) a
instance Monoid m => Applicative (Response m) where
    pure x = Response mempty (Right x)
    (<*>) = ap
instance Monoid m => Monad (Response m) where
    return x = Response mempty (Right x)
    Response m1 (Left e) >>= _ = Response m1 (Left e)
    Response m1 (Right x) >>= f = let Response m2 y = f x
                                  in Response (m1 `mappend` m2) y 
instance Monoid m => MonadThrow (Response m) where
    throwM e = Response mempty (throwM e)
tellMetadataRef :: Monoid m => IORef m -> m -> IO ()
tellMetadataRef r m = modifyIORef r (`mappend` m)
type HTTPResponseConsumer a = HTTP.Response (C.ConduitM () ByteString (ResourceT IO) ())
                              -> ResourceT IO a
class Monoid (ResponseMetadata resp) => ResponseConsumer req resp where
    
    
    type ResponseMetadata resp
    
    
    
    responseConsumer :: HTTP.Request -> req -> IORef (ResponseMetadata resp) -> HTTPResponseConsumer resp
instance ResponseConsumer r (HTTP.Response L.ByteString) where
    type ResponseMetadata (HTTP.Response L.ByteString) = ()
    responseConsumer _ _ _ resp = do
        bss <- C.runConduit $ HTTP.responseBody resp .| CL.consume
        return resp
            { HTTP.responseBody = L.fromChunks bss
            }
class AsMemoryResponse resp where
    type MemoryResponse resp :: *
    loadToMemory :: resp -> ResourceT IO (MemoryResponse resp)
class ListResponse resp item | resp -> item where
    listResponse :: resp -> [item]
class (SignQuery r, ResponseConsumer r a, Loggable (ResponseMetadata a))
      => Transaction r a
      | r -> a
class Transaction r a => IteratedTransaction r a | r -> a where
    nextIteratedRequest :: r -> a -> Maybe r
type V4Key = ((B.ByteString,B.ByteString),(B.ByteString,B.ByteString))
data Credentials
    = Credentials {
        
        accessKeyID :: B.ByteString
        
      , secretAccessKey :: B.ByteString
        
      , v4SigningKeys :: IORef [V4Key]
        
      , iamToken :: Maybe B.ByteString
      }
instance Show Credentials where
    show c = "Credentials{accessKeyID=" ++ show (accessKeyID c) ++ ",secretAccessKey=" ++ show (secretAccessKey c) ++ ",iamToken=" ++ show (iamToken c) ++ "}"
makeCredentials :: MonadIO io
                => B.ByteString 
                -> B.ByteString 
                -> io Credentials
makeCredentials accessKeyID secretAccessKey = liftIO $ do
    v4SigningKeys <- newIORef []
    let iamToken = Nothing
    return Credentials { .. }
credentialsDefaultFile :: MonadIO io => io (Maybe FilePath)
credentialsDefaultFile = liftIO $ tryMaybe ((</> ".aws-keys") <$> getHomeDirectory)
tryMaybe :: IO a -> IO (Maybe a)
tryMaybe action = E.catch (Just <$> action) f
  where
    f :: E.SomeException -> IO (Maybe a)
    f _ = return Nothing
credentialsDefaultKey :: T.Text
credentialsDefaultKey = "default"
loadCredentialsFromFile :: MonadIO io => FilePath -> T.Text -> io (Maybe Credentials)
loadCredentialsFromFile file key = liftIO $ do
  exists <- doesFileExist file
  if exists
    then do
      contents <- map T.words . T.lines <$> T.readFile file
      Traversable.sequence $ do
        [_key, keyID, secret] <- find (hasKey key) contents
        return (makeCredentials (T.encodeUtf8 keyID) (T.encodeUtf8 secret))
    else return Nothing
  where
    hasKey _ [] = False
    hasKey k (k2 : _) = k == k2
loadCredentialsFromEnv :: MonadIO io => io (Maybe Credentials)
loadCredentialsFromEnv = liftIO $ do
  env <- getEnvironment
  let lk = fmap (T.encodeUtf8 . T.pack) . flip lookup env
      keyID = lk "AWS_ACCESS_KEY_ID"
      secret = lk "AWS_ACCESS_KEY_SECRET" `mplus` lk "AWS_SECRET_ACCESS_KEY"
      setSession creds = creds { iamToken = lk "AWS_SESSION_TOKEN" }
      makeCredentials' k s = setSession <$> makeCredentials k s
  Traversable.sequence $ makeCredentials' <$> keyID <*> secret
loadCredentialsFromInstanceMetadata :: MonadIO io => io (Maybe Credentials)
loadCredentialsFromInstanceMetadata = do
    mgr <- liftIO $ HTTP.newManager HTTP.tlsManagerSettings
    
    avail <- liftIO $ hostAvailable "169.254.169.254"
    if not avail
      then return Nothing
      else do
        info <- liftIO $ E.catch (getInstanceMetadata mgr "latest/meta-data/iam" "info" >>= return . Just) (\(_ :: HTTP.HttpException) -> return Nothing)
        let infodict = info >>= A.decode :: Maybe (M.Map String String)
            info'    = infodict >>= M.lookup "InstanceProfileArn"
        case info' of
          Just name ->
            do
              let name' = drop 1 $ dropWhile (/= '/') $ name
              creds <- liftIO $ E.catch (getInstanceMetadata mgr "latest/meta-data/iam/security-credentials" name' >>= return . Just) (\(_ :: HTTP.HttpException) -> return Nothing)
              
              let dict   = creds >>= A.decode :: Maybe (M.Map String String)
                  keyID  = dict  >>= M.lookup "AccessKeyId"
                  secret = dict  >>= M.lookup "SecretAccessKey"
                  token  = dict  >>= M.lookup "Token"
              ref <- liftIO $ newIORef []
              return (Credentials <$> (T.encodeUtf8 . T.pack <$> keyID)
                                  <*> (T.encodeUtf8 . T.pack <$> secret)
                                  <*> return ref
                                  <*> (Just . T.encodeUtf8 . T.pack <$> token))
          Nothing -> return Nothing
loadCredentialsFromEnvOrFile :: MonadIO io => FilePath -> T.Text -> io (Maybe Credentials)
loadCredentialsFromEnvOrFile file key =
  do
    envcr <- loadCredentialsFromEnv
    case envcr of
      Just cr -> return (Just cr)
      Nothing -> loadCredentialsFromFile file key
loadCredentialsFromEnvOrFileOrInstanceMetadata :: MonadIO io => FilePath -> T.Text -> io (Maybe Credentials)
loadCredentialsFromEnvOrFileOrInstanceMetadata file key =
  do
    envcr <- loadCredentialsFromEnv
    case envcr of
      Just cr -> return (Just cr)
      Nothing ->
        do
          filecr <- loadCredentialsFromFile file key
          case filecr of
            Just cr -> return (Just cr)
            Nothing -> loadCredentialsFromInstanceMetadata
loadCredentialsDefault :: MonadIO io => io (Maybe Credentials)
loadCredentialsDefault = do
  mfile <- credentialsDefaultFile
  case mfile of
      Just file -> loadCredentialsFromEnvOrFileOrInstanceMetadata file credentialsDefaultKey
      Nothing   -> loadCredentialsFromEnv
data Protocol
    = HTTP
    | HTTPS
    deriving (Eq,Read,Show,Ord,Typeable)
defaultPort :: Protocol -> Int
defaultPort HTTP = 80
defaultPort HTTPS = 443
data Method
    = Head      
    | Get       
    | PostQuery 
                
    | Post      
    | Put       
    | Delete    
    deriving (Show, Eq, Ord)
httpMethod :: Method -> HTTP.Method
httpMethod Head      = "HEAD"
httpMethod Get       = "GET"
httpMethod PostQuery = "POST"
httpMethod Post      = "POST"
httpMethod Put       = "PUT"
httpMethod Delete    = "DELETE"
data SignedQuery
    = SignedQuery {
        
        sqMethod :: !Method
        
      , sqProtocol :: !Protocol
        
      , sqHost :: !B.ByteString
        
      , sqPort :: !Int
        
      , sqPath :: !B.ByteString
        
      , sqQuery :: !HTTP.Query
        
      , sqDate :: !(Maybe UTCTime)
        
      , sqAuthorization :: !(Maybe (IO B.ByteString))
        
      , sqContentType :: !(Maybe B.ByteString)
        
      , sqContentMd5 :: !(Maybe (CH.Digest CH.MD5))
        
      , sqAmzHeaders :: !HTTP.RequestHeaders
        
      , sqOtherHeaders :: !HTTP.RequestHeaders
        
      , sqBody :: !(Maybe HTTP.RequestBody)
        
      , sqStringToSign :: !B.ByteString
      }
    
queryToHttpRequest :: SignedQuery -> IO HTTP.Request
queryToHttpRequest SignedQuery{..} =  do
    mauth <- maybe (return Nothing) (Just<$>) sqAuthorization
    return $ HTTP.defaultRequest {
        HTTP.method = httpMethod sqMethod
      , HTTP.secure = case sqProtocol of
                        HTTP -> False
                        HTTPS -> True
      , HTTP.host = sqHost
      , HTTP.port = sqPort
      , HTTP.path = sqPath
      , HTTP.queryString =
          if sqMethod == PostQuery
            then ""
            else HTTP.renderQuery False sqQuery
      , HTTP.requestHeaders = catMaybes [ checkDate (\d -> ("Date", fmtRfc822Time d)) sqDate
                                        , fmap (\c -> ("Content-Type", c)) contentType
                                        , fmap (\md5 -> ("Content-MD5", Base64.encode $ ByteArray.convert md5)) sqContentMd5
                                        , fmap (\auth -> ("Authorization", auth)) mauth]
                              ++ sqAmzHeaders
                              ++ sqOtherHeaders
      , HTTP.requestBody =
        
        case sqBody of
          Just x -> x
          Nothing ->
            
            case sqMethod of
              PostQuery -> HTTP.RequestBodyLBS . Blaze.toLazyByteString $
                           HTTP.renderQueryBuilder False sqQuery
              _         -> HTTP.RequestBodyBuilder 0 mempty
      , HTTP.decompress = HTTP.alwaysDecompress
#if MIN_VERSION_http_conduit(2,2,0)
      , HTTP.checkResponse = \_ _ -> return ()
#else
      , HTTP.checkStatus = \_ _ _-> Nothing
#endif
      , HTTP.redirectCount = 10
      }
    where
      checkDate f mb = maybe (f <$> mb) (const Nothing) $ lookup "date" sqOtherHeaders
      
      contentType = sqContentType `mplus` defContentType
      defContentType = case sqMethod of
                         PostQuery -> Just "application/x-www-form-urlencoded; charset=utf-8"
                         _ -> Nothing
queryToUri :: SignedQuery -> B.ByteString
queryToUri SignedQuery{..}
    = B.concat [
       case sqProtocol of
         HTTP -> "http://"
         HTTPS -> "https://"
      , sqHost
      , if sqPort == defaultPort sqProtocol then "" else T.encodeUtf8 . T.pack $ ':' : show sqPort
      , sqPath
      , HTTP.renderQuery True sqQuery
      ]
data TimeInfo
    = Timestamp                                      
    | ExpiresAt { fromExpiresAt :: UTCTime }         
    | ExpiresIn { fromExpiresIn :: NominalDiffTime } 
                                                     
    deriving (Show)
data AbsoluteTimeInfo
    = AbsoluteTimestamp { fromAbsoluteTimestamp :: UTCTime }
    | AbsoluteExpires { fromAbsoluteExpires :: UTCTime }
    deriving (Show)
fromAbsoluteTimeInfo :: AbsoluteTimeInfo -> UTCTime
fromAbsoluteTimeInfo (AbsoluteTimestamp time) = time
fromAbsoluteTimeInfo (AbsoluteExpires time) = time
makeAbsoluteTimeInfo :: TimeInfo -> UTCTime -> AbsoluteTimeInfo
makeAbsoluteTimeInfo Timestamp     now = AbsoluteTimestamp now
makeAbsoluteTimeInfo (ExpiresAt t) _   = AbsoluteExpires t
makeAbsoluteTimeInfo (ExpiresIn s) now = AbsoluteExpires $ addUTCTime s now
data SignatureData
    = SignatureData {
        
        signatureTimeInfo :: AbsoluteTimeInfo
        
      , signatureTime :: UTCTime
        
      , signatureCredentials :: Credentials
      }
signatureData :: TimeInfo -> Credentials -> IO SignatureData
signatureData rti cr = do
  now <- getCurrentTime
  let ti = makeAbsoluteTimeInfo rti now
  return SignatureData { signatureTimeInfo = ti, signatureTime = now, signatureCredentials = cr }
data NormalQuery
data UriOnlyQuery
class SignQuery request where
    
    type ServiceConfiguration request :: *  -> *
    
    signQuery :: request -> ServiceConfiguration request queryType -> SignatureData -> SignedQuery
data AuthorizationHash
    = HmacSHA1
    | HmacSHA256
    deriving (Show)
amzHash :: AuthorizationHash -> B.ByteString
amzHash HmacSHA1 = "HmacSHA1"
amzHash HmacSHA256 = "HmacSHA256"
signature :: Credentials -> AuthorizationHash -> B.ByteString -> B.ByteString
signature cr ah input = Base64.encode sig
    where
      sig = case ah of
              HmacSHA1 -> ByteArray.convert (CMH.hmac (secretAccessKey cr) input :: CMH.HMAC CH.SHA1)
              HmacSHA256 -> ByteArray.convert (CMH.hmac (secretAccessKey cr) input :: CMH.HMAC CH.SHA256)
credentialV4
    :: SignatureData
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString
credentialV4 sd region service = B.concat
    [ accessKeyID (signatureCredentials sd)
    , "/"
    , date
    , "/"
    , region
    , "/"
    , service
    , "/aws4_request"
    ]
    where
        date = fmtTime "%Y%m%d" $ signatureTime sd
authorizationV4 :: SignatureData
                -> AuthorizationHash
                -> B.ByteString 
                -> B.ByteString 
                -> B.ByteString 
                -> B.ByteString 
                -> IO B.ByteString
authorizationV4 sd ah region service headers canonicalRequest = do
    let ref = v4SigningKeys $ signatureCredentials sd
        date = fmtTime "%Y%m%d" $ signatureTime sd
    
    allkeys <- readIORef ref
    let mkey = case lookup (region,service) allkeys of
            Just (d,k) | d /= date -> Nothing
                       | otherwise -> Just k
            Nothing -> Nothing
    
    let createNewKey = atomicModifyIORef ref $ \keylist ->
            let kSigning = signingKeyV4 sd ah region service
                lstK     = (region,service)
                keylist' = (lstK,(date,kSigning)) : filter ((lstK/=).fst) keylist
             in (keylist', kSigning)
    
    constructAuthorizationV4Header sd ah region service headers
         .  signatureV4WithKey sd ah region service canonicalRequest
        <$> maybe createNewKey return mkey
authorizationV4'
    :: SignatureData
    -> AuthorizationHash
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString
authorizationV4' sd ah region service headers canonicalRequest
    = constructAuthorizationV4Header sd ah region service headers
        $ signatureV4 sd ah region service canonicalRequest
constructAuthorizationV4Header
    :: SignatureData
    -> AuthorizationHash
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString
constructAuthorizationV4Header sd ah region service headers sig = B.concat
    [ alg
    , " Credential="
    , credentialV4 sd region service
    , ",SignedHeaders="
    , headers
    , ",Signature="
    , sig
    ]
    where
        alg = case ah of
            HmacSHA1 -> "AWS4-HMAC-SHA1"
            HmacSHA256 -> "AWS4-HMAC-SHA256"
signatureV4WithKey
    :: SignatureData
    -> AuthorizationHash
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString
signatureV4WithKey sd ah region service canonicalRequest key = Base16.encode $ mkHmac key stringToSign
    where
        date = fmtTime "%Y%m%d" $ signatureTime sd
        mkHmac k i = case ah of
            HmacSHA1 -> ByteArray.convert (CMH.hmac k i :: CMH.HMAC CH.SHA1)
            HmacSHA256 -> ByteArray.convert (CMH.hmac k i :: CMH.HMAC CH.SHA256)
        mkHash i = case ah of
            HmacSHA1 -> ByteArray.convert (CH.hash i :: CH.Digest CH.SHA1)
            HmacSHA256 -> ByteArray.convert (CH.hash i :: CH.Digest CH.SHA256)
        alg = case ah of
            HmacSHA1 -> "AWS4-HMAC-SHA1"
            HmacSHA256 -> "AWS4-HMAC-SHA256"
        
        canonicalRequestHash = Base16.encode $ mkHash canonicalRequest
        stringToSign = B.concat
            [ alg
            , "\n"
            , fmtTime "%Y%m%dT%H%M%SZ" $ signatureTime sd
            , "\n"
            , date
            , "/"
            , region
            , "/"
            , service
            , "/aws4_request\n"
            , canonicalRequestHash
            ]
signingKeyV4
    :: SignatureData
    -> AuthorizationHash
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString
signingKeyV4 sd ah region service = kSigning
    where
        mkHmac k i = case ah of
            HmacSHA1 -> ByteArray.convert (CMH.hmac k i :: CMH.HMAC CH.SHA1)
            HmacSHA256 -> ByteArray.convert (CMH.hmac k i :: CMH.HMAC CH.SHA256)
        date = fmtTime "%Y%m%d" $ signatureTime sd
        secretKey = secretAccessKey $ signatureCredentials sd
        kDate = mkHmac ("AWS4" <> secretKey) date
        kRegion = mkHmac kDate region
        kService = mkHmac kRegion service
        kSigning = mkHmac kService "aws4_request"
signatureV4
    :: SignatureData
    -> AuthorizationHash
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString 
    -> B.ByteString
signatureV4 sd ah region service canonicalRequest
    = signatureV4WithKey sd ah region service canonicalRequest
        $ signingKeyV4 sd ah region service
class DefaultServiceConfiguration config where
    
    defServiceConfig :: config
    
    debugServiceConfig :: config
    debugServiceConfig = defServiceConfig
queryList :: (a -> [(B.ByteString, B.ByteString)]) -> B.ByteString -> [a] -> [(B.ByteString, B.ByteString)]
queryList f prefix xs = concat $ zipWith combine prefixList (map f xs)
    where prefixList = map (dot prefix . BU.fromString . show) [(1 :: Int) ..]
          combine pf = map $ first (pf `dot`)
          dot x y = B.concat [x, BU.fromString ".", y]
awsBool :: Bool -> B.ByteString
awsBool True = "true"
awsBool False = "false"
awsTrue :: B.ByteString
awsTrue = awsBool True
awsFalse :: B.ByteString
awsFalse = awsBool False
fmtTime :: String -> UTCTime -> B.ByteString
fmtTime s t = BU.fromString $ formatTime defaultTimeLocale s t
rfc822Time :: String
rfc822Time = "%a, %0d %b %Y %H:%M:%S GMT"
fmtRfc822Time :: UTCTime -> B.ByteString
fmtRfc822Time = fmtTime rfc822Time
fmtAmzTime :: UTCTime -> B.ByteString
fmtAmzTime = fmtTime "%Y-%m-%dT%H:%M:%S"
fmtTimeEpochSeconds :: UTCTime -> B.ByteString
fmtTimeEpochSeconds = fmtTime "%s"
parseHttpDate :: String -> Maybe UTCTime
parseHttpDate s =     p "%a, %d %b %Y %H:%M:%S GMT" s 
                  <|> p "%A, %d-%b-%y %H:%M:%S GMT" s 
                  <|> p "%a %b %_d %H:%M:%S %Y" s     
                  <|> p "%Y-%m-%dT%H:%M:%S%QZ" s      
                  <|> p "%Y-%m-%dT%H:%M:%S%Q%Z" s     
  where p = parseTimeM True defaultTimeLocale
httpDate1 :: String
httpDate1 = "%a, %d %b %Y %H:%M:%S GMT" 
textHttpDate :: UTCTime -> T.Text
textHttpDate = T.pack . formatTime defaultTimeLocale httpDate1
iso8601UtcDate :: String
iso8601UtcDate = "%Y-%m-%dT%H:%M:%S%QZ"
readHex2 :: [Char] -> Maybe Word8
readHex2 [c1,c2] = do n1 <- readHex1 c1
                      n2 <- readHex1 c2
                      return . fromIntegral $ n1 * 16 + n2
    where
      readHex1 c | c >= '0' && c <= '9' = Just $ ord c - ord '0'
                 | c >= 'A' && c <= 'F' = Just $ ord c - ord 'A' + 10
                 | c >= 'a' && c <= 'f' = Just $ ord c - ord 'a' + 10
      readHex1 _                        = Nothing
readHex2 _ = Nothing
newtype XmlException = XmlException { xmlErrorMessage :: String }
    deriving (Show, Typeable)
instance E.Exception XmlException
newtype HeaderException = HeaderException { headerErrorMessage :: String }
    deriving (Show, Typeable)
instance E.Exception HeaderException
newtype FormException = FormException { formErrorMesage :: String }
    deriving (Show, Typeable)
instance E.Exception FormException
newtype NoCredentialsException = NoCredentialsException { noCredentialsErrorMessage :: String }
    deriving (Show, Typeable)
instance E.Exception NoCredentialsException
throwStatusCodeException :: MonadThrow m => HTTP.Request -> HTTP.Response (C.ConduitM () ByteString m ()) -> m a
throwStatusCodeException req resp = do
    let resp' = fmap (const ()) resp
    
    body <- C.runConduit $ HTTP.responseBody resp .| CB.take (10*1024)
    let sce = HTTP.StatusCodeException resp' (L.toStrict body)
    throwM $ HTTP.HttpExceptionRequest req sce
elContent :: T.Text -> Cursor -> [T.Text]
elContent name = laxElement name &/ content
elCont :: T.Text -> Cursor -> [String]
elCont name = laxElement name &/ content &| T.unpack
force :: MonadThrow m => String -> [a] -> m a
force = Cu.force . XmlException
forceM :: MonadThrow m => String -> [m a] -> m a
forceM = Cu.forceM . XmlException
textReadBool :: MonadThrow m => T.Text -> m Bool
textReadBool s = case T.unpack s of
                  "true"  -> return True
                  "false" -> return False
                  _        -> throwM $ XmlException "Invalid Bool"
textReadInt :: (MonadThrow m, Num a) => T.Text -> m a
textReadInt s = case reads $ T.unpack s of
                  [(n,"")] -> return $ fromInteger n
                  _        -> throwM $ XmlException "Invalid Integer"
readInt :: (MonadThrow m, Num a) => String -> m a
readInt s = case reads s of
              [(n,"")] -> return $ fromInteger n
              _        -> throwM $ XmlException "Invalid Integer"
xmlCursorConsumer ::
    (Monoid m)
    => (Cu.Cursor -> Response m a)
    -> IORef m
    -> HTTPResponseConsumer a
xmlCursorConsumer parse metadataRef res
    = do doc <- C.runConduit $ HTTP.responseBody res .| XML.sinkDoc XML.def
         let cursor = Cu.fromDocument doc
         let Response metadata x = parse cursor
         liftIO $ tellMetadataRef metadataRef metadata
         case x of
           Left err -> liftIO $ throwM err
           Right v  -> return v