diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 3b68667e4..3a0e13157 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -2250,9 +2250,9 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, | otherwise -> ignored _ -> ignored ignored = pure "END from disconnected client - ignored" - _ -> do + r -> do logServer "<--" c srv rId $ "unexpected: " <> bshow cmd - notify . ERR $ BROKER (B.unpack $ strEncode srv) $ if isResponse then TIMEOUT else UNEXPECTED + notify . ERR $ BROKER (B.unpack $ strEncode srv) $ if isResponse then TIMEOUT else UNEXPECTED $ take 32 $ show r where notify :: forall e m. MonadIO m => AEntityI e => ACommand 'Agent e -> m () notify = atomically . notify' diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index c29e35499..35015af5a 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -1032,8 +1032,15 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) sendViaProxy destSess = withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do - liftClient SMP (clientServer smp) $ proxySMPMessage smp proxySess spKey_ senderId msgFlags msg - pure . Just $ protocolClientServer' smp + liftClient SMP (clientServer smp) (proxySMPMessage smp proxySess spKey_ senderId msgFlags msg) >>= \case + Right () -> pure . Just $ protocolClientServer' smp + Left proxyErr -> + throwError + PROXY + { proxyServer = protocolClientServer smp, + relayServer = B.unpack $ strEncode destSrv, + proxyErr + } sendDirectly tSess = withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg @@ -1066,7 +1073,7 @@ protocolClientError :: (Show err, Encoding err) => (err -> AgentErrorType) -> Ho protocolClientError protocolError_ host = \case PCEProtocolError e -> protocolError_ e PCEResponseError e -> BROKER host $ RESPONSE $ B.unpack $ smpEncode e - PCEUnexpectedResponse _ -> BROKER host UNEXPECTED + PCEUnexpectedResponse r -> BROKER host $ UNEXPECTED $ take 32 $ show r PCEResponseTimeout -> BROKER host TIMEOUT PCENetworkError -> BROKER host NETWORK PCEIncompatibleHost -> BROKER host HOST @@ -1263,15 +1270,23 @@ processSubResult c rq r = do temporaryAgentError :: AgentErrorType -> Bool temporaryAgentError = \case - BROKER _ NETWORK -> True - BROKER _ TIMEOUT -> True + BROKER _ e -> tempBrokerError e + SMP (SMP.PROXY (SMP.BROKER e)) -> tempBrokerError e + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> tempBrokerError e INACTIVE -> True _ -> False + where + tempBrokerError = \case + NETWORK -> True + TIMEOUT -> True + _ -> False {-# INLINE temporaryAgentError #-} temporaryOrHostError :: AgentErrorType -> Bool temporaryOrHostError = \case BROKER _ HOST -> True + SMP (SMP.PROXY (SMP.BROKER HOST)) -> True + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER HOST))) -> True e -> temporaryAgentError e {-# INLINE temporaryOrHostError #-} diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 895a8ddc1..61ab2400b 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -189,6 +189,7 @@ import Simplex.FileTransfer.Description import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Transport (XFTPErrorType) import Simplex.Messaging.Agent.QueryString +import Simplex.Messaging.Client (ProxyClientError) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet ( InitialKeys (..), @@ -206,6 +207,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol ( AProtocolType, + BrokerErrorType (..), EntityId, ErrorType, MsgBody, @@ -233,7 +235,7 @@ import Simplex.Messaging.Protocol ) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme -import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTransportError, transportErrorP) +import Simplex.Messaging.Transport (Transport (..), serializeTransportError, transportErrorP) import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -1474,6 +1476,8 @@ data AgentErrorType NTF {ntfErr :: ErrorType} | -- | XFTP protocol errors forwarded to agent clients XFTP {xftpErr :: XFTPErrorType} + | -- | SMP proxy errors + PROXY {proxyServer :: String, relayServer :: String, proxyErr :: ProxyClientError} | -- | XRCP protocol errors forwarded to agent clients RCP {rcpErr :: RCErrorType} | -- | SMP server errors @@ -1516,22 +1520,6 @@ data ConnectionErrorType NOT_AVAILABLE deriving (Eq, Read, Show, Exception) --- | SMP server errors. -data BrokerErrorType - = -- | invalid server response (failed to parse) - RESPONSE {smpErr :: String} - | -- | unexpected response - UNEXPECTED - | -- | network error - NETWORK - | -- | no compatible server host (e.g. onion when public is required, or vice versa) - HOST - | -- | handshake or other transport error - TRANSPORT {transportErr :: TransportError} - | -- | command response timeout - TIMEOUT - deriving (Eq, Read, Show, Exception) - -- | Errors of another SMP agent. data SMPAgentError = -- | client or agent message that failed to parse @@ -1587,8 +1575,10 @@ instance StrEncoding AgentErrorType where <|> "SMP " *> (SMP <$> strP) <|> "NTF " *> (NTF <$> strP) <|> "XFTP " *> (XFTP <$> strP) + <|> "PROXY " *> (PROXY <$> textP <* A.space <*> textP <*> _strP) <|> "RCP " *> (RCP <$> strP) <|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP)) + <|> "BROKER " *> (BROKER <$> textP <* " UNEXPECTED " <*> (UNEXPECTED <$> textP)) <|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP)) <|> "BROKER " *> (BROKER <$> textP <* A.space <*> parseRead1) <|> "AGENT CRYPTO " *> (AGENT . A_CRYPTO <$> parseRead A.takeByteString) @@ -1605,8 +1595,10 @@ instance StrEncoding AgentErrorType where SMP e -> "SMP " <> strEncode e NTF e -> "NTF " <> strEncode e XFTP e -> "XFTP " <> strEncode e + PROXY pxy srv e -> B.unwords ["PROXY", text pxy, text srv, strEncode e] RCP e -> "RCP " <> strEncode e BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e + BROKER srv (UNEXPECTED e) -> "BROKER " <> text srv <> " UNEXPECTED " <> text e BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e BROKER srv e -> "BROKER " <> text srv <> " " <> bshow e AGENT (A_CRYPTO e) -> "AGENT CRYPTO " <> bshow e @@ -1977,8 +1969,6 @@ $(J.deriveJSON (sumTypeJSON id) ''CommandErrorType) $(J.deriveJSON (sumTypeJSON id) ''ConnectionErrorType) -$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType) - $(J.deriveJSON (sumTypeJSON id) ''AgentCryptoError) $(J.deriveJSON (sumTypeJSON id) ''SMPAgentError) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 2936e3841..8a1349089 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -64,6 +64,7 @@ module Simplex.Messaging.Client -- * Supporting types and client configuration ProtocolClientError (..), SMPClientError, + ProxyClientError (..), ProtocolClientConfig (..), NetworkConfig (..), TransportSessionMode (..), @@ -97,6 +98,7 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J +import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) @@ -534,16 +536,17 @@ temporaryClientError = \case _ -> False {-# INLINE temporaryClientError #-} +-- converts error of client running on proxy to the error sent to client connected to proxy smpProxyError :: SMPClientError -> ErrorType smpProxyError = \case - PCEProtocolError et -> PROXY (PROTOCOL et) - PCEResponseError et -> PROXY (RESPONSE et) - PCEUnexpectedResponse bs -> PROXY (UNEXPECTED $ B.unpack $ B.take 32 bs) - PCEResponseTimeout -> PROXY TIMEOUT - PCENetworkError -> PROXY NETWORK - PCEIncompatibleHost -> PROXY BAD_HOST - PCETransportError t -> PROXY (TRANSPORT t) - PCECryptoError _ -> INTERNAL + PCEProtocolError e -> PROXY $ PROTOCOL e + PCEResponseError e -> PROXY $ BROKER $ RESPONSE $ B.unpack $ strEncode e + PCEUnexpectedResponse s -> PROXY $ BROKER $ UNEXPECTED $ B.unpack $ B.take 32 s + PCEResponseTimeout -> PROXY $ BROKER TIMEOUT + PCENetworkError -> PROXY $ BROKER NETWORK + PCEIncompatibleHost -> PROXY $ BROKER HOST + PCETransportError t -> PROXY $ BROKER $ TRANSPORT t + PCECryptoError _ -> CRYPTO PCEIOError _ -> INTERNAL -- | Create a new SMP queue. @@ -699,21 +702,18 @@ deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO deleteSMPQueues = okSMPCommands DEL {-# INLINE deleteSMPQueues #-} --- TODO picture - -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case - PKEY sId vr (chain, key) -> do + PKEY sId vr (chain, key) -> case supportedClientSMPRelayVRange `compatibleVersion` vr of - Nothing -> throwE PCEIncompatibleHost -- TODO different error - Just (Compatible v) -> liftEitherWith x509Error $ ProxiedRelay sId v <$> validateRelay chain key + Nothing -> throwE $ relayErr VERSION + Just (Compatible v) -> liftEitherWith (const $ relayErr IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key r -> throwE . PCEUnexpectedResponse $ bshow r where - x509Error :: String -> SMPClientError - x509Error _msg = PCEResponseError $ error "TODO: x509 error" -- TODO different error + relayErr = PCEProtocolError . PROXY . BROKER . TRANSPORT . TEHandshake validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 validateRelay (X.CertificateChain cert) exact = do serverKey <- case cert of @@ -730,10 +730,53 @@ data ProxiedRelay = ProxiedRelay prServerKey :: C.PublicKeyX25519 } +data ProxyClientError + = -- | protocol error response from proxy + ProxyProtocolError ErrorType + | -- | unexpexted response + ProxyUnexpectedResponse String + | -- | error between proxy and server + ProxyResponseError ErrorType + deriving (Eq, Show, Exception) + +instance StrEncoding ProxyClientError where + strEncode = \case + ProxyProtocolError e -> "PROTOCOL " <> strEncode e + ProxyUnexpectedResponse s -> "UNEXPECTED " <> B.pack s + ProxyResponseError e -> "SYNTAX " <> strEncode e + strP = + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> ProxyProtocolError <$> _strP + "UNEXPECTED" -> ProxyUnexpectedResponse . B.unpack <$> (A.space *> A.takeByteString) + "SYNTAX" -> ProxyResponseError <$> _strP + _ -> fail "bad ProxyClientError" + -- consider how to process slow responses - is it handled somehow locally or delegated to the caller -- this method is used in the client -- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender -- receives PRES :: EncResponse -> BrokerMsg -- proxy to client + +-- When client sends message via proxy, there may be one successful scenario and 9 error scenarios +-- as shown below (WTF stands for unexpected response, ??? for response that failed to parse). +-- client proxy relay proxy client +-- 0) PFWD(SEND) -> RFWD -> RRES -> PRES(OK) -> ok +-- 1) PFWD(SEND) -> RFWD -> RRES -> PRES(ERR) -> PCEProtocolError - business logic error for client +-- 2) PFWD(SEND) -> RFWD -> RRES -> PRES(WTF) -> PCEUnexpectedReponse - relay/client protocol logic error +-- 3) PFWD(SEND) -> RFWD -> RRES -> PRES(???) -> PCEResponseError - relay/client syntax error +-- 4) PFWD(SEND) -> RFWD -> ERR -> ERR PROXY PROTOCOL -> ProxyProtocolError - proxy/relay business logic error +-- 5) PFWD(SEND) -> RFWD -> WTF -> ERR PROXY $ BROKER (UNEXPECTED s) -> ProxyProtocolError - proxy/relay protocol logic +-- 6) PFWD(SEND) -> RFWD -> ??? -> ERR PROXY $ BROKER (RESPONSE s) -> ProxyProtocolError - - proxy/relay syntax +-- 7) PFWD(SEND) -> ERR -> ProxyProtocolError - client/proxy business logic +-- 8) PFWD(SEND) -> WTF -> ProxyUnexpectedResponse - client/proxy protocol logic +-- 9) PFWD(SEND) -> ??? -> ProxyResponseError - client/proxy syntax +-- +-- We report as proxySMPMessage error (ExceptT error) the errors of two kinds: +-- - protocol errors from the destination relay wrapped in PRES - to simplify processing of AUTH and QUOTA errors, in this case proxy is "transparent" for such errors (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) +-- - other response/transport/connection errors from the client connected to proxy itself +-- Other errors are reported in the function result as `Either ProxiedRelayError ()`, including +-- - protocol errors from the client connected to proxy in ProxyClientError (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) +-- - other errors from the client running on proxy and connected to relay in PREProxiedRelayError + proxySMPMessage :: SMPClient -> -- proxy session from PKEY @@ -743,7 +786,7 @@ proxySMPMessage :: SenderId -> MsgFlags -> MsgBody -> - ExceptT SMPClientError IO () + ExceptT SMPClientError IO (Either ProxyClientError ()) -- TODO use version proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId _v serverKey) spKey sId flags msg = do -- prepare params @@ -756,24 +799,32 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND flags msg) auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of - [] -> throwE $ PCETransportError TELargeMsg -- some other error. Internal? - TBError e _ : _ -> throwE $ PCETransportError e -- large message error? + [] -> throwE $ PCETransportError TELargeMsg + TBError e _ : _ -> throwE $ PCETransportError e TBTransmission s _ : _ -> pure s TBTransmissions s _ _ : _ -> pure s et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength - sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et)) >>= \case - -- TODO support PKEY + resend? - PRES (EncResponse er) -> do - t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er - case tParse proxyThParams t' of - t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of - (_auth, _signed, (_c, _e, r)) -> case r of -- TODO: verify - Left e -> throwE $ PCEResponseError e - Right OK -> pure () - Right (ERR e) -> throwE $ PCEProtocolError e - Right u -> throwE . PCEUnexpectedResponse $ bshow u -- possibly differentiate unexpected response from server/proxy - _ -> throwE $ PCETransportError TEBadBlock - r -> throwE . PCEUnexpectedResponse $ bshow r -- from proxy + -- proxy interaction errors are wrapped + tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et))) >>= \case + Right r -> case r of + PRES (EncResponse er) -> do + -- server interaction errors are thrown directly + t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er + case tParse proxyThParams t' of + t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of + (_auth, _signed, (_c, _e, cmd)) -> case cmd of + Right OK -> pure $ Right () + Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay + Right e -> throwE $ PCEUnexpectedResponse $ B.take 32 $ bshow e + Left e -> throwE $ PCEResponseError e + _ -> throwE $ PCETransportError TEBadBlock + ERR e -> pure . Left $ ProxyProtocolError e -- this will not happen, this error is returned via Left + _ -> pure . Left $ ProxyUnexpectedResponse $ take 32 $ show r + Left e -> case e of + PCEProtocolError e' -> pure . Left $ ProxyProtocolError e' + PCEUnexpectedResponse r -> pure . Left $ ProxyUnexpectedResponse $ B.unpack r + PCEResponseError e' -> pure . Left $ ProxyResponseError e' + _ -> throwE e -- this method is used in the proxy -- sends RFWD :: EncFwdTransmission -> Command Sender @@ -783,8 +834,8 @@ forwardSMPMessage :: SMPClient -> CorrId -> C.PublicKeyX25519 -> EncTransmission forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do -- prepare params sessSecret <- case thAuth thParams of - Nothing -> throwError $ PCEProtocolError INTERNAL -- different error - proxy didn't pass key? - Just THAuthClient {sessSecret} -> maybe (throwError $ PCEProtocolError INTERNAL) pure sessSecret + Nothing -> throwError $ PCETransportError TENoServerAuth + Just THAuthClient {sessSecret} -> maybe (throwError $ PCETransportError TENoServerAuth) pure sessSecret nonce <- liftIO . atomically $ C.randomCbNonce g -- wrap let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} @@ -796,7 +847,7 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' pure fwdResponse - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE . PCEUnexpectedResponse $ B.take 32 $ bshow r okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = @@ -933,3 +984,5 @@ $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) $(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) $(J.deriveJSON defaultJSON ''NetworkConfig) + +$(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError) diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 118f3b084..d86d65251 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -66,6 +66,7 @@ module Simplex.Messaging.Protocol ErrorType (..), CommandError (..), ProxyError (..), + BrokerErrorType (..), Transmission, TransmissionAuth (..), SignedTransmission, @@ -176,6 +177,7 @@ module Simplex.Messaging.Protocol where import Control.Applicative (optional, (<|>)) +import Control.Exception (Exception) import Control.Monad import Control.Monad.Except import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -194,8 +196,6 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) @@ -211,7 +211,7 @@ import Simplex.Messaging.Parsers import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -1144,6 +1144,8 @@ data ErrorType PROXY {proxyErr :: ProxyError} | -- | command authorization error - bad signature or non-existing SMP queue AUTH + | -- | encryption/decryption error in proxy protocol + CRYPTO | -- | SMP queue capacity is exceeded on the server QUOTA | -- | ACK command is sent without message to be acknowledged @@ -1186,19 +1188,32 @@ data CommandError data ProxyError = -- | Correctly parsed SMP server ERR response. - -- This error is forwarded to the agent client as `ERR SMP err`. + -- This error is forwarded to the agent client as AgentErrorType `ERR PROXY PROTOCOL err`. PROTOCOL {protocolErr :: ErrorType} - | -- | Invalid server response that failed to parse. - -- Forwarded to the agent client as `ERR BROKER RESPONSE`. - RESPONSE {responseErr :: ErrorType} - | UNEXPECTED {unexpectedResponse :: String} -- 'String' for using derived JSON and Arbitrary instances - | TIMEOUT - | NETWORK - | BAD_HOST - | NO_SESSION - | TRANSPORT {transportErr :: TransportError} + | -- | destination server error + BROKER {brokerErr :: BrokerErrorType} + | -- | basic auth provided to proxy is invalid + BASIC_AUTH + | -- no destination server error + NO_SESSION deriving (Eq, Read, Show) +-- | SMP server errors. +data BrokerErrorType + = -- | invalid server response (failed to parse) + RESPONSE {respErr :: String} + | -- | unexpected response + UNEXPECTED {respErr :: String} + | -- | network error + NETWORK + | -- | no compatible server host (e.g. onion when public is required, or vice versa) + HOST + | -- | handshake or other transport error + TRANSPORT {transportErr :: TransportError} + | -- | command response timeout + TIMEOUT + deriving (Eq, Read, Show, Exception) + -- | SMP transmission parser. transmissionP :: THandleParams v p -> Parser RawTransmission transmissionP THandleParams {sessionId, implySessId} = do @@ -1423,6 +1438,7 @@ instance Encoding ErrorType where CMD err -> "CMD " <> smpEncode err PROXY err -> "PROXY " <> smpEncode err AUTH -> "AUTH" + CRYPTO -> "CRYPTO" QUOTA -> "QUOTA" EXPIRED -> "EXPIRED" NO_MSG -> "NO_MSG" @@ -1437,13 +1453,14 @@ instance Encoding ErrorType where "CMD" -> CMD <$> _smpP "PROXY" -> PROXY <$> _smpP "AUTH" -> pure AUTH + "CRYPTO" -> pure CRYPTO "QUOTA" -> pure QUOTA "EXPIRED" -> pure EXPIRED "NO_MSG" -> pure NO_MSG "LARGE_MSG" -> pure LARGE_MSG "INTERNAL" -> pure INTERNAL "DUPLICATE_" -> pure DUPLICATE_ - _ -> fail "bad error type" + _ -> fail "bad ErrorType" instance Encoding CommandError where smpEncode e = case e of @@ -1462,45 +1479,71 @@ instance Encoding CommandError where "HAS_AUTH" -> pure HAS_AUTH "NO_ENTITY" -> pure NO_ENTITY "NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility - _ -> fail "bad command error type" + _ -> fail "bad CommandError" instance Encoding ProxyError where - smpEncode e = case e of - PROTOCOL et -> "PROTOCOL " <> smpEncode et - RESPONSE et -> "RESPONSE " <> smpEncode et - UNEXPECTED s -> "UNEXPECTED " <> smpEncode (encodeUtf8 $ T.pack s) - TIMEOUT -> "TIMEOUT" - NETWORK -> "NETWORK" - BAD_HOST -> "BAD_HOST" + smpEncode = \case + PROTOCOL e -> "PROTOCOL " <> smpEncode e + BROKER e -> "BROKER " <> smpEncode e + BASIC_AUTH -> "BASIC_AUTH" NO_SESSION -> "NO_SESSION" - TRANSPORT t -> "TRANSPORT " <> serializeTransportError t smpP = A.takeTill (== ' ') >>= \case "PROTOCOL" -> PROTOCOL <$> _smpP - "RESPONSE" -> RESPONSE <$> _smpP - "UNEXPECTED" -> UNEXPECTED . (T.unpack . safeDecodeUtf8) <$> _smpP - "TIMEOUT" -> pure TIMEOUT - "NETWORK" -> pure NETWORK - "BAD_HOST" -> pure BAD_HOST + "BROKER" -> BROKER <$> _smpP + "BASIC_AUTH" -> pure BASIC_AUTH "NO_SESSION" -> pure NO_SESSION - "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) - _ -> fail "bad command error type" + _ -> fail "bad ProxyError" instance StrEncoding ProxyError where strEncode = \case - PROTOCOL et -> "PROTOCOL " <> strEncode et - RESPONSE et -> "RESPONSE " <> strEncode et - UNEXPECTED "" -> "UNEXPECTED" -- Arbitrary instance generates empty strings which String instance can't handle - UNEXPECTED s -> "UNEXPECTED " <> strEncode s - TRANSPORT t -> "TRANSPORT " <> serializeTransportError t - e -> bshow e + PROTOCOL e -> "PROTOCOL " <> strEncode e + BROKER e -> "BROKER " <> strEncode e + BASIC_AUTH -> "BASIC_AUTH" + NO_SESSION -> "NO_SESSION" strP = - "PROTOCOL " *> (PROTOCOL <$> strP) - <|> "RESPONSE " *> (RESPONSE <$> strP) - <|> "UNEXPECTED " *> (UNEXPECTED <$> strP) - <|> "UNEXPECTED" $> UNEXPECTED "" - <|> "TRANSPORT " *> (TRANSPORT <$> transportErrorP) - <|> parseRead1 + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> PROTOCOL <$> _strP + "BROKER" -> BROKER <$> _strP + "BASIC_AUTH" -> pure BASIC_AUTH + "NO_SESSION" -> pure NO_SESSION + _ -> fail "bad ProxyError" + +instance Encoding BrokerErrorType where + smpEncode = \case + RESPONSE e -> "RESPONSE " <> smpEncode e + UNEXPECTED e -> "UNEXPECTED " <> smpEncode e + TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + NETWORK -> "NETWORK" + TIMEOUT -> "TIMEOUT" + HOST -> "HOST" + smpP = + A.takeTill (== ' ') >>= \case + "RESPONSE" -> RESPONSE <$> _smpP + "UNEXPECTED" -> UNEXPECTED <$> _smpP + "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "NETWORK" -> pure NETWORK + "TIMEOUT" -> pure TIMEOUT + "HOST" -> pure HOST + _ -> fail "bad BrokerErrorType" + +instance StrEncoding BrokerErrorType where + strEncode = \case + RESPONSE e -> "RESPONSE " <> strEncode e + UNEXPECTED e -> "UNEXPECTED " <> strEncode e + TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + NETWORK -> "NETWORK" + TIMEOUT -> "TIMEOUT" + HOST -> "HOST" + strP = + A.takeTill (== ' ') >>= \case + "RESPONSE" -> RESPONSE <$> _strP + "UNEXPECTED" -> UNEXPECTED <$> _strP + "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "NETWORK" -> pure NETWORK + "TIMEOUT" -> pure TIMEOUT + "HOST" -> pure HOST + _ -> fail "bad BrokerErrorType" -- | Send signed SMP transmission to TCP transport. tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] @@ -1639,5 +1682,7 @@ $(J.deriveJSON defaultJSON ''MsgFlags) $(J.deriveJSON (sumTypeJSON id) ''CommandError) +$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType) + -- run deriveJSON in one TH splice to allow mutual instance $(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''ErrorType]) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index d1fcfbc24..0c0426958 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -54,7 +54,7 @@ import Data.Functor (($>)) import Data.Int (Int64) import qualified Data.IntMap.Strict as IM import Data.List (intercalate) -import Data.List.NonEmpty (NonEmpty) +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (isNothing) @@ -68,7 +68,7 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock -import Simplex.Messaging.Client (ProtocolClient (thParams), forwardSMPMessage, smpProxyError) +import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), forwardSMPMessage, smpProxyError) import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), getSMPServerClient', lookupSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -616,28 +616,27 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi reply = atomically . writeTBQueue sndQ processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Transmission BrokerMsg) processProxiedCmd (corrId, sessId, command) = (corrId, sessId,) <$> case command of - PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR AUTH) + PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR $ PROXY BASIC_AUTH) where allowProxy = do ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth getRelay = do ProxyAgent {smpAgent} <- asks proxyAgent - -- TODO catch IO errors too - liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) + liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) `catch` (pure . Left . PCEIOError) where proxyResp = \case + Left err -> ERR $ smpProxyError err Right smp -> let THandleParams {sessionId = srvSessId, thAuth} = thParams smp vr = supportedServerSMPRelayVRange in case thAuth of Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey - Nothing -> ERR $ PROXY (TRANSPORT TENoServerAuth) - Left err -> ERR $ smpProxyError err + Nothing -> ERR . PROXY . BROKER $ TRANSPORT TENoServerAuth PFWD pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent atomically (lookupSMPServerClient smpAgent sessId) >>= \case - Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) + Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) Nothing -> pure $ ERR $ PROXY NO_SESSION processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = do @@ -916,50 +915,48 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi pure . (cbNonce,) $ fromRight "" encNMsgMeta processForwardedCommand :: EncFwdTransmission -> M BrokerMsg - processForwardedCommand (EncFwdTransmission s) = fmap (either id id) . runExceptT $ do - -- TODO error - THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwError $ ERR INTERNAL) pure thAuth - sessSecret <- maybe (throwError $ ERR INTERNAL) pure sessSecret' + processForwardedCommand (EncFwdTransmission s) = fmap (either ERR id) . runExceptT $ do + THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE noRelayAuth) pure (thAuth thParams') + sessSecret <- maybe (throwE noRelayAuth) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId - -- TODO error - s' <- liftEitherWith internalErr $ C.cbDecryptNoPad sessSecret proxyNonce s - -- TODO error - FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith internalErr $ smpDecode s' - -- TODO error - this error is reported to proxy, as we failed to get to client's transmission + s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s + FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' let clientSecret = C.dh' fwdKey serverPrivKey clientNonce = C.cbNonce $ bs fwdCorrId - b <- liftEitherWith internalErr $ C.cbDecrypt clientSecret clientNonce et + b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et -- only allowing single forwarded transactions - let t' = tDecodeParseValidate thParams' $ L.head $ tParse thParams' b - clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} - -- TODO error + t' <- case tParse thParams' b of + t :| [] -> pure $ tDecodeParseValidate thParams' t + _ -> throwE BLOCK + let clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} + -- process forwarded SEND r <- lift (rejectOrVerify clntThAuth t') >>= \case Left r -> pure r - Right t''@(_, (corrId', entId', _)) -> - -- Left will not be returned by processCommand, as only SEND command is allowed - fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') - + Right t''@(_, (corrId', entId', cmd')) -> case cmd' of + Cmd SSender SEND {} -> + -- Left will not be returned by processCommand, as only SEND command is allowed + fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') + _ -> + pure (corrId', entId', ERR $ CMD PROHIBITED) -- encode response r' <- case batchTransmissions (batch thParams') (blockSize thParams') [Right (Nothing, encodeTransmission thParams' r)] of - [] -> throwE $ ERR INTERNAL -- TODO error - TBError _ _ : _ -> throwE $ ERR INTERNAL -- TODO error + [] -> throwE INTERNAL -- at least 1 item is guaranteed from NonEmpty/Right + TBError _ _ : _ -> throwE BLOCK TBTransmission b' _ : _ -> pure b' TBTransmissions b' _ _ : _ -> pure b' -- encrypt to client - r2 <- liftEitherWith internalErr $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength + r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength -- encrypt to proxy let fr = FwdResponse {fwdCorrId, fwdResponse = r2} r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) pure $ RRES r3 where - internalErr _ = ERR INTERNAL -- TODO errors - THandleParams {thAuth} = thParams' + noRelayAuth = PROXY $ BROKER $ TRANSPORT TENoServerAuth rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = case cmdOrError of Left e -> pure $ Left (corrId', entId', ERR e) - -- flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody Right cmd'@(Cmd SSender SEND {}) -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd' where verified = \case diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 8f5ad70e7..0bf60afdd 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -6,17 +6,15 @@ module CoreTests.ProtocolErrorTests where -import qualified Data.ByteString.Char8 as B -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.FileTransfer.Transport (XFTPErrorType (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as Agent +import Simplex.Messaging.Client (ProxyClientError (..)) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..), ProxyError (..)) +import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) import Simplex.RemoteControl.Types (RCErrorType (..)) @@ -27,21 +25,32 @@ import Test.QuickCheck protocolErrorTests :: Spec protocolErrorTests = modifyMaxSuccess (const 1000) $ do describe "errors parsing / serializing" $ do - it "should parse SMP protocol errors" . property $ \(err :: ErrorType) -> + it "should parse SMP protocol errors" . property . forAll possibleErrorType $ \err -> smpDecode (smpEncode err) == Right err - it "should parse SMP agent errors" . property . forAll possible $ \err -> + it "should parse SMP agent errors" . property . forAll possibleAgentErrorType $ \err -> strDecode (strEncode err) == Right err where - possible :: Gen AgentErrorType - possible = + possibleErrorType :: Gen ErrorType + possibleErrorType = arbitrary >>= \e -> if skipErrorType e then discard else pure e + possibleAgentErrorType :: Gen AgentErrorType + possibleAgentErrorType = arbitrary >>= \case - BROKER srv (Agent.RESPONSE e) | hasSpaces srv || hasSpaces e -> discard - BROKER srv _ | hasSpaces srv -> discard - SMP (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard - NTF (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard + BROKER srv _ | skip srv -> discard + BROKER _ (RESPONSE e) | skip e -> discard + BROKER _ (UNEXPECTED e) | skip e -> discard + SMP e | skipErrorType e -> discard + NTF e | skipErrorType e -> discard + Agent.PROXY pxy srv _ | skip pxy || skip srv -> discard + Agent.PROXY _ _ (ProxyProtocolError e) | skipErrorType e -> discard + Agent.PROXY _ _ (ProxyUnexpectedResponse e) | skip e -> discard + Agent.PROXY _ _ (ProxyResponseError e) | skipErrorType e -> discard ok -> pure ok - hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) - hasUnicode = any (>= '\255') + skip s = null s || any (\c -> c <= ' ' || c >= '\255') s + skipErrorType = \case + SMP.PROXY (SMP.PROTOCOL e) -> skipErrorType e + SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> skip s + SMP.PROXY (SMP.BROKER (RESPONSE s)) -> skip s + _ -> False deriving instance Generic AgentErrorType @@ -49,6 +58,8 @@ deriving instance Generic CommandErrorType deriving instance Generic ConnectionErrorType +deriving instance Generic ProxyClientError + deriving instance Generic BrokerErrorType deriving instance Generic SMPAgentError @@ -59,7 +70,7 @@ deriving instance Generic ErrorType deriving instance Generic CommandError -deriving instance Generic ProxyError +deriving instance Generic SMP.ProxyError deriving instance Generic TransportError @@ -75,6 +86,8 @@ instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU +instance Arbitrary ProxyClientError where arbitrary = genericArbitraryU + instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU @@ -85,7 +98,7 @@ instance Arbitrary ErrorType where arbitrary = genericArbitraryU instance Arbitrary CommandError where arbitrary = genericArbitraryU -instance Arbitrary ProxyError where arbitrary = genericArbitraryU +instance Arbitrary SMP.ProxyError where arbitrary = genericArbitraryU instance Arbitrary TransportError where arbitrary = genericArbitraryU diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index b70c88883..52145a992 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -113,7 +113,7 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do -- get proxy session sess <- connectSMPProxiedRelay pc relayServ (Just "correct") -- send via proxy to unsecured queue - proxySMPMessage pc sess Nothing sndId noMsgFlags msg + Right () <- proxySMPMessage pc sess Nothing sndId noMsgFlags msg -- receive 1 (_tSess, _v, _sid, _isResp, _entId, SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId encBody `shouldBe` Right msg @@ -122,7 +122,7 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g secureSMPQueue rc rPriv rcvId sPub -- send via proxy to secured queue - proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' + Right () <- proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' -- receive 2 (_tSess, _v, _sid, _isResp, _entId, SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId' encBody' `shouldBe` Right msg' @@ -171,14 +171,14 @@ testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) - reply `shouldBe` Right (SMP.ERR SMP.AUTH) + reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") - reply `shouldBe` Right (SMP.ERR SMP.AUTH) + reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) where proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"}