mariadb-haskell-0.1.0.0: src/Database/MySQL/Authentication/GSSAPI.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Internal platform provider. SQL transport and authentication policy remain
-- in Connection; this module owns only native context lifetime and token steps.
module Database.MySQL.Authentication.GSSAPI (GSSAPIError(..), withGSSAPI) where
import Control.Exception
import Data.ByteString (ByteString)
#ifndef mingw32_HOST_OS
import Control.Concurrent
import Control.Monad
import qualified Data.ByteString as B
import Foreign hiding (void)
import Foreign.C.Types
#endif
newtype GSSAPIError = GSSAPIError String deriving Show
instance Exception GSSAPIError
#ifdef mingw32_HOST_OS
-- Windows needs an SSPI provider; do not fail every ordinary-driver build by
-- attempting to link Unix GSSAPI libraries on that platform.
withGSSAPI :: ByteString -> Bool -> ((ByteString -> IO (Bool, ByteString)) -> IO a) -> IO a
withGSSAPI _ _ _ = throwIO (GSSAPIError "native GSSAPI requires a Unix platform provider")
#else
-- A worker is the sole owner of native state. Cancelling the connecting thread
-- enqueues Stop without waiting for a potentially blocking KDC request. The
-- worker finishes that request, releases its context and exits; it has no socket
-- access and cannot continue authentication after the caller's transport closes.
-- This also prevents a finalizer racing gss_init_sec_context in a safe FFI call.
data Request = Step ByteString (MVar (Either SomeException (Bool, ByteString))) | Stop
data NativeContext
foreign import ccall safe "mariadb_gss_open"
nativeOpen :: Ptr CChar -> CSize -> CInt -> Ptr (Ptr NativeContext)
-> Ptr Word32 -> Ptr Word32 -> IO CInt
foreign import ccall safe "mariadb_gss_step"
nativeStep :: Ptr NativeContext -> Ptr CChar -> CSize -> Ptr (Ptr CChar)
-> Ptr CSize -> Ptr CInt -> Ptr Word32 -> Ptr Word32 -> IO CInt
foreign import ccall safe "mariadb_gss_close"
nativeClose :: Ptr NativeContext -> IO ()
-- | Internal provider boundary. One serialized step yields completion plus an
-- owned token copy. All output is bounded to 1 MiB. Native work already in
-- progress can outlive cancellation, subject to the OS Kerberos retry policy;
-- cancellation itself never waits for it and never exposes the socket to it.
withGSSAPI :: ByteString -> Bool -> ((ByteString -> IO (Bool, ByteString)) -> IO a) -> IO a
withGSSAPI principal negotiate action = mask $ \restore -> do
-- A non-threaded RTS cannot run the timeout handler while another Haskell
-- thread is inside a safe foreign call. Fail before acquiring credentials
-- rather than silently weakening the connection deadline.
unless rtsSupportsBoundThreads
(throwIO (GSSAPIError "GSSAPI authentication requires an application built with -threaded"))
requests <- newEmptyMVar
ready <- newEmptyMVar
_ <- forkIO $ do
opened <- try (openContext principal negotiate)
putMVar ready opened
case opened of
Left (_ :: SomeException) -> pure ()
Right context -> worker requests context `finally` nativeClose context
-- A cancelled caller can leave Step in the single-slot queue. Queue Stop
-- from a helper rather than dropping it or blocking connection cleanup.
let stop = void (forkIO (putMVar requests Stop))
step token = do
reply <- newEmptyMVar
putMVar requests (Step token reply)
takeMVar reply >>= either throwIO pure
flip finally stop $ do
restore (takeMVar ready) >>= either throwIO (const (pure ()))
restore (action step)
where
worker requests context = do
request <- takeMVar requests
case request of
Stop -> pure ()
Step token reply -> do
result <- try (stepContext context token)
putMVar reply result
worker requests context
openContext :: ByteString -> Bool -> IO (Ptr NativeContext)
openContext principal negotiate = B.useAsCStringLen principal $ \(name,size) ->
alloca $ \result -> alloca $ \major -> alloca $ \minor -> do
status <- nativeOpen name (fromIntegral size) (if negotiate then 1 else 0) result major minor
checkNative status major minor
peek result
stepContext :: Ptr NativeContext -> ByteString -> IO (Bool, ByteString)
stepContext context token = B.useAsCStringLen token $ \(input,size) ->
alloca $ \output -> alloca $ \lengthPtr -> alloca $ \complete ->
alloca $ \major -> alloca $ \minor -> do
status <- nativeStep context input (fromIntegral size) output lengthPtr complete major minor
checkNative status major minor
count <- fromIntegral <$> peek lengthPtr
bytes <- if count == 0 then pure B.empty else peek output >>= \ptr -> B.packCStringLen (ptr,count)
done <- (/= 0) <$> peek complete
pure (done,bytes)
checkNative :: CInt -> Ptr Word32 -> Ptr Word32 -> IO ()
checkNative status major minor = unless (status == 0) $ do
majorCode <- peek major
minorCode <- peek minor
-- Numeric provider codes retain diagnostics without logging ticket bodies,
-- credentials, cache names or server-controlled challenge text.
throwIO (GSSAPIError ("GSSAPI authentication failed (status " ++ show status ++
", major " ++ show majorCode ++ ", minor " ++ show minorCode ++ ")"))
#endif