packages feed

dnsbase-1.0.0.0: internal/Net/DNSBase/Internal/SockIO.hs

-- |
-- Module      : Net.DNSBase.Internal.SockIO
-- Description : Low-level UDP/TCP socket I/O for DNS messages
-- Copyright   : (c) IIJ Innovation Institute Inc., 2009
--               (c) Viktor Dukhovni, 2020-2026
-- License     : BSD-3-Clause
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
{-# LANGUAGE RecordWildCards #-}

module Net.DNSBase.Internal.SockIO (
    -- * Receiving DNS messages
    receiveUDP
  , receiveTCP
    -- * Sending pre-encoded DNS messages
  , sendUDP
  , sendTCP
  ) where

import qualified Data.ByteString as B
import qualified Network.Socket.ByteString as Socket
import Network.Socket (Socket)
import Network.Socket.ByteString (recv)
import System.IO.Error (tryIOError, mkIOError, eofErrorType)

import Net.DNSBase.Internal.Error
import Net.DNSBase.Internal.Util
import Net.DNSBase.Resolver.Internal.Types

----------------------------------------------------------------

-- | Receive and a single 'Net.DNSBase.Message.DNSMessage' over a UDP 'Socket'.  Messages
-- longer than 'Net.DNSBase.Resolver.maxUdpSize' are silently truncated, but this should not occur
-- in practice, since we cap the advertised EDNS UDP buffer size limit at the
-- same value.  A 'DNSError' is raised if the I/O operation fails.
--
receiveUDP :: Word16 -> Socket -> DNSIO B.ByteString
receiveUDP maxudp sock = withExceptT wrapError $ recv' sock bufsiz
  where
    bufsiz = fromIntegral maxudp
    wrapError = NetworkError . NetworkFailure

recv' :: Socket -> Int -> ExceptT IOError IO ByteString
recv' sock bufsiz = ExceptT $ tryIOError $ recv sock bufsiz

-- | Receive a single DNS message over a virtual-circuit (TCP) connection.  It
-- is up to the caller to implement any desired timeout. An 'DNSError' is
-- raised if the I/O operation fails.
--
receiveTCP :: Socket -> DNSIO B.ByteString
receiveTCP sock = recvDNS sock 2 >>= recvDNS sock . toLen
  where
    toLen :: ByteString -> Int
    toLen = fromIntegral . word16be

recvDNS :: Socket -> Int -> DNSIO ByteString
recvDNS sock len = withExceptT wrapError recv1
  where
    wrapError = NetworkError . NetworkFailure

    recv1 :: ExceptT IOError IO ByteString
    recv1 = recvCore len >>= cond (B.length .= len) return loop

    loop :: ByteString -> ExceptT IOError IO ByteString
    loop bs0 = do
        let left = len - B.length bs0
        bs1 <- recvCore left
        cond (B.length .= len) return loop $! bs0 <> bs1

    eofE = mkIOError eofErrorType "connection terminated" Nothing Nothing

    recvCore :: Int -> ExceptT IOError IO ByteString
    recvCore len0 = recv' sock len0
                >>= cond B.null (const $ throwE eofE) return

----------------------------------------------------------------

-- | Send an encoded 'Net.DNSBase.Message.DNSMessage' datagram over UDP.  The socket must be
-- explicitly connected to the destination nameserver.  The message length is
-- implicit in the size of the UDP datagram.  With TCP you must use 'sendTCP',
-- because TCP does not have message boundaries, and each message needs to be
-- prepended with an explicit length.
--
sendUDP :: Socket -> ByteString -> DNSIO ()
sendUDP sock = lift . void . Socket.send sock

-- | Send one or more encoded 'Net.DNSBase.Message.DNSMessage' buffers over TCP, each already
-- encapsulated with an explicit length prefix and then concatenated into a
-- single buffer.  DO NOT use 'sendTCP' with UDP.
--
sendTCP :: Socket -> ByteString -> DNSIO ()
sendTCP vc = lift . Socket.sendAll vc