2020-09-22 20:34:55 +02:00
{- # LANGUAGE ForeignFunctionInterface # -}
module Carrion.Plugin.TCL
( initPlugin ,
2020-09-28 17:39:43 +02:00
tellCommands ,
myPlugName
2020-09-22 20:34:55 +02:00
) where
import Control.Monad
2020-09-23 20:17:56 +02:00
import Control.Concurrent ( forkIO , threadDelay , killThread )
2020-09-22 20:34:55 +02:00
import Control.Concurrent.STM
import System.Posix.DynamicLinker
import System.Environment
import Foreign.Ptr
import Foreign.C.String
import qualified Data.Text as T
2020-09-27 17:27:55 +02:00
import GypsFulvus.PluginStuff ( Manhole ( .. ) , Sewage ( .. ) , InitStatus ( .. ) , SewageAutorInfo ( .. ) , genericAutorToNSAutor , stripCommandPrefix' , regift , nsAutorToGenericAutor )
2020-09-22 20:34:55 +02:00
data Tcl_Interp = Tcl_Interp deriving Show
type Tcl_Interp_Ptr = Ptr Tcl_Interp
type TCL_Actual_Version = CString
type TCL_Wanted_Version = CString
type TclScriptString = CString
type TclScriptStringByteLen = Int
type TclEvalFlags = Int
type WantExact = Int
type TclScriptFilename = CString
data TCLCommand = TCLCommand { getTCLCNick :: String ,
getTCLCMask :: String ,
getTCLCHandle_o_O :: String ,
getTCLCChannel :: String ,
getTCLCActualCommand :: String
}
2020-09-23 20:17:56 +02:00
type Tcl_EvalFile_Sig = ( Tcl_Interp_Ptr -> TclScriptFilename -> IO Int )
type Tcl_EvalEx_Sig = ( Tcl_Interp_Ptr -> TclScriptString -> TclScriptStringByteLen -> TclEvalFlags -> IO Int )
type Tcl_GetStringResult_Sig = ( Tcl_Interp_Ptr -> IO CString )
type Tcl_CancelEval_Sig = ( Tcl_Interp_Ptr -> Ptr Tcl_Obj_Dummy -> Ptr Tcl_ClientData_Dummy -> Int -> IO Int )
type Tcl_AsyncInvoke_Sig = ( Tcl_Interp_Ptr -> Int -> IO Int )
2020-09-22 20:34:55 +02:00
foreign import ccall " dynamic " mkTcl_CreateInterp :: FunPtr ( IO Tcl_Interp_Ptr ) -> IO ( Tcl_Interp_Ptr )
2020-09-23 20:17:56 +02:00
data Tcl_Obj_Dummy = Tcl_Obj_Dummy
data Tcl_ClientData_Dummy = Tcl_ClientData_Dummy
2020-09-22 20:34:55 +02:00
foreign import ccall " &Tcl_InitStubs_wrap " tcl_InitStubs :: FunPtr ( Tcl_Interp_Ptr -> TCL_Wanted_Version -> WantExact -> IO TCL_Actual_Version )
foreign import ccall " dynamic " mkTcl_InitStubs :: FunPtr ( Tcl_Interp_Ptr -> TCL_Wanted_Version -> WantExact -> IO TCL_Actual_Version ) -> ( Tcl_Interp_Ptr -> TCL_Wanted_Version -> WantExact -> IO TCL_Actual_Version )
foreign import ccall " dynamic " mkTcl_FindExecutable :: FunPtr ( CString -> IO CString ) -> ( CString -> IO CString )
foreign import ccall " dynamic " mkTcl_InitMemory :: FunPtr ( Tcl_Interp_Ptr -> IO () ) -> ( Tcl_Interp_Ptr -> IO () )
foreign import ccall " dynamic " mkTcl_Init :: FunPtr ( Tcl_Interp_Ptr -> IO Int ) -> ( Tcl_Interp_Ptr -> IO Int )
2020-09-23 20:17:56 +02:00
foreign import ccall " dynamic " mkTcl_CancelEval :: FunPtr Tcl_CancelEval_Sig -> Tcl_CancelEval_Sig
foreign import ccall " dynamic " mkTcl_AsyncInvoke :: FunPtr Tcl_AsyncInvoke_Sig -> Tcl_AsyncInvoke_Sig
foreign import ccall " dynamic " mkTcl_EvalFile :: FunPtr Tcl_EvalFile_Sig -> Tcl_EvalFile_Sig
foreign import ccall " dynamic " mkTcl_GetStringResult :: FunPtr Tcl_GetStringResult_Sig -> Tcl_GetStringResult_Sig
2020-09-22 20:34:55 +02:00
foreign import ccall " dynamic " mkTcl_EvalEx :: FunPtr ( Tcl_Interp_Ptr -> TclScriptString -> TclScriptStringByteLen -> TclEvalFlags -> IO Int ) -> ( Tcl_Interp_Ptr -> TclScriptString -> TclScriptStringByteLen -> TclEvalFlags -> IO Int )
2020-09-23 20:17:56 +02:00
2020-09-22 20:34:55 +02:00
tu :: T . Text -> String
tu = T . unpack
2020-09-28 17:39:43 +02:00
2020-09-22 22:09:59 +02:00
tellCommands :: [ T . Text ]
2020-09-27 17:27:55 +02:00
tellCommands = map T . pack [ " tcl " , " tclAdmin " ]
2020-09-28 17:39:43 +02:00
privilegedAutors :: [ T . Text ]
2020-09-27 17:27:55 +02:00
privilegedAutors = map T . pack [ " core " , " STDIO haskeline " , " hastur " , " IRC-Simple " ]
2020-09-28 17:39:43 +02:00
myPluginName :: T . Text
2020-09-24 20:03:10 +02:00
myPluginName = T . pack " TCL-Simple "
2020-09-28 17:39:43 +02:00
myPlugName = myPluginName
lOCAL :: T . Text
lOCAL = T . pack " local "
2020-09-22 22:09:59 +02:00
mySignature :: SewageAutorInfo
2020-09-28 17:39:43 +02:00
mySignature = GenericStyleAutor myPluginName myPluginName lOCAL
sigWithChan :: T . Text -> SewageAutorInfo
sigWithChan ch = GenericStyleAutor myPluginName myPluginName ch
2020-09-22 22:09:59 +02:00
stripCommandLocal :: T . Text -> Manhole -> IO ( Maybe T . Text )
2020-09-22 20:34:55 +02:00
stripCommandLocal c m = stripCommandPrefix' c tellCommands m mySignature
mkTCLCommandFromAIAndMsg :: SewageAutorInfo -> String -> TCLCommand
2020-09-28 17:39:43 +02:00
mkTCLCommandFromAIAndMsg b = case b of
GenericStyleAutor a b c -> mkTCLCommandFromAIAndMsg . genericAutorToNSAutor $ GenericStyleAutor a b c
NetworkIdentStyleAutor a b c -> TCLCommand ( tu a ) ( show b ) " " ( tu c )
2020-09-23 20:17:56 +02:00
data TCLInterpreterWrapper = TCLInterpreterWrapper { getInterp :: TMVar ( Tcl_Interp_Ptr ) ,
2020-09-22 20:34:55 +02:00
getEvalFile :: Tcl_EvalFile_Sig ,
getEvalEx :: Tcl_EvalEx_Sig ,
2020-09-23 20:17:56 +02:00
getGetStringResult :: Tcl_GetStringResult_Sig ,
getCancelEval :: Tcl_CancelEval_Sig ,
getAsyncInvoke :: Tcl_AsyncInvoke_Sig
2020-09-22 20:34:55 +02:00
}
lEN_AUTO :: Int
lEN_AUTO = - 1
2020-09-28 17:39:43 +02:00
2020-09-22 20:34:55 +02:00
eVAL_FLAGS_CLEAR :: Int
eVAL_FLAGS_CLEAR = 0
2022-12-06 16:48:54 -08:00
--dumpDebug :: Monad m => p -> m ()
--dumpDebug _ = return ()
dumpDebug = putStrLn
2020-09-22 20:34:55 +02:00
initPlugin :: Manhole -> IO InitStatus
initPlugin manhole = do
myFakeArg0 <- getExecutablePath >>= newCString
2022-12-06 16:48:54 -08:00
myTCLDl <- dlopen " libtcl8.6.so " [ RTLD_NOW ]
2020-09-22 20:34:55 +02:00
let bless name convf = dlsym myTCLDl name >>= \ fp -> return $ convf $ fp
tcl_CreateInterp <- bless " Tcl_CreateInterp " mkTcl_CreateInterp
interp <- tcl_CreateInterp
let tcl_InitStubs' = mkTcl_InitStubs tcl_InitStubs
wanted_interp_version <- newCString " 8.6 "
actual_version <- tcl_InitStubs' interp wanted_interp_version 0 >>= peekCString
dumpDebug actual_version
tcl_FindExecutable <- bless " Tcl_FindExecutable " mkTcl_FindExecutable
theComputedExecutablePath <- tcl_FindExecutable $ myFakeArg0
if nullPtr == theComputedExecutablePath then
dumpDebug " Couldn't Tcl_FindExecutable() "
else
peekCString theComputedExecutablePath >>= dumpDebug
tcl_InitMemory <- bless " Tcl_InitMemory " mkTcl_InitMemory
tcl_InitMemory interp
tcl_Init <- bless " Tcl_Init " mkTcl_Init
tcl_Init_status <- tcl_Init interp
dumpDebug $ show tcl_Init_status
if ( tcl_Init_status /= 0 ) then
return $ BadInitStatus $ T . pack " non-zero return "
else do
tcl_EvalEx <- bless " Tcl_EvalEx " mkTcl_EvalEx
tcl_GetStringResult <- bless " Tcl_GetStringResult " mkTcl_GetStringResult
tcl_EvalFile <- bless " Tcl_EvalFile " mkTcl_EvalFile
2020-09-23 20:17:56 +02:00
tcl_CancelEval <- bless " Tcl_CancelEval " mkTcl_CancelEval
tcl_AsyncInvoke <- bless " Tcl_AsyncInvoke " mkTcl_AsyncInvoke
2020-09-22 20:34:55 +02:00
smeginitstatus <- newCString " ./src/smeggdrop/smeggdrop.tcl " >>= \ fn -> tcl_EvalFile interp fn
2020-09-23 20:17:56 +02:00
threadsafe_interp_duh <- atomically $ newTMVar interp
let wrappedinterp = TCLInterpreterWrapper threadsafe_interp_duh tcl_EvalFile tcl_EvalEx tcl_GetStringResult tcl_CancelEval tcl_AsyncInvoke
2020-09-22 20:34:55 +02:00
forkIO $ rEPL wrappedinterp manhole
return GoodInitStatus
2020-09-22 22:09:59 +02:00
2020-09-26 21:26:42 +02:00
processCommand :: TCLInterpreterWrapper -> Sewage -> Bool -> IO T . Text
processCommand wi s ip = do
2020-09-22 20:34:55 +02:00
let tcl_EvalEx = getEvalEx wi
tcl_GetStringResult = getGetStringResult wi
2020-09-23 20:17:56 +02:00
tcl_CancelEval = getCancelEval wi
tcl_AsyncInvoke = getAsyncInvoke wi
i = getInterp wi
2020-09-22 20:34:55 +02:00
autInfo = getSewageAutor s
sewCmd = T . unpack $ getSewage s
autDefNS = genericAutorToNSAutor autInfo
sewNick = T . unpack $ getNick autDefNS
sewMask = show $ getMask autDefNS
sewChan = T . unpack $ getChannel autDefNS
2020-09-23 20:17:56 +02:00
interp <- atomically $ takeTMVar i
let runscript s = tcl_EvalEx interp s lEN_AUTO eVAL_FLAGS_CLEAR
runTclCommand s = newCString s >>= runscript
errorInfo = runTclCommand " return $errorInfo "
2020-09-22 20:34:55 +02:00
doTheTCL c = runTclCommand c >>= \ st ->
case st of
0 -> tcl_GetStringResult interp >>= \ rs -> if nullPtr == rs then dumpDebug ( " Command: " ++ c ++ " ; returned a null pointer result. " ) >> return " FAILED " else peekCString rs >>= \ nrs -> dumpDebug ( " Output of command: " ++ c ++ " ; " ++ nrs ++ " ; " ) >> return nrs
_ -> errorInfo >> tcl_GetStringResult interp >>= peekCString
2020-09-23 20:17:56 +02:00
performFromIRC = doTheTCL $ " pub:tcl:perform \ " " ++ sewNick ++ " \ " \ " " ++ sewMask ++ " \ " {} \ " " ++ sewChan ++ " \ " { " ++ sewCmd ++ " } "
2020-09-26 21:26:42 +02:00
performAdminLevel = doTheTCL sewCmd
res <- if ( ip ) then performAdminLevel else performFromIRC
2020-09-23 20:17:56 +02:00
atomically $ putTMVar i interp
return $ T . pack res
2020-09-22 20:34:55 +02:00
2020-09-28 17:39:43 +02:00
sigWithChan' :: T . Text -> T . Text -> SewageAutorInfo
sigWithChan' thechannel originallocation = GenericStyleAutor originallocation myPluginName thechannel
rEPL :: TCLInterpreterWrapper -> Manhole -> IO b
2020-09-22 20:34:55 +02:00
rEPL wrappedtclinterp manhole =
let inspectManhole = atomically . readTChan . getInputChan
regift g = atomically . ( flip writeTChan g ) . getOutputChan in
forever $ do
newGift <- inspectManhole manhole
2020-09-22 22:09:59 +02:00
strippedCmd <- stripCommandLocal ( getSewage newGift ) manhole
case strippedCmd of
Just cmdBodyStripped -> do
let giftStripped = Sewage ( getSewageAutor newGift ) cmdBodyStripped
2020-09-23 20:17:56 +02:00
let hmm = gnarlyBalanced $ T . unpack cmdBodyStripped
case hmm of
Nothing -> do
2020-09-27 17:27:55 +02:00
let theOriginalChannel = getContext . nsAutorToGenericAutor . getSewageAutor $ newGift
2020-09-28 17:39:43 +02:00
theOriginalPlugin = getLocation . nsAutorToGenericAutor . getSewageAutor $ newGift
2020-09-27 17:27:55 +02:00
let isPrivileged = if T . pack " tclAdmin " ` T . isPrefixOf ` ( getSewage newGift ) && ( getNick . genericAutorToNSAutor . getSewageAutor $ newGift ) ` elem ` privilegedAutors then True else False
2020-09-26 21:26:42 +02:00
processedGift <- processCommand wrappedtclinterp giftStripped isPrivileged
2020-09-28 17:39:43 +02:00
regift ( Sewage ( sigWithChan' theOriginalChannel theOriginalPlugin ) processedGift ) manhole
2020-09-23 20:17:56 +02:00
Just berror -> regift ( Sewage mySignature ( T . pack berror ) ) manhole
2020-09-22 22:09:59 +02:00
Nothing -> return ()
2020-09-23 20:17:56 +02:00
isBalanced :: Char -> Char -> String -> Maybe String
isBalanced openc closec = bal ( - 1 ) 0
where
bal :: Int -> Int -> String -> Maybe String
bal _ 0 [] = Nothing
bal i _ [] = Just $ " Opening bracket unmatched until end of command. " -- unmatched opening
bal i ( - 1 ) _ = Just $ " Unmatched closing bracket at position " ++ show i -- unmatched close
bal i n ( singlec : bs )
| singlec == openc = bal ( i + 1 ) ( n + 1 ) bs
| singlec == closec = bal ( i + 1 ) ( n - 1 ) bs
| singlec == '\\ ' = c a s e b s o f
( sc : rs ) -> if sc == openc || sc == closec then bal ( i + 2 ) n rs else bal ( i + 1 ) n rs
| otherwise = bal ( i + 1 ) n bs
2020-09-28 17:39:43 +02:00
gnarlyBalanced :: String -> Maybe String
2020-09-23 20:17:56 +02:00
gnarlyBalanced = isBalanced '{' '}'
-- it's better not to check for double quotes and square brackets I think since they can be escaped and not used internally for pub:tcl:perform...
2020-09-28 17:39:43 +02:00
squareBalanced :: String -> Maybe String
2020-09-23 20:17:56 +02:00
squareBalanced = isBalanced '[' ']'
2020-09-28 17:39:43 +02:00
dquoteBalanced :: String -> Maybe String
2020-09-23 20:17:56 +02:00
dquoteBalanced = isBalanced '"' '"'