mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-05 19:05:49 +00:00
138 lines
5.1 KiB
Haskell
138 lines
5.1 KiB
Haskell
{-# LANGUAGE LambdaCase #-}
|
||
{-# LANGUAGE NamedFieldPuns #-}
|
||
{-# LANGUAGE OverloadedStrings #-}
|
||
{-# LANGUAGE StrictData #-}
|
||
{-# LANGUAGE TemplateHaskell #-}
|
||
|
||
module Simplex.Messaging.SimplexName
|
||
( SimplexNameInfo (..),
|
||
SimplexDomain (..),
|
||
SimplexTLD (..),
|
||
SimplexNameType (..),
|
||
fullDomainName,
|
||
shortNameInfoStr,
|
||
)
|
||
where
|
||
|
||
import Control.Applicative (optional, (<|>))
|
||
import qualified Data.Aeson.TH as J
|
||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||
import qualified Data.Attoparsec.Text as AT
|
||
import Data.ByteString.Char8 (ByteString)
|
||
import qualified Data.ByteString.Char8 as B
|
||
import Data.Char (isDigit)
|
||
import Data.Functor (($>))
|
||
import Data.Text (Text)
|
||
import qualified Data.Text as T
|
||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_)
|
||
import Simplex.Messaging.Encoding (Encoding (..))
|
||
import Simplex.Messaging.Encoding.String
|
||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
|
||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||
|
||
data SimplexNameInfo = SimplexNameInfo
|
||
{ nameType :: SimplexNameType,
|
||
nameDomain :: SimplexDomain
|
||
}
|
||
deriving (Eq, Show)
|
||
|
||
data SimplexDomain = SimplexDomain
|
||
{ nameTLD :: SimplexTLD,
|
||
domain :: Text,
|
||
subDomain :: [Text] -- parent to child: ["b", "a"] for a.b.domain.simplex
|
||
}
|
||
deriving (Eq, Show)
|
||
|
||
data SimplexTLD = TLDSimplex | TLDTesting | TLDWeb
|
||
deriving (Eq, Show)
|
||
|
||
data SimplexNameType = NTPublicGroup | NTContact
|
||
deriving (Eq, Show)
|
||
|
||
instance StrEncoding SimplexNameType where
|
||
strEncode = \case
|
||
NTPublicGroup -> "#"
|
||
NTContact -> "@"
|
||
strP = A.char '#' $> NTPublicGroup <|> A.char '@' $> NTContact
|
||
|
||
nameLabelP :: AT.Parser Text
|
||
nameLabelP = do
|
||
label <- T.intercalate "-" <$> AT.takeWhile1 (\c -> isNameLetter c || isDigit c) `AT.sepBy1` AT.char '-'
|
||
-- DNS label limit: each dot-separated component is at most 63 bytes (labels
|
||
-- are ASCII, so character count == byte count)
|
||
if T.length label > 63 then fail "name label exceeds 63 bytes" else pure label
|
||
where
|
||
-- ASCII letters only. SNRC contracts hash byte sequences via keccak; ENS
|
||
-- uses UTS-46 + Punycode for IDN, which we do not implement. Admitting
|
||
-- Cyrillic / Greek / etc. via Data.Char.isAlpha would (a) make namehash
|
||
-- diverge from any IDN-aware registrar and (b) allow homograph spoofing
|
||
-- (Cyrillic а vs ASCII a hash to different on-chain records).
|
||
isNameLetter c = c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
|
||
|
||
-- | Cap the name at 253 bytes (DNS full-domain limit)
|
||
boundedNonSpace :: A.Parser ByteString
|
||
boundedNonSpace = do
|
||
bs <- A.scan (0 :: Int) $ \i c ->
|
||
if i <= 253 && not (A.isSpace c) then Just (i + 1) else Nothing
|
||
if B.null bs
|
||
then fail "expected non-empty name token"
|
||
else if B.length bs > 253 then fail "name exceeds 253 bytes" else pure bs
|
||
|
||
instance StrEncoding SimplexNameInfo where
|
||
strEncode SimplexNameInfo {nameType, nameDomain} =
|
||
strEncode nameType <> strEncode nameDomain
|
||
strP = optional "simplex:/name" *> ((strP >>= infoP) <|> infoP NTPublicGroup)
|
||
where
|
||
infoP NTPublicGroup = SimplexNameInfo NTPublicGroup <$> (strP <|> bareName)
|
||
infoP NTContact = SimplexNameInfo NTContact <$> strP
|
||
bareName = parseBare . safeDecodeUtf8 <$?> boundedNonSpace
|
||
parseBare s = (\name -> SimplexDomain TLDSimplex (T.toLower name) []) <$> AT.parseOnly (nameLabelP <* AT.endOfInput) s
|
||
|
||
instance StrEncoding SimplexDomain where
|
||
strEncode = encodeUtf8 . fullDomainName
|
||
strP = parseDomain . safeDecodeUtf8 <$?> boundedNonSpace
|
||
where
|
||
parseDomain s = AT.parseOnly (nameLabelP `AT.sepBy1` AT.char '.' <* AT.endOfInput) s >>= mkDomain
|
||
mkDomain labels = case reverse lowered of
|
||
[] -> Left "empty name"
|
||
[_] -> Left "domain requires TLD"
|
||
"simplex" : name : sub -> Right (SimplexDomain TLDSimplex name sub)
|
||
"testing" : name : sub -> Right (SimplexDomain TLDTesting name sub)
|
||
_ -> Right (SimplexDomain TLDWeb (T.intercalate "." lowered) [])
|
||
where
|
||
lowered = map T.toLower labels
|
||
|
||
instance Encoding SimplexDomain where
|
||
smpEncode = strEncode
|
||
smpP = strP
|
||
|
||
fullDomainName :: SimplexDomain -> Text
|
||
fullDomainName SimplexDomain {nameTLD, domain, subDomain} = T.intercalate "." (reverse subDomain ++ [domain] ++ tld')
|
||
where
|
||
tld' = case nameTLD of
|
||
TLDSimplex -> ["simplex"]
|
||
TLDTesting -> ["testing"]
|
||
TLDWeb -> []
|
||
|
||
shortNameInfoStr :: SimplexNameInfo -> Text
|
||
shortNameInfoStr = \case
|
||
SimplexNameInfo {nameType = NTPublicGroup, nameDomain = SimplexDomain {nameTLD = TLDSimplex, domain, subDomain = []}} -> "#" <> domain
|
||
info -> pfx <> fullDomainName (nameDomain info)
|
||
where
|
||
pfx = case nameType info of
|
||
NTPublicGroup -> "#"
|
||
NTContact -> "@"
|
||
|
||
instance ToField SimplexDomain where toField = toField . decodeLatin1 . strEncode
|
||
|
||
instance FromField SimplexDomain where fromField = fromTextField_ (eitherToMaybe . strDecode . encodeUtf8)
|
||
|
||
$(J.deriveJSON (enumJSON $ dropPrefix "TLD") ''SimplexTLD)
|
||
|
||
$(J.deriveJSON (enumJSON $ dropPrefix "NT") ''SimplexNameType)
|
||
|
||
$(J.deriveJSON defaultJSON ''SimplexDomain)
|
||
|
||
$(J.deriveJSON defaultJSON ''SimplexNameInfo)
|