2019-09-19 07:47:36 +03:00
|
|
|
{-# LANGUAGE TypeApplications #-}
|
2018-06-27 16:11:32 +03:00
|
|
|
module Hasura.Server.Utils where
|
|
|
|
|
2018-09-29 08:42:47 +03:00
|
|
|
import Data.Aeson
|
2019-07-11 08:37:06 +03:00
|
|
|
import Data.Char
|
2019-09-06 01:59:26 +03:00
|
|
|
import Data.List (find)
|
2019-05-14 09:24:46 +03:00
|
|
|
import Data.Time.Clock
|
2019-09-19 07:47:36 +03:00
|
|
|
import Language.Haskell.TH.Syntax (Lift)
|
2019-04-11 07:11:48 +03:00
|
|
|
import System.Environment
|
2018-07-03 18:34:25 +03:00
|
|
|
import System.Exit
|
|
|
|
import System.Process
|
2018-06-28 13:49:40 +03:00
|
|
|
|
2019-09-06 01:59:26 +03:00
|
|
|
import qualified Data.ByteString as B
|
|
|
|
import qualified Data.CaseInsensitive as CI
|
|
|
|
import qualified Data.HashSet as Set
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import qualified Data.Text.Encoding as TE
|
|
|
|
import qualified Data.Text.IO as TI
|
|
|
|
import qualified Data.UUID as UUID
|
|
|
|
import qualified Data.UUID.V4 as UUID
|
|
|
|
import qualified Language.Haskell.TH.Syntax as TH
|
|
|
|
import qualified Network.HTTP.Client as HC
|
|
|
|
import qualified Network.HTTP.Types as HTTP
|
|
|
|
import qualified Text.Regex.TDFA as TDFA
|
|
|
|
import qualified Text.Regex.TDFA.ByteString as TDFA
|
2018-07-03 18:34:25 +03:00
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.Prelude
|
|
|
|
|
2019-07-11 08:37:06 +03:00
|
|
|
newtype RequestId
|
|
|
|
= RequestId { unRequestId :: Text }
|
|
|
|
deriving (Show, Eq, ToJSON, FromJSON)
|
|
|
|
|
2018-07-03 18:34:25 +03:00
|
|
|
jsonHeader :: (T.Text, T.Text)
|
|
|
|
jsonHeader = ("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
2019-05-16 10:45:29 +03:00
|
|
|
sqlHeader :: (T.Text, T.Text)
|
|
|
|
sqlHeader = ("Content-Type", "application/sql; charset=utf-8")
|
|
|
|
|
|
|
|
htmlHeader :: (T.Text, T.Text)
|
|
|
|
htmlHeader = ("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
|
|
|
gzipHeader :: (T.Text, T.Text)
|
|
|
|
gzipHeader = ("Content-Encoding", "gzip")
|
|
|
|
|
2019-09-19 15:54:40 +03:00
|
|
|
brHeader :: (T.Text, T.Text)
|
|
|
|
brHeader = ("Content-Encoding", "br")
|
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
userRoleHeader :: T.Text
|
|
|
|
userRoleHeader = "x-hasura-role"
|
|
|
|
|
2019-02-14 12:37:47 +03:00
|
|
|
deprecatedAccessKeyHeader :: T.Text
|
|
|
|
deprecatedAccessKeyHeader = "x-hasura-access-key"
|
|
|
|
|
|
|
|
adminSecretHeader :: T.Text
|
|
|
|
adminSecretHeader = "x-hasura-admin-secret"
|
2018-06-28 13:49:40 +03:00
|
|
|
|
2018-08-29 08:47:13 +03:00
|
|
|
userIdHeader :: T.Text
|
|
|
|
userIdHeader = "x-hasura-user-id"
|
|
|
|
|
2019-07-11 08:37:06 +03:00
|
|
|
requestIdHeader :: T.Text
|
|
|
|
requestIdHeader = "x-request-id"
|
|
|
|
|
|
|
|
getRequestHeader :: B.ByteString -> [HTTP.Header] -> Maybe B.ByteString
|
|
|
|
getRequestHeader hdrName hdrs = snd <$> mHeader
|
|
|
|
where
|
|
|
|
mHeader = find (\h -> fst h == CI.mk hdrName) hdrs
|
add support for jwt authorization (close #186) (#255)
The API:
1. HGE has `--jwt-secret` flag or `HASURA_GRAPHQL_JWT_SECRET` env var. The value of which is a JSON.
2. The structure of this JSON is: `{"type": "<standard-JWT-algorithms>", "key": "<the-key>"}`
`type` : Standard JWT algos : `HS256`, `RS256`, `RS512` etc. (see jwt.io).
`key`:
i. Incase of symmetric key, the key as it is.
ii. Incase of asymmetric keys, only the public key, in a PEM encoded string or as a X509 certificate.
3. The claims in the JWT token must contain the following:
i. `x-hasura-default-role` field: default role of that user
ii. `x-hasura-allowed-roles` : A list of allowed roles for the user. The default role is overriden by `x-hasura-role` header.
4. The claims in the JWT token, can have other `x-hasura-*` fields where their values can only be strings.
5. The JWT tokens are sent as `Authorization: Bearer <token>` headers.
---
To test:
1. Generate a shared secret (for HMAC-SHA256) or RSA key pair.
2. Goto https://jwt.io/ , add the keys
3. Edit the claims to have `x-hasura-role` (mandatory) and other `x-hasura-*` fields. Add permissions related to the claims to test permissions.
4. Start HGE with `--jwt-secret` flag or `HASURA_GRAPHQL_JWT_SECRET` env var, which takes a JSON string: `{"type": "HS256", "key": "mylongsharedsecret"}` or `{"type":"RS256", "key": "<PEM-encoded-public-key>"}`
5. Copy the JWT token from jwt.io and use it in the `Authorization: Bearer <token>` header.
---
TODO: Support EC public keys. It is blocked on frasertweedale/hs-jose#61
2018-08-30 13:32:09 +03:00
|
|
|
|
2019-07-11 08:37:06 +03:00
|
|
|
getRequestId :: (MonadIO m) => [HTTP.Header] -> m RequestId
|
|
|
|
getRequestId headers =
|
|
|
|
-- generate a request id for every request if the client has not sent it
|
|
|
|
case getRequestHeader (txtToBs requestIdHeader) headers of
|
|
|
|
Nothing -> RequestId <$> liftIO generateFingerprint
|
|
|
|
Just reqId -> return $ RequestId $ bsToTxt reqId
|
2019-05-16 10:45:29 +03:00
|
|
|
|
2019-04-11 07:11:48 +03:00
|
|
|
-- Get an env var during compile time
|
|
|
|
getValFromEnvOrScript :: String -> String -> TH.Q TH.Exp
|
|
|
|
getValFromEnvOrScript n s = do
|
|
|
|
maybeVal <- TH.runIO $ lookupEnv n
|
|
|
|
case maybeVal of
|
|
|
|
Just val -> TH.lift val
|
|
|
|
Nothing -> runScript s
|
|
|
|
|
|
|
|
-- Run a shell script during compile time
|
2018-07-03 18:34:25 +03:00
|
|
|
runScript :: FilePath -> TH.Q TH.Exp
|
|
|
|
runScript fp = do
|
|
|
|
TH.addDependentFile fp
|
|
|
|
fileContent <- TH.runIO $ TI.readFile fp
|
|
|
|
(exitCode, stdOut, stdErr) <- TH.runIO $
|
|
|
|
readProcessWithExitCode "/bin/sh" [] $ T.unpack fileContent
|
|
|
|
when (exitCode /= ExitSuccess) $ fail $
|
|
|
|
"Running shell script " ++ fp ++ " failed with exit code : "
|
|
|
|
++ show exitCode ++ " and with error : " ++ stdErr
|
|
|
|
TH.lift stdOut
|
2018-09-29 08:42:47 +03:00
|
|
|
|
2018-10-12 13:36:47 +03:00
|
|
|
-- find duplicates
|
|
|
|
duplicates :: Ord a => [a] -> [a]
|
|
|
|
duplicates = mapMaybe greaterThanOne . group . sort
|
|
|
|
where
|
|
|
|
greaterThanOne l = bool Nothing (Just $ head l) $ length l > 1
|
2018-11-02 17:01:01 +03:00
|
|
|
|
2019-01-18 13:45:59 +03:00
|
|
|
-- regex related
|
|
|
|
matchRegex :: B.ByteString -> Bool -> T.Text -> Either String Bool
|
|
|
|
matchRegex regex caseSensitive src =
|
|
|
|
fmap (`TDFA.match` TE.encodeUtf8 src) compiledRegexE
|
|
|
|
where
|
|
|
|
compOpt = TDFA.defaultCompOpt
|
|
|
|
{ TDFA.caseSensitive = caseSensitive
|
|
|
|
, TDFA.multiline = True
|
|
|
|
, TDFA.lastStarGreedy = True
|
|
|
|
}
|
|
|
|
execOption = TDFA.defaultExecOpt {TDFA.captureGroups = False}
|
|
|
|
compiledRegexE = TDFA.compile compOpt execOption regex
|
2019-02-14 08:58:38 +03:00
|
|
|
|
|
|
|
|
|
|
|
fmapL :: (a -> a') -> Either a b -> Either a' b
|
|
|
|
fmapL fn (Left e) = Left (fn e)
|
|
|
|
fmapL _ (Right x) = pure x
|
2019-05-14 09:24:46 +03:00
|
|
|
|
|
|
|
-- diff time to micro seconds
|
|
|
|
diffTimeToMicro :: NominalDiffTime -> Int
|
|
|
|
diffTimeToMicro diff =
|
2019-08-01 13:51:59 +03:00
|
|
|
floor (realToFrac diff :: Double) * aSecond
|
2019-05-14 09:24:46 +03:00
|
|
|
where
|
|
|
|
aSecond = 1000 * 1000
|
2019-06-04 13:10:28 +03:00
|
|
|
|
2019-07-11 08:37:06 +03:00
|
|
|
generateFingerprint :: IO Text
|
|
|
|
generateFingerprint = UUID.toText <$> UUID.nextRandom
|
|
|
|
|
2019-07-08 08:51:41 +03:00
|
|
|
-- json representation of HTTP exception
|
|
|
|
httpExceptToJSON :: HC.HttpException -> Value
|
|
|
|
httpExceptToJSON e = case e of
|
|
|
|
HC.HttpExceptionRequest x c ->
|
|
|
|
let reqObj = object
|
|
|
|
[ "host" .= bsToTxt (HC.host x)
|
|
|
|
, "port" .= show (HC.port x)
|
|
|
|
, "secure" .= HC.secure x
|
|
|
|
, "path" .= bsToTxt (HC.path x)
|
|
|
|
, "method" .= bsToTxt (HC.method x)
|
|
|
|
, "proxy" .= (showProxy <$> HC.proxy x)
|
|
|
|
, "redirectCount" .= show (HC.redirectCount x)
|
|
|
|
, "responseTimeout" .= show (HC.responseTimeout x)
|
|
|
|
, "requestVersion" .= show (HC.requestVersion x)
|
|
|
|
]
|
|
|
|
msg = show c
|
|
|
|
in object ["request" .= reqObj, "message" .= msg]
|
|
|
|
_ -> toJSON $ show e
|
|
|
|
where
|
|
|
|
showProxy (HC.Proxy h p) =
|
|
|
|
"host: " <> bsToTxt h <> " port: " <> T.pack (show p)
|
2019-06-04 13:10:28 +03:00
|
|
|
|
2019-07-08 08:51:41 +03:00
|
|
|
-- ignore the following request headers from the client
|
2019-06-04 13:10:28 +03:00
|
|
|
commonClientHeadersIgnored :: (IsString a) => [a]
|
|
|
|
commonClientHeadersIgnored =
|
|
|
|
[ "Content-Length", "Content-MD5", "User-Agent", "Host"
|
|
|
|
, "Origin", "Referer" , "Accept", "Accept-Encoding"
|
|
|
|
, "Accept-Language", "Accept-Datetime"
|
|
|
|
, "Cache-Control", "Connection", "DNT", "Content-Type"
|
|
|
|
]
|
|
|
|
|
|
|
|
commonResponseHeadersIgnored :: (IsString a) => [a]
|
|
|
|
commonResponseHeadersIgnored =
|
|
|
|
[ "Server", "Transfer-Encoding", "Cache-Control"
|
|
|
|
, "Access-Control-Allow-Credentials"
|
|
|
|
, "Access-Control-Allow-Methods"
|
|
|
|
, "Access-Control-Allow-Origin"
|
|
|
|
, "Content-Type", "Content-Length"
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
filterRequestHeaders :: [HTTP.Header] -> [HTTP.Header]
|
|
|
|
filterRequestHeaders =
|
|
|
|
filterHeaders $ Set.fromList commonClientHeadersIgnored
|
|
|
|
|
|
|
|
-- ignore the following response headers from remote
|
|
|
|
filterResponseHeaders :: [HTTP.Header] -> [HTTP.Header]
|
|
|
|
filterResponseHeaders =
|
|
|
|
filterHeaders $ Set.fromList commonResponseHeadersIgnored
|
|
|
|
|
|
|
|
filterHeaders :: Set.HashSet HTTP.HeaderName -> [HTTP.Header] -> [HTTP.Header]
|
|
|
|
filterHeaders list = filter (\(n, _) -> not $ n `Set.member` list)
|
2019-07-11 08:37:06 +03:00
|
|
|
|
|
|
|
hyphenate :: String -> String
|
|
|
|
hyphenate = u . applyFirst toLower
|
|
|
|
where u [] = []
|
|
|
|
u (x:xs) | isUpper x = '-' : toLower x : hyphenate xs
|
|
|
|
| otherwise = x : u xs
|
|
|
|
|
|
|
|
applyFirst :: (Char -> Char) -> String -> String
|
|
|
|
applyFirst _ [] = []
|
|
|
|
applyFirst f [x] = [f x]
|
|
|
|
applyFirst f (x:xs) = f x: xs
|
2019-09-19 07:47:36 +03:00
|
|
|
|
|
|
|
-- | The version integer
|
|
|
|
data APIVersion
|
|
|
|
= VIVersion1
|
|
|
|
| VIVersion2
|
|
|
|
deriving (Show, Eq, Lift)
|
|
|
|
|
|
|
|
instance ToJSON APIVersion where
|
|
|
|
toJSON VIVersion1 = toJSON @Int 1
|
|
|
|
toJSON VIVersion2 = toJSON @Int 2
|
|
|
|
|
|
|
|
instance FromJSON APIVersion where
|
|
|
|
parseJSON v = do
|
|
|
|
verInt :: Int <- parseJSON v
|
|
|
|
case verInt of
|
|
|
|
1 -> return VIVersion1
|
|
|
|
2 -> return VIVersion2
|
|
|
|
i -> fail $ "expected 1 or 2, encountered " ++ show i
|
2019-11-20 09:47:06 +03:00
|
|
|
|
|
|
|
makeReasonMessage :: [a] -> (a -> Text) -> Text
|
|
|
|
makeReasonMessage errors showError =
|
|
|
|
case errors of
|
|
|
|
[singleError] -> "because " <> showError singleError
|
|
|
|
_ -> "for the following reasons:\n" <> T.unlines
|
|
|
|
(map ((" • " <>) . showError) errors)
|
2020-01-07 23:25:32 +03:00
|
|
|
|
|
|
|
withElapsedTime :: MonadIO m => m a -> m (NominalDiffTime, a)
|
|
|
|
withElapsedTime ma = do
|
|
|
|
t1 <- liftIO getCurrentTime
|
|
|
|
a <- ma
|
|
|
|
t2 <- liftIO getCurrentTime
|
|
|
|
return (diffUTCTime t2 t1, a)
|