{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Snap.Http.Server
( simpleHttpServe
, httpServe
, quickHttpServe
, snapServerVersion
, setUnicodeLocale
, rawHttpServe
, module Snap.Http.Server.Config
) where
import Control.Applicative ((<$>), (<|>))
import Control.Concurrent (killThread, newEmptyMVar, newMVar, putMVar, readMVar, withMVar)
import Control.Concurrent.Extended (forkIOLabeledWithUnmaskBs)
import Control.Exception (SomeException, bracket, catch, finally, mask, mask_)
import qualified Control.Exception.Lifted as L
import Control.Monad (liftM, when)
import Control.Monad.Trans (MonadIO)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import Data.Maybe (catMaybes, fromJust, fromMaybe)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Version (showVersion)
import Data.Word (Word64)
import Network.Socket (Socket, sClose)
import Prelude (Bool (..), Eq (..), IO, Maybe (..), Monad (..), Show (..), String, const, flip, fst, id, mapM, mapM_, maybe, snd, unzip3, zip, ($), ($!), (++), (.))
import System.IO (hFlush, hPutStrLn, stderr)
#ifndef PORTABLE
import System.Posix.Env
#endif
import Data.ByteString.Builder (Builder, toLazyByteString)
import qualified Paths_snap_server as V
import Snap.Core (MonadSnap (..), Request, Response, Snap, rqClientAddr, rqHeaders, rqMethod, rqURI, rqVersion, rspStatus)
import Snap.Http.Server.Config
import qualified Snap.Http.Server.Types as Ty
import Snap.Internal.Debug (debug)
import Snap.Internal.Http.Server.Config (ProxyType (..), emptyStartupInfo, setStartupConfig, setStartupSockets)
import Snap.Internal.Http.Server.Session (httpAcceptLoop, snapToServerHandler)
import qualified Snap.Internal.Http.Server.Socket as Sock
import qualified Snap.Internal.Http.Server.TLS as TLS
import Snap.Internal.Http.Server.Types (AcceptFunc, ServerConfig, ServerHandler)
import qualified Snap.Types.Headers as H
import Snap.Util.GZip (withCompression)
import Snap.Util.Proxy (behindProxy)
import qualified Snap.Util.Proxy as Proxy
import System.FastLogger (combinedLogEntry, logMsg, newLoggerWithCustomErrorFunction, stopLogger, timestampedLogEntry)
snapServerVersion :: ByteString
snapServerVersion = S.pack $! showVersion V.version
rawHttpServe :: ServerHandler s
-> ServerConfig s
-> [AcceptFunc]
-> IO ()
rawHttpServe h cfg loops = do
mvars <- mapM (const newEmptyMVar) loops
mask $ \restore -> bracket (mapM runLoop $ mvars `zip` loops)
(\mvTids -> do
mapM_ (killThread . snd) mvTids
mapM_ (readMVar . fst) mvTids)
(const $ restore $ mapM_ readMVar mvars)
where
runLoop (mvar, loop) = do
tid <- forkIOLabeledWithUnmaskBs
"snap-server http master thread" $
\r -> (r $ httpAcceptLoop h cfg loop) `finally` putMVar mvar ()
return (mvar, tid)
simpleHttpServe :: MonadSnap m => Config m a -> Snap () -> IO ()
simpleHttpServe config handler = do
conf <- completeConfig config
let output = when (fromJust $ getVerbose conf) . hPutStrLn stderr
(descrs, sockets, afuncs) <- unzip3 <$> listeners conf
mapM_ (output . ("Listening on " ++) . S.unpack) descrs
go conf sockets afuncs `finally` (mask_ $ do
output "\nShutting down.."
mapM_ (eatException . sClose) sockets)
where
eatException :: IO a -> IO ()
eatException act =
let r0 = return $! ()
in (act >> r0) `catch` \(_::SomeException) -> r0
debugE :: (MonadIO m) => ByteString -> m ()
debugE s = debug $ "Error: " ++ S.unpack s
logE :: Maybe (ByteString -> IO ()) -> Builder -> IO ()
logE elog b = let x = S.concat $ L.toChunks $ toLazyByteString b
in (maybe debugE (\l s -> debugE s >> logE' l s) elog) x
logE' :: (ByteString -> IO ()) -> ByteString -> IO ()
logE' logger s = (timestampedLogEntry s) >>= logger
logA :: Maybe (ByteString -> IO ())
-> Request
-> Response
-> Word64
-> IO ()
logA alog = maybe (\_ _ _ -> return $! ()) logA' alog
logA' logger req rsp cl = do
let hdrs = rqHeaders req
let host = rqClientAddr req
let user = Nothing
let (v, v') = rqVersion req
let ver = S.concat [ "HTTP/", bshow v, ".", bshow v' ]
let method = bshow (rqMethod req)
let reql = S.intercalate " " [ method, rqURI req, ver ]
let status = rspStatus rsp
let referer = H.lookup "referer" hdrs
let userAgent = fromMaybe "-" $ H.lookup "user-agent" hdrs
msg <- combinedLogEntry host user reql status cl referer userAgent
logger msg
go conf sockets afuncs = do
let tout = fromMaybe 60 $ getDefaultTimeout conf
let shandler = snapToServerHandler handler
setUnicodeLocale $ fromJust $ getLocale conf
withLoggers (fromJust $ getAccessLog conf)
(fromJust $ getErrorLog conf) $ \(alog, elog) -> do
let scfg = Ty.setDefaultTimeout tout .
Ty.setLocalHostname (fromJust $ getHostname conf) .
Ty.setLogAccess (logA alog) .
Ty.setLogError (logE elog) $
Ty.emptyServerConfig
maybe (return $! ())
($ mkStartupInfo sockets conf)
(getStartupHook conf)
rawHttpServe shandler scfg afuncs
mkStartupInfo sockets conf =
setStartupSockets sockets $
setStartupConfig conf emptyStartupInfo
maybeSpawnLogger f (ConfigFileLog fp) =
liftM Just $ newLoggerWithCustomErrorFunction f fp
maybeSpawnLogger _ _ = return Nothing
maybeIoLog (ConfigIoLog a) = Just a
maybeIoLog _ = Nothing
withLoggers afp efp act =
bracket (do mvar <- newMVar ()
let f s = withMVar mvar
(const $ S.hPutStr stderr s >> hFlush stderr)
alog <- maybeSpawnLogger f afp
elog <- maybeSpawnLogger f efp
return (alog, elog))
(\(alog, elog) -> do
maybe (return ()) stopLogger alog
maybe (return ()) stopLogger elog)
(\(alog, elog) -> act ( liftM logMsg alog <|> maybeIoLog afp
, liftM logMsg elog <|> maybeIoLog efp))
{-# INLINE simpleHttpServe #-}
listeners :: Config m a -> IO [(ByteString, Socket, AcceptFunc)]
listeners conf = TLS.withTLS $ do
let fs = catMaybes [httpListener, httpsListener, unixListener]
mapM (\(str, mkAfunc) -> do (sock, afunc) <- mkAfunc
return $! (str, sock, afunc)) fs
where
httpsListener = do
b <- getSSLBind conf
p <- getSSLPort conf
cert <- getSSLCert conf
chainCert <- getSSLChainCert conf
key <- getSSLKey conf
return (S.concat [ "https://"
, b
, ":"
, bshow p ],
do (sock, ctx) <- TLS.bindHttps b p cert chainCert key
return (sock, TLS.httpsAcceptFunc sock ctx)
)
httpListener = do
p <- getPort conf
b <- getBind conf
return (S.concat [ "http://"
, b
, ":"
, bshow p ],
do sock <- Sock.bindSocket b p
if getProxyType conf == Just HaProxy
then return (sock, Sock.haProxyAcceptFunc sock)
else return (sock, Sock.httpAcceptFunc sock))
unixListener = do
path <- getUnixSocket conf
let accessMode = getUnixSocketAccessMode conf
return (T.encodeUtf8 . T.pack $ "unix:" ++ path,
do sock <- Sock.bindUnixSocket accessMode path
return (sock, Sock.httpAcceptFunc sock))
httpServe :: Config Snap a -> Snap () -> IO ()
httpServe config handler0 = do
conf <- completeConfig config
let !handler = chooseProxy conf
let serve = compress conf . catch500 conf $ handler
simpleHttpServe conf serve
where
chooseProxy conf = maybe handler0
(\ptype -> pickProxy ptype handler0)
(getProxyType conf)
pickProxy NoProxy = id
pickProxy HaProxy = id
pickProxy X_Forwarded_For = behindProxy Proxy.X_Forwarded_For
catch500 :: MonadSnap m => Config m a -> m () -> m ()
catch500 conf = flip L.catch $ fromJust $ getErrorHandler conf
compress :: MonadSnap m => Config m a -> m () -> m ()
compress conf = if fromJust $ getCompression conf then withCompression else id
quickHttpServe :: Snap () -> IO ()
quickHttpServe handler = do
conf <- commandLineConfig defaultConfig
httpServe conf handler
setUnicodeLocale :: String -> IO ()
#ifndef PORTABLE
setUnicodeLocale lang = mapM_ (\k -> setEnv k (lang ++ ".UTF-8") True)
[ "LANG"
, "LC_CTYPE"
, "LC_NUMERIC"
, "LC_TIME"
, "LC_COLLATE"
, "LC_MONETARY"
, "LC_MESSAGES"
, "LC_PAPER"
, "LC_NAME"
, "LC_ADDRESS"
, "LC_TELEPHONE"
, "LC_MEASUREMENT"
, "LC_IDENTIFICATION"
, "LC_ALL" ]
#else
setUnicodeLocale = const $ return ()
#endif
bshow :: (Show a) => a -> ByteString
bshow = S.pack . show