module Test.Sandwich.Contexts.Kubernetes.Util.SocketUtil (
  isPortOpen
  , simpleSockAddr
  ) where

-- Taken from
-- https://stackoverflow.com/questions/39139787/i-want-to-check-whether-or-not-a-certain-port-is-open-haskell
-- https://gist.github.com/nh2/0a1442eb71ec0405a1e3ce83a467dfde#file-socketutils-hs

import Foreign.C.Error (Errno(..), eCONNREFUSED)
import GHC.IO.Exception (IOException(..))
import Network.Socket (Family(AF_INET), PortNumber, SocketType(Stream), SockAddr(SockAddrInet), socket, connect, close', tupleToHostAddress)
import Relude
import UnliftIO.Exception

-- | 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 -> IO Bool
isPortOpen SockAddr
sockAddr = do
  IO Socket -> (Socket -> IO ()) -> (Socket -> IO Bool) -> IO Bool
forall (m :: * -> *) a b c.
MonadUnliftIO m =>
m a -> (a -> m b) -> (a -> m c) -> m c
bracket (Family -> SocketType -> ProtocolNumber -> IO Socket
socket Family
AF_INET SocketType
Stream ProtocolNumber
6 {- TCP -}) Socket -> IO ()
close' ((Socket -> IO Bool) -> IO Bool) -> (Socket -> IO Bool) -> IO Bool
forall a b. (a -> b) -> a -> b
$ \Socket
sock -> do
    res <- IO () -> IO (Either IOException ())
forall (m :: * -> *) e a.
(MonadUnliftIO m, Exception e) =>
m a -> m (Either e a)
try (IO () -> IO (Either IOException ()))
-> IO () -> IO (Either IOException ())
forall a b. (a -> b) -> a -> b
$ Socket -> SockAddr -> IO ()
connect Socket
sock SockAddr
sockAddr
    case res of
      Right () -> Bool -> IO Bool
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
True
      Left IOException
e ->
        if (ProtocolNumber -> Errno
Errno (ProtocolNumber -> Errno) -> Maybe ProtocolNumber -> Maybe Errno
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IOException -> Maybe ProtocolNumber
ioe_errno IOException
e) Maybe Errno -> Maybe Errno -> Bool
forall a. Eq a => a -> a -> Bool
== Errno -> Maybe Errno
forall a. a -> Maybe a
Just Errno
eCONNREFUSED
          then Bool -> IO Bool
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
False
          else IOException -> IO Bool
forall (m :: * -> *) e a. (MonadIO m, Exception e) => e -> m a
throwIO IOException
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 :: (Word8, Word8, Word8, Word8) -> PortNumber -> SockAddr
simpleSockAddr (Word8, Word8, Word8, Word8)
addr PortNumber
port = PortNumber -> HostAddress -> SockAddr
SockAddrInet PortNumber
port ((Word8, Word8, Word8, Word8) -> HostAddress
tupleToHostAddress (Word8, Word8, Word8, Word8)
addr)


-- Example usage:
-- > isPortOpen (simpleSockAddr (127,0,0,1) 8000)
-- True