inspector-wrecker-0.1.0.0: src/Wrecker/Inspector/HAR.hs
{-# LANGUAGE LambdaCase, RecordWildCards, OverloadedStrings, CPP #-}
module Wrecker.Inspector.HAR where
import Control.Exception
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Types as HTTP
import Wrecker(Recorder, record)
import Data.Time.Clock
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Char8 as BSC
import Data.ByteString (ByteString)
import qualified Data.CaseInsensitive as CI
import Data.Text.Encoding
import Data.Maybe
import Data.Time.Calendar
import Data.Aeson
import Data.Either
import Control.Monad
import Network.HTTP.Client.TLS
import Data.Time.Format
import Network.Connection
import Data.Default
parseExpiry :: Text -> Either SomeException UTCTime
parseExpiry
= maybe (Left $ toException $ userError "can't format expires time") Right
. parseTimeM True defaultTimeLocale "%FT%T%QZ"
. T.unpack
data HAR = HAR
{ hLog :: Log
} deriving (Show, Eq)
instance FromJSON HAR where
parseJSON = withObject "HAR" $ \o -> do
HAR <$> o .: "log"
data Log = Log
{ lEntries :: [Entry]
} deriving (Show, Eq)
instance FromJSON Log where
parseJSON = withObject "Log" $ \o -> do
Log <$> o .: "entries"
data Entry = Entry
{ eRequest :: Request
} deriving (Show, Eq)
instance FromJSON Entry where
parseJSON = withObject "Entry" $ \o -> do
Entry <$> o .: "request"
data Request = Request
{ rMethod :: Text
, rUrl :: Text
, rHttpVersion :: Text
, rHeaders :: [Header]
, rQueryString :: [QueryItem]
, rCookies :: [Cookie]
, rPostData :: Maybe PostData
} deriving (Show, Eq)
instance FromJSON Request where
parseJSON = withObject "Request" $ \o -> do
Request <$> o .: "method"
<*> o .: "url"
<*> o .: "httpVersion"
<*> o .: "headers"
<*> o .: "queryString"
<*> o .: "cookies"
<*> o .:? "postData" .!= Nothing
data Cookie = Cookie
{ cName :: Text
, cValue :: Text
, cDomain :: Maybe Text
, cPath :: Maybe Text
, cExpires :: Maybe Text
, cHttpOnly :: Maybe Bool
, cSecure :: Maybe Bool
} deriving (Show, Eq)
instance FromJSON Cookie where
parseJSON = withObject "Cookie" $ \o -> do
Cookie <$> o .: "name"
<*> o .: "value"
<*> o .:? "domain" .!= Nothing
<*> o .:? "path" .!= Nothing
<*> o .:? "expires" .!= Nothing
<*> o .:? "httpOnly" .!= Nothing
<*> o .:? "secure" .!= Nothing
data Header = Header
{ hName :: Text
, hValue :: Text
} deriving (Show, Eq)
instance FromJSON Header where
parseJSON = withObject "Header" $ \o -> do
Header <$> o .: "name"
<*> o .: "value"
data QueryItem = QueryItem
{ qName :: Text
, qValue :: Text
} deriving (Show, Eq)
instance FromJSON QueryItem where
parseJSON = withObject "QueryItem" $ \o -> do
QueryItem <$> o .: "name"
<*> o .: "value"
data PostData = PostData
{ pMimeType :: Text
, pText :: Text
} deriving (Show, Eq)
instance FromJSON PostData where
parseJSON = withObject "PostData" $ \o -> do
PostData <$> o .: "mimeType"
<*> o .: "text"
toHTTP :: UTCTime -> Request -> Either SomeException HTTP.Request
toHTTP now Request {..} = do
initRequest <- HTTP.parseUrlThrow
$ T.unpack
$ head
$ T.splitOn "?"
$ decodeUtf8
$ HTTP.urlDecode False
$ encodeUtf8 rUrl
version <- parseHTTPVersion rHttpVersion
httpCookies <- mapM (toHTTPCookie now) rCookies
return $ initRequest
{ HTTP.method = encodeUtf8 rMethod
, HTTP.queryString = toHTTPQueryString rQueryString
, HTTP.requestHeaders = filter ((/= "Content-Length"). fst) $ map toHTTPHeader rHeaders
, HTTP.requestBody = HTTP.RequestBodyLBS . BSL.fromStrict $ maybe "" toBody rPostData
, HTTP.requestVersion = version
, HTTP.cookieJar = Just $ HTTP.createCookieJar httpCookies
}
toBody :: PostData -> ByteString
toBody PostData {..} = encodeUtf8 pText
parseHTTPVersion :: Text -> Either SomeException HTTP.HttpVersion
parseHTTPVersion = \case
"HTTP/2.0" -> Right $ HTTP.HttpVersion 2 0
"HTTP/1.1" -> Right $ HTTP.http11
"HTTP/1.0" -> Right $ HTTP.http10
"HTTP/0.9" -> Right $ HTTP.http09
"unknown" -> Right $ HTTP.http11
x -> Left $ toException $ userError $ "unknown http version " ++ T.unpack x
toHTTPQueryString :: [QueryItem] -> ByteString
toHTTPQueryString = HTTP.urlDecode False . HTTP.renderQuery False . map toQueryItem
toQueryItem :: QueryItem -> HTTP.QueryItem
toQueryItem QueryItem {..} = (encodeUtf8 qName, Just $ encodeUtf8 qValue)
toHTTPHeader :: Header -> HTTP.Header
toHTTPHeader Header {..} = (CI.mk $ encodeUtf8 hName, encodeUtf8 hValue)
toHTTPCookie :: UTCTime -> Cookie -> Either SomeException HTTP.Cookie
toHTTPCookie now Cookie {..} = do
let nullUTCTime = UTCTime (365000 `addDays` utctDay now) (secondsToDiffTime 0)
expiryTime <- maybe (return nullUTCTime) parseExpiry cExpires
return HTTP.Cookie
{ HTTP.cookie_name = encodeUtf8 cName
, HTTP.cookie_value = encodeUtf8 cValue
, HTTP.cookie_expiry_time = expiryTime
, HTTP.cookie_domain = maybe "" encodeUtf8 cDomain
, HTTP.cookie_path = maybe "" encodeUtf8 cPath
, HTTP.cookie_creation_time = now
, HTTP.cookie_last_access_time = now
, HTTP.cookie_persistent = True
, HTTP.cookie_host_only = False
, HTTP.cookie_secure_only = fromMaybe False cSecure
, HTTP.cookie_http_only = fromMaybe False cHttpOnly
}
makeRequestKey :: Int -> HTTP.Request -> String
makeRequestKey index req = show index ++ " " ++ show (HTTP.getUri req)
harFileToRequests :: FilePath -> IO ([SomeException], [HTTP.Request])
harFileToRequests filePath = do
harFile <- either (throwIO . userError) return
=<< eitherDecode <$> BSL.readFile filePath
now <- getCurrentTime
let harRequests = map eRequest $ lEntries $ hLog harFile
return $ partitionEithers $ map (toHTTP now) harRequests
recordRequest :: HTTP.Manager -> Recorder -> Int -> HTTP.Request -> IO ()
recordRequest manager recorder index req = do
let key = makeRequestKey index req
record recorder key $ void $ HTTP.httpLbs req manager
-- | 'runHar' takes in a file path to HAR dump created by Chrome's Inspector
-- and creates a function that be used by 'wrecker''s 'run' or
-- 'defaultMain'.
runHar :: FilePath -> IO (Recorder -> IO ())
runHar filePath = do
(errors, reqs) <- harFileToRequests filePath
mapM_ print errors
context <- initConnectionContext
return $ \recorder -> do
#if MIN_VERSION_http_client(0,5,0)
manager <- HTTP.newManager
$ (mkManagerSettingsContext (Just context) def Nothing)
{ HTTP.managerResponseTimeout = HTTP.responseTimeoutNone }
#else
manager <- HTTP.newManager
$ (mkManagerSettingsContext (Just context) def Nothing)
{ HTTP.managerResponseTimeout = Nothing }
#endif
mapM_ (uncurry $ recordRequest manager recorder) $ zip [0..] reqs