module Network.Hawk.Internal.Client where
import Control.Monad.IO.Class (MonadIO, liftIO)
import Crypto.Hash
import Crypto.Random
import qualified Data.ByteArray as BA (unpack)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as BL
import Data.ByteArray (constEq)
import Data.CaseInsensitive (CI (..))
import qualified Data.Map as M
import Data.Maybe (catMaybes, fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Time.Clock (NominalDiffTime)
import Data.Time.Clock.POSIX
import Network.HTTP.Types.Header (HeaderName, hContentType, hWWWAuthenticate, hAuthorization, ResponseHeaders)
import Network.HTTP.Types.Method (Method)
import Network.HTTP.Types.Status (Status, statusCode)
import Network.HTTP.Types.URI (extractPath)
import Network.HTTP.Client (Response, responseHeaders, responseStatus)
import Network.HTTP.Client (Request, requestHeaders, requestBody, getUri, method, secure)
import Network.HTTP.Client (HttpException(..), HttpExceptionContent(..))
import URI.ByteString (authorityHost, authorityPort,
hostBS, laxURIParserOptions,
parseURI, portNumber, uriAuthority,
uriScheme, schemeBS)
import Data.Typeable (Typeable)
import Control.Exception (Exception, throwIO)
import Control.Monad.Catch as E (MonadThrow(..), MonadCatch(..), handle)
import Control.Monad (join, void)
import Network.Hawk.Internal
import Network.Hawk.Internal.Types
import Network.Hawk.Util
import Network.Iron.Util
import Network.Hawk.Internal.Client.Types
import Network.Hawk.Internal.Client.HeaderParser
header :: Text
-> Method
-> Credentials
-> Maybe PayloadInfo
-> NominalDiffTime
-> Maybe ExtData
-> IO Header
header url method creds payload skew ext =
headerBase url method creds payload skew ext Nothing Nothing
headerOz :: Text
-> Method
-> Credentials
-> Maybe PayloadInfo
-> NominalDiffTime
-> Maybe ExtData
-> Text
-> Maybe Text
-> IO Header
headerOz url method creds payload skew ext app dlg =
headerBase url method creds payload skew ext (Just app) dlg
headerBase :: Text -> Method -> Credentials -> Maybe PayloadInfo -> NominalDiffTime
-> Maybe ExtData -> Maybe Text -> Maybe Text -> IO Header
headerBase url method creds payload skew ext app dlg =
headerBase' url method creds payload skew ext app dlg <$> getPOSIXTime <*> genNonce
headerBase' :: Text -> Method -> Credentials
-> Maybe PayloadInfo -> NominalDiffTime
-> Maybe ExtData -> Maybe Text -> Maybe Text
-> POSIXTime -> ByteString -> Header
headerBase' url method creds payload skew ext app dlg ts nonce = let
arts = header' HawkHeader url method creds payload skew ext app dlg ts nonce
in Header (clientHawkAuth arts) arts
message :: Credentials
-> ByteString
-> Maybe Int
-> BL.ByteString
-> NominalDiffTime
-> IO MessageAuth
message creds host port msg skew =
message' creds host port msg skew <$> getPOSIXTime <*> genNonce
message' :: Credentials
-> ByteString
-> Maybe Int
-> BL.ByteString
-> NominalDiffTime
-> POSIXTime
-> ByteString
-> MessageAuth
message' creds host port msg skew ts nonce = artsMsg creds arts
where
arts = HeaderArtifacts "" host port "" (ccId creds) ts' nonce "" (Just hash) Nothing Nothing Nothing
hash = calculatePayloadHash (ccAlgorithm creds) payload
payload = PayloadInfo "" msg
ts' = ts + skew
artsMsg :: Credentials -> HeaderArtifacts -> MessageAuth
artsMsg creds arts@HeaderArtifacts{..} = MessageAuth haId haTimestamp haNonce hash mac
where
mac = clientMac creds HawkMessage arts
hash = fromMaybe "" haHash
header' :: HawkType -> Text -> Method -> Credentials
-> Maybe PayloadInfo -> NominalDiffTime
-> Maybe ExtData -> Maybe Text -> Maybe Text
-> POSIXTime -> ByteString -> HeaderArtifacts
header' ty url method creds payload skew ext app dlg ts nonce = arts
where
arts = headerArtifacts ts' nonce method (encodeUtf8 url)
hash ext app dlg (ccId creds) mac
hash = calculatePayloadHash (ccAlgorithm creds) <$> payload
mac = clientMac creds ty arts
ts' = ts + skew
headerArtifacts :: POSIXTime -> ByteString -> Method -> ByteString
-> Maybe ByteString -> Maybe ByteString
-> Maybe Text -> Maybe Text
-> ClientId -> ByteString
-> HeaderArtifacts
headerArtifacts now nonce method url hash ext app dlg cid mac =
HeaderArtifacts method host (Just port') resource cid now nonce mac hash ext app dlg
where
s@(SplitURL _ host port resource) = fromMaybe relUrl $ splitUrl url
relUrl = SplitURL HTTP "" Nothing url
port' = urlPort' s
clientHawkAuth :: HeaderArtifacts -> ByteString
clientHawkAuth arts@HeaderArtifacts{..} = hawkHeaderString (hawkHeaderItems items)
where
items = [ ("id", Just . encodeUtf8 $ haId)
, ("ts", Just . S8.pack . show . round $ haTimestamp)
, ("nonce", Just haNonce)
, ("hash", haHash)
, ("ext", haExt)
, ("mac", Just haMac)
, ("app", encodeUtf8 <$> haApp)
, ("dlg", encodeUtf8 <$> haDlg)
]
clientMac :: Credentials -> HawkType -> HeaderArtifacts -> ByteString
clientMac Credentials{..} = calculateMac ccAlgorithm ccKey
hawkHeaderItems :: [(ByteString, Maybe ByteString)] -> [(ByteString, ByteString)]
hawkHeaderItems = catMaybes . map pull
where
pull (k, Just v) = Just (k, v)
pull (k, Nothing) = Nothing
splitUrl :: ByteString -> Maybe SplitURL
splitUrl url = SplitURL s <$> host <*> pure port <*> path
where
p = either (const Nothing) Just (parseURI laxURIParserOptions url)
a = p >>= uriAuthority
https = fmap (schemeBS . uriScheme) p == Just "https"
s = if https then HTTPS else HTTP
host = fmap (hostBS . authorityHost) a
port :: Maybe Int
port = fmap portNumber (a >>= authorityPort)
path = fmap (const (extractPath url)) a
genNonce :: IO ByteString
genNonce = takeRandom 10 <$> getSystemDRG
where takeRandom n g = fst $ withRandomBytes g n (b64url :: ByteString -> ByteString)
data ServerAuthorizationCheck = ServerAuthorizationNotRequired
| ServerAuthorizationRequired
deriving Show
authenticate :: Response body
-> Credentials
-> HeaderArtifacts
-> Maybe BL.ByteString
-> ServerAuthorizationCheck
-> IO (Either String (Maybe ServerAuthorizationHeader))
authenticate r creds artifacts payload saCheck = do
now <- getPOSIXTime
return $ authenticate' r creds artifacts payload saCheck now
authenticate' :: Response body -> Credentials -> HeaderArtifacts
-> Maybe BL.ByteString -> ServerAuthorizationCheck
-> POSIXTime -> Either String (Maybe ServerAuthorizationHeader)
authenticate' r creds arts payload saCheck now = do
let w = responseHeader hWWWAuthenticate r
ts <- mapM (checkWwwAuthenticateHeader creds) w
let sa = responseHeader hServerAuthorization r
msah <- checkServerAuthorizationHeader creds arts saCheck now sa
let ct = fromMaybe "" $ responseHeader hContentType r
let payload' = PayloadInfo ct <$> payload
case msah of
Just sah -> checkPayloadHash (ccAlgorithm creds) (sahHash sah) payload'
Nothing -> return ()
return msah
responseHeader :: HeaderName -> Response body -> Maybe ByteString
responseHeader h = lookup h . responseHeaders
checkWwwAuthenticateHeader :: Credentials -> ByteString -> Either String (Maybe POSIXTime)
checkWwwAuthenticateHeader creds w = parseWwwAuthenticateHeader w >>= check
where
check h | tsm `tsmEq` (wahTsm h) = Right (wahTs h)
| otherwise = Left "Invalid server timestamp hash"
where tsm = calculateTsMac (ccAlgorithm creds) <$> wahTs h
tsmEq :: Maybe ByteString -> Maybe ByteString -> Bool
tsmEq (Just a) (Just b) = a `constEq` b
tsmEq (Just _) Nothing = False
tsmEq _ _ = True
checkServerAuthorizationHeader :: Credentials -> HeaderArtifacts
-> ServerAuthorizationCheck -> POSIXTime
-> Maybe ByteString
-> Either String (Maybe ServerAuthorizationHeader)
checkServerAuthorizationHeader _ _ ServerAuthorizationNotRequired _ Nothing = Right Nothing
checkServerAuthorizationHeader _ _ ServerAuthorizationRequired _ Nothing = Left "Missing Server-Authorization header"
checkServerAuthorizationHeader creds arts _ now (Just sa) =
parseServerAuthorizationHeader sa >>= check
where check sah | sahMac sah `constEq` mac = Right (Just sah)
| otherwise = Left "Bad response mac"
where
arts' = responseArtifacts sah arts
mac = clientMac creds HawkResponse arts'
responseArtifacts :: ServerAuthorizationHeader -> HeaderArtifacts -> HeaderArtifacts
responseArtifacts ServerAuthorizationHeader{..} arts = arts { haMac = sahMac
, haExt = sahExt
, haHash = sahHash
}
getBewit :: Credentials
-> NominalDiffTime
-> Maybe ExtData
-> NominalDiffTime
-> ByteString
-> IO (Maybe ByteString)
getBewit creds ttl ext offset uri = do
exp <- fmap (+ (ttl + offset)) getPOSIXTime
return $ encodeBewit creds <$> bewitArtifacts uri exp ext
bewitArtifacts :: ByteString -> POSIXTime -> Maybe ExtData -> Maybe HeaderArtifacts
bewitArtifacts uri exp ext = make <$> splitUrl uri
where make (SplitURL s host port resource) =
HeaderArtifacts "GET" host port resource "" exp "" "" Nothing ext Nothing Nothing
encodeBewit :: Credentials -> HeaderArtifacts -> ByteString
encodeBewit creds arts = bewitString (ccId creds) (haTimestamp arts) mac (haExt arts)
where mac = clientMac creds HawkBewit arts
bewitString :: ClientId -> POSIXTime -> ByteString -> Maybe ExtData -> ByteString
bewitString cid exp mac ext = b64url (S8.intercalate "\\" parts)
where parts = [ encodeUtf8 cid, S8.pack . show . round $ exp
, mac, fromMaybe "" ext ]
sign :: MonadIO m => Credentials
-> Maybe ExtData
-> Maybe PayloadInfo
-> NominalDiffTime
-> Request
-> m (HeaderArtifacts, Request)
sign creds ext payload skew req = do
let uri = T.pack . show . getUri $ req
hdr <- liftIO $ header uri (method req) creds payload skew ext
return $ (hdrArtifacts hdr, addAuth hdr req)
addAuth :: Header -> Request -> Request
addAuth hdr req = req { requestHeaders = (auth:requestHeaders req) }
where auth = (hAuthorization, hdrField hdr)
data HawkException = HawkServerAuthorizationException String
deriving (Show, Typeable)
instance Exception HawkException
withHawk :: (MonadIO m, MonadCatch m) =>
Credentials
-> Maybe ExtData
-> Maybe PayloadInfo
-> ServerAuthorizationCheck
-> (Request -> m (Response body))
-> Request
-> m (Response body)
withHawk creds ext payload ck http req = withHawkBase creds ext payload ck http req
withHawkPayload :: (MonadIO m, MonadCatch m) =>
Credentials -> Maybe ExtData -> PayloadInfo
-> ServerAuthorizationCheck
-> (Request -> m (Response body)) -> Request -> m (Response body)
withHawkPayload creds ext payload ck http req = withHawkBase creds ext (Just payload) ck http req
withHawkBase :: (MonadIO m, MonadThrow m, MonadCatch m) =>
Credentials -> Maybe ExtData -> Maybe PayloadInfo
-> ServerAuthorizationCheck
-> (Request -> m (Response body)) -> Request -> m (Response body)
withHawkBase creds ext payload ck http req = do
let handle = makeExpiryHandler creds req
r <- handle $ doSignedRequest 0 creds ext payload ck http req
case r of
Right res -> return res
Left ts -> do
now <- liftIO getPOSIXTime
doSignedRequest (now ts) creds ext payload ck http req
makeExpiryHandler :: MonadCatch m => Credentials -> Request
-> m a -> m (Either NominalDiffTime a)
makeExpiryHandler creds req = E.handle handler . fmap Right
where
handler e@(HttpExceptionRequest req (StatusCodeException res _)) =
case wasStale req res creds of
Just ts -> return $ Left ts
Nothing -> throwM e
doSignedRequest :: (MonadIO m, MonadThrow m) =>
NominalDiffTime
-> Credentials -> Maybe ExtData -> Maybe PayloadInfo
-> ServerAuthorizationCheck
-> (Request -> m (Response body)) -> Request
-> m (Response body)
doSignedRequest skew creds ext payload ck http req = do
(arts, req') <- sign creds ext payload skew req
resp <- http req'
auth <- authResponse creds arts ck resp
case auth of
Left e -> throwM $ HawkServerAuthorizationException e
Right _ -> return resp
authResponse :: MonadIO m => Credentials -> HeaderArtifacts
-> ServerAuthorizationCheck
-> Response body -> m (Either String (Maybe ServerAuthorizationHeader))
authResponse creds arts ck resp = do
let body = Nothing
case ck of
ServerAuthorizationRequired ->
liftIO $ authenticate resp creds arts body ck
ServerAuthorizationNotRequired -> return (Right Nothing)
wasStale :: Request -> Response () -> Credentials -> Maybe NominalDiffTime
wasStale req res creds | secure req && unauthorized = serverTs
| otherwise = Nothing
where
unauthorized = statusCode (responseStatus res) == 401
serverTs = hawkTs creds (responseHeaders res)
hawkTs :: Credentials -> ResponseHeaders -> Maybe POSIXTime
hawkTs creds = join . join . fmap parseTs . wwwAuthenticate
where
wwwAuthenticate = lookup hWWWAuthenticate
parseTs = rightJust . checkWwwAuthenticateHeader creds