spacecookie-0.2.1.0: server/Main.hs
{-# LANGUAGE OverloadedStrings #-}
import Config
import Systemd
import Network.Gopher
import Network.Gopher.Util (santinizePath, uEncode)
import Network.Gopher.Util.Gophermap
import Data.ByteString (ByteString ())
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.List (isPrefixOf)
import Control.Applicative ((<|>), (<$>), pure)
import Control.Monad (unless, filterM, sequence, join)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (decode)
import Data.Attoparsec.ByteString (parseOnly)
import Data.Char (toLower)
import Data.Maybe (fromJust)
import System.Directory (doesDirectoryExist, doesFileExist, getDirectoryContents)
import System.Environment
import System.FilePath.Posix (takeFileName, takeExtension, (</>), dropDrive, splitDirectories)
import System.Posix.Directory (changeWorkingDirectory)
import System.Socket (close)
main :: IO ()
main = do
args <- getArgs
case args of
[ configFile ] -> do
doesFileExist configFile >>= (flip unless) (error "could not open config file")
config' <- decode <$> BL.readFile configFile
case config' of
Just config -> do
changeWorkingDirectory (rootDirectory config)
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"
spacecookie :: String -> IO GopherResponse
spacecookie path' = do
let path = "." </> dropDrive (santinizePath path')
fileType <- gopherFileType path
pathType <- pathType path
if not (isListable pathType path')
then pure . ErrorResponse $ "Accessing '" ++ path' ++ "' is not allowed."
else case fileType of
Error -> pure $
if "URL:" `isPrefixOf` path'
then ErrorResponse $ "spacecookie does not support proxying HTTP, try using a gopher client that supports the h-type. If you tried to request a file called '" ++ path' ++ "', it does not exist."
else ErrorResponse $ "The requested file '" ++ path' ++ "' does not exist or is not available."
-- always use gophermapResponse which falls back
-- to directoryResponse if there is no gophermap file
Directory -> gophermapResponse path
_ -> fileResponse path
fileResponse :: FilePath -> IO GopherResponse
fileResponse path = FileResponse <$> B.readFile path
makeAbsolute :: FilePath -> FilePath
makeAbsolute x = if "./" `isPrefixOf` x
then tail x
else x
directoryResponse :: FilePath -> IO GopherResponse
directoryResponse path = do
dir <- join (filterM (\x -> ((flip isListable) x) <$> pathType x) . map (path </>) <$> getDirectoryContents path)
fileTypes <- mapM gopherFileType dir
pure . MenuResponse . map (\f -> f Nothing Nothing) $ zipWith (\t f -> Item t (uEncode (takeFileName f)) f) fileTypes (map makeAbsolute dir)
gophermapResponse :: FilePath -> IO GopherResponse
gophermapResponse path = do
let gophermap = path </> ".gophermap"
exists <- doesFileExist gophermap
parsed <- if exists
then parseOnly parseGophermap <$> B.readFile gophermap
else pure $ Left "Gophermap file does not exist"
case parsed of
Left _ -> directoryResponse path
Right right -> pure $ gophermapToDirectoryResponse right
-- | calculates the file type identifier used in the Gopher protocol
-- for a given file
gopherFileType :: FilePath -> IO GopherFileType
gopherFileType f = do
isDir <- ioCheck Directory doesDirectoryExist
isFile <- ioCheck File doesFileExist
let isGif = boolToMaybe GifFile $ takeExtension f == "gif"
let isImage = boolToMaybe ImageFile $ map toLower (takeExtension f) `elem` ["png", "jpg", "jpeg", "raw", "cr2", "nef"]
return . fromJust $
isDir <|> isGif <|> isImage <|> isFile <|> Just Error
where ioCheck onSuccess check = fmap (boolToMaybe onSuccess) . check $ f
-- | isListable filters out system files for directory listings
isListable :: PathType -> FilePath -> Bool
isListable Directory' "" = True -- "" is root
isListable _ "" = False
isListable DoesNotExist _ = False
isListable Directory' p
| (head . last . splitDirectories) p == '.' = False
| otherwise = True
isListable File' p
| head (takeFileName p) == '.' = False
| otherwise = True
-- | True -> Just a
-- False -> Nothing
boolToMaybe :: a -> Bool -> Maybe a
boolToMaybe a True = Just a
boolToMaybe _ False = Nothing
data PathType
= Directory'
| File'
| DoesNotExist
deriving (Show, Eq)
pathType :: FilePath -> IO PathType
pathType p = do
file <- doesFileExist p
dir <- doesDirectoryExist p
if file
then pure File'
else if dir
then pure Directory'
else pure DoesNotExist