hprox-0.7.1: src/Network/HProx/Unix.hs
-- SPDX-License-Identifier: Apache-2.0
--
-- Copyright (C) 2026 Bin Jin. All Rights Reserved.
{-# LANGUAGE CPP #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Network.HProx.Unix
( PrivilegeDropPlan(..)
, ResolvedGroup(..)
, ResolvedUser(..)
, dropRootPriviledge
, installSighupHandler
, planPrivilegeDrop
) where
import Control.Applicative
import Control.Monad
import Data.List (nub, sort)
import Network.HProx.Log
#ifdef OS_UNIX
import Control.Exception (SomeException, catch)
import System.Exit
import System.Posix.Process (exitImmediately)
import System.Posix.Signals (Handler(Catch), installHandler, sigHUP)
import System.Posix.User
#endif
type PrivilegeID = Integer
data ResolvedUser = ResolvedUser
{ resolvedUserName :: String
, resolvedUserID :: PrivilegeID
, resolvedUserPrimaryGroupID :: PrivilegeID
}
deriving (Eq, Show)
data ResolvedGroup = ResolvedGroup
{ resolvedGroupName :: String
, resolvedGroupID :: PrivilegeID
, resolvedGroupMembers :: [String]
}
deriving (Eq, Show)
data PrivilegeDropPlan = PrivilegeDropPlan
{ privilegeDropUserName :: Maybe String
, privilegeDropUserID :: Maybe PrivilegeID
, privilegeDropGroupName :: Maybe String
, privilegeDropGroupID :: Maybe PrivilegeID
, privilegeDropSupplementaryGroups :: [PrivilegeID]
}
deriving (Eq, Show)
planPrivilegeDrop :: Maybe ResolvedUser -> Maybe ResolvedGroup -> [ResolvedGroup] -> PrivilegeDropPlan
planPrivilegeDrop userEntry groupEntry allGroups = PrivilegeDropPlan
{ privilegeDropUserName = resolvedUserName <$> userEntry
, privilegeDropUserID = resolvedUserID <$> userEntry
, privilegeDropGroupName = resolvedGroupName <$> groupEntry
, privilegeDropGroupID = finalGroupID
, privilegeDropSupplementaryGroups = supplementaryGroups
}
where
finalGroupID = resolvedGroupID <$> groupEntry <|> resolvedUserPrimaryGroupID <$> userEntry
supplementaryGroups = case (resolvedUserName <$> userEntry, finalGroupID) of
(Just name, Just primaryGroupID) ->
nub $ primaryGroupID : [resolvedGroupID entry | entry <- allGroups, name `elem` resolvedGroupMembers entry]
_otherwise -> []
#ifdef OS_UNIX
installSighupHandler :: IO () -> IO ()
installSighupHandler action = do
_ <- installHandler sigHUP (Catch action) Nothing
return ()
dropRootPriviledge :: Logger -> Maybe String -> Maybe String -> IO Bool
dropRootPriviledge _ Nothing Nothing = return False
dropRootPriviledge logger user groupName' = do
currentUser <- getRealUserID
currentGroup <- getRealGroupID
if currentUser /= 0 || currentGroup /= 0
then do
logger WARN $ "Unable to setuid/setgid without root priviledge" <>
", userID=" <> toLogStr (show currentUser) <>
", groupID=" <> toLogStr (show currentGroup)
return False
else do
let abort msg = logger ERROR msg >> exitImmediately (ExitFailure 1)
let resolveUser entry = ResolvedUser (userName entry) (fromIntegral $ userID entry) (fromIntegral $ userGroupID entry)
resolveGroup entry = ResolvedGroup (groupName entry) (fromIntegral $ groupID entry) (groupMembers entry)
resolvedUser <- fmap resolveUser <$> mapM getUserEntryForName user
resolvedGroup <- fmap resolveGroup <$> mapM getGroupEntryForName groupName'
allGroups <- maybe (return []) (const $ map resolveGroup <$> getAllGroupEntries) resolvedUser
let plan = planPrivilegeDrop resolvedUser resolvedGroup allGroups
case privilegeDropUserName plan of
Just userName' -> do
logger INFO $ "set supplementary groups for " <> toLogStr userName'
setGroups $ map fromIntegral $ privilegeDropSupplementaryGroups plan
Nothing ->
forM_ (privilegeDropGroupID plan) $ \_ -> do
logger INFO "clear supplementary groups"
setGroups []
forM_ (privilegeDropGroupID plan) $ \gid -> do
logger INFO $ "setgid to " <> maybe (toLogStr $ show gid) toLogStr (privilegeDropGroupName plan)
setGroupID $ fromIntegral gid
verifyGroupID abort gid
forM_ (privilegeDropUserID plan) $ \uid -> do
logger INFO $ "setuid to " <> maybe (toLogStr $ show uid) toLogStr (privilegeDropUserName plan)
setUserID $ fromIntegral uid
verifyUserID abort uid
verifySupplementaryGroups abort $ privilegeDropSupplementaryGroups plan
logger DEBUG "testing setuid(0), verify that root priviledge can't be regranted"
catch (setUserID 0) $ \(_ :: SomeException) -> logger DEBUG "setuid(0) failed as expected"
changedUser <- getRealUserID
when (changedUser == 0) $ abort "unable to drop root priviledge, aborting"
return True
verifyUserID :: (LogStr -> IO ()) -> PrivilegeID -> IO ()
verifyUserID abort expectedUserID = do
realUserID <- getRealUserID
effectiveUserID <- getEffectiveUserID
when (fromIntegral realUserID /= expectedUserID || fromIntegral effectiveUserID /= expectedUserID || realUserID == 0) $
abort "failed to setuid, aborting"
verifyGroupID :: (LogStr -> IO ()) -> PrivilegeID -> IO ()
verifyGroupID abort expectedGroupID = do
realGroupID <- getRealGroupID
effectiveGroupID <- getEffectiveGroupID
when (fromIntegral realGroupID /= expectedGroupID || fromIntegral effectiveGroupID /= expectedGroupID || realGroupID == 0) $
abort "failed to setgid, aborting"
verifySupplementaryGroups :: (LogStr -> IO ()) -> [PrivilegeID] -> IO ()
verifySupplementaryGroups abort expectedGroups = do
changedGroups <- map fromIntegral <$> getGroups
when (sort (nub changedGroups) /= sort (nub expectedGroups)) $
abort "failed to set supplementary groups, aborting"
#else
dropRootPriviledge :: Logger -> Maybe String -> Maybe String -> IO Bool
dropRootPriviledge _ _ _ = return False
installSighupHandler :: IO () -> IO ()
installSighupHandler _ = return ()
#endif