module Snap.Internal.Http.Server.Config where
import Blaze.ByteString.Builder
import Blaze.ByteString.Builder.Char8
import Control.Exception (SomeException)
import Control.Monad
import qualified Data.ByteString.Char8 as B
import Data.ByteString (ByteString)
import Data.Char
import Data.Function
import Data.List
import Data.Maybe
import Data.Monoid
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Typeable
import Network(Socket)
#if !MIN_VERSION_base(4,6,0)
import Prelude hiding (catch)
#endif
import Snap.Core
import Snap.Iteratee ((>==>), enumBuilder)
import Snap.Internal.Debug (debug)
import Snap.Util.Proxy
import System.Console.GetOpt
import System.Environment hiding (getEnv)
#ifndef PORTABLE
import System.Posix.Env
#endif
import System.Exit
import System.IO
import Snap.Internal.Http.Server (requestErrorMessage)
data ConfigBackend = ConfigSimpleBackend
| ConfigLibEvBackend
deriving (Show, Eq)
data ConfigLog = ConfigNoLog
| ConfigFileLog FilePath
| ConfigIoLog (ByteString -> IO ())
instance Show ConfigLog where
show ConfigNoLog = "no log"
show (ConfigFileLog f) = "log to file " ++ show f
show (ConfigIoLog _) = "custom logging handler"
data Config m a = Config
{ hostname :: Maybe ByteString
, accessLog :: Maybe ConfigLog
, errorLog :: Maybe ConfigLog
, locale :: Maybe String
, port :: Maybe Int
, bind :: Maybe ByteString
, sslport :: Maybe Int
, sslbind :: Maybe ByteString
, sslcert :: Maybe FilePath
, sslkey :: Maybe FilePath
, compression :: Maybe Bool
, verbose :: Maybe Bool
, errorHandler :: Maybe (SomeException -> m ())
, defaultTimeout :: Maybe Int
, other :: Maybe a
, backend :: Maybe ConfigBackend
, proxyType :: Maybe ProxyType
, startupHook :: Maybe (StartupInfo m a -> IO ())
}
#if MIN_VERSION_base(4,7,0)
deriving Typeable
#else
configTyCon :: TyCon
configTyCon = mkTyCon "Snap.Http.Server.Config.Config"
instance (Typeable1 m) => Typeable1 (Config m) where
typeOf1 _ = mkTyConApp configTyCon [typeOf1 (undefined :: m ())]
#endif
instance Show (Config m a) where
show c = unlines [ "Config:"
, "hostname: " ++ _hostname
, "accessLog: " ++ _accessLog
, "errorLog: " ++ _errorLog
, "locale: " ++ _locale
, "port: " ++ _port
, "bind: " ++ _bind
, "sslport: " ++ _sslport
, "sslbind: " ++ _sslbind
, "sslcert: " ++ _sslcert
, "sslkey: " ++ _sslkey
, "compression: " ++ _compression
, "verbose: " ++ _verbose
, "defaultTimeout: " ++ _defaultTimeout
, "backend: " ++ _backend
, "proxyType: " ++ _proxyType
]
where
_hostname = show $ hostname c
_accessLog = show $ accessLog c
_errorLog = show $ errorLog c
_locale = show $ locale c
_port = show $ port c
_bind = show $ bind c
_sslport = show $ sslport c
_sslbind = show $ sslbind c
_sslcert = show $ sslcert c
_sslkey = show $ sslkey c
_compression = show $ compression c
_verbose = show $ verbose c
_defaultTimeout = show $ defaultTimeout c
_backend = show $ backend c
_proxyType = show $ proxyType c
emptyConfig :: Config m a
emptyConfig = mempty
instance Monoid (Config m a) where
mempty = Config
{ hostname = Nothing
, accessLog = Nothing
, errorLog = Nothing
, locale = Nothing
, port = Nothing
, bind = Nothing
, sslport = Nothing
, sslbind = Nothing
, sslcert = Nothing
, sslkey = Nothing
, compression = Nothing
, verbose = Nothing
, errorHandler = Nothing
, defaultTimeout = Nothing
, other = Nothing
, backend = Nothing
, proxyType = Nothing
, startupHook = Nothing
}
a `mappend` b = Config
{ hostname = ov hostname
, accessLog = ov accessLog
, errorLog = ov errorLog
, locale = ov locale
, port = ov port
, bind = ov bind
, sslport = ov sslport
, sslbind = ov sslbind
, sslcert = ov sslcert
, sslkey = ov sslkey
, compression = ov compression
, verbose = ov verbose
, errorHandler = ov errorHandler
, defaultTimeout = ov defaultTimeout
, other = ov other
, backend = ov backend
, proxyType = ov proxyType
, startupHook = ov startupHook
}
where
ov f = getLast $! (mappend `on` (Last . f)) a b
defaultConfig :: MonadSnap m => Config m a
defaultConfig = mempty
{ hostname = Just "localhost"
, accessLog = Just $ ConfigFileLog "log/access.log"
, errorLog = Just $ ConfigFileLog "log/error.log"
, locale = Just "en_US"
, compression = Just True
, verbose = Just True
, errorHandler = Just defaultErrorHandler
, bind = Just "0.0.0.0"
, sslbind = Just "0.0.0.0"
, sslcert = Just "cert.pem"
, sslkey = Just "key.pem"
, defaultTimeout = Just 60
}
getHostname :: Config m a -> Maybe ByteString
getHostname = hostname
getAccessLog :: Config m a -> Maybe ConfigLog
getAccessLog = accessLog
getErrorLog :: Config m a -> Maybe ConfigLog
getErrorLog = errorLog
getLocale :: Config m a -> Maybe String
getLocale = locale
getPort :: Config m a -> Maybe Int
getPort = port
getBind :: Config m a -> Maybe ByteString
getBind = bind
getSSLPort :: Config m a -> Maybe Int
getSSLPort = sslport
getSSLBind :: Config m a -> Maybe ByteString
getSSLBind = sslbind
getSSLCert :: Config m a -> Maybe FilePath
getSSLCert = sslcert
getSSLKey :: Config m a -> Maybe FilePath
getSSLKey = sslkey
getCompression :: Config m a -> Maybe Bool
getCompression = compression
getVerbose :: Config m a -> Maybe Bool
getVerbose = verbose
getErrorHandler :: Config m a -> Maybe (SomeException -> m ())
getErrorHandler = errorHandler
getDefaultTimeout :: Config m a -> Maybe Int
getDefaultTimeout = defaultTimeout
getOther :: Config m a -> Maybe a
getOther = other
getBackend :: Config m a -> Maybe ConfigBackend
getBackend = backend
getProxyType :: Config m a -> Maybe ProxyType
getProxyType = proxyType
getStartupHook :: Config m a -> Maybe (StartupInfo m a -> IO ())
getStartupHook = startupHook
setHostname :: ByteString -> Config m a -> Config m a
setHostname x c = c { hostname = Just x }
setAccessLog :: ConfigLog -> Config m a -> Config m a
setAccessLog x c = c { accessLog = Just x }
setErrorLog :: ConfigLog -> Config m a -> Config m a
setErrorLog x c = c { errorLog = Just x }
setLocale :: String -> Config m a -> Config m a
setLocale x c = c { locale = Just x }
setPort :: Int -> Config m a -> Config m a
setPort x c = c { port = Just x }
setBind :: ByteString -> Config m a -> Config m a
setBind x c = c { bind = Just x }
setSSLPort :: Int -> Config m a -> Config m a
setSSLPort x c = c { sslport = Just x }
setSSLBind :: ByteString -> Config m a -> Config m a
setSSLBind x c = c { sslbind = Just x }
setSSLCert :: FilePath -> Config m a -> Config m a
setSSLCert x c = c { sslcert = Just x }
setSSLKey :: FilePath -> Config m a -> Config m a
setSSLKey x c = c { sslkey = Just x }
setCompression :: Bool -> Config m a -> Config m a
setCompression x c = c { compression = Just x }
setVerbose :: Bool -> Config m a -> Config m a
setVerbose x c = c { verbose = Just x }
setErrorHandler :: (SomeException -> m ()) -> Config m a -> Config m a
setErrorHandler x c = c { errorHandler = Just x }
setDefaultTimeout :: Int -> Config m a -> Config m a
setDefaultTimeout x c = c { defaultTimeout = Just x }
setOther :: a -> Config m a -> Config m a
setOther x c = c { other = Just x }
setBackend :: ConfigBackend -> Config m a -> Config m a
setBackend x c = c { backend = Just x }
setProxyType :: ProxyType -> Config m a -> Config m a
setProxyType x c = c { proxyType = Just x }
setStartupHook :: (StartupInfo m a -> IO ()) -> Config m a -> Config m a
setStartupHook x c = c { startupHook = Just x }
data StartupInfo m a = StartupInfo
{ startupHookConfig :: Config m a
, startupHookSockets :: [Socket]
}
emptyStartupInfo :: StartupInfo m a
emptyStartupInfo = StartupInfo emptyConfig []
getStartupSockets :: StartupInfo m a -> [Socket]
getStartupSockets = startupHookSockets
getStartupConfig :: StartupInfo m a -> Config m a
getStartupConfig = startupHookConfig
setStartupSockets :: [Socket] -> StartupInfo m a -> StartupInfo m a
setStartupSockets x c = c { startupHookSockets = x }
setStartupConfig :: Config m a -> StartupInfo m a -> StartupInfo m a
setStartupConfig x c = c { startupHookConfig = x }
completeConfig :: (MonadSnap m) => Config m a -> IO (Config m a)
completeConfig config = do
when noPort $ hPutStrLn stderr
"no port specified, defaulting to port 8000"
return $! cfg `mappend` cfg'
where
cfg = defaultConfig `mappend` config
sslVals = map ($ cfg) [ isJust . getSSLPort
, isJust . getSSLBind
, isJust . getSSLKey
, isJust . getSSLCert ]
sslValid = and sslVals
noPort = isNothing (getPort cfg) && not sslValid
cfg' = emptyConfig { port = if noPort then Just 8000 else Nothing }
bsFromString :: String -> ByteString
bsFromString = T.encodeUtf8 . T.pack
toString :: ByteString -> String
toString = T.unpack . T.decodeUtf8
optDescrs :: MonadSnap m =>
Config m a
-> [OptDescr (Maybe (Config m a))]
optDescrs defaults =
[ Option [] ["hostname"]
(ReqArg (Just . setConfig setHostname . bsFromString) "NAME")
$ "local hostname" ++ defaultC getHostname
, Option ['b'] ["address"]
(ReqArg (\s -> Just $ mempty { bind = Just $ bsFromString 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 $ bsFromString 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 s}) "PATH")
$ "path to ssl certificate in PEM format" ++ defaultO sslcert
, Option [] ["ssl-key"]
(ReqArg (\s -> Just $ mempty { sslkey = Just s}) "PATH")
$ "path to ssl private key in PEM format" ++ defaultO sslkey
, Option [] ["access-log"]
(ReqArg (Just . setConfig setAccessLog . ConfigFileLog) "PATH")
$ "access log" ++ (defaultC $ getAccessLog)
, Option [] ["error-log"]
(ReqArg (Just . setConfig setErrorLog . ConfigFileLog) "PATH")
$ "error log" ++ (defaultC $ getErrorLog)
, Option [] ["no-access-log"]
(NoArg $ Just $ setConfig setAccessLog ConfigNoLog)
$ "don't have an access log"
, Option [] ["no-error-log"]
(NoArg $ Just $ setConfig setErrorLog ConfigNoLog)
$ "don't have an error log"
, Option ['c'] ["compression"]
(NoArg $ Just $ setConfig setCompression True)
$ "use gzip compression on responses" ++
defaultB getCompression "compressed" "uncompressed"
, Option ['t'] ["timeout"]
(ReqArg (\t -> Just $ mempty {
defaultTimeout = Just $ read t
}) "SECS")
$ "set default timeout in seconds" ++ defaultC defaultTimeout
, Option [] ["no-compression"]
(NoArg $ Just $ setConfig setCompression False)
$ "serve responses uncompressed" ++
defaultB compression "compressed" "uncompressed"
, Option ['v'] ["verbose"]
(NoArg $ Just $ setConfig setVerbose True)
$ "print server status updates to stderr" ++
defaultC getVerbose
, Option ['q'] ["quiet"]
(NoArg $ Just $ setConfig setVerbose False)
$ "do not print anything to stderr" ++
defaultB getVerbose "verbose" "quiet"
, Option [] ["proxy"]
(ReqArg (\t -> Just $ setConfig setProxyType $ read t)
"X_Forwarded_For")
$ concat [ "Set --proxy=X_Forwarded_For if your snap application "
, "is behind an HTTP reverse proxy to ensure that "
, "rqRemoteAddr is set properly."
, defaultC getProxyType ]
, Option ['h'] ["help"]
(NoArg Nothing)
$ "display this help and exit"
]
where
setConfig f c = f c mempty
conf = defaultConfig `mappend` defaults
defaultB f y n = maybe "" (\b -> ", default " ++ if b
then y
else n) $ f conf
defaultC f = maybe "" ((", default " ++) . show) $ f conf
defaultO f = maybe ", default off" ((", default " ++) . show) $ f conf
defaultErrorHandler :: MonadSnap m => SomeException -> m ()
defaultErrorHandler e = do
debug "Snap.Http.Server.Config errorHandler:"
req <- getRequest
let sm = smsg req
debug $ toString sm
logError sm
finishWith $ setContentType "text/plain; charset=utf-8"
. setContentLength (fromIntegral $ B.length msg)
. setResponseStatus 500 "Internal Server Error"
. modifyResponseBody
(>==> enumBuilder (fromByteString msg))
$ emptyResponse
where
smsg req = toByteString $ requestErrorMessage req e
msg = toByteString msgB
msgB = mconcat [
fromByteString "A web handler threw an exception. Details:\n"
, fromShow e
]
commandLineConfig :: MonadSnap m
=> Config m a
-> IO (Config m a)
commandLineConfig defaults = extendedCommandLineConfig (optDescrs defaults) f defaults
where
f = undefined
extendedCommandLineConfig :: MonadSnap m
=> [OptDescr (Maybe (Config m a))]
-> (a -> a -> a)
-> Config m a
-> IO (Config m a)
extendedCommandLineConfig opts combiningFunction defaults = do
args <- getArgs
prog <- getProgName
result <- either (usage prog)
return
(case getOpt Permute opts args of
(f, _, [] ) -> maybe (Left []) Right $
fmap (foldl' combine mempty) $
sequence f
(_, _, errs) -> Left errs)
#ifndef PORTABLE
lang <- getEnv "LANG"
completeConfig $ mconcat [defaults,
mempty {locale = fmap upToUtf8 lang},
result]
#else
completeConfig $ mconcat [defaults, result]
#endif
where
usage prog errs = do
let hdr = "Usage:\n " ++ prog ++ " [OPTION...]\n\nOptions:"
let msg = concat errs ++ usageInfo hdr opts
hPutStrLn stderr msg
exitFailure
#ifndef PORTABLE
upToUtf8 = takeWhile $ \c -> isAlpha c || '_' == c
#endif
combine !a !b = a `mappend` b `mappend` newOther
where
combined = do
x <- getOther a
y <- getOther b
return $! combiningFunction x y
newOther = mempty { other = combined }
fmapArg :: (a -> b) -> ArgDescr a -> ArgDescr b
fmapArg f (NoArg a) = NoArg (f a)
fmapArg f (ReqArg g s) = ReqArg (f . g) s
fmapArg f (OptArg g s) = OptArg (f . g) s
fmapOpt :: (a -> b) -> OptDescr a -> OptDescr b
fmapOpt f (Option s l d e) = Option s l (fmapArg f d) e