packages feed

natskell-1.1.0.0: 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.List                 (isPrefixOf)
import           Data.Maybe                (fromMaybe, isJust)
import           Network.ConnectionAPI
    ( Conn
    , ConnectionAPI
    , TransportOption (..)
    , bufferRead
    , configure
    , readData
    , reader
    , writeDataLazy
    , writer
    )
import           Parser.API
    ( ParseStep (DropPrefix, Emit, NeedMore, Reject)
    , ParsedMessage (..)
    , ParserAPI
    , parse
    )
import           State.Store
    ( ClientState
    , config
    , notifyServerError
    , setServerInfo
    , updateLogContextFromInfo
    )
import           State.Types
    ( ClientConfig (..)
    , serverErrorFromProtocol
    )
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
  deriving (Eq, Show)

handshakeControlFrameLimit :: Int
handshakeControlFrameLimit = 64 * 1024

performHandshake :: ConnectionAPI -> ParserAPI ParsedMessage -> ClientState -> Auth -> Conn -> String -> IO (Either HandshakeError ())
performHandshake connectionApi parserApi state auth conn host = handshake
  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 = readMore mempty
      where
        readMore 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 consumeInfoFrames (acc <> chunk)

        consumeInfoFrames bytes =
          case parse parserApi bytes of
            NeedMore ->
              retainIncompleteFrame "INFO" bytes readMore
            DropPrefix n _
              | n <= 0 ->
                  pure (Left (HandshakeProtocolError "malformed protocol frame"))
              | otherwise ->
                  continueInfoWith (BS.drop n bytes)
            Reject reason ->
              pure (Left (HandshakeProtocolError (reasonCategory reason)))
            Emit (ParsedInfo info) rest ->
              pure (Right (info, rest))
            Emit (ParsedErr err) _ -> do
              notifyServerError state (serverErrorFromProtocol err)
              if isAuthenticationError err
                then pure (Left (HandshakeAuthError (AuthError (show err))))
                else pure (Left (HandshakeProtocolError "server error before INFO"))
            Emit _ rest ->
              continueInfoWith rest

        continueInfoWith rest
          | BS.null rest = readMore mempty
          | otherwise = consumeInfoFrames 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 ->
          retainIncompleteFrame "PONG" bytes awaitPong
        DropPrefix _ reason ->
          pure (Left (HandshakeProtocolError (reasonCategory reason)))
        Reject reason ->
          pure (Left (HandshakeProtocolError (reasonCategory reason)))
        Emit (ParsedPong _) rest -> do
          bufferRead (reader connectionApi) conn rest
          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) _ -> do
          notifyServerError state (serverErrorFromProtocol err)
          if isAuthenticationError err
            then pure (Left (HandshakeAuthError (AuthError (show err))))
            else pure (Left (HandshakeProtocolError "server error before PONG"))
        Emit message _ ->
          pure (Left (HandshakeProtocolError ("unexpected " ++ frameKind message ++ " frame before PONG")))

    continueWith rest
      | BS.null rest = awaitPong mempty
      | otherwise = consumePongFrames rest

    retainIncompleteFrame frame bytes continue
      | BS.length bytes > handshakeControlFrameLimit =
          pure . Left . HandshakeProtocolError $
            frame ++ " control frame exceeds 65536 byte limit"
      | otherwise = continue bytes

    isAuthenticationError (Err.ErrAuthViolation _)      = True
    isAuthenticationError (Err.ErrAuthTimeout _)        = True
    isAuthenticationError (Err.ErrAuthExpired _)        = True
    isAuthenticationError (Err.ErrAuthRevoked _)        = True
    isAuthenticationError (Err.ErrAccountAuthExpired _) = True
    isAuthenticationError _                             = False

    reasonCategory reason
      | "unknown protocol prefix" `isPrefixOf` reason = "unknown protocol prefix"
      | "invalid INFO" `isPrefixOf` reason = "invalid INFO frame"
      | "invalid MSG" `isPrefixOf` reason = "invalid MSG frame"
      | "invalid HMSG" `isPrefixOf` reason = "invalid HMSG frame"
      | otherwise = "malformed protocol frame"

    frameKind (ParsedMsg _)               = "MSG"
    frameKind (ParsedInfo _)              = "INFO"
    frameKind (ParsedPing _)              = "PING"
    frameKind (ParsedPong _)              = "PONG"
    frameKind (ParsedOk _)                = "+OK"
    frameKind (ParsedErr _)               = "-ERR"
    frameKind (ParsedMessageTooLarge _ _) = "oversized message"