{-# LANGUAGE CPP #-}
-- Disable this warning so we can still test deprecated functionality.
{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}

module Common (
    printCiphers,
    printDHParams,
    printGroups,
    readNumber,
    readCiphers,
    readDHParams,
    readGroups,
    printHandshakeInfo,
    makeAddrInfo,
    AddrInfo (..),
    getCertificateStore,
) where

import Data.Char (isDigit)
import Network.Socket
import Numeric (showHex)

import Crypto.System.CPU
import Data.X509.CertificateStore
import System.X509

import Network.TLS hiding (HostName)
import Network.TLS.Extra.Cipher
import Network.TLS.Extra.FFDHE

import Imports

namedDHParams :: [(String, DHParams)]
namedDHParams =
    [ ("ffdhe2048", ffdhe2048)
    , ("ffdhe3072", ffdhe3072)
    , ("ffdhe4096", ffdhe4096)
    , ("ffdhe6144", ffdhe6144)
    , ("ffdhe8192", ffdhe8192)
    ]

namedCiphersuites :: [(String, [CipherID])]
namedCiphersuites =
    [ ("all", map cipherID ciphersuite_all)
    , ("default", map cipherID ciphersuite_default)
    , ("strong", map cipherID ciphersuite_strong)
    ]

namedGroups :: [(String, Group)]
namedGroups =
    [ ("ffdhe2048", FFDHE2048)
    , ("ffdhe3072", FFDHE3072)
    , ("ffdhe4096", FFDHE4096)
    , ("ffdhe6144", FFDHE6144)
    , ("ffdhe8192", FFDHE8192)
    , ("p256", P256)
    , ("p384", P384)
    , ("p521", P521)
    , ("x25519", X25519)
    , ("x448", X448)
    ]

readNumber :: (Num a, Read a) => String -> Maybe a
readNumber s
    | all isDigit s = Just $ read s
    | otherwise = Nothing

readCiphers :: String -> Maybe [CipherID]
readCiphers s =
    case lookup s namedCiphersuites of
        Nothing -> (: []) `fmap` readNumber s
        just -> just

readDHParams :: String -> IO (Maybe DHParams)
readDHParams s =
    case lookup s namedDHParams of
        Nothing -> (Just . read) `fmap` readFile s
        mparams -> return mparams

readGroups :: String -> Maybe [Group]
readGroups s = traverse (`lookup` namedGroups) (split ',' s)

printCiphers :: IO ()
printCiphers = do
    putStrLn "Supported ciphers"
    putStrLn "====================================="
    forM_ ciphersuite_all_det $ \c ->
        putStrLn
            ( pad 50 (cipherName c)
                ++ " = "
                ++ pad 5 (show $ cipherID c)
                ++ "  0x"
                ++ showHex (cipherID c) ""
            )
    putStrLn ""
    putStrLn "Ciphersuites"
    putStrLn "====================================="
    forM_ namedCiphersuites $ \(name, _) -> putStrLn name
    putStrLn ""
    putStrLn
        ("Using crypton-" ++ VERSION_crypton ++ " with CPU support for: " ++ cpuSupport)
  where
    pad n s
        | length s < n = s ++ replicate (n - length s) ' '
        | otherwise = s

    cpuSupport
        | null processorOptions = "(nothing)"
        | otherwise = intercalate ", " (map show processorOptions)

printDHParams :: IO ()
printDHParams = do
    putStrLn "DH Parameters"
    putStrLn "====================================="
    forM_ namedDHParams $ \(name, _) -> putStrLn name
    putStrLn "(or /path/to/dhparams)"

printGroups :: IO ()
printGroups = do
    putStrLn "Groups"
    putStrLn "====================================="
    forM_ namedGroups $ \(name, _) -> putStrLn name

printHandshakeInfo :: Context -> IO ()
printHandshakeInfo ctx = do
    info <- contextGetInformation ctx
    case info of
        Nothing -> return ()
        Just i -> do
            putStrLn ("version: " ++ show (infoVersion i))
            putStrLn ("cipher: " ++ show (infoCipher i))
            putStrLn ("compression: " ++ show (infoCompression i))
            putStrLn ("group: " ++ maybe "(none)" show (infoSupportedGroup i))
            when (infoVersion i < TLS13) $ do
                putStrLn ("extended master secret: " ++ show (infoExtendedMainSecret i))
                putStrLn ("resumption: " ++ show (infoTLS12Resumption i))
            when (infoVersion i == TLS13) $ do
                putStrLn ("handshake emode: " ++ show (fromJust (infoTLS13HandshakeMode i)))
                putStrLn ("early data accepted: " ++ show (infoIsEarlyDataAccepted i))
    sni <- getClientSNI ctx
    case sni of
        Nothing -> return ()
        Just n -> putStrLn ("server name indication: " ++ n)

makeAddrInfo :: Maybe HostName -> PortNumber -> IO AddrInfo
makeAddrInfo maddr port = do
    let flgs = [AI_ADDRCONFIG, AI_NUMERICSERV, AI_PASSIVE]
        hints =
            defaultHints
                { addrFlags = flgs
                , addrSocketType = Stream
                }
    head <$> getAddrInfo (Just hints) maddr (Just $ show port)

split :: Char -> String -> [String]
split _ "" = []
split c s = case break (c ==) s of
    ("", r) -> split c (tail r)
    (s', "") -> [s']
    (s', r) -> s' : split c (tail r)

getCertificateStore :: [FilePath] -> IO CertificateStore
getCertificateStore [] = getSystemCertificateStore
getCertificateStore paths = foldM readPathAppend mempty paths
  where
    readPathAppend acc path = do
        mstore <- readCertificateStore path
        case mstore of
            Nothing -> error ("invalid certificate store: " ++ path)
            Just st -> return $! mappend st acc
