about summary refs log tree commit diff
diff options
context:
space:
mode:
authorsternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org>2021-03-15 14:11:30 +0100
committersterni <sternenseemann@systemli.org>2021-03-15 14:25:36 +0100
commit58973d3eb8e88ac4979a7057234ca59d9628b96c (patch)
treec26eee307de5c2f7f35cc246b6dcf63f61c54de8
parent17c6cb0ae61dd31078d95bf0acf2ec559a09888d (diff)
feat(Network.Gopher): remove cRunUserName from GopherConfig
It is not necessary to implement privilege dropping as part of the main
Network.Gopher API anymore since it can easily be done in the ready
action passed to runGopherManual.

To ease transition we expose an exception-throwing version of the
formerly internal function dropPrivileges via Network.Gopher.Util
which can be plugged into the ready action of runGopherManual if
desired.

feat(server): exit if dropPrivileges fails

We don't catch the exception of dropPrivileges anymore, exiting on
a failure related to it instead (user doesn't exist, insufficient
privileges).

BREAKING CHANGES:

* cRunUserName has been removed from GopherConfig. This breaks all library
  usage which relied on this feature.
* The spacecookie server daemon now exits if changing user fails instead
  of just logging an error like before.
-rw-r--r--CHANGELOG.md7
-rw-r--r--server/Main.hs21
-rw-r--r--src/Network/Gopher.hs26
-rw-r--r--src/Network/Gopher/Util.hs14
4 files changed, 39 insertions, 29 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8269ae2..1ee5763 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,8 @@ settings.
   can't be parsed.
 * An error is now logged when a gophermap file doesn't parse and the standard
   directory response is used as fallback.
