packages feed

dnsbase-1.0.0.0: src/Net/DNSBase/RData/SVCB/SPV.hs

{-|
Module      : Net.DNSBase.RData.SVCB.SPV
Description : Concrete value types for SVCB / HTTPS service parameters
Copyright   : (c) Viktor Dukhovni, 2026
License     : BSD-3-Clause
Maintainer  : ietf-dane@dukhovni.org
Stability   : unstable

One 'KnownSVCParamValue' instance per standardised service
parameter key.  Each type corresponds to a 'SVCParamKey' from
the IANA registry and carries the value payload for that key
in a form suited to the application that consumes it (a port
as 'Word16', address hints as a 'NonEmpty' of 'IPv4' / 'IPv6',
and so on).

An unknown key falls through to
'Net.DNSBase.RData.SVCB.OpaqueSPV', which preserves the raw wire
bytes.  Applications register additional typed values at runtime
via 'Net.DNSBase.Resolver.extendRRwithType' on the @SVCB@ or
@HTTPS@ RR type — see "Net.DNSBase.Extensible" for a worked
example.
-}

module Net.DNSBase.RData.SVCB.SPV
    ( SPV_mandatory(SPV_MANDATORY)
    , SPV_alpn(..)
    , SPV_ndalpn(..)
    , SPV_port(..)
    , SPV_ipv4hint(..)
    , SPV_ipv6hint(..)
    , SPV_ech(..)
    , SPV_dohpath(..)
    , SPV_docpath(..)
    , SPV_tlsgroups(..)
    , SPV_pvd(..)
    ) where

import qualified Data.ByteString.Short as SB
import qualified Data.List.NonEmpty as NE
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Text.Unsafe as T
import Data.Set (Set)

import Net.DNSBase.Internal.Util

import Net.DNSBase.Bytes
import Net.DNSBase.Decode.State
import Net.DNSBase.Encode.Metric
import Net.DNSBase.Encode.State
import Net.DNSBase.NonEmpty
import Net.DNSBase.Present
import Net.DNSBase.RData.SVCB.SPVList
import Net.DNSBase.RData.SVCB.SVCParamKey
import Net.DNSBase.RData.SVCB.SVCParamValue
import Net.DNSBase.Text

