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 Jingle.StoreChunks
sessionAccept :: JingleSID -> Text -> XMPP.JID -> XML.Element -> XML.Element
sessionAccept (JingleSID 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 :: JingleSID -> Text -> Text -> XMPP.JID -> XML.Element
ibbTransportAccept (JingleSID 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 :: JingleSID -> Text -> Text -> XMPP.JID -> XML.Element
s5bCandidateErrorTI (JingleSID 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 ())
-> (JingleTSID -> Int -> UIO ())
-> XMPP.IQ
-> JingleSID
-> [XML.Element]
-> Text
-> Maybe Int
-> XMPP.XMPP ()
sessionInitiate hostPort newTransport setSize iq@XMPP.IQ {
XMPP.iqTo = Just to,
XMPP.iqFrom = Just from
} sid content contentName size
| [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
liftIO $ UIO.run $ forM_ size $ \inner -> do
setSize (JingleTSID tsid) inner
setSize (s5bPseudoTsid tsid from to) inner
setSize (s5bPseudoTsid tsid to from) inner
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")
)
fileSizeFromDescription :: XML.Element -> Maybe Int
fileSizeFromDescription description = readZ $ textToString $ mconcat $
XML.elementText =<< fileTransferSize =<<
XML.elementChildren =<< fileTransferFile =<<
XML.elementChildren description
fileTransferFile :: XML.Element -> [XML.Element]
fileTransferFile = uncurry (<|>) . (
XML.isNamed (s"{urn:xmpp:jingle:apps:file-transfer:5}file")
&&&
XML.isNamed (s"{urn:xmpp:jingle:apps:file-transfer:3}file")
)
fileTransferSize :: XML.Element -> [XML.Element]
fileTransferSize = uncurry (<|>) . (
XML.isNamed (s"{urn:xmpp:jingle:apps:file-transfer:5}size")
&&&
XML.isNamed (s"{urn:xmpp:jingle:apps:file-transfer:3}size")
)
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 ())
-> (JingleTSID -> Int -> UIO ())
-> XMPP.IQ
-> [XML.Element]
-> JingleSID
-> Maybe (XMPP.IQ -> UIO ())
-> XMPP.XMPP ()
jingleHandler' hostPort newSession newTransport setSize iq@XMPP.IQ {
XMPP.iqFrom = Just from
} children sid handlerForUnknown
| jingleAction (s"session-initiate") iq,
(desc:_) <- fileTransferDescription `overChildrenOf` content = do
liftIO $ UIO.run $ newSession sid iq
sessionInitiate hostPort
(newTransport sid)
setSize
iq sid content contentName (fileSizeFromDescription desc)
| Just handler <- handlerForUnknown = liftIO $ UIO.run $ handler iq
| 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 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 -> UIO (Maybe (XMPP.IQ -> UIO ())))
-> (JingleSID -> JingleTSID -> UIO ())
-> (JingleTSID -> Int -> UIO ())
-> XMPP.IQ
-> XML.Element
-> XMPP.XMPP ()
jingleHandler hostPort newSession handlerWhenUnknown newTransport setSize iq jingle
| Just sid <- JingleSID <$> XML.attributeText (s"sid") jingle = do
handlerForUnknown <- liftIO $ UIO.run $ handlerWhenUnknown sid
jingleHandler' hostPort newSession newTransport setSize
iq (XML.elementChildren jingle) sid handlerForUnknown
| 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 Nothing 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 -> UIO (Maybe (XMPP.IQ -> UIO ())))
-> (JingleSID -> JingleTSID -> UIO ())
-> (JingleTSID -> Int -> UIO ())
-> (JingleTSID -> FilePath -> UIO ())
-> XMPP.IQ
-> XMPP.XMPP ()
iqSetHandler storePath hostPort newSession handleUnknownSession newTransport setSize transportDone iq
| Just jingle <- child (s"{urn:xmpp:jingle:1}jingle") iq =
jingleHandler hostPort newSession handleUnknownSession newTransport setSize 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
)
tsidToSizeMap :: (Unexceptional m) =>
m (
JingleTSID -> Int -> UIO (),
JingleTSID -> UIO (Maybe Int)
)
tsidToSizeMap =
fromIO_ (Cache.newCache (Just $ TimeSpec 900 0)) >>= \cache ->
return (
\(JingleTSID tsid) size -> do
fromIO_ $ Cache.purgeExpired cache
fromIO_ $ Cache.insert cache tsid size
,
\(JingleTSID tsid) ->
fromIO_ $ Cache.lookup' cache tsid
)
sidToIqMap :: (Unexceptional m) =>
(XMPP.IQ -> FilePath -> UIO ())
-> (XMPP.IQ -> UIO ())
-> m (
JingleSID -> XMPP.IQ -> UIO (),
JingleSID -> UIO (Maybe (XMPP.IQ -> UIO ())),
JingleSID -> FilePath -> UIO ()
)
sidToIqMap notifyByIq handleUnknownSession =
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) -> do
session <- fromIO_ (Cache.lookup' cache sid)
return $ case session of
Just _ -> Nothing
Nothing -> Just handleUnknownSession
,
\(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 ())
-> (XMPP.IQ -> UIO ())
-> m (Either IOError (XMPP.IQ -> XMPP.XMPP ()))
setupJingleHandlers storePath ports hostPort logger transferDoneIq otherJingle = do
(newSession, handlerWhenUnknown, transferDone) <- sidToIqMap transferDoneIq otherJingle
(newTransport, transportDone) <- tsidToSidMap transferDone
(setSize, getSize) <- tsidToSizeMap
(fmap.fmap) (\() ->
iqSetHandler storePath hostPort
newSession handlerWhenUnknown newTransport setSize transportDone
) $
Socks5Server.start storePath ports logger getSize transportDone