packages feed

espial-0.0.39: src/Application.hs

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}

module Application
  ( getApplicationDev,
    appMain,
    develMain,
    makeFoundation,
    makeLogWare,

    -- * for DevelMain
    getApplicationRepl,
    shutdownApp,

    -- * for GHCI
    handler,
    db,
  )
where

-- Import all relevant handler modules here.
-- Don't forget to add new modules to your cabal file!

import Archiver.ArchiveBox07 (archiveBox07Backend)
import Archiver.Backend (ArchiverBackend)
import Archiver.Debug (debugArchiverBackend)
import Archiver.WaybackMachine (waybackMachineBackend)
import Control.Monad.Logger
import Data.Aeson qualified as A
import Database.Persist.Sqlite (ConnectionPool, createSqlitePoolFromInfo, fkEnabled, mkSqliteConnectionInfo, runSqlPool, sqlDatabase, sqlPoolSize)
import Handler.AccountSettings
import Handler.Add
import Handler.Archive
import Handler.Common
import Handler.Docs
import Handler.Edit
import Handler.Home
import Handler.Locales
import Handler.Notes
import Handler.User
import I18n qualified
import Import
import Language.Haskell.TH.Syntax (qLocation)
import Lens.Micro
import Model.Migrations (runPersistentMigrations, runAppMigrations)
import Network.Connection qualified as NC
import Network.HTTP.Client.TLS
import Network.HTTP.Client.TLS qualified as NHT
import Network.Wai (Middleware)
import Network.Wai.Handler.Warp (Settings, defaultSettings, defaultShouldDisplayException, getPort, runSettings, setHost, setOnException, setPort)
import Network.Wai.Handler.WarpTLS (TLSSettings (..), runTLS, tlsSettings)
import Network.TLS (Credentials (..), ServerHooks (..))
import Network.TLS qualified as TLS
import Control.Concurrent (forkIO, threadDelay)
import Network.Wai.Middleware.AcceptOverride
import Network.Wai.Middleware.Autohead
import Network.Wai.Middleware.Gzip hiding (def)
import Network.Wai.Middleware.MethodOverride
import Network.Wai.Middleware.RequestLogger (Destination (Logger), IPAddrSource (..), OutputFormat (..), destination, mkRequestLogger, outputFormat)
import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet)
import Version qualified as Version

#ifndef mingw32_HOST_OS
import System.Posix.Signals qualified as PS (Handler (Catch, CatchOnce), installHandler, sigHUP, sigTERM)
import Control.Concurrent qualified as CC (killThread, myThreadId)
#endif

mkYesodDispatch "App" resourcesApp

