barley-0.3: src/Barley/Utils.hs
-- Copyright 2010 Google Inc.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
module Barley.Utils (
-- File Utilties
legalPath,
-- Snap Utilities
finish,
routeWhen, routeWhenIO,
finishWithError,
errorBadRequest, errorForbidden, errorNotFound, errorMethodNotAllowed,
processModificationTime,
) where
import Control.Monad (unless, when)
import Control.Monad.IO.Class
import qualified Data.ByteString.Char8 as C
import Data.CIByteString (toCI)
import Snap.Types
import System.FilePath (joinPath, splitDirectories)
import System.Posix.Time (epochTime)
import System.Posix.Types (EpochTime)
-- | Sanitize a file path for serving.
-- If the path contains any illegal path components, then Nothing is returned.
-- Otherwise a new FilePath, built from the dissected components is returned.
--
-- Illegal path components are any that start with a period, including "." and
-- "..", as well as those that contain any of '/', '\', or ':', or are empty.
legalPath :: FilePath -> Maybe FilePath
legalPath p =
if any illegalComponent components
then Nothing
else Just $ joinPath components
where
components = splitDirectories p
illegalComponent "" = True
illegalComponent ('.':_) = True
illegalComponent s = any (`elem` "/\\:") s
--
-- These should be in Snap
--
-- | Immediately exit with what ever response was set in the Snap monad.
finish :: Snap a
finish = getResponse >>= finishWith
-- | If the test is true, then finish immediately with the given action.
routeWhen :: Bool -> Snap () -> Snap ()
routeWhen f act = if f then act >> finish else return ()
-- | Like routeWhen, but test is provided in the IO monad.
routeWhenIO :: IO Bool -> Snap () -> Snap ()
routeWhenIO fio act = do
f <- liftIO fio
if f then act else return ()
-- | Immediately finish with an HTTP error status
finishWithError :: Int -> String -> Snap ()
finishWithError status message =
finishWith $ setResponseStatus status (C.pack message) emptyResponse
-- | Common HTTP error statuses
errorBadRequest, errorForbidden, errorNotFound, errorMethodNotAllowed :: Snap ()
errorBadRequest = finishWithError 400 "Bad Request"
errorForbidden = finishWithError 403 "Forbidden"
errorNotFound = finishWithError 404 "Not Found"
errorMethodNotAllowed = finishWithError 405 "Method Not Allowed"
-- | Process requests for resources that have a known modification time and
-- optional maximum age. Note: This may finish processing if the request
-- includes validators that can be satisfied.
processModificationTime :: EpochTime -> Maybe Int -> Snap ()
processModificationTime modTime maxAge = do
req <- getRequest
maybeM_ validateIfModifiedSince $ (getHeader (toCI $ C.pack "If-Modified-Since")) req
maybeM_ validateIfUnmodifiedSince $ (getHeader (toCI $ C.pack "If-Unmodified-Since")) req
modTimeS <- liftIO $ formatHttpTime modTime
modifyResponse $ setHeader (toCI $ C.pack "Last-Modified") modTimeS
maybeM_ setCacheControlMaxAge maxAge
maybeM_ setExpires maxAge
where
validateIfModifiedSince ims = do
imsTime <- liftIO $ parseHttpTime ims
unless (modTime > imsTime) $ do
modifyResponse clearContentLength
finishWithError 304 "Not Modified"
validateIfUnmodifiedSince ius = do
iusTime <- liftIO $ parseHttpTime ius
when (modTime > iusTime) $ do
modifyResponse clearContentLength
finishWithError 412 "Precondition Failed"
setCacheControlMaxAge ma =
modifyResponse $ setHeader (toCI $ C.pack "Cache-Control")
(C.pack $ "max-age=" ++ show ma)
setExpires ma = do
now <- liftIO $ epochTime
expTimeS <- liftIO $ formatHttpTime (now + fromIntegral ma)
modifyResponse $ setHeader (toCI $ C.pack "Expires") expTimeS
maybeM_ :: (Monad m) => (a -> m ()) -> Maybe a -> m ()
maybeM_ = maybe (return ())