Puru Gupta
Puru Gupta

Reputation: 43

How to make a request to an IPv6 address using the http-client package in haskell?

I've been trying to make a request to an IPv6 address using the parseRequest function from Network.HTTP.Client (https://hackage.haskell.org/package/http-client-0.7.10/docs/Network-HTTP-Client.html) package as follows:

request <- parseRequest "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"

Instead of parsing it as an address/addrInfo, it is parsed as a hostname and throws the error: does not exist (Name or service not known). As a next step, I tried pointing a domain to the same IPv6 address and then using the domain name in parseRequest, then it successfully resolves that into the IPv6 address and makes the request. Is there some other way I can directly use the IPv6 address to make the request using the http-client package?

PS: I also tried without square brackets around the IP address, in this case the error is Invalid URL:

request <- parseRequest "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334"

More context:

For an IPv4 address, the getAddrInfo function generates the address as:

AddrInfo {addrFlags = [AI_NUMERICHOST], addrFamily = AF_INET, addrSocketType = Stream, addrProtocol = 6, addrAddress = 139.59.90.1:80, addrCanonName = Nothing}

whereas for IPv6 address(inside the square brackets format):

AddrInfo {addrFlags = [AI_ADDRCONFIG], addrFamily = AF_UNSPEC, addrSocketType = Stream, addrProtocol = 6, addrAddress = 0.0.0.0:0, addrCanonName = Nothing}

and the error prints as:

(ConnectionFailure Network.Socket.getAddrInfo (called with preferred socket type/protocol: AddrInfo {addrFlags = [AI_ADDRCONFIG], addrFamily = AF_UNSPEC, addrSocketType = Stream, addrProtocol = 6, addrAddress = 0.0.0.0:0, addrCanonName = Nothing}, host name: Just "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", service name: Just "80"): does not exist (Name or service not known))

Upvotes: 4

Views: 2277

Answers (1)

K. A. Buhr
K. A. Buhr

Reputation: 50819

When a literal IPv6 address is used in a URL, it should be surrounded by square brackets (as per RFC 2732) so the colons in the literal address aren't misinterpreted as some kind of port designation.

When a literal IPv6 address is resolved using the C library function getaddrinfo (or the equivalent Haskell function getAddrInfo), these functions are not required to handle these extra square brackets, and at least on Linux they don't.

Therefore, it's the responsibility of the HTTP client library to remove the square brackets from the hostname extracted from the URL before resolving the literal IPv6 address using getaddrinfo, and the http-client package doesn't do this, at least as of version 0.7.10. So, this is a bug, and I can see you've appropriately filed a bug report.

Unfortunately, I don't see an easy way to work around the issue. You can manipulate the Request after parsing to remove the square brackets from the host field, like so:

{-# LANGUAGE OverloadedStrings #-}

import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Network.HTTP.Client
import Network.HTTP.Types.Status (statusCode)

main :: IO ()
main = do
  manager <- newManager defaultManagerSettings
  request <- parseRequest "http://[::1]"
  let request' = request { host = removeBrackets (host request) }
  response <- httpLbs request' manager
  print response

removeBrackets :: ByteString -> ByteString
removeBrackets bs =
  case BS.stripPrefix "[" bs >>= BS.stripSuffix "]" of
    Just bs' -> bs'
    Nothing  -> bs

The problem with this is that it also removes the square brackets from the value in the Host header, so the HTTP request will contain the header:

Host: ::1

instead of the correct

Host: [::1]

which may or may not cause problems, depending on the web server at the other end.

You could try using a patched http-client package. The following patch against version 0.7.10 seems to work, but I didn't test it very extensively:

diff --git a/Network/HTTP/Client/Connection.hs b/Network/HTTP/Client/Connection.hs
index 0e329cd..719822e 100644
--- a/Network/HTTP/Client/Connection.hs
+++ b/Network/HTTP/Client/Connection.hs
@@ -15,6 +15,7 @@ module Network.HTTP.Client.Connection
 
 import Data.ByteString (ByteString, empty)
 import Data.IORef
+import Data.List (stripPrefix, isSuffixOf)
 import Control.Monad
 import Network.HTTP.Client.Types
 import Network.Socket (Socket, HostAddress)
@@ -158,8 +159,12 @@ withSocket :: (Socket -> IO ())
 withSocket tweakSocket hostAddress' host' port' f = do
     let hints = NS.defaultHints { NS.addrSocketType = NS.Stream }
     addrs <- case hostAddress' of
-        Nothing ->
-            NS.getAddrInfo (Just hints) (Just host') (Just $ show port')
+        Nothing -> do
+            let port'' = Just $ show port'
+            case ip6Literal host' of
+                Just lit -> NS.getAddrInfo (Just hints { NS.addrFlags = [NS.AI_NUMERICHOST] })
+                                           (Just lit) port''
+                Nothing -> NS.getAddrInfo (Just hints) (Just host') port''
         Just ha ->
             return
                 [NS.AddrInfo
@@ -173,6 +178,11 @@ withSocket tweakSocket hostAddress' host' port' f = do
 
     E.bracketOnError (firstSuccessful addrs $ openSocket tweakSocket) NS.close f
 
+  where
+    ip6Literal h = case stripPrefix "[" h of
+        Just rest | "]" `isSuffixOf` rest -> Just (init rest)
+        _ -> Nothing
+
 openSocket tweakSocket addr =
     E.bracketOnError
         (NS.socket (NS.addrFamily addr) (NS.addrSocketType addr)

Upvotes: 3

Related Questions