{-# LANGUAGE CPP #-}
module SocketUtils
( isPortOpen
, simpleSockAddr
) where
import Data.Word (Word8)
import Foreign.C.Error (Errno(..), eCONNREFUSED)
import GHC.IO.Exception (IOException(..))
import Network.Socket (Socket, PortNumber, socket, connect, Family(AF_INET), SocketType(Stream), SockAddr(SockAddrInet), tupleToHostAddress)
import qualified Network.Socket as Socket
import UnliftIO.Exception (try, bracket, throwIO)
-- | `socket` < 2.7.0.2 does not have `close'` which throws on error,
-- which we desire for sanity.
-- If it's not available, we fall back to the silently failing one.
close'fallback :: Socket -> IO ()
close'fallback =
-- Unfortunately, `MIN_VERSION` does not accept a 4th argument,
-- so we have to make the check for 2.8.0.
#if MIN_VERSION_network(2,8,0)
Socket.close'
#else
Socket.close
#endif
-- | Checks whether @connect()@ to a given TCPv4 `SockAddr` succeeds or
-- returns `eCONNREFUSED`.
--
-- Rethrows connection exceptions in all other cases (e.g. when the host
-- is unroutable).
isPortOpen :: SockAddr -> IO Bool
isPortOpen sockAddr = do
bracket (socket AF_INET Stream 6 {- TCP -}) close'fallback $ \sock -> do
res <- try $ connect sock sockAddr
case res of
Right () -> return True
Left e ->
if (Errno <$> ioe_errno e) == Just eCONNREFUSED
then return False
else throwIO e
-- | Creates a `SockAttr` from host IP and port number.
--
-- Example:
-- > simpleSockAddr (127,0,0,1) 8000
simpleSockAddr :: (Word8, Word8, Word8, Word8) -> PortNumber -> SockAddr
simpleSockAddr addr port = SockAddrInet port (tupleToHostAddress addr)