{-# LANGUAGE RecordWildCards            #-} 

module Aws.CloudFront.Signer
    ( URL
    , JSONPOlicy
    , CloudFrontSigningKey(..)
    , CloudFrontPolicy(..)
    , readCloudFrontSigningKeyFromDER
    , parseRSAPrivateKeyDER
    , signCannedPolicyURL
    , signCustomPolicyURL
    , signCustomPolicyURL_
    , cannedPolicy
    , customPolicy
    , unixTime
    ) where

import qualified Data.ASN1.Encoding             as A
import qualified Data.ASN1.BinaryEncoding       as A
import qualified Data.ASN1.Types                as A
import qualified Data.ByteString.Lazy.Char8     as LBS
import qualified Data.ByteString.Base64.Lazy    as B64
import           Data.Time
import           Data.Maybe
import           Codec.Crypto.RSA
import qualified Crypto.Types.PubKey.RSA        as C
import           Text.Printf
import           System.Locale

-- | input and output URLs
type URL        = String

-- | a JSON CloudFront policy
type JSONPOlicy = String        

-- | the CloudFront key pair identifier
type KeyID      = String        


-- | a CloudFront siging key has an identifier and an RSA private key

data CloudFrontSigningKey
    = CloudFrontSigningKey
        { cfk_key_id :: KeyID
        , cfk_key    :: PrivateKey
        }
    deriving (Show)

-- | a CloudFront policy must identify the resource being accessed and the
--   expiry time; a starting time and IPv4 address may also be specified

data CloudFrontPolicy
    = CloudFrontPolicy
        { cfp_Resource        :: URL
        , cfp_DateLessThan    :: UTCTime
        , cfp_DateGreaterThan :: Maybe UTCTime
        , cfp_IpAddress       :: Maybe String
        }

-- | RSA private keys can only be read from DER file for now (the OpenSSL
--   tools can be used to convert from PEM:
--
--      openssl rsa -in input.pem -inform PEM -out output.der -outform DER
--

readCloudFrontSigningKeyFromDER :: KeyID -> FilePath -> IO CloudFrontSigningKey
readCloudFrontSigningKeyFromDER ki fp = 
 do pk_b <- LBS.readFile fp
    case parseRSAPrivateKeyDER pk_b of
      Left err -> error err
      Right pk ->
        return $
            CloudFrontSigningKey 
                { cfk_key_id = ki
                , cfk_key    = pk
                }

-- | If you have the DER ByteString then you can construct a private key
--   functionally.

parseRSAPrivateKeyDER :: LBS.ByteString -> Either String C.PrivateKey
parseRSAPrivateKeyDER bs = 
    case A.decodeASN1 A.DER bs of
      Left err -> Left $ show err
      Right as ->
        case A.fromASN1 as of
          Left err -> Left $ show err
          Right pr -> 
            case pr of
              (pk,[]) -> Right pk
              _       -> Left "residula data"

-- | In most cases only a time-limited, signed URL is needed, in which case a
--   canned policy can be used; URLs signed with a canned policy are shorter
--   than those signed with a custom policy.

signCannedPolicyURL :: CloudFrontSigningKey -> UTCTime -> URL -> URL
signCannedPolicyURL CloudFrontSigningKey{..} exp_utc url = 
    printf "%s%cExpires=%s&Signature=%s&Key-Pair-Id=%s" url sep exp_eps pol_sig cfk_key_id  
  where
    exp_eps = unixTime exp_utc
    pol_sig = b64 $ rsa_sha1 cfk_key pol 
    pol     = cannedPolicy exp_utc url
    sep     = if any (=='?') url then '&' else '?'

-- | Signing a URL with a custom policy allows a start time to be specified and
--   the IP address of the recipient(s) to be specified.

signCustomPolicyURL :: CloudFrontSigningKey -> CloudFrontPolicy -> URL
signCustomPolicyURL cfk cfp = signCustomPolicyURL_ cfk (customPolicy cfp) $ cfp_Resource cfp

-- | The URL can also be signed with the custom policy in JSON format.
--   (See the CloudFront documentation for details.)

signCustomPolicyURL_ :: CloudFrontSigningKey -> JSONPOlicy -> URL -> URL
signCustomPolicyURL_ CloudFrontSigningKey{..} pol url =
    printf "%s%cPolicy=%s&Signature=%s&Key-Pair-Id=%s" url sep pol_b64 pol_sig cfk_key_id  
  where
    pol_sig = b64 $ rsa_sha1 cfk_key pol 
    pol_b64 = b64            pol
    sep     = if any (=='?') url then '&' else '?'

-- | The JSON canned policy can be generated from the expiry time and
--   the URL of the distributed resource.

cannedPolicy :: UTCTime -> URL -> JSONPOlicy
cannedPolicy exp_utc url =
    concat
        [ "{\"Statement\":[{\"Resource\":\""
        , url    
        , "\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":"
        , unixTime exp_utc
        , "}}}]}"
        ]

-- | JSON custom policies provide more flexibility (allowing start times and
--   recipient IP addresses to be specified) but generate longer signed URLs.

customPolicy :: CloudFrontPolicy -> JSONPOlicy
customPolicy CloudFrontPolicy{..} = unlines $ catMaybes
    [ ok $          "{"
    , ok $          "   \"Statement\": [{"
    , ok $          "      \"Resource\":\""                             ++                      cfp_Resource     ++ "\","
    , ok $          "      \"Condition\":{"
    , ok $          "         \"DateLessThan\":{\"AWS:EpochTime\":"     ++ unixTime             cfp_DateLessThan ++ "},"
    , st $ \ust ->  "         \"DateGreaterThan\":{\"AWS:EpochTime\":"  ++ unixTime             ust              ++ "},"
    , ok $          "         \"IpAddress\":{\"AWS:SourceIp\":\""       ++ maybe "0.0.0.0/0" id cfp_IpAddress    ++"\"}"
    , ok $          "      }"
    , ok $          "   }]"
    , ok $          "}"
    ]
  where
    ok   = Just
    st f = maybe Nothing (Just . f) cfp_DateGreaterThan

-- | CloudFront uses Unix Epoch time (number of seconds since 1970, UTC) to
--   specify UTC.

unixTime :: UTCTime -> String
unixTime = formatTime defaultTimeLocale "%s" 

rsa_sha1 :: PrivateKey -> String -> String
rsa_sha1 pk = LBS.unpack . rsassa_pkcs1_v1_5_sign ha_SHA1 pk . LBS.pack

b64 :: String -> String
b64 = map f . LBS.unpack . B64.encode . LBS.pack
  where
    f '+' = '-'
    f '=' = '_'
    f '/' = '~'
    f c   = c