packages feed

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 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"