{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TupleSections #-} module Webcrank.Internal.DecisionCore where import qualified Blaze.ByteString.Builder as BB import Control.Applicative import Control.Lens import Control.Monad.Reader import Control.Monad.RWS import Control.Monad.Trans.Either import Control.Monad.Trans.Maybe import Data.ByteString (ByteString) import qualified Data.ByteString as B hiding (drop, take) import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.UTF8 as B import qualified Data.CaseInsensitive as CI import Data.Foldable (find, traverse_) import qualified Data.List as List import qualified Data.List.NonEmpty as NE import Data.Maybe import Data.Text (Text) import Network.HTTP.Date import Network.HTTP.Media import Network.HTTP.Types import Webcrank.Internal.ETag import Webcrank.Internal.Headers import Webcrank.Internal.Types import Webcrank.Internal.ReqData import Webcrank.Internal.ResourceData data FlowChart m a where Decision :: String -> m (FlowChart m a) -> FlowChart m a Done :: m a -> FlowChart m a decision :: String -- label -> m (FlowChart m a) -- next step -> FlowChart m a decision = Decision decision' :: Functor m => String -- label -> m Bool -- condition -> FlowChart m a -- false path -> FlowChart m a -- true path -> FlowChart m a decision' lbl cond ff tf = decision lbl (bool ff tf <$> cond) done :: m a -> FlowChart m a done = Done done' :: (Applicative m, Monad m) => a -> FlowChart m a done' = Done . return runFlowChart :: Monad m => FlowChart m a -> m a runFlowChart = \case Decision _ m -> m >>= runFlowChart Done m -> m respond :: Monad m => Status -> FlowChart (HaltT m) Status respond s = if statusCode s >= 400 && statusCode s < 600 then done $ errorResponse' s else done' s errorResponse :: Monad m => Status -> LB.ByteString -> HaltT m a errorResponse s = HaltT . left . Error s errorResponse' :: Monad m => Status -> HaltT m a errorResponse' s = errorResponse s (LB.fromStrict $ statusMessage s) -- Service Available b13 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b13 = decision' "b13" (callr serviceAvailable) (respond serviceUnavailable503) b12 -- Known method? b12 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b12 = decision' "b12" knownMethod (respond notImplemented501) b11 where knownMethod = (`elem` knownMethods) <$> getRequestMethod -- TODO make it part of the config or part of the resource? knownMethods = [methodGet, methodHead, methodPost, methodPut, methodDelete, methodTrace, methodConnect, methodOptions] -- URI too long? b11 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b11 = decision' "b11" (callr uriTooLong) b10 (respond requestURITooLong414) -- Method allowed? b10 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b10 = decision "b10" $ do ms <- callr' allowedMethods m <- getRequestMethod if m `elem` ms then return b9 else do putResponseHeader hAllow (B.intercalate ", " ms) return $ respond methodNotAllowed405 -- Malformed? b9 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b9 = decision' "b9" (callr malformedRequest) b8 (respond badRequest400) -- Authorized? b8 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b8 = decision "b8" $ callr isAuthorized >>= \case Authorized -> return b7 Unauthorized h -> do putResponseHeader hWWWAuthenticate h return $ respond unauthorized401 -- Forbidden? b7 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b7 = decision' "b7" (callr forbidden) b6 (respond forbidden403) -- Okay Content-* Headers? b6 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b6 = decision' "b6" (callr validContentHeaders) (respond notImplemented501) b5 -- Known Content-Type? b5 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b5 = decision' "b5" (callr knownContentType) (respond unsupportedMediaType415) b4 -- Req Entity Too Large? b4 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b4 = decision' "b4" (callr validEntityLength) (respond requestEntityTooLarge413) b3 -- OPTIONS? b3 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status b3 = decision "b3" $ getRequestMethod >>= \m -> if m == methodOptions then respond ok200 <$ (callr' options >>= putResponseHeaders) else return c3 -- Accept exists? c3 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status c3 = decision "c3" $ getRequestHeader hAccept >>= maybe d4' (return . c4) where d4' = do ts <- callr' contentTypesProvided traverse_ (assign reqDataRespMediaType . fst) (listToMaybe ts) return d4 -- Acceptable media type available? c4 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status c4 acc = decision "c4" $ maybe (return noAcc) d4' =<< match where d4' = (d4 <$) . assign reqDataRespMediaType match = flip matchAccept acc . fmap fst <$> callr' contentTypesProvided noAcc = done $ errorResponse notAcceptable406 "No acceptable media type available" -- Accept-Language exists? d4 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status d4 = decision "d4" $ maybe e5 d5 <$> getRequestHeader hAcceptLanguage -- Acceptable Language available? -- TODO implement proper conneg d5 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status d5 _ = decision "d5" $ return e5 -- Accept-Charset exists? e5 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status e5 = decision "e5" $ getRequestHeader hAcceptCharset >>= maybe (f6 <$ setCharsetFrom "*") (return . e6) -- Acceptable Charset available? e6 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status e6 acc = decision "e6" $ f6 <$ setCharsetFrom acc setCharsetFrom :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> HaltT m () setCharsetFrom acc = callr' charsetsProvided >>= match where match = \case NoCharset -> return () CharsetsProvided cs -> match' (fst <$> NE.toList cs) match' = maybe noAcc matched . flip matchAccept acc matched = assign reqDataRespCharset . Just noAcc = errorResponse notAcceptable406 "No acceptable charset available" -- Accept-Encoding exists? -- also set Content-Type header now that charset is chosen f6 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status f6 = decision "f6" $ do putResponseHeader hContentType =<< do mt <- use reqDataRespMediaType cs <- use reqDataRespCharset return $ renderHeader $ maybe mt ((mt /:) . ("charset",) . CI.original) cs acc <- getRequestHeader hAcceptEncoding maybe (g7 <$ chooseEncoding "identity;q=1.0,*,q=0.5") (return . f7) acc -- Acceptable encoding available? -- -- Note: This is a departure from webmachine and the activity diagram. -- Webcrank will NEVER give a "406 Not Acceptable" response if an encoding -- cannot be found. -- -- If an Accept-Encoding header field is present in a request and none of -- the available representations for the response have a content-coding -- that is listed as acceptable, the origin server SHOULD send a response -- without any content-coding. -- -- http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-24#section-5.3.4 f7 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status f7 acc = decision "f7" $ g7 <$ chooseEncoding acc chooseEncoding :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> HaltT m () chooseEncoding acc = callr' encodingsProvided >>= choose where choose = traverse_ putEnc . match . (fst <$>) match es = matchAccept es acc >>= \case "identity" -> Nothing e -> Just e putEnc e = do putResponseHeader hContentEncoding (CI.original e) reqDataRespEncoding .= Just e -- Resource exists? -- also sets variances now that all conneg is done g7 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status g7 = decision "g7" $ do getVariances >>= \case [] -> return () vs -> putResponseHeader hVary $ renderHeader vs bool h7 g8 <$> callr resourceExists getVariances :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => HaltT m [HeaderName] getVariances = do acc <- bool [] [hAccept] . (> 1) . List.length <$> callr' contentTypesProvided accEnc <- bool [] [hAcceptEncoding] . (> 1) . List.length <$> callr' encodingsProvided accCh <- flip fmap (callr' charsetsProvided) $ \case NoCharset -> [] CharsetsProvided cs -> [hAcceptCharset | NE.length cs > 1] vs <- callr' variances return $ mconcat [acc, accEnc, accCh, vs] -- If-Match exists? g8 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status g8 = decision "g8" $ maybe h10 g9 <$> getRequestHeader hIfMatch -- If-Match: * exists g9 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status g9 h = decision "g9" $ return $ bool (g11 h) h10 (h == "*") -- ETag in If-Match g11 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status g11 h = decision "g11" $ check <$> callr' (runMaybeT . generateETag) where check = maybe (respond preconditionFailed412) (const h10) . mfilter test test e = any (strongComparison e) (parseETags h) -- If-Match exists (no existing resource variant)? h7 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status h7 = decision "h7" $ maybe i7 (const $ respond preconditionFailed412) <$> getRequestHeader hIfMatch -- If-Unmodified-Since exists? h10 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status h10 = decision "h10" $ maybe i12 h11 <$> getRequestHeader hIfUnmodifiedSince -- If-Unmodified-Since is valid date? h11 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status h11 = decision "h11" . return . maybe i12 h12 . parseHTTPDate -- Last-Modified > If-Unmodified-Since? h12 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => HTTPDate -> FlowChart (HaltT m) Status h12 ius = decision "h12" $ check <$> callr' (runMaybeT . lastModified) where check = maybe (respond preconditionFailed412) (const i12) . mfilter (<= ius) -- Moved permanently? (apply PUT to different URI) i4 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status i4 = decision "i4" $ movedPermanentlyOr p3 movedPermanentlyOr :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status -> HaltT m (FlowChart (HaltT m) Status) movedPermanentlyOr n = check =<< callr (runMaybeT . movedPermanently) where check = maybe (return n) moved moved uri = respond movedPermanently301 <$ putResponseLocation uri -- PUT? i7 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status i7 = decision "i7" $ bool k7 i4 . (== methodPut) <$> getRequestMethod -- If-None-Match exists? i12 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status i12 = decision "i12" $ maybe l13 i13 <$> getRequestHeader hIfNoneMatch -- If-None-Match: * exists? i13 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status i13 h = decision "i13" $ return $ bool (k13 h) j18 (h == "*") -- GET or HEAD (resource exists)? j18 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status j18 = decision "j18" $ respond . s <$> getRequestMethod where s = bool preconditionFailed412 notModified304 . (`elem` [methodGet, methodHead]) -- Moved permanently? (non-PUT edition) k5 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status k5 = decision "k5" $ movedPermanentlyOr l5 -- Previously existed? k7 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status k7 = decision "k7" $ bool l7 k5 <$> callr previouslyExisted -- Etag in if-none-match? k13 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status k13 h = decision "k13" $ check <$> callr' (runMaybeT . generateETag) where check = maybe l13 (const j18) . mfilter (`elem` inm) inm = parseETags h -- Moved temporarily? l5 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status l5 = decision "l5" $ callr (runMaybeT . movedTemporarily) >>= check where check = maybe (return m5) redirect redirect uri = respond temporaryRedirect307 <$ putResponseLocation uri -- POST? (resource did not previously exist variant) l7 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status l7 = decision' "l7" ((== methodPost) <$> getRequestMethod) (respond notFound404) m7 -- If-Modified-Since exists? l13 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status l13 = decision "l13" $ maybe m16 l14 <$> getRequestHeader hIfModifiedSince -- If-Modified-Since is a valid date? l14 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => ByteString -> FlowChart (HaltT m) Status l14 = decision "l14" . return . maybe m16 l15 . parseHTTPDate -- If-Modified-Since > Now? l15 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => HTTPDate -> FlowChart (HaltT m) Status l15 ims = decision' "l15" ((ims >) <$> getRequestTime) (l17 ims) m16 -- Last-Modified > If-Modified-Since? l17 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => HTTPDate -> FlowChart (HaltT m) Status l17 ims = decision "l17" $ check <$> callr' (runMaybeT . lastModified) where check = maybe m16 (const $ respond notModified304) . mfilter (<= ims) -- POST? (resource previously existed variant) m5 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status m5 = decision' "m5" ((== methodPost) <$> getRequestMethod) (respond gone410) n5 -- Server allows POST to missing resource? m7 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status m7 = decision' "m7" (callr allowMissingPost) (respond notFound404) n11 -- DELETE? m16 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status m16 = decision' "m16" ((== methodDelete) <$> getRequestMethod) n16 m20 -- DELETE and check for completion? m20 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status m20 = decision "m20" $ callr deleteResource >>= \r -> if r then bool (respond accepted202) n11 <$> callr deleteCompleted else return $ respond internalServerError500 -- Server allows POST to missing resource? (resource did not exist previously) n5 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status n5 = decision' "n5" (callr allowMissingPost) (respond gone410) n11 -- Redirect? n11 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status n11 = decision "n11" $ callr' postAction >>= run where run = \case PostCreate p -> p11 <$ create p PostCreateRedir p -> respond seeOther303 <$ create p PostProcess process -> p11 <$ (process >> encodeBodyIfSet) PostProcessRedir process -> respond seeOther303 <$ (process >>= putResponseLocation >> encodeBodyIfSet) create newPath = do reqURI <- getRequestURI reqDataDispPath .= newPath putResponseLocation $ appendPath reqURI newPath accept appendPath :: ByteString -> [Text] -> ByteString appendPath uri p = h <> p'' where (h, p') = splitURI uri p'' = p' <> dropSlash (BB.toByteString (encodePathSegments p)) dropSlash = B.drop (if B.last p' == 47 then 1 else 0) splitURI :: ByteString -> (ByteString, ByteString) splitURI = ensureNonEmpty . extract where extract path | "http://" `B.isPrefixOf` path = split 7 path | "https://" `B.isPrefixOf` path = split 8 path | otherwise = ("", path) ensureNonEmpty (b, "") = (b, "/") ensureNonEmpty p = p split i path = case breakOnSlash $ B.drop i path of (a, p) -> (B.take i path <> a, p) breakOnSlash = B.breakByte 47 -- POST? (resource exists) n16 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status n16 = decision' "n16" ((== methodPost) <$> getRequestMethod) o16 n11 -- Conflict? (resource exists) o14 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status o14 = decision "o14" isConflict' isConflict' :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => HaltT m (FlowChart (HaltT m) Status) isConflict' = callr' isConflict >>= \conflict -> if conflict then return $ respond conflict409 else p11 <$ accept -- PUT? (resource exists) o16 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status o16 = decision' "o16" ((== methodPut) <$> getRequestMethod) o18 o14 -- Multiple representations? -- also generate body for GET and HEAD o18 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status o18 = decision "o18" $ genBody >> next where genBody = do m <- getRequestMethod putHeaders m putBody m putHeaders m = when (m == methodGet || m == methodHead) $ do let header h rm = traverse_ (putResponseHeader h . renderHeader) =<< callr' (runMaybeT . rm) header hETag generateETag header hLastModified lastModified header hExpires expires putBody m = when (m == methodGet) $ use reqDataRespMediaType >>= \mt -> callr' contentTypesProvided >>= \cts -> case find ((mt ==) . fst) cts of Nothing -> return () Just (_, f) -> f >>= encodeBody >>= assign reqDataRespBody . Just next = bool (respond ok200) (respond multipleChoices300) <$> callr multipleChoices -- Response includes an entity? o20 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status o20 = decision "o20" $ maybe (respond noContent204) (const o18) <$> use reqDataRespBody -- Conflict? (resource doesn't exist) p3 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status p3 = decision "p3" isConflict' -- New resource? (new if there is a location header) p11 :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => FlowChart (HaltT m) Status p11 = decision "p11" $ maybe o20 (const $ respond created201) <$> getResponseLocation accept :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => HaltT m () accept = getRequestContentType >>= accept' >> encodeBodyIfSet where getRequestContentType = fromMaybe "application/octet-stream" <$> getRequestHeader hContentType accept' ct = callr' contentTypesAccepted >>= \fs -> fromMaybe (errorResponse' unsupportedMediaType415) (mapContentMedia fs ct) bool :: a -> a -> Bool -> a bool x y p = if p then y else x (<%%=):: MonadState s m => Lens' s a -> (a -> m a) -> m () l <%%= f = use l >>= f >>= assign l encodeBodyIfSet :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => HaltT m () encodeBodyIfSet = reqDataRespBody <%%= traverse encodeBody encodeBody :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => Body -> HaltT m Body encodeBody = lift . encodeBody' encodeBody' :: (Applicative m, MonadReader r m, HasResourceData r m, MonadState s m, HasReqData s) => Body -> m Body encodeBody' b = do cs <- use reqDataRespCharset >>= \case Nothing -> return id Just cs -> callr'' charsetsProvided <&> \case NoCharset -> id CharsetsProvided cps -> case find ((cs ==) . fst) cps of Nothing -> id Just (_, x) -> x enc <- use reqDataRespEncoding >>= \case Nothing -> return id Just e -> callr'' encodingsProvided <&> \es -> case find ((e ==) . fst) es of Nothing -> id Just (_, x) -> x return $ enc $ cs b