natskell-1.0.0.1: internal/Policy/Handshake/Nats.hs
module Handshake.Nats
( HandshakeError (..)
, performHandshake
) where
import Auth.Resolver (applyAuthPatch, buildAuthPatch)
import Auth.Types
import qualified Data.ByteString as BS
import Data.Maybe (fromMaybe, isJust)
import Network.ConnectionAPI
( Conn
, ConnectionAPI
, TransportOption (..)
, configure
, readData
, reader
, writeDataLazy
, writer
)
import Parser.API
( ParseStep (DropPrefix, Emit, NeedMore, Reject)
, ParsedMessage (..)
, ParserAPI
, parse
)
import State.Store
( ClientState
, config
, setServerInfo
, updateLogContextFromInfo
)
import State.Types (ClientConfig (..))
import System.Timeout (timeout)
import Transformers.Transformers (Transformer (transform))
import qualified Types.Connect as Connect
import qualified Types.Err as Err
import qualified Types.Info as I
import Types.Ping (Ping (Ping))
import Types.Pong (Pong (Pong))
import Types.TLS (defaultTLSConfig)
data HandshakeError = HandshakeTransportError String
| HandshakeTLSError String
| HandshakeProtocolError String
| HandshakeAuthError AuthError
| HandshakeTimeout
deriving (Eq, Show)
performHandshake :: ConnectionAPI -> ParserAPI ParsedMessage -> ClientState -> Auth -> Conn -> String -> IO (Either HandshakeError ())
performHandshake connectionApi parserApi state auth conn host = do
result <- timeout (max 1 (connectTimeoutMicros (config state))) handshake
pure (fromMaybe (Left HandshakeTimeout) result)
where
handshake = do
infoResult <- readInitialInfo
case infoResult of
Left err ->
pure (Left err)
Right (info, rest) -> do
let cfg = config state
tlsRequested = isJust (tlsConfig cfg) || Connect.tls_required (connectConfig cfg)
tlsRequired = fromMaybe False (I.tls_required info)
tlsAvailable = fromMaybe False (I.tls_available info)
useTls = tlsRequired || (tlsRequested && tlsAvailable)
transportOption =
TransportOption
{ transportHost = host
, transportTlsRequired = tlsRequired
, transportTlsRequested = tlsRequested && tlsAvailable
, transportTlsConfig =
if useTls
then Just (fromMaybe defaultTLSConfig (tlsConfig cfg))
else Nothing
, transportInitialBytes = rest
}
if tlsRequested && not (tlsRequired || tlsAvailable)
then pure (Left (HandshakeTLSError "server does not offer TLS"))
else do
transportResult <- configure connectionApi conn transportOption
case transportResult of
Left err ->
pure (Left (HandshakeTLSError err))
Right () -> do
setServerInfo state info
updateLogContextFromInfo state info
let authContext = AuthContext { authNonce = I.nonce info }
connectPayload =
(connectConfig cfg)
{ Connect.tls_required = useTls
}
authResult <- buildAuthPatch auth authContext
case authResult of
Left err ->
pure (Left (HandshakeAuthError err))
Right patch -> do
writeResult <-
writeDataLazy
(writer connectionApi)
conn
( transform (applyAuthPatch patch connectPayload)
<> transform Ping
)
case writeResult of
Left err ->
pure (Left (HandshakeTransportError err))
Right () ->
awaitPong mempty
readInitialInfo :: IO (Either HandshakeError (I.Info, BS.ByteString))
readInitialInfo = go mempty
where
go acc = do
result <- readData (reader connectionApi) conn 4096
case result of
Left err ->
pure (Left (HandshakeTransportError err))
Right chunk ->
if BS.null chunk
then pure (Left (HandshakeTransportError "read returned empty chunk before INFO"))
else do
let bytes = acc <> chunk
case parse parserApi bytes of
NeedMore ->
go bytes
DropPrefix n _ ->
go (BS.drop n bytes)
Reject reason ->
pure (Left (HandshakeProtocolError reason))
Emit (ParsedInfo info) rest ->
pure (Right (info, rest))
Emit (ParsedErr err) _
| isAuthenticationError err ->
pure (Left (HandshakeAuthError (AuthError (show err))))
| otherwise ->
pure (Left (HandshakeProtocolError ("server error before INFO: " ++ show err)))
Emit _ rest ->
go rest
awaitPong :: BS.ByteString -> IO (Either HandshakeError ())
awaitPong acc = do
result <- readData (reader connectionApi) conn 4096
case result of
Left err ->
pure (Left (HandshakeTransportError err))
Right chunk
| BS.null chunk ->
pure (Left (HandshakeTransportError "read returned empty chunk before PONG"))
| otherwise ->
consumePongFrames (acc <> chunk)
consumePongFrames :: BS.ByteString -> IO (Either HandshakeError ())
consumePongFrames bytes =
case parse parserApi bytes of
NeedMore ->
awaitPong bytes
DropPrefix _ reason ->
pure (Left (HandshakeProtocolError reason))
Reject reason ->
pure (Left (HandshakeProtocolError reason))
Emit (ParsedPong _) _ ->
pure (Right ())
Emit (ParsedOk _) rest ->
continueWith rest
Emit (ParsedPing _) rest -> do
pongResult <- writeDataLazy (writer connectionApi) conn (transform Pong)
case pongResult of
Left err -> pure (Left (HandshakeTransportError err))
Right () -> continueWith rest
Emit (ParsedInfo info) rest -> do
setServerInfo state info
updateLogContextFromInfo state info
continueWith rest
Emit (ParsedErr err) _
| isAuthenticationError err ->
pure (Left (HandshakeAuthError (AuthError (show err))))
| otherwise ->
pure (Left (HandshakeProtocolError (show err)))
Emit message _ ->
pure (Left (HandshakeProtocolError ("unexpected frame before PONG: " ++ show message)))
continueWith rest
| BS.null rest = awaitPong mempty
| otherwise = consumePongFrames rest
isAuthenticationError (Err.ErrAuthViolation _) = True
isAuthenticationError (Err.ErrAuthTimeout _) = True
isAuthenticationError (Err.ErrAuthExpired _) = True
isAuthenticationError (Err.ErrAuthRevoked _) = True
isAuthenticationError (Err.ErrAccountAuthExpired _) = True
isAuthenticationError _ = False