module Jingle ( setupJingleHandlers, jingleHandler, ibbHandler, tsidToSidMap, JingleSID, JingleTSID ) where import Prelude () import BasicPrelude import System.Directory (createDirectoryIfMissing, listDirectory, removeDirectoryRecursive) import Control.Error (readZ, headZ, hush) import Text.Printf (printf) import Data.IORef (newIORef, readIORef, writeIORef) import Crypto.Hash (SHA1, hashWith) import System.Clock (TimeSpec(..)) import UnexceptionalIO (UIO, Unexceptional) import qualified UnexceptionalIO as UIO import qualified Data.ByteString as ByteString import qualified Data.ByteString.Base64 as Base64 import qualified Network.Protocol.XMPP as XMPP import qualified Data.XML.Types as XML import qualified Data.Cache as Cache import qualified Network.Socket as Socket import qualified Jingle.Socks5Server as Socks5Server import Util import StoreChunks sessionAccept :: Text -> Text -> XMPP.JID -> XML.Element -> XML.Element sessionAccept sid contentName responder transport = XML.Element (s"{urn:xmpp:jingle:1}jingle") [ (s"sid", [XML.ContentText sid]), (s"action", [s"session-accept"]), (s"responder", [XML.ContentText $ XMPP.formatJID responder]) ] [ XML.NodeElement $ XML.Element (s"{urn:xmpp:jingle:1}content") [ (s"creator", [s"initiator"]), (s"name", [XML.ContentText contentName]) ] [XML.NodeElement transport] ] s5bTransport :: (Text, Socket.PortNumber) -> Text -> XMPP.JID -> XML.Element s5bTransport (host, port) tsid jid = XML.Element (s"{urn:xmpp:jingle:transports:s5b:1}transport") [ (s"sid", [XML.ContentText tsid]) ] [ XML.NodeElement $ XML.Element (s"{urn:xmpp:jingle:transports:s5b:1}candidate") [ (s"jid", [XML.ContentText $ XMPP.formatJID jid]), (s"host", [XML.ContentText host]), (s"cid", [s"directJingleS5B"]), (s"priority", [s"8323071"]), (s"port", [XML.ContentText $ tshow port]) ] [] ] ibbTransport :: Text -> XML.Element ibbTransport tsid = XML.Element (s"{urn:xmpp:jingle:transports:ibb:1}transport") [ (s"block-size", [s"8192"]), (s"sid", [XML.ContentText tsid]) ] [] ibbTransportAccept :: Text -> Text -> Text -> XMPP.JID -> XML.Element ibbTransportAccept sid tsid contentName initiator = XML.Element (s"{urn:xmpp:jingle:1}jingle") [ (s"sid", [XML.ContentText sid]), (s"action", [s"transport-accept"]), (s"initiator", [XML.ContentText $ XMPP.formatJID initiator]) ] [ XML.NodeElement $ XML.Element (s"{urn:xmpp:jingle:1}content") [ (s"creator", [s"initiator"]), (s"name", [XML.ContentText contentName]) ] [ XML.NodeElement $ ibbTransport tsid ] ] s5bCandidateError :: XML.Element s5bCandidateError = XML.Element (s"{urn:xmpp:jingle:transports:s5b:1}candidate-error") [] [] s5bCandidateErrorTI :: Text -> Text -> Text -> XMPP.JID -> XML.Element s5bCandidateErrorTI sid tsid contentName initiator = XML.Element (s"{urn:xmpp:jingle:1}jingle") [ (s"sid", [XML.ContentText sid]), (s"action", [s"transport-info"]), (s"initiator", [XML.ContentText $ XMPP.formatJID initiator]) ] [ XML.NodeElement $ XML.Element (s"{urn:xmpp:jingle:1}content") [ (s"creator", [s"initiator"]), (s"name", [XML.ContentText contentName]) ] [ XML.NodeElement $ XML.Element (s"{urn:xmpp:jingle:transports:s5b:1}transport") [ (s"sid", [XML.ContentText tsid]) ] [ XML.NodeElement s5bCandidateError ] ] ] s5bPseudoTsid :: Text -> XMPP.JID -> XMPP.JID -> JingleTSID s5bPseudoTsid tsid jid1 jid2 = JingleTSID $ tshow $ Crypto.Hash.hashWith (undefined :: SHA1) $ encodeUtf8 $ tsid ++ XMPP.formatJID jid1 ++ XMPP.formatJID jid2 sessionInitiate :: (Text, Socket.PortNumber) -> (JingleTSID -> UIO ()) -> XMPP.IQ -> Text -> [XML.Element] -> Text -> XMPP.XMPP () sessionInitiate hostPort newTransport iq@XMPP.IQ { XMPP.iqTo = Just to, XMPP.iqFrom = Just from } sid content contentName | [tsid] <- elementAttributeText (s"sid") (s"{urn:xmpp:jingle:transports:s5b:1}transport") `overChildrenOf` content = do liftIO $ UIO.run $ newTransport (JingleTSID tsid) liftIO $ UIO.run $ newTransport $ s5bPseudoTsid tsid from to liftIO $ UIO.run $ newTransport $ s5bPseudoTsid tsid to from XMPP.putStanza $ iqReply Nothing iq -- In a strange case of IQ use, we don't really care about the -- response so long as it's not an error XMPP.putStanza $ iqNewRequest iq XMPP.IQSet (s"dontcare-session-accept") $ sessionAccept sid contentName to $ s5bTransport hostPort tsid to XMPP.putStanza $ iqNewRequest iq XMPP.IQSet (s"dontcare-transport-info") (s5bCandidateErrorTI sid tsid contentName from) | [tsid] <- elementAttributeText (s"sid") (s"{urn:xmpp:jingle:transports:ibb:1}transport") `overChildrenOf` content = do liftIO $ UIO.run $ newTransport (JingleTSID tsid) XMPP.putStanza $ iqReply Nothing iq -- In a strange case of IQ use, we don't really care about the -- response so long as it's not an error XMPP.putStanza $ iqNewRequest iq XMPP.IQSet (s"dontcare-session-accept") $ sessionAccept sid contentName to $ ibbTransport tsid sessionInitiate _ _ iq _ _ _ = XMPP.putStanza $ iqError notImplemented iq jingleAction :: Text -> XMPP.IQ -> Bool jingleAction action iq = isJust $ mfilter (\j -> Just action == XML.attributeText (s"action") j) $ child (s"{urn:xmpp:jingle:1}jingle") iq fileTransferDescription :: XML.Element -> [XML.Element] fileTransferDescription = uncurry (<|>) . ( XML.isNamed (s"{urn:xmpp:jingle:apps:file-transfer:5}description") &&& XML.isNamed (s"{urn:xmpp:jingle:apps:file-transfer:3}description") ) jingleTransport :: XML.Element -> [XML.Element] jingleTransport = uncurry (<|>) . ( XML.isNamed (s"{urn:xmpp:jingle:transports:s5b:1}transport") &&& XML.isNamed (s"{urn:xmpp:jingle:transports:ibb:1}transport") ) jingleHandler' :: (Text, Socket.PortNumber) -> (JingleSID -> XMPP.IQ -> UIO ()) -> (JingleSID -> JingleTSID -> UIO ()) -> XMPP.IQ -> [XML.Element] -> Text -> XMPP.XMPP () jingleHandler' hostPort newSession newTransport iq@XMPP.IQ { XMPP.iqFrom = Just from } children sid | jingleAction (s"session-initiate") iq, (_:_) <- fileTransferDescription `overChildrenOf` content = do liftIO $ UIO.run $ newSession (JingleSID sid) iq sessionInitiate hostPort (newTransport (JingleSID sid)) iq sid content contentName | jingleAction (s"transport-info") iq, (_:_) <- jingleTransport `overChildrenOf` content = XMPP.putStanza $ iqReply Nothing iq | jingleAction (s"session-info") iq, (_:_) <- XML.isNamed (s"{urn:xmpp:jingle:apps:file-transfer:5}checksum") =<< children = XMPP.putStanza $ iqReply Nothing iq | jingleAction (s"transport-replace") iq, [tsid] <- elementAttributeText (s"sid") (s"{urn:xmpp:jingle:transports:ibb:1}transport") `overChildrenOf` content = do liftIO $ UIO.run $ newTransport (JingleSID sid) (JingleTSID tsid) XMPP.putStanza $ iqReply Nothing iq XMPP.putStanza $ iqNewRequest iq XMPP.IQSet (s"dontcare-transport-accept") (ibbTransportAccept sid tsid contentName from) | jingleAction (s"session-terminate") iq = XMPP.putStanza $ iqReply Nothing iq where content = XML.isNamed (s"{urn:xmpp:jingle:1}content") =<< children contentName = fromMaybe mempty $ XML.attributeText (s"name") =<< headZ content jingleHandler' _ _ _ iq _ _ = XMPP.putStanza $ iqError notImplemented iq jingleHandler :: (Text, Socket.PortNumber) -> (JingleSID -> XMPP.IQ -> UIO ()) -> (JingleSID -> JingleTSID -> UIO ()) -> XMPP.IQ -> XML.Element -> XMPP.XMPP () jingleHandler hostPort newSession newTransport iq jingle | Just sid <- XML.attributeText (s"sid") jingle = jingleHandler' hostPort newSession newTransport iq (XML.elementChildren jingle) sid | otherwise = XMPP.putStanza $ iqError notImplemented iq ibbHandler :: FilePath -> (JingleTSID -> FilePath -> UIO ()) -> XMPP.IQ -> XMPP.XMPP () ibbHandler storePath transferFinished iq | Just _ <- child (s"{http://jabber.org/protocol/ibb}open") iq = XMPP.putStanza $ iqReply Nothing iq | Just close <- child (s"{http://jabber.org/protocol/ibb}close") iq, Just sid <- XML.attributeText (s"sid") close = do XMPP.putStanza $ iqReply Nothing iq let dir = storePath ++ "/tmp/" ++ textToString sid chunkRef <- liftIO $ newIORef =<< fmap sort (listDirectory dir) let tmpName = textToString sid ++ "/FINAL" (path, _, _, _) <- liftIO $ UIO.runEitherIO $ storeChunks storePath tmpName $ do chunkFiles <- fromIO_ $ readIORef chunkRef case chunkFiles of [] -> return mempty (x:xs) -> do fromIO_ $ writeIORef chunkRef xs fmap hush $ UIO.fromIO $ ByteString.readFile (dir ++ "/" ++ x) liftIO $ removeDirectoryRecursive dir liftIO $ UIO.run $ transferFinished (JingleTSID sid) path | Just dta <- child (s"{http://jabber.org/protocol/ibb}data") iq, Just sid <- XML.attributeText (s"sid") dta, Just seqn <- readZ . textToString =<< XML.attributeText (s"seq") dta = do XMPP.putStanza $ iqReply Nothing iq let dir = storePath ++ "/tmp/" ++ textToString sid liftIO $ createDirectoryIfMissing True dir liftIO $ ByteString.writeFile (dir ++ "/" ++ printf "%010d" (seqn :: Word32)) $ Base64.decodeLenient $ encodeUtf8 $ mconcat $ XML.elementText dta | otherwise = XMPP.putStanza $ iqError notImplemented iq iqSetHandler :: FilePath -> (Text, Socket.PortNumber) -> (JingleSID -> XMPP.IQ -> UIO ()) -> (JingleSID -> JingleTSID -> UIO ()) -> (JingleTSID -> FilePath -> UIO ()) -> XMPP.IQ -> XMPP.XMPP () iqSetHandler storePath hostPort newSession newTransport transportDone iq | Just jingle <- child (s"{urn:xmpp:jingle:1}jingle") iq = jingleHandler hostPort newSession newTransport iq jingle | Just (s"http://jabber.org/protocol/ibb") == (XML.nameNamespace =<< XML.elementName <$> XMPP.iqPayload iq) = ibbHandler storePath transportDone iq | otherwise = XMPP.putStanza $ iqError notImplemented iq tsidToSidMap :: (Unexceptional m) => (JingleSID -> FilePath -> UIO ()) -> m ( JingleSID -> JingleTSID -> UIO (), JingleTSID -> FilePath -> UIO () ) tsidToSidMap notifyBySid = fromIO_ (Cache.newCache (Just $ TimeSpec 900 0)) >>= \cache -> return ( \sid (JingleTSID tsid) -> do fromIO_ $ Cache.purgeExpired cache fromIO_ $ Cache.insert cache tsid sid , \(JingleTSID tsid) path -> do msid <- fromIO_ $ Cache.lookup' cache tsid case msid of Nothing -> return () Just sid -> notifyBySid sid path ) sidToIqMap :: (Unexceptional m) => (XMPP.IQ -> FilePath -> UIO ()) -> m ( JingleSID -> XMPP.IQ -> UIO (), JingleSID -> FilePath -> UIO () ) sidToIqMap notifyByIq = fromIO_ (Cache.newCache (Just $ TimeSpec 900 0)) >>= \cache -> return ( \(JingleSID sid) iq -> do fromIO_ $ Cache.purgeExpired cache fromIO_ $ Cache.insert cache sid iq , \(JingleSID sid) path -> do miq <- fromIO_ $ Cache.lookup' cache sid case miq of Nothing -> return () Just iq -> notifyByIq iq path ) -- | Probably you want this setupJingleHandlers :: (Unexceptional m) => FilePath -> [Socket.SockAddr] -> (Text, Socket.PortNumber) -> (String -> UIO ()) -> (XMPP.IQ -> FilePath -> UIO ()) -> m (Either IOError (XMPP.IQ -> XMPP.XMPP ())) setupJingleHandlers storePath ports hostPort logger transferDoneIq = do (newSession, transferDone) <- sidToIqMap transferDoneIq (newTransport, transportDone) <- tsidToSidMap transferDone (fmap.fmap) (\() -> iqSetHandler storePath hostPort newSession newTransport transportDone ) $ Socks5Server.start storePath ports logger transportDone