hyperbole-0.5.0: src/Web/Hyperbole/Server/Socket.hs
module Web.Hyperbole.Server.Socket where
import Control.Monad (void)
import Data.Bifunctor (first)
import Data.List qualified as L
import Data.Maybe (fromMaybe)
import Data.String.Conversions (cs)
import Data.Text (Text)
import Effectful
import Effectful.Concurrent.Async
import Effectful.Exception
import Network.HTTP.Types as HTTP (parseQuery)
import Network.Wai qualified as Wai
import Network.WebSockets (Connection)
import Network.WebSockets qualified as WS
import Web.Cookie qualified
import Web.Hyperbole.Data.Cookie qualified as Cookie
import Web.Hyperbole.Data.URI (URI, path)
import Web.Hyperbole.Effect.Hyperbole
import Web.Hyperbole.Server.Message
import Web.Hyperbole.Server.Options
import Web.Hyperbole.Types.Request
import Web.Hyperbole.Types.Response
import Web.Hyperbole.View (View, addContext, renderLazyByteString)
data SocketRequest = SocketRequest
{ request :: Maybe Request
}
handleRequestSocket
:: (IOE :> es, Concurrent :> es)
=> ServerOptions
-> Wai.Request
-> Connection
-> Eff (Hyperbole : es) Response
-> Eff es ()
handleRequestSocket opts wreq conn actions = do
flip catch onMessageError $ do
msg <- receiveMessage
req <- parseMessageRequest msg
void $ async $ do
res <- trySync $ runHyperbole req actions
case res of
-- TODO: catch socket errors separately from SomeException?
Left (ex :: SomeException) -> do
-- It's not safe to send any exception over the wire
-- log it to the console and send the error to the client
liftIO $ print ex
res2 <- trySync $ sendError conn (requestMetadata req) (opts.serverError ErrInternal)
case res2 of
Left e -> liftIO $ putStrLn $ "Socket Error while sending previous error to client: " <> show e
Right _ -> pure ()
Right (resp, clnt, rmts) -> do
let meta = requestMetadata req <> responseMetadata req.path clnt rmts
case resp of
(Response _ vw) -> do
sendUpdateView conn meta vw
-- (Err (ErrServer m)) -> sendError conn meta (opts.serverError m)
(Err err) -> sendError conn meta (opts.serverError err)
(Redirect url) -> sendRedirect conn meta url
where
onMessageError :: (IOE :> es) => MessageError -> Eff es a
onMessageError e = do
liftIO $ do
putStrLn "Socket Message Error"
throwIO e
receiveMessage :: (IOE :> es) => Eff es Message
receiveMessage = do
t <- receiveText conn
case parseActionMessage t of
Left e -> throwIO $ InvalidMessage e t
Right msg -> pure msg
receiveText :: (IOE :> es) => Connection -> Eff es Text
receiveText _ = do
-- c <- ask @Connection
liftIO $ WS.receiveData conn
parseMessageRequest :: (IOE :> es) => Message -> Eff es Request
parseMessageRequest msg =
case messageRequest msg of
Left e -> throwIO e
Right a -> pure a
messageRequest :: Message -> Either MessageError Request
messageRequest msg = do
let pth = path $ cs $ Wai.rawPathInfo wreq
host = Host $ fromMaybe "" $ L.lookup "Host" headers
headers = Wai.requestHeaders wreq
method = "POST"
body = msg.body.value
query <- HTTP.parseQuery . cs <$> requireMeta "Query" msg.metadata
cookie <- cs <$> requireMeta "Cookie" msg.metadata
cookies <- first (InvalidCookie cookie) <$> Cookie.parse $ Web.Cookie.parseCookies cookie
pure $
Request
{ path = pth
, event = Just msg.event
, host
, query
, body
, method
, cookies
, requestId = msg.requestId
}
where
requireMeta :: MetaKey -> Metadata -> Either MessageError Text
requireMeta key m =
maybe (Left $ MissingMeta (cs key)) pure $ lookupMetadata key m
sendUpdateView :: (IOE :> es) => Connection -> Metadata -> View Body () -> Eff es ()
sendUpdateView conn meta vw = do
sendMessage conn meta (RenderedHtml $ renderLazyByteString $ addContext Body vw)
sendRedirect :: (IOE :> es) => Connection -> Metadata -> URI -> Eff es ()
sendRedirect conn meta u = do
sendMessage conn (metaRedirect u <> meta) mempty
sendError :: (IOE :> es) => Connection -> Metadata -> ServerError -> Eff es ()
sendError conn meta (ServerError err body) = do
sendMessage conn (metadata "Error" err <> meta) (RenderedHtml $ renderLazyByteString $ addContext Body body)
-- low level message. Use sendResponse
sendMessage :: (MonadIO m) => Connection -> Metadata -> RenderedHtml -> m ()
sendMessage conn meta' (RenderedHtml html) = do
let out = "|UPDATE|\n" <> cs (renderMetadata meta') <> "\n\n" <> html
liftIO $ WS.sendTextData conn out