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 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.Database.Prelude (databaseParameters, HasConfig(..))
48 import Gargantext.Prelude
49 import Gargantext.Prelude.Config (gc_repofilepath)
51 devSettings :: FilePath -> IO Settings
52 devSettings jwkFile = do
53 jwkExists <- doesFileExist jwkFile
54 when (not jwkExists) $ writeKey jwkFile
55 jwk <- readKey jwkFile
57 { _allowedOrigin = "http://localhost:8008"
58 , _allowedHost = "localhost:3000"
60 , _logLevelLimit = LevelDebug
61 -- , _dbServer = "localhost"
62 , _sendLoginEmails = LogEmailToConsole
63 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
64 , _cookieSettings = defaultCookieSettings { cookieXsrfSetting = Just xsrfCookieSetting } -- TODO-SECURITY tune
65 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
68 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
71 import System.Environment (lookupEnv)
73 reqSetting :: FromHttpApiData a => Text -> IO a
75 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
76 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
78 optSetting :: FromHttpApiData a => Text -> a -> IO a
79 optSetting name d = do
80 me <- lookupEnv (unpack name)
83 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
85 settingsFromEnvironment :: IO Settings
86 settingsFromEnvironment =
87 Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
88 <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
89 <*> optSetting "PORT" 3000
90 <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
91 <*> reqSetting "DB_SERVER"
92 <*> (parseJwk <$> reqSetting "JWT_SECRET")
93 <*> optSetting "SEND_EMAIL" SendEmailViaAws
96 -----------------------------------------------------------------------
97 -- | RepoDir FilePath configuration
98 type RepoDirFilePath = FilePath
100 repoSnapshot :: RepoDirFilePath -> FilePath
101 repoSnapshot repoDir = repoDir <> "/repo.cbor"
105 -- This assumes we own the lock on repoSnapshot.
106 repoSaverAction :: RepoDirFilePath -> Serialise a => a -> IO ()
107 repoSaverAction repoDir a = do
108 withTempFile repoDir "tmp-repo.cbor" $ \fp h -> do
109 printDebug "repoSaverAction" fp
110 L.hPut h $ serialise a
112 renameFile fp (repoSnapshot repoDir)
117 -- The use of mkDebounce makes sure that repoSaverAction is not called too often.
118 -- If repoSaverAction start taking more time than the debounceFreq then it should
120 mkRepoSaver :: RepoDirFilePath -> MVar NgramsRepo -> IO (IO ())
121 mkRepoSaver repoDir repo_var = mkDebounce settings'
123 settings' = defaultDebounceSettings
124 { debounceFreq = let n = 6 :: Int in 10^n -- 1 second
125 , debounceAction = withMVar repo_var (repoSaverAction repoDir)
126 -- Here this not only `readMVar` but `takeMVar`.
127 -- Namely while repoSaverAction is saving no other change
128 -- can be made to the MVar.
129 -- This might be not efficent and thus reconsidered later.
130 -- However this enables to safely perform a *final* save.
133 -- Add a new MVar just for saving.
136 readRepoEnv :: FilePath -> IO RepoEnv
137 readRepoEnv repoDir = do
138 -- Does file exist ? :: Bool
139 _repoDir <- createDirectoryIfMissing True repoDir
141 repoFile <- doesFileExist (repoSnapshot repoDir)
143 -- Is file not empty ? :: Bool
144 repoExists <- if repoFile
145 then (>0) <$> getFileSize (repoSnapshot repoDir)
148 mlock <- tryLockFile (repoSnapshot repoDir) Exclusive
149 lock <- maybe (panic "Repo file already locked") pure mlock
154 -- e_repo <- eitherDecodeStrict <$> deserialise <$> L.readFile repoSnapshot
155 repo <- deserialise <$> L.readFile (repoSnapshot repoDir)
156 -- repo <- either fail pure e_repo
157 let archive = (repoSnapshot repoDir) <> ".v" <> show (repo ^. r_version)
158 copyFile (repoSnapshot repoDir) archive
162 -- TODO save in DB here
163 saver <- mkRepoSaver repoDir mvar
164 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
167 devJwkFile :: FilePath
168 devJwkFile = "dev.jwk"
170 newEnv :: PortNumber -> FilePath -> IO Env
171 newEnv port file = do
172 manager_env <- newTlsManager
173 settings' <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
174 when (port /= settings' ^. appPort) $
175 panic "TODO: conflicting settings of port"
177 config_env <- readConfig file
178 self_url_env <- parseBaseUrl $ "http://0.0.0.0:" <> show port
179 dbParam <- databaseParameters file
180 pool <- newPool dbParam
181 repo <- readRepoEnv (_gc_repofilepath config_env)
182 nodeStory_env <- readNodeStoryEnv (_gc_repofilepath config_env)
183 scrapers_env <- newJobEnv defaultSettings manager_env
184 logger <- newStderrLoggerSet defaultBufSize
187 { _env_settings = settings'
188 , _env_logger = logger
191 , _env_nodeStory = nodeStory_env
192 , _env_manager = manager_env
193 , _env_scrapers = scrapers_env
194 , _env_self_url = self_url_env
195 , _env_config = config_env
198 newPool :: ConnectInfo -> IO (Pool Connection)
199 newPool param = createPool (connect param) close 1 (60*60) 8
202 cleanEnv :: (HasConfig env, HasRepo env) => env -> IO ()
204 r <- takeMVar (env ^. repoEnv . renv_var)
205 repoSaverAction (env ^. hasConfig . gc_repofilepath) r
206 unlockFile (env ^. repoEnv . renv_lock)