module Snap.AtlassianConnect.QueryStringHash
( createQueryStringHash
, module Network.HTTP.Types
) where
import Control.Applicative
import Control.Monad (guard)
import qualified Crypto.Hash as SHA
import qualified Data.ByteString.Char8 as B
import Data.Function
import Data.List
import Data.List.Split
import Data.Maybe
import Data.Monoid
import Data.Ord
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Network.HTTP.Types
import Network.URI
createQueryStringHash :: StdMethod -> URI -> T.Text -> Maybe T.Text
createQueryStringHash method baseUrl fullUrl =
(TE.decodeUtf8 . SHA.digestToHexByteString . hsh) <$> toCanonicalUrl method baseUrl fullUrl
hsh :: T.Text -> SHA.Digest SHA.SHA256
hsh = SHA.hash . TE.encodeUtf8
toCanonicalUrl :: StdMethod -> URI -> T.Text -> Maybe T.Text
toCanonicalUrl method baseUrl' rawFullUrl = do
fullUrl <- parseURI (T.unpack rawFullUrl)
guard (comparing uriScheme baseUrl' fullUrl == EQ)
guard (comparing uriAuthority baseUrl' fullUrl == EQ)
path' <- uriPath <$> stripBaseUrl baseUrl' fullUrl
let sqs = sortedQueryString fullUrl
return . T.pack $ intercalate "&" [show method, path', sqs]
sortedQueryString :: URI -> String
sortedQueryString = toCanonicalQueryString . parseQueryText . B.pack . uriQuery
stripBaseUrl :: URI -> URI -> Maybe URI
stripBaseUrl baseUrl' fullUrl = do
strippedPath <- stripPrefix (uriPath baseUrl') (uriPath fullUrl)
return fullUrl
{ uriScheme = ""
, uriAuthority = Nothing
, uriPath = strippedPath
}
toCanonicalQueryString :: QueryText -> String
toCanonicalQueryString = T.unpack . render . joinQueryParams . groupAndSortQueryParams . ignoreJWTParam
type QueryParam = (T.Text, Maybe T.Text)
ignoreJWTParam :: [(T.Text, a)] -> [(T.Text, a)]
ignoreJWTParam = filter ((/= "jwt") . fst)
sortParamKeys :: Ord a => [(a, b)] -> [(a, b)]
sortParamKeys = sortBy (comparing fst)
sortParamValues :: Ord b => [(a, b)] -> [(a, b)]
sortParamValues = sortBy (comparing snd)
groupAndSortQueryParams :: [QueryParam] -> [[QueryParam]]
groupAndSortQueryParams = fmap sortParamValues . groupBy ((==) `on` fst) . sortParamKeys
joinQueryParams :: [[QueryParam]] -> [(T.Text, T.Text)]
joinQueryParams = catMaybes . fmap joinQueryParam
joinQueryParam :: [QueryParam] -> Maybe (T.Text, T.Text)
joinQueryParam [] = Nothing
joinQueryParam xs@(x : _) = return (fst x, T.intercalate sep . catMaybes . fmap snd $ xs)
where
sep = T.singleton ','
queryParamToString :: (T.Text, T.Text) -> T.Text
queryParamToString (key, value) = encode key <> T.singleton '=' <> encode value
render :: [(T.Text, T.Text)] -> T.Text
render = T.intercalate "&" . fmap queryParamToString
encode :: T.Text -> T.Text
encode = TE.decodeUtf8 . urlEncode True . TE.encodeUtf8