packages feed

configuration-tools 0.2.9 → 0.2.10

raw patch · 12 files changed

+715/−520 lines, 12 filesdep +deepseqPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: deepseq

API changes (from Hackage documentation)

- Configuration.Utils.ConfigFile: parseConfigFiles :: (ConfigFileParser μ, FromJSON (α -> α)) => ConfigFilesConfig -> α -> [ConfigFile] -> μ α
- Configuration.Utils.HttpsCertPolicy: HttpsCertPolicy :: !Bool -> !(HashMap ServiceID Fingerprint) -> HttpsCertPolicy
- Configuration.Utils.HttpsCertPolicy: VerboseTlsException :: Text -> VerboseTlsException
- Configuration.Utils.HttpsCertPolicy: _certPolicyHostFingerprints :: HttpsCertPolicy -> !(HashMap ServiceID Fingerprint)
- Configuration.Utils.HttpsCertPolicy: _certPolicyInsecure :: HttpsCertPolicy -> !Bool
- Configuration.Utils.HttpsCertPolicy: certPolicyHostFingerprints :: Lens' HttpsCertPolicy (HashMap ServiceID Fingerprint)
- Configuration.Utils.HttpsCertPolicy: certPolicyInsecure :: Lens' HttpsCertPolicy Bool
- Configuration.Utils.HttpsCertPolicy: data HttpsCertPolicy
- Configuration.Utils.HttpsCertPolicy: defaultHttpsCertPolicy :: HttpsCertPolicy
- Configuration.Utils.HttpsCertPolicy: httpWithValidationPolicy :: Text -> HttpsCertPolicy -> IO (Response ByteString)
- Configuration.Utils.HttpsCertPolicy: instance Eq HttpsCertPolicy
- Configuration.Utils.HttpsCertPolicy: instance Eq VerboseTlsException
- Configuration.Utils.HttpsCertPolicy: instance Exception VerboseTlsException
- Configuration.Utils.HttpsCertPolicy: instance Ord VerboseTlsException
- Configuration.Utils.HttpsCertPolicy: instance Show HttpsCertPolicy
- Configuration.Utils.HttpsCertPolicy: instance Show VerboseTlsException
- Configuration.Utils.HttpsCertPolicy: instance Typeable HttpsCertPolicy
- Configuration.Utils.HttpsCertPolicy: instance Typeable VerboseTlsException
- Configuration.Utils.HttpsCertPolicy: newtype VerboseTlsException
- Configuration.Utils.HttpsCertPolicy: pHttpsCertPolicy :: Text -> MParser HttpsCertPolicy
+ Configuration.Utils.Internal: (<&>) :: Functor φ => φ α -> (α -> β) -> φ β
+ Configuration.Utils.Internal.ConfigFileReader: Json :: ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: Other :: ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: Yaml :: ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: contentType :: ByteString -> ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: data ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Bounded ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Constructor C1_0ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Constructor C1_1ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Constructor C1_2ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Datatype D1ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Enum ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Eq ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Generic ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance NFData ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Ord ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Read ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Show ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: instance Typeable ConfigFileFormat
+ Configuration.Utils.Internal.ConfigFileReader: isRemote :: ConfigFile -> Bool
+ Configuration.Utils.Internal.ConfigFileReader: jsonMimeType :: IsString s => [s]
+ Configuration.Utils.Internal.ConfigFileReader: loadLocal :: (Functor μ, MonadIO μ, MonadError Text μ, FromJSON (α -> α)) => ConfigFile -> μ (α -> α)
+ Configuration.Utils.Internal.ConfigFileReader: loadRemote :: (ConfigFileParser μ, FromJSON (α -> α)) => ConfigFilesConfig -> ConfigFile -> μ (α -> α)
+ Configuration.Utils.Internal.ConfigFileReader: parseConfigFiles :: (ConfigFileParser μ, FromJSON (α -> α)) => ConfigFilesConfig -> α -> [ConfigFile] -> μ α
+ Configuration.Utils.Internal.ConfigFileReader: readConfigFile :: (ConfigFileParser μ, FromJSON (α -> α)) => ConfigFilesConfig -> ConfigFile -> μ (α -> α)
+ Configuration.Utils.Internal.ConfigFileReader: requestHeaders :: Lens' Request RequestHeaders
+ Configuration.Utils.Internal.ConfigFileReader: yamlMimeType :: IsString s => [s]
+ Configuration.Utils.Internal.HttpsCertPolicy: HttpsCertPolicy :: !Bool -> !(HashMap ServiceID Fingerprint) -> HttpsCertPolicy
+ Configuration.Utils.Internal.HttpsCertPolicy: VerboseTlsException :: Text -> VerboseTlsException
+ Configuration.Utils.Internal.HttpsCertPolicy: _certPolicyHostFingerprints :: HttpsCertPolicy -> !(HashMap ServiceID Fingerprint)
+ Configuration.Utils.Internal.HttpsCertPolicy: _certPolicyInsecure :: HttpsCertPolicy -> !Bool
+ Configuration.Utils.Internal.HttpsCertPolicy: certPolicyHostFingerprints :: Lens' HttpsCertPolicy (HashMap ServiceID Fingerprint)
+ Configuration.Utils.Internal.HttpsCertPolicy: certPolicyInsecure :: Lens' HttpsCertPolicy Bool
+ Configuration.Utils.Internal.HttpsCertPolicy: data HttpsCertPolicy
+ Configuration.Utils.Internal.HttpsCertPolicy: defaultHttpsCertPolicy :: HttpsCertPolicy
+ Configuration.Utils.Internal.HttpsCertPolicy: httpWithValidationPolicy :: Request -> HttpsCertPolicy -> IO (Response ByteString)
+ Configuration.Utils.Internal.HttpsCertPolicy: instance Eq HttpsCertPolicy
+ Configuration.Utils.Internal.HttpsCertPolicy: instance Eq VerboseTlsException
+ Configuration.Utils.Internal.HttpsCertPolicy: instance Exception VerboseTlsException
+ Configuration.Utils.Internal.HttpsCertPolicy: instance Ord VerboseTlsException
+ Configuration.Utils.Internal.HttpsCertPolicy: instance Show HttpsCertPolicy
+ Configuration.Utils.Internal.HttpsCertPolicy: instance Show VerboseTlsException
+ Configuration.Utils.Internal.HttpsCertPolicy: instance Typeable HttpsCertPolicy
+ Configuration.Utils.Internal.HttpsCertPolicy: instance Typeable VerboseTlsException
+ Configuration.Utils.Internal.HttpsCertPolicy: newtype VerboseTlsException
+ Configuration.Utils.Internal.HttpsCertPolicy: pHttpsCertPolicy :: Text -> MParser HttpsCertPolicy
+ Configuration.Utils.Internal.HttpsCertPolicy: simpleHttpWithValidationPolicy :: Text -> HttpsCertPolicy -> IO (Response ByteString)

Files

