{-#LANGUAGE NoImplicitPrelude #-} {-#LANGUAGE OverloadedStrings #-} {-#LANGUAGE OverloadedLists #-} {-#LANGUAGE LambdaCase #-} {-#LANGUAGE ScopedTypeVariables #-} {-#LANGUAGE TypeSynonymInstances #-} module Web.Sprinkles.ServerConfig where import Web.Sprinkles.Prelude import Web.Sprinkles.Rule import Data.Aeson as JSON import Data.Aeson.TH import qualified Data.Yaml as YAML import Web.Sprinkles.Backends import Data.Default import System.FilePath.Glob (glob) import Control.MaybeEitherMonad (maybeFail) import System.Directory (doesFileExist) import Data.Scientific (Scientific) import Data.Time.Clock.POSIX (POSIXTime) import Web.Sprinkles.Logger (LogLevel (..)) import Web.Sprinkles.Exceptions import Web.Sprinkles.Databases (DSN (..), SqlDriver (..)) data BackendCacheConfig = FilesystemCache FilePath POSIXTime | MemCache POSIXTime | MemcachedCache deriving (Show) instance FromJSON BackendCacheConfig where parseJSON (String str) = maybeFail $ backendCacheConfigFromString str parseJSON (Object obj) = (obj .: "type") >>= \case "file" -> FilesystemCache <$> (obj .:? "dir" .!= ".cache") <*> (obj .:? "max-age" .!= 300) "mem" -> MemCache <$> (obj .:? "max-age" .!= 60) "memcached" -> return MemcachedCache x -> fail $ "Invalid backend cache type: '" <> x parseJSON x = fail $ "Invalid backend cache specification: " <> show x backendCacheConfigFromString :: Text -> Maybe BackendCacheConfig backendCacheConfigFromString str = case splitSeq ":" str of ["file", dir] -> return $ FilesystemCache (unpack dir) 300 ["file"] -> return $ FilesystemCache ".cache" 300 ["mem"] -> return $ MemCache 60 ["memcached"] -> return MemcachedCache xs -> Nothing data ServerDriver = WarpDriver (Maybe Int) | CGIDriver | SCGIDriver | FastCGIDriver | BakeDriver | DefaultDriver deriving (Show, Read, Eq) instance Default ServerDriver where def = DefaultDriver instance FromJSON ServerDriver where parseJSON (String "warp") = return $ WarpDriver Nothing parseJSON (String "cgi") = return CGIDriver parseJSON (String "fastcgi") = return FastCGIDriver parseJSON (String "fcgi") = return FastCGIDriver parseJSON (String "scgi") = return SCGIDriver parseJSON (String "bake") = return BakeDriver parseJSON (String "default") = return def parseJSON (String x) = fail $ "Invalid server driver " ++ show x parseJSON (Object o) = do st :: Text <- o .: "type" case st of "warp" -> WarpDriver <$> o .:? "port" "cgi" -> return CGIDriver "fcgi" -> return FastCGIDriver "fastcgi" -> return FastCGIDriver "scgi" -> return SCGIDriver "bake" -> return BakeDriver x -> fail $ "Invalid server driver " ++ show x parseJSON x = fail $ "Invalid server driver " ++ show x data LoggerConfig = DiscardLog | Syslog LogLevel | StdioLog LogLevel deriving (Show) instance FromJSON LoggerConfig where parseJSON (Object obj) = do dest <- obj .:? "destination" case dest :: Maybe Text of Nothing -> return DiscardLog Just "discard" -> return DiscardLog Just "null" -> return DiscardLog Just "stdio" -> StdioLog . fromMaybe Warning <$> obj .:? "level" Just "syslog" -> Syslog . fromMaybe Warning <$> obj .:? "level" Just x -> fail $ "Invalid logger type " ++ show x parseJSON _ = fail "Invalid logger config" data SessionExpiration = NeverExpire | SlidingExpiration Integer deriving (Show) instance FromJSON SessionExpiration where parseJSON = \case Number n -> return . SlidingExpiration . floor $ n Null -> return NeverExpire String "never" -> return NeverExpire _ -> fail "Invalid session expiration" data SessionDriver = NoSessionDriver | InProcSessionDriver | SqlSessionDriver DSN deriving (Show) instance FromJSON SessionDriver where parseJSON = \case Null -> return NoSessionDriver String "inproc" -> return InProcSessionDriver String "sql" -> return $ SqlSessionDriver (DSN SqliteDriver "sessions.sqlite") String x -> fail $ "Invalid session driver " ++ show x Object obj -> do ty <- obj .: "type" case (ty :: Text) of "inproc" -> return InProcSessionDriver "sql" -> do SqlSessionDriver <$> obj .:? "connection" .!= DSN SqliteDriver "sessions.sqlite" x -> fail $ "Invalid session driver " ++ show x _ -> fail "Invalid session driver" data SessionConfig = SessionConfig { sessCookieName :: ByteString , sessCookieSecure :: Bool , sessExpiration :: SessionExpiration , sessDriver :: SessionDriver } deriving (Show) instance FromJSON SessionConfig where parseJSON (Object obj) = do cookieName <- encodeUtf8 <$> obj .:? "cookie-name" .!= "ssid" expiration <- obj .:? "expiration" .!= NeverExpire secure <- obj .:? "secure" .!= True -- secure default driver <- obj .:? "driver" .!= InProcSessionDriver return $ SessionConfig cookieName secure expiration driver parseJSON x = do driver <- parseJSON x return $ SessionConfig "ssid" True NeverExpire driver instance Default SessionConfig where def = SessionConfig "ssid" True NeverExpire NoSessionDriver data ServerConfig = ServerConfig { scBackendCache :: [BackendCacheConfig] , scDriver :: ServerDriver , scLogger :: Maybe LoggerConfig , scSessions :: SessionConfig , scRootDir :: FilePath } deriving (Show) instance Default ServerConfig where def = ServerConfig { scBackendCache = def , scDriver = def , scLogger = Nothing , scSessions = def , scRootDir = "" } instance Semigroup ServerConfig where (<>) = scAppend instance Monoid ServerConfig where mempty = def mappend = scAppend instance FromJSON ServerConfig where parseJSON (Object obj) = do caches <- fromMaybe [] <$> ( obj .:? "backend-cache" <|> (fmap (:[]) <$> obj .:? "backend-cache") ) driver <- fromMaybe def <$> ( obj .:? "driver" ) logger <- obj .:? "log" sessions <- obj .:? "sessions" .!= def rootDir <- obj .:? "dir" .!= "" return ServerConfig { scBackendCache = caches , scDriver = driver , scLogger = logger , scSessions = sessions , scRootDir = rootDir } parseJSON _ = fail "Invalid server config" scAppend :: ServerConfig -> ServerConfig -> ServerConfig scAppend a b = ServerConfig { scBackendCache = firstNonNull (scBackendCache b) (scBackendCache a) , scLogger = scLogger b <|> scLogger a , scDriver = if scDriver b == DefaultDriver then scDriver a else scDriver b , scSessions = case sessDriver (scSessions b) of NoSessionDriver -> scSessions a _ -> scSessions b , scRootDir = firstNonNull (scRootDir b) (scRootDir a) } firstNonNull :: [a] -> [a] -> [a] firstNonNull [] xs = xs firstNonNull xs _ = xs loadServerConfigFile :: FilePath -> IO ServerConfig loadServerConfigFile fn = YAML.decodeFileEither fn >>= either (throwM . withSourceContext (pack fn)) return loadServerConfig :: FilePath -> IO ServerConfig loadServerConfig dir = do homeDirMay <- lookupEnv ("HOME" :: String) let systemGlobalFilename = "/etc/sprinkles/server.yml" globalFilename = "/usr/local/etc/sprinkles/server.yml" userFilenameMay = ( ".config" "sprinkles" "server.yml") <$> homeDirMay localFilename = dir "config" "server.yml" serverConfigFilename = dir "server.yml" let filenames' = catMaybes [ Just systemGlobalFilename , Just globalFilename , userFilenameMay , Just localFilename , Just serverConfigFilename ] filenames <- filterM doesFileExist filenames' mconcat <$> forM filenames loadServerConfigFile