module Snap.Http.Server.Config
( Config
, ConfigListen(..)
, ConfigBackend(..)
, emptyConfig
, defaultConfig
, completeConfig
, commandLineConfig
, getHostname
, getListen
, getAccessLog
, getErrorLog
, getLocale
, getBackend
, getCompression
, getVerbose
, getErrorHandler
, getDefaultTimeout
, getOther
, setHostname
, addListen
, setAccessLog
, setErrorLog
, setLocale
, setBackend
, setCompression
, setVerbose
, setErrorHandler
, setDefaultTimeout
, setOther
) where
import Blaze.ByteString.Builder
import Control.Exception (SomeException)
import Control.Monad
import qualified Data.ByteString.UTF8 as U
import qualified Data.ByteString.Char8 as B
import Data.ByteString (ByteString)
import Data.Char
import Data.List
import Data.Monoid
import Prelude hiding (catch)
import Snap.Types
import Snap.Iteratee ((>==>), enumBuilder)
import System.Console.GetOpt
import System.Environment hiding (getEnv)
#ifndef PORTABLE
import System.Posix.Env
#endif
import System.Exit
import System.IO
data ConfigListen = ListenHttp ByteString Int
| ListenHttps ByteString Int FilePath FilePath
instance Show ConfigListen where
show (ListenHttp b p) = "http://" ++ U.toString b ++ ":" ++ show p
show (ListenHttps b p c k) =
"https://" ++ U.toString b ++ ":" ++ show p ++
" (cert = " ++ show c ++ ", key = " ++ show k ++ ")"
data ConfigBackend = ConfigSimpleBackend
| ConfigLibEvBackend
deriving (Eq,Show)
data MonadSnap m => Config m a = Config
{ hostname :: Maybe ByteString
, listen :: [ConfigListen]
, accessLog :: Maybe (Maybe FilePath)
, errorLog :: Maybe (Maybe FilePath)
, locale :: Maybe String
, backend :: Maybe ConfigBackend
, compression :: Maybe Bool
, verbose :: Maybe Bool
, errorHandler :: Maybe (SomeException -> m ())
, defaultTimeout :: Maybe Int
, other :: Maybe a
}
instance MonadSnap m => Show (Config m a) where
show c = "Config {" ++ concat (intersperse ", " $ filter (/="") $ map ($c)
[ showM "hostname" . hostname
, showL "listen" . listen
, showM "accessLog" . accessLog
, showM "errorLog" . errorLog
, showM "locale" . locale
, showM "backend" . backend
, showM "compression" . compression
, showM "verbose" . verbose
, showM "errorHandler" . fmap (const ()) . errorHandler
, showM "defaultTimeout" . fmap (const ()) . defaultTimeout
]) ++ "}"
where
showM s = maybe "" ((++) (s ++ " = ") . show)
showL s l = s ++ " = " ++ show l
emptyConfig :: MonadSnap m => Config m a
emptyConfig = mempty
instance MonadSnap m => Monoid (Config m a) where
mempty = Config
{ hostname = Nothing
, listen = []
, accessLog = Nothing
, errorLog = Nothing
, locale = Nothing
, backend = Nothing
, compression = Nothing
, verbose = Nothing
, errorHandler = Nothing
, defaultTimeout = Nothing
, other = Nothing
}
a `mappend` b = Config
{ hostname = (hostname b) `mplus` (hostname a)
, listen = (listen b) ++ (listen a)
, accessLog = (accessLog b) `mplus` (accessLog a)
, errorLog = (errorLog b) `mplus` (errorLog a)
, locale = (locale b) `mplus` (locale a)
, backend = (backend b) `mplus` (backend a)
, compression = (compression b) `mplus` (compression a)
, verbose = (verbose b) `mplus` (verbose a)
, errorHandler = (errorHandler b) `mplus` (errorHandler a)
, defaultTimeout = (defaultTimeout b) `mplus` (defaultTimeout a)
, other = (other b) `mplus` (other a)
}
defaultConfig :: MonadSnap m => Config m a
defaultConfig = Config
{ hostname = Just "localhost"
, listen = []
, accessLog = Just $ Just "log/access.log"
, errorLog = Just $ Just "log/error.log"
, locale = Just "en_US"
, backend = Nothing
, compression = Just True
, verbose = Just True
, errorHandler = Just $ \e -> do
let err = U.fromString $ show e
msg = mappend "A web handler threw an exception. Details:\n" err
finishWith $ setContentType "text/plain; charset=utf-8"
. setContentLength (fromIntegral $ B.length msg)
. setResponseStatus 500 "Internal Server Error"
. modifyResponseBody
(>==> enumBuilder (fromByteString msg))
$ emptyResponse
, defaultTimeout = Just 60
, other = Nothing
}
completeConfig :: MonadSnap m => Config m a -> Config m a
completeConfig c = case listen c' of
[] -> addListen (ListenHttp "0.0.0.0" 8000) c'
_ -> c'
where c' = mappend defaultConfig c
data MonadSnap m => OptionData m a = OptionData
{ config :: Config m a
, bind :: Maybe ByteString
, port :: Maybe Int
, sslbind :: Maybe ByteString
, sslport :: Maybe Int
, sslcert :: Maybe FilePath
, sslkey :: Maybe FilePath
, tout :: Maybe Int
}
instance MonadSnap m => Monoid (OptionData m a) where
mempty = OptionData
{ config = mempty
, bind = Nothing
, port = Nothing
, sslbind = Nothing
, sslport = Nothing
, sslcert = Nothing
, sslkey = Nothing
, tout = Nothing
}
a `mappend` b = OptionData
{ config = (config b) `mappend` (config a)
, bind = (bind b) `mplus` (bind a)
, port = (port b) `mplus` (port a)
, sslbind = (sslbind b) `mplus` (sslbind a)
, sslport = (sslport b) `mplus` (sslport a)
, sslcert = (sslcert b) `mplus` (sslcert a)
, sslkey = (sslkey b) `mplus` (sslkey a)
, tout = (tout b) `mplus` (tout a)
}
defaultOptions :: MonadSnap m => OptionData m a
defaultOptions = OptionData
{ config = defaultConfig
, bind = Just "0.0.0.0"
, port = Just 8000
, sslbind = Just "0.0.0.0"
, sslport = Nothing
, sslcert = Just "cert.pem"
, sslkey = Just "key.pem"
, tout = Just 60
}
optionsToConfig :: MonadSnap m => OptionData m a -> Config m a
optionsToConfig o = mconcat $ [config o] ++ http ++ https ++ [tmOut]
where lhttp = maybe2 [] ListenHttp (bind o) (port o)
lhttps = maybe4 [] ListenHttps (sslbind o)
(sslport o)
(sslcert o)
(sslkey o)
http = map (flip addListen mempty) lhttp
https = map (flip addListen mempty) lhttps
maybe2 _ f (Just a) (Just b) = [f a b]
maybe2 d _ _ _ = d
maybe4 _ f (Just a) (Just b) (Just c) (Just d) = [f a b c d]
maybe4 d _ _ _ _ _ = d
tmOut = maybe mempty
(\t -> mempty { defaultTimeout = Just t })
(tout o)
configToOptions :: MonadSnap m => Config m a -> OptionData m a
configToOptions c = OptionData
{ config = c
, bind = Nothing
, port = Nothing
, sslbind = Nothing
, sslport = Nothing
, sslcert = Nothing
, sslkey = Nothing
, tout = (defaultTimeout c)
}
options :: MonadSnap m
=> OptionData m a
-> [OptDescr (Maybe (OptionData m a))]
options defaults =
[ Option [] ["hostname"]
(ReqArg (Just . setConfig setHostname . U.fromString) "NAME")
$ "local hostname" ++ defaultC getHostname
, Option ['b'] ["address"]
(ReqArg (\s -> Just $ mempty { bind = Just $ U.fromString s })
"ADDRESS")
$ "address to bind to" ++ defaultO bind
, Option ['p'] ["port"]
(ReqArg (\s -> Just $ mempty { port = Just $ read s}) "PORT")
$ "port to listen on" ++ defaultO port
, Option [] ["ssl-address"]
(ReqArg (\s -> Just $ mempty { sslbind = Just $ U.fromString s })
"ADDRESS")
$ "ssl address to bind to" ++ defaultO sslbind
, Option [] ["ssl-port"]
(ReqArg (\s -> Just $ mempty { sslport = Just $ read s}) "PORT")
$ "ssl port to listen on" ++ defaultO sslport
, Option [] ["ssl-cert"]
(ReqArg (\s -> Just $ mempty { sslcert = Just $ read s}) "PATH")
$ "path to ssl certificate in PEM format" ++ defaultO sslcert
, Option [] ["ssl-key"]
(ReqArg (\s -> Just $ mempty { sslkey = Just $ read s}) "PATH")
$ "path to ssl private key in PEM format" ++ defaultO sslkey
, Option [] ["access-log"]
(ReqArg (Just . setConfig setAccessLog . Just) "PATH")
$ "access log" ++ (defaultC $ join . getAccessLog)
, Option [] ["error-log"]
(ReqArg (Just . setConfig setErrorLog . Just) "PATH")
$ "error log" ++ (defaultC $ join . getErrorLog)
, Option [] ["no-access-log"]
(NoArg $ Just $ setConfig setErrorLog Nothing)
$ "don't have an access log"
, Option [] ["no-error-log"]
(NoArg $ Just $ setConfig setAccessLog Nothing)
$ "don't have an error log"
, Option ['c'] ["compression"]
(NoArg $ Just $ setConfig setCompression True)
$ "use gzip compression on responses"
, Option ['t'] ["timeout"]
(ReqArg (\t -> Just $ mempty { tout = Just $ read t}) "SECS")
$ "set default timeout in seconds"
, Option [] ["no-compression"]
(NoArg $ Just $ setConfig setCompression False)
$ "serve responses uncompressed"
, Option ['v'] ["verbose"]
(NoArg $ Just $ setConfig setVerbose True)
$ "print server status updates to stderr"
, Option ['q'] ["quiet"]
(NoArg $ Just $ setConfig setVerbose False)
$ "do not print anything to stderr"
, Option ['h'] ["help"]
(NoArg Nothing)
$ "display this help and exit"
]
where
setConfig f c = configToOptions $ f c mempty
conf = completeConfig $ config defaults
opts = mappend defaultOptions defaults
defaultC f = maybe "" ((", default " ++) . show) $ f conf
defaultO f = maybe ", default off" ((", default " ++) . show) $ f opts
commandLineConfig :: MonadSnap m => Config m a -> IO (Config m a)
commandLineConfig defaults = do
args <- getArgs
prog <- getProgName
result <- either (usage prog) return $ case getOpt Permute opts args of
(f, _, [] ) -> maybe (Left []) Right $ fmap mconcat $ sequence f
(_, _, errs) -> Left errs
let result' = optionsToConfig $ mappend defaultOptions result
#ifndef PORTABLE
lang <- getEnv "LANG"
return $ mconcat [defaults, result', mempty {locale = fmap upToUtf8 lang}]
#else
return $ mconcat [defaults, result']
#endif
where
opts = options $ configToOptions defaults
usage prog errs = do
let hdr = "Usage:\n " ++ prog ++ " [OPTION...]\n\nOptions:"
let msg = concat errs ++ usageInfo hdr opts
hPutStrLn stderr msg
exitFailure
upToUtf8 = takeWhile $ \c -> isAlpha c || '_' == c
getHostname :: MonadSnap m => Config m a -> Maybe ByteString
getHostname = hostname
getListen :: MonadSnap m => Config m a -> [ConfigListen]
getListen = listen
getAccessLog :: MonadSnap m => Config m a -> Maybe (Maybe FilePath)
getAccessLog = accessLog
getErrorLog :: MonadSnap m => Config m a -> Maybe (Maybe FilePath)
getErrorLog = errorLog
getLocale :: MonadSnap m => Config m a -> Maybe String
getLocale = locale
getBackend :: MonadSnap m => Config m a -> Maybe ConfigBackend
getBackend = backend
getCompression :: MonadSnap m => Config m a -> Maybe Bool
getCompression = compression
getVerbose :: MonadSnap m => Config m a -> Maybe Bool
getVerbose = verbose
getErrorHandler :: MonadSnap m => Config m a -> Maybe (SomeException -> m ())
getErrorHandler = errorHandler
getOther :: MonadSnap m => Config m a -> Maybe a
getOther = other
getDefaultTimeout :: MonadSnap m => Config m a -> Maybe Int
getDefaultTimeout = defaultTimeout
setHostname :: MonadSnap m => ByteString -> Config m a -> Config m a
setHostname a m = m {hostname = Just a}
addListen :: MonadSnap m => ConfigListen -> Config m a -> Config m a
addListen a m = m {listen = a : listen m}
setAccessLog :: MonadSnap m => Maybe FilePath -> Config m a -> Config m a
setAccessLog a m = m {accessLog = Just a}
setErrorLog :: MonadSnap m => Maybe FilePath -> Config m a -> Config m a
setErrorLog a m = m {errorLog = Just a}
setLocale :: MonadSnap m => String -> Config m a -> Config m a
setLocale a m = m {locale = Just a}
setBackend :: MonadSnap m => ConfigBackend -> Config m a -> Config m a
setBackend a m = m { backend = Just a}
setCompression :: MonadSnap m => Bool -> Config m a -> Config m a
setCompression a m = m {compression = Just a}
setVerbose :: MonadSnap m => Bool -> Config m a -> Config m a
setVerbose a m = m {verbose = Just a}
setErrorHandler :: MonadSnap m => (SomeException -> m ()) -> Config m a
-> Config m a
setErrorHandler a m = m {errorHandler = Just a}
setOther :: MonadSnap m => a -> Config m a -> Config m a
setOther a m = m {other = Just a}
setDefaultTimeout :: MonadSnap m => Int -> Config m a -> Config m a
setDefaultTimeout t m = m {defaultTimeout = Just t}