packages feed

spacecookie 0.2.0.1 → 0.2.1.0

raw patch · 8 files changed

+193/−30 lines, 8 filesdep +systemd

Dependencies added: systemd

Files

CHANGELOG.md view
@@ -1,5 +1,19 @@ # Revision history for spacecookie +## 0.2.1.0 Systemd Support++* Improved systemd support.+  * Support for the notify service type+  * Support for socket activation and socket (fd) storage+  * To make use of these new features you'll have to update your service files+* Added `defaultConfig` value to prevent future breakage in software using the+  library when the `GopherConfig` type is extended.+* Pretty print IPv6 addresses in logging++## 0.2.0.1 Hackage release++Fixed a problem hindering the hackage release.+ ## 0.2.0.0 initial release  * First version. Released on an unsuspecting world. Includes:
README.md view
@@ -1,5 +1,5 @@-	 ____                                       _    _      -	/ ___| _ __   __ _  ___ ___  ___ ___   ___ | | _(_) ___ +	 ____                                       _    _+	/ ___| _ __   __ _  ___ ___  ___ ___   ___ | | _(_) ___ 	\___ \| '_ \ / _` |/ __/ _ \/ __/ _ \ / _ \| |/ / |/ _ \ 	 ___) | |_) | (_| | (_|  __/ (_| (_) | (_) |   <| |  __/ 	|____/| .__/ \__,_|\___\___|\___\___/ \___/|_|\_\_|\___|@@ -18,7 +18,8 @@ * is RFC1436-compliant * supports info-line in menus (compatible protocol extension) * supports gophermaps (see below)-* includes a library for custom gopher applications+* provides a library for custom gopher applications ([see documentation](http://hackage.haskell.org/package/spacecookie/docs/Network-Gopher.html))+* can integrate with systemd using socket activation  ## Configuration @@ -26,7 +27,7 @@  Let's have a quick look at the options: -option     | meaning                                                 +option     | meaning -----------|-------------------------------------------------------------------------------------------------------- `hostname` | The hostname your spacecookie will be reachable through. `user`     | The user that just run spacecookie. It is used to drop root priveleges after binding the server socket.@@ -39,13 +40,34 @@  	spacecookie /path/to/spacecookie.json -Of course it is more convenient to run it as a system wide demon. For that reason a systemd `spacecookie.service` is provided. You can use it like this:+Spacecookie runs as a simple process and doesn't fork or write a PID file.+Therefore you'll need to use a supervisor to run it as a proper daemon. -	systemctl enable spacecookie.service-	systemctl start  spacecookie.service+### With systemd -Please note that you have to move the necessary file in place manually at the moment.+Spacecookie supports systemd socket activation. To set it up you'll need+to install `spacecookie.service` and `spacecookie.socket` like so: +	cp ./etc/spacecookie.{service,socket} /etc/systemd/system/+	systemctl daemon-reload+	systemctl enable spacecookie.socket+	systemctl start  spacecookie.socket+	systemctl start  spacecookie.service # optional, started by the socket automatically if needed++Don't forget to change the paths in the systemd files and match the user name in+`spacecookie.socket` with the one in `spacecookie.json`!++### Without systemd++Spacecookie does **not** require systemd nor depend on it. Since socket+activation only uses an environment variable and a type unix socket, it+is very lightweight and can be utilized without using a systemd library+or dbus.++Spacecookie currently fully supports any GNU/Linux. It should be pretty+easy to, for example, write a [runit](http://smarden.org/runit/) service+file.+ ## Adding Content  Spacecookie acts as a simple file server, only excluding files that start with a dot.@@ -73,3 +95,10 @@ * "Links" to other servers are like file/directory menu entries but the server's hostname and its port must be added (tab-separated).  The file type characters are defined in [RFC1436](https://tools.ietf.org/html/rfc1436#page-10). Detailed documentation on the gophermap format [can be found here](./docs/gophermap-pygopherd.txt).++## HTTP Support?++Spacecookie will never support HTTP to keep the code simple and clean. You can+however use an HTTP to Gopher Proxy with Spacecookie just fine. Any proxy+supporting RFC1436 should work, my own [gopher-proxy](https://github.com/sternenseemann/gopher-proxy)+might work for you.
etc/spacecookie.service view
@@ -1,10 +1,13 @@ [Unit] Description=Spacecookie Gopher Daemon-After=network.target+Requires=spacecookie.socket  [Service]-Type=simple-ExecStart=/usr/local/bin/spacecookie /usr/local/etc/spacecookie.json+Type=notify+ExecStart=/path/to/spacecookie /path/to/spacecookie.json+FileDescriptorStoreMax=1+NotifyAccess=main+StandardError=journal  [Install] WantedBy=multi-user.target
+ etc/spacecookie.socket view
@@ -0,0 +1,9 @@+[Unit]+Description=Socket for the Spacecookie Gopher Daemon++[Socket]+BindIPv6Only=both+SocketGroup=spacecookie+SocketUser=spacecookie++ListenStream=[::]:70
server/Main.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} import Config+import Systemd+ import Network.Gopher import Network.Gopher.Util (santinizePath, uEncode) import Network.Gopher.Util.Gophermap@@ -18,6 +20,7 @@ import System.Environment import System.FilePath.Posix (takeFileName, takeExtension, (</>), dropDrive, splitDirectories) import System.Posix.Directory (changeWorkingDirectory)+import System.Socket (close)  main :: IO () main = do@@ -29,7 +32,11 @@       case config' of         Just config -> do           changeWorkingDirectory (rootDirectory config)-          runGopher (GopherConfig (serverName config) (serverPort config) ((Just (runUserName config)))) spacecookie+          let cfg = GopherConfig (serverName config) (serverPort config) ((Just (runUserName config)))+          runGopherManual (systemdSocket cfg)+                          (notifyReady >> pure ())+                          (\s -> notifyStopping >> systemdStoreOrClose s)+                          cfg spacecookie         Nothing -> error "failed to parse config"     _ -> error "config file must be given" 
+ server/Systemd.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE BlockArguments #-}+module Systemd+  ( systemdSocket+  , notifyReady+  , notifyStopping+  , systemdStoreOrClose+  ) where++import Control.Concurrent.MVar (newMVar, takeMVar, mkWeakMVar)+import Control.Exception.Base+import Control.Monad (when, void)+import Foreign.C.Types (CInt (..))+import GHC.Conc (closeFdWith)+import Network.Gopher (setupGopherSocket, GopherConfig (..))+import System.Exit+import System.Posix.Types (Fd (..))+import System.Socket hiding (Error (..))+import System.Socket.Family.Inet6+import System.Socket.Type.Stream+import System.Socket.Protocol.TCP+import System.Socket.Unsafe (Socket (..))+import System.Systemd.Daemon (notifyReady, notifyStopping)+import System.Systemd.Daemon.Fd (storeFd, getActivatedSockets)++foreign import ccall unsafe "close"+  c_close :: CInt -> IO CInt++-- TODO Check Socket type, ...+data SystemdException = IncorrectNum | InvalidFd+  deriving (Eq, Ord)++instance Show SystemdException where+  show IncorrectNum = "SystemdException: Only exactly one Socket is supported"+  show InvalidFd    = "SystemdException: Invalid File Descriptor received"+instance Exception SystemdException++systemdSocket :: GopherConfig -> IO (Socket Inet6 Stream TCP)+systemdSocket cfg = getActivatedSockets >>= \sockets ->+  case sockets of+    Nothing -> setupGopherSocket cfg+    Just [fd] -> toSocket fd+    Just _ -> throwIO IncorrectNum+  where toSocket :: Fd -> IO (Socket Inet6 Stream TCP)+        toSocket fd = do+          when (fd < 0) $ throwIO InvalidFd+          mfd <- newMVar (fromIntegral fd)+          let s = Socket mfd+          _ <- mkWeakMVar mfd (close s)+          pure s++systemdStoreOrClose :: Socket Inet6 Stream TCP -> IO ()+systemdStoreOrClose s = do+  fd <- toFd s+  res <- storeFd fd+  case res of+    Just () -> return ()+    Nothing -> closeFdWith (void . c_close . fromIntegral) fd+  where toFd :: Socket Inet6 Stream TCP -> IO Fd+        toFd (Socket mvar) = fmap (Fd . fromIntegral) (takeMVar mvar)
spacecookie.cabal view
@@ -1,20 +1,23 @@ name:                spacecookie-version:             0.2.0.1-synopsis:            gopher server daemon-description:         simple gopher server daemon+version:             0.2.1.0+synopsis:            Gopher Library and Server Daemon+description:         Simple gopher library that allows writing custom gopher+                     applications. Also includes a fully-featured gopher server+                     daemon complete with gophermap-support built on top of it. license:             GPL-3 license-file:        LICENSE author:              Lukas Epple maintainer:          git@lukasepple.de category:            Network build-type:          Simple-cabal-version:       >=1.10+cabal-version:       >=2.0 homepage:            https://github.com/sternenseemann/spacecookie bug-reports:         https://github.com/sternenseemann/spacecookie/issues extra-source-files:  CHANGELOG.md                      README.md                      etc/spacecookie.json                      etc/spacecookie.service+                     etc/spacecookie.socket                      docs/gophermap                      docs/gophermap-pygopherd.txt                      docs/rfc1436.txt@@ -34,9 +37,11 @@                      , aeson                      , attoparsec                      , spacecookie+                     , systemd >= 2.1.0   hs-source-dirs:      server   default-language:    Haskell2010   other-modules:       Config+                     , Systemd  library   hs-source-dirs:      src
src/Network/Gopher.hs view
@@ -28,6 +28,8 @@ * 'ErrorResponse': gopher way to show an error (e. g. if a file is not found). A 'ErrorResponse' results in a menu response with a single entry.  If you use 'runGopher', it is the same story like in the example above, but you can do 'IO' effects. To see a more elaborate example, have a look at the server code in this package.++Note: In practice it is probably best to use record update syntax on 'defaultConfig' which won't break your application every time the config record fields are changed. -}  {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -35,9 +37,12 @@   -- * Main API     runGopher   , runGopherPure+  , runGopherManual   , GopherConfig (..)+  , defaultConfig   -- * Helper Functions   , gophermapToDirectoryResponse+  , setupGopherSocket   -- * Representations   -- ** Responses   , GopherResponse (..)@@ -82,6 +87,10 @@                  , cRunUserName   :: Maybe String -- ^ user to run the process as                  } +-- | Default 'GopherConfig' describing a server on @localhost:70@.+defaultConfig :: GopherConfig+defaultConfig = GopherConfig "localhost" 70 Nothing+ data Env   = Env { serverSocket :: Socket Inet6 Stream TCP         , serverName   :: ByteString@@ -113,6 +122,9 @@   (logger, _) <- logger <$> ask   liftIO $ logger (\t -> "[" <> toLogStr t <> "]" <> (toLogStr logMsg) <> "\n") +prettyAddr :: SocketAddress Inet6 -> String+prettyAddr (SocketAddressInet6 addr port _ _) = drop 13 (show addr) <> ":" <> show (fromIntegral port)+ receiveRequest :: Socket Inet6 Stream TCP -> IO ByteString receiveRequest sock = receiveRequest' sock mempty   where lengthLimit = 1024@@ -133,21 +145,44 @@   setGroupID $ userGroupID user   setUserID $ userID user +-- | Auxiliary function that sets up the listening socket for+--   'runGopherManual' correctly and starts to listen.+setupGopherSocket :: GopherConfig -> IO (Socket Inet6 Stream TCP)+setupGopherSocket cfg = do+  sock <- (socket :: IO (Socket Inet6 Stream TCP))+  setSocketOption sock (ReuseAddress True)+  setSocketOption sock (V6Only False)+  bind sock (SocketAddressInet6 inet6Any (fromInteger (cServerPort cfg)) 0 0)+  listen sock 5+  pure sock+ -- | Run a gopher application that may cause effects in 'IO'. --   The application function is given the gopher request (path) --   and required to produce a GopherResponse. runGopher :: GopherConfig -> (String -> IO GopherResponse) -> IO ()-runGopher cfg f = bracket-  (socket :: IO (Socket Inet6 Stream TCP))-  close+runGopher cfg f = runGopherManual (setupGopherSocket cfg) (pure ()) close cfg f++-- | Same as 'runGopher', but allows you to setup the 'Socket' manually+--   and calls an action of type @IO ()@ as soon as the server is ready+--   to accept requests. When the server terminates, it calls the action+--   of type @Socket Inet6 Stream TCP -> IO ()@ to clean up the socket.+--+--   Spacecookie assumes the 'Socket' is properly set up to listen on the+--   port and host specified in the 'GopherConfig' (i. e. 'bind' and+--   'listen' have been called). This can be achieved using 'setupGopherSocket'.+--+--   This is intended for supporting systemd socket activation and storage.+--   Only use, if you know what you are doing.+runGopherManual :: IO (Socket Inet6 Stream TCP) -> IO () -> (Socket Inet6 Stream TCP -> IO ())+                -> GopherConfig -> (String -> IO GopherResponse) -> IO ()+runGopherManual sock ready term cfg f = bracket+  sock+  term   (\sock -> do     env <- initEnv sock (cServerName cfg) (fromInteger (cServerPort cfg)) f     gopherM env $ do-      liftIO $ setSocketOption sock (ReuseAddress True)-      liftIO $ setSocketOption sock (V6Only False)-      liftIO $ bind sock (SocketAddressInet6 inet6Any (fromInteger (cServerPort cfg)) 0 0)-      liftIO $ listen sock 5-      log. LogInfo $ "Now listening [::]:" ++ show (cServerPort cfg)+      addr <- liftIO $ getAddress sock+      log . LogInfo $ "Now listening on " ++ prettyAddr addr        -- Change UID and GID if necessary       if isJust (cRunUserName cfg)@@ -156,6 +191,8 @@           log . LogInfo $ "Dropped privileges to " ++ fromJust (cRunUserName cfg)         else log .LogInfo $ "Privileges were not dropped" +      liftIO $ ready+       (forever (acceptAndHandle sock) `catchError`         (\e -> do           log . LogError $ show e@@ -164,25 +201,25 @@ forkGopherM :: GopherM () -> GopherM ThreadId forkGopherM action = ask >>= liftIO . forkIO . (flip gopherM) action -handleIncoming :: Socket Inet6 Stream TCP -> Inet6Address -> GopherM ()+handleIncoming :: Socket Inet6 Stream TCP -> SocketAddress Inet6 -> GopherM () handleIncoming clientSock addr = do   req <- liftIO $ uDecode . stripNewline <$> receiveRequest clientSock-  log . LogInfo $ "Got request '" ++ req ++ "' from " ++ show addr+  log . LogInfo $ "Got request '" ++ req ++ "' from " ++ prettyAddr addr    fun <- serverFun <$> ask   res <- liftIO (fun req) >>= response    liftIO $ sendAll clientSock res msgNoSignal   liftIO $ close clientSock-  log . LogInfo $ "Closed connection succesfully to " ++ show addr+  log . LogInfo $ "Closed connection succesfully to " ++ prettyAddr addr  acceptAndHandle :: Socket Inet6 Stream TCP -> GopherM () acceptAndHandle sock = do-  (clientSock, (SocketAddressInet6 addr _ _ _)) <- liftIO $ accept sock-  log . LogInfo $ "Accepted Connection from " ++ show addr+  (clientSock, addr) <- liftIO $ accept sock+  log . LogInfo $ "Accepted Connection from " ++ prettyAddr addr   forkGopherM $ handleIncoming clientSock addr `catchError` (\e -> do     liftIO (close clientSock)-    log . LogError $ "Closed connection to " ++ show addr ++ " after error: " ++ show e)+    log . LogError $ "Closed connection to " ++ prettyAddr addr ++ " after error: " ++ show e)   return ()  -- | Run a gopher application that may not cause effects in 'IO'.