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