packages feed

sockaddr-0.1.0: Network/SockAddr.hs

-- | Converting the address part of 'SockAddr' (without port).

module Network.SockAddr (
    showSockAddr
  , showSockAddrBS
  ) where

import Data.Bits (shiftR)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Builder as B
import qualified Data.ByteString.Char8 as C8
import qualified Data.ByteString.Lazy as BL
import Data.List (intersperse)
import Data.Word (Word16, Word8)
import Network.Socket (
    HostAddress6,
    SockAddr (..),
    hostAddress6ToTuple,
    hostAddressToTuple,
 )

----------------------------------------------------------------

-- | Convert the address part of 'SockAddr' to 'String'.
--   The port number is not included. If the address is
--   an IPv4-mapped IPv6 address (@::ffff:0:0/96@),
--   it is shown as IPv4. A non-zero scope ID of IPv6 is appended
--   as a number, e.g. @fe80::1%4@.
--   For 'SockAddrUnix', @"unknownSocket"@ is returned
--   so that its path is not exposed.
--
-- >>> import Network.Socket
-- >>> showSockAddr $ SockAddrInet 80 (tupleToHostAddress (192,0,2,1))
-- "192.0.2.1"
-- >>> showSockAddr $ SockAddrInet6 80 0 (tupleToHostAddress6 (0x2001,0xdb8,1,2,3,4,5,6)) 0
-- "2001:db8:1:2:3:4:5:6"
-- >>> showSockAddr $ SockAddrInet6 80 0 (tupleToHostAddress6 (0x2001,0xdb8,0,0,1,0,0,1)) 0
-- "2001:db8::1:0:0:1"
showSockAddr :: SockAddr -> String
showSockAddr = C8.unpack . showSockAddrBS

-- | Convert the address part of 'SockAddr' to 'ByteString'.
--   The port number is not included. If the address is
--   an IPv4-mapped IPv6 address (@::ffff:0:0/96@),
--   it is shown as IPv4. A non-zero scope ID of IPv6 is appended
--   as a number, e.g. @fe80::1%4@.
--   For 'SockAddrUnix', @"unknownSocket"@ is returned
--   so that its path is not exposed.
--
-- >>> import Network.Socket
-- >>> showSockAddrBS $ SockAddrInet6 80 0 (tupleToHostAddress6 (0,0,0,0,0,0,0,1)) 0
-- "::1"
showSockAddrBS :: SockAddr -> ByteString
showSockAddrBS = BL.toStrict . B.toLazyByteString . sockAddrBuilder

----------------------------------------------------------------

sockAddrBuilder :: SockAddr -> B.Builder
sockAddrBuilder (SockAddrInet _ addr4) = buildIPv4 $ hostAddressToTuple addr4
sockAddrBuilder (SockAddrInet6 _ _ addr6 scope) = case hostAddress6ToTuple addr6 of
    (0, 0, 0, 0, 0, 0xffff, h, l) -> buildIPv4 (hi h, lo h, hi l, lo l)
    _ -> buildIPv6 addr6 <> zone
  where
    zone
        | scope == 0 = mempty
        | otherwise = B.char7 '%' <> B.word32Dec scope
    hi w = fromIntegral (w `shiftR` 8)
    lo w = fromIntegral w
sockAddrBuilder (SockAddrUnix _) = B.string7 "unknownSocket"

buildIPv4 :: (Word8, Word8, Word8, Word8) -> B.Builder
buildIPv4 (a, b, c, d) = mconcat $ intersperse (B.char7 '.') $ map B.word8Dec [a, b, c, d]

-- RFC 5952: the longest (first if tied) run of two or more
-- zero fields is compressed to "::".
buildIPv6 :: HostAddress6 -> B.Builder
buildIPv6 addr6 = case longestZeros ws of
    Just (i, n) -> hexes (take i ws) <> B.string7 "::" <> hexes (drop (i + n) ws)
    Nothing -> hexes ws
  where
    (a, b, c, d, e, f, g, h) = hostAddress6ToTuple addr6
    ws = [a, b, c, d, e, f, g, h]
    hexes = mconcat . intersperse (B.char7 ':') . map B.word16Hex

-- Returns (start index, length) of the zero run to be compressed.
longestZeros :: [Word16] -> Maybe (Int, Int)
longestZeros ws = case runs of
    [] -> Nothing
    _ -> Just $ foldr1 (\x y -> if snd y > snd x then y else x) runs
  where
    runs = [(i, n) | (i, n) <- go 0 ws, n >= 2]
    go _ [] = []
    go i xs@(0 : _) = let n = length (takeWhile (== 0) xs) in (i, n) : go (i + n) (drop n xs)
    go i (_ : xs) = go (i + 1) xs