+* Exit if dropping privileges fails instead of just logging an error like before.
+  See [#45](https://github.com/sternenseemann/spacecookie/pull/45).
 
 ### Library
 
@@ -219,6 +221,11 @@ The remaining, less significant changes are:
 * Requests from clients are now limited to 1MB in size and are received by
   a single call to `recv(2)`. If this causes issues with any gopher client,
   please open an issue.
+* `cRunUserName` has been removed from `GopherConfig` since the functionality
+  doesn't need special treatment as users can implement it easily via the
+  ready action of `runGopherManual`. The formerly internal `dropPrivileges`
+  function is now available via `Network.Gopher.Util` to be used for this
+  purpose. See [#45](https://github.com/sternenseemann/spacecookie/pull/45).
 
 ## 0.2.1.2 Bump fast-logger
 
diff --git a/server/Main.hs b/server/Main.hs
index 37be629..30c57e4 100644
--- a/server/Main.hs
+++ b/server/Main.hs
@@ -6,7 +6,7 @@ import Network.Spacecookie.Systemd
 import Paths_spacecookie (version)
 
 import Network.Gopher
-import Network.Gopher.Util (sanitizePath, boolToMaybe)
+import Network.Gopher.Util (sanitizePath, boolToMaybe, dropPrivileges)
 import Network.Gopher.Util.Gophermap
 import qualified Data.ByteString as B
 import Control.Applicative ((<|>))
@@ -64,12 +64,12 @@ runServer configFile = do
             { cServerName = serverName config
             , cListenAddr = listenAddr config
             , cServerPort = serverPort config
-            , cRunUserName = runUserName config
             , cLogHandler = logHandler
             }
+          logIO = fromMaybe noLog logHandler
 
       let setupFailureHandler e = do
-            (fromMaybe noLog logHandler) GopherLogLevelError
+            logIO GopherLogLevelError
               $  "Exception occurred in setup step: "
               <> toGopherLogStr (show e)
             logStopAction
@@ -81,12 +81,23 @@ runServer configFile = do
 
       catchSetupFailure $ runGopherManual
         (systemdSocket cfg)
-        (notifyReady >> pure ())
+        (afterSocketSetup logIO config)
         (\s -> do
           _ <- notifyStopping
           logStopAction
           systemdStoreOrClose s)
-        cfg $ spacecookie (fromMaybe noLog logHandler)
+        cfg
+        (spacecookie logIO)
+
+afterSocketSetup :: GopherLogHandler -> Config -> IO ()
+afterSocketSetup logIO cfg = do
+  case runUserName cfg of
+    Nothing -> pure ()
+    Just u  -> do
+      dropPrivileges u
+      logIO GopherLogLevelInfo $ "Changed to user " <> toGopherLogStr u
+  _ <- notifyReady
+  pure ()
 
 printUsage :: IO ()
 printUsage = do
diff --git a/src/Network/Gopher.hs b/src/Network/Gopher.hs
index 00f7804..45f4d2d 100644
--- a/src/Network/Gopher.hs
+++ b/src/Network/Gopher.hs
@@ -89,7 +89,7 @@ import Network.Gopher.Util.Gophermap
 import Network.Gopher.Util.Socket
 
 import Control.Concurrent (forkIO, ThreadId ())
-import Control.Exception (bracket, catch, handle, throw, SomeException (), Exception ())
+import Control.Exception (bracket, catch, throw, SomeException (), Exception ())
 import Control.Monad (forever, when, void)
 import Control.Monad.IO.Class (liftIO, MonadIO (..))
 import Control.Monad.Reader (ask, runReaderT, MonadReader (..), ReaderT (..))
@@ -104,7 +104,6 @@ import System.Socket hiding (Error (..))
 import System.Socket.Family.Inet6
 import System.Socket.Type.Stream
 import System.Socket.Protocol.TCP
-import System.Posix.User
 
 -- | Necessary information to handle gopher requests
 data GopherConfig
@@ -118,8 +117,6 @@ data GopherConfig
   --   If 'Nothing', listen on all addresses.
   , cServerPort    :: Integer
   -- ^ Port to listen on
-  , cRunUserName   :: Maybe String
-  -- ^ User to run the process as
   , cLogHandler    :: Maybe GopherLogHandler
   -- ^ 'IO' action spacecookie will call to output its log messages.
   --   If it is 'Nothing', logging is disabled. See [the logging section](#logging)
@@ -129,7 +126,7 @@ data GopherConfig
 -- | Default 'GopherConfig' describing a server on @localhost:70@ with
 --   no registered log handler.
 defaultConfig :: GopherConfig
-defaultConfig = GopherConfig "localhost" Nothing 70 Nothing Nothing
+defaultConfig = GopherConfig "localhost" Nothing 70 Nothing
 
 -- | Type for an user defined 'IO' action which handles logging a
 --   given 'GopherLogStr' of a given 'GopherLogLevel'. It may
@@ -232,15 +229,6 @@ receiveRequest sock = do
            then B.init req
            else req
 
-dropPrivileges :: String -> IO Bool
-dropPrivileges username =
-  handle ((\_ -> return False) :: SomeException -> IO Bool)
-    $ do
-      user <- getUserEntryForName username
-      setGroupID $ userGroupID user
-      setUserID $ userID user
-      return True
-
 -- | Auxiliary function that sets up the listening socket for
 --   'runGopherManual' correctly and starts to listen.
 --
@@ -315,16 +303,6 @@ runGopherManual sockAction ready term cfg f = bracket
       addr <- liftIO $ getAddress sock
       logInfo $ "Listening on " <> toGopherLogStr addr
 
-      -- Change UID and GID if necessary
-      case cRunUserName cfg of
-        Nothing -> pure ()
-        Just u -> do
-          success <- liftIO $ dropPrivileges u
-          if success
-            then logInfo $ "Changed to user " <> toGopherLogStr u
-            else logError $ "Can' change to user " <> toGopherLogStr u
-            -- TODO: abort?
-
       liftIO $ ready
 
       forever $ acceptAndHandle sock)
diff --git a/src/Network/Gopher/Util.hs b/src/Network/Gopher/Util.hs
index cf022e2..02edeb7 100644
--- a/src/Network/Gopher/Util.hs
+++ b/src/Network/Gopher/Util.hs
@@ -10,6 +10,7 @@ module Network.Gopher.Util (
   -- * Security
     sanitizePath
   , sanitizeIfNotUrl
+  , dropPrivileges
   -- * String Encoding
   , asciiOrd
   , asciiChr
@@ -27,6 +28,7 @@ import Data.Char (ord, chr, toLower)
 import qualified Data.String.UTF8 as U
 import Data.Word (Word8 ())
 import System.FilePath.Posix.ByteString (RawFilePath, normalise, joinPath, splitPath)
+import System.Posix.User
 
 -- | 'chr' a 'Word8'
 asciiChr :: Word8 -> Char
@@ -82,3 +84,15 @@ sanitizeIfNotUrl path =
 boolToMaybe :: Bool -> a -> Maybe a
 boolToMaybe True  a = Just a
 boolToMaybe False _ = Nothing
+
+-- | Call 'setGroupID' and 'setUserID' to switch to
+--   the given user and their primary group.
+--   Requires special privileges.
+--   Will raise an exception if either the user
+--   does not exist or the current user has no
+--   permission to change UID/GID.
+dropPrivileges :: String -> IO ()
+dropPrivileges username = do
+  user <- getUserEntryForName username
+  setGroupID $ userGroupID user
+  setUserID $ userID user