inspector-wrecker (empty) → 0.1.0.0
raw patch · 9 files changed
+409/−0 lines, 9 filesdep +aesondep +basedep +bytestringsetup-changed
Dependencies added: aeson, base, bytestring, case-insensitive, connection, data-default, http-client, http-client-tls, http-types, inspector-wrecker, optparse-applicative, text, time, wrecker
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- inspector-wrecker.cabal +61/−0
- src/Main.hs +5/−0
- src/Wrecker/Inspector.hs +13/−0
- src/Wrecker/Inspector/HAR.hs +226/−0
- src/Wrecker/Inspector/Main.hs +10/−0
- src/Wrecker/Inspector/Options.hs +60/−0
- test/Spec.hs +2/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright skedge.me (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ inspector-wrecker.cabal view
@@ -0,0 +1,61 @@+name: inspector-wrecker+version: 0.1.0.0+synopsis: Create benchmarks from the HAR files+description: + inspector-wrecker is a library and executable for creating HTTP benchmarks from+ a HAR file dump from Chrome's Inspector. + + The executable exposes the wrecker options and additionally takes in a path to + a HAR file.+ + The library exposes a single function, 'runHar', which produces a function + 'wrecker''s library can use for benchmarks.+ +homepage: https://github.com/skedgeme/inspector-wrecker#readme+license: BSD3+license-file: LICENSE+author: Jonathan Fischoff+maintainer: jonathangfischoff@gmail.com+copyright: 2016 skedge.me+category: Web+build-type: Simple+-- extra-source-files:+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules: Wrecker.Inspector.HAR+ , Wrecker.Inspector.Options+ , Wrecker.Inspector.Main+ , Wrecker.Inspector+ build-depends: base >= 4.7 && < 5+ , wrecker+ , optparse-applicative+ , http-client+ , http-types+ , time+ , text+ , bytestring+ , case-insensitive+ , aeson+ , http-client-tls >= 0.3.3 && < 0.4+ , connection+ , data-default+ default-language: Haskell2010++executable inspector-wrecker-exe+ main-is: src/Main.hs+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends: base+ , inspector-wrecker+ default-language: Haskell2010++test-suite inspector-wrecker-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: base+ , inspector-wrecker+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ default-language: Haskell2010+
+ src/Main.hs view
@@ -0,0 +1,5 @@+import qualified Wrecker.Inspector.Main as Wrecker++main :: IO ()+main = Wrecker.main+
+ src/Wrecker/Inspector.hs view
@@ -0,0 +1,13 @@+{- | + 'inspector-wrecker' is a library and executable for creating HTTP benchmarks from+ a HAR file dump from Chrome's Inspector. + + The executable exposes the wrecker options and additionally takes in a path to + a HAR file.+ + The library exposes a single function, 'runHar', which produces a function + 'wrecker''s library can use for benchmarks.++-}+module Wrecker.Inspector (runHar) where+import Wrecker.Inspector.HAR
+ src/Wrecker/Inspector/HAR.hs view
@@ -0,0 +1,226 @@+{-# 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
+ src/Wrecker/Inspector/Main.hs view
@@ -0,0 +1,10 @@+module Wrecker.Inspector.Main where+import Wrecker.Inspector.HAR+import Wrecker.Inspector.Options+import Wrecker++main :: IO ()+main = do + options <- runParser+ runner <- runHar (harFilePath options)+ run (wreckerOptions options) [(harFilePath options, runner)]
+ src/Wrecker/Inspector/Options.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE RecordWildCards, CPP #-}+module Wrecker.Inspector.Options where+import qualified Wrecker.Options as W+import Options.Applicative.Builder+import Options.Applicative+import Control.Exception+#if __GLASGOW_HASKELL__ < 800+import Data.Monoid+#endif++data Options = Options + { harFilePath :: FilePath+ , wreckerOptions :: W.Options+ } deriving (Show, Eq)+ +data PartialOptions = PartialOptions+ { mHarFilePath :: Maybe FilePath+ , mWreckerOptions :: Maybe W.PartialOptions+ } deriving (Show, Eq)+ +instance Monoid PartialOptions where+ mempty = PartialOptions { mHarFilePath = Nothing, mWreckerOptions = mempty }+ mappend x y = PartialOptions + { mHarFilePath = mHarFilePath x <|> mHarFilePath y+ , mWreckerOptions = mWreckerOptions x <|> mWreckerOptions y+ } + +optionalOption :: Read a => Mod OptionFields a -> Parser (Maybe a)+optionalOption = optional . option auto + +pPartialOptions :: Parser PartialOptions+pPartialOptions + = PartialOptions + <$> optionalOption + ( long "file-path"+ <> help "HAR file path"+ )+ <*> optional W.pPartialOptions++completeOptions :: PartialOptions -> Maybe Options+completeOptions options = case mempty <> options of+ PartialOptions + { mHarFilePath = Just harFilePath+ , mWreckerOptions = Just partialWreckerOptions+ } -> Options harFilePath + <$> W.completeOptions partialWreckerOptions+ _ -> Nothing+ +runParser :: IO Options+runParser = do + let opts = info (helper <*> pPartialOptions)+ ( fullDesc+ <> progDesc "Welcome to inspector-wrecker"+ <> header "inspector-wrecker - HTTP stress tester and benchmarker with HAR reader" + )+ + partialOptions <- execParser opts + case completeOptions partialOptions of+ Nothing -> throwIO $ userError "Missing file-path argument!"+ Just x -> return x
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"