From e35a67ffd5809c52fb9de302be455134f7d449bc Mon Sep 17 00:00:00 2001 From: Ximin Luo Date: Wed, 27 May 2020 01:04:57 +0100 Subject: [PATCH 1/3] Implement socket endpoints, in particular reading a string description of them Useful for 3rd-party networking applications that might want to pass around service specifiers without worrying whether these are IP addresses, DNS names, or UNIX-domain socket paths. Previously, there was no data type to encapsulate these options together. In particular, getAddrInfo had to be used to resolve DNS names into a SockAddr before calling connect/bind, but it could not deal with UNIX domain sockets. The new function sockNameToAddr takes this role, transparently converting DNS names and passing through non-DNS-names unaltered, so that it can be used uniformly without worrying about the specific type of input name/address. --- Network/Socket.hs | 7 ++++ Network/Socket/Info.hsc | 71 +++++++++++++++++++++++++++++++++++-- Network/Socket/Types.hsc | 25 +++++++++++++ tests/Network/SocketSpec.hs | 21 +++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/Network/Socket.hs b/Network/Socket.hs index 3c9f4f75..bae4358d 100644 --- a/Network/Socket.hs +++ b/Network/Socket.hs @@ -153,6 +153,7 @@ module Network.Socket , Socket , socket , openSocket + , socketFromEndpoint , withFdSocket , unsafeFdSocket , touchSocket @@ -183,8 +184,14 @@ module Network.Socket -- ** Protocol number , ProtocolNumber , defaultProtocol + -- * Basic socket endpoint type + , SockEndpoint(..) + , readSockEndpoint + , showSockEndpoint + , resolveEndpoint -- * Basic socket address type , SockAddr(..) + , sockAddrFamily , isSupportedSockAddr , getPeerName , getSocketName diff --git a/Network/Socket/Info.hsc b/Network/Socket/Info.hsc index 0a874a92..719d7ec0 100644 --- a/Network/Socket/Info.hsc +++ b/Network/Socket/Info.hsc @@ -7,14 +7,17 @@ module Network.Socket.Info where +import Control.Exception (try, IOException) import Foreign.Marshal.Alloc (alloca, allocaBytes) import Foreign.Marshal.Utils (maybeWith, with) import GHC.IO.Exception (IOErrorType(NoSuchThing)) import System.IO.Error (ioeSetErrorString, mkIOError) +import System.IO.Unsafe (unsafePerformIO) +import Text.Read (readEither) import Network.Socket.Imports import Network.Socket.Internal -import Network.Socket.Syscall +import Network.Socket.Syscall (socket) import Network.Socket.Types ----------------------------------------------------------------------------- @@ -467,10 +470,74 @@ showHostAddress6 ha6@(a1, a2, a3, a4) scanl (\c i -> if i == 0 then c - 1 else 0) 0 fields `zip` [0..] ----------------------------------------------------------------------------- - -- | A utility function to open a socket with `AddrInfo`. -- This is a just wrapper for the following code: -- -- > \addr -> socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr) openSocket :: AddrInfo -> IO Socket openSocket addr = socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr) + +----------------------------------------------------------------------------- +-- SockEndpoint + +-- | Read a string representing a socket endpoint. +readSockEndpoint :: PortNumber -> String -> Either String SockEndpoint +readSockEndpoint defPort hostport = case hostport of + '/':_ -> Right $ EndpointByAddr $ SockAddrUnix hostport + '[':tl -> case span ((/=) ']') tl of + (_, []) -> Left $ "unterminated IPv6 address: " <> hostport + (ipv6, _:port) -> case readAddr ipv6 of + Nothing -> Left $ "invalid IPv6 address: " <> ipv6 + Just addr -> EndpointByAddr . sockAddrPort addr <$> readPort port + _ -> case span ((/=) ':') hostport of + (host, port) -> case readAddr host of + Nothing -> EndpointByName host <$> readPort port + Just addr -> EndpointByAddr . sockAddrPort addr <$> readPort port + where + readPort "" = Right defPort + readPort ":" = Right defPort + readPort (':':port) = case readEither port of + Right p -> Right p + Left _ -> Left $ "bad port: " <> port + readPort x = Left $ "bad port: " <> x + hints = Just $ defaultHints { addrFlags = [AI_NUMERICHOST] } + readAddr host = case unsafePerformIO (try (getAddrInfo hints (Just host) Nothing)) of + Left e -> Nothing where _ = e :: IOException + Right r -> Just (addrAddress (head r)) + sockAddrPort h p = case h of + SockAddrInet _ a -> SockAddrInet p a + SockAddrInet6 _ f a s -> SockAddrInet6 p f a s + x -> x + +showSockEndpoint :: SockEndpoint -> String +showSockEndpoint n = case n of + EndpointByName h p -> h <> ":" <> show p + EndpointByAddr a -> show a + +-- | Resolve a socket endpoint into a list of socket addresses. +-- The result is always non-empty; Haskell throws an exception if name +-- resolution fails. +resolveEndpoint :: SockEndpoint -> IO [SockAddr] +resolveEndpoint name = case name of + EndpointByAddr a -> pure [a] + EndpointByName host port -> fmap addrAddress <$> getAddrInfo hints (Just host) (Just (show port)) + where + hints = Just $ defaultHints { addrSocketType = Stream } + -- prevents duplicates, otherwise getAddrInfo returns all socket types + +-- | Shortcut for creating a socket from a socket endpoint. +-- +-- >>> import Network.Socket +-- >>> let Right sn = readSockEndpoint 0 "0.0.0.0:0" +-- >>> (s, a) <- socketFromEndpoint sn head Stream defaultProtocol +-- >>> bind s a +socketFromEndpoint + :: SockEndpoint + -> ([SockAddr] -> SockAddr) + -> SocketType + -> ProtocolNumber + -> IO (Socket, SockAddr) +socketFromEndpoint end select stype protocol = do + a <- select <$> resolveEndpoint end + s <- socket (sockAddrFamily a) stype protocol + pure (s, a) diff --git a/Network/Socket/Types.hsc b/Network/Socket/Types.hsc index be9b9c45..6ea7db5e 100644 --- a/Network/Socket/Types.hsc +++ b/Network/Socket/Types.hsc @@ -53,7 +53,9 @@ module Network.Socket.Types ( , withNewSocketAddress -- * Socket address type + , SockEndpoint(..) , SockAddr(..) + , sockAddrFamily , isSupportedSockAddr , HostAddress , hostAddressToTuple @@ -1047,6 +1049,23 @@ type FlowInfo = Word32 -- | Scope identifier. type ScopeID = Word32 +-- | Socket endpoints. +-- +-- A wrapper around socket addresses that also accommodates the +-- popular usage of specifying them by name, e.g. "example.com:80". +-- We don't support service names here (string aliases for port +-- numbers) because they also imply a particular socket type, which +-- is outside of the scope of this data type. +-- +-- This roughly corresponds to the "authority" part of a URI, as +-- defined here: https://tools.ietf.org/html/rfc3986#section-3.2 +-- +-- See also 'Network.Socket.socketFromEndpoint'. +data SockEndpoint + = EndpointByName !String !PortNumber + | EndpointByAddr !SockAddr + deriving (Eq, Ord) + -- | Socket addresses. -- The existence of a constructor does not necessarily imply that -- that socket address type is supported on your system: see @@ -1070,6 +1089,12 @@ instance NFData SockAddr where rnf (SockAddrInet6 _ _ _ _) = () rnf (SockAddrUnix str) = rnf str +sockAddrFamily :: SockAddr -> Family +sockAddrFamily addr = case addr of + SockAddrInet _ _ -> AF_INET + SockAddrInet6 _ _ _ _ -> AF_INET6 + SockAddrUnix _ -> AF_UNIX + -- | Is the socket address type supported on this system? isSupportedSockAddr :: SockAddr -> Bool isSupportedSockAddr addr = case addr of diff --git a/tests/Network/SocketSpec.hs b/tests/Network/SocketSpec.hs index d5413b06..cac92e1c 100644 --- a/tests/Network/SocketSpec.hs +++ b/tests/Network/SocketSpec.hs @@ -163,6 +163,27 @@ spec = do -- check if an exception is not thrown. isSupportedSockAddr addr `shouldBe` True + it "endpoints API, IPv4" $ do + let Right end = readSockEndpoint 0 "127.0.0.1:6001" + (sock, addr) <- socketFromEndpoint end head Stream defaultProtocol + bind sock addr + listen sock 1 + close sock + + it "endpoints API, IPv6" $ do + let Right end = readSockEndpoint 0 "[::1]:6001" + (sock, addr) <- socketFromEndpoint end head Stream defaultProtocol + bind sock addr + listen sock 1 + close sock + + it "endpoints API, DNS" $ do + let Right end = readSockEndpoint 0 "localhost:6001" + (sock, addr) <- socketFromEndpoint end head Stream defaultProtocol + bind sock addr + listen sock 1 + close sock + #if !defined(mingw32_HOST_OS) when isUnixDomainSocketAvailable $ do context "unix sockets" $ do From b6c9ef59fb88ed7b743b234a1348f098f596b3ab Mon Sep 17 00:00:00 2001 From: Ximin Luo Date: Wed, 27 May 2020 11:41:32 +0100 Subject: [PATCH 2/3] Use return instead of pure --- Network/Socket/Info.hsc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Network/Socket/Info.hsc b/Network/Socket/Info.hsc index 719d7ec0..b262182c 100644 --- a/Network/Socket/Info.hsc +++ b/Network/Socket/Info.hsc @@ -519,7 +519,7 @@ showSockEndpoint n = case n of -- resolution fails. resolveEndpoint :: SockEndpoint -> IO [SockAddr] resolveEndpoint name = case name of - EndpointByAddr a -> pure [a] + EndpointByAddr a -> return [a] EndpointByName host port -> fmap addrAddress <$> getAddrInfo hints (Just host) (Just (show port)) where hints = Just $ defaultHints { addrSocketType = Stream } @@ -540,4 +540,4 @@ socketFromEndpoint socketFromEndpoint end select stype protocol = do a <- select <$> resolveEndpoint end s <- socket (sockAddrFamily a) stype protocol - pure (s, a) + return (s, a) From 364b4a69234c7164bf70f97577c7c6cb623a786d Mon Sep 17 00:00:00 2001 From: Ximin Luo Date: Wed, 27 May 2020 11:47:44 +0100 Subject: [PATCH 3/3] use HostName rather than String, and move those aliases into Types.hsc --- Network/Socket/Info.hsc | 9 --------- Network/Socket/Types.hsc | 11 ++++++++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Network/Socket/Info.hsc b/Network/Socket/Info.hsc index b262182c..6b92ebb4 100644 --- a/Network/Socket/Info.hsc +++ b/Network/Socket/Info.hsc @@ -20,15 +20,6 @@ import Network.Socket.Internal import Network.Socket.Syscall (socket) import Network.Socket.Types ------------------------------------------------------------------------------ - --- | Either a host name e.g., @\"haskell.org\"@ or a numeric host --- address string consisting of a dotted decimal IPv4 address or an --- IPv6 address e.g., @\"192.168.0.1\"@. -type HostName = String --- | Either a service name e.g., @\"http\"@ or a numeric port number. -type ServiceName = String - ----------------------------------------------------------------------------- -- Address and service lookups diff --git a/Network/Socket/Types.hsc b/Network/Socket/Types.hsc index 6ea7db5e..26afe0da 100644 --- a/Network/Socket/Types.hsc +++ b/Network/Socket/Types.hsc @@ -74,6 +74,8 @@ module Network.Socket.Types ( , defaultProtocol , PortNumber , defaultPort + , HostName + , ServiceName -- * Low-level helpers , zeroMemory @@ -291,6 +293,13 @@ type ProtocolNumber = CInt defaultProtocol :: ProtocolNumber defaultProtocol = 0 +-- | Either a host name e.g., @\"haskell.org\"@ or a numeric host +-- address string consisting of a dotted decimal IPv4 address or an +-- IPv6 address e.g., @\"192.168.0.1\"@. +type HostName = String +-- | Either a service name e.g., @\"http\"@ or a numeric port number. +type ServiceName = String + ----------------------------------------------------------------------------- -- Socket types @@ -1062,7 +1071,7 @@ type ScopeID = Word32 -- -- See also 'Network.Socket.socketFromEndpoint'. data SockEndpoint - = EndpointByName !String !PortNumber + = EndpointByName !HostName !PortNumber | EndpointByAddr !SockAddr deriving (Eq, Ord)