packages feed

lnd-client-0.1.0.1: src/LndClient/Data/LndEnv.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}

module LndClient.Data.LndEnv
  ( LndEnv (..),
    RawConfig,
    LndWalletPassword (..),
    LndTlsCert,
    LndHexMacaroon (..),
    LndHost' (..),
    LndPort',
    LndConfig (..),
    newLndEnv,
    readLndEnv,
    createLndTlsCert,
    unLndTlsCert,
    createLndPort,
    enforceDebugSev,
  )
where

import Data.Aeson as A
  ( Result (..),
    Value (..),
    camelTo2,
    defaultOptions,
    eitherDecodeStrict,
    fieldLabelModifier,
    genericParseJSON,
    withObject,
    (.:?),
  )
import qualified Data.ByteString.Char8 as C8
import qualified Data.PEM as Pem
import Data.Scientific
import Data.X509
import Data.X509.CertificateStore
import Env
import LndClient.Class
import LndClient.Data.Newtype
import LndClient.Data.Type
import LndClient.Import.External as Ex
import LndClient.Util as U
import Network.GRPC.Client.Helpers
  ( GrpcClientConfig (..),
    grpcClientConfigSimple,
  )
import Network.GRPC.HTTP2.Encoding (uncompressed)
import Network.HTTP2.Client2
import qualified Network.TLS as TLS
import qualified Network.TLS.Extra.Cipher as TLS
import qualified Universum
import qualified Prelude

newtype LndWalletPassword = LndWalletPassword Text
  deriving newtype (PersistField, PersistFieldSql, Eq, FromJSON, IsString)

data LndTlsCert = LndTlsCert ByteString SignedCertificate
  deriving stock (Eq, Show)

newtype LndHexMacaroon = LndHexMacaroon Text
  deriving newtype (PersistField, PersistFieldSql, Eq, FromJSON, IsString)

newtype LndHost' = LndHost' Text
  deriving newtype (PersistField, PersistFieldSql, Eq, FromJSON, IsString)
  deriving stock (Generic)

instance Out LndHost'

newtype LndPort' = LndPort' Int
  deriving newtype (PersistField, PersistFieldSql, Eq)
  deriving stock (Generic)

instance Out LndPort'

data LndConfig = LndConfig
  { lndConfigHost :: HostName,
    lndConfigPort :: PortNumber,
    lndConfigTlsEnabled :: Bool,
    lndConfigCompression :: Bool
  }
  deriving stock (Show)

data RawConfig = RawConfig
  { rawConfigLndWalletPassword :: LndWalletPassword,
    rawConfigLndTlsCert :: LndTlsCert,
    rawConfigLndHexMacaroon :: LndHexMacaroon,
    rawConfigLndHost :: LndHost',
    rawConfigLndPort :: LndPort',
    rawConfigLndCipherSeedMnemonic :: Maybe CipherSeedMnemonic,
    rawConfigLndAezeedPassphrase :: Maybe AezeedPassphrase
  }
  deriving stock (Eq, Generic)

data LndEnv = LndEnv
  { envLndWalletPassword :: LndWalletPassword,
    envLndHexMacaroon :: LndHexMacaroon,
    envLndLogStrategy :: LoggingStrategy,
    envLndCipherSeedMnemonic :: Maybe CipherSeedMnemonic,
    envLndAezeedPassphrase :: Maybe AezeedPassphrase,
    envLndSyncGrpcTimeout :: Maybe GrpcTimeoutSeconds,
    envLndAsyncGrpcTimeout :: Maybe GrpcTimeoutSeconds,
    envLndConfig :: GrpcClientConfig,
    envLndLogSeverity :: Maybe Severity
  }

instance ToGrpc LndWalletPassword ByteString where
  toGrpc x = Right $ encodeUtf8 (coerce x :: Text)

instance FromJSON LndTlsCert where
  parseJSON x =
    case x of
      A.String s ->
        case createLndTlsCert $ encodeUtf8 s of
          Right cert -> return cert
          Left e -> failure e
      e -> failure e
    where
      failure err =
        fail $
          "Json certificate parsing error: " <> Prelude.show err

instance FromJSON LndPort' where
  parseJSON x =
    case x of
      A.Number s -> do
        let ePort :: Either LndError Word32 =
              maybeToRight
                (LndEnvError "Port should be Int")
                $ toBoundedInteger s
        case ePort >>= createLndPort of
          Right lndPort -> return lndPort
          Left err -> failure err
      err -> failure err
    where
      failure err =
        fail $
          "Json port loading error: " <> Prelude.show err

