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 BangPatterns #-}
16 {-# LANGUAGE ScopedTypeVariables #-}
17 {-# LANGUAGE TemplateHaskell #-}
19 module Gargantext.API.Admin.Settings
22 -- import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
23 import Codec.Serialise (Serialise(), serialise)
25 import Control.Monad.Logger (LogLevel(..))
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 Gargantext.Core.NodeStory
31 import Gargantext.Prelude.Config ({-GargConfig(..),-} {-gc_repofilepath,-} readConfig)
32 import Network.HTTP.Client.TLS (newTlsManager)
33 import Servant.Auth.Server (defaultJWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
34 import Servant.Client (parseBaseUrl)
35 import Servant.Job.Async (newJobEnv, defaultSettings)
36 import System.Directory
37 -- import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
38 import System.IO (FilePath, hClose)
39 import System.IO.Temp (withTempFile)
40 import System.Log.FastLogger
41 import qualified Data.ByteString.Lazy as L
44 import Gargantext.API.Admin.EnvTypes
45 import Gargantext.API.Admin.Types
46 -- import Gargantext.API.Ngrams.Types (NgramsRepo, HasRepo(..), RepoEnv(..), r_version, initRepo, renv_var, renv_lock)
47 import Gargantext.Core.NLP (nlpServerMap)
48 import Gargantext.Database.Prelude (databaseParameters, hasConfig)
49 import Gargantext.Prelude
50 import Gargantext.Prelude.Config (gc_js_job_timeout, gc_js_id_timeout)
51 import qualified Gargantext.Prelude.Mail as Mail
52 import qualified Gargantext.Prelude.NLP as NLP
53 import qualified Gargantext.Utils.Jobs as Jobs
54 import qualified Gargantext.Utils.Jobs.Monad as Jobs
55 import qualified Gargantext.Utils.Jobs.Queue as Jobs
56 import qualified Gargantext.Utils.Jobs.Settings as Jobs
58 devSettings :: FilePath -> IO Settings
59 devSettings jwkFile = do
60 jwkExists <- doesFileExist jwkFile
61 when (not jwkExists) $ writeKey jwkFile
62 jwk <- readKey jwkFile
64 { _allowedOrigin = "http://localhost:8008"
65 , _allowedHost = "localhost:3000"
67 , _logLevelLimit = LevelDebug
68 -- , _dbServer = "localhost"
69 , _sendLoginEmails = LogEmailToConsole
70 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
71 , _cookieSettings = defaultCookieSettings { cookieXsrfSetting = Just xsrfCookieSetting } -- TODO-SECURITY tune
72 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
75 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
78 import System.Environment (lookupEnv)
80 reqSetting :: FromHttpApiData a => Text -> IO a
82 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
83 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
85 optSetting :: FromHttpApiData a => Text -> a -> IO a
86 optSetting name d = do
87 me <- lookupEnv (unpack name)
90 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
92 settingsFromEnvironment :: IO Settings
93 settingsFromEnvironment =
94 Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
95 <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
96 <*> optSetting "PORT" 3000
97 <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
98 <*> reqSetting "DB_SERVER"
99 <*> (parseJwk <$> reqSetting "JWT_SECRET")
100 <*> optSetting "SEND_EMAIL" SendEmailViaAws
103 -----------------------------------------------------------------------
104 -- | RepoDir FilePath configuration
105 type RepoDirFilePath = FilePath
107 repoSnapshot :: RepoDirFilePath -> FilePath
108 repoSnapshot repoDir = repoDir <> "/repo.cbor"
112 -- This assumes we own the lock on repoSnapshot.
113 repoSaverAction :: RepoDirFilePath -> Serialise a => a -> IO ()
114 repoSaverAction repoDir a = do
115 withTempFile repoDir "tmp-repo.cbor" $ \fp h -> do
116 -- printDebug "repoSaverAction" fp
117 L.hPut h $ serialise a
119 renameFile fp (repoSnapshot repoDir)
124 -- The use of mkDebounce makes sure that repoSaverAction is not called too often.
125 -- If repoSaverAction start taking more time than the debounceFreq then it should
127 mkRepoSaver :: RepoDirFilePath -> MVar NgramsRepo -> IO (IO ())
128 mkRepoSaver repoDir repo_var = mkDebounce settings'
130 settings' = defaultDebounceSettings
131 { debounceFreq = let n = 6 :: Int in 10^n -- 1 second
132 , debounceAction = withMVar repo_var (repoSaverAction repoDir)
133 -- Here this not only `readMVar` but `takeMVar`.
134 -- Namely while repoSaverAction is saving no other change
135 -- can be made to the MVar.
136 -- This might be not efficent and thus reconsidered later.
137 -- However this enables to safely perform a *final* save.
140 -- Add a new MVar just for saving.
145 readRepoEnv :: FilePath -> IO RepoEnv
146 readRepoEnv repoDir = do
147 -- Does file exist ? :: Bool
148 _repoDir <- createDirectoryIfMissing True repoDir
150 repoFile <- doesFileExist (repoSnapshot repoDir)
152 -- Is file not empty ? :: Bool
153 repoExists <- if repoFile
154 then (>0) <$> getFileSize (repoSnapshot repoDir)
157 mlock <- tryLockFile (repoSnapshot repoDir) Exclusive
158 lock <- maybe (panic "Repo file already locked") pure mlock
163 -- e_repo <- eitherDecodeStrict <$> deserialise <$> L.readFile repoSnapshot
164 repo <- deserialise <$> L.readFile (repoSnapshot repoDir)
165 -- repo <- either fail pure e_repo
166 let archive = (repoSnapshot repoDir) <> ".v" <> show (repo ^. r_version)
167 copyFile (repoSnapshot repoDir) archive
171 -- TODO save in DB here
172 saver <- mkRepoSaver repoDir mvar
173 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 prios <- Jobs.readPrios (file <> ".jobs")
188 let prios' = Jobs.applyPrios prios Jobs.defaultPrios
189 putStrLn $ "Overrides: " <> show prios
190 putStrLn $ "New priorities: " <> show prios'
191 !self_url_env <- parseBaseUrl $ "http://0.0.0.0:" <> show port
192 dbParam <- databaseParameters file
193 !pool <- newPool dbParam
194 --nodeStory_env <- readNodeStoryEnv (_gc_repofilepath config_env)
195 !nodeStory_env <- readNodeStoryEnv pool
196 !scrapers_env <- newJobEnv defaultSettings manager_env
198 secret <- Jobs.genSecret
199 let jobs_settings = (Jobs.defaultJobSettings 1 secret)
200 & Jobs.l_jsJobTimeout .~ (fromIntegral $ config_env ^. hasConfig ^. gc_js_job_timeout)
201 & Jobs.l_jsIDTimeout .~ (fromIntegral $ config_env ^. hasConfig ^. gc_js_id_timeout)
202 !jobs_env <- Jobs.newJobEnv jobs_settings prios' manager_env
203 !logger <- newStderrLoggerSet defaultBufSize
204 !config_mail <- Mail.readConfig file
205 !nlp_env <- nlpServerMap <$> NLP.readConfig file
207 {- An 'Env' by default doesn't have strict fields, but when constructing one in production
208 we want to force them to WHNF to avoid accumulating unnecessary thunks.
211 { _env_settings = settings'
212 , _env_logger = logger
214 , _env_nodeStory = nodeStory_env
215 , _env_manager = manager_env
216 , _env_scrapers = scrapers_env
217 , _env_jobs = jobs_env
218 , _env_self_url = self_url_env
219 , _env_config = config_env
220 , _env_mail = config_mail
224 newPool :: ConnectInfo -> IO (Pool Connection)
225 newPool param = createPool (connect param) close 1 (60*60) 8
228 cleanEnv :: (HasConfig env, HasRepo env) => env -> IO ()
230 r <- takeMVar (env ^. repoEnv . renv_var)
231 repoSaverAction (env ^. hasConfig . gc_repofilepath) r
232 unlockFile (env ^. repoEnv . renv_lock)