network-conduit-1.0.4: Data/Conduit/Network/Utils.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Data.Conduit.Network.Utils
( -- * Helper utilities
HostPreference (..)
, bindPort
, getSocket
) where
import Network.Socket (AddrInfo, Socket, SocketType)
import qualified Network.Socket as NS
import Data.String (IsString (fromString))
import Control.Exception (bracketOnError, IOException)
import qualified Control.Exception as E
-- | Attempt to connect to the given host/port using given @SocketType@.
getSocket :: String -> Int -> SocketType -> IO (Socket, AddrInfo)
getSocket host' port' sockettype = do
let hints = NS.defaultHints {
NS.addrFlags = [NS.AI_ADDRCONFIG]
, NS.addrSocketType = sockettype
}
(addr:_) <- NS.getAddrInfo (Just hints) (Just host') (Just $ show port')
sock <- NS.socket (NS.addrFamily addr) (NS.addrSocketType addr)
(NS.addrProtocol addr)
return (sock, addr)
-- | Which host to bind.
--
-- Note: The @IsString@ instance recognizes the following special values:
--
-- * @*@ means @HostAny@
--
-- * @*4@ means @HostIPv4@
--
-- * @!4@ means @HostIPv4Only@
--
-- * @*6@ means @HostIPv6@
--
-- * @!6@ means @HostIPv6Only@
data HostPreference =
HostAny
| HostIPv4
| HostIPv4Only
| HostIPv6
| HostIPv6Only
| Host String
deriving (Eq, Ord, Show, Read)
instance IsString HostPreference where
-- The funny code coming up is to get around some irritating warnings from
-- GHC. I should be able to just write:
{-
fromString "*" = HostAny
fromString "*4" = HostIPv4
fromString "!4" = HostIPv4Only
fromString "*6" = HostIPv6
fromString "!6" = HostIPv6Only
-}
fromString s'@('*':s) =
case s of
[] -> HostAny
['4'] -> HostIPv4
['6'] -> HostIPv6
_ -> Host s'
fromString s'@('!':s) =
case s of
['4'] -> HostIPv4Only
['6'] -> HostIPv6Only
_ -> Host s'
fromString s = Host s
-- | Attempt to bind a listening @Socket@ on the given host/port using given
-- @SocketType@. If no host is given, will use the first address available.
bindPort :: Int -> HostPreference -> SocketType -> IO Socket
bindPort p s sockettype = do
let hints = NS.defaultHints
{ NS.addrFlags = [ NS.AI_PASSIVE
, NS.AI_NUMERICSERV
, NS.AI_NUMERICHOST
]
, NS.addrSocketType = sockettype
}
host =
case s of
Host s' -> Just s'
_ -> Nothing
port = Just . show $ p
addrs <- NS.getAddrInfo (Just hints) host port
-- Choose an IPv6 socket if exists. This ensures the socket can
-- handle both IPv4 and IPv6 if v6only is false.
let addrs4 = filter (\x -> NS.addrFamily x /= NS.AF_INET6) addrs
addrs6 = filter (\x -> NS.addrFamily x == NS.AF_INET6) addrs
addrs' =
case s of
HostIPv4 -> addrs4 ++ addrs6
HostIPv4Only -> addrs4
HostIPv6 -> addrs6 ++ addrs4
HostIPv6Only -> addrs6
_ -> addrs
tryAddrs (addr1:rest@(_:_)) =
E.catch
(theBody addr1)
(\(_ :: IOException) -> tryAddrs rest)
tryAddrs (addr1:[]) = theBody addr1
tryAddrs _ = error "bindPort: addrs is empty"
sockOpts =
case sockettype of
NS.Datagram -> [(NS.ReuseAddr,1)]
_ -> [(NS.NoDelay,1), (NS.ReuseAddr,1)]
theBody addr =
bracketOnError
(NS.socket (NS.addrFamily addr) (NS.addrSocketType addr) (NS.addrProtocol addr))
NS.sClose
(\sock -> do
mapM_ (\(opt,v) -> NS.setSocketOption sock opt v) sockOpts
NS.bindSocket sock (NS.addrAddress addr)
return sock
)
tryAddrs addrs'