packages feed

gemini-server-0.1.0.0: Network/Gemini/Server.hs

module Network.Gemini.Server (
  Request
, Response(..)
, Handler
, runServer
, okGemini
, okPlain
, redirect
) where

import Network.Socket (HostName, ServiceName, SockAddr, getPeerName)
import Network.Socket.ByteString.Lazy (recv, sendAll)
import Network.Run.TCP (runTCPServer)
import Network.URI (URI(URI), parseURI, uriToString)

import qualified Data.ByteString.Lazy as LBS
import Data.ByteString.Lazy.UTF8 (toString)

import Data.String (fromString)

import Control.Exception (SomeException, try)

import System.Log.Logger
  ( updateGlobalLogger, setLevel, logM, Priority(INFO, ERROR) )

--MAYBE switch to a more modern/efficient uri library
--TODO add client cert
type Request = URI

data Response = Response
  { responseStatus :: Int
  , responseMeta :: String
  , responseBody :: LBS.ByteString }

type Handler = Request -> IO Response

renderHeader :: Int -> String -> LBS.ByteString
renderHeader status meta =
  fromString (show status) <>
  fromString " " <>
  fromString meta <>
  fromString "\CR\LF"

runServer :: Maybe HostName -> ServiceName -> (Request -> IO Response) -> IO ()
runServer host service handler = do
  updateGlobalLogger "Network.Gemini.Server" $ setLevel INFO
  runTCPServer host service talk -- MAYBE server config
  where
    talk s = do --TODO timeouts on send and receive (and maybe on handler)
      msg <- toString <$> recv s 1025 -- 1024 + CR or LF
      -- It makes sense to be very lenient here
      let mURI = parseURI $ takeWhile (not . (`elem` ['\CR', '\LF'])) msg
      peer <- getPeerName s
      case mURI of
        Nothing -> do
          logRequest INFO peer (Left msg) 59 Nothing
          sendAll s $ renderHeader 59 $ fromString "Invalid URL"
        Just uri@(URI "gemini:" _ _ _ _) -> do
          response <- try $ handler uri
          case response of
            Right (Response status meta body) -> do
              logRequest INFO peer (Right uri) status $ Just meta
              sendAll s $ renderHeader status meta
              sendAll s body
            Left e -> do
              logRequest ERROR peer (Right uri) 42 $ Just $ show (e :: SomeException)
              sendAll s $ renderHeader 42 $ fromString "Internal server error"
        Just uri@(URI scheme _ _ _ _) -> do
              logRequest INFO peer (Right uri) 59 Nothing
              sendAll s $ renderHeader 59 $ fromString $ "Invalid scheme: " <> scheme

logRequest :: Priority -> SockAddr -> Either String URI -> Int -> Maybe String -> IO ()
logRequest p peer uri code meta = logM "Network.Gemini.Server" p $ unwords
  [ show peer
  , either show show uri
  , show code
  , maybe "-" show meta ]

-- | Shorthand for @Response 20 "text/gemini"@
okGemini :: LBS.ByteString -> Response
okGemini = Response 20 $ fromString "text/gemini"

-- | Shorthand for @Response 20 "text/plain"@
okPlain :: LBS.ByteString -> Response
okPlain = Response 20 $ fromString "text/plain"

redirect :: URI -> Response
redirect uri = Response 30 (uriToString id uri "") mempty