CHANGELOG.md view
@@ -1,13 +1,27 @@+0.2.10+======++*   Moved all internal APIs to the `Internal` name space. Exposing them+    in 0.2.9 was considered a bug.++*   Configuration files can be formatted either as JSON or as YAML.+    For remote configuration files the HTTP `Content-Type` header is used to+    determine the format, for local files the file suffix is used.+    The default format is YAML.++*   Set the HTTP `accept` header for JSON and YAML when requesting remote+    configuration files.+ 0.2.9 =====  *   Use tight constraint for all validation functions. Previously the-    constraint where unessecarly restrictive.+    constraint where unnecessarily restrictive.  *   Added `updateProperty` function that generalized `%.:` in the same     way as `setProperty` generalizes `..:`. -*   Added validation function for config file arguments.+*   Added a validation function for configuration file arguments.  *   Allow usage of more than a single `--config-file` option on the     command line.@@ -15,9 +29,9 @@ *   Support for static configuration file locations. Configuration files     can be marked as `required` or `optional`. -*   Support loading of configuraiton files form HTTP and HTTPS URLs-    including flags for disabling validation of SSL certificates and-    whitelisting SSL certificates based on their fingerprint.+*   Support for loading of configuration files form HTTP and HTTPS URLs.+    There are new flags for disabling validation of SSL certificates and+    white listing SSL certificates based on their fingerprint.  *   Added tools for updating configurations with a monoid instance. @@ -39,7 +53,7 @@ 0.2.8 ===== -*   Added validation funcitons for Boolean values, numberic values and+*   Added validation functions for Boolean values, numeric values and     orders.  *   Added operator `!..:` for parsing of configuration values that are@@ -53,7 +67,7 @@ 0.2.7 ===== -*   Added `view` funtion for lenses to `Configuration.Utils.Internal`.+*   Added `view` function for lenses to `Configuration.Utils.Internal`.  *   Added support for validation of configuration values. @@ -90,7 +104,7 @@ =====  *   Configuration.Utils.Setup: fixed generation of `PkgInfo` module for-    package configurations with explict flags.+    package configurations with explicit flags.  *   Improved documentation for `Maybe` values. 
README.md view
@@ -3,7 +3,7 @@ Overview ======== -This package provides a collection of utils on top of the packages+This package provides a collection of utilities on top of the packages [optparse-applicative](http://hackage.haskell.org/package/optparse-applicative), [aeson](http://hackage.haskell.org/package/aeson), and [yaml](http://hackage.haskell.org/package/yaml) for configuring libraries and@@ -34,7 +34,7 @@ Installation ============ -Assuming that you have a recent version verison of `GHC` and `Cabal`+Assuming that you have a recent version version of `GHC` and `Cabal` installed in your system this package can be install from [Hackage](http://hackage.haskell.org/) via @@ -81,7 +81,7 @@ Optionally, a function for validating the configuration value may be provided. -The package provides operators and functions that make the implmentation of+The package provides operators and functions that make the implementation of these requisites easy for the common case that the configuration is encoded mainly through nested records. @@ -89,9 +89,9 @@ options are recognized by the application:  *   `--config-file, -c`-    parses the given file as a --possibly partial-- configuration in YAML-    format. The file location can be provided either as a local filesystem path-    or as a remote HTTP or HTTPS URL. In addition a list of static+    parses the given file as a --possibly partial-- configuration in YAML or+    JSON format. The file location can be provided either as a local file+    system path or as a remote HTTP or HTTPS URL. In addition a list of static     configuration file locations can be defined in the code.      If this option is provided more than a single time the configuration@@ -109,7 +109,7 @@     prints a help message and exits.  As long as the package wasn't build with `-f-remote-configs` the following-two options are availabe. They affect how configuration files+two options are available. They affect how configuration files are loaded from remote URLs.  *   `--config-https-insecure=true|false`@@ -158,7 +158,7 @@     } ~~~ -We have to define lenses for the configuration types. Here we do it explicitely.+We have to define lenses for the configuration types. Here we do it explicitly. Alternatively one could have used TemplateHaskell along with `makeLenses` from the module `Control.Lens` from the [lens](http://hackage.haskell.org/package/lens) package.@@ -191,7 +191,7 @@ instance that yields a function that updates a given `Auth` value with the values from the parsed JSON value. The `<*<` operator is functional composition lifted for applicative functors and `%` is a version of `$` with a different-precedence that helps to reduce the use of paranthesis in applicative style+precedence that helps to reduce the use of parenthesis in applicative style code.  ~~~{.haskell}@@ -233,7 +233,7 @@ [optparse-applicative](http://hackage.haskell.org/package/optparse-applicative) package for further information on how to define command line options. -The following definitons for the `HttpURL` are similar to definitions for+The following definitions for the `HttpURL` are similar to definitions for the `Auth` type above. In addition it is demonstrated how to deal with nested configuration types. Mainly the usage of `..:` is replaced by `%.:` and `.::` is replaced by `%::`.@@ -313,10 +313,10 @@  Sum types can not be used as configuration types in the same way as product types. The reason is that the nondeterminism in the choice of a term for the type is-not restricted to the choosen constructor arguments but in addition there+not restricted to the chosen constructor arguments but in addition there is non-determinism in the choice of the constructor, too. -An update function for a product type can be defined pointwise as a mapping from+An update function for a product type can be defined point-wise as a mapping from constructor parameters to values. An update for a sum type must take the constructor context into account. Moreover, when applied to a given default value the function may not be applicable at all if the default value uses a@@ -338,7 +338,7 @@  The module `Configuration.Utils.Setup` an example `Setup.hs` script that hooks into the cabal build process at the end of the configuration phase and generates-a module with package information for each component of the cabal pacakge.+a module with package information for each component of the cabal package.  The modules are created in the *autogen* build directory where also the *Path_* module is created by cabal's simple build setup. This is usually the directory@@ -421,7 +421,7 @@     indirect dependencies along with their licenses and copyrights.  *   `--license`-    prints the text of the lincense of the application and exits.+    prints the text of the license of the application and exits.  Here is the example output of `--long-info` for the example `examples/Trivial.hs` from this package:@@ -518,7 +518,7 @@ *   Simplify specification of Configuration data types by     integrating the aeson instances and the option parser. -*   Come up with a storry for sum types. We may use the following approach: The+*   Come up with a story for sum types. We may use the following approach: The     definition of the default should include alternate values for each     constructor. Effectively, this means to map the sum type onto a product type     by interpreting the summands as factors. For mapping back from the product@@ -537,7 +537,7 @@  *   Nicer error messages if parsing fails. -*   Suport JSON encoded configuration files.+*   Support JSON encoded configuration files.  *   Support mode where JSON/YAML parsing fails when unexpected     properties are encountered.
configuration-tools.cabal view
@@ -3,7 +3,7 @@ -- ------------------------------------------------------ --  Name:                configuration-tools-Version:             0.2.9+Version:             0.2.10 Synopsis:            Tools for specifying and parsing configurations description:     Tools for specifying and parsing configurations@@ -33,7 +33,7 @@ License-file:        LICENSE Author:              Lars Kuhtz <lars@alephcloud.com> Maintainer:          Lars Kuhtz <lars@alephcloud.com>-Copyright:           Copyright (c) 2014 AlephCloud, Inc.+Copyright:           Copyright (c) 2014-2015 AlephCloud, Inc. Category:            Configuration, Console Build-type:          Custom @@ -54,7 +54,7 @@ source-repository this     type: git     location: https://github.com/alephcloud/hs-configuration-tools.git-    tag: 0.2.9+    tag: 0.2.10  flag remote-configs     Description: enable loading of configuration files from HTTP URLs@@ -71,6 +71,7 @@         Configuration.Utils.ConfigFile         Configuration.Utils.Http         Configuration.Utils.Internal+        Configuration.Utils.Internal.ConfigFileReader         Configuration.Utils.Maybe         Configuration.Utils.Monoid         Configuration.Utils.Operators@@ -78,7 +79,7 @@         Configuration.Utils.Validation     if flag(remote-configs)         exposed-modules:-            Configuration.Utils.HttpsCertPolicy+            Configuration.Utils.Internal.HttpsCertPolicy      build-depends:         Cabal >= 1.20,@@ -89,6 +90,7 @@         base-unicode-symbols >= 0.2.2.4,         bytestring >= 0.10.0.2,         case-insensitive >= 1.2,+        deepseq >= 1.3,         directory >= 1.2.1.0,         dlist >= 0.7.1,         errors >= 1.4.3,@@ -110,6 +112,7 @@             enclosed-exceptions >= 1.0,             http-client >= 0.4,             http-client-tls >= 0.2,+            http-types >= 0.8,             monad-control >= 1.0,             tls >= 1.2,             x509 >= 1.5,
src/Configuration/Utils.hs view
@@ -124,8 +124,8 @@ ) where  import Configuration.Utils.CommandLine-import Configuration.Utils.ConfigFile hiding (parseConfigFiles)-import qualified Configuration.Utils.ConfigFile as CF (parseConfigFiles)+import Configuration.Utils.ConfigFile+import qualified Configuration.Utils.Internal.ConfigFileReader as CF import Configuration.Utils.Internal import Configuration.Utils.Maybe import Configuration.Utils.Monoid@@ -375,7 +375,7 @@         × O.long "config-file"         ⊕ O.short 'c'         ⊕ O.metavar "FILE"-        ⊕ O.help "Configuration file in YAML format. If more than a single config file option is present files are loaded in the order in which they appear on the command line."+        ⊕ O.help "Configuration file in YAML or JSON format. If more than a single config file option is present files are loaded in the order in which they appear on the command line."  -- -------------------------------------------------------------------------- -- -- Main Configuration without Package Info@@ -387,7 +387,7 @@ -- options are recognized: -- -- [@--config-file, -c@]---     Parse the given file path as a (partial) configuration in YAML+--     Parse the given file path as a (partial) configuration in YAML or JSON --     format. -- -- [@--print-config, -p@]@@ -471,7 +471,7 @@ -- options are recognized: -- -- [@--config-file, -c@]---     Parse the given file path as a (partial) configuration in YAML+--     Parse the given file path as a (partial) configuration in YAML or JSON --     format. -- -- [@--print-config, -p@]
src/Configuration/Utils/ConfigFile.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE CPP #-}-{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-}@@ -61,17 +60,11 @@ -- * Miscellaneous Utilities , dropAndUncaml , module Data.Aeson---- * Internal Tools for Parsing Configuration Files-, parseConfigFiles ) where  import Configuration.Utils.CommandLine import Configuration.Utils.Internal-import Configuration.Utils.Validation -import Control.Monad.Except hiding (mapM_)- import Data.Aeson import Data.Aeson.Types (Parser) import Data.Char@@ -82,26 +75,12 @@ import Data.String import qualified Data.Text as T import Data.Typeable-import qualified Data.Yaml as Yaml  import Prelude hiding (concatMap, mapM_, any)  #ifdef REMOTE_CONFIGS-import Configuration.Utils.HttpsCertPolicy+import Configuration.Utils.Internal.HttpsCertPolicy import Configuration.Utils.Operators--import Control.Exception.Enclosed-import Control.Monad.Trans.Control--import qualified Data.ByteString.Lazy as LB-import qualified Data.List as L-import qualified Data.Text.IO as T--import qualified Network.HTTP.Client as HTTP--import Prelude.Unicode--import System.IO #endif  -- | A JSON 'Value' parser for a property of a given@@ -288,102 +267,6 @@  pConfigFilesConfig ∷ MParser ConfigFilesConfig pConfigFilesConfig = pure id-#endif---- -------------------------------------------------------------------------- ----- Tools for parsing configuration files--#ifdef REMOTE_CONFIGS-type ConfigFileParser μ =-    ( Functor μ-    , Applicative μ-    , MonadIO μ-    , MonadBaseControl IO μ-    , MonadError T.Text μ-    )-#else-type ConfigFileParser μ =-    ( Functor μ-    , Applicative μ-    , MonadIO μ-    , MonadError T.Text μ-    )-#endif--parseConfigFiles-    ∷ (ConfigFileParser μ, FromJSON (α → α))-    ⇒ ConfigFilesConfig-    → α-        -- ^ default configuration value-    → [ConfigFile]-        -- ^ list of configuration file paths-    → μ α-parseConfigFiles conf = foldM $ \val file →-    readConfigFile conf file <*> pure val--readConfigFile-    ∷ (ConfigFileParser μ, FromJSON (α → α))-    ⇒ ConfigFilesConfig-    → ConfigFile-        -- ^ file path-    → μ (α → α)-readConfigFile _conf file =-#ifdef REMOTE_CONFIGS-    if isRemote file then loadRemote _conf file else loadLocal file-#else-    loadLocal file-#endif--loadLocal-    ∷ (Functor μ, MonadIO μ, MonadError T.Text μ, FromJSON (α → α))-    ⇒ ConfigFile-        -- ^ file path-    → μ (α → α)-loadLocal path = do-    validateFilePath "config-file" (T.unpack file)-    exists ← (True <$ validateFile "config-file" (T.unpack file)) `catchError` \e → case path of-        ConfigFileOptional _ → return False-        ConfigFileRequired _ → throwError $ "failed to read config file: " ⊕ e-    if exists-      then-        liftIO (Yaml.decodeFileEither (T.unpack file)) >>= \case-            Left e → throwError $ "failed to parse configuration file " ⊕ file ⊕ ": " ⊕ sshow e-            Right r → return r-      else-        return id-  where-    file = getConfigFile path--#ifdef REMOTE_CONFIGS-isRemote-    ∷ ConfigFile-    → Bool-isRemote path = L.any (`T.isPrefixOf` getConfigFile path) ["http://", "https://"]--loadRemote-    ∷ (ConfigFileParser μ, FromJSON (α → α))-    ⇒ ConfigFilesConfig-    → ConfigFile-        -- ^ URL-    → μ (α → α)-loadRemote conf path = do-    validateHttpOrHttpsUrl "config-file" (T.unpack url)-    dat ← (Just <$> doHttp) `catchAnyDeep` \e →-        case path of-            ConfigFileOptional _ → do-                liftIO ∘ T.hPutStrLn stderr $ "WARNING: failed to download remote configuration file " ⊕ url ⊕ ": " ⊕ sshow e-                return Nothing-            ConfigFileRequired _ → throwError $ "failed to download remote configuration file " ⊕ url ⊕ ": " ⊕ sshow e--    case dat of-        Nothing → return id-        Just d → case Yaml.decodeEither' d of-            Left e → throwError $ "failed to parse remote configuration " ⊕ url ⊕ ": " ⊕ sshow e-            Right r → return r-  where-    url = getConfigFile path-    policy = _cfcHttpsPolicy conf-    doHttp = LB.toStrict ∘ HTTP.responseBody <$> liftIO × httpWithValidationPolicy url policy #endif  -- -------------------------------------------------------------------------- --
− src/Configuration/Utils/HttpsCertPolicy.hs
@@ -1,308 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE UnicodeSyntax #-}---- |--- Module: Configuration.Utils.HttpsCertPolicy--- Description: HTTPS certificate validation policy--- Copyright: Copyright © 2015 PivotCloud, Inc.--- License: MIT--- Maintainer: Lars Kuhtz <lkuhtz@pivotmail.com>--- Stability: experimental------ This module provides means for defining and using HTTPS--- certificate validation polices for HTTPS requests.----module Configuration.Utils.HttpsCertPolicy-(--- * HTTPS Certificate Validation Policy-  HttpsCertPolicy(..)-, certPolicyInsecure-, certPolicyHostFingerprints-, defaultHttpsCertPolicy-, pHttpsCertPolicy---- * HTTP Requests With Certificate Validation Policy-, httpWithValidationPolicy-, VerboseTlsException(..)-) where--import Configuration.Utils.CommandLine-import Configuration.Utils.Internal-import Configuration.Utils.Monoid-import Configuration.Utils.Operators-import Configuration.Utils.Validation--import Control.Monad.Except hiding (mapM_)-import Control.Monad.Writer hiding (mapM_)--import qualified Data.ByteString.Char8 as B8-import Data.Maybe-import Data.Monoid.Unicode-import Data.String-import qualified Data.Text as T-import Data.Typeable--import qualified Options.Applicative as O--import Prelude hiding (concatMap, mapM_, any)-import Prelude.Unicode--import Control.Arrow (second)-import qualified Data.ByteString.Lazy as LB-import qualified Data.List as L-import qualified Network.Connection as HTTP-import qualified Network.HTTP.Client as HTTP-import qualified Network.HTTP.Client.TLS as HTTP-import qualified System.X509 as TLS (getSystemCertificateStore)-import qualified Data.X509.Validation as TLS (ServiceID, Fingerprint(..), getFingerprint, ValidationCacheQueryCallback)-import qualified Network.TLS as TLS hiding (HashSHA256)-import Data.Default (def)-import qualified Network.TLS.Extra as TLS (ciphersuite_all)-import Text.Read (readEither)-import qualified Data.ByteString.Base64 as B64-import Control.Monad.State hiding (mapM_)-import qualified Data.HashMap.Strict as HM-import Control.Exception (Exception, catch, throwIO, fromException)-import Data.IORef-import qualified Data.Text.Encoding as T-import qualified Data.X509 as TLS (HashALG(HashSHA256), Certificate, SignedExact, CertificateChain(..))---- -------------------------------------------------------------------------- ----- HTTPS Certificate Validation Policy--data HttpsCertPolicy = HttpsCertPolicy-    { _certPolicyInsecure ∷ !Bool-        -- ^ disable certificate validation-    , _certPolicyHostFingerprints ∷ !(HM.HashMap TLS.ServiceID TLS.Fingerprint)-        -- ^ a whitelist for services with trusted certificates-    }-    deriving (Show, Eq, Typeable)--certPolicyInsecure ∷ Lens' HttpsCertPolicy Bool-certPolicyInsecure = lens _certPolicyInsecure $ \s a → s { _certPolicyInsecure = a }--certPolicyHostFingerprints ∷ Lens' HttpsCertPolicy (HM.HashMap TLS.ServiceID TLS.Fingerprint)-certPolicyHostFingerprints = lens _certPolicyHostFingerprints $ \s a → s { _certPolicyHostFingerprints = a }--defaultHttpsCertPolicy ∷ HttpsCertPolicy-defaultHttpsCertPolicy = HttpsCertPolicy-    { _certPolicyInsecure = False-    , _certPolicyHostFingerprints = mempty-    }--pHttpsCertPolicy-    ∷ T.Text-        -- ^ prefix for the command line options-    → MParser HttpsCertPolicy-pHttpsCertPolicy prefix = id-    <$< certPolicyInsecure .:: boolOption_-        × O.long (T.unpack prefix ⊕ "https-insecure")-        ⊕ O.help "Bypass certificate validation for all HTTPS connections to all services. ONLY USE THIS WHEN YOU UNDERSTAND WHAT YOU DO."-    <*< certPolicyHostFingerprints %:: pLeftMonoidalUpdate × pRule-  where-    pRule = O.option (O.eitherReader readFingerprint)-        × O.long (T.unpack prefix ⊕ "https-allow-cert")-        ⊕ O.help "Unconditionally trust the certificate for connecting to the service. ONLY USE THIS WHEN YOU ARE SURE THAT THE CERTIFICATE CAN BE TRUSTED."-        ⊕ O.metavar "HOSTNAME:PORT:FINGERPRINT"-    readFingerprint = evalStateT $ do-        hostname ∷ String ← next-        x $ validateNonEmpty "hostname" hostname-        port ∷ Int ← lift ∘ readEither =<< next-        x $ validatePort "port" port-        fingerprint ← lift ∘ B64.decode ∘ B8.pack =<< next-        x $ validateNonEmpty "fingerprint" fingerprint -- FIXME we should evaluate the length-        return $ HM.singleton (hostname, sshow port) (TLS.Fingerprint fingerprint)--    next = state $ second (drop 1) ∘ break (≡ ':')--    x = lift ∘ fmapL T.unpack---- -------------------------------------------------------------------------- ----- HTTP Requests With Certificate Validation Policy----- | Make an HTTP request with a given certificate validation policy.------ NOTE that the HTTP request is strictly loaded into memory.------ NOTE that this implementation opens a new TCP connection for each single--- request. HTTPS certificates validation results are not cached between different--- requests.----httpWithValidationPolicy-    ∷ T.Text-        -- ^ HTTP or HTTPS URL-    → HttpsCertPolicy-    → IO (HTTP.Response LB.ByteString)-httpWithValidationPolicy url policy = do-    request ← HTTP.parseUrl $ T.unpack url-    certVar ← newIORef Nothing-    settings ← getSettings policy certVar-    HTTP.withManager settings (HTTP.httpLbs request) `catch` \httpEx → case httpEx of-        HTTP.TlsException tlsEx → case fromException tlsEx of-            Nothing → throwIO httpEx-            Just e → do-                cert ← readIORef certVar-                handleTlsException request cert e-        _ → throwIO httpEx---- -------------------------------------------------------------------------- ----- Verbose TLS exceptions---- | The Haskell @tls@ library provides only limited means for providing--- user friendly error messages. In particular we'd like to provide the--- user with fingerprints of the reject certificate for self-signed--- certificates. Also we want to provide the user with some guidance what--- a particular failure may indicate with respect to security of the--- connection.------ Here we employ a /hack/ for better error handling: Based on the assumption--- that we initialize a new connection 'Manager' and also a new certificate--- cache for each request, we write the certificate that is received--- from the server in the TLS handshake to an 'IORef'. If the handshakes--- fails later on because the certificate is rejected we can recover the--- rejected certificate from the 'IORef'.------ What we really want are exceptions that can be consumed programatically.--- In particular exceptions should include rejected certificates.----newtype VerboseTlsException = VerboseTlsException T.Text-    deriving (Eq, Ord, Typeable)--instance Show VerboseTlsException where-    show (VerboseTlsException msg) = "TLS exception: " ⊕ T.unpack msg--instance Exception VerboseTlsException--handleTlsException-    ∷ HTTP.Request-    → Maybe (TLS.SignedExact TLS.Certificate)-    → TLS.TLSException-    → IO a-handleTlsException request cert e@(TLS.HandshakeFailed (TLS.Error_Protocol (msg, _b, _alert)))-    | "certificate rejected: [SelfSigned]" `L.isPrefixOf` msg = throwIO ∘ VerboseTlsException-        $ "The server uses a self-signed certificate. If you are sure that no-one"-        ⊕ " is intercepting the connection and this is the correct certificate you"-        ⊕ " may enable usage of this certificate with the following command line option:"-        ⊕ "\n\n"-        ⊕ "   " ⊕ allowCertOption-        ⊕ "\n"--    | "certificate rejected: [CacheSaysNo" `L.isPrefixOf` msg = throwIO ∘ VerboseTlsException-        $ "There is a mismatch between the expected certificate provided for"-        ⊕ " this service and the certificate provided by the service. You may try to remove"-        ⊕ " the expected certificate fingerprint and check if the certificate that is"-        ⊕ " offered by the service validates cleanly. If that is not the case this could"-        ⊕ " mean that someone is intercepting the connections. In this case YOU SHOULD ONLY"-        ⊕ " PROCEED WHEN YOU ARE SURE THAT IT IS SAFE. If you still want to proceed you may"-        ⊕ " accept the new certificate by using following command line option:"-        ⊕ "\n\n"-        ⊕ "   " ⊕ allowCertOption-        ⊕ "\n\n"-        ⊕ " The error message was: " ⊕ T.pack msg-        ⊕ "\n"--    | "certificate rejected: [NameMismatch" `L.isPrefixOf` msg = throwIO ∘ VerboseTlsException-        $ "There is a mismatch between the certificate name and the server name. This"-        ⊕ " could mean that someone is intercepting the connection or that you are not"-        ⊕ " connected to the correct service. YOU SHOULD ONLY PROCEED WHEN YOU ARE SURE"-        ⊕ " THAT IT IS SAFE TO DO SO. If you still want to proceed you may"-        ⊕ " accept the certificate by using following command line option:"-        ⊕ "\n\n"-        ⊕ "   " ⊕ allowCertOption-        ⊕ "\n\n"-        ⊕ " The error message was: " ⊕ T.pack msg-        ⊕ "\n"--    | "certificate rejected:" `L.isPrefixOf` msg = throwIO ∘ VerboseTlsException-        $ "The certificate that was offered by the service was rejected. This"-        ⊕ " could mean that someone is intercepting the connection or that you are not"-        ⊕ " connected to the correct service. YOU SHOULD ONLY PROCEED WHEN YOU ARE SURE"-        ⊕ " THAT IT IS SAFE TO DO SO. If you still want to proceed you may"-        ⊕ " accept the certificate by using following command line option:"-        ⊕ "\n\n"-        ⊕ "   " ⊕ allowCertOption-        ⊕ "\n\n"-        ⊕ " The error message was: " ⊕ T.pack msg-        ⊕ "\n"-    | otherwise = throwIO e-  where-    printFingerprint (TLS.Fingerprint f) = fromString ∘ B8.unpack ∘ B64.encode $ f-    printCertF c = printFingerprint (TLS.getFingerprint c fingerprintAlg)-    fingerprintAlg = TLS.HashSHA256-    hostText = T.decodeUtf8 $ HTTP.host request-    portText = sshow $ HTTP.port request--    allowCertOption = case cert of-        Nothing → "--insecure-remote-config-files"-        (Just c) →-            "--remote-config-fingerprint=" ⊕ hostText ⊕ ":" ⊕ portText ⊕ ":" ⊕ printCertF c--handleTlsException _ _  e = throwIO e---- -------------------------------------------------------------------------- ----- TLS Settings---- | The usage of the 'certVar' parameter is not thread-safe!------ FIXME We could make this thread-safe by using a cache for--- "unvalidated" certificates.----getSettings-    ∷ HttpsCertPolicy-    → IORef (Maybe (TLS.SignedExact TLS.Certificate))-    → IO HTTP.ManagerSettings-getSettings policy certVar = do-    certstore ← TLS.getSystemCertificateStore-    return $ HTTP.mkManagerSettings-        (HTTP.TLSSettings (tlsSettings certstore))-        Nothing-  where-    -- It is safe to pass empty strings for host and port since 'TLS.connectFromHandle'-    -- and 'TLS.connectTo' are going to overwrite this anyways.-    ---    tlsSettings certstore = (TLS.defaultParamsClient "" "")-        { TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_all }-        , TLS.clientShared = def-            { TLS.sharedCAStore = certstore-            , TLS.sharedValidationCache = validationCache-            }-        , TLS.clientHooks = def-            { TLS.onServerCertificate = \store cache serviceId certChain@(TLS.CertificateChain certs) → do-                modifyIORef' certVar (const $ listToMaybe certs)-                TLS.onServerCertificate def store cache serviceId certChain-            }-        }--    validationCache-        | _certPolicyInsecure policy = TLS.ValidationCache-            (\_ _ _ → return TLS.ValidationCachePass)-            (\_ _ _ → return ())-        | otherwise = certCache (_certPolicyHostFingerprints policy)--    -- 'TLS.exceptionValidationCache' would have worked to here, but it's hard to get-    -- the certificate fingerprint of the failing certificate from the exceptions it-    -- generates. Unfortunately, the TLS package allows us to pass only a string message,-    -- so that we have to encode and to decode the fingerprint.-    ---    certCache ∷ HM.HashMap TLS.ServiceID TLS.Fingerprint → TLS.ValidationCache-    certCache fingerprints = TLS.ValidationCache-        (queryCallback fingerprints)-        (\_ _ _ → return ())--    queryCallback ∷ HM.HashMap TLS.ServiceID TLS.Fingerprint → TLS.ValidationCacheQueryCallback-    queryCallback cache serviceID fingerprint _ = return $-        case HM.lookup serviceID cache of-            Nothing → TLS.ValidationCacheUnknown-            Just f-                | fingerprint ≡ f → TLS.ValidationCachePass-                | otherwise → TLS.ValidationCacheDenied-                    $ "for host: " ⊕ fst serviceID ⊕ ":" ⊕ B8.unpack (snd serviceID)-                    ⊕ " expected fingerprint: " ⊕ printFingerprint f-                    ⊕ " but got fingerprint: " ⊕ printFingerprint fingerprint-      where-        printFingerprint (TLS.Fingerprint f) = fromString ∘ B8.unpack ∘ B64.encode $ f-
src/Configuration/Utils/Internal.hs view
@@ -27,6 +27,7 @@  -- * Misc Utils , (&)+, (<&>) , sshow , exceptT , errorT@@ -47,7 +48,7 @@  import Prelude.Unicode -infixl 1 &+infixl 1 &, <&>  -- -------------------------------------------------------------------------- -- -- Lenses@@ -104,6 +105,10 @@ (&) ∷ α → (α → β) → β (&) = flip ($) {-# INLINE (&) #-}++(<&>) ∷ Functor φ ⇒ φ α → (α → β) → φ β+(<&>) = flip fmap+{-# INLINE (<&>) #-}  sshow     ∷ (Show α, IsString τ)
+ src/Configuration/Utils/Internal/ConfigFileReader.hs view
@@ -0,0 +1,221 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnicodeSyntax #-}++-- |+-- Module: Configuration.Utils.Internal.ConfigFileReader+-- Description: Internal Tools for Parsing Configuration Files+-- Copyright: Copyright © 2015 PivotCloud, Inc.+-- License: MIT+-- Maintainer: Lars Kuhtz <lkuhtz@pivotmail.com>+-- Stability: experimental+--+module Configuration.Utils.Internal.ConfigFileReader+(+  parseConfigFiles+, readConfigFile+, ConfigFileFormat(..)++-- * Local Config Files+, loadLocal++#ifdef REMOTE_CONFIGS+-- * Remote Config Files+, isRemote+, loadRemote+, yamlMimeType+, jsonMimeType+, contentType+, requestHeaders+#endif+) where++import Configuration.Utils.ConfigFile+import Configuration.Utils.Internal+import Configuration.Utils.Validation++import Control.Applicative+import Control.DeepSeq (NFData)+import Control.Monad.Except hiding (mapM_)++import qualified Data.ByteString.Char8 as B8+import qualified Data.CaseInsensitive as CI+import Data.Monoid.Unicode+import qualified Data.Text as T+import Data.Typeable+import qualified Data.Yaml as Yaml++import GHC.Generics++import Prelude hiding (concatMap, mapM_, any)+import Prelude.Unicode++#ifdef REMOTE_CONFIGS+import Configuration.Utils.Internal.HttpsCertPolicy++import Control.Exception.Enclosed+import Control.Monad.Trans.Control++import qualified Data.ByteString.Lazy as LB+import qualified Data.List as L+import Data.String+import qualified Data.Text.IO as T++import qualified Network.HTTP.Client as HTTP+import qualified Network.HTTP.Types.Header as HTTP++import System.IO+#endif++-- -------------------------------------------------------------------------- --+-- Tools for parsing configuration files++#ifdef REMOTE_CONFIGS+type ConfigFileParser μ =+    ( Functor μ+    , Applicative μ+    , MonadIO μ+    , MonadBaseControl IO μ+    , MonadError T.Text μ+    )+#else+type ConfigFileParser μ =+    ( Functor μ+    , Applicative μ+    , MonadIO μ+    , MonadError T.Text μ+    )+#endif++parseConfigFiles+    ∷ (ConfigFileParser μ, FromJSON (α → α))+    ⇒ ConfigFilesConfig+    → α+        -- ^ default configuration value+    → [ConfigFile]+        -- ^ list of configuration file paths+    → μ α+parseConfigFiles conf = foldM $ \val file →+    readConfigFile conf file <*> pure val++readConfigFile+    ∷ (ConfigFileParser μ, FromJSON (α → α))+    ⇒ ConfigFilesConfig+    → ConfigFile+        -- ^ file path+    → μ (α → α)+readConfigFile _conf file =+#ifdef REMOTE_CONFIGS+    if isRemote file then loadRemote _conf file else loadLocal file+#else+    loadLocal file+#endif++fileType ∷ T.Text → ConfigFileFormat+fileType f+    | CI.foldCase ".yaml" `T.isSuffixOf` CI.foldCase f = Yaml+    | CI.foldCase ".yml" `T.isSuffixOf` CI.foldCase f = Yaml+    | CI.foldCase ".json" `T.isSuffixOf` CI.foldCase f = Json+    | CI.foldCase ".js" `T.isSuffixOf` CI.foldCase f = Json+    | otherwise = Other++loadLocal+    ∷ (Functor μ, MonadIO μ, MonadError T.Text μ, FromJSON (α → α))+    ⇒ ConfigFile+        -- ^ file path+    → μ (α → α)+loadLocal path = do+    validateFilePath "config-file" (T.unpack file)+    exists ← (True <$ validateFile "config-file" (T.unpack file)) `catchError` \e → case path of+        ConfigFileOptional _ → return False+        ConfigFileRequired _ → throwError $ "failed to read config file: " ⊕ e+    if exists+      then+        liftIO (parser (fileType file) file) >>= \case+            Left e → throwError $ "failed to parse configuration file " ⊕ file ⊕ ": " ⊕ sshow e+            Right r → return r+      else+        return id+  where+    file = getConfigFile path++    parser Json f = fmapL T.pack ∘ eitherDecodeStrict' <$> B8.readFile (T.unpack f)+    parser _ f = fmapL sshow <$> Yaml.decodeFileEither (T.unpack f)++data ConfigFileFormat+    = Yaml+    | Json+    | Other+    deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable, Generic)++instance NFData ConfigFileFormat++#ifdef REMOTE_CONFIGS+isRemote+    ∷ ConfigFile+    → Bool+isRemote path = L.any (`T.isPrefixOf` getConfigFile path) ["http://", "https://"]++yamlMimeType ∷ IsString s ⇒ [s]+yamlMimeType = map fromString ["application/x-yaml", "text/yaml"]++-- | Defined in  RFC 4627+--+jsonMimeType ∷ IsString s ⇒ [s]+jsonMimeType = map fromString ["application/json"]++contentType+    ∷ B8.ByteString+        -- ^ value of an HTTP @Content-Type@ header+    → ConfigFileFormat+contentType headerValue+    | CI.foldCase "yaml" `B8.isInfixOf` CI.foldCase headerValue = Yaml+    | CI.foldCase "json" `B8.isInfixOf` CI.foldCase headerValue = Json+    | otherwise = Other++loadRemote+    ∷ (ConfigFileParser μ, FromJSON (α → α))+    ⇒ ConfigFilesConfig+    → ConfigFile+        -- ^ URL+    → μ (α → α)+loadRemote conf path = do+    validateHttpOrHttpsUrl "config-file" (T.unpack url)+    result ← (Just <$> doHttp) `catchAnyDeep` \e →+        case path of+            ConfigFileOptional _ → do+                liftIO ∘ T.hPutStrLn stderr $ "WARNING: failed to download remote configuration file " ⊕ url ⊕ ": " ⊕ sshow e+                return Nothing+            ConfigFileRequired _ → throwError $ "failed to download remote configuration file " ⊕ url ⊕ ": " ⊕ sshow e++    case result of+        Nothing → return id+        Just (format, d) → case (parser format) d of+            Left e → throwError $ "failed to parse remote configuration " ⊕ url ⊕ ": " ⊕ e+            Right r → return r+  where+    parser Json = fmapL T.pack ∘ eitherDecodeStrict'+    parser _ = fmapL sshow ∘ Yaml.decodeEither'++    url = getConfigFile path+    policy = _cfcHttpsPolicy conf+    doHttp = liftIO $ do+        request ← (HTTP.parseUrl $ T.unpack url)+            <&> over requestHeaders ((:) acceptHeader)+        resp ← httpWithValidationPolicy request policy+        let format = maybe Other contentType ∘ L.lookup HTTP.hContentType $ HTTP.responseHeaders resp+        return (format, LB.toStrict (HTTP.responseBody resp))++    acceptHeader = (HTTP.hAccept, B8.intercalate "," (yamlMimeType ⊕ jsonMimeType))++requestHeaders ∷ Lens' HTTP.Request HTTP.RequestHeaders+requestHeaders = lens HTTP.requestHeaders $ \s a → s { HTTP.requestHeaders = a }++#endif+
+ src/Configuration/Utils/Internal/HttpsCertPolicy.hs view
@@ -0,0 +1,316 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnicodeSyntax #-}++-- |+-- Module: Configuration.Utils.Internal.HttpsCertPolicy+-- Description: HTTPS certificate validation policy+-- Copyright: Copyright © 2015 PivotCloud, Inc.+-- License: MIT+-- Maintainer: Lars Kuhtz <lkuhtz@pivotmail.com>+-- Stability: experimental+--+-- This module provides means for defining and using HTTPS+-- certificate validation polices for HTTPS requests.+--+module Configuration.Utils.Internal.HttpsCertPolicy+(+-- * HTTPS Certificate Validation Policy+  HttpsCertPolicy(..)+, certPolicyInsecure+, certPolicyHostFingerprints+, defaultHttpsCertPolicy+, pHttpsCertPolicy++-- * HTTP Requests With Certificate Validation Policy+, simpleHttpWithValidationPolicy+, httpWithValidationPolicy+, VerboseTlsException(..)+) where++import Configuration.Utils.CommandLine+import Configuration.Utils.Internal+import Configuration.Utils.Monoid+import Configuration.Utils.Operators+import Configuration.Utils.Validation++import Control.Monad.Except hiding (mapM_)+import Control.Monad.Writer hiding (mapM_)++import qualified Data.ByteString.Char8 as B8+import Data.Maybe+import Data.Monoid.Unicode+import Data.String+import qualified Data.Text as T+import Data.Typeable++import qualified Options.Applicative as O++import Prelude hiding (concatMap, mapM_, any)+import Prelude.Unicode++import Control.Arrow (second)+import qualified Data.ByteString.Lazy as LB+import qualified Data.List as L+import qualified Network.Connection as HTTP+import qualified Network.HTTP.Client as HTTP+import qualified Network.HTTP.Client.TLS as HTTP+import qualified System.X509 as TLS (getSystemCertificateStore)+import qualified Data.X509.Validation as TLS (ServiceID, Fingerprint(..), getFingerprint, ValidationCacheQueryCallback)+import qualified Network.TLS as TLS hiding (HashSHA256)+import Data.Default (def)+import qualified Network.TLS.Extra as TLS (ciphersuite_all)+import Text.Read (readEither)+import qualified Data.ByteString.Base64 as B64+import Control.Monad.State hiding (mapM_)+import qualified Data.HashMap.Strict as HM+import Control.Exception (Exception, catch, throwIO, fromException)+import Data.IORef+import qualified Data.Text.Encoding as T+import qualified Data.X509 as TLS (HashALG(HashSHA256), Certificate, SignedExact, CertificateChain(..))++-- -------------------------------------------------------------------------- --+-- HTTPS Certificate Validation Policy++data HttpsCertPolicy = HttpsCertPolicy+    { _certPolicyInsecure ∷ !Bool+        -- ^ disable certificate validation+    , _certPolicyHostFingerprints ∷ !(HM.HashMap TLS.ServiceID TLS.Fingerprint)+        -- ^ a whitelist for services with trusted certificates+    }+    deriving (Show, Eq, Typeable)++certPolicyInsecure ∷ Lens' HttpsCertPolicy Bool+certPolicyInsecure = lens _certPolicyInsecure $ \s a → s { _certPolicyInsecure = a }++certPolicyHostFingerprints ∷ Lens' HttpsCertPolicy (HM.HashMap TLS.ServiceID TLS.Fingerprint)+certPolicyHostFingerprints = lens _certPolicyHostFingerprints $ \s a → s { _certPolicyHostFingerprints = a }++defaultHttpsCertPolicy ∷ HttpsCertPolicy+defaultHttpsCertPolicy = HttpsCertPolicy+    { _certPolicyInsecure = False+    , _certPolicyHostFingerprints = mempty+    }++pHttpsCertPolicy+    ∷ T.Text+        -- ^ prefix for the command line options+    → MParser HttpsCertPolicy+pHttpsCertPolicy prefix = id+    <$< certPolicyInsecure .:: boolOption_+        × O.long (T.unpack prefix ⊕ "https-insecure")+        ⊕ O.help "Bypass certificate validation for all HTTPS connections to all services. ONLY USE THIS WHEN YOU UNDERSTAND WHAT YOU DO."+    <*< certPolicyHostFingerprints %:: pLeftMonoidalUpdate × pRule+  where+    pRule = O.option (O.eitherReader readFingerprint)+        × O.long (T.unpack prefix ⊕ "https-allow-cert")+        ⊕ O.help "Unconditionally trust the certificate for connecting to the service. ONLY USE THIS WHEN YOU ARE SURE THAT THE CERTIFICATE CAN BE TRUSTED."+        ⊕ O.metavar "HOSTNAME:PORT:FINGERPRINT"+    readFingerprint = evalStateT $ do+        hostname ∷ String ← next+        x $ validateNonEmpty "hostname" hostname+        port ∷ Int ← lift ∘ readEither =<< next+        x $ validatePort "port" port+        fingerprint ← lift ∘ B64.decode ∘ B8.pack =<< next+        x $ validateNonEmpty "fingerprint" fingerprint -- FIXME we should evaluate the length+        return $ HM.singleton (hostname, sshow port) (TLS.Fingerprint fingerprint)++    next = state $ second (drop 1) ∘ break (≡ ':')++    x = lift ∘ fmapL T.unpack++-- -------------------------------------------------------------------------- --+-- HTTP Requests With Certificate Validation Policy+++-- | Make an HTTP request with a given certificate validation policy.+--+-- NOTE that the HTTP request is strictly loaded into memory.+--+-- NOTE that this implementation opens a new TCP connection for each single+-- request. HTTPS certificates validation results are not cached between different+-- requests.+--+simpleHttpWithValidationPolicy+    ∷ T.Text+        -- ^ HTTP or HTTPS URL+    → HttpsCertPolicy+    → IO (HTTP.Response LB.ByteString)+simpleHttpWithValidationPolicy url policy = do+    request ← (HTTP.parseUrl $ T.unpack url)+    httpWithValidationPolicy request policy++httpWithValidationPolicy+    ∷ HTTP.Request+    → HttpsCertPolicy+    → IO (HTTP.Response LB.ByteString)+httpWithValidationPolicy request policy = do+    certVar ← newIORef Nothing+    settings ← getSettings policy certVar+    HTTP.withManager settings (HTTP.httpLbs request) `catch` \httpEx → case httpEx of+        HTTP.TlsException tlsEx → case fromException tlsEx of+            Nothing → throwIO httpEx+            Just e → do+                cert ← readIORef certVar+                handleTlsException request cert e+        _ → throwIO httpEx++-- -------------------------------------------------------------------------- --+-- Verbose TLS exceptions++-- | The Haskell @tls@ library provides only limited means for providing+-- user friendly error messages. In particular we'd like to provide the+-- user with fingerprints of the reject certificate for self-signed+-- certificates. Also we want to provide the user with some guidance what+-- a particular failure may indicate with respect to security of the+-- connection.+--+-- Here we employ a /hack/ for better error handling: Based on the assumption+-- that we initialize a new connection 'Manager' and also a new certificate+-- cache for each request, we write the certificate that is received+-- from the server in the TLS handshake to an 'IORef'. If the handshakes+-- fails later on because the certificate is rejected we can recover the+-- rejected certificate from the 'IORef'.+--+-- What we really want are exceptions that can be consumed programatically.+-- In particular exceptions should include rejected certificates.+--+newtype VerboseTlsException = VerboseTlsException T.Text+    deriving (Eq, Ord, Typeable)++instance Show VerboseTlsException where+    show (VerboseTlsException msg) = "TLS exception: " ⊕ T.unpack msg++instance Exception VerboseTlsException++handleTlsException+    ∷ HTTP.Request+    → Maybe (TLS.SignedExact TLS.Certificate)+    → TLS.TLSException+    → IO a+handleTlsException request cert e@(TLS.HandshakeFailed (TLS.Error_Protocol (msg, _b, _alert)))+    | "certificate rejected: [SelfSigned]" `L.isPrefixOf` msg = throwIO ∘ VerboseTlsException+        $ "The server uses a self-signed certificate. If you are sure that no-one"+        ⊕ " is intercepting the connection and this is the correct certificate you"+        ⊕ " may enable usage of this certificate with the following command line option:"+        ⊕ "\n\n"+        ⊕ "   " ⊕ allowCertOption+        ⊕ "\n"++    | "certificate rejected: [CacheSaysNo" `L.isPrefixOf` msg = throwIO ∘ VerboseTlsException+        $ "There is a mismatch between the expected certificate provided for"+        ⊕ " this service and the certificate provided by the service. You may try to remove"+        ⊕ " the expected certificate fingerprint and check if the certificate that is"+        ⊕ " offered by the service validates cleanly. If that is not the case this could"+        ⊕ " mean that someone is intercepting the connections. In this case YOU SHOULD ONLY"+        ⊕ " PROCEED WHEN YOU ARE SURE THAT IT IS SAFE. If you still want to proceed you may"+        ⊕ " accept the new certificate by using following command line option:"+        ⊕ "\n\n"+        ⊕ "   " ⊕ allowCertOption+        ⊕ "\n\n"+        ⊕ " The error message was: " ⊕ T.pack msg+        ⊕ "\n"++    | "certificate rejected: [NameMismatch" `L.isPrefixOf` msg = throwIO ∘ VerboseTlsException+        $ "There is a mismatch between the certificate name and the server name. This"+        ⊕ " could mean that someone is intercepting the connection or that you are not"+        ⊕ " connected to the correct service. YOU SHOULD ONLY PROCEED WHEN YOU ARE SURE"+        ⊕ " THAT IT IS SAFE TO DO SO. If you still want to proceed you may"+        ⊕ " accept the certificate by using following command line option:"+        ⊕ "\n\n"+        ⊕ "   " ⊕ allowCertOption+        ⊕ "\n\n"+        ⊕ " The error message was: " ⊕ T.pack msg+        ⊕ "\n"++    | "certificate rejected:" `L.isPrefixOf` msg = throwIO ∘ VerboseTlsException+        $ "The certificate that was offered by the service was rejected. This"+        ⊕ " could mean that someone is intercepting the connection or that you are not"+        ⊕ " connected to the correct service. YOU SHOULD ONLY PROCEED WHEN YOU ARE SURE"+        ⊕ " THAT IT IS SAFE TO DO SO. If you still want to proceed you may"+        ⊕ " accept the certificate by using following command line option:"+        ⊕ "\n\n"+        ⊕ "   " ⊕ allowCertOption+        ⊕ "\n\n"+        ⊕ " The error message was: " ⊕ T.pack msg+        ⊕ "\n"+    | otherwise = throwIO e+  where+    printFingerprint (TLS.Fingerprint f) = fromString ∘ B8.unpack ∘ B64.encode $ f+    printCertF c = printFingerprint (TLS.getFingerprint c fingerprintAlg)+    fingerprintAlg = TLS.HashSHA256+    hostText = T.decodeUtf8 $ HTTP.host request+    portText = sshow $ HTTP.port request++    allowCertOption = case cert of+        Nothing → "--insecure-remote-config-files"+        (Just c) →+            "--remote-config-fingerprint=" ⊕ hostText ⊕ ":" ⊕ portText ⊕ ":" ⊕ printCertF c++handleTlsException _ _  e = throwIO e++-- -------------------------------------------------------------------------- --+-- TLS Settings++-- | The usage of the 'certVar' parameter is not thread-safe!+--+-- FIXME We could make this thread-safe by using a cache for+-- "unvalidated" certificates.+--+getSettings+    ∷ HttpsCertPolicy+    → IORef (Maybe (TLS.SignedExact TLS.Certificate))+    → IO HTTP.ManagerSettings+getSettings policy certVar = do+    certstore ← TLS.getSystemCertificateStore+    return $ HTTP.mkManagerSettings+        (HTTP.TLSSettings (tlsSettings certstore))+        Nothing+  where+    -- It is safe to pass empty strings for host and port since 'TLS.connectFromHandle'+    -- and 'TLS.connectTo' are going to overwrite this anyways.+    --+    tlsSettings certstore = (TLS.defaultParamsClient "" "")+        { TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_all }+        , TLS.clientShared = def+            { TLS.sharedCAStore = certstore+            , TLS.sharedValidationCache = validationCache+            }+        , TLS.clientHooks = def+            { TLS.onServerCertificate = \store cache serviceId certChain@(TLS.CertificateChain certs) → do+                modifyIORef' certVar (const $ listToMaybe certs)+                TLS.onServerCertificate def store cache serviceId certChain+            }+        }++    validationCache+        | _certPolicyInsecure policy = TLS.ValidationCache+            (\_ _ _ → return TLS.ValidationCachePass)+            (\_ _ _ → return ())+        | otherwise = certCache (_certPolicyHostFingerprints policy)++    -- 'TLS.exceptionValidationCache' would have worked to here, but it's hard to get+    -- the certificate fingerprint of the failing certificate from the exceptions it+    -- generates. Unfortunately, the TLS package allows us to pass only a string message,+    -- so that we have to encode and to decode the fingerprint.+    --+    certCache ∷ HM.HashMap TLS.ServiceID TLS.Fingerprint → TLS.ValidationCache+    certCache fingerprints = TLS.ValidationCache+        (queryCallback fingerprints)+        (\_ _ _ → return ())++    queryCallback ∷ HM.HashMap TLS.ServiceID TLS.Fingerprint → TLS.ValidationCacheQueryCallback+    queryCallback cache serviceID fingerprint _ = return $+        case HM.lookup serviceID cache of+            Nothing → TLS.ValidationCacheUnknown+            Just f+                | fingerprint ≡ f → TLS.ValidationCachePass+                | otherwise → TLS.ValidationCacheDenied+                    $ "for host: " ⊕ fst serviceID ⊕ ":" ⊕ B8.unpack (snd serviceID)+                    ⊕ " expected fingerprint: " ⊕ printFingerprint f+                    ⊕ " but got fingerprint: " ⊕ printFingerprint fingerprint+      where+        printFingerprint (TLS.Fingerprint f) = fromString ∘ B8.unpack ∘ B64.encode $ f+
test/TestExample.hs view
@@ -25,6 +25,7 @@  import Configuration.Utils import Configuration.Utils.Internal+import Configuration.Utils.Internal.ConfigFileReader  import Control.Monad @@ -43,37 +44,74 @@ -- main  main ∷ IO ()-main =-    withConfigFile config0 $ \tmpPath0 →-    withConfigFileText config1Part$ \tmpPath1 → do+main = do -#ifdef REMOTE_CONFIGS-    withConfigFileServer [("config0", ConfigType config0)] [("config1", config1Part), ("invalid", "invalid: invalid")] $ do-#endif-        (successes, failures) ← L.partition id <$> sequence-            × tests0-            ⊕ testsConfigFile [tmpPath0, tmpPath1]-            ⊕ tests2Files1 [tmpPath0, tmpPath1]-            ⊕ tests2Files2 "local-" (tmpPath0) (tmpPath1)-            ⊕ tests2Files3 "local-" (tmpPath0) (tmpPath1)+    -- run tests+    localResults ← sequence+        × tests0+        ⊕ monoidUpdateTests pkgInfo+        ⊕ boolOptionTests pkgInfo+    localFileResults ← localFileTests+    remoteResults ← remoteTests+    helpResults ← helpTests++    -- report results+    let (successes, failures) = L.partition id+            × localResults+            ⊕ remoteResults+            ⊕ localFileResults+            ⊕ helpResults++    T.putStrLn $ "success: " ⊕ sshow (length successes)+    T.putStrLn $ "failures: " ⊕ sshow (length failures)+    unless (length failures ≡ 0) $ do+        error "test suite failed"++-- -------------------------------------------------------------------------- --+-- Test categories++helpTests ∷ IO [Bool]+helpTests =+    withConfigFile Yaml config0 $ \tmpPath0 →+    withConfigFile Json config1Part $ \tmpPath1 → sequence+        × testPrintHelp [tmpPath0, tmpPath1]++localFileTests ∷ IO [Bool]+localFileTests = concat <$> mapM run+    [ (Yaml, Yaml, "yaml-yaml-")+    , (Json, Json, "json-json-")+    , (Yaml, Json, "yaml-json-")+    , (Json, Yaml, "json-yaml-")+    ]+  where+    run (format1, format2, label) =+        withConfigFile format1 config0 $ \tmpPath0 →+        withConfigFile format2 config1Part $ \tmpPath1 → sequence+            × testsConfigFile ("configFile-" ⊕ label) [tmpPath0, tmpPath1]+            ⊕ tests2Files1 ("local-" ⊕ label) [tmpPath0, tmpPath1]+            ⊕ tests2Files2 ("local-" ⊕ label) (tmpPath0) (tmpPath1)+            ⊕ tests2Files3 ("local-" ⊕ label) (tmpPath0) (tmpPath1)++remoteTests ∷ IO [Bool] #ifdef REMOTE_CONFIGS-            ⊕ tests2Files2 "remote-" (serverUrl ⊕ "/config0") (serverUrl ⊕ "/config1")-            ⊕ tests2Files3 "remote-" (serverUrl ⊕ "/config0") (serverUrl ⊕ "/config1")+remoteTests = concat <$> mapM run+    [ (Just Yaml, "yaml")+    , (Just Json, "json")+    ]+  where+    typedConfigs = [("config0", ConfigType config0), ("config1", ConfigType config1Part)]+    textConfigs = [("invalid", "invalid: invalid")]++    run (format, label) = withConfigFileServer typedConfigs textConfigs format $+        sequence+            × tests2Files2 ("remote-" ⊕ label) (serverUrl ⊕ "/config0") (serverUrl ⊕ "/config1")+            ⊕ tests2Files3 ("remote-" ⊕ label) (serverUrl ⊕ "/config0") (serverUrl ⊕ "/config1")             ⊕ testsInvalidUrl             ⊕ testsTlsUrl+#else+remoteTests = return [] #endif-            ⊕ monoidUpdateTests pkgInfo-            ⊕ boolOptionTests pkgInfo-            ⊕ testPrintHelp [tmpPath0, tmpPath1] -        T.putStrLn $ "success: " ⊕ sshow (length successes)-        T.putStrLn $ "failures: " ⊕ sshow (length failures)-        unless (length failures ≡ 0) $ do-            debug $ do-                T.readFile (T.unpack tmpPath0) >>= T.putStrLn-                T.readFile (T.unpack tmpPath1) >>= T.putStrLn-            error "test suite failed"- -- -------------------------------------------------------------------------- -- -- Test Cases @@ -87,10 +125,10 @@   where     configFiles = zipWith ($) (ConfigFileRequired : repeat ConfigFileOptional) files -testsConfigFile ∷ [T.Text] → [IO Bool]-testsConfigFile files =-    [ runTest pkgInfo (mainInfoConfigFile configFiles0) "config-file-1" True [trueAssertion []]-    , runTest pkgInfo (mainInfoConfigFile configFiles1) "config-file-2" False [trueAssertion []]+testsConfigFile ∷ T.Text → [T.Text] → [IO Bool]+testsConfigFile prefix files =+    [ runTest pkgInfo (mainInfoConfigFile configFiles0) (prefix ⊕ "-1") True [trueAssertion []]+    , runTest pkgInfo (mainInfoConfigFile configFiles1) (prefix ⊕ "-2") False [trueAssertion []]     ]   where     configFiles0 = zipWith ($) (ConfigFileRequired : repeat ConfigFileOptional) (files ⊕ ["./invalid"])@@ -133,10 +171,10 @@  -- | Tests with two configuration files ---tests2Files1 ∷ [T.Text] → [IO Bool]-tests2Files1 files =-    twoFileCasesC0C1 "2files-1-" files (trueAssertion [])-    ⊕ twoFileCasesC1C0 "2files-1-" selif (trueAssertion [])+tests2Files1 ∷ T.Text → [T.Text] → [IO Bool]+tests2Files1 prefix files =+    twoFileCasesC0C1 (prefix ⊕ "2files-1-") files (trueAssertion [])+    ⊕ twoFileCasesC1C0 (prefix ⊕ "2files-1-") selif (trueAssertion [])   where     selif = reverse files @@ -144,7 +182,7 @@ -- tests2Files2     ∷ T.Text-        -- ^ test label prefix+        -- ^ test label suffix     → T.Text         -- ^ file for config0     → T.Text@@ -161,7 +199,7 @@ -- tests2Files3     ∷ T.Text-        -- ^ test label prefix+        -- ^ test label suffix     → T.Text         -- ^ file for config0     → T.Text@@ -281,11 +319,12 @@  -- | A partial version of configuration 1 ---config1Part ∷ T.Text-config1Part = T.unlines-    [ "domain: " ⊕ T.pack (view domain config1)-    , "auth:"-    , "    user: " ⊕ T.pack (view (auth ∘ user) config1)+config1Part ∷ Value+config1Part = object+    [ "domain" .= view domain config1+    , "auth" .= object+        [ "user" .= view (auth ∘ user) config1+        ]     ]  mainInfo ∷ ProgramInfoValidate HttpURL []
test/TestTools.hs view
@@ -46,11 +46,13 @@  import Configuration.Utils import Configuration.Utils.Internal+import Configuration.Utils.Internal.ConfigFileReader  import Control.Exception import Control.Monad  import qualified Data.ByteString.Char8 as B8+import qualified Data.ByteString.Lazy as LB import Data.IORef import Data.Monoid.Unicode import qualified Data.Text as T@@ -66,7 +68,7 @@  #ifdef REMOTE_CONFIGS import Control.Concurrent-import qualified Data.ByteString.Lazy as LB+import qualified Data.List as L import Data.Maybe import qualified Data.Text.Encoding as T import qualified Network.Wai as WAI@@ -178,21 +180,27 @@  withConfigFile     ∷ ToJSON γ-    ⇒ γ+    ⇒ ConfigFileFormat+    → γ     → (T.Text → IO α)     → IO α-withConfigFile config inner =-    withTempFile "." "tmp_TestExample.yml" $ \tmpPath tmpHandle → do-        B8.hPutStrLn tmpHandle ∘ Yaml.encode $ config+withConfigFile format config inner =+    withTempFile "." ("tmp_TestExample." ⊕ suffix format) $ \tmpPath tmpHandle → do+        B8.hPutStrLn tmpHandle ∘ formatter format $ config         hClose tmpHandle         inner $ T.pack tmpPath+  where+    suffix Json = "json"+    suffix _ = "yaml"+    formatter Json = LB.toStrict ∘ encode+    formatter _ = Yaml.encode  withConfigFileText     ∷ T.Text     → (T.Text → IO α)     → IO α withConfigFileText configText inner =-    withTempFile "." "tmp_TestExample.yml" $ \tmpPath tmpHandle → do+    withTempFile "." "tmp_TestExample.txt" $ \tmpPath tmpHandle → do         T.hPutStrLn tmpHandle configText         hClose tmpHandle         inner $ T.pack tmpPath@@ -207,24 +215,38 @@ withConfigFileServer     ∷ [(T.Text, ConfigType)]     → [(T.Text, T.Text)]+    → Maybe ConfigFileFormat     → IO α     → IO α-withConfigFileServer configs configTexts inner = do-    void ∘ forkIO $ WARP.run serverPort app-    void ∘ forkIO $ WARP.runTLS tlsSettings (warpSettings serverTlsPort) app-    inner+withConfigFileServer configs configTexts maybeFormat inner = do+    w0 ← forkIO $ WARP.run serverPort app+    w1 ←  forkIO $ WARP.runTLS tlsSettings (warpSettings serverTlsPort) app+    inner `finally` do+        killThread w0+        killThread w1+   where     app req respond = do-        let maybeBody = LB.fromStrict <$> do++        let format = fromMaybe Other $ maybeFormat+                <|> (contentType <$> L.lookup HTTP.hAccept × WAI.requestHeaders req)++            maybeBody = LB.fromStrict <$> do                 p ← listToMaybe $ WAI.pathInfo req                 do-                    Yaml.encode <$> lookup p configs+                    formatter format <$> lookup p configs                     <|>                     (T.encodeUtf8 <$> lookup p configTexts)          respond $ case maybeBody of             Just body → WAI.responseLBS HTTP.status200 [] body-            Nothing → WAI.responseLBS HTTP.status404 [] "resource not found"+            Nothing → WAI.responseLBS HTTP.status404 [contentTypeHeader format] "resource not found"++    formatter Json = LB.toStrict ∘ encode+    formatter _ = Yaml.encode++    contentTypeHeader Json = (HTTP.hContentType, head jsonMimeType)+    contentTypeHeader _ = (HTTP.hContentType, head yamlMimeType)  serverPort ∷ Int serverPort = 8283
test/Tests/MonoidConfig.hs view
@@ -21,6 +21,7 @@  import Configuration.Utils import Configuration.Utils.Internal+import Configuration.Utils.Internal.ConfigFileReader import Configuration.Utils.Validation  import qualified Data.HashMap.Strict as HM@@ -102,7 +103,6 @@         Just v' → HM.insert k v' m       where         mv = HM.lookup k m-        (<&>) = flip fmap  -- -------------------------------------------------------------------------- -- -- Text with right append@@ -168,12 +168,12 @@   where     ca = StringConfigR "a"     cb = StringConfigR "b"-    run c (x ∷ Int) b a = withConfigFile c $ \file →+    run c (x ∷ Int) b a = withConfigFile Yaml c $ \file →         runTest pkgInfo (mi [file]) ("stringR-file1-" ⊕ sshow x) b a      run2 c0 c1 (x ∷ Int) b a =-        withConfigFile c0 $ \file0 →-        withConfigFile c1 $ \file1 →+        withConfigFile Json c0 $ \file0 →+        withConfigFile Yaml c1 $ \file1 →         runTest pkgInfo (mi [file0,file1]) ("stringR-file2-" ⊕ sshow x) b a      mi files = set piConfigurationFiles (map ConfigFileRequired files) $@@ -243,12 +243,12 @@   where     ca = StringConfigL "a"     cb = StringConfigL "b"-    run c (x ∷ Int) b a = withConfigFile c $ \file →+    run c (x ∷ Int) b a = withConfigFile Json c $ \file →         runTest pkgInfo (mi [file]) ("stringL-file1-" ⊕ sshow x) b a      run2 c0 c1 (x ∷ Int) b a =-        withConfigFile c0 $ \file0 →-        withConfigFile c1 $ \file1 →+        withConfigFile Yaml c0 $ \file0 →+        withConfigFile Json c1 $ \file1 →         runTest pkgInfo (mi [file0,file1]) ("stringL-file2-" ⊕ sshow x) b a      mi files = set piConfigurationFiles (map ConfigFileRequired files) $