, GeneralizedNewtypeDeriving
, DoAndIfThenElse
, OverloadedStrings
+ , QuasiQuotes
, RecordWildCards
, UnicodeSyntax
#-}
-- automatically completes it depending on the status code. (To be
-- exact, such completion only occurs when the 'Resource' transits
-- to this state without even declaring the \"Content-Type\" header
--- field. See 'setContentType'.)
+-- field. See: 'setContentType')
--
-- [/Done/] Everything is over. A 'Resource' can do nothing for the
-- HTTP interaction anymore.
import qualified Blaze.ByteString.Builder as BB
import qualified Blaze.ByteString.Builder.Internal as BB
import Control.Applicative
+import Control.Arrow
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Unicode
import Data.Ascii (Ascii, CIAscii)
import qualified Data.Ascii as A
import qualified Data.Attoparsec.Char8 as P
-import qualified Data.Attoparsec.Lazy as LP
import Data.ByteString (ByteString)
import qualified Data.ByteString as Strict
import qualified Data.ByteString.Lazy as Lazy
import Data.Monoid.Unicode
import Data.Text (Text)
import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
import Data.Time
import qualified Data.Time.HTTP as HTTP
import Network.HTTP.Lucu.Abortion
import Network.HTTP.Lucu.HttpVersion
import Network.HTTP.Lucu.Interaction
import Network.HTTP.Lucu.MultipartForm
+import Network.HTTP.Lucu.Parser
import Network.HTTP.Lucu.Request
import Network.HTTP.Lucu.Resource.Internal
import Network.HTTP.Lucu.Response
-import Network.HTTP.Lucu.MIMEType
+import Network.HTTP.Lucu.MIMEParams
+import Network.HTTP.Lucu.MIMEType (MIMEType(..))
+import qualified Network.HTTP.Lucu.MIMEType as MT
+import Network.HTTP.Lucu.MIMEType.TH
import Network.HTTP.Lucu.Utils
import Network.Socket hiding (accept)
import Network.URI hiding (path)
-- |Get the string representation of the address of remote host. If
-- you want a 'SockAddr' instead of 'HostName', use 'getRemoteAddr'.
getRemoteAddr' ∷ Resource HostName
-getRemoteAddr'
- = do sa ← getRemoteAddr
- (fromJust ∘ fst) <$> (liftIO $ getNameInfo [NI_NUMERICHOST] True False sa)
+getRemoteAddr' = liftIO ∘ toNM =≪ getRemoteAddr
+ where
+ toNM ∷ SockAddr → IO HostName
+ toNM = (fromJust ∘ fst <$>) ∘ getNameInfo [NI_NUMERICHOST] True False
-- |Resolve an address to the remote host.
getRemoteHost ∷ Resource (Maybe HostName)
-getRemoteHost
- = do sa ← getRemoteAddr
- fst <$> (liftIO $ getNameInfo [] True False sa)
+getRemoteHost = liftIO ∘ getHN =≪ getRemoteAddr
+ where
+ getHN ∷ SockAddr → IO (Maybe HostName)
+ getHN = (fst <$>) ∘ getNameInfo [] True False
-- |Get the 'Method' value of the request.
getMethod ∷ Resource Method
-- |This is an analogy of CGI PATH_INFO. 'getPathInfo' always returns
-- @[]@ if the corresponding
--- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' is not greedy. See
--- 'getResourcePath'.
+-- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' is not greedy. See:
+-- 'getResourcePath'
--
-- Note that the returned path components are URI-decoded.
getPathInfo ∷ Resource [Strict.ByteString]
-- |Assume the query part of request URI as
-- application\/x-www-form-urlencoded, and parse it into pairs of
-- @(name, formData)@. This function doesn't read the request
--- body. Field names are decoded in UTF-8 for an hardly avoidable
--- reason. See 'getForm'.
-getQueryForm ∷ Resource [(Text, FormData)]
+-- body.
+getQueryForm ∷ Resource [(Strict.ByteString, FormData)]
getQueryForm = parse' <$> getRequestURI
where
parse' = map toPairWithFormData ∘
drop 1 ∘
uriQuery
-toPairWithFormData ∷ (ByteString, ByteString) → (Text, FormData)
+toPairWithFormData ∷ (ByteString, ByteString) → (Strict.ByteString, FormData)
toPairWithFormData (name, value)
= let fd = FormData {
fdFileName = Nothing
+ , fdMIMEType = [mimeType| text/plain |]
, fdContent = Lazy.fromChunks [value]
}
- in (T.decodeUtf8 name, fd)
+ in (name, fd)
-- |@'getHeader' name@ returns the value of the request header field
-- @name@. Comparison of header name is case-insensitive. Note that
Nothing
→ return []
Just accept
- → case P.parseOnly p (A.toByteString accept) of
+ → case P.parseOnly (finishOff MT.mimeTypeList) (A.toByteString accept) of
Right xs → return xs
Left _ → abort $ mkAbortion' BadRequest
$ "Unparsable Accept: " ⊕ A.toText accept
- where
- p = do xs ← mimeTypeListP
- P.endOfInput
- return xs
-- |Return the list of @(contentCoding, qvalue)@ enumerated on the
-- value of request header \"Accept-Encoding\". The list is sorted in
-- identity のみが許される。
return [("identity", Nothing)]
else
- case P.parseOnly p (A.toByteString ae) of
+ case P.parseOnly (finishOff acceptEncodingList) (A.toByteString ae) of
Right xs → return $ map toTuple $ reverse $ sort xs
Left _ → abort $ mkAbortion' BadRequest
$ "Unparsable Accept-Encoding: " ⊕ A.toText ae
where
- p = do xs ← acceptEncodingListP
- P.endOfInput
- return xs
-
toTuple (AcceptEncoding {..})
= (aeEncoding, aeQValue)
Nothing
→ return Nothing
Just cType
- → case P.parseOnly p (A.toByteString cType) of
+ → case P.parseOnly (finishOff MT.mimeType) (A.toByteString cType) of
Right t → return $ Just t
Left _ → abort $ mkAbortion' BadRequest
$ "Unparsable Content-Type: " ⊕ A.toText cType
- where
- p = do t ← mimeTypeP
- P.endOfInput
- return t
-- |Return the value of request header \"Authorization\" as
-- 'AuthCredential'.
Nothing
→ return Nothing
Just auth
- → case P.parseOnly p (A.toByteString auth) of
+ → case P.parseOnly (finishOff authCredential) (A.toByteString auth) of
Right ac → return $ Just ac
Left _ → return Nothing
- where
- p = do ac ← authCredentialP
- P.endOfInput
- return ac
-- |Tell the system that the 'Resource' found an entity for the
-- request URI. If this is a GET or HEAD request, a found entity means
-- If-Match があればそれを見る。
ifMatch ← getHeader "If-Match"
case ifMatch of
- Nothing → return ()
- Just value → if value ≡ "*" then
- return ()
- else
- case P.parseOnly p (A.toByteString value) of
- Right tags
- -- tags の中に一致するものが無ければ
- -- PreconditionFailed で終了。
- → when ((¬) (any (≡ tag) tags))
- $ abort
- $ mkAbortion' PreconditionFailed
- $ "The entity tag doesn't match: " ⊕ A.toText value
- Left _
- → abort $ mkAbortion' BadRequest
- $ "Unparsable If-Match: " ⊕ A.toText value
+ Nothing
+ → return ()
+ Just value
+ → if value ≡ "*" then
+ return ()
+ else
+ case P.parseOnly (finishOff eTagList) (A.toByteString value) of
+ Right tags
+ -- tags の中に一致するものが無ければ
+ -- PreconditionFailed で終了。
+ → when ((¬) (any (≡ tag) tags))
+ $ abort
+ $ mkAbortion' PreconditionFailed
+ $ "The entity tag doesn't match: " ⊕ A.toText value
+ Left _
+ → abort $ mkAbortion' BadRequest
+ $ "Unparsable If-Match: " ⊕ A.toText value
let statusForNoneMatch
= if method ≡ GET ∨ method ≡ HEAD then
- NotModified
+ fromStatusCode NotModified
else
- PreconditionFailed
+ fromStatusCode PreconditionFailed
-- If-None-Match があればそれを見る。
ifNoneMatch ← getHeader "If-None-Match"
case ifNoneMatch of
- Nothing → return ()
- Just value → if value ≡ "*" then
- abort $ mkAbortion' statusForNoneMatch
- $ "The entity tag matches: *"
- else
- case P.parseOnly p (A.toByteString value) of
- Right tags
- → when (any (≡ tag) tags)
- $ abort
- $ mkAbortion' statusForNoneMatch
- $ "The entity tag matches: " ⊕ A.toText value
- Left _
- → abort $ mkAbortion' BadRequest
- $ "Unparsable If-None-Match: " ⊕ A.toText value
+ Nothing
+ → return ()
+ Just value
+ → if value ≡ "*" then
+ abort $ mkAbortion' statusForNoneMatch
+ $ "The entity tag matches: *"
+ else
+ case P.parseOnly (finishOff eTagList) (A.toByteString value) of
+ Right tags
+ → when (any (≡ tag) tags)
+ $ abort
+ $ mkAbortion' statusForNoneMatch
+ $ "The entity tag matches: " ⊕ A.toText value
+ Left _
+ → abort $ mkAbortion' BadRequest
+ $ "Unparsable If-None-Match: " ⊕ A.toText value
driftTo ReceivingBody
- where
- p = do xs ← eTagListP
- P.endOfInput
- return xs
-- |Tell the system that the 'Resource' found an entity for the
-- request URI. The only difference from 'foundEntity' is that
when (method ≡ POST)
$ abort
$ mkAbortion' InternalServerError
- "Illegal computation of foundTimeStamp for POST request."
+ "Illegal call of foundTimeStamp for POST request."
let statusForIfModSince
= if method ≡ GET ∨ method ≡ HEAD then
- NotModified
+ fromStatusCode NotModified
else
- PreconditionFailed
+ fromStatusCode PreconditionFailed
- -- If-Modified-Since があればそれを見る。
ifModSince ← getHeader "If-Modified-Since"
case ifModSince of
Just str → case HTTP.fromAscii str of
$ abort
$ mkAbortion' statusForIfModSince
$ "The entity has not been modified since " ⊕ A.toText str
- Left _
- → return () -- 不正な時刻は無視
+ Left e
+ → abort $ mkAbortion' BadRequest
+ $ "Malformed If-Modified-Since: " ⊕ T.pack e
Nothing → return ()
- -- If-Unmodified-Since があればそれを見る。
ifUnmodSince ← getHeader "If-Unmodified-Since"
case ifUnmodSince of
Just str → case HTTP.fromAscii str of
$ abort
$ mkAbortion' PreconditionFailed
$ "The entity has not been modified since " ⊕ A.toText str
- Left _
- → return () -- 不正な時刻は無視
+ Left e
+ → abort $ mkAbortion' BadRequest
+ $ "Malformed If-Unmodified-Since: " ⊕ T.pack e
Nothing → return ()
driftTo ReceivingBody
{-# INLINE foundNoEntity' #-}
foundNoEntity' = foundNoEntity Nothing
-
-- |@'getChunks' limit@ attemts to read the entire request body up to
-- @limit@ bytes, and then make the 'Resource' transit to the
-- /Deciding Header/ state. When the actual size of the body is larger
-- Media Type\". If the request has no \"Content-Type\", it aborts
-- with \"400 Bad Request\".
--
--- Field names in @multipart\/form-data@ will be precisely decoded in
--- accordance with RFC 2231. On the other hand,
--- @application\/x-www-form-urlencoded@ says nothing about character
--- encodings for field names, so they'll always be decoded in
--- UTF-8. (This could be a bad design, but I can't think of any better
--- idea.)
-getForm ∷ Maybe Int → Resource [(Text, FormData)]
+-- Note that there are currently a few limitations on parsing
+-- @multipart/form-data@. See: 'parseMultipartFormData'
+getForm ∷ Maybe Int → Resource [(Strict.ByteString, FormData)]
getForm limit
= do cTypeM ← getContentType
case cTypeM of
$ A.toText
$ A.fromAsciiBuilder
$ A.toAsciiBuilder "Unsupported media type: "
- ⊕ printMIMEType cType
+ ⊕ MT.printMIMEType cType
where
readWWWFormURLEncoded
= (map toPairWithFormData ∘ parseWWWFormURLEncoded)
Just a → return a
Nothing → abort $ mkAbortion' BadRequest "Malformed x-www-form-urlencoded"
- readMultipartFormData params
- = case M.lookup "boundary" params of
+ readMultipartFormData (MIMEParams m)
+ = case M.lookup "boundary" m of
Nothing
→ abort $ mkAbortion' BadRequest "Missing boundary of multipart/form-data"
Just boundary
Just b → return b
Nothing → abort $ mkAbortion' BadRequest
$ "Malformed boundary: " ⊕ boundary
- case LP.parse (p b) src of
- LP.Done _ formList
- → return formList
- LP.Fail _ eCtx e
- → abort $ mkAbortion' BadRequest
- $ "Unparsable multipart/form-data: "
- ⊕ T.pack (intercalate ", " eCtx)
- ⊕ ": "
- ⊕ T.pack e
- where
- p b = do xs ← multipartFormP b
- P.endOfInput
- return xs
+ case parseMultipartFormData b src of
+ Right xs → return $ map (first A.toByteString) xs
+ Left err → abort $ mkAbortion' BadRequest $ T.pack err
-- |@'redirect' code uri@ declares the response status as @code@ and
-- \"Location\" header field as @uri@. The @code@ must satisfy
-- 'isRedirection' or it raises an error.
-redirect ∷ StatusCode → URI → Resource ()
-redirect code uri
- = do when (code ≡ NotModified ∨ not (isRedirection code))
+redirect ∷ StatusCode sc ⇒ sc → URI → Resource ()
+redirect sc uri
+ = do when (sc ≈ NotModified ∨ (¬) (isRedirection sc))
$ abort
$ mkAbortion' InternalServerError
$ A.toText
$ A.fromAsciiBuilder
$ A.toAsciiBuilder "Attempted to redirect with status "
- ⊕ printStatusCode code
- setStatus code
+ ⊕ printStatusCode sc
+ setStatus sc
setLocation uri
-- |@'setContentType' mType@ declares the response header
-- mandatory for sending a response body.
setContentType ∷ MIMEType → Resource ()
setContentType
- = setHeader "Content-Type" ∘ A.fromAsciiBuilder ∘ printMIMEType
+ = setHeader "Content-Type" ∘ A.fromAsciiBuilder ∘ MT.printMIMEType
-- |@'setLocation' uri@ declares the response header \"Location\" as
-- @uri@. You usually don't need to call this function directly.
-- |Write a chunk in 'Strict.ByteString' to the response body. You
-- must first declare the response header \"Content-Type\" before
--- applying this function. See 'setContentType'.
+-- applying this function. See: 'setContentType'
putChunk ∷ Strict.ByteString → Resource ()
putChunk = putBuilder ∘ BB.fromByteString
-- can be safely applied to an infinitely long 'Lazy.ByteString'.
--
-- Note that you must first declare the response header
--- \"Content-Type\" before applying this function. See
--- 'setContentType'.
+-- \"Content-Type\" before applying this function. See:
+-- 'setContentType'
putChunks ∷ Lazy.ByteString → Resource ()
putChunks = putBuilder ∘ BB.fromLazyByteString