makeFoundation :: AppSettings -> IO App
makeFoundation appSettings@AppSettings {..} = do
  appHttpManager <- getGlobalManager
  appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
  appFrontendBundleName <- loadFrontendBundleName appStaticDir
  appStatic <- (if appMutableStatic then staticDevel else static) appStaticDir
  (appTranslationsHash, appTranslations) <- I18n.loadTranslations appStaticDir
  appPublicTagCloudCache <- newIORef mempty
  appLoginRateLimiter <- newIORef mempty
  let appTranslate lang = I18n.translate appTranslations lang (I18nNs "translation")
      appI18nR = LocalesFileR appTranslationsHash []
      mkFoundation appConnPool appArchiver = App {..}
      appLogFunc = messageLoggerSource (mkFoundation (error "connPool forced in tempFoundation") Nothing) appLogger
      startupLogFunc = if appEnableStartupLogging then appLogFunc else \_ _ _ _ -> pure ()
  flip runLoggingT startupLogFunc $ do 
    (logInfoNS "startup" Version.versionSpec)
  do migrationsPool <- mkPool startupLogFunc False
     runSqlPool (runPersistentMigrations appEnableStartupLogging) migrationsPool
     runSqlPool (runAppMigrations appEnableStartupLogging) migrationsPool
  appPool <- mkPool appLogFunc True
  appArchiver <- mkArchiverBackend appLogFunc appPool
  flip runLoggingT startupLogFunc $ do 
    (logInfoNS "startup" $ "archive backend: " <> tshow appArchiveBackend)
    (logDebugNS "startup" ("language-default: " <> fromI18nLang' appLanguageDefault))
  pure (mkFoundation appPool appArchiver)
  where
    mkPool :: _ -> Bool -> IO ConnectionPool
    mkPool logFunc isFkEnabled =
      flip runLoggingT logFunc $ do
        let dbPath = sqlDatabase appDatabaseConf
            poolSize = sqlPoolSize appDatabaseConf
            connInfo =
              mkSqliteConnectionInfo dbPath
                & set fkEnabled isFkEnabled
        createSqlitePoolFromInfo connInfo poolSize
    mkArchiverBackend :: _ -> ConnectionPool -> IO (Maybe ArchiverBackend)
    mkArchiverBackend logFunc pool = do
      case appArchiveBackend of
        ArchiveBackendDisabled -> pure Nothing
        ArchiveBackendDebug -> pure (Just (debugArchiverBackend logFunc))
        ArchiveBackendArchiveLi -> mkArchiveLiArchiver
        ArchiveBackendWaybackMachine -> mkWaybackArchiver
        ArchiveBackendArchiveBox07 -> mkArchiveBox07Archiver
      where
        mkArchiveLiArchiver = do
          flip runLoggingT logFunc ($(logWarn) ("Archive backend `archive-li` selected but functionality has been removed; archiving disabled"))
          pure Nothing
        mkWaybackArchiver = do
          case (appWaybackMachineAccessKey, appWaybackMachineSecretKey) of
            (Just accessKey, Just secretKey) -> do
              archiveManager <- do
                let mSocks = NC.SockSettingsSimple <$> fmap unpack appArchiveSocksProxyHost <*> fmap toEnum appArchiveSocksProxyPort
                NHT.newTlsManagerWith (NHT.mkManagerSettings def mSocks)
              pure $ Just $ waybackMachineBackend accessKey secretKey pool archiveManager logFunc
            _ -> do
              flip runLoggingT logFunc ($(logWarn) ("Archive backend `wayback-machine` selected but access/secret key missing; archiving disabled"))
              pure Nothing
        mkArchiveBox07Archiver = archiveBox07Backend appSettings pool logFunc

loadFrontendBundleName :: FilePath -> IO Text
loadFrontendBundleName staticDir = do
  let manifestPath = staticDir <> "/js/manifest.json"
      fallbackName = "app.min.js"
  mManifest <- A.decodeFileStrict' manifestPath
  pure $ maybe fallbackName appBundle mManifest

newtype FrontendBundleManifest = FrontendBundleManifest
  { appBundle :: Text
  }

instance A.FromJSON FrontendBundleManifest where
  parseJSON = A.withObject "FrontendBundleManifest" $ \o -> do
    appBundle <- o A..: "appBundle"
    pure FrontendBundleManifest {..}

makeApplication :: App -> IO Application
makeApplication foundation = do
  logWare <- makeLogWare foundation
  appPlain <- toWaiAppPlain foundation
  pure (logWare (makeMiddleware appPlain))

makeMiddleware :: Middleware
makeMiddleware =
  acceptOverride
    . autohead
    . gzip def {gzipFiles = GzipPreCompressed GzipIgnore}
    . methodOverride

makeLogWare :: App -> IO Middleware
makeLogWare foundation
  | appEnableRequestLogging (appSettings foundation) =
      mkRequestLogger
        def
          { outputFormat =
              if appDetailedRequestLogging (appSettings foundation)
                then Detailed True
                else
                  Apache
                    ( if appIpFromHeader (appSettings foundation)
                        then FromFallback
                        else FromSocket
                    ),
            destination = Logger (loggerSet (appLogger foundation))
          }
  | otherwise = pure id

-- | Warp settings for the given foundation value.
warpSettings :: App -> Settings
warpSettings foundation =
  setPort (appPort (appSettings foundation))
    $ setHost (appHost (appSettings foundation))
    $ setOnException
      ( \_req e ->
          when (defaultShouldDisplayException e)
            $ messageLoggerSource
              foundation
              (appLogger foundation)
              $(qLocation >>= liftLoc)
              "yesod"
              LevelError
              (toLogStr $ "Exception from Warp: " ++ show e)
      )
      defaultSettings

makeTlsSettings :: FilePath -> FilePath -> IO TLSSettings
makeTlsSettings certPath keyPath = do
  creds <- loadCreds
  credsRef <- newIORef creds
  void $ forkIO $ forever $ do
    threadDelay reloadCredsFrequency
    reloadCreds credsRef
#ifndef mingw32_HOST_OS
  void $ PS.installHandler PS.sigHUP (PS.Catch $ reloadCreds credsRef) Nothing
#endif
  pure (tlsSettings certPath keyPath)
    { tlsServerHooks = def { onServerNameIndication = \_ -> readIORef credsRef } }
  where
    reloadCredsFrequency = 12 * 60 * 60 * 1_000_000 -- 12 hours in microseconds
    loadCreds =
      TLS.credentialLoadX509 certPath keyPath >>= \case
        Left e  -> throwString $ "TLS credential load failed: " ++ e
        Right c -> pure $ Credentials [c]
    reloadCreds ref =
      tryAny loadCreds >>= \case
        Left e  -> putStrLn $ "TLS cert reload failed: " <> tshow e
        Right c -> writeIORef ref c

-- | For yesod devel, return the Warp settings and WAI Application.
getApplicationDev :: IO (Settings, Application)
getApplicationDev = do
  settings <- getAppSettings
  foundation <- makeFoundation settings
  wsettings <- getDevSettings (warpSettings foundation)
  app <- makeApplication foundation
  pure (wsettings, app)

getAppSettings :: IO AppSettings
getAppSettings = loadYamlSettings [configSettingsYml] [configSettingsYmlValue] useEnv

-- | main function for use by yesod devel
develMain :: IO ()
develMain = develMainHelper getApplicationDev

-- | The @main@ function for an executable running this site.
appMain :: IO ()
appMain = do
  hSetBuffering stdout LineBuffering
  hSetBuffering stderr LineBuffering
  settings <- getAppSettings
  foundation <- makeFoundation settings
  app <- makeApplication foundation
#ifndef mingw32_HOST_OS
  mainThreadId <- CC.myThreadId
  void $ PS.installHandler PS.sigTERM (PS.CatchOnce (CC.killThread mainThreadId)) Nothing
#endif
  case (appTLSCertFile (appSettings foundation), appTLSKeyFile (appSettings foundation)) of
    (Just certFile, Just keyFile) -> do
      tlsS <- makeTlsSettings certFile keyFile
      flip runLoggingT (messageLoggerSource foundation (appLogger foundation)) (logInfoNS "startup" "starting espial server [tls]")
      runTLS tlsS (warpSettings foundation) app
    _ -> do
      flip runLoggingT (messageLoggerSource foundation (appLogger foundation)) (logInfoNS "startup" "starting espial server [http]")
      runSettings (warpSettings foundation) app

getApplicationRepl :: IO (Int, App, Application)
getApplicationRepl = do
  settings <- getAppSettings
  foundation <- makeFoundation settings
  wsettings <- getDevSettings (warpSettings foundation)
  app1 <- makeApplication foundation
  pure (getPort wsettings, foundation, app1)

shutdownApp :: App -> IO ()
shutdownApp _ = pure ()

-- | Run a handler
handler :: Handler a -> IO a
handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h

-- | Run DB queries
db :: ReaderT SqlBackend (HandlerFor App) a -> IO a
db = handler . runDB