-- | The @mandatory@ service parameter
-- ([RFC 9460 section 8](https://datatracker.ietf.org/doc/html/rfc9460#section-8))
-- — the set of additional 'SVCParamKey' codes a client must
-- recognise to make sense of this RR, on top of any keys that
-- are automatically mandatory for the application protocol.  A
-- client that does not understand all the listed keys must
-- ignore the RR.
--
-- The set may not be empty on the wire (an empty list is
-- encoded as the parameter being absent) and may hold at most
-- 32767 keys.  The constructor is not exported; build instances
-- via 'fromNonEmptyList' or by combining values with their
-- 'Semigroup' instance.
newtype SPV_mandatory = SPV_mandatory (Set SVCParamKey)
    deriving (Eq, Semigroup)

-- | One-way pattern exposing the underlying key set for lookups.
-- Construction is available only via 'fromNonEmptyList' or via the
-- 'Semigroup' instance.
pattern SPV_MANDATORY :: Set SVCParamKey -- ^ The keys as a 'Set'
                      -> SPV_mandatory
pattern SPV_MANDATORY s <- SPV_mandatory s
{-# COMPLETE SPV_MANDATORY #-}

instance Show SPV_mandatory where
    showsPrec p (SPV_mandatory s) = showsP p $
        showString "fromNonEmptyList @SPV_mandatory "
        . shows' (NE.fromList $ Set.toList s)

instance IsNonEmptyList SPV_mandatory where
    type Item1 SPV_mandatory = SVCParamKey
    fromNonEmptyList = coerce . Set.fromList . NE.toList
    toNonEmptyList = NE.fromList . Set.toList . coerce

-- | Wire-form order
instance Ord SPV_mandatory where
    SPV_MANDATORY a `compare` SPV_MANDATORY b =
        comparing Set.size a b
        <> comparing Set.toList a b

instance Presentable SPV_mandatory where
    present (SPV_MANDATORY (NE.fromList . Set.toList -> key :| keys)) =
        present MANDATORY
        . pfst key
        . flip (foldr pnxt) keys
      where
        pfst = presentCharSep '='
        pnxt = presentCharSep ','

instance KnownSVCParamValue SPV_mandatory where
    spvKey _ = MANDATORY
    encodeSPV (SPV_MANDATORY (coerce -> ks)) = do
        when (Set.size ks > 0x7fff) do failWith CantEncode
        putSizedBuilder $ foldMap mbWord16
                        $ coerce @[SVCParamKey] @[Word16] $ Set.toList ks

    -- | Decode the mandatory key list.
    -- XXX: Does not yet enforce ascending order, non-duplication,
    -- self-exclusion, or exclusion of automatically mandatory keys, all of
    -- which are required by the SVCB draft:
    -- <https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-12#section-2.2>,
    -- <https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-12#section-8>
    --
    -- The resulting keyset will however be de-duplicated and ordered.
    --
    decodeSPV _ len = do
        k <- mkKey <$> get16
        ks <- getFixedWidthSequence 2 (mkKey <$> get16) (len - 2)
        pure $ SVCParamValue $ mkMandatory $ Set.fromList $ k : ks
      where
        mkKey :: Word16 -> SVCParamKey
        mkKey = coerce
        mkMandatory :: Set SVCParamKey -> SPV_mandatory
        mkMandatory = coerce

-- | The @alpn@ service parameter
-- ([RFC 9460 section 7.1](https://datatracker.ietf.org/doc/html/rfc9460#section-7.1))
-- — the set of Application-Layer Protocol Negotiation
-- ([RFC 7301](https://datatracker.ietf.org/doc/html/rfc7301))
-- protocol identifiers this service endpoint supports.  Together
-- with 'SPV_ndalpn' it defines the SVCB ALPN set: by default the
-- scheme's protocol is implicitly included, and 'SPV_ndalpn'
-- suppresses that default.
newtype SPV_alpn = SPV_ALPN (NonEmpty ShortByteString)
    deriving (Eq, Show)

instance Ord SPV_alpn where
    (SPV_ALPN as) `compare` (SPV_ALPN bs) =
        comparing len as bs
        <> comparing (coerce @(NonEmpty ShortByteString) @(NonEmpty DnsText)) as bs
      where
        len xs = foldl' (\a x -> a + 1 + SB.length x) 0 xs

instance Presentable SPV_alpn where
    present (SPV_ALPN vs) =
        present ALPN . present '=' . presentSPVList vs

instance KnownSVCParamValue SPV_alpn where
    spvKey _ = ALPN
    encodeSPV (SPV_ALPN vs) =
        putSizedBuilder $ foldMap (mbShortByteStringLen8 . coerce) vs
    decodeSPV _ len = do
        pos0 <- getPosition
        a <- getShortByteStringLen8
        pos1 <- getPosition
        let used = pos1 - pos0
        as <- getVarWidthSequence getShortByteStringLen8 (len - used)
        pure $ SVCParamValue . SPV_ALPN $ a :| as

-- | The @no-default-alpn@ service parameter
-- ([RFC 9460 section 7.1](https://datatracker.ietf.org/doc/html/rfc9460#section-7.1))
-- — a value-less flag that suppresses the scheme's default ALPN
-- from the SVCB ALPN set.  Meaningful only alongside an explicit
-- 'SPV_alpn' that supplies a replacement protocol list.
data SPV_ndalpn = SPV_NDALPN
    deriving (Eq, Ord, Show)

instance Presentable SPV_ndalpn where
    present _ = present NODEFAULTALPN

instance KnownSVCParamValue SPV_ndalpn where
    spvKey _ = NODEFAULTALPN
    encodeSPV _ = pure ()
    decodeSPV _ _ = pure $ SVCParamValue SPV_NDALPN

-- | The @port@ service parameter
-- ([RFC 9460 section 7.2](https://datatracker.ietf.org/doc/html/rfc9460#section-7.2))
-- — overrides the scheme's default TCP or UDP port for reaching
-- this service endpoint.
newtype SPV_port = SPV_PORT Word16
    deriving newtype (Eq, Ord, Enum, Bounded, Num, Real, Integral, Show, Read)

instance Presentable SPV_port where
    present (SPV_PORT port) =
        present PORT
        . presentCharSep '=' port

instance KnownSVCParamValue SPV_port where
    spvKey _ = PORT
    encodeSPV = put16 . coerce
    decodeSPV _ _ = SVCParamValue . SPV_PORT <$> get16

-- | The @ipv4hint@ service parameter
-- ([RFC 9460 section 7.3](https://datatracker.ietf.org/doc/html/rfc9460#section-7.3))
-- — a non-empty list of IPv4 addresses for this service endpoint
-- that a client may use to start a connection in parallel with
-- an authoritative @A@ lookup.  Hints are advisory; clients
-- should still validate them against authoritative address
-- records when they arrive.
newtype SPV_ipv4hint = SPV_IPV4HINT (NonEmpty IPv4)
    deriving (Eq)

instance Show SPV_ipv4hint where
    showsPrec p (SPV_IPV4HINT ips) = showsP p $
        showString "SPV_IPV4HINT "
        . shows' (fmap show ips)

instance Ord SPV_ipv4hint where
    compare (SPV_IPV4HINT a) (SPV_IPV4HINT b) =
        comparing NE.length a b <> compare a b

instance Presentable SPV_ipv4hint where
    present (SPV_IPV4HINT (a :| as)) =
        present IPV4HINT
        . pfst a
        . flip (foldr pnxt) as
      where
        pfst = presentCharSep '='
        pnxt = presentCharSep ','

instance KnownSVCParamValue SPV_ipv4hint where
    spvKey _ = IPV4HINT
    encodeSPV (SPV_IPV4HINT ips) = putSizedBuilder $ foldMap mbIPv4 ips
    decodeSPV _ len = do
        ip  <- getIPv4
        ips <- getFixedWidthSequence 4 getIPv4 (len - 4)
        return $ SVCParamValue $ SPV_IPV4HINT (ip :| ips)

-- | The @ipv6hint@ service parameter
-- ([RFC 9460 section 7.3](https://datatracker.ietf.org/doc/html/rfc9460#section-7.3))
-- — the IPv6 counterpart of 'SPV_ipv4hint': a non-empty list of
-- IPv6 addresses to try in parallel with the authoritative
-- @AAAA@ lookup.
newtype SPV_ipv6hint = SPV_IPV6HINT (NonEmpty IPv6)
    deriving (Eq)

instance Show SPV_ipv6hint where
    showsPrec p (SPV_IPV6HINT ips) = showsP p $
        showString "SPV_IPV6HINT "
        . shows' (fmap show ips)

instance Ord SPV_ipv6hint where
    compare (SPV_IPV6HINT a) (SPV_IPV6HINT b) =
        comparing NE.length a b <> compare a b

instance Presentable SPV_ipv6hint where
    present (SPV_IPV6HINT (a :| as)) =
        present IPV6HINT
        . pfst a
        . flip (foldr pnxt) as
      where
        pfst = presentCharSep '='
        pnxt = presentCharSep ','

instance KnownSVCParamValue SPV_ipv6hint where
    spvKey _ = IPV6HINT
    encodeSPV (SPV_IPV6HINT ips) = putSizedBuilder $ foldMap mbIPv6 ips
    decodeSPV _ len = do
        ip  <- getIPv6
        ips <- getFixedWidthSequence 16 getIPv6 (len - 16)
        return $ SVCParamValue $ SPV_IPV6HINT (ip :| ips)

-- | The @ech@ service parameter
-- ([RFC9848, Section 3](https://datatracker.ietf.org/doc/html/rfc9848#section-3))
-- — an Encrypted Client Hello (ECH) configuration list.
-- Presented as a base64-encoded value.
newtype SPV_ech = SPV_ECH ShortByteString
    deriving (Eq, Show)

instance Ord SPV_ech where
    compare = dnsTextCmp

instance Presentable SPV_ech where
    present (SPV_ECH c) = present ECH . presentCharSep @Bytes64 '=' (coerce c)

instance KnownSVCParamValue SPV_ech where
    spvKey _ = ECH
    encodeSPV (SPV_ECH c) = putShortByteString $ coerce c
    decodeSPV _ 0 = failSGet "Invalid empty 'ech' ParamKey value"
    decodeSPV _ len
        | len < 4 = failSGet "'ech' ParamKey value too short"
        | otherwise = SVCParamValue . SPV_ECH <$> getShortNByteString len

-- | The @dohpath@ service parameter
-- ([RFC 9461](https://datatracker.ietf.org/doc/html/rfc9461#name-new-svcparamkey-dohpath))
-- — a URI template (UTF-8) advertising the DNS-over-HTTPS
-- endpoint at this service.  Typically seen on resolver-discovery
-- answers to queries for @_dns.resolver.arpa@ or on explicit
-- queries to a specific operator's resolver.
newtype SPV_dohpath = SPV_DOHPATH T.Text
    deriving (Eq, Show)

instance Ord SPV_dohpath where
    compare (SPV_DOHPATH a) (SPV_DOHPATH b) =
        comparing T.lengthWord8 a b
        <> compare a b

instance Presentable SPV_dohpath where
    present (SPV_DOHPATH uri) =
        present DOHPATH . presentCharSep @DnsUtf8Text '=' (coerce uri)

instance KnownSVCParamValue SPV_dohpath where
    spvKey _ = DOHPATH
    encodeSPV (SPV_DOHPATH uri) = putUtf8Text uri
    decodeSPV _ = SVCParamValue . SPV_DOHPATH <.> getUtf8Text

-- | The @docpath@ service parameter
-- ([RFC 9953 section 8.2](https://datatracker.ietf.org/doc/html/rfc9953#section-8.2))
-- — the absolute path to the DNS-over-CoAP (DoC) resource at this
-- service endpoint, represented as a list of path segments.  An
-- empty list denotes the root path @\"/\"@ (on the wire: a
-- zero-length SvcParamValue).
--
-- Each segment is 1..255 octets per the RFC ABNF; the wire-form
-- decoder rejects zero-length segments.  Presentation form is a
-- comma-separated list of segments using the standard RFC 9460
-- Appendix A.1 quoting and escaping rules, identical to those of
-- 'SPV_alpn'.
newtype SPV_docpath = SPV_DOCPATH [ShortByteString]
    deriving (Eq, Show)

-- | Wire-form canonical order: by total wire length first, then
-- by the segment-list 'DnsText' ordering.
instance Ord SPV_docpath where
    (SPV_DOCPATH as) `compare` (SPV_DOCPATH bs) =
        comparing wireLen as bs
        <> comparing (coerce @[ShortByteString] @[DnsText]) as bs
      where
        wireLen = foldl' (\a x -> a + 1 + SB.length x) 0

instance Presentable SPV_docpath where
    present (SPV_DOCPATH vs) =
        present DOCPATH . present '=' . presentCSVList vs

instance KnownSVCParamValue SPV_docpath where
    spvKey _ = DOCPATH
    encodeSPV (SPV_DOCPATH vs) =
        putSizedBuilder $ foldMap mbShortByteStringLen8 vs
    decodeSPV _ len = do
        vs <- getVarWidthSequence segment len
        pure $ SVCParamValue $ SPV_DOCPATH vs
      where
        segment = do
            seg <- getShortByteStringLen8
            when (SB.null seg) $ failSGet "Empty docpath segment"
            pure seg

-- | The @tls-supported-groups@ service parameter
-- ([draft-ietf-tls-key-share-prediction-04 section 5](https://datatracker.ietf.org/doc/html/draft-ietf-tls-key-share-prediction-04#section-5))
-- — a non-empty list of TLS Named Group codepoints (the
-- [IANA TLS Supported Groups registry](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8))
-- that the service supports.  Clients can use this hint to
-- predict an acceptable key share, reducing the chance of a
-- HelloRetryRequest round-trip.
--
-- The wire form is a non-empty sequence of 16-bit codepoints in
-- network byte order; the decoder rejects an empty value.  The
-- presentation form is a comma-separated list of decimal
-- integers and forbids zone-file escape sequences (per the
-- draft).  Duplicates are flagged as a syntax error by the
-- specification but are not currently rejected by either the
-- encoder or the decoder — callers are expected to feed in a
-- duplicate-free list, in their preferred order (the spec does
-- not impose canonical ordering on the wire).
newtype SPV_tlsgroups = SPV_TLSGROUPS (NonEmpty Word16)
    deriving (Eq, Show)

-- | Wire-form canonical order: by length first, then elementwise.
instance Ord SPV_tlsgroups where
    (SPV_TLSGROUPS as) `compare` (SPV_TLSGROUPS bs) =
        comparing NE.length as bs <> compare as bs

instance Presentable SPV_tlsgroups where
    present (SPV_TLSGROUPS (g :| gs)) =
        present TLSGROUPS
        . pfst g
        . flip (foldr pnxt) gs
      where
        pfst = presentCharSep '='
        pnxt = presentCharSep ','

instance KnownSVCParamValue SPV_tlsgroups where
    spvKey _ = TLSGROUPS
    encodeSPV (SPV_TLSGROUPS gs) = putSizedBuilder $ foldMap mbWord16 gs
    decodeSPV _ len = do
        g  <- get16
        gs <- getFixedWidthSequence 2 get16 (len - 2)
        pure $ SVCParamValue $ SPV_TLSGROUPS (g :| gs)

-- | The @pvd@ service parameter
-- ([draft-ietf-intarea-proxy-config-14 section 7.5](https://datatracker.ietf.org/doc/html/draft-ietf-intarea-proxy-config-14#section-7.5))
-- — a value-less flag that announces the host supports
-- Provisioning Domain discovery via the well-known PvD URI.  Its
-- presence in an SVCB or HTTPS RR signals that a client MAY
-- fetch PvD Additional Information from
-- @https:\/\/host\/.well-known\/pvd@.  Per the draft, the wire
-- value MUST be empty; the presentation form is just the bare
-- key name with no @\'=\'@.
data SPV_pvd = SPV_PVD
    deriving (Eq, Ord, Show)

instance Presentable SPV_pvd where
    present _ = present PVD

instance KnownSVCParamValue SPV_pvd where
    spvKey _ = PVD
    encodeSPV _ = pure ()
    decodeSPV _ _ = pure $ SVCParamValue SPV_PVD