{-# LANGUAGE CPP #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
module Airship.Internal.Decision
( flow
, appendRequestPath
) where
import Airship.Headers (addResponseHeader)
import Airship.Internal.Date (parseRfc1123Date,
utcTimeToRfc1123)
import Airship.Internal.Parsers (parseEtagList)
import Airship.Resource (PostResponse (..),
Resource (..))
import Airship.Types (Response (..),
ResponseBody (..),
Webmachine, addTrace,
etagToByteString,
getResponseBody,
getResponseHeaders, halt,
pathInfo, putResponseBody,
request, requestHeaders,
requestMethod, requestTime)
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative ((<$>))
#endif
import Control.Monad (when)
import Control.Monad.Trans (lift)
import Control.Monad.Trans.State.Strict (StateT (..), evalStateT, get,
modify)
import Blaze.ByteString.Builder (toByteString)
import Data.ByteString (ByteString, intercalate)
import Data.Maybe (isJust)
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Network.HTTP.Media
import qualified Network.HTTP.Types as HTTP
hAcceptCharset :: HTTP.HeaderName
hAcceptCharset = "Accept-Charset"
hAcceptEncoding :: HTTP.HeaderName
hAcceptEncoding = "Accept-Encoding"
hIfMatch :: HTTP.HeaderName
hIfMatch = "If-Match"
hIfUnmodifiedSince :: HTTP.HeaderName
hIfUnmodifiedSince = "If-Unmodified-Since"
hIfNoneMatch :: HTTP.HeaderName
hIfNoneMatch = "If-None-Match"
data FlowState m = FlowState
{ _contentType :: Maybe (MediaType, Webmachine m ResponseBody) }
type FlowStateT m a = StateT (FlowState m) (Webmachine m) a
type Flow m = Resource m -> FlowStateT m Response
initFlowState :: FlowState m
initFlowState = FlowState Nothing
flow :: Monad m => Resource m -> Webmachine m Response
flow r = evalStateT (b13 r) initFlowState
trace :: Monad m => ByteString -> FlowStateT m ()
trace a = lift $ addTrace a
newtype IfMatch = IfMatch ByteString
newtype IfNoneMatch = IfNoneMatch ByteString
negotiateContentTypesAccepted :: Monad m => [(MediaType, Webmachine m a)] -> FlowStateT m a
negotiateContentTypesAccepted accepted = do
req <- lift request
let reqHeaders = requestHeaders req
result = do
cType <- lookup HTTP.hContentType reqHeaders
mapContentMedia accepted cType
case result of
(Just process) -> lift process
Nothing -> lift $ halt HTTP.status415
appendRequestPath :: Monad m => [Text] -> Webmachine m ByteString
appendRequestPath ts = do
currentPath <- pathInfo <$> request
return $ toByteString (HTTP.encodePathSegments (currentPath ++ ts))
requestHeaderDate :: Monad m => HTTP.HeaderName ->
Webmachine m (Maybe UTCTime)
requestHeaderDate headerName = do
req <- request
let reqHeaders = requestHeaders req
dateHeader = lookup headerName reqHeaders
parsedDate = dateHeader >>= parseRfc1123Date
return parsedDate
writeCacheTags :: Monad m => Resource m -> FlowStateT m ()
writeCacheTags Resource{..} = lift $ do
etag <- generateETag
case etag of
Nothing -> return ()
Just t -> addResponseHeader ("ETag", etagToByteString t)
modified <- lastModified
case modified of
Nothing -> return ()
Just d -> addResponseHeader ("Last-Modified", utcTimeToRfc1123 d)
b13, b12, b11, b10, b09, b08, b07, b06, b05, b04, b03 :: Monad m => Flow m
c04, c03 :: Monad m => Flow m
d05, d04 :: Monad m => Flow m
e06, e05 :: Monad m => Flow m
f07, f06 :: Monad m => Flow m
g11, g09 :: Monad m => IfMatch -> Flow m
g08, g07 :: Monad m => Flow m
h12, h11, h10, h07 :: Monad m => Flow m
i13 :: Monad m => IfNoneMatch -> Flow m
i12, i07, i04 :: Monad m => Flow m
j18 :: Monad m => Flow m
k13 :: Monad m => IfNoneMatch -> Flow m
k07, k05 :: Monad m => Flow m
l17, l15, l14, l13, l07, l05 :: Monad m => Flow m
m20, m16, m07, m05 :: Monad m => Flow m
n16, n11, n05 :: Monad m => Flow m
o20, o18, o17, o16, o14 :: Monad m => Flow m
p11, p03 :: Monad m => Flow m
b13 r@Resource{..} = do
trace "b13"
available <- lift serviceAvailable
if available
then b12 r
else lift $ halt HTTP.status503
b12 r@Resource{..} = do
trace "b12"
req <- lift request
let knownMethods = [ HTTP.methodGet
, HTTP.methodPost
, HTTP.methodHead
, HTTP.methodPut
, HTTP.methodDelete
, HTTP.methodTrace
, HTTP.methodConnect
, HTTP.methodOptions
, HTTP.methodPatch
]
if requestMethod req `elem` knownMethods
then b11 r
else lift $ halt HTTP.status501
b11 r@Resource{..} = do
trace "b11"
long <- lift uriTooLong
if long
then lift $ halt HTTP.status414
else b10 r
b10 r@Resource{..} = do
trace "b10"
req <- lift request
allowed <- lift allowedMethods
if requestMethod req `elem` allowed
then b09 r
else do
lift $ addResponseHeader ("Allow", intercalate "," allowed)
lift $ halt HTTP.status405
b09 r@Resource{..} = do
trace "b09"
malformed <- lift malformedRequest
if malformed
then lift $ halt HTTP.status400
else b08 r
b08 r@Resource{..} = do
trace "b08"
authorized <- lift isAuthorized
if authorized
then b07 r
else lift $ halt HTTP.status401
b07 r@Resource{..} = do
trace "b07"
forbid <- lift forbidden
if forbid
then lift $ halt HTTP.status403
else b06 r
b06 r@Resource{..} = do
trace "b06"
validC <- lift validContentHeaders
if validC
then b05 r
else lift $ halt HTTP.status501
b05 r@Resource{..} = do
trace "b05"
known <- lift knownContentType
if known
then b04 r
else lift $ halt HTTP.status415
b04 r@Resource{..} = do
trace "b04"
large <- lift entityTooLarge
if large
then lift $ halt HTTP.status413
else b03 r
b03 r@Resource{..} = do
trace "b03"
req <- lift request
allowed <- lift allowedMethods
if requestMethod req == HTTP.methodOptions
then do
lift $ addResponseHeader ("Allow", intercalate "," allowed)
lift $ halt HTTP.status204
else c03 r
c04 r@Resource{..} = do
trace "c04"
req <- lift request
provided <- lift contentTypesProvided
let reqHeaders = requestHeaders req
result = do
acceptStr <- lookup HTTP.hAccept reqHeaders
(acceptTyp, resource) <- mapAcceptMedia provided' acceptStr
Just (acceptTyp, resource)
where
provided' = map dupContentType provided
dupContentType (a, b) = (a, (a, b))
case result of
Nothing -> lift $ halt HTTP.status406
Just res -> do
modify (\fs -> fs { _contentType = Just res })
d04 r
c03 r@Resource{..} = do
trace "c03"
req <- lift request
let reqHeaders = requestHeaders req
case lookup HTTP.hAccept reqHeaders of
(Just _h) ->
c04 r
Nothing ->
d04 r
d05 r@Resource{..} = do
trace "d05"
langAvailable <- lift languageAvailable
if langAvailable
then e05 r
else lift $ halt HTTP.status406
d04 r@Resource{..} = do
trace "d04"
req <- lift request
let reqHeaders = requestHeaders req
case lookup HTTP.hAcceptLanguage reqHeaders of
(Just _h) ->
d05 r
Nothing ->
e05 r
e06 r@Resource{..} = do
trace "e06"
f06 r
e05 r@Resource{..} = do
trace "e05"
req <- lift request
let reqHeaders = requestHeaders req
case lookup hAcceptCharset reqHeaders of
(Just _h) ->
e06 r
Nothing ->
f06 r
f07 r@Resource{..} = do
trace "f07"
g07 r
f06 r@Resource{..} = do
trace "f06"
req <- lift request
let reqHeaders = requestHeaders req
case lookup hAcceptEncoding reqHeaders of
(Just _h) ->
f07 r
Nothing ->
g07 r
g11 (IfMatch ifMatch) r@Resource{..} = do
trace "g11"
let etags = parseEtagList ifMatch
if null etags
then lift $ halt HTTP.status412
else h10 r
g09 ifMatch r@Resource{..} = do
trace "g09"
case ifMatch of
(IfMatch "*") ->
h10 r
_ ->
g11 ifMatch r
g08 r@Resource{..} = do
trace "g08"
req <- lift request
let reqHeaders = requestHeaders req
case IfMatch <$> lookup hIfMatch reqHeaders of
(Just h) ->
g09 h r
Nothing ->
h10 r
g07 r@Resource{..} = do
trace "g07"
exists <- lift resourceExists
if exists
then g08 r
else h07 r
h12 r@Resource{..} = do
trace "h12"
modified <- lift lastModified
parsedDate <- lift $ requestHeaderDate hIfUnmodifiedSince
let maybeGreater = do
lastM <- modified
headerDate <- parsedDate
return (lastM > headerDate)
if maybeGreater == Just True
then lift $ halt HTTP.status412
else i12 r
h11 r@Resource{..} = do
trace "h11"
parsedDate <- lift $ requestHeaderDate hIfUnmodifiedSince
if isJust parsedDate
then h12 r
else i12 r
h10 r@Resource{..} = do
trace "h10"
req <- lift request
let reqHeaders = requestHeaders req
case lookup hIfUnmodifiedSince reqHeaders of
(Just _h) ->
h11 r
Nothing ->
i12 r
h07 r@Resource {..} = do
trace "h07"
req <- lift request
let reqHeaders = requestHeaders req
case lookup hIfMatch reqHeaders of
(Just "*") ->
lift $ halt HTTP.status412
_ ->
i07 r
i13 ifNoneMatch r@Resource{..} = do
trace "i13"
case ifNoneMatch of
(IfNoneMatch "*") ->
j18 r
_ ->
k13 ifNoneMatch r
i12 r@Resource{..} = do
trace "i12"
req <- lift request
let reqHeaders = requestHeaders req
case IfNoneMatch <$> lookup hIfNoneMatch reqHeaders of
(Just h) ->
i13 h r
Nothing ->
l13 r
i07 r = do
trace "i07"
req <- lift request
if requestMethod req == HTTP.methodPut
then i04 r
else k07 r
i04 r@Resource{..} = do
trace "i04"
moved <- lift movedPermanently
case moved of
(Just loc) -> do
lift $ addResponseHeader ("Location", loc)
lift $ halt HTTP.status301
Nothing ->
p03 r
j18 _ = do
trace "j18"
req <- lift request
let getOrHead = [ HTTP.methodGet
, HTTP.methodHead
]
if requestMethod req `elem` getOrHead
then lift $ halt HTTP.status304
else lift $ halt HTTP.status412
k13 (IfNoneMatch ifNoneMatch) r@Resource{..} = do
trace "k13"
let etags = parseEtagList ifNoneMatch
if null etags
then l13 r
else j18 r
k07 r@Resource{..} = do
trace "k07"
prevExisted <- lift previouslyExisted
if prevExisted
then k05 r
else l07 r
k05 r@Resource{..} = do
trace "k05"
moved <- lift movedPermanently
case moved of
(Just loc) -> do
lift $ addResponseHeader ("Location", loc)
lift $ halt HTTP.status301
Nothing ->
l05 r
l17 r@Resource{..} = do
trace "l17"
parsedDate <- lift $ requestHeaderDate HTTP.hIfModifiedSince
modified <- lift lastModified
let maybeGreater = do
lastM <- modified
ifModifiedSince <- parsedDate
return (lastM > ifModifiedSince)
if maybeGreater == Just True
then m16 r
else lift $ halt HTTP.status304
l15 r@Resource{..} = do
trace "l15"
parsedDate <- lift $ requestHeaderDate HTTP.hIfModifiedSince
now <- lift requestTime
let maybeGreater = (> now) <$> parsedDate
if maybeGreater == Just True
then m16 r
else l17 r
l14 r@Resource{..} = do
trace "l14"
req <- lift request
let reqHeaders = requestHeaders req
dateHeader = lookup HTTP.hIfModifiedSince reqHeaders
validDate = isJust (dateHeader >>= parseRfc1123Date)
if validDate
then l15 r
else m16 r
l13 r@Resource{..} = do
trace "l13"
req <- lift request
let reqHeaders = requestHeaders req
case lookup HTTP.hIfModifiedSince reqHeaders of
(Just _h) ->
l14 r
Nothing ->
m16 r
l07 r = do
trace "l07"
req <- lift request
if requestMethod req == HTTP.methodPost
then m07 r
else lift $ halt HTTP.status404
l05 r@Resource{..} = do
trace "l05"
moved <- lift movedTemporarily
case moved of
(Just loc) -> do
lift $ addResponseHeader ("Location", loc)
lift $ halt HTTP.status307
Nothing ->
m05 r
m20 r@Resource{..} = do
trace "m20"
deleteAccepted <- lift deleteResource
if deleteAccepted
then do
completed <- lift deleteCompleted
if completed
then o20 r
else lift $ halt HTTP.status202
else lift $ halt HTTP.status500
m16 r = do
trace "m16"
req <- lift request
if requestMethod req == HTTP.methodDelete
then m20 r
else n16 r
m07 r@Resource{..} = do
trace "m07"
allowMissing <- lift allowMissingPost
if allowMissing
then n11 r
else lift $ halt HTTP.status404
m05 r = do
trace "m05"
req <- lift request
if requestMethod req == HTTP.methodPost
then n05 r
else lift $ halt HTTP.status410
n16 r = do
trace "n16"
req <- lift request
if requestMethod req == HTTP.methodPost
then n11 r
else o16 r
n11 r@Resource{..} = trace "n11" >> lift processPost >>= flip processPostAction r
create :: Monad m => [Text] -> Resource m -> FlowStateT m ()
create ts Resource{..} = do
loc <- lift (appendRequestPath ts)
lift (addResponseHeader ("Location", loc))
lift contentTypesAccepted >>= negotiateContentTypesAccepted
processPostAction :: Monad m => PostResponse m -> Flow m
processPostAction (PostCreate ts) r = do
create ts r
p11 r
processPostAction (PostCreateRedirect ts) r = do
create ts r
lift $ halt HTTP.status303
processPostAction (PostProcess accepted) r = do
negotiateContentTypesAccepted accepted >> p11 r
processPostAction (PostProcessRedirect accepted) _r = do
locBs <- negotiateContentTypesAccepted accepted
lift $ addResponseHeader ("Location", locBs)
lift $ halt HTTP.status303
n05 r@Resource{..} = do
trace "n05"
allow <- lift allowMissingPost
if allow
then n11 r
else lift $ halt HTTP.status410
o20 r = do
trace "o20"
body <- lift getResponseBody
case body of
Empty -> lift $ halt HTTP.status204
_ -> o18 r
o18 r@Resource{..} = do
trace "o18"
multiple <- lift multipleChoices
if multiple
then lift $ halt HTTP.status300
else do
req <- lift request
let getOrHead = [ HTTP.methodGet
, HTTP.methodHead
]
when (requestMethod req `elem` getOrHead) $ do
m <- _contentType <$> get
(cType, body) <- case m of
Nothing -> do
provided <- lift contentTypesProvided
return (head provided)
Just (cType, body) ->
return (cType, body)
b <- lift body
lift $ putResponseBody b
lift $ addResponseHeader ("Content-Type", renderHeader cType)
writeCacheTags r
lift $ halt HTTP.status200
o16 r = do
trace "o16"
req <- lift request
if requestMethod req == HTTP.methodPut
then o14 r
else o17 r
o17 r@Resource{..} = do
trace "o17"
req <- lift request
if requestMethod req /= HTTP.methodPatch
then o18 r
else lift patchContentTypesAccepted >>= negotiateContentTypesAccepted >> o20 r
o14 r@Resource{..} = do
trace "o14"
conflict <- lift isConflict
if conflict
then lift $ halt HTTP.status409
else lift contentTypesAccepted >>= negotiateContentTypesAccepted >> p11 r
p11 r = do
trace "p11"
headers <- lift getResponseHeaders
case lookup HTTP.hLocation headers of
(Just _) ->
lift $ halt HTTP.status201
_ ->
o20 r
p03 r@Resource{..} = do
trace "p03"
conflict <- lift isConflict
if conflict
then lift $ halt HTTP.status409
else lift contentTypesAccepted >>= negotiateContentTypesAccepted >> p11 r