wai-logger 2.2.7 → 2.5.0
raw patch · 6 files changed
Files
- Network/Wai/Logger.hs +51/−14
- Network/Wai/Logger/Apache.hs +164/−19
- Network/Wai/Logger/IP.hs +1/−1
- Setup.hs +5/−0
- test/doctests.hs +0/−6
- wai-logger.cabal +36/−45
Network/Wai/Logger.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE GADTs #-} -- | Apache style logger for WAI applications. --@@ -7,7 +8,7 @@ -- > {-# LANGUAGE OverloadedStrings #-} -- > module Main where -- >--- > import Blaze.ByteString.Builder (fromByteString)+-- > import Data.ByteString.Builder (byteString) -- > import Control.Monad.IO.Class (liftIO) -- > import qualified Data.ByteString.Char8 as BS -- > import Network.HTTP.Types (status200)@@ -27,19 +28,25 @@ -- > status = status200 -- > hdr = [("Content-Type", "text/plain")] -- > pong = "PONG"--- > msg = fromByteString pong+-- > msg = byteString pong -- > len = fromIntegral $ BS.length pong module Network.Wai.Logger ( -- * High level functions ApacheLogger , withStdoutLogger+ , ServerPushLogger -- * Creating a logger- , ApacheLoggerActions(..)+ , ApacheLoggerActions+ , apacheLogger+ , serverpushLogger+ , logRotator+ , logRemover+ , initLoggerUser , initLogger -- * Types , IPAddrSource(..)- , LogType(..)+ , LogType'(..), LogType , FileLogSpec(..) -- * Utilities , showSockAddr@@ -56,6 +63,7 @@ #endif import Control.Exception (bracket) import Control.Monad (void)+import Data.ByteString (ByteString) import Network.HTTP.Types (Status) import Network.Wai (Request) import System.Log.FastLogger@@ -85,12 +93,19 @@ -- | Apache style logger. type ApacheLogger = Request -> Status -> Maybe Integer -> IO () +-- | HTTP/2 server push logger in Apache style.+type ServerPushLogger = Request -> ByteString -> Integer -> IO ()++-- | Function set of Apache style logger. data ApacheLoggerActions = ApacheLoggerActions {+ -- | The Apache logger. apacheLogger :: ApacheLogger+ -- | The HTTP/2 server push logger.+ , serverpushLogger :: ServerPushLogger -- | This is obsoleted. Rotation is done on-demand. -- So, this is now an empty action. , logRotator :: IO ()- -- | Removing resources relating Apache logger.+ -- | Removing resources relating to Apache logger. -- E.g. flushing and deallocating internal buffers. , logRemover :: IO () }@@ -98,28 +113,46 @@ ---------------------------------------------------------------- -- | Creating 'ApacheLogger' according to 'LogType'.+initLoggerUser :: ToLogStr user => Maybe (Request -> Maybe user) -> IPAddrSource -> LogType -> IO FormattedTime+ -> IO ApacheLoggerActions+initLoggerUser ugetter ipsrc typ tgetter = do+ (fl, cleanUp) <- newFastLogger typ+ return $ ApacheLoggerActions {+ apacheLogger = apache fl ipsrc ugetter tgetter+ , serverpushLogger = serverpush fl ipsrc ugetter tgetter+ , logRotator = return ()+ , logRemover = cleanUp+ }+ initLogger :: IPAddrSource -> LogType -> IO FormattedTime -> IO ApacheLoggerActions-initLogger ipsrc typ tgetter = do- (fl, cleanUp) <- newFastLogger typ- return $ ApacheLoggerActions (apache fl ipsrc tgetter) (return ()) cleanUp+initLogger = initLoggerUser nouser+ where+ nouser :: Maybe (Request -> Maybe ByteString)+ nouser = Nothing --- | Checking if a log file can be written if 'LogType' is 'LogFileNoRotate' or 'LogFile'. logCheck :: LogType -> IO () logCheck LogNone = return () logCheck (LogStdout _) = return () logCheck (LogStderr _) = return ()-logCheck (LogFileNoRotate fp _) = check fp-logCheck (LogFile spec _) = check (log_file spec)+logCheck (LogFileNoRotate fp _) = check fp+logCheck (LogFile spec _) = check (log_file spec)+logCheck (LogFileTimedRotate spec _) = check (timed_log_file spec) logCheck (LogCallback _ _) = return () ---------------------------------------------------------------- -apache :: (LogStr -> IO ()) -> IPAddrSource -> IO FormattedTime -> ApacheLogger-apache cb ipsrc dateget req st mlen = do+apache :: ToLogStr user => (LogStr -> IO ()) -> IPAddrSource -> Maybe (Request -> Maybe user) -> IO FormattedTime -> ApacheLogger+apache cb ipsrc userget dateget req st mlen = do zdata <- dateget- cb (apacheLogStr ipsrc zdata req st mlen)+ cb (apacheLogStr ipsrc (justGetUser userget) zdata req st mlen) +serverpush :: ToLogStr user => (LogStr -> IO ()) -> IPAddrSource -> Maybe (Request -> Maybe user) -> IO FormattedTime -> ServerPushLogger+serverpush cb ipsrc userget dateget req path size = do+ zdata <- dateget+ cb (serverpushLogStr ipsrc (justGetUser userget) zdata req path size)+ --------------------------------------------------------------- -- | Getting cached 'ZonedDate'.@@ -142,3 +175,7 @@ clockDateCacher = do tgetter <- newTimeCache simpleTimeFormat return (tgetter, return ())++justGetUser :: Maybe (Request -> Maybe user) -> (Request -> Maybe user)+justGetUser (Just getter) = getter+justGetUser Nothing = \_ -> Nothing
Network/Wai/Logger/Apache.hs view
@@ -1,8 +1,9 @@-{-# LANGUAGE OverloadedStrings, CPP #-}+{-# LANGUAGE OverloadedStrings, CPP, TupleSections #-} module Network.Wai.Logger.Apache ( IPAddrSource(..) , apacheLogStr+ , serverpushLogStr ) where #ifndef MIN_VERSION_base@@ -17,38 +18,61 @@ import Data.List (find) import Data.Maybe (fromMaybe) #if MIN_VERSION_base(4,5,0)-import Data.Monoid ((<>))+import Data.Monoid ((<>), First (..)) #else import Data.Monoid (mappend) #endif import Network.HTTP.Types (Status, statusCode)+import Network.HTTP.Types.Header (HeaderName) import Network.Wai (Request(..)) import Network.Wai.Logger.IP import System.Log.FastLogger -- $setup -- >>> :set -XOverloadedStrings--- >>> import Network.Wai.Test+-- >>> import Network.Wai (defaultRequest) -- | Source from which the IP source address of the client is obtained. data IPAddrSource = -- | From the peer address of the HTTP connection. FromSocket- -- | From X-Real-IP: or X-Forwarded-For: in the HTTP header.+ -- | From @X-Real-IP@ or @X-Forwarded-For@ in the HTTP header.+ --+ -- This picks either @X-Real-IP@ or @X-Forwarded-For@ depending on which of these+ -- headers comes first in the ordered list of request headers.+ --+ -- If the @X-Forwarded-For@ header is picked, the value will be assumed to be a+ -- comma-separated list of IP addresses. The value will be parsed, and the+ -- left-most IP address will be used (which is mostly likely to be the actual+ -- client IP address). | FromHeader- -- | From the peer address if header is not found.+ -- | From a custom HTTP header, useful in proxied environment.+ --+ -- The header value will be assumed to be a comma-separated list of IP+ -- addresses. The value will be parsed, and the left-most IP address will be+ -- used (which is mostly likely to be the actual client IP address).+ --+ -- Note that this still works as expected for a single IP address.+ | FromHeaderCustom [HeaderName]+ -- | Just like 'FromHeader', but falls back on the peer address if header is not found. | FromFallback+ -- | This gives you the most flexibility to figure out the IP source address+ -- from the 'Request'. The returned 'ByteString' is used as the IP source+ -- address.+ | FromRequest (Request -> ByteString) -- | Apache style log format.-apacheLogStr :: IPAddrSource -> FormattedTime -> Request -> Status -> Maybe Integer -> LogStr-apacheLogStr ipsrc tmstr req status msize =+apacheLogStr :: ToLogStr user => IPAddrSource -> (Request -> Maybe user) -> FormattedTime -> Request -> Status -> Maybe Integer -> LogStr+apacheLogStr ipsrc userget tmstr req status msize = toLogStr (getSourceIP ipsrc req)- <> " - - ["+ <> " - "+ <> maybe "-" toLogStr (userget req)+ <> " [" <> toLogStr tmstr <> "] \"" <> toLogStr (requestMethod req) <> " "- <> toLogStr (rawPathInfo req)+ <> toLogStr path <> " " <> toLogStr (show (httpVersion req)) <> "\" "@@ -61,6 +85,7 @@ <> toLogStr (fromMaybe "" mua) <> "\"\n" where+ path = rawPathInfo req <> rawQueryString req #if !MIN_VERSION_base(4,5,0) (<>) = mappend #endif@@ -72,12 +97,40 @@ mua = lookup "user-agent" $ requestHeaders req #endif --- getSourceIP = getSourceIP fromString fromByteString+-- | HTTP/2 Push log format in the Apache style.+serverpushLogStr :: ToLogStr user => IPAddrSource -> (Request -> Maybe user) -> FormattedTime -> Request -> ByteString -> Integer -> LogStr+serverpushLogStr ipsrc userget tmstr req path size =+ toLogStr (getSourceIP ipsrc req)+ <> " - "+ <> maybe "-" toLogStr (userget req)+ <> " ["+ <> toLogStr tmstr+ <> "] \"PUSH "+ <> toLogStr path+ <> " HTTP/2\" 200 "+ <> toLogStr (show size)+ <> " \""+ <> toLogStr ref+ <> "\" \""+ <> toLogStr (fromMaybe "" mua)+ <> "\"\n"+ where+ ref = rawPathInfo req+#if !MIN_VERSION_base(4,5,0)+ (<>) = mappend+#endif+#if MIN_VERSION_wai(3,2,0)+ mua = requestHeaderUserAgent req+#else+ mua = lookup "user-agent" $ requestHeaders req+#endif getSourceIP :: IPAddrSource -> Request -> ByteString-getSourceIP FromSocket = getSourceFromSocket-getSourceIP FromHeader = getSourceFromHeader+getSourceIP FromSocket = getSourceFromSocket+getSourceIP FromHeader = getSourceFromHeader getSourceIP FromFallback = getSourceFromFallback+getSourceIP (FromHeaderCustom hs) = fromMaybe "-" . getSourceFromHeaderCustom hs+getSourceIP (FromRequest fromReq) = fromReq -- | -- >>> getSourceFromSocket defaultRequest@@ -91,11 +144,25 @@ -- >>> getSourceFromHeader defaultRequest { requestHeaders = [ ("X-Forwarded-For", "127.0.0.1") ] } -- "127.0.0.1" -- >>> getSourceFromHeader defaultRequest { requestHeaders = [ ("Something", "127.0.0.1") ] }--- ""+-- "-" -- >>> getSourceFromHeader defaultRequest { requestHeaders = [] }--- ""+-- "-"+--+-- 'getSourceFromHeader' uses the first instance of either @"X-Real-IP"@ or+-- @"X-Forwarded-For"@ that it finds in the ordered header list:+--+-- >>> getSourceFromHeader defaultRequest { requestHeaders = [ ("X-Real-IP", "1.2.3.4"), ("X-Forwarded-For", "5.6.7.8") ] }+-- "1.2.3.4"+-- >>> getSourceFromHeader defaultRequest { requestHeaders = [ ("X-Forwarded-For", "5.6.7.8"), ("X-Real-IP", "1.2.3.4") ] }+-- "5.6.7.8"+--+-- 'getSourceFromHeader' handles pulling out the first IP in the+-- comma-separated IP list in X-Forwarded-For:+--+-- >>> getSourceFromHeader defaultRequest { requestHeaders = [ ("X-Forwarded-For", "5.6.7.8, 10.11.12.13, 1.2.3.4") ] }+-- "5.6.7.8" getSourceFromHeader :: Request -> ByteString-getSourceFromHeader = fromMaybe "" . getSource+getSourceFromHeader = fromMaybe "-" . getSource -- | -- >>> getSourceFromFallback defaultRequest { requestHeaders = [ ("X-Real-IP", "127.0.0.1") ] }@@ -106,6 +173,20 @@ -- "0.0.0.0" -- >>> getSourceFromFallback defaultRequest { requestHeaders = [] } -- "0.0.0.0"+--+-- 'getSourceFromFallback' uses the first instance of either @"X-Real-IP"@ or+-- @"X-Forwarded-For"@ that it finds in the ordered header list:+--+-- >>> getSourceFromFallback defaultRequest { requestHeaders = [ ("X-Real-IP", "1.2.3.4"), ("X-Forwarded-For", "5.6.7.8") ] }+-- "1.2.3.4"+-- >>> getSourceFromFallback defaultRequest { requestHeaders = [ ("X-Forwarded-For", "5.6.7.8"), ("X-Real-IP", "1.2.3.4") ] }+-- "5.6.7.8"+--+-- 'getSourceFromFallback' handles pulling out the first IP in the+-- comma-separated IP list in X-Forwarded-For:+--+-- >>> getSourceFromFallback defaultRequest { requestHeaders = [ ("X-Forwarded-For", "5.6.7.8, 10.11.12.13, 1.2.3.4") ] }+-- "5.6.7.8" getSourceFromFallback :: Request -> ByteString getSourceFromFallback req = fromMaybe (getSourceFromSocket req) $ getSource req @@ -118,9 +199,73 @@ -- Nothing -- >>> getSource defaultRequest -- Nothing+--+-- 'getSource' uses the first instance of either @"X-Real-IP"@ or+-- @"X-Forwarded-For"@ that it finds in the ordered header list:+--+-- >>> getSource defaultRequest { requestHeaders = [ ("X-Real-IP", "1.2.3.4"), ("X-Forwarded-For", "5.6.7.8") ] }+-- Just "1.2.3.4"+-- >>> getSource defaultRequest { requestHeaders = [ ("X-Forwarded-For", "5.6.7.8"), ("X-Real-IP", "1.2.3.4") ] }+-- Just "5.6.7.8"+--+-- 'getSource' handles pulling out the first IP in the comma-separated IP list+-- in X-Forwarded-For:+--+-- >>> getSource defaultRequest { requestHeaders = [ ("X-Forwarded-For", "5.6.7.8, 10.11.12.13, 1.2.3.4") ] }+-- Just "5.6.7.8" getSource :: Request -> Maybe ByteString-getSource req = addr+getSource = getSourceFromHeaders [("x-real-ip", id), ("x-forwarded-for", firstIpInXFF)]++-- | Pull out the first IP in a comma-separated list of X-Forwarded-For IPs.+--+-- >>> firstIpInXFF "1.2.3.4, 5.6.7.8, 10.11.12.13"+-- "1.2.3.4"+--+-- If there are no commas, just return the whole input ByteString:+--+-- >>> firstIpInXFF "5.6.7.8"+-- "5.6.7.8"+--+-- Note that this function doesn't make sure the input is actually an IP address:+--+-- >>> firstIpInXFF "hello, world"+-- "hello"+firstIpInXFF :: ByteString -> ByteString+firstIpInXFF = BS.takeWhile (/= ',')++getSourceFromHeaders :: [(HeaderName, ByteString -> ByteString)] -> Request -> Maybe ByteString+getSourceFromHeaders headerNamesAndPostProc req = getFirst $ foldMap f $ requestHeaders req where- maddr = find (\x -> fst x `elem` ["x-real-ip", "x-forwarded-for"]) hdrs- addr = fmap snd maddr- hdrs = requestHeaders req+ -- Take a header name and value from the request, and try match it against+ -- the list of headers and post-processing functions. If it matches,+ -- return the ByteString resulting from applying the post-processing function+ -- to the header value.+ f :: (HeaderName, ByteString) -> First ByteString+ f (headerNameFromReq, headerValFromReq) =+ let maybePostProc = find (\(headerNameFromPostProc, _) -> headerNameFromReq == headerNameFromPostProc) headerNamesAndPostProc+ in First $ fmap (\(_, postProc) -> postProc headerValFromReq) maybePostProc++-- |+-- >>> getSourceFromHeaderCustom ["x-foobar"] defaultRequest { requestHeaders = [ ("X-catdog", "1.2.3.4"), ("X-Foobar", "5.6.7.8"), ("Other", "1.1.1.1") ] }+-- Just "5.6.7.8"+--+-- If none of the headers in the passed-in list are in the 'Request', then return 'Nothing':+--+-- >>> getSourceFromHeaderCustom ["x-foobar", "baz"] defaultRequest { requestHeaders = [ ("abb", "1.2.3.4"), ("xyz", "5.6.7.8") ] }+-- Nothing+--+-- 'getSourceFromHeaderCustom' uses the first instance of any header in the+-- passed in list that it finds in the ordered header list from the request:+--+-- >>> getSourceFromHeaderCustom ["x-foobar", "baz"] defaultRequest { requestHeaders = [ ("baz", "1.2.3.4"), ("x-foobar", "5.6.7.8") ] }+-- Just "1.2.3.4"+--+-- 'getSourceFromHeaderCustom' splits the value of the header it finds by @,@+-- and uses the first item. This makes it easy to use with headers like+-- @X-Forwarded-For@, which are expected to have a comma-separated list of IP+-- addresses:+--+-- >>> getSourceFromHeaderCustom ["x-foobar"] defaultRequest { requestHeaders = [ ("X-Foobar", "5.6.7.8, 10.11.12.13, 1.2.3.4") ] }+-- Just "5.6.7.8"+getSourceFromHeaderCustom :: [HeaderName] -> Request -> Maybe ByteString+getSourceFromHeaderCustom hs = getSourceFromHeaders (fmap (,firstIpInXFF) hs)
Network/Wai/Logger/IP.hs view
@@ -46,4 +46,4 @@ showSockAddr (SockAddrInet6 _ _ (0,0,0x0000ffff,addr4) _) = showIPv4 addr4 False showSockAddr (SockAddrInet6 _ _ (0,0,0,1) _) = "::1" showSockAddr (SockAddrInet6 _ _ addr6 _) = showIPv6 addr6-showSockAddr _ = "unknownSocket"+showSockAddr (SockAddrUnix _) = "-"
Setup.hs view
@@ -1,2 +1,7 @@+{-# OPTIONS_GHC -Wall #-}+module Main (main) where+ import Distribution.Simple++main :: IO () main = defaultMain
− test/doctests.hs
@@ -1,6 +0,0 @@-module Main where--import Test.DocTest--main :: IO ()-main = doctest ["Network"]
wai-logger.cabal view
@@ -1,48 +1,39 @@-Name: wai-logger-Version: 2.2.7-Author: Kazu Yamamoto <kazu@iij.ad.jp>-Maintainer: Kazu Yamamoto <kazu@iij.ad.jp>-License: BSD3-License-File: LICENSE-Synopsis: A logging system for WAI-Description: A logging system for WAI-Category: Web, Yesod-Cabal-Version: >= 1.10-Build-Type: Simple+cabal-version: >=1.10+name: wai-logger+version: 2.5.0+license: BSD3+license-file: LICENSE+maintainer: Kazu Yamamoto <kazu@iij.ad.jp>+author: Kazu Yamamoto <kazu@iij.ad.jp>+tested-with:+ ghc ==7.8.4 || ==7.10.3 || ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.3 -Library- Default-Language: Haskell2010- GHC-Options: -Wall- Exposed-Modules: Network.Wai.Logger- Other-Modules: Network.Wai.Logger.Apache- Network.Wai.Logger.IP- Network.Wai.Logger.IORef- Build-Depends: base >= 4 && < 5- , blaze-builder- , byteorder- , bytestring- , case-insensitive- , fast-logger >= 2.4.5- , http-types- , network- , wai >= 2.0.0- if os(windows)- Cpp-Options: -DWINDOWS- Build-Depends: time- , old-locale- else- Build-Depends: unix- , unix-time >= 0.2.2+synopsis: A logging system for WAI+description: A logging system for WAI(Web Application Interface)+category: Web, Yesod+build-type: Simple -Test-Suite doctest- Type: exitcode-stdio-1.0- Default-Language: Haskell2010- HS-Source-Dirs: test- Ghc-Options: -Wall- Main-Is: doctests.hs- Build-Depends: base- , doctest >= 0.10.1+source-repository head+ type: git+ location: https://github.com/kazu-yamamoto/logger.git -Source-Repository head- Type: git- Location: git://github.com/kazu-yamamoto/logger.git+library+ exposed-modules: Network.Wai.Logger+ other-modules:+ Network.Wai.Logger.Apache+ Network.Wai.Logger.IP+ Network.Wai.Logger.IORef++ default-language: Haskell2010+ ghc-options: -Wall+ build-depends:+ base >=4 && <5,+ byteorder,+ bytestring,+ fast-logger >=3,+ http-types,+ network,+ wai >=2.0.0++ if impl(ghc >=8)+ default-extensions: Strict StrictData