Files
postgrest/src/Dbapi.hs
T

236 lines
7.7 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
-- {{{ Imports
module Dbapi where
import Types (SqlRow, getRow)
import Middleware(reportPgErrors)
import Control.Monad (join)
import Control.Exception.Base (bracket_)
import Control.Arrow ((***))
import Control.Applicative
import Options.Applicative hiding (columns)
import Data.Maybe (fromMaybe, isJust)
import Text.Regex.TDFA ((=~))
import Data.Map (intersection, fromList, toList, Map)
import Data.List (sort)
import qualified Data.Set as S
import Data.Convertible.Base (convert)
import Data.Text (strip)
import Network.HTTP.Types.Status
import Network.HTTP.Types.Header
import Network.HTTP.Types.URI
import Network.HTTP.Base (urlEncodeVars)
import Network.Wai
import Network.Wai.Internal
import Network.Wai.Middleware.Cors (CorsResourcePolicy(..))
import qualified Data.ByteString.Char8 as BS
import Data.String.Conversions (cs)
import qualified Data.CaseInsensitive as CI
import Database.HDBC.PostgreSQL (Connection)
import PgStructure (printTables, printColumns, primaryKeyColumns,
columns, Column(colName))
import qualified Data.Aeson as JSON
import PgQuery
import RangeQuery
import Data.Ranged.Ranges (emptyRange)
import Codec.Binary.Base64.String (decode)
-- }}}
data AppConfig = AppConfig {
configDbUri :: String
, configPort :: Int
, configSslCert :: FilePath
, configSslKey :: FilePath
, configAnonRole :: String
}
jsonContentType :: (HeaderName, BS.ByteString)
jsonContentType = (hContentType, "application/json")
jsonBodyAction :: Request -> (SqlRow -> IO Response) -> IO Response
jsonBodyAction req handler = do
parse <- jsonBody req
case parse of
Left err -> return $ responseLBS status400 [jsonContentType] json
where json = JSON.encode . JSON.object $ [("error", JSON.String $ cs err)]
Right body -> handler body
jsonBody :: Request -> IO (Either String SqlRow)
jsonBody = fmap JSON.eitherDecode . strictRequestBody
filterByKeys :: Ord a => Map a b -> [a] -> Map a b
filterByKeys m keys =
if null keys then m else
m `intersection` fromList (zip keys $ repeat undefined)
httpRequesterRole :: RequestHeaders -> Connection -> IO LoginAttempt
httpRequesterRole hdrs conn = do
let auth = fromMaybe "" $ lookup hAuthorization hdrs
case BS.split ' ' (cs auth) of
("Basic" : b64 : _) ->
case BS.split ':' $ cs (decode $ cs b64) of
(u:p:_) -> signInRole u p conn
_ -> return MalformedAuth
_ -> return NoCredentials
app :: DbRole -> Connection -> Application
app anonymous conn req respond = do
attempt <- httpRequesterRole (requestHeaders req) conn
case attempt of
MalformedAuth ->
respond $ responseLBS status400 [] "Malformed basic auth header"
LoginFailed ->
respond $ responseLBS status401 [] "Invalid username or password"
LoginSuccess role ->
bracket_ (pgSetRole conn role) (pgResetRole conn) $ appWithRole conn req respond
NoCredentials ->
bracket_ (pgSetRole conn anonymous) (pgResetRole conn) $ appWithRole conn req respond
appWithRole :: Connection -> Application
appWithRole conn = reportPgErrors (\req respond ->
let
path = pathInfo req
verb = requestMethod req
qq = queryString req
hdrs = requestHeaders req
ver = fromMaybe "1" $ requestedVersion hdrs
range = requestedRange hdrs
cRange = requestedContentRange hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
in respond =<< case (path, verb) of
([], _) ->
responseLBS status200 [jsonContentType] <$> printTables ver conn
([table], "OPTIONS") ->
responseLBS status200 [jsonContentType, allOrigins] <$>
printColumns ver (cs table) conn
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else do
r <- respondWithRangedResult <$> getRows ver (cs table) qq range conn
let canonical = urlEncodeVars $ sort $
map (join (***) cs) $
parseSimpleQuery $
rawQueryString req
return $ addHeaders [
("Content-Location",
"/" <> cs table <> "?" <> cs canonical
)] r
([table], "POST") ->
jsonBodyAction req (\row -> do
allvals <- insert ver table row conn
keys <- primaryKeyColumns ver (cs table) conn
let params = urlEncodeVars $ map (\t -> (fst t, "eq." <> convert (snd t) :: String)) $ toList $ filterByKeys allvals keys
return $ responseLBS status201
[ jsonContentType
, (hLocation, "/" <> cs table <> "?" <> cs params)
] ""
)
([table], "PUT") ->
jsonBodyAction req (\row -> do
keys <- primaryKeyColumns ver (cs table) conn
let specifiedKeys = map (cs . fst) qq
if S.fromList keys /= S.fromList specifiedKeys
then return $ responseLBS status405 []
"You must speficy all and only primary keys as params"
else
if isJust cRange
then return $ responseLBS status400 []
"Content-Range is not allowed in PUT request"
else do
cols <- columns ver (cs table) conn
let colNames = S.fromList $ map (cs . colName) cols
let specifiedCols = S.fromList $ map fst $ getRow row
return $ if colNames == specifiedCols then
responseLBS status200 [ jsonContentType ] ""
else if S.null colNames then responseLBS status404 [] ""
else responseLBS status400 []
"You must specify all columns in PUT request"
)
(_, _) ->
return $ responseLBS status404 [] ""
)
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
(Just $ 60*60*24) False False True
corsPolicy :: Request -> Maybe CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of
Just origin -> Just defaultCorsPolicy {
corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders
}
Nothing -> Nothing
where
headers = requestHeaders req
accHeaders = case lookup "access-control-request-headers" headers of
Just hdrs -> map (CI.mk . cs . strip . cs) $ BS.split ',' hdrs
Nothing -> []
respondWithRangedResult :: RangedResult -> Response
respondWithRangedResult rr =
responseLBS status [
jsonContentType,
("Content-Range",
if total == 0 || from > total
then "*/" <> cs (show total)
else cs (show from) <> "-"
<> cs (show to) <> "/"
<> cs (show total)
)
] (rrBody rr)
where
from = rrFrom rr
to = rrTo rr
total = rrTotal rr
status
| from > total = status416
| total == 0 = status204
| (1 + to - from) < total = status206
| otherwise = status200
requestedVersion :: RequestHeaders -> Maybe String
requestedVersion hdrs =
case verStr of
Just [[_, ver]] -> Just ver
_ -> Nothing
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
accept = cs <$> lookup hAccept hdrs :: Maybe String
verStr = (=~ verRegex) <$> accept :: Maybe [[String]]
addHeaders :: ResponseHeaders -> Response -> Response
addHeaders hdrs (ResponseFile s headers fp m) =
ResponseFile s (headers ++ hdrs) fp m
addHeaders hdrs (ResponseBuilder s headers b) =
ResponseBuilder s (headers ++ hdrs) b
addHeaders hdrs (ResponseStream s headers b) =
ResponseStream s (headers ++ hdrs) b
addHeaders hdrs (ResponseRaw s resp) =
ResponseRaw s (addHeaders hdrs resp)