diff --git a/.gitignore b/.gitignore index 9d27c4ccb..9550e48c0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ cabal.project.local~ *.tix .coverage +__pycache__/ diff --git a/scripts/resolver/README.md b/scripts/resolver/README.md index 13ef998d6..87143a6f3 100644 --- a/scripts/resolver/README.md +++ b/scripts/resolver/README.md @@ -200,10 +200,13 @@ then be quoted at list price while the registrar charges the premium. Configure `SNRC_CONTROLLER_` wherever `SNRC_REGISTRAR_` is set, and upgrade this service before the routers that query it. -Upgrade the resolver before the router that queries it. A resolver without this -status reports a name in its auction as plain `expired`, which reads as "free at -the ordinary price" — the price the registrar actually charges is still the -premium one, so the quote is wrong until the resolver is current. +**Upgrade this service before the routers that query it.** Routers from v22 hash +the 2LD of every query, and two things only this version does are needed to +answer them: decoding a bracket label that sits under a subname +(`sub.[].tld`, which an older resolver hashes as literal text and so +answers about a node nobody asked about), and reporting `auction` at all — an +older resolver calls a name in its auction plain `expired`, which reads as "free +at the ordinary price" while the registrar charges the premium. ### Why a name is reserved diff --git a/scripts/resolver/service/__pycache__/snrc-resolve.cpython-314.pyc b/scripts/resolver/service/__pycache__/snrc-resolve.cpython-314.pyc deleted file mode 100644 index 7b33e989e..000000000 Binary files a/scripts/resolver/service/__pycache__/snrc-resolve.cpython-314.pyc and /dev/null differ diff --git a/scripts/resolver/service/__pycache__/test_snrc_resolve.cpython-314.pyc b/scripts/resolver/service/__pycache__/test_snrc_resolve.cpython-314.pyc deleted file mode 100644 index 025a0b6e5..000000000 Binary files a/scripts/resolver/service/__pycache__/test_snrc_resolve.cpython-314.pyc and /dev/null differ diff --git a/scripts/resolver/service/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py index acb950b96..7ada4d98a 100755 --- a/scripts/resolver/service/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -66,8 +66,8 @@ Unrecognised payloads fall back to `0x`-prefixed raw hex. import hashlib import json import os -import time import sys +import time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import unquote, urlparse from urllib.request import Request, urlopen @@ -289,7 +289,8 @@ def name_status(name: str): } # nameExpires and reservedNames are keyed on uint256(keccak(label)). - # Decoded for a 2LD only, the same rule node_of applies to the node. + # Only the 2LD's label is a registry key, wherever in the name it sits, so it + # is the only one decoded - the same rule node_of applies to the node. label = labels[-2] if is_encoded_labelhash(label): token = int(label[1:-1], 16) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 4ab436cf4..b68d8a900 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -67,7 +67,7 @@ module Simplex.Messaging.Agent deleteConnShortLink, getConnShortLink, resolveSimplexName, - simplexNameAvailability, + getSimplexNameAvailability, getConnLinkPrivKey, deleteLocalInvShortLink, changeConnectionUser, @@ -465,9 +465,9 @@ resolveSimplexName :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDoma resolveSimplexName c nm userId domain = withAgentEnv c $ resolveSimplexName' c nm userId domain {-# INLINE resolveSimplexName #-} -simplexNameAvailability :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AE NameAvailability -simplexNameAvailability c nm userId domain = withAgentEnv c $ simplexNameAvailability' c nm userId domain -{-# INLINE simplexNameAvailability #-} +getSimplexNameAvailability :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AE NameAvailability +getSimplexNameAvailability c nm userId domain = withAgentEnv c $ getSimplexNameAvailability' c nm userId domain +{-# INLINE getSimplexNameAvailability #-} getConnLinkPrivKey :: AgentClient -> ConnId -> AE (Maybe C.PrivateKeyEd25519) getConnLinkPrivKey c = withAgentEnv c . getConnLinkPrivKey' c @@ -1279,10 +1279,10 @@ resolveSimplexName' c nm userId domain = do resolverSrv <- getNextNameServer c userId resolveName c nm userId resolverSrv domain -simplexNameAvailability' :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AM NameAvailability -simplexNameAvailability' c nm userId domain = do +getSimplexNameAvailability' :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AM NameAvailability +getSimplexNameAvailability' c nm userId domain = do resolverSrv <- getNextNameServer c userId - nameAvailability c nm userId resolverSrv domain + getNameAvailability c nm userId resolverSrv domain changeConnectionUser' :: AgentClient -> UserId -> ConnId -> UserId -> AM () changeConnectionUser' c oldUserId connId newUserId = do diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 7d51ed3e3..4557541af 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -69,7 +69,7 @@ module Simplex.Messaging.Agent.Client secureGetQueueLink, getQueueLink, resolveName, - nameAvailability, + getNameAvailability, getNextNameServer, enableQueueNotifications, EnableQueueNtfReq (..), @@ -2004,8 +2004,8 @@ resolveName c nm userId server domain = -- | Ask whether a name can be registered, by the same proxy-preferred path as -- `resolveName`. -nameAvailability :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> SimplexDomain -> AM NameAvailability -nameAvailability c nm userId server domain = +getNameAvailability :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> SimplexDomain -> AM NameAvailability +getNameAvailability c nm userId server domain = snd <$> sendOrProxySMPCommand c nm userId server "" "NAVL" NoEntity availViaProxy availDirectly where availViaProxy smp proxySess = proxyNameAvailability smp nm proxySess domain diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 0e7b6ba2f..8964d8e0d 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -110,7 +110,7 @@ import Simplex.Messaging.Server.Env.STM as Env import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Server.MsgStore import Simplex.Messaging.Server.MsgStore.Journal (JournalMsgStore, JournalQueue (..), getJournalQueueMessages) -import Simplex.Messaging.Server.Names (NamesEnv, closeNamesEnv, nameAvailability, resolveName) +import Simplex.Messaging.Server.Names (NamesEnv, closeNamesEnv, getNameAvailability, resolveName) import Simplex.Messaging.Server.MsgStore.STM import Simplex.Messaging.Server.MsgStore.Types import Simplex.Messaging.Server.NtfStore @@ -1499,7 +1499,7 @@ client nameAvailMsg nenv d = do st <- asks (rslvStats . serverStats) (selector, msg) <- - liftIO (nameAvailability nenv d) <&> \case + liftIO (getNameAvailability nenv d) <&> \case Right a -> (rslvSucc, NAVAIL a) Left e -> (rslvResolverErrs, ERR $ NAME e) incStat (selector st) $> msg diff --git a/src/Simplex/Messaging/Server/Names.hs b/src/Simplex/Messaging/Server/Names.hs index 6e4e90540..50d106d17 100644 --- a/src/Simplex/Messaging/Server/Names.hs +++ b/src/Simplex/Messaging/Server/Names.hs @@ -11,7 +11,7 @@ module Simplex.Messaging.Server.Names newNamesEnv, closeNamesEnv, pingEndpoint, - nameAvailability, + getNameAvailability, resolveName, ) where @@ -75,8 +75,8 @@ resolveName env d = do -- | Whether a name can be registered. Same timeout and failure handling as -- 'resolveName', which is the other question this server asks the resolver. -nameAvailability :: NamesEnv -> SimplexDomain -> IO (Either NameErrorType NameAvailability) -nameAvailability env d = do +getNameAvailability :: NamesEnv -> SimplexDomain -> IO (Either NameErrorType NameAvailability) +getNameAvailability env d = do r <- E.try (timeout (resolverTimeoutMs (config env) * 1000) (fetchAvail env d)) case r of Right result -> pure (fromMaybe (Left (RESOLVER "timeout")) result) @@ -114,7 +114,9 @@ mapAvailability NameStatusResp {nsStatus, nsExpires, nsGraceEnds, nsAuctionEnds, "registered" -> Right $ NATaken nsExpires -- registered, but its records point nowhere "noResolver" -> Right $ NATaken nsExpires - s -> Left (RESOLVER s) + -- the resolver's own word for what it could not do, bounded because it is + -- its text, not ours, and it travels to the client inside ERR + s -> Left (RESOLVER (T.take 32 s)) where -- A lapsed name missing the deadline or price that its status carries: -- withholding it is safer than quoting the ordinary price, but its expiry is diff --git a/src/Simplex/Messaging/Server/Prometheus.hs b/src/Simplex/Messaging/Server/Prometheus.hs index 575f699c6..421e0e5d9 100644 --- a/src/Simplex/Messaging/Server/Prometheus.hs +++ b/src/Simplex/Messaging/Server/Prometheus.hs @@ -465,11 +465,11 @@ prometheusMetrics sm rtm ts = in "# Names\n\ \# -----\n\ \\n\ - \# HELP simplex_smp_names_reqs Total RSLV requests forwarded to this server.\n\ + \# HELP simplex_smp_names_reqs Total RSLV and NAVL requests forwarded to this server.\n\ \# TYPE simplex_smp_names_reqs counter\n\ \simplex_smp_names_reqs " <> mshow _rslvReqs <> "\n# rslvReqs\n\ \\n\ - \# HELP simplex_smp_names_success NameRecord successfully resolved and returned.\n\ + \# HELP simplex_smp_names_success NameRecord resolved, or availability answered.\n\ \# TYPE simplex_smp_names_success counter\n\ \simplex_smp_names_success " <> mshow _rslvSucc <> "\n# rslvSucc\n\ \\n\ @@ -481,7 +481,7 @@ prometheusMetrics sm rtm ts = \# TYPE simplex_smp_names_resolver_errs counter\n\ \simplex_smp_names_resolver_errs " <> mshow _rslvResolverErrs <> "\n# rslvResolverErrs\n\ \\n\ - \# HELP simplex_smp_names_disabled RSLV requests rejected because no resolver is configured (names role off).\n\ + \# HELP simplex_smp_names_disabled RSLV and NAVL requests rejected because no resolver is configured (names role off).\n\ \# TYPE simplex_smp_names_disabled counter\n\ \simplex_smp_names_disabled " <> mshow _rslvDisabled <> "\n# rslvDisabled\n\ \\n" diff --git a/src/Simplex/Messaging/SimplexName.hs b/src/Simplex/Messaging/SimplexName.hs index d07b74205..b309b661c 100644 --- a/src/Simplex/Messaging/SimplexName.hs +++ b/src/Simplex/Messaging/SimplexName.hs @@ -97,7 +97,7 @@ hashedDomain d@SimplexDomain {nameTLD, domain} | nameTLD == TLDWeb || isLabelHash domain = d | otherwise = d {domain = "[" <> labelHash <> "]"} where - labelHash = decodeLatin1 $ BAE.convertToBase BAE.Base16 (hash (encodeUtf8 domain) :: Digest Keccak_256) + labelHash = decodeLatin1 $ BAE.convertToBase BAE.Base16 (hash (encodeUtf8 (T.toLower domain)) :: Digest Keccak_256) -- | Cap the name at 253 bytes (DNS full-domain limit) boundedNonSpace :: A.Parser ByteString diff --git a/tests/AgentTests/ResolveNameTests.hs b/tests/AgentTests/ResolveNameTests.hs index f55faf75f..04ba926e2 100644 --- a/tests/AgentTests/ResolveNameTests.hs +++ b/tests/AgentTests/ResolveNameTests.hs @@ -23,7 +23,7 @@ import qualified NamesResolverServer as NRS import SMPAgentClient import SMPClient import SMPNamesTests (testNameRecord) -import Simplex.Messaging.Agent (resolveSimplexName) +import Simplex.Messaging.Agent (resolveSimplexName, getSimplexNameAvailability) import Simplex.Messaging.Agent.Client (AgentClient) import Simplex.Messaging.Agent.Env.SQLite (InitialAgentServers (..), ServerCfg, ServerRoles (..), presetServerCfg) import Simplex.Messaging.Agent.Protocol (AgentErrorType (..)) @@ -86,6 +86,34 @@ resolveNameTests = do it "surfaces as SMP host (NAME (RESOLVER ..))" testBackendError describe "success path" $ it "returns NameRecord" testDirectSuccess + describe "name availability" $ do + it "answers through the agent's own server selection" testAvailSuccess + it "answers NAME NO_RESOLVER when the chosen server has none" testAvailNoResolver + it "fails agent-side with NO_NAME_SERVERS when no server has the names role" testAvailNoNameServers + +testAvailSuccess :: HasCallStack => IO () +testAvailSuccess = + withDirectResolver (status404, "{\"error\":\"unregistered\"}") $ \c -> do + r <- runExceptT $ getSimplexNameAvailability c NRMInteractive 1 (SimplexDomain TLDSimplex "alice" []) + case r of + Right a -> a `shouldBe` SMP.NAVailable + _ -> expectationFailure $ "expected Right NAVailable, got: " <> show r + +testAvailNoResolver :: HasCallStack => IO () +testAvailNoResolver = + withNoResolver $ \c -> do + r <- runExceptT $ getSimplexNameAvailability c NRMInteractive 1 (SimplexDomain TLDSimplex "alice" []) + case r of + Left (SMP _ (SMP.NAME SMP.NO_RESOLVER)) -> pure () + _ -> expectationFailure $ "expected Left (SMP _ (NAME NO_RESOLVER)), got: " <> show r + +testAvailNoNameServers :: HasCallStack => IO () +testAvailNoNameServers = + withNoNameServers $ \c -> do + r <- runExceptT $ getSimplexNameAvailability c NRMInteractive 1 (SimplexDomain TLDSimplex "alice" []) + case r of + Left NO_NAME_SERVERS -> pure () + _ -> expectationFailure $ "expected Left NO_NAME_SERVERS, got: " <> show r testDirectNotFound :: HasCallStack => IO () testDirectNotFound = diff --git a/tests/SMPNamesTests.hs b/tests/SMPNamesTests.hs index 5ddc2e49d..13eb3a294 100644 --- a/tests/SMPNamesTests.hs +++ b/tests/SMPNamesTests.hs @@ -22,7 +22,7 @@ import Simplex.Messaging.Server.Main (validateUrl) import Simplex.Messaging.Server.Names ( NamesConfig (..), RpcAuth (..), - nameAvailability, + getNameAvailability, newNamesEnv, pingEndpoint, resolveName, @@ -149,12 +149,14 @@ availabilitySpec = do refuses status400 "{\"error\":\"tldNotConfigured\"}" (RESOLVER "tldNotConfigured") it "a TLD with no registrar, so status could not be read, is a resolver error" $ refuses status200 "{\"status\":\"unknown\",\"expires\":null}" (RESOLVER "unknown") + it "a status longer than the error carries is bounded, not passed through" $ + refuses status502 (jsonBody ("{\"error\":\"" <> replicate 400 'e' <> "\"}")) (RESOLVER (T.replicate 32 "e")) it "a body that is not the resolver's JSON is never NOT_FOUND" $ refuses status404 "gateway" (RESOLVER "HTTP 404") it "a body past the configured cap is a resolver error" $ withResolverServer (resolveResp status200 (jsonBody ("{\"status\":\"registered\",\"pad\":\"" <> replicate 400 'x' <> "\"}"))) $ \port _ -> do env <- newNamesEnv (testNamesConfig port) {resolverMaxResponseBytes = 200} - nameAvailability env navlDomain `shouldReturn` Left (RESOLVER "response too large") + getNameAvailability env navlDomain `shouldReturn` Left (RESOLVER "response too large") it "every answer survives the wire" $ mapM_ (\a -> smpDecode (smpEncode a) `shouldBe` Right a) @@ -177,7 +179,7 @@ availabilitySpec = do asks_ st body expected = withResolverServer (resolveResp st body) $ \port _ -> do env <- newNamesEnv (testNamesConfig port) - nameAvailability env navlDomain `shouldReturn` expected + getNameAvailability env navlDomain `shouldReturn` expected navlDomain = SimplexDomain {nameTLD = TLDSimplex, domain = "alice", subDomain = []} parseNameSpec :: Spec