aws-cloudfront-signed-cookies 0.1.0.1 → 0.2.0.0
raw patch · 11 files changed
+558/−96 lines, 11 filesdep +aeson-prettydep +hedgehogdep +lensPVP ok
version bump matches the API change (PVP)
Dependencies added: aeson-pretty, hedgehog, lens, lens-aeson, neat-interpolation
API changes (from Hackage documentation)
- Network.AWS.CloudFront.SignedCookies.CLI: Opts :: PemFilePath -> KeyPairId -> Resource -> Lifespan -> Opts
- Network.AWS.CloudFront.SignedCookies.CLI: [opt_keyPairId] :: Opts -> KeyPairId
- Network.AWS.CloudFront.SignedCookies.CLI: [opt_lifespan] :: Opts -> Lifespan
- Network.AWS.CloudFront.SignedCookies.CLI: [opt_pemFilePath] :: Opts -> PemFilePath
- Network.AWS.CloudFront.SignedCookies.CLI: [opt_resource] :: Opts -> Resource
- Network.AWS.CloudFront.SignedCookies.CLI: data Opts
- Network.AWS.CloudFront.SignedCookies.CLI: optsParser :: Parser Opts
+ Network.AWS.CloudFront.SignedCookies: PolicyCookie :: Text -> PolicyCookie
+ Network.AWS.CloudFront.SignedCookies: SignatureCookie :: Text -> SignatureCookie
+ Network.AWS.CloudFront.SignedCookies: cookiePolicy :: PolicyCookie -> Either String Policy
+ Network.AWS.CloudFront.SignedCookies: jsonTextPolicy :: Text -> Either String Policy
+ Network.AWS.CloudFront.SignedCookies: jsonValPolicy :: Value -> Either String Policy
+ Network.AWS.CloudFront.SignedCookies: newtype PolicyCookie
+ Network.AWS.CloudFront.SignedCookies: newtype SignatureCookie
+ Network.AWS.CloudFront.SignedCookies: policyJSON :: Policy -> ByteString
+ Network.AWS.CloudFront.SignedCookies.CLI.Decode: Opts :: PolicyCookie -> Opts
+ Network.AWS.CloudFront.SignedCookies.CLI.Decode: [opt_policyCookie] :: Opts -> PolicyCookie
+ Network.AWS.CloudFront.SignedCookies.CLI.Decode: data Opts
+ Network.AWS.CloudFront.SignedCookies.CLI.Decode: main :: IO ()
+ Network.AWS.CloudFront.SignedCookies.CLI.Decode: mainOpts :: Opts -> IO ()
+ Network.AWS.CloudFront.SignedCookies.CLI.Decode: mainParserInfo :: ParserInfo (IO ())
+ Network.AWS.CloudFront.SignedCookies.CLI.Decode: optsParser :: Parser Opts
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: Opts :: PemFilePath -> KeyPairId -> Resource -> Lifespan -> Opts
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: [opt_keyPairId] :: Opts -> KeyPairId
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: [opt_lifespan] :: Opts -> Lifespan
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: [opt_pemFilePath] :: Opts -> PemFilePath
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: [opt_resource] :: Opts -> Resource
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: data Opts
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: main :: IO ()
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: mainOpts :: Opts -> IO ()
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: mainParserInfo :: ParserInfo (IO ())
+ Network.AWS.CloudFront.SignedCookies.CLI.Sign: optsParser :: Parser Opts
+ Network.AWS.CloudFront.SignedCookies.Encoding: base64Decode :: Text -> Either String ByteString
+ Network.AWS.CloudFront.SignedCookies.Policy: jsonTextPolicy :: Text -> Either String Policy
+ Network.AWS.CloudFront.SignedCookies.Policy: jsonValPolicy :: Value -> Either String Policy
+ Network.AWS.CloudFront.SignedCookies.Types: PolicyCookie :: Text -> PolicyCookie
+ Network.AWS.CloudFront.SignedCookies.Types: SignatureCookie :: Text -> SignatureCookie
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.CookieDomain
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.CookiePath
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.EndTime
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.IpAddress
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.KeyPairId
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.Lifespan
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.PemFilePath
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.Policy
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.PolicyCookie
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.Resource
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.SignatureCookie
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Classes.Eq Network.AWS.CloudFront.SignedCookies.Types.StartTime
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.CookieDomain
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.CookiePath
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.EndTime
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.IpAddress
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.KeyPairId
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.Lifespan
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.PemFilePath
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.Policy
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.PolicyCookie
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.Resource
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.SignatureCookie
+ Network.AWS.CloudFront.SignedCookies.Types: instance GHC.Show.Show Network.AWS.CloudFront.SignedCookies.Types.StartTime
+ Network.AWS.CloudFront.SignedCookies.Types: newtype PolicyCookie
+ Network.AWS.CloudFront.SignedCookies.Types: newtype SignatureCookie
Files
- aws-cloudfront-signed-cookies.cabal +20/−2
- library/Network/AWS/CloudFront/SignedCookies.hs +24/−0
- library/Network/AWS/CloudFront/SignedCookies/CLI.hs +23/−76
- library/Network/AWS/CloudFront/SignedCookies/CLI/Decode.hs +94/−0
- library/Network/AWS/CloudFront/SignedCookies/CLI/Internal.hs +30/−0
- library/Network/AWS/CloudFront/SignedCookies/CLI/Sign.hs +93/−0
- library/Network/AWS/CloudFront/SignedCookies/Encoding.hs +26/−8
- library/Network/AWS/CloudFront/SignedCookies/Policy.hs +67/−0
- library/Network/AWS/CloudFront/SignedCookies/Types.hs +19/−0
- readme.md +68/−10
- test/hedgehog.hs +94/−0
aws-cloudfront-signed-cookies.cabal view
@@ -1,5 +1,5 @@ name: aws-cloudfront-signed-cookies-version: 0.1.0.1+version: 0.2.0.0 synopsis: Generate signed cookies for AWS CloudFront category: Network, AWS, Cloud@@ -33,16 +33,20 @@ exposed-modules: Network.AWS.CloudFront.SignedCookies , Network.AWS.CloudFront.SignedCookies.CLI+ , Network.AWS.CloudFront.SignedCookies.CLI.Decode+ , Network.AWS.CloudFront.SignedCookies.CLI.Sign , Network.AWS.CloudFront.SignedCookies.Crypto , Network.AWS.CloudFront.SignedCookies.Encoding , Network.AWS.CloudFront.SignedCookies.Policy , Network.AWS.CloudFront.SignedCookies.Types other-modules:- Network.AWS.CloudFront.SignedCookies.Crypto.Internal+ Network.AWS.CloudFront.SignedCookies.CLI.Internal+ , Network.AWS.CloudFront.SignedCookies.Crypto.Internal build-depends: aeson+ , aeson-pretty , asn1-encoding , asn1-types , base >=4.7 && <5@@ -50,6 +54,8 @@ , bytestring , cookie , cryptonite+ , lens+ , lens-aeson , optparse-applicative , pem , text@@ -65,3 +71,15 @@ build-depends: aws-cloudfront-signed-cookies , base >=4.7 && <5++test-suite hedgehog+ default-language: Haskell2010+ hs-source-dirs: test+ type: exitcode-stdio-1.0+ main-is: hedgehog.hs+ ghc-options: -threaded+ build-depends:+ aws-cloudfront-signed-cookies+ , base >=4.7 && <5+ , hedgehog+ , neat-interpolation
library/Network/AWS/CloudFront/SignedCookies.hs view
@@ -57,11 +57,21 @@ , KeyPairId (..) , PrivateKey + -- * Policy JSON+ , policyJSON+ , jsonTextPolicy+ , jsonValPolicy++ -- * Reading cookies+ , cookiePolicy+ -- * Miscellaneous -- ** Cookies , CookiesText , renderCookiesText+ , PolicyCookie (..)+ , SignatureCookie (..) -- ** Time , NominalDiffTime@@ -79,10 +89,17 @@ import Network.AWS.CloudFront.SignedCookies.Policy import Network.AWS.CloudFront.SignedCookies.Types +-- aeson+import qualified Data.Aeson as A+ -- base+import Control.Monad ((>=>)) import Data.Coerce (coerce) import Data.Semigroup ((<>)) +-- bytestring+import qualified Data.ByteString.Lazy as LBS+ -- text import qualified Data.Text as Text @@ -123,3 +140,10 @@ renderCookiesText :: CookiesText -> Text renderCookiesText = Text.unlines . map (\(k, v) -> "Cookie: " <> k <> "=" <> v)++-- | Parse the text value of a @CloudFront-Policy@ cookie into a 'Policy'+-- value, producing 'Left' with an error message if parsing fails.+cookiePolicy :: PolicyCookie -> Either String Policy+cookiePolicy =+ (base64Decode . coerce @PolicyCookie @Text) >=>+ (A.eitherDecode' . LBS.fromStrict) >=> jsonValPolicy
library/Network/AWS/CloudFront/SignedCookies/CLI.hs view
@@ -1,94 +1,41 @@ {-# LANGUAGE FlexibleContexts, OverloadedStrings, RecordWildCards #-} -- | Command-line interface for generating AWS CloudFront signed cookies.+--+-- This program is the aggregation of these subcommands:+--+-- * "Network.AWS.CloudFront.SignedCookies.CLI.Decode"+-- * "Network.AWS.CloudFront.SignedCookies.CLI.Sign" module Network.AWS.CloudFront.SignedCookies.CLI- ( main, Opts (..), optsParser+ ( main ) where -import Network.AWS.CloudFront.SignedCookies+import Network.AWS.CloudFront.SignedCookies.CLI.Internal +import qualified Network.AWS.CloudFront.SignedCookies.CLI.Decode as Decode+import qualified Network.AWS.CloudFront.SignedCookies.CLI.Sign as Sign+ -- base-import Data.Coerce (Coercible, coerce)-import Data.Foldable (for_)+import Control.Monad (join) import Data.Semigroup ((<>)) -- optparse-applicative import qualified Options.Applicative as Opt --- text-import qualified Data.Text as Text-import qualified Data.Text.IO as Text---- time-import Data.Time.Clock (nominalDay)---- | The entry point for the AWS CloudFront cookie-signing--- command-line interface.+-- | Entry point for the AWS CloudFront signed cookie command-line interface. main :: IO ()-main = do-- -- Parse the command-line arguments- Opts{..} <- Opt.execParser $- Opt.info (Opt.helper <*> optsParser) $- Opt.header "Generator of signed cookies for AWS CloudFront"-- -- Construct an IAM policy- policy <- simplePolicy opt_resource opt_lifespan-- -- Parse the .pem file to get the private key- key <- readPrivateKeyPemFile opt_pemFilePath-- -- Construct signed cookies- cookies <- createSignedCookies opt_keyPairId key policy-- -- Print the cookies to stdout- (Text.putStr . renderCookiesText) cookies---- | The parse result for the command-line arguments.--data Opts =- Opts- { opt_pemFilePath :: PemFilePath- -- ^ Location in the filesystem where a .pem file- -- containing an RSA secret key can be found- , opt_keyPairId :: KeyPairId- -- ^ CloudFront key pair ID for the key pair that- -- you are using to generate signature- , opt_resource :: Resource- -- ^ URL that the policy will grant access to,- -- optionally containing asterisks for wildcards- , opt_lifespan :: Lifespan- -- ^ Amount of time until the policy expires- }---- | Parser for all of the command-line arguments.------ See "Options.Applicative", 'Opt.info', and 'Opt.execParser'--- to learn how to use a 'Opt.Parser'.--optsParser :: Opt.Parser Opts-optsParser =- Opts- <$> text "pem-file" "Location in the filesystem where a .pem file \- \containing an RSA secret key can be found"-- <*> text "key-pair-id" "CloudFront key pair ID for the key pair that \- \you are using to generate signature"-- <*> text "resource" "URL that the policy will grant access to, \- \optionally containing asterisks for wildcards"-- <*> days "days" "Integer number of days until the policy expires"-- where- text :: Coercible Text a => String -> String -> Opt.Parser a- text long help = fmap (coerce . Text.pack) $ Opt.strOption $ mod long help+main =+ join $ Opt.execParser mainParserInfo - days :: Coercible NominalDiffTime a => String -> String -> Opt.Parser a- days long help = fmap (coerce . (* nominalDay) . fromInteger) $- Opt.option Opt.auto $ mod long help+mainParserInfo :: Opt.ParserInfo (IO ())+mainParserInfo =+ Opt.info (Opt.helper <*> mainParser) $+ Opt.header "AWS CloudFront signed cookie utility" - mod :: Opt.HasName f => String -> String -> Opt.Mod f a- mod long help = Opt.long long <> Opt.help help+mainParser :: Opt.Parser (IO ())+mainParser =+ Opt.hsubparser $+ Opt.command "decode" Decode.mainParserInfo <>+ Opt.command "sign" Sign.mainParserInfo
+ library/Network/AWS/CloudFront/SignedCookies/CLI/Decode.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings, RecordWildCards, ScopedTypeVariables, TypeApplications #-}++-- | Command-line interface for decoding AWS CloudFront policy cookies.++module Network.AWS.CloudFront.SignedCookies.CLI.Decode+ ( main, mainOpts, Opts (..), optsParser, mainParserInfo+ ) where++import Network.AWS.CloudFront.SignedCookies+import Network.AWS.CloudFront.SignedCookies.CLI.Internal+import Network.AWS.CloudFront.SignedCookies.Encoding++-- aeson+import qualified Data.Aeson as A++-- aeson-pretty+import Data.Aeson.Encode.Pretty (encodePrettyToTextBuilder)++-- base+import Data.Coerce (Coercible, coerce)+import Data.Foldable (for_)+import Data.Semigroup ((<>))++-- bytestring+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS++-- optparse-applicative+import qualified Options.Applicative as Opt++-- text+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import qualified Data.Text.Lazy as LText+import qualified Data.Text.Lazy.IO as LText+import Data.Text.Lazy.Builder as Text++-- time+import Data.Time.Clock (nominalDay)++-- | Entry point for the AWS CloudFront cookie decoding command-line interface.++main :: IO ()+main = do++ opts <- Opt.execParser $ Opt.info (Opt.helper <*> optsParser) infoMod++ mainOpts opts++mainOpts :: Opts -> IO ()+mainOpts Opts{..} = do++ bs :: BS.ByteString <-+ case base64Decode $ coerce @PolicyCookie @Text opt_policyCookie of+ Left e -> fail e+ Right x -> pure x++ value <-+ case A.eitherDecode' (LBS.fromStrict bs) of+ Left e -> fail e+ Right x -> pure x++ let+ result :: LText.Text =+ Text.toLazyText $ encodePrettyToTextBuilder @A.Value value++ -- Print the JSON to stdout+ LText.putStr result++-- | Parse result for the command-line arguments for cookie decoding.++data Opts =+ Opts+ { opt_policyCookie :: PolicyCookie+ -- ^ The value of a @CloudFront-Policy@ cookie.+ }++-- | Parser for all of the command-line arguments for cookie decoding.+--+-- See "Options.Applicative", 'Opt.info', and 'Opt.execParser'+-- to learn how to use a 'Opt.Parser'.++optsParser :: Opt.Parser Opts+optsParser =+ Opts+ <$> text "policy-cookie" "The value of a CloudFront-Policy cookie"++mainParserInfo :: Opt.ParserInfo (IO ())+mainParserInfo =+ mainOpts <$> Opt.info optsParser infoMod++infoMod :: Opt.InfoMod a+infoMod =+ Opt.header "Decode signed AWS CloudFront policy cookies"
+ library/Network/AWS/CloudFront/SignedCookies/CLI/Internal.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE FlexibleContexts #-}++module Network.AWS.CloudFront.SignedCookies.CLI.Internal+ ( text, days+ ) where++import Network.AWS.CloudFront.SignedCookies+import Network.AWS.CloudFront.SignedCookies.Types++-- base+import Data.Coerce (Coercible, coerce)+import Data.Semigroup ((<>))+import Prelude hiding (mod)++-- optparse-applicative+import qualified Options.Applicative as Opt++-- text+import qualified Data.Text as Text+++text :: Coercible Text a => String -> String -> Opt.Parser a+text long help = fmap (coerce . Text.pack) $ Opt.strOption $ mod long help++days :: Coercible NominalDiffTime a => String -> String -> Opt.Parser a+days long help = fmap (coerce . (* nominalDay) . fromInteger) $+ Opt.option Opt.auto $ mod long help++mod :: Opt.HasName f => String -> String -> Opt.Mod f a+mod long help = Opt.long long <> Opt.help help
+ library/Network/AWS/CloudFront/SignedCookies/CLI/Sign.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}++-- | Command-line interface for generating AWS CloudFront signed cookies.++module Network.AWS.CloudFront.SignedCookies.CLI.Sign+ ( main, mainOpts, Opts (..), optsParser, mainParserInfo+ ) where++import Network.AWS.CloudFront.SignedCookies+import Network.AWS.CloudFront.SignedCookies.CLI.Internal++-- base+import Data.Coerce (Coercible, coerce)+import Data.Foldable (for_)+import Data.Semigroup ((<>))++-- optparse-applicative+import qualified Options.Applicative as Opt++-- text+import qualified Data.Text as Text+import qualified Data.Text.IO as Text++-- time+import Data.Time.Clock (nominalDay)++-- | Entry point for the AWS CloudFront cookie-signing command-line interface.++main :: IO ()+main = do++ opts <- Opt.execParser $ Opt.info (Opt.helper <*> optsParser) infoMod++ mainOpts opts++mainOpts :: Opts -> IO ()+mainOpts Opts{..} = do++ -- Construct an IAM policy+ policy <- simplePolicy opt_resource opt_lifespan++ -- Parse the .pem file to get the private key+ key <- readPrivateKeyPemFile opt_pemFilePath++ -- Construct signed cookies+ cookies <- createSignedCookies opt_keyPairId key policy++ -- Print the cookies to stdout+ (Text.putStr . renderCookiesText) cookies++-- | Parse result for the command-line arguments for signed cookie generation.++data Opts =+ Opts+ { opt_pemFilePath :: PemFilePath+ -- ^ Location in the filesystem where a .pem file+ -- containing an RSA secret key can be found+ , opt_keyPairId :: KeyPairId+ -- ^ CloudFront key pair ID for the key pair that+ -- you are using to generate signature+ , opt_resource :: Resource+ -- ^ URL that the policy will grant access to,+ -- optionally containing asterisks for wildcards+ , opt_lifespan :: Lifespan+ -- ^ Amount of time until the policy expires+ }++-- | Parser for all the command-line arguments for signed cookie generation.+--+-- See "Options.Applicative", 'Opt.info', and 'Opt.execParser'+-- to learn how to use a 'Opt.Parser'.++optsParser :: Opt.Parser Opts+optsParser =+ Opts+ <$> text "pem-file" "Location in the filesystem where a .pem file \+ \containing an RSA secret key can be found"++ <*> text "key-pair-id" "CloudFront key pair ID for the key pair that \+ \you are using to generate signature"++ <*> text "resource" "URL that the policy will grant access to, \+ \optionally containing asterisks for wildcards"++ <*> days "days" "Integer number of days until the policy expires"++mainParserInfo :: Opt.ParserInfo (IO ())+mainParserInfo =+ mainOpts <$> Opt.info optsParser infoMod++infoMod :: Opt.InfoMod a+infoMod =+ Opt.header "Generate signed cookies for AWS CloudFront"
library/Network/AWS/CloudFront/SignedCookies/Encoding.hs view
@@ -2,6 +2,7 @@ module Network.AWS.CloudFront.SignedCookies.Encoding ( base64Encode+ , base64Decode ) where import Network.AWS.CloudFront.SignedCookies.Types@@ -40,12 +41,29 @@ base64Encode :: ByteString -> Text base64Encode =- Text.map charSubstitution . Text.decodeUtf8 . Base64.encode+ Text.map charEncode . Text.decodeUtf8 . Base64.encode+ where+ charEncode =+ \case+ '+' -> '-'+ '=' -> '_'+ '/' -> '~'+ x -> x -charSubstitution :: Char -> Char-charSubstitution =- \case- '+' -> '-'- '=' -> '_'- '/' -> '~'- x -> x+{- |++The inverse of 'base64Encode'. Produces a 'Left' value with+an error message if decoding fails.++-}++base64Decode :: Text -> Either String ByteString+base64Decode =+ Base64.decode . Text.encodeUtf8 . Text.map charDecode+ where+ charDecode =+ \case+ '-' -> '+'+ '_' -> '='+ '~' -> '/'+ x -> x
library/Network/AWS/CloudFront/SignedCookies/Policy.hs view
@@ -15,6 +15,8 @@ -- * JSON representation , policyJSON+ , jsonTextPolicy+ , jsonValPolicy ) where @@ -24,11 +26,21 @@ import qualified Data.Aeson as A -- base+import Control.Monad ((>=>)) import Data.Semigroup ((<>)) -- bytestring import qualified Data.ByteString.Lazy as LBS +-- lens+import Control.Lens ((&), (^.), (^?))++-- lens-aeson+import Data.Aeson.Lens (AsNumber (..), AsPrimitive (..), key, nth, _Array, _Object)++-- text+import qualified Data.Text.Encoding as Text+ -- time import Data.Time.Clock.POSIX (getPOSIXTime) @@ -91,6 +103,61 @@ (.=) :: Text -> A.Value -> A.Object (.=) = Map.singleton++jsonTextPolicy :: Text -> Either String Policy+jsonTextPolicy =+ (A.eitherDecode' . LBS.fromStrict . Text.encodeUtf8) >=>+ jsonValPolicy++jsonValPolicy :: A.Value -> Either String Policy+jsonValPolicy val =+ Policy+ <$> jsonResource val+ <*> jsonStart val+ <*> jsonEnd val+ <*> jsonIpAddress val++jsonResource :: A.Value -> Either String Resource+jsonResource val =+ maybe (Left "Missing \"Resource\"") Right $+ fmap Resource+ (val ^? key "Statement"+ . nth 0+ . key "Resource"+ . _String)++jsonStart :: A.Value -> Either String StartTime+jsonStart val =+ Right $+ maybe StartImmediately (StartTime . fromInteger)+ (val ^? key "Statement"+ . nth 0+ . key "Condition"+ . key "DateGreaterThan"+ . key "AWS:EpochTime"+ . _Integer)++jsonEnd :: A.Value -> Either String EndTime+jsonEnd val =+ maybe (Left "Missing \"DateLessThan\"") Right $+ fmap (EndTime . fromInteger)+ (val ^? key "Statement"+ . nth 0+ . key "Condition"+ . key "DateLessThan"+ . key "AWS:EpochTime"+ . _Integer)++jsonIpAddress :: A.Value -> Either String IpAddress+jsonIpAddress val =+ Right $+ maybe AnyIp IpAddress+ (val ^? key "Statement"+ . nth 0+ . key "Condition"+ . key "IpAddress"+ . key "AWS:SourceIp"+ . _String) {- |
library/Network/AWS/CloudFront/SignedCookies/Types.hs view
@@ -9,6 +9,7 @@ -- * Cookies , CookiesText, SetCookie, CookieDomain (..), CookiePath (..)+ , PolicyCookie (..), SignatureCookie (..) -- * Time , NominalDiffTime, POSIXTime, Lifespan (..), StartTime (..), EndTime (..)@@ -51,6 +52,7 @@ -} newtype PemFilePath = PemFilePath Text+ deriving (Eq, Show) {- | @@ -64,6 +66,7 @@ -} newtype KeyPairId = KeyPairId Text+ deriving (Eq, Show) {- | @@ -74,10 +77,20 @@ -} newtype CookieDomain = CookieDomain Text+ deriving (Eq, Show) -- | Usually @"/"@ newtype CookiePath = CookiePath Text+ deriving (Eq, Show) +-- | The value of a @CloudFront-Policy@ cookie.+newtype PolicyCookie = PolicyCookie Text+ deriving (Eq, Show)++-- | The value of a @CloudFront-Signature@ cookie.+newtype SignatureCookie = SignatureCookie Text+ deriving (Eq, Show)+ {- | URL that a policy will grant access to, optionally containing@@ -90,18 +103,23 @@ -} newtype Resource = Resource Text+ deriving (Eq, Show) -- | How long from now the credentials expire newtype Lifespan = Lifespan NominalDiffTime+ deriving (Eq, Show) -- | The time at which credentials begin to take effect data StartTime = StartImmediately | StartTime POSIXTime+ deriving (Eq, Show) -- | The time at which credentials expire newtype EndTime = EndTime POSIXTime+ deriving (Eq, Show) -- | The IP address or address range of clients allowed to make requests data IpAddress = AnyIp | IpAddress Text+ deriving (Eq, Show) {- | @@ -123,3 +141,4 @@ , policyIpAddress :: IpAddress -- ^ The IP address or address range of clients allowed to make requests }+ deriving (Eq, Show)
readme.md view
@@ -6,6 +6,8 @@ ## The library +### Creating signed cookies+ Example usage: ```haskell@@ -37,26 +39,41 @@ The output should look something like this: ```haskell-Cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc29...-Cookie: CloudFront-Signature=wMN6V3Okxk7sdSPZeebMh-wo...+Cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zZWNyZXRzLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE1MjM2NjQxMjV9fX1dfQ__+Cookie: CloudFront-Signature=R17isgiJD36sb4kh4pNxqq5eFCx5Q~vC~QbQKMhi9BqyhLboLQfb3pCdPp-WSVYOn4wlOobqhgcwoX2vjCd3y8eiUtelX1~ddaDlPBkkdZA~5Imd2z-W1o3r0coKB1SE2SPFT7M1XgNvwOPanQoft1VLchfVPGaitFYMum4KS~GXffqQZzaVybKJ64KfFLLBPSobg8MmBhHvpO9DBiwKijmGhDip~6L3W7OcqGT8HdqAmWxjnTHInXpx7bbVotla6a~J6WB6xmtJrmQzMLdTUxAY6IbJ2PzMtTXKgdNzbByGHvImg9k3Q3fNBTim8l7Tds1zpwX9GsuPcFkPe9HZ1Q__ Cookie: CloudFront-Key-Pair-Id=APKAIATXN3RCIOVT5WRQ ``` -You can see a very similar example in action in the `Network.AWS.CloudFront.SignedCookies.CLI` module which defines the command-line interface.+You can see a very similar example in action in the `Network.AWS.CloudFront.SignedCookies.CLI.Sign` module which defines the command-line interface for creating signed cookies. +### Decoding policy cookies++After you generate a policy cookie and send it to a client, you may later want to parse it back into a `Policy` value -- for example, to determine whether you need to send a new set of cookies to replace an expired policy.++Example in GHCi:++```+λ> cookiePolicy (PolicyCookie "eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zZWNyZXRzLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE1MjM2NjQxMjV9fX1dfQ__")+Right (Policy {policyResource = Resource "https://example.com/secrets/*", policyStart = StartImmediately, policyEnd = EndTime 1523664125s, policyIpAddress = AnyIp})+```+ ## The executable -You can also generate cookies using the command-line interface.+You can also generate cookies using the command-line interface. It provides two commands: +* `sign` - Given a private key and policy options, produces signed cookies and prints them as HTTP request headers.+* `decode` - Decodes a `CloudFront-Policy` cookie and prints it in human-readable JSON format.++### `sign`+ ```-$ aws-cloudfront-signed-cookies --help-Generator of signed cookies for AWS CloudFront+$ aws-cloudfront-signed-cookies sign --help+Generate signed cookies for AWS CloudFront -Usage: aws-cloudfront-signed-cookies --pem-file ARG --key-pair-id ARG- --resource ARG --days ARG+Usage: aws-cloudfront-signed-cookies sign --pem-file ARG --key-pair-id ARG+ --resource ARG --days ARG Available options:- -h,--help Show this help text --pem-file ARG Location in the filesystem where a .pem file containing an RSA secret key can be found --key-pair-id ARG CloudFront key pair ID for the key pair that you are@@ -64,14 +81,55 @@ --resource ARG URL that the policy will grant access to, optionally containing asterisks for wildcards --days ARG Integer number of days until the policy expires+ -h,--help Show this help text ``` Example usage: ```-$ aws-cloudfront-signed-cookies \+$ aws-cloudfront-signed-cookies sign \ --pem-file pk-APKAIATXN3RCIOVT5WRQ.pem \ --key-pair-id APKAIATXN3RCIOVT5WRQ \ --resource "https://example.com/secrets/*" \ --days 2+```++Output:++```+Cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zZWNyZXRzLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE1MjM2NjQxMjV9fX1dfQ__+Cookie: CloudFront-Signature=R17isgiJD36sb4kh4pNxqq5eFCx5Q~vC~QbQKMhi9BqyhLboLQfb3pCdPp-WSVYOn4wlOobqhgcwoX2vjCd3y8eiUtelX1~ddaDlPBkkdZA~5Imd2z-W1o3r0coKB1SE2SPFT7M1XgNvwOPanQoft1VLchfVPGaitFYMum4KS~GXffqQZzaVybKJ64KfFLLBPSobg8MmBhHvpO9DBiwKijmGhDip~6L3W7OcqGT8HdqAmWxjnTHInXpx7bbVotla6a~J6WB6xmtJrmQzMLdTUxAY6IbJ2PzMtTXKgdNzbByGHvImg9k3Q3fNBTim8l7Tds1zpwX9GsuPcFkPe9HZ1Q__+Cookie: CloudFront-Key-Pair-Id=APKAIATXN3RCIOVT5WRQ+```++### `decode`++```+$ aws-cloudfront-signed-cookies decode --help+Decode signed AWS CloudFront policy cookies++Usage: aws-cloudfront-signed-cookies decode --policy-cookie ARG++Available options:+ --policy-cookie ARG The value of a CloudFront-Policy cookie+ -h,--help Show this help text++```++Example usage:++```+$ aws-cloudfront-signed-cookies decode --policy-cookie eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zZWNyZXRzLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE1MjM2NjQxMjV9fX1dfQ__+{+ "Statement": [+ {+ "Resource": "https://example.com/secrets/*",+ "Condition": {+ "DateLessThan": {+ "AWS:EpochTime": 1523664125+ }+ }+ }+ ]+} ```
+ test/hedgehog.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell #-}++module Main (main) where++import Network.AWS.CloudFront.SignedCookies++-- base+import Control.Monad (unless)+import Data.Foldable (for_)+import qualified System.Exit as Exit+import qualified System.IO as IO++-- hedgehog+import Hedgehog+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++-- neat-interpolation+import NeatInterpolation++main :: IO ()+main = do+ for_ [IO.stdout, IO.stderr] $ \h -> do+ IO.hSetEncoding h IO.utf8+ IO.hSetBuffering h IO.LineBuffering+ success <- Hedgehog.checkParallel $$(Hedgehog.discover)+ unless success Exit.exitFailure++prop_jsonPolicy_1 :: Property+prop_jsonPolicy_1 = withTests 1 $ property $+ let+ x = [text|+ {+ "Statement": [+ {+ "Resource": "http://d111111abcdef8.cloudfront.net/game_download.zip",+ "Condition": {+ "IpAddress": {"AWS:SourceIp": "192.0.2.0/24"},+ "DateLessThan": {"AWS:EpochTime": 1357034400}+ }+ }+ ]+ }+ |]++ p = Policy+ (Resource "http://d111111abcdef8.cloudfront.net/game_download.zip")+ StartImmediately+ (EndTime 1357034400)+ (IpAddress "192.0.2.0/24")+ in+ jsonTextPolicy x === Right p++prop_jsonPolicy_2 :: Property+prop_jsonPolicy_2 = withTests 1 $ property $+ let+ x = [text|+ {+ "Statement": [+ {+ "Resource": "http://*",+ "Condition": {+ "IpAddress": {"AWS:SourceIp": "192.0.2.10/32"},+ "DateGreaterThan": {"AWS:EpochTime": 1357034400},+ "DateLessThan": {"AWS:EpochTime": 1357120800}+ }+ }+ ]+ }+ |]++ p = Policy+ (Resource "http://*")+ (StartTime 1357034400)+ (EndTime 1357120800)+ (IpAddress "192.0.2.10/32")+ in+ jsonTextPolicy x === Right p++prop_jsonPolicy_3 :: Property+prop_jsonPolicy_3 = withTests 1 $ property $+ let+ x = [text|+ {+ "Statement": [+ {+ "Resource": "http://*",+ "Condition": {+ "IpAddress": {"AWS:SourceIp": "192.0.2.10/32"},+ "DateGreaterThan":+ |]++ in+ jsonTextPolicy x === Left "Error in $: not enough input"