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 +14/−0
- README.md +37/−8
- etc/spacecookie.service +6/−3
- etc/spacecookie.socket +9/−0
- server/Main.hs +8/−1
- server/Systemd.hs +59/−0
- spacecookie.cabal +9/−4
- src/Network/Gopher.hs +51/−14
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'.