2 Module : Gargantext.API.Admin.Settings
3 Description : Settings of the API (Server and Client)
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
10 TODO-SECURITY: Critical
15 {-# LANGUAGE ScopedTypeVariables #-}
16 {-# LANGUAGE TemplateHaskell #-}
18 module Gargantext.API.Admin.Settings
21 import Codec.Serialise (Serialise(), serialise, deserialise)
22 import Control.Concurrent
23 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
25 import Control.Monad.Logger
26 import Control.Monad.Reader
27 import Data.Maybe (fromMaybe)
28 import Data.Pool (Pool, createPool)
29 import Database.PostgreSQL.Simple (Connection, connect, close, ConnectInfo)
30 import Network.HTTP.Client.TLS (newTlsManager)
31 import Servant.Auth.Server (defaultJWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
32 import Servant.Client (parseBaseUrl)
33 import Servant.Job.Async (newJobEnv, defaultSettings)
34 import System.Directory
35 import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
36 import System.IO (FilePath, hClose)
37 import System.IO.Temp (withTempFile)
38 import System.Log.FastLogger
39 import qualified Data.ByteString.Lazy as L
41 import Gargantext.Core.Types (NodeId)
42 import Gargantext.API.Admin.EnvTypes
43 import Gargantext.API.Admin.Types
44 import Gargantext.API.Ngrams.Types (NgramsRepo, HasRepo(..), RepoEnv(..), r_version, initRepo, renv_var, renv_lock)
45 import Gargantext.Database.Prelude (databaseParameters, HasConfig(..))
46 import Gargantext.Prelude
47 import Gargantext.Prelude.Config (GargConfig(..), gc_repofilepath, readConfig)
49 devSettings :: FilePath -> IO Settings
50 devSettings jwkFile = do
51 jwkExists <- doesFileExist jwkFile
52 when (not jwkExists) $ writeKey jwkFile
53 jwk <- readKey jwkFile
55 { _allowedOrigin = "http://localhost:8008"
56 , _allowedHost = "localhost:3000"
58 , _logLevelLimit = LevelDebug
59 -- , _dbServer = "localhost"
60 , _sendLoginEmails = LogEmailToConsole
61 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
62 , _cookieSettings = defaultCookieSettings { cookieXsrfSetting = Just xsrfCookieSetting } -- TODO-SECURITY tune
63 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
66 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
69 import System.Environment (lookupEnv)
71 reqSetting :: FromHttpApiData a => Text -> IO a
73 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
74 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
76 optSetting :: FromHttpApiData a => Text -> a -> IO a
77 optSetting name d = do
78 me <- lookupEnv (unpack name)
81 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
83 settingsFromEnvironment :: IO Settings
84 settingsFromEnvironment =
85 Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
86 <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
87 <*> optSetting "PORT" 3000
88 <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
89 <*> reqSetting "DB_SERVER"
90 <*> (parseJwk <$> reqSetting "JWT_SECRET")
91 <*> optSetting "SEND_EMAIL" SendEmailViaAws
94 -----------------------------------------------------------------------
95 -- | RepoDir FilePath configuration
96 type RepoDirFilePath = FilePath
98 repoSnapshot :: RepoDirFilePath -> FilePath
99 repoSnapshot repoDir = repoDir <> "/repo.cbor"
101 repoSnapshot' :: RepoDirFilePath -> NodeId -> FilePath
102 repoSnapshot' repoDir nId = repoDir <> "/repo" <> "-" <> (cs $ show nId) <> ".cbor"
106 -- | TODO add hard coded file in Settings
107 -- This assumes we own the lock on repoSnapshot.
108 repoSaverAction :: RepoDirFilePath -> Serialise a => a -> IO ()
109 repoSaverAction repoDir a = do
110 withTempFile "repos" "tmp-repo.cbor" $ \fp h -> do
111 printDebug "repoSaverAction" fp
112 L.hPut h $ serialise a
114 renameFile fp (repoSnapshot repoDir)
117 repoSaverAction' :: RepoDirFilePath -> NgramsRepo -> IO ()
118 repoSaverAction' repoDir a = do
119 withTempFile "repos" "tmp-repo.cbor" $ \fp h -> do
120 printDebug "repoSaverAction" fp
121 L.hPut h $ serialise a
123 renameFile fp (repoSnapshot repoDir)
127 -- The use of mkDebounce makes sure that repoSaverAction is not called too often.
128 -- If repoSaverAction start taking more time than the debounceFreq then it should
130 mkRepoSaver :: RepoDirFilePath -> MVar NgramsRepo -> IO (IO ())
131 mkRepoSaver repoDir repo_var = mkDebounce settings'
133 settings' = defaultDebounceSettings
134 { debounceFreq = let n = 6 :: Int in 10^n -- 1 second
135 , debounceAction = withMVar repo_var (repoSaverAction repoDir)
136 -- Here this not only `readMVar` but `takeMVar`.
137 -- Namely while repoSaverAction is saving no other change
138 -- can be made to the MVar.
139 -- This might be not efficent and thus reconsidered later.
140 -- However this enables to safely perform a *final* save.
143 -- Add a new MVar just for saving.
146 readRepoEnv :: FilePath -> IO RepoEnv
147 readRepoEnv repoDir = do
148 -- Does file exist ? :: Bool
149 _repoDir <- createDirectoryIfMissing True repoDir
151 repoFile <- doesFileExist (repoSnapshot repoDir)
153 -- Is file not empty ? :: Bool
154 repoExists <- if repoFile
155 then (>0) <$> getFileSize (repoSnapshot repoDir)
158 mlock <- tryLockFile (repoSnapshot repoDir) Exclusive
159 lock <- maybe (panic "Repo file already locked") pure mlock
164 -- e_repo <- eitherDecodeStrict <$> deserialise <$> L.readFile repoSnapshot
165 repo <- deserialise <$> L.readFile (repoSnapshot repoDir)
166 -- repo <- either fail pure e_repo
167 let archive = (repoSnapshot repoDir) <> ".v" <> show (repo ^. r_version)
168 copyFile (repoSnapshot repoDir) archive
172 -- TODO save in DB here
173 saver <- mkRepoSaver repoDir mvar
174 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
176 devJwkFile :: FilePath
177 devJwkFile = "dev.jwk"
179 newEnv :: PortNumber -> FilePath -> IO Env
180 newEnv port file = do
181 manager_env <- newTlsManager
182 settings' <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
183 when (port /= settings' ^. appPort) $
184 panic "TODO: conflicting settings of port"
186 config_env <- readConfig file
187 self_url_env <- parseBaseUrl $ "http://0.0.0.0:" <> show port
188 dbParam <- databaseParameters file
189 pool <- newPool dbParam
190 repo <- readRepoEnv (_gc_repofilepath config_env)
191 scrapers_env <- newJobEnv defaultSettings manager_env
192 logger <- newStderrLoggerSet defaultBufSize
195 { _env_settings = settings'
196 , _env_logger = logger
199 , _env_manager = manager_env
200 , _env_scrapers = scrapers_env
201 , _env_self_url = self_url_env
202 , _env_config = config_env
205 newPool :: ConnectInfo -> IO (Pool Connection)
206 newPool param = createPool (connect param) close 1 (60*60) 8
208 cleanEnv :: (HasConfig env, HasRepo env) => env -> IO ()
210 r <- takeMVar (env ^. repoEnv . renv_var)
211 repoSaverAction (env ^. hasConfig . gc_repofilepath) r
212 unlockFile (env ^. repoEnv . renv_lock)