packages feed

btc-lsp-0.1.0.0: src/BtcLsp/Grpc/Client/LowLevel.hs

module BtcLsp.Grpc.Client.LowLevel
  ( runUnary,
    GCEnv (..),
    GCPort (..),
  )
where

import BtcLsp.Grpc.Data
import BtcLsp.Grpc.Orphan ()
import qualified BtcLsp.Grpc.Sig as Sig
import BtcLsp.Import.Witch
import Data.Aeson
  ( FromJSON (..),
    withObject,
    withScientific,
    (.:),
    (.:?),
  )
import qualified Data.Binary.Builder as BS
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy as BL
import qualified Data.CaseInsensitive as CI
import Data.Coerce (coerce)
import Data.ProtoLens (Message)
import Data.ProtoLens.Encoding (encodeMessage)
import Data.ProtoLens.Service.Types (HasMethod, HasMethodImpl (..))
import Data.Scientific (floatingOrInteger)
import GHC.TypeLits (Symbol)
import Network.GRPC.Client
import Network.GRPC.Client.Helpers
import qualified Network.GRPC.HTTP2.Encoding as G
import qualified Network.GRPC.HTTP2.ProtoLens as ProtoLens
import Network.HTTP2.Client2
import Text.PrettyPrint.GenericPretty
  ( Out,
  )
import Text.PrettyPrint.GenericPretty.Import
  ( inspectPlain,
  )
import Universum

data GCEnv = GCEnv
  { gcEnvHost :: String,
    gcEnvPort :: GCPort,
    gcEnvEncryption :: Maybe Encryption,
    gcEnvSigHeaderName :: SigHeaderName,
    gcEnvCompressMode :: CompressMode,
    gcEnvSigner :: Sig.MsgToSign -> IO (Maybe Sig.LndSig)
  }
  deriving stock
    ( Generic
    )

instance FromJSON GCEnv where
  parseJSON =
    withObject
      "GCEnv"
      ( \x ->
          GCEnv
            <$> x .: "host"
            <*> x .: "port"
            <*> x .:? "encryption"
            <*> x .: "sig_header_name"
            <*> x .: "compress_mode"
            <*> pure (const $ pure Nothing)
      )

newtype GCPort
  = GCPort PortNumber
  deriving newtype
    ( Enum,
      Eq,
      Integral,
      Num,
      Ord,
      Read,
      Real,
      Show
    )

instance FromJSON GCPort where
  parseJSON =
    withScientific "GCPort" $ \x0 ->
      case floatingOrInteger x0 of
        Left (_ :: Double) -> fail "Non-integer"
        Right x -> pure x

runUnary ::
  ( Out res,
    Show res,
    HasMethod s m,
    req ~ MethodInput s m,
    res ~ MethodOutput s m
  ) =>
  ProtoLens.RPC s (m :: Symbol) ->
  GCEnv ->
  (res -> ByteString -> CompressMode -> IO Bool) ->
  req ->
  IO (Either Text res)
runUnary rpc env verifySig req = do
  res <-
    runClientIO $
      bracket
        ( makeClient env req
            . maybe True (== Encrypted)
            $ gcEnvEncryption env
        )
        close
        (\grpc -> rawUnary rpc grpc req)
  case res of
    Right (Right (Right (h, mh, Right x))) ->
      case find (\header -> fst header == sigHeaderName) $ h <> fromMaybe mempty mh of
        Nothing ->
          pure . Left $
            "Client ==> missing server header "
              <> inspectPlain sigHeaderName
        Just (_, b64sig) -> do
          let sigDer = B64.decodeLenient b64sig
          isVerified <-
            verifySig x sigDer $
              gcEnvCompressMode env
          pure $
            if isVerified
              then Right x
              else
                Left $
                  "Client ==> server signature verification"
                    <> " failed for raw bytes"
                    <> " from decoded payload "
                    <> inspectPlain x
                    <> " with signature "
                    <> inspectPlain sigDer
    x ->
      pure . Left $
        "Client ==> server grpc failure "
          <> Universum.show x
  where
    sigHeaderName = CI.mk . from $ gcEnvSigHeaderName env

msgToSignBytes ::
  ( Message msg
  ) =>
  CompressMode ->
  msg ->
  ByteString
msgToSignBytes compressMode msg = header <> body
  where
    rawBody = encodeMessage msg
    body =
      case compressMode of
        Compressed -> G._compressionFunction G.gzip rawBody
        Uncompressed -> rawBody
    header =
      BS.pack
        [ case compressMode of
            Compressed -> 1
            Uncompressed -> 0
        ]
        <> ( BL.toStrict
               . BS.toLazyByteString
               . BS.putWord32be
               . fromIntegral -- Length is non-neg, it's fine.
               $ BS.length body
           )

makeClient ::
  Message req =>
  GCEnv ->
  req ->
  UseTlsOrNot ->
  ClientIO GrpcClient
makeClient env req tlsEnabled = do
  mSignature <- liftIO doSignature
  case mSignature of
    Just signature ->
      setupGrpcClient $
        (grpcClientConfigSimple (gcEnvHost env) (coerce $ gcEnvPort env) tlsEnabled)
          { _grpcClientConfigCompression = compression,
            _grpcClientConfigHeaders =
              [ ( sigHeaderName,
                  B64.encode $ Sig.unLndSig signature
                )
              ]
          }
    Nothing -> throwError EarlyEndOfStream
  where
    signer = gcEnvSigner env
    sigHeaderName = from $ gcEnvSigHeaderName env
    compressMode = gcEnvCompressMode env
    doSignature =
      signer . Sig.MsgToSign $
        msgToSignBytes compressMode req
    compression =
      case compressMode of
        Compressed -> gzip
        Uncompressed -> uncompressed