spacecookie-1.1.0.0: src/Network/Gopher/Log.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
-- | This module is completely exposed by 'Network.Gopher'
module Network.Gopher.Log
( GopherLogStr ()
, makeSensitive
, hideSensitive
, GopherLogLevel (..)
, ToGopherLogStr (..)
, FromGopherLogStr (..)
) where
import Data.ByteString.Builder (Builder ())
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.Sequence as S
import Data.String (IsString (..))
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Encoding.Error as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import System.OsString.Internal.Types (PosixString (..))
import System.Socket.Family.Inet6
-- | Indicates the log level of a 'GopherLogStr' to a
-- 'Network.Gopher.GopherLogHandler'. If you want to
-- filter by log level you can use either the 'Ord'
-- or 'Enum' instance of 'GopherLogLevel' as the following
-- holds:
--
-- @
-- 'GopherLogLevelError' < 'GopherLogLevelWarn' < 'GopherLogLevelInfo'
-- @
data GopherLogLevel
= GopherLogLevelError
| GopherLogLevelWarn
| GopherLogLevelInfo
deriving (Show, Eq, Ord, Enum)
-- | UTF-8 encoded string which may have parts of it marked as
-- sensitive (see 'makeSensitive'). Use its 'ToGopherLogStr',
-- 'Semigroup' and 'IsString' instances to construct
-- 'GopherLogStr's and 'FromGopherLogStr' to convert to the
-- commonly used Haskell string types.
--
-- Note that encoding isn't checked for conversions from
-- encoding agnostic types, e.g. 'BB.ByteString'.
-- In 'FromGopherLogStr', invalid encoding is retained when converting to
-- encoding agnostic types (e.g. 'BB.ByteString'), but will be replaced by
-- replacement characters for types that enforce encoding (e.g. 'T.Text').
newtype GopherLogStr
= GopherLogStr { unGopherLogStr :: S.Seq GopherLogStrChunk }
instance Show GopherLogStr where
show = show . (fromGopherLogStr :: GopherLogStr -> String)
instance Semigroup GopherLogStr where
GopherLogStr s1 <> GopherLogStr s2 = GopherLogStr (s1 <> s2)
instance Monoid GopherLogStr where
mempty = GopherLogStr mempty
instance IsString GopherLogStr where
fromString = toGopherLogStr
data GopherLogStrChunk
= GopherLogStrChunk
{ glscSensitive :: Bool
, glscBuilder :: Builder
}
-- | Mark a 'GopherLogStr' as sensitive. This is used by this
-- library mostly to mark IP addresses of connecting clients.
-- By using 'hideSensitive' on a 'GopherLogStr' sensitive
-- parts will be hidden from the string — even if the sensitive
-- string was concatenated to other strings.
makeSensitive :: GopherLogStr -> GopherLogStr
makeSensitive = GopherLogStr
. fmap (\c -> c { glscSensitive = True })
. unGopherLogStr
-- | Replaces all chunks of the 'GopherLogStr' that have been
-- marked as sensitive by 'makeSensitive' with @[redacted]@.
-- Note that the chunking is dependent on the way the string
-- was assembled by the user and the internal implementation
-- of 'GopherLogStr' which can lead to multiple consecutive
-- @[redacted]@ being returned unexpectedly. This may be
-- improved in the future.
hideSensitive :: GopherLogStr -> GopherLogStr
hideSensitive = GopherLogStr
. fmap (\c -> GopherLogStrChunk False $
if glscSensitive c
then BB.byteString "[redacted]"
else glscBuilder c)
. unGopherLogStr
-- | Convert 'GopherLogStr's to other string types. Since it is used
-- internally by 'GopherLogStr', it is best to use the 'Builder'
-- instance for performance if possible.
class FromGopherLogStr a where
fromGopherLogStr :: GopherLogStr -> a
instance FromGopherLogStr GopherLogStr where
fromGopherLogStr = id
instance FromGopherLogStr Builder where
fromGopherLogStr = foldMap glscBuilder . unGopherLogStr
instance FromGopherLogStr BL.ByteString where
fromGopherLogStr = BB.toLazyByteString . fromGopherLogStr
instance FromGopherLogStr B.ByteString where
fromGopherLogStr = BL.toStrict . fromGopherLogStr
-- | Any non-UTF-8 portions (introduced e.g. via @'ToGopherLogStr'
-- 'BB.ByteString'@) are replaced with U+FFFD.
instance FromGopherLogStr T.Text where
-- text >= 2.0 introduces a shortcut for this, but we support a wider range
fromGopherLogStr = T.decodeUtf8With T.lenientDecode . fromGopherLogStr
-- | Any non-UTF-8 portions (introduced e.g. via @'ToGopherLogStr'
-- 'BB.ByteString'@) are replaced with U+FFFD.
instance FromGopherLogStr TL.Text where
fromGopherLogStr = TL.decodeUtf8With T.lenientDecode . fromGopherLogStr
-- | Any non-UTF-8 portions (introduced e.g. via @'ToGopherLogStr'
-- 'BB.ByteString'@) are replaced with U+FFFD.
instance FromGopherLogStr [Char] where
fromGopherLogStr = UTF8.toString . fromGopherLogStr
-- | Convert something to a 'GopherLogStr'. In terms of
-- performance it is best to implement a 'Builder' for
-- the type you are trying to render to 'GopherLogStr'
-- and then reuse its 'ToGopherLogStr' instance.
class ToGopherLogStr a where
toGopherLogStr :: a -> GopherLogStr
instance ToGopherLogStr GopherLogStr where
toGopherLogStr = id
-- | UTF-8 encoding is not checked, needs to be ensured by the user.
instance ToGopherLogStr Builder where
toGopherLogStr b = GopherLogStr
. S.singleton
$ GopherLogStrChunk
{ glscSensitive = False
, glscBuilder = b
}
-- | UTF-8 encoding is not checked, needs to be ensured by the user.
instance ToGopherLogStr B.ByteString where
toGopherLogStr = toGopherLogStr . BB.byteString
-- | UTF-8 encoding is not checked, needs to be ensured by the user.
instance ToGopherLogStr BL.ByteString where
toGopherLogStr = toGopherLogStr . BB.lazyByteString
instance ToGopherLogStr [Char] where
toGopherLogStr = toGopherLogStr . UTF8.fromString
instance ToGopherLogStr GopherLogLevel where
toGopherLogStr l =
case l of
GopherLogLevelInfo -> toGopherLogStr ("info" :: B.ByteString)
GopherLogLevelWarn -> toGopherLogStr ("warn" :: B.ByteString)
GopherLogLevelError -> toGopherLogStr ("error" :: B.ByteString)
instance ToGopherLogStr (SocketAddress Inet6) where
-- TODO shorten address if possible
toGopherLogStr (SocketAddressInet6 addr port _ _) =
let (b1, b2, b3, b4, b5, b6, b7, b8) = inet6AddressToTuple addr
in toGopherLogStr $
BB.charUtf8 '[' <>
BB.word16HexFixed b1 <> BB.charUtf8 ':' <>
BB.word16HexFixed b2 <> BB.charUtf8 ':' <>
BB.word16HexFixed b3 <> BB.charUtf8 ':' <>
BB.word16HexFixed b4 <> BB.charUtf8 ':' <>
BB.word16HexFixed b5 <> BB.charUtf8 ':' <>
BB.word16HexFixed b6 <> BB.charUtf8 ':' <>
BB.word16HexFixed b7 <> BB.charUtf8 ':' <>
BB.word16HexFixed b8 <> BB.charUtf8 ']' <>
BB.charUtf8 ':' <> BB.intDec (fromIntegral port)
-- | UTF-8 encoding is not checked, needs to be ensured by the user.
instance ToGopherLogStr PosixString where
toGopherLogStr = toGopherLogStr . BB.shortByteString . getPosixString