clickhouse-migrate-0.1.0.0: src/ClickHouseMigrate/Target.hs
-- | Explicit, validated configuration for one direct administrative target.
module ClickHouseMigrate.Target
( TargetKey,
targetKey,
targetKeyText,
DirectEndpoint,
directEndpoint,
endpointText,
ServerId,
serverId,
serverIdText,
QueryId,
queryId,
queryIdText,
LedgerId,
ledgerId,
newLedgerId,
ledgerIdText,
LedgerNamespace,
ledgerNamespace,
ledgerNamespaceText,
LedgerBinding (FirstUse, ExistingLedger),
Credentials,
basicCredentials,
HttpLimits,
httpLimits,
defaultHttpLimits,
TargetConfig,
targetConfig,
configuredTargetKey,
configuredLedgerBinding,
TargetError (..),
ServerIdentity,
identityServerId,
identityVersion,
identityHostName,
)
where
import ClickHouseMigrate.Internal.Identity (freshUuid)
import ClickHouseMigrate.Internal.TargetTypes
import ClickHouseMigrate.Prelude
import Data.Char (isAsciiLower, isAsciiUpper)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Encoding
import Text.Read (readMaybe)
data TargetError = InvalidTargetKey | InvalidEndpoint | InvalidServerId | InvalidQueryId | InvalidLedgerId | InvalidLedgerNamespace | InvalidCredentials | InvalidHttpLimits
deriving stock (Eq, Ord, Show)
targetKey :: Text -> Either TargetError TargetKey
targetKey value
| valid 256 (\c -> lowerDigit c || c `elem` ("/_:.-" :: String)) value && maybe False (lowerDigit . fst) (Text.uncons value) = Right (TargetKey value)
| otherwise = Left InvalidTargetKey
directEndpoint :: Text -> Either TargetError DirectEndpoint
directEndpoint value = do
authority <- case Text.stripPrefix "http://" value of
Just rest -> Right rest
Nothing -> maybe (Left InvalidEndpoint) Right (Text.stripPrefix "https://" value)
let hostPort = maybe authority id (Text.stripSuffix "/" authority)
validHost c = isAsciiLower c || isAsciiUpper c || digit c || c `elem` (".-" :: String)
validPort "" = True
validPort suffix = case Text.stripPrefix ":" suffix of
Just port | not (Text.null port) && Text.all digit port -> case readMaybe (Text.unpack port) :: Maybe Int of
Just n -> n > 0 && n <= 65535
_ -> False
_ -> False
validAuthority = case Text.stripPrefix "[" hostPort of
Just ipv6 -> case Text.breakOn "]" ipv6 of
(host, suffix) -> not (Text.null host) && Text.all (\c -> digit c || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || c `elem` (":." :: String)) host && maybe False validPort (Text.stripPrefix "]" suffix)
Nothing -> let (host, port) = Text.breakOn ":" hostPort in not (Text.null host) && Text.all validHost host && validPort port
if Text.length value <= 2048 && validAuthority then Right (DirectEndpoint value) else Left InvalidEndpoint
serverId :: Text -> Either TargetError ServerId
serverId value = if uuid value then Right (ServerId value) else Left InvalidServerId
queryId :: Text -> Either TargetError QueryId
queryId value = if valid 128 (\c -> lowerDigit c || isAsciiUpper c || c `elem` ("_-" :: String)) value then Right (QueryId value) else Left InvalidQueryId
ledgerId :: Text -> Either TargetError LedgerId
ledgerId value = if uuid value then Right (LedgerId value) else Left InvalidLedgerId
ledgerNamespace :: Text -> Either TargetError LedgerNamespace
ledgerNamespace value
| valid 128 (\c -> lowerDigit c || c == '_') value && maybe False (\(c, _) -> isAsciiLower c || c == '_') (Text.uncons value) = Right (LedgerNamespace value)
| otherwise = Left InvalidLedgerNamespace
basicCredentials :: Text -> Text -> Either TargetError Credentials
basicCredentials user password
| Text.null user || Text.any (`elem` (":\r\n\0" :: String)) user || Text.any (== '\0') password = Left InvalidCredentials
| otherwise = Right (Credentials (Encoding.encodeUtf8 user) (Encoding.encodeUtf8 password))
-- | Deadline and raw-byte bound. Ordinary responses are bounded in total;
-- complete ledger reads apply this byte bound per JSON row (excluding LF).
-- Decoded history and reports still retain memory proportional to history size.
httpLimits :: Int -> Int -> Either TargetError HttpLimits
httpLimits timeoutMicros maxBytes
| timeoutMicros > 0 && timeoutMicros <= 300000000 && maxBytes > 0 && maxBytes <= 16777216 = Right (HttpLimits timeoutMicros maxBytes)
| otherwise = Left InvalidHttpLimits
defaultHttpLimits :: HttpLimits
defaultHttpLimits = HttpLimits 30000000 1048576
targetConfig :: TargetKey -> DirectEndpoint -> ServerId -> LedgerNamespace -> LedgerBinding -> Credentials -> HttpLimits -> TargetConfig
targetConfig = TargetConfig
targetKeyText :: TargetKey -> Text
targetKeyText (TargetKey value) = value
endpointText :: DirectEndpoint -> Text
endpointText (DirectEndpoint value) = value
serverIdText :: ServerId -> Text
serverIdText (ServerId value) = value
queryIdText :: QueryId -> Text
queryIdText (QueryId value) = value
ledgerIdText :: LedgerId -> Text
ledgerIdText (LedgerId value) = value
ledgerNamespaceText :: LedgerNamespace -> Text
ledgerNamespaceText (LedgerNamespace value) = value
valid :: Int -> (Char -> Bool) -> Text -> Bool
valid limit allowed value = not (Text.null value) && Text.length value <= limit && Text.all allowed value
lowerDigit :: Char -> Bool
lowerDigit c = isAsciiLower c || digit c
digit :: Char -> Bool
digit c = c >= '0' && c <= '9'
uuid :: Text -> Bool
uuid value = map Text.length (Text.splitOn "-" value) == [8, 4, 4, 4, 12] && Text.all (\c -> c == '-' || digit c || (c >= 'a' && c <= 'f')) value
identityServerId :: ServerIdentity -> ServerId
identityServerId (ServerIdentity value _ _) = value
identityVersion :: ServerIdentity -> Text
identityVersion (ServerIdentity _ value _) = value
identityHostName :: ServerIdentity -> Text
identityHostName (ServerIdentity _ _ value) = value
-- | Fresh client-generated UUID for a one-time first-use authorization. Persist
-- this value before submission and retain it after interrupted initialization.
newLedgerId :: IO LedgerId
newLedgerId = LedgerId <$> freshUuid
-- | Read-only nonsecret configuration identity for application adapters.
configuredTargetKey :: TargetConfig -> TargetKey
configuredTargetKey (TargetConfig key _ _ _ _ _ _) = key
-- | Binding mode is explicit; this accessor grants no initialization authority.
configuredLedgerBinding :: TargetConfig -> LedgerBinding
configuredLedgerBinding (TargetConfig _ _ _ _ binding _ _) = binding