instance FromJSON RawConfig where
  parseJSON =
    genericParseJSON
      defaultOptions
        { fieldLabelModifier = camelTo2 '_' . Ex.drop 9
        }

instance FromJSON LndEnv where
  parseJSON arg =
    case fromJSON arg :: Result RawConfig of
      Error e -> fail e
      Success rc -> do
        let res =
              newLndEnv
                (rawConfigLndWalletPassword rc)
                (rawConfigLndTlsCert rc)
                (rawConfigLndHexMacaroon rc)
                (rawConfigLndHost rc)
                (rawConfigLndPort rc)
                (rawConfigLndCipherSeedMnemonic rc)
                (rawConfigLndAezeedPassphrase rc)
        let logStrategy =
              envLndLogStrategy res
        withObject
          "LndEnv"
          ( \obj ->
              ( \x0 x1 x2 x3 ->
                  res
                    { envLndSyncGrpcTimeout = x0,
                      envLndAsyncGrpcTimeout = x1,
                      envLndLogSeverity = x2,
                      envLndLogStrategy =
                        logStrategy
                          { loggingStrategyMeta =
                              fromMaybe (loggingStrategyMeta logStrategy) x3
                          }
                    }
              )
                <$> obj .:? "lnd_sync_grpc_timeout_seconds"
                <*> obj .:? "lnd_async_grpc_timeout_seconds"
                <*> obj .:? "lnd_log_severity"
                <*> obj .:? "lnd_log_meta"
          )
          arg

createLndTlsCert :: ByteString -> Either LndError LndTlsCert
createLndTlsCert bs = do
  pemsM <- first (LndEnvError . pack) $ Pem.pemParseBS bs
  pem <-
    note (LndEnvError $ pack "No pem found") $ safeHead pemsM
  bimap
    (LndEnvError . pack . ("Certificate is not valid: " <>))
    (LndTlsCert bs)
    (decodeSignedCertificate $ Pem.pemContent pem)

unLndTlsCert :: LndTlsCert -> ByteString
unLndTlsCert (LndTlsCert bs _) = coerce bs

createLndPort :: Word32 -> Either LndError LndPort'
createLndPort p = do
  let maybePort :: Maybe Int = U.safeFromIntegral p
  maybeToRight (LndEnvError "Wrong port") $ LndPort' <$> maybePort

readLndEnv :: IO LndEnv
readLndEnv =
  parse
    (header "LndEnv")
    $ var
      (parser <=< nonempty)
      "LND_CLIENT_ENV_DATA"
      (keep <> help "")
  where
    parser :: String -> Either Error LndEnv
    parser x =
      first UnreadError $ eitherDecodeStrict $ C8.pack x

selfSignedCertificateValidation :: [SignedCertificate] -> TLS.ClientParams -> TLS.ClientParams
selfSignedCertificateValidation extraCerts cp =
  cp
    { TLS.clientShared =
        (TLS.clientShared cp) {TLS.sharedCAStore = makeCertificateStore extraCerts},
      TLS.clientSupported =
        (TLS.clientSupported cp) {TLS.supportedCiphers = TLS.ciphersuite_default}
    }

newLndEnv ::
  LndWalletPassword ->
  LndTlsCert ->
  LndHexMacaroon ->
  LndHost' ->
  LndPort' ->
  Maybe CipherSeedMnemonic ->
  Maybe AezeedPassphrase ->
  LndEnv
newLndEnv pwd (LndTlsCert _ cert) mac (LndHost' host) (LndPort' port) seed aezeed =
  LndEnv
    { envLndWalletPassword = pwd,
      envLndHexMacaroon = mac,
      envLndLogStrategy = logDefault,
      envLndCipherSeedMnemonic = seed,
      envLndAezeedPassphrase = aezeed,
      envLndSyncGrpcTimeout = Nothing,
      envLndAsyncGrpcTimeout = Nothing,
      envLndConfig =
        (grpcClientConfigSimple host_ port_ True)
          { _grpcClientConfigCompression = uncompressed,
            _grpcClientConfigHeaders =
              [ ("macaroon", encodeUtf8 (coerce mac :: Text))
              ],
            _grpcClientConfigTLS =
              Just . selfSignedCertificateValidation [cert] $
                TLS.defaultParamsClient host_ (Universum.show port_)
          },
      envLndLogSeverity = Just DebugS
    }
  where
    host_ = unpack host
    port_ :: PortNumber
    port_ = fromInteger (toInteger port)

enforceDebugSev :: LndEnv -> LndEnv
enforceDebugSev env =
  env
    { envLndLogStrategy =
        (envLndLogStrategy env)
          { loggingStrategySeverity =
              loggingStrategySeverity logDebug
          }
    }