mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-17 01:35:12 +00:00
304 lines
14 KiB
Haskell
304 lines
14 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE PatternSynonyms #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
|
|
module RSLVTests (rslvTests) where
|
|
|
|
import Control.Monad.Trans.Except (ExceptT, runExceptT)
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.ByteString.Char8 as B
|
|
import qualified Data.ByteString.Lazy as LB
|
|
import Data.IORef (IORef, readIORef)
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
|
import Data.Text (Text)
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
import Data.Time.Clock (getCurrentTime)
|
|
import Network.HTTP.Types (Status, status200, status404, status410, status502)
|
|
import NamesResolverServer (memCfg, memCfg2, memProxyCfg, withNames)
|
|
import qualified NamesResolverServer as NRS
|
|
import SMPClient
|
|
import Simplex.Messaging.Client
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Encoding.String (strDecode)
|
|
import SMPNamesTests (testNameRecord)
|
|
import Simplex.Messaging.Protocol
|
|
( BrokerMsg (..),
|
|
Cmd (..),
|
|
Command (..),
|
|
CorrId (..),
|
|
ErrorType (..),
|
|
NameAvailability (..),
|
|
NameErrorType (..),
|
|
NameReservedReason (..),
|
|
SParty (..),
|
|
Transmission,
|
|
TransmissionForAuth (..),
|
|
encodeTransmissionForAuth,
|
|
pattern SMPServer,
|
|
tGetClient,
|
|
tPut,
|
|
)
|
|
import qualified Simplex.Messaging.Protocol as SMP
|
|
import Simplex.Messaging.SimplexName (SimplexDomain)
|
|
import Simplex.Messaging.Transport
|
|
import Simplex.Messaging.Version (mkVersionRange)
|
|
import Test.Hspec hiding (fit, it)
|
|
import Util (it)
|
|
|
|
domain :: Text -> SimplexDomain
|
|
domain = either error id . strDecode . encodeUtf8
|
|
|
|
withResolverServer :: (Status, LB.ByteString) -> IO a -> IO a
|
|
withResolverServer (st, body) runTest =
|
|
NRS.withResolverServer (NRS.resolveResp st body) $ \port _ ->
|
|
withSmpServerConfigOn (transport @TLS) (withNames port memCfg) testPort (const runTest)
|
|
|
|
withResolverServerReqs :: (Status, LB.ByteString) -> (IORef [[Text]] -> IO a) -> IO a
|
|
withResolverServerReqs (st, body) runTest =
|
|
NRS.withResolverServer (NRS.resolveResp st body) $ \port reqs ->
|
|
withSmpServerConfigOn (transport @TLS) (withNames port memCfg) testPort (const (runTest reqs))
|
|
|
|
withProxyAndResolver :: (Status, LB.ByteString) -> IO a -> IO a
|
|
withProxyAndResolver (st, body) runTest =
|
|
NRS.withResolverServer (NRS.resolveResp st body) $ \port _ ->
|
|
withSmpServerConfigOn (transport @TLS) memProxyCfg testPort $ \_ ->
|
|
withSmpServerConfigOn (transport @TLS) (withNames port memCfg2) testPort2 (const runTest)
|
|
|
|
sendRslv :: Transport c => THandleSMP c 'TClient -> B.ByteString -> SimplexDomain -> IO (Transmission (Either ErrorType BrokerMsg))
|
|
sendRslv h@THandle {params} corrId d = do
|
|
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, NoEntity, Cmd SResolver (RSLV d))
|
|
[Right ()] <- tPut h (Right (Nothing, tToSend) :| [])
|
|
r :| _ <- tGetClient h
|
|
pure r
|
|
|
|
sendNavl :: Transport c => THandleSMP c 'TClient -> B.ByteString -> SimplexDomain -> IO (Transmission (Either ErrorType BrokerMsg))
|
|
sendNavl h@THandle {params} corrId d = do
|
|
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, NoEntity, Cmd SResolver (NAVL d))
|
|
[Right ()] <- tPut h (Right (Nothing, tToSend) :| [])
|
|
r :| _ <- tGetClient h
|
|
pure r
|
|
|
|
rslvTests :: Spec
|
|
rslvTests = do
|
|
describe "RSLV direct (non-forwarded)" $ do
|
|
it "resolver replies 404 -> NAME NOT_FOUND (reached, not CMD PROHIBITED)" testRslvBackendNotFound
|
|
it "resolver replies 410 -> NAME NOT_FOUND (a lapsed name, not a resolver failure)" testRslvBackendGone
|
|
it "resolver replies 502 -> NAME (RESOLVER ..)" testRslvBackendHttpErr
|
|
it "no names config -> NAME NO_RESOLVER" testRslvDisabled
|
|
it "refuses to send RSLV on a session below namesSMPVersion" testRslvVersion
|
|
describe "RSLV forwarded (PFWD)" $ do
|
|
it "PFWD-wrapped RSLV reaches resolver via proxy (PCEProtocolError (NAME NOT_FOUND))" testRslvForwarded
|
|
it "PFWD-wrapped RSLV success returns RNAME (record JSON frames over the proxy)" testRslvForwardedSuccess
|
|
describe "RSLV success path (RNAME response)" $ do
|
|
it "returns RNAME with NameRecord" testRslvSuccess
|
|
describe "NAVL (availability)" $ do
|
|
it "a name nobody has taken comes back AVAILABLE" testNavlAvailable
|
|
it "a lapsed name in its auction comes back with the premium and deadline" testNavlAuction
|
|
it "a reserved name comes back with the reason it is held back" testNavlReserved
|
|
it "no names config -> NAME NO_RESOLVER" testNavlDisabled
|
|
it "refuses to send NAVL on a session below nameAvailSMPVersion" testNavlVersion
|
|
it "PFWD-wrapped NAVL reaches the resolver via the proxy" testNavlForwarded
|
|
describe "hashed lookups" $ do
|
|
it "RSLV sends the second-level label as its hash, never the name" testRslvSendsTheHash
|
|
it "NAVL sends the second-level label as its hash, never the name" testNavlSendsTheHash
|
|
it "a subname keeps its own labels as text, hashing only the 2LD" testSubnameKeepsItsLabels
|
|
|
|
testRslvBackendNotFound :: IO ()
|
|
testRslvBackendNotFound =
|
|
withResolverServer (status404, "{}") $
|
|
testSMPClient @TLS $ \h -> do
|
|
(corrId, _entId, resp) <- sendRslv h "rs01" (domain "ghost.simplex")
|
|
corrId `shouldBe` CorrId "rs01"
|
|
resp `shouldBe` Right (ERR (NAME NOT_FOUND))
|
|
|
|
testRslvBackendGone :: IO ()
|
|
testRslvBackendGone =
|
|
withResolverServer (status410, "{}") $
|
|
testSMPClient @TLS $ \h -> do
|
|
(_, _, resp) <- sendRslv h "rs08" (domain "lapsed.simplex")
|
|
resp `shouldBe` Right (ERR (NAME NOT_FOUND))
|
|
|
|
testRslvBackendHttpErr :: IO ()
|
|
testRslvBackendHttpErr =
|
|
withResolverServer (status502, "{}") $
|
|
testSMPClient @TLS $ \h -> do
|
|
(_, _, resp) <- sendRslv h "rs05" (domain "alice.simplex")
|
|
resp `shouldBe` Right (ERR (NAME (RESOLVER "HTTP 502")))
|
|
|
|
testRslvDisabled :: IO ()
|
|
testRslvDisabled =
|
|
withSmpServerConfigOn (transport @TLS) memCfg testPort $ const $
|
|
testSMPClient @TLS $ \h -> do
|
|
(_, _, resp) <- sendRslv h "rs06" (domain "alice.simplex")
|
|
resp `shouldBe` Right (ERR (NAME NO_RESOLVER))
|
|
|
|
testRslvVersion :: IO ()
|
|
testRslvVersion =
|
|
withResolverServer (status200, J.encode testNameRecord) $ do
|
|
g <- C.newRandom
|
|
ts <- getCurrentTime
|
|
let srv = SMPServer testHost testPort testKeyHash
|
|
oldCfg = defaultSMPClientConfig {serverVRange = mkVersionRange minServerSMPRelayVersion rcvServiceSMPVersion}
|
|
pcE <- getProtocolClient g NRMInteractive (1, srv, Nothing) oldCfg [] Nothing ts (\_ -> pure ())
|
|
pc <- either (fail . show) pure pcE
|
|
r <- runExceptT (directResolveName pc NRMInteractive (domain "alice.simplex"))
|
|
case r of
|
|
Left (PCETransportError TEVersion) -> pure ()
|
|
_ -> expectationFailure $ "expected Left (PCETransportError TEVersion), got: " <> show r
|
|
|
|
forwardedResolveAlice :: IO (Either SMPClientError (Either ProxyClientError SMP.NameRecord))
|
|
forwardedResolveAlice = do
|
|
g <- C.newRandom
|
|
ts <- getCurrentTime
|
|
let proxyServ = SMPServer testHost testPort testKeyHash
|
|
relayServ = SMPServer testHost2 testPort2 testKeyHash
|
|
cfg' = defaultSMPClientConfig {serverVRange = mkVersionRange minServerSMPRelayVersion currentClientSMPRelayVersion}
|
|
pcE <- getProtocolClient g NRMInteractive (1, proxyServ, Nothing) cfg' [] Nothing ts (\_ -> pure ())
|
|
pc <- either (fail . show) pure pcE
|
|
sess <- runExceptT' (connectSMPProxiedRelay pc NRMInteractive relayServ Nothing)
|
|
runExceptT (proxyResolveName pc NRMInteractive sess (domain "alice.simplex"))
|
|
|
|
testRslvForwarded :: IO ()
|
|
testRslvForwarded =
|
|
withProxyAndResolver (status404, "{}") $
|
|
forwardedResolveAlice >>= \r -> case r of
|
|
Left (PCEProtocolError (SMP.NAME SMP.NOT_FOUND)) -> pure ()
|
|
_ -> expectationFailure $ "expected Left (PCEProtocolError (NAME NOT_FOUND)), got: " <> show r
|
|
|
|
testRslvForwardedSuccess :: IO ()
|
|
testRslvForwardedSuccess =
|
|
withProxyAndResolver (status200, J.encode testNameRecord) $
|
|
forwardedResolveAlice >>= \r -> case r of
|
|
Right (Right nr) -> nr `shouldBe` testNameRecord
|
|
_ -> expectationFailure $ "expected Right (Right NameRecord), got: " <> show r
|
|
|
|
testRslvSuccess :: IO ()
|
|
testRslvSuccess =
|
|
withResolverServer (status200, J.encode testNameRecord) $
|
|
testSMPClient @TLS $ \h -> do
|
|
(corrId, _entId, resp) <- sendRslv h "rs07" (domain "alice.simplex")
|
|
corrId `shouldBe` CorrId "rs07"
|
|
case resp of
|
|
Right (RNAME nr) -> nr `shouldBe` testNameRecord
|
|
_ -> expectationFailure $ "expected Right (RNAME ..), got: " <> show resp
|
|
|
|
testNavlAvailable :: IO ()
|
|
testNavlAvailable =
|
|
withResolverServer (status404, "{\"error\":\"unregistered\"}") $
|
|
testSMPClient @TLS $ \h -> do
|
|
(corrId, _entId, resp) <- sendNavl h "na01" (domain "ghost.simplex")
|
|
corrId `shouldBe` CorrId "na01"
|
|
resp `shouldBe` Right (NAVAIL NAVailable)
|
|
|
|
testNavlAuction :: IO ()
|
|
testNavlAuction =
|
|
withResolverServer (status410, auctionBody) $
|
|
testSMPClient @TLS $ \h -> do
|
|
(_, _, resp) <- sendNavl h "na02" (domain "lapsed.simplex")
|
|
resp `shouldBe` Right (NAVAIL (NAAuction "99999952316384526016153087" 1798191621))
|
|
|
|
testNavlReserved :: IO ()
|
|
testNavlReserved =
|
|
withResolverServer (status404, "{\"error\":\"reserved\",\"reasonCode\":\"trademark\"}") $
|
|
testSMPClient @TLS $ \h -> do
|
|
(_, _, resp) <- sendNavl h "na03" (domain "acme.simplex")
|
|
resp `shouldBe` Right (NAVAIL (NAReserved NRTrademark))
|
|
|
|
testNavlDisabled :: IO ()
|
|
testNavlDisabled =
|
|
withSmpServerConfigOn (transport @TLS) memCfg testPort $ const $
|
|
testSMPClient @TLS $ \h -> do
|
|
(_, _, resp) <- sendNavl h "na04" (domain "alice.simplex")
|
|
resp `shouldBe` Right (ERR (NAME NO_RESOLVER))
|
|
|
|
testNavlVersion :: IO ()
|
|
testNavlVersion =
|
|
withResolverServer (status404, "{\"error\":\"unregistered\"}") $ do
|
|
g <- C.newRandom
|
|
ts <- getCurrentTime
|
|
let srv = SMPServer testHost testPort testKeyHash
|
|
-- the version just below the gate: a lower ceiling would also pass for
|
|
-- a gate at 20 or 21 and prove nothing about v22
|
|
oldCfg = defaultSMPClientConfig {serverVRange = mkVersionRange minServerSMPRelayVersion serverInfoSMPVersion}
|
|
pcE <- getProtocolClient g NRMInteractive (1, srv, Nothing) oldCfg [] Nothing ts (\_ -> pure ())
|
|
pc <- either (fail . show) pure pcE
|
|
r <- runExceptT (directNameAvailability pc NRMInteractive (domain "alice.simplex"))
|
|
case r of
|
|
Left (PCETransportError TEVersion) -> pure ()
|
|
_ -> expectationFailure $ "expected Left (PCETransportError TEVersion), got: " <> show r
|
|
|
|
testNavlForwarded :: IO ()
|
|
testNavlForwarded =
|
|
withProxyAndResolver (status410, auctionBody) $ do
|
|
g <- C.newRandom
|
|
ts <- getCurrentTime
|
|
let proxyServ = SMPServer testHost testPort testKeyHash
|
|
relayServ = SMPServer testHost2 testPort2 testKeyHash
|
|
cfg' = defaultSMPClientConfig {serverVRange = mkVersionRange minServerSMPRelayVersion currentClientSMPRelayVersion}
|
|
pcE <- getProtocolClient g NRMInteractive (1, proxyServ, Nothing) cfg' [] Nothing ts (\_ -> pure ())
|
|
pc <- either (fail . show) pure pcE
|
|
sess <- runExceptT' (connectSMPProxiedRelay pc NRMInteractive relayServ Nothing)
|
|
r <- runExceptT (proxyNameAvailability pc NRMInteractive sess (domain "lapsed.simplex"))
|
|
case r of
|
|
Right (Right a) -> a `shouldBe` NAAuction "99999952316384526016153087" 1798191621
|
|
_ -> expectationFailure $ "expected Right (Right NAAuction ..), got: " <> show r
|
|
|
|
-- a name one day past its grace period, priced by the .testing auction curve
|
|
auctionBody :: LB.ByteString
|
|
auctionBody = "{\"error\":\"auction\",\"premium\":\"99999952316384526016153087\",\"auctionEnds\":1798191621}"
|
|
|
|
-- keccak-256("alice"), the registry key
|
|
aliceHash :: Text
|
|
aliceHash = "[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]"
|
|
|
|
-- | A current client must never put a registrable name on the wire.
|
|
resolvePaths :: IORef [[Text]] -> IO [[Text]]
|
|
resolvePaths reqs = filter isResolve <$> readIORef reqs
|
|
where
|
|
isResolve = \case ("resolve" : _) -> True; _ -> False
|
|
|
|
currentClient :: IO SMPClient
|
|
currentClient = do
|
|
g <- C.newRandom
|
|
ts <- getCurrentTime
|
|
let srv = SMPServer testHost testPort testKeyHash
|
|
pcE <- getProtocolClient g NRMInteractive (1, srv, Nothing) defaultSMPClientConfig [] Nothing ts (\_ -> pure ())
|
|
either (fail . show) pure pcE
|
|
|
|
testRslvSendsTheHash :: IO ()
|
|
testRslvSendsTheHash =
|
|
withResolverServerReqs (status200, J.encode echoed) $ \reqs -> do
|
|
pc <- currentClient
|
|
nr <- runExceptT' (directResolveName pc NRMInteractive (domain "alice.simplex"))
|
|
resolvePaths reqs `shouldReturn` [["resolve", aliceHash <> ".simplex"]]
|
|
-- the record names what the caller asked for
|
|
SMP.nrName nr `shouldBe` "alice.simplex"
|
|
where
|
|
-- the resolver echoes what it was asked about, which is the hash
|
|
echoed = testNameRecord {SMP.nrName = aliceHash <> ".simplex"}
|
|
|
|
testNavlSendsTheHash :: IO ()
|
|
testNavlSendsTheHash =
|
|
withResolverServerReqs (status404, "{\"error\":\"unregistered\"}") $ \reqs -> do
|
|
pc <- currentClient
|
|
a <- runExceptT' (directNameAvailability pc NRMInteractive (domain "alice.simplex"))
|
|
a `shouldBe` NAVailable
|
|
resolvePaths reqs `shouldReturn` [["resolve", aliceHash <> ".simplex"]]
|
|
|
|
testSubnameKeepsItsLabels :: IO ()
|
|
testSubnameKeepsItsLabels =
|
|
withResolverServerReqs (status404, "{\"error\":\"unregistered\"}") $ \reqs -> do
|
|
pc <- currentClient
|
|
_ <- runExceptT' (directNameAvailability pc NRMInteractive (domain "x.alice.simplex"))
|
|
resolvePaths reqs `shouldReturn` [["resolve", "x." <> aliceHash <> ".simplex"]]
|
|
|
|
runExceptT' :: Show e => ExceptT e IO a -> IO a
|
|
runExceptT' a = runExceptT a >>= either (fail . show) pure
|