module Network.AWS.Authentication (
runAction,
SimpleDBAction(..),
) where
import Network.AWS.AWSResult
import Network.AWS.AWSConnection
import Network.AWS.ArrowUtils
import Network.HTTP as HTTP hiding (simpleHTTP_)
import Network.HTTP.HandleStream (simpleHTTP_)
import Network.Stream (Result)
import Network.URI as URI
import qualified Data.ByteString.Lazy.Char8 as L
import Data.ByteString.Char8 (pack, unpack)
import Data.HMAC
import Codec.Binary.Base64 (encode, decode)
import Codec.Utils (Octet)
import Data.Char (intToDigit, digitToInt, ord, chr, toLower)
import Data.Bits ((.&.))
import qualified Codec.Binary.UTF8.String as US
import Data.List (sortBy, groupBy, intersperse, intercalate, sort)
import Data.Maybe
import System.Time
import System.Locale
import Text.XML.HXT.Arrow
data SimpleDBAction =
SimpleDBAction {
sdbConnection :: AWSConnection,
sdbQuery :: [String],
sdbMetaData :: [(String, String)],
sdbBody :: L.ByteString,
sdbOperation :: RequestMethod
} deriving (Show)
requestFromAction :: SimpleDBAction
-> HTTP.HTTPRequest L.ByteString
requestFromAction a =
Request { rqURI = URI { uriScheme = "",
uriAuthority = Nothing,
uriPath = qpath,
uriQuery = '?':intercalate "&" (sdbQuery a),
uriFragment = "" },
rqMethod = sdbOperation a,
rqHeaders = Header HdrHost (sdbHostname a) :
headersFromAction a,
rqBody = (sdbBody a)
}
where qpath = ['/']
headersFromAction :: SimpleDBAction
-> [Header]
headersFromAction = map (\(k,v) -> case k of
"Content-Type" -> Header HdrContentType v
otherwise -> Header (HdrCustom k) (mimeEncodeQP v))
. sdbMetaData
addContentLengthHeader :: HTTP.HTTPRequest L.ByteString -> HTTP.HTTPRequest L.ByteString
addContentLengthHeader req = insertHeader HdrContentLength (show (L.length (rqBody req))) req
addAuthenticationHeader :: SimpleDBAction
-> HTTP.HTTPRequest L.ByteString
-> HTTP.HTTPRequest L.ByteString
addAuthenticationHeader act req
= appendQuery ("&Signature="++ urlEncode signature) $
req
where auth_string = "AWS " ++ awsAccessKey conn ++ ":" ++ signature
signature = makeSignature conn (stringToSign act req)
conn = sdbConnection act
appendQuery :: String -> HTTP.HTTPRequest L.ByteString -> HTTP.HTTPRequest L.ByteString
appendQuery string req
= req{ rqURI = uri{ uriQuery = uriQuery uri ++ string } }
where uri = rqURI req
makeSignature :: AWSConnection
-> String
-> String
makeSignature c s =
encode (hmac_sha1 keyOctets msgOctets)
where keyOctets = string2words (awsSecretKey c)
msgOctets = string2words s
stringToSign :: SimpleDBAction -> HTTP.HTTPRequest L.ByteString -> String
stringToSign a r =
canonicalizeHeaders r ++
canonicalizeResource a ++ "\n" ++
intercalate "&" (sort (sdbQuery a))
canonicalizeHeaders :: HTTP.HTTPRequest L.ByteString -> String
canonicalizeHeaders r =
http_verb ++ "\n" ++
get_header HdrHost ++ "\n"
where http_verb = show (rqMethod r)
hdr_content_md5 = get_header HdrContentMD5
hdr_date = get_header HdrDate
hdr_content_type = get_header HdrContentType
get_header h = fromMaybe "" (findHeader h r)
dateOrExpiration = fromMaybe hdr_date (findHeader HdrExpires r)
combineHeaders :: [[(String, String)]] -> [(String, String)]
combineHeaders = map mergeSameHeaders
mergeSameHeaders :: [(String, String)] -> (String, String)
mergeSameHeaders h@(x:_) = let values = map snd h
in ((fst x), (concat $ intersperse "," values))
groupHeaders :: [(String, String)] -> [[(String, String)]]
groupHeaders = groupBy (\a b -> fst a == fst b)
sortHeaders :: [(String, String)] -> [(String, String)]
sortHeaders = sortBy (\a b -> fst a `compare` fst b)
headerToLCKeyValue :: Header -> (String, String)
headerToLCKeyValue (Header k v) = (map toLower (show k), v)
isAmzHeader :: Header -> Bool
isAmzHeader h =
case h of
Header (HdrCustom k) _ -> isPrefix amzHeader k
otherwise -> False
isPrefix :: Eq a => [a] -> [a] -> Bool
isPrefix a b = a == take (length a) b
amzHeader :: String
amzHeader = "x-amz-"
canonicalizeResource :: SimpleDBAction -> String
canonicalizeResource a = uri
where uri = '/' : []
addDateToReq :: HTTP.HTTPRequest L.ByteString
-> String
-> String
-> HTTP.HTTPRequest L.ByteString
addDateToReq r d ts
=
r {HTTP.rqHeaders =
HTTP.Header HTTP.HdrDate d : HTTP.rqHeaders r}
addExpirationToReq :: HTTP.HTTPRequest L.ByteString -> String -> HTTP.HTTPRequest L.ByteString
addExpirationToReq r = addHeaderToReq r . HTTP.Header HTTP.HdrExpires
addHeaderToReq :: HTTP.HTTPRequest L.ByteString -> Header -> HTTP.HTTPRequest L.ByteString
addHeaderToReq r h = r {HTTP.rqHeaders = h : HTTP.rqHeaders r}
sdbHostname :: SimpleDBAction -> String
sdbHostname a = sdbhost
where sdbhost = awsHost (sdbConnection a)
httpCurrentDate :: IO String
httpCurrentDate =
do c <- getClockTime
let utc_time = (toUTCTime c) {ctTZName = "GMT"}
return $ formatCalendarTime defaultTimeLocale rfc822DateFormat utc_time
httpCurrentTimestamp :: IO String
httpCurrentTimestamp =
do c <- getClockTime
let utc_time = (toUTCTime c) {ctTZName = "GMT"}
return $ formatCalendarTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S" utc_time
string2words :: String -> [Octet]
string2words = US.encode
runAction :: SimpleDBAction -> IO (AWSResult (HTTPResponse L.ByteString))
runAction a = runAction' a (sdbHostname a)
runAction' :: SimpleDBAction -> String -> IO (AWSResult (HTTPResponse L.ByteString))
runAction' a hostname = do
c <- (openTCPConnection hostname (awsPort (sdbConnection a)))
cd <- httpCurrentDate
ts <- httpCurrentTimestamp
let a' = a{sdbQuery = ("Timestamp="++urlEncode ts) : ("AWSAccessKeyId="++awsAccessKey (sdbConnection a)) : sdbQuery a}
let aReq = addAuthenticationHeader a' $
addContentLengthHeader $
addDateToReq (requestFromAction a') cd ts
result <- simpleHTTP_ c aReq
close c
createAWSResult a result
preSignedURI :: SimpleDBAction
-> Integer
-> URI
preSignedURI a e =
let c = (sdbConnection a)
srv = (awsHost c)
pt = (show (awsPort c))
accessKeyQuery = "AWSAccessKeyId=" ++ awsAccessKey c
beginQuery = case (sdbQuery a) of
[] -> "?"
x -> intercalate "&" x ++ "&"
expireQuery = "Expires=" ++ show e
toSign = "GET\n\n\n" ++ show e ++ "\n/"
sigQuery = "Signature=" ++ urlEncode (makeSignature c toSign)
q = beginQuery ++ accessKeyQuery ++ "&" ++
expireQuery ++ "&" ++ sigQuery
in URI { uriScheme = "http:",
uriAuthority = Just (URIAuth "" srv (':' : pt)),
uriPath = "/",
uriQuery = q,
uriFragment = ""
}
createAWSResult :: SimpleDBAction -> Result (HTTPResponse L.ByteString) -> IO (AWSResult (HTTPResponse L.ByteString))
createAWSResult a b = either handleError handleSuccess b
where handleError = return . Left . NetworkError
handleSuccess s = case (rspCode s) of
(2,_,_) -> return (Right s)
(3,0,7) -> case (findHeader HdrLocation s) of
Just l -> runAction' a (getHostname l)
Nothing -> return (Left $ AWSError "Temporary Redirect" "Redirect without location header")
(4,0,4) -> return (Left $ AWSError "NotFound" "404 Not Found")
otherwise -> do e <- parseRestErrorXML (L.unpack (rspBody s))
return (Left e)
getHostname :: String -> String
getHostname h = case parseURI h of
Just u -> case (uriAuthority u) of
Just auth -> (uriRegName auth)
Nothing -> ""
Nothing -> ""
parseRestErrorXML :: String -> IO ReqError
parseRestErrorXML x =
do e <- runX (readString [(a_validate,v_0)] x
>>> processRestError)
case e of
[] -> return (AWSError "NoErrorInMsg"
("HTTP Error condition, but message body"
++ "did not contain error code."))
x:xs -> return x
processRestError = deep (isElem >>> hasName "Error") >>>
split >>> first (text <<< atTag "Code") >>>
second (text <<< atTag "Message") >>>
unsplit (\x y -> AWSError x y)
mimeEncodeQP, mimeDecode :: String -> String
mimeDecode a
| isPrefix utf8qp a =
mimeDecodeQP $ encoded_payload utf8qp a
| isPrefix utf8b64 a =
mimeDecodeB64 $ encoded_payload utf8b64 a
| otherwise =
a
where
utf8qp = "=?UTF-8?Q?"
utf8b64 = "=?UTF-8?B?"
encoded_payload prefix = reverse . drop 2 . reverse . drop (length prefix)
mimeDecodeQP :: String -> String
mimeDecodeQP =
US.decodeString . mimeDecodeQP'
mimeDecodeQP' :: String -> String
mimeDecodeQP' ('=':a:b:rest) =
chr (16 * digitToInt a + digitToInt b)
: mimeDecodeQP' rest
mimeDecodeQP' (h:t) =h : mimeDecodeQP' t
mimeDecodeQP' [] = []
mimeDecodeB64 :: String -> String
mimeDecodeB64 s =
case decode s of
Nothing -> ""
Just a -> US.decode a
mimeEncodeQP s =
if any reservedChar s
then "=?UTF-8?Q?" ++ (mimeEncodeQP' $ US.encodeString s) ++ "?="
else s
mimeEncodeQP' :: String -> String
mimeEncodeQP' [] = []
mimeEncodeQP' (h:t) =
let str = if reservedChar h then escape h else [h]
in str ++ mimeEncodeQP' t
where
escape x =
let y = ord x in
[ '=', intToDigit ((y `div` 16) .&. 0xf), intToDigit (y .&. 0xf) ]
reservedChar :: Char -> Bool
reservedChar x
| xi >= 0x20 && xi <= 0x7e = False
| otherwise = True
where xi = ord x