{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Lib.WebServer where import Startlude hiding ( exp ) import Control.Monad.Logger import Data.Default import Data.IORef import Language.Haskell.TH.Syntax ( qLocation ) import Network.Wai import Network.Wai.Handler.Warp ( Settings , defaultSettings , defaultShouldDisplayException , runSettings , setHost , setOnException , setPort ) import Network.Wai.Middleware.Cors ( CorsResourcePolicy(..) , cors , simpleCorsResourcePolicy ) import Network.Wai.Middleware.RequestLogger ( Destination(Logger) , IPAddrSource(..) , OutputFormat(..) , destination , mkRequestLogger , outputFormat ) import Yesod.Core import Yesod.Core.Types hiding ( Logger ) import Auth import Foundation import Handler.Apps import Handler.Authenticate import Handler.Backups import Handler.Hosts import Handler.Icons import Handler.Login import Handler.Network import Handler.Notifications import Handler.PasswordUpdate import Handler.PowerOff import Handler.Register import Handler.SelfUpdate import Handler.SshKeys import Handler.Status import Handler.Wifi import Handler.V0 import Settings import Network.HTTP.Types.Header ( hOrigin ) import Data.List (lookup) -- 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 "AgentCtx" resourcesAgentCtx instance YesodSubDispatch Auth AgentCtx where yesodSubDispatch = $(mkYesodSubDispatch resourcesAuth) 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 } -- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and -- applying some additional middlewares. makeApplication :: AgentCtx -> IO Application makeApplication foundation = do logWare <- makeLogWare foundation -- Create the WAI application and apply middlewares appPlain <- toWaiAppPlain foundation pure . logWare . cors dynamicCorsResourcePolicy . defaultMiddlewaresNoLogging $ appPlain startWeb :: AgentCtx -> IO () startWeb foundation = do app <- makeApplication foundation putStrLn @Text $ "Launching Web Server on port " <> show (appPort $ appSettings foundation) action <- async $ runSettings (warpSettings foundation) app setWebProcessThreadId (asyncThreadId action) foundation wait action shutdownAll :: [ThreadId] -> IO () shutdownAll threadIds = do for_ threadIds killThread exitSuccess shutdownWeb :: AgentCtx -> IO () shutdownWeb AgentCtx {..} = do mThreadId <- readIORef appWebServerThreadId for_ mThreadId $ \tid -> do killThread tid writeIORef appWebServerThreadId Nothing makeLogWare :: AgentCtx -> IO Middleware makeLogWare 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 } -- | Warp settings for the given foundation value. warpSettings :: AgentCtx -> Settings warpSettings foundation = setPort (fromIntegral . 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