sproxy2-1.97.1: src/Sproxy/Server.hs
module Sproxy.Server
( server
) where
import Control.Concurrent (forkIO)
import Control.Exception (bracketOnError)
import Control.Monad (void, when)
import Data.ByteString.Char8 (pack)
import qualified Data.HashMap.Strict as HM
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Yaml.Include (decodeFileEither)
import Network.HTTP.Client
( Manager
, ManagerSettings(..)
, defaultManagerSettings
, newManager
, responseTimeoutMicro
, socketConnection
)
import Network.HTTP.Client.Internal (Connection)
import Network.Socket
( AddrInfoFlag(AI_NUMERICSERV)
, Family(AF_INET, AF_UNIX)
, SockAddr(SockAddrInet, SockAddrUnix)
, Socket
, SocketOption(ReuseAddr)
, SocketType(Stream)
, addrAddress
, addrFamily
, addrFlags
, addrProtocol
, addrSocketType
, bind
, close
, connect
, defaultHints
, getAddrInfo
, listen
, maxListenQueue
, setSocketOption
, socket
)
import Network.Wai (Application)
import Network.Wai.Handler.Warp
( Settings
, defaultSettings
, runSettingsSocket
, setHTTP2Disabled
, setOnException
)
import Network.Wai.Handler.WarpTLS (runTLSSocket, tlsSettingsChain)
import System.Entropy (getEntropy)
import System.Environment (setEnv)
import System.Exit (exitFailure)
import System.FilePath.Glob (compile)
import System.IO (hPutStrLn, stderr)
import System.Posix.User
( GroupEntry(..)
, UserEntry(..)
, getAllGroupEntries
, getRealUserID
, getUserEntryForName
, setGroupID
, setGroups
, setUserID
)
import Sproxy.Application (redirect, sproxy)
import qualified Sproxy.Application.OAuth2 as OAuth2
import Sproxy.Application.OAuth2.Common (OAuth2Client)
import Sproxy.Config (BackendConf(..), ConfigFile(..), OAuth2Conf(..))
import qualified Sproxy.Logging as Log
import qualified Sproxy.Server.DB as DB
{- TODO:
- Log.error && exitFailure should be replaced
- by Log.fatal && wait for logger thread to print && exitFailure
-}
server :: FilePath -> IO ()
server configFile = do
cf <- readConfigFile configFile
Log.start $ cfLogLevel cf
sock <- socket AF_INET Stream 0
setSocketOption sock ReuseAddr 1
bind sock $ SockAddrInet (fromIntegral $ cfListen cf) 0
maybe80 <-
if fromMaybe (443 == cfListen cf) (cfListen80 cf)
then do
sock80 <- socket AF_INET Stream 0
setSocketOption sock80 ReuseAddr 1
bind sock80 $ SockAddrInet 80 0
return (Just sock80)
else return Nothing
uid <- getRealUserID
when (0 == uid) $ do
let user = cfUser cf
Log.info $ "switching to user " ++ show user
u <- getUserEntryForName user
groupIDs <-
map groupID . filter (elem user . groupMembers) <$> getAllGroupEntries
setGroups groupIDs
setGroupID $ userGroupID u
setUserID $ userID u
ds <- newDataSource cf
db <- DB.start (cfHome cf) ds
key <-
maybe
(Log.info "using new random key" >> getEntropy 64)
(return . pack)
(cfKey cf)
let settings =
(if cfHTTP2 cf
then id
else setHTTP2Disabled) $
setOnException (\_ _ -> return ()) defaultSettings
oauth2clients <-
HM.fromList <$> mapM newOAuth2Client (HM.toList (cfOAuth2 cf))
backends <-
mapM
(\be -> do
m <- newBackendManager be
return (compile $ beName be, be, m)) $
cfBackends cf
warpServer <- newServer cf
case maybe80 of
Nothing -> return ()
Just sock80 -> do
let httpsPort = fromMaybe (cfListen cf) (cfHttpsPort cf)
Log.info "listening on port 80 (HTTP redirect)"
listen sock80 maxListenQueue
void . forkIO $ runSettingsSocket settings sock80 (redirect httpsPort)
-- XXX 2048 is from bindPortTCP from streaming-commons used internally by runTLS.
-- XXX Since we don't call runTLS, we listen socket here with the same options.
Log.info $ "proxy listening on port " ++ show (cfListen cf)
listen sock (max 2048 maxListenQueue)
warpServer settings sock (sproxy key db oauth2clients backends)
newDataSource :: ConfigFile -> IO (Maybe DB.DataSource)
newDataSource cf =
case (cfDataFile cf, cfDatabase cf) of
(Nothing, Just str) -> do
case cfPgPassFile cf of
Nothing -> return ()
Just f -> do
Log.info $ "pgpassfile: " ++ show f
setEnv "PGPASSFILE" f
return . Just $ DB.PostgreSQL str
(Just f, Nothing) -> return . Just $ DB.File f
(Nothing, Nothing) -> return Nothing
_ -> do
Log.error "only one data source can be used"
exitFailure
newOAuth2Client :: (Text, OAuth2Conf) -> IO (Text, OAuth2Client)
newOAuth2Client (name, cfg) =
case HM.lookup name OAuth2.providers of
Nothing -> do
Log.error $ "OAuth2 provider " ++ show name ++ " is not supported"
exitFailure
Just provider -> do
Log.info $ "oauth2: adding " ++ show name
return (name, provider (client_id, client_secret))
where
client_id = pack $ oa2ClientId cfg
client_secret = pack $ oa2ClientSecret cfg
newBackendManager :: BackendConf -> IO Manager
newBackendManager be = do
openConn <-
case (beSocket be, bePort be) of
(Just f, Nothing) -> do
Log.info $ "backend `" ++ beName be ++ "' on UNIX socket " ++ f
return $ openUnixSocketConnection f
(Nothing, Just n) -> do
let svc = show n
Log.info $
"backend `" ++ beName be ++ "' on " ++ beAddress be ++ ":" ++ svc
return $ openTCPConnection (beAddress be) svc
_ -> do
Log.error "either backend port number or UNIX socket path is required."
exitFailure
newManager
defaultManagerSettings
{ managerRawConnection = return $ \_ _ _ -> openConn
, managerConnCount = beConnCount be
, managerResponseTimeout = responseTimeoutMicro (1000000 * beTimeout be)
}
newServer :: ConfigFile -> IO (Settings -> Socket -> Application -> IO ())
newServer cf
| cfSsl cf =
case (cfSslKey cf, cfSslCert cf) of
(Just k, Just c) ->
return $ runTLSSocket (tlsSettingsChain c (cfSslCertChain cf) k)
_ -> do
Log.error "missings SSL certificate"
exitFailure
| otherwise = do
Log.warn "not using SSL!"
return runSettingsSocket
openUnixSocketConnection :: FilePath -> IO Connection
openUnixSocketConnection f =
bracketOnError
(socket AF_UNIX Stream 0)
close
(\s -> do
connect s (SockAddrUnix f)
socketConnection s 8192)
openTCPConnection :: String -> String -> IO Connection
openTCPConnection host svc = do
addr:_ <- getAddrInfo (Just hints) (Just host) (Just svc)
bracketOnError
(socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
close
(\s -> do
connect s (addrAddress addr)
socketConnection s 8192)
where
hints = defaultHints {addrFlags = [AI_NUMERICSERV], addrSocketType = Stream}
readConfigFile :: FilePath -> IO ConfigFile
readConfigFile f = do
r <- decodeFileEither f
case r of
Left e -> do
hPutStrLn stderr $ "FATAL: " ++ f ++ ": " ++ show e
exitFailure
Right cf -> return cf