mirror of
https://github.com/Start9Labs/registry.git
synced 2026-03-26 02:11:53 +00:00
* handle root url * add handler file * fix compilation bug * fix compilation error * attempt redirect * add query param of hostname * add name to root query params * cleanup
553 lines
17 KiB
Haskell
553 lines
17 KiB
Haskell
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
{-# LANGUAGE ViewPatterns #-}
|
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
|
|
module Application (
|
|
appMain,
|
|
develMain,
|
|
makeFoundation,
|
|
makeLogWare,
|
|
shutdownApp,
|
|
shutdownAll,
|
|
shutdownWeb,
|
|
startApp,
|
|
startWeb,
|
|
|
|
-- * for DevelMain
|
|
getApplicationRepl,
|
|
getAppSettings,
|
|
|
|
-- * for GHCI
|
|
handler,
|
|
db,
|
|
) where
|
|
|
|
import Startlude (
|
|
Applicative (pure),
|
|
Async (asyncThreadId),
|
|
Bool (False, True),
|
|
Either (Left, Right),
|
|
Eq ((==)),
|
|
ExitCode (ExitSuccess),
|
|
IO,
|
|
Int,
|
|
Maybe (Just),
|
|
Monad (return, (>>=)),
|
|
MonadIO (..),
|
|
Print (putStr, putStrLn),
|
|
ReaderT (runReaderT),
|
|
Text,
|
|
ThreadId,
|
|
async,
|
|
flip,
|
|
for_,
|
|
forever,
|
|
forkIO,
|
|
fromIntegral,
|
|
killThread,
|
|
newEmptyMVar,
|
|
newMVar,
|
|
panic,
|
|
print,
|
|
putMVar,
|
|
show,
|
|
stdout,
|
|
swapMVar,
|
|
takeMVar,
|
|
void,
|
|
waitEitherCatchCancel,
|
|
when,
|
|
($),
|
|
(++),
|
|
(.),
|
|
(<$>),
|
|
(<||>),
|
|
)
|
|
|
|
import Control.Monad.Logger (
|
|
LoggingT,
|
|
liftLoc,
|
|
runLoggingT,
|
|
)
|
|
import Data.Default (Default (def))
|
|
import Database.Persist.Postgresql (
|
|
createPostgresqlPool,
|
|
pgConnStr,
|
|
pgPoolSize,
|
|
runMigration,
|
|
runSqlPool,
|
|
)
|
|
import Language.Haskell.TH.Syntax (qLocation)
|
|
import Network.Wai (
|
|
Application,
|
|
Middleware,
|
|
Request (requestHeaders),
|
|
ResponseReceived,
|
|
)
|
|
import Network.Wai.Handler.Warp (
|
|
Settings,
|
|
defaultSettings,
|
|
defaultShouldDisplayException,
|
|
getPort,
|
|
runSettings,
|
|
setHTTP2Disabled,
|
|
setHost,
|
|
setOnException,
|
|
setPort,
|
|
setTimeout,
|
|
)
|
|
import Network.Wai.Handler.WarpTLS (
|
|
runTLS,
|
|
tlsSettings,
|
|
)
|
|
import Network.Wai.Middleware.AcceptOverride (
|
|
acceptOverride,
|
|
)
|
|
import Network.Wai.Middleware.Autohead (
|
|
autohead,
|
|
)
|
|
import Network.Wai.Middleware.Cors (
|
|
CorsResourcePolicy (..),
|
|
cors,
|
|
simpleCorsResourcePolicy,
|
|
)
|
|
import Network.Wai.Middleware.MethodOverride (
|
|
methodOverride,
|
|
)
|
|
import Network.Wai.Middleware.RequestLogger (
|
|
Destination (Logger),
|
|
OutputFormat (..),
|
|
destination,
|
|
mkRequestLogger,
|
|
outputFormat,
|
|
)
|
|
import System.IO (
|
|
BufferMode (..),
|
|
hSetBuffering,
|
|
)
|
|
import System.Log.FastLogger (
|
|
defaultBufSize,
|
|
newStdoutLoggerSet,
|
|
toLogStr,
|
|
)
|
|
import Yesod.Core (
|
|
HandlerFor,
|
|
LogLevel (LevelError),
|
|
Yesod (messageLoggerSource),
|
|
logInfo,
|
|
mkYesodDispatch,
|
|
toWaiAppPlain,
|
|
typeOctet,
|
|
)
|
|
import Yesod.Core.Types (Logger (loggerSet))
|
|
import Yesod.Default.Config2 (
|
|
configSettingsYml,
|
|
develMainHelper,
|
|
getDevSettings,
|
|
loadYamlSettings,
|
|
loadYamlSettingsArgs,
|
|
makeYesodLogger,
|
|
useEnv,
|
|
)
|
|
|
|
import Control.Lens (both)
|
|
import Data.List (lookup)
|
|
import Data.String.Interpolate.IsString (
|
|
i,
|
|
)
|
|
import Database.Persist.Migration qualified
|
|
import Database.Persist.Migration.Postgres qualified
|
|
import Database.Persist.Sql (SqlBackend)
|
|
import Foundation (
|
|
Handler,
|
|
RegistryCtx (..),
|
|
Route (..),
|
|
resourcesRegistryCtx,
|
|
setWebProcessThreadId,
|
|
unsafeHandler,
|
|
)
|
|
import Handler.Admin (
|
|
deleteCategoryR,
|
|
deletePkgCategorizeR,
|
|
getPkgDeindexR,
|
|
postCategoryR,
|
|
postEosUploadR,
|
|
postPkgCategorizeR,
|
|
postPkgDeindexR,
|
|
postPkgIndexR,
|
|
postPkgUploadR,
|
|
)
|
|
import Handler.Eos (getEosR, getEosVersionR)
|
|
import Handler.Root(getRootR)
|
|
import Handler.Package
|
|
import Lib.Ssl (
|
|
doesSslNeedRenew,
|
|
renewSslCerts,
|
|
setupSsl,
|
|
)
|
|
import Migration (manualMigration)
|
|
import Model (migrateAll)
|
|
import Network.HTTP.Types.Header (hOrigin)
|
|
import Network.Wai.Middleware.Gzip (
|
|
GzipFiles (GzipCompress),
|
|
GzipSettings (gzipCheckMime, gzipFiles),
|
|
defaultCheckMime,
|
|
gzip,
|
|
)
|
|
import Network.Wai.Middleware.RequestLogger.JSON (
|
|
formatAsJSONWithHeaders,
|
|
)
|
|
import Settings (
|
|
AppPort,
|
|
AppSettings (..),
|
|
configSettingsYmlValue,
|
|
)
|
|
import System.Directory (createDirectoryIfMissing)
|
|
import System.Posix.Process (exitImmediately)
|
|
import System.Time.Extra (sleep)
|
|
import Yesod (YesodPersist (runDB))
|
|
|
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
|
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
|
|
-- comments there for more details.
|
|
mkYesodDispatch "RegistryCtx" resourcesRegistryCtx
|
|
|
|
|
|
-- | This function allocates resources (such as a database connection pool),
|
|
-- performs initialization and returns a foundation datatype value. This is also
|
|
-- the place to put your migrate statements to have automatic database
|
|
-- migrations handled by Yesod.
|
|
makeFoundation :: AppSettings -> IO RegistryCtx
|
|
makeFoundation appSettings = do
|
|
-- Some basic initializations: HTTP connection manager, logger, and static
|
|
-- subsite.
|
|
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
|
|
|
|
appWebServerThreadId <- newEmptyMVar
|
|
appShouldRestartWeb <- newMVar False
|
|
|
|
-- We need a log function to create a connection pool. We need a connection
|
|
-- pool to create our foundation. And we need our foundation to get a
|
|
-- logging function. To get out of this loop, we initially create a
|
|
-- temporary foundation without a real connection pool, get a log function
|
|
-- from there, and then create the real foundation.
|
|
let mkFoundation appConnPool = RegistryCtx{..}
|
|
-- The RegistryCtx {..} syntax is an example of record wild cards. For more
|
|
-- information, see:
|
|
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
|
|
tempFoundation =
|
|
mkFoundation (panic "connPool forced in tempFoundation")
|
|
logFunc = messageLoggerSource tempFoundation appLogger
|
|
|
|
createDirectoryIfMissing True (errorLogRoot appSettings)
|
|
|
|
-- Create the database connection pool
|
|
pool <-
|
|
flip runLoggingT logFunc $
|
|
createPostgresqlPool (pgConnStr $ appDatabaseConf appSettings) (pgPoolSize . appDatabaseConf $ appSettings)
|
|
|
|
if (needsMigration appSettings)
|
|
then
|
|
runSqlPool
|
|
(Database.Persist.Migration.Postgres.runMigration Database.Persist.Migration.defaultSettings manualMigration)
|
|
pool
|
|
else
|
|
-- Preform database migration using application logging settings
|
|
runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc
|
|
|
|
-- Return the foundation
|
|
return $ mkFoundation pool
|
|
|
|
|
|
-- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and
|
|
-- applying some additional middlewares.
|
|
makeApplication :: RegistryCtx -> IO Application
|
|
makeApplication foundation = do
|
|
logWare <- makeLogWare foundation
|
|
let authWare = makeAuthWare foundation
|
|
-- Create the WAI application and apply middlewares
|
|
appPlain <- toWaiAppPlain foundation
|
|
let gzipSettings =
|
|
-- TODO: change this to the cached version when we have better release processes
|
|
-- since caches aren't invalidated, publishing a new package/eos won't take effect
|
|
-- because the cached file will be downloaded.
|
|
def{gzipFiles = GzipCompress, gzipCheckMime = defaultCheckMime <||> (== typeOctet)}
|
|
pure
|
|
. logWare
|
|
. cors dynamicCorsResourcePolicy
|
|
. authWare
|
|
. acceptOverride
|
|
. autohead
|
|
. methodOverride
|
|
. gzip gzipSettings
|
|
$ appPlain
|
|
|
|
|
|
dynamicCorsResourcePolicy :: Request -> Maybe CorsResourcePolicy
|
|
dynamicCorsResourcePolicy req = Just . policy . lookup hOrigin $ requestHeaders req
|
|
where
|
|
policy o =
|
|
simpleCorsResourcePolicy
|
|
{ corsOrigins = (\o' -> ([o'], True)) <$> o
|
|
, corsMethods = ["GET", "POST", "HEAD", "PUT", "DELETE", "TRACE", "CONNECT", "OPTIONS", "PATCH"]
|
|
, corsRequestHeaders =
|
|
[ "app-version"
|
|
, "Accept"
|
|
, "Accept-Charset"
|
|
, "Accept-Encoding"
|
|
, "Accept-Language"
|
|
, "Accept-Ranges"
|
|
, "Age"
|
|
, "Allow"
|
|
, "Authorization"
|
|
, "Cache-Control"
|
|
, "Connection"
|
|
, "Content-Encoding"
|
|
, "Content-Language"
|
|
, "Content-Length"
|
|
, "Content-Location"
|
|
, "Content-MD5"
|
|
, "Content-Range"
|
|
, "Content-Type"
|
|
, "Date"
|
|
, "ETag"
|
|
, "Expect"
|
|
, "Expires"
|
|
, "From"
|
|
, "Host"
|
|
, "If-Match"
|
|
, "If-Modified-Since"
|
|
, "If-None-Match"
|
|
, "If-Range"
|
|
, "If-Unmodified-Since"
|
|
, "Last-Modified"
|
|
, "Location"
|
|
, "Max-Forwards"
|
|
, "Pragma"
|
|
, "Proxy-Authenticate"
|
|
, "Proxy-Authorization"
|
|
, "Range"
|
|
, "Referer"
|
|
, "Retry-After"
|
|
, "Server"
|
|
, "TE"
|
|
, "Trailer"
|
|
, "Transfer-Encoding"
|
|
, "Upgrade"
|
|
, "User-Agent"
|
|
, "Vary"
|
|
, "Via"
|
|
, "WWW-Authenticate"
|
|
, "Warning"
|
|
, "Content-Disposition"
|
|
, "MIME-Version"
|
|
, "Cookie"
|
|
, "Set-Cookie"
|
|
, "Origin"
|
|
, "Prefer"
|
|
, "Preference-Applied"
|
|
]
|
|
, corsIgnoreFailures = True
|
|
}
|
|
|
|
|
|
makeLogWare :: RegistryCtx -> IO Middleware
|
|
makeLogWare foundation =
|
|
mkRequestLogger
|
|
def
|
|
{ outputFormat =
|
|
if appDetailedRequestLogging $ appSettings foundation
|
|
then Detailed True
|
|
else CustomOutputFormatWithDetailsAndHeaders formatAsJSONWithHeaders
|
|
, destination = Logger $ loggerSet $ appLogger foundation
|
|
}
|
|
|
|
|
|
makeAuthWare :: RegistryCtx -> Middleware
|
|
makeAuthWare _ app req res = next
|
|
where
|
|
next :: IO ResponseReceived
|
|
next = app req res
|
|
|
|
|
|
-- | Warp settings for the given foundation value.
|
|
warpSettings :: AppPort -> RegistryCtx -> Settings
|
|
warpSettings port foundation =
|
|
setTimeout 90 $
|
|
setPort (fromIntegral port) $
|
|
setHost (appHost $ appSettings foundation) $
|
|
setOnException
|
|
( \_req e ->
|
|
when (defaultShouldDisplayException e) $
|
|
messageLoggerSource
|
|
foundation
|
|
(appLogger foundation)
|
|
$(qLocation >>= liftLoc)
|
|
"yesod"
|
|
LevelError
|
|
(toLogStr $ "Exception from Warp: " ++ show e)
|
|
)
|
|
(setHTTP2Disabled defaultSettings)
|
|
|
|
|
|
getAppSettings :: IO AppSettings
|
|
getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv
|
|
|
|
|
|
-- | The @main@ function for an executable running this site.
|
|
appMain :: IO ()
|
|
appMain = do
|
|
hSetBuffering stdout LineBuffering
|
|
-- Get the settings from all relevant sources
|
|
settings <-
|
|
loadYamlSettingsArgs
|
|
-- fall back to compile-time values, set to [] to require values at runtime
|
|
[configSettingsYmlValue]
|
|
-- allow environment variables to override
|
|
useEnv
|
|
|
|
-- Generate the foundation from the settings
|
|
makeFoundation settings >>= startApp
|
|
|
|
|
|
startApp :: RegistryCtx -> IO ()
|
|
startApp foundation = do
|
|
when (sslAuto . appSettings $ foundation) $ do
|
|
-- set up ssl certificates
|
|
runLog $ $logInfo "Setting up SSL"
|
|
_ <- setupSsl $ appSettings foundation
|
|
runLog $ $logInfo "SSL Setup Complete"
|
|
|
|
-- certbot renew loop
|
|
void . forkIO $
|
|
forever $
|
|
flip runReaderT foundation $ do
|
|
shouldRenew <- doesSslNeedRenew
|
|
runLog $ $logInfo [i|Checking if SSL Certs should be renewed: #{shouldRenew}|]
|
|
when shouldRenew $ do
|
|
runLog $ $logInfo "Renewing SSL Certs."
|
|
renewSslCerts
|
|
liftIO $ restartWeb foundation
|
|
liftIO $ sleep 86_400
|
|
|
|
startWeb foundation
|
|
where
|
|
runLog :: MonadIO m => LoggingT m a -> m a
|
|
runLog a = runLoggingT a (messageLoggerSource foundation (appLogger foundation))
|
|
|
|
|
|
startWeb :: RegistryCtx -> IO ()
|
|
startWeb foundation = do
|
|
app <- makeApplication foundation
|
|
startWeb' app
|
|
where
|
|
startWeb' app = do
|
|
let AppSettings{..} = appSettings foundation
|
|
runLog $ $logInfo [i|Launching Tor Web Server on port #{torPort}|]
|
|
torAction <- async $ runSettings (warpSettings torPort foundation) app
|
|
runLog $ $logInfo [i|Launching Web Server on port #{appPort}|]
|
|
action <-
|
|
async $
|
|
if sslAuto
|
|
then runTLS (tlsSettings sslCertLocation sslKeyLocation) (warpSettings appPort foundation) app
|
|
else runSettings (warpSettings appPort foundation) app
|
|
|
|
setWebProcessThreadId (asyncThreadId action, asyncThreadId torAction) foundation
|
|
res <- waitEitherCatchCancel action torAction
|
|
case res of
|
|
Left clearRes -> case clearRes of
|
|
Left e -> do
|
|
putStr @Text "Clearnet ServerError: "
|
|
print e
|
|
Right _ -> do
|
|
putStrLn @Text "Clearnet Server Exited"
|
|
void $ swapMVar (appShouldRestartWeb foundation) True
|
|
Right torRes -> case torRes of
|
|
Left e -> do
|
|
putStr @Text "Tor ServerError: "
|
|
print e
|
|
Right _ -> do
|
|
putStrLn @Text "Tor Server Exited"
|
|
void $ swapMVar (appShouldRestartWeb foundation) True
|
|
shouldRestart <- takeMVar (appShouldRestartWeb foundation)
|
|
when shouldRestart $ do
|
|
putMVar (appShouldRestartWeb foundation) False
|
|
runLog $ $logInfo "Restarting Web Server"
|
|
startWeb' app
|
|
runLog a = runLoggingT a (messageLoggerSource foundation (appLogger foundation))
|
|
|
|
|
|
restartWeb :: RegistryCtx -> IO ()
|
|
restartWeb foundation = do
|
|
void $ swapMVar (appShouldRestartWeb foundation) True
|
|
shutdownWeb foundation
|
|
|
|
|
|
shutdownAll :: [ThreadId] -> IO ()
|
|
shutdownAll threadIds = do
|
|
for_ threadIds killThread
|
|
exitImmediately ExitSuccess
|
|
|
|
|
|
-- Careful, you should always spawn this within forkIO so as to avoid accidentally killing the running process
|
|
shutdownWeb :: RegistryCtx -> IO ()
|
|
shutdownWeb RegistryCtx{..} = do
|
|
threadIds <- takeMVar appWebServerThreadId
|
|
void $ both killThread threadIds
|
|
|
|
|
|
--------------------------------------------------------------
|
|
-- Functions for DevelMain.hs (a way to run the RegistryCtx from GHCi)
|
|
--------------------------------------------------------------
|
|
|
|
getApplicationRepl :: IO (Int, RegistryCtx, Application)
|
|
getApplicationRepl = do
|
|
settings <- getAppSettings
|
|
foundation <- getAppSettings >>= makeFoundation
|
|
wsettings <- getDevSettings $ warpSettings (appPort settings) foundation
|
|
app1 <- makeApplication foundation
|
|
return (getPort wsettings, foundation, app1)
|
|
|
|
|
|
shutdownApp :: RegistryCtx -> IO ()
|
|
shutdownApp _ = return ()
|
|
|
|
|
|
-- | For yesod devel, return the Warp settings and WAI Application.
|
|
getApplicationDev :: AppPort -> IO (Settings, Application)
|
|
getApplicationDev port = do
|
|
settings <- getAppSettings
|
|
foundation <- makeFoundation settings
|
|
app <- makeApplication foundation
|
|
wsettings <- getDevSettings $ warpSettings port foundation
|
|
return (wsettings, app)
|
|
|
|
|
|
-- | main function for use by yesod devel
|
|
develMain :: IO ()
|
|
develMain = do
|
|
settings <- getAppSettings
|
|
develMainHelper $ getApplicationDev $ appPort settings
|
|
|
|
|
|
---------------------------------------------
|
|
-- Functions for use in development with GHCi
|
|
---------------------------------------------
|
|
|
|
-- | Run a handler
|
|
handler :: Handler a -> IO a
|
|
handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h
|
|
|
|
|
|
-- | Run DB queries
|
|
db :: ReaderT SqlBackend (HandlerFor RegistryCtx) a -> IO a
|
|
db = handler . runDB
|