diff options
Diffstat (limited to 'Presence')
-rw-r--r-- | Presence/ByteStringOperators.hs | 50 | ||||
-rw-r--r-- | Presence/ClientState.hs | 34 | ||||
-rw-r--r-- | Presence/ConfigFiles.hs | 130 | ||||
-rw-r--r-- | Presence/ConnectionKey.hs | 10 | ||||
-rw-r--r-- | Presence/ConsoleWriter.hs | 411 | ||||
-rw-r--r-- | Presence/ControlMaybe.hs | 29 | ||||
-rw-r--r-- | Presence/DNSCache.hs | 261 | ||||
-rw-r--r-- | Presence/EventUtil.hs | 83 | ||||
-rw-r--r-- | Presence/FGConsole.hs | 62 | ||||
-rw-r--r-- | Presence/GetHostByAddr.hs | 77 | ||||
-rw-r--r-- | Presence/IDMangler.hs | 68 | ||||
-rw-r--r-- | Presence/LocalPeerCred.hs | 213 | ||||
-rw-r--r-- | Presence/LockedChan.hs | 78 | ||||
-rw-r--r-- | Presence/Logging.hs | 25 | ||||
-rw-r--r-- | Presence/Nesting.hs | 88 | ||||
-rw-r--r-- | Presence/Paths.hs | 62 | ||||
-rw-r--r-- | Presence/PeerResolve.hs | 39 | ||||
-rw-r--r-- | Presence/Server.hs | 851 | ||||
-rw-r--r-- | Presence/SockAddr.hs | 13 | ||||
-rw-r--r-- | Presence/SocketLike.hs | 76 | ||||
-rw-r--r-- | Presence/UTmp.hs | 249 | ||||
-rw-r--r-- | Presence/XMPP.hs | 1461 | ||||
-rw-r--r-- | Presence/XMPPServer.hs | 1829 | ||||
-rw-r--r-- | Presence/monitortty.c | 173 |
24 files changed, 6372 insertions, 0 deletions
diff --git a/Presence/ByteStringOperators.hs b/Presence/ByteStringOperators.hs new file mode 100644 index 00000000..4453aca0 --- /dev/null +++ b/Presence/ByteStringOperators.hs | |||
@@ -0,0 +1,50 @@ | |||
1 | {-# LANGUAGE CPP #-} | ||
2 | module ByteStringOperators where | ||
3 | |||
4 | import qualified Data.ByteString as S (ByteString) | ||
5 | import Data.ByteString.Lazy.Char8 as L | ||
6 | import Control.Applicative | ||
7 | |||
8 | -- These two were imported to provide an NFData instance. | ||
9 | import qualified Data.ByteString.Lazy.Internal as L (ByteString(..)) | ||
10 | import Control.DeepSeq | ||
11 | |||
12 | |||
13 | (<++>) :: ByteString -> ByteString -> ByteString | ||
14 | (<++.>) :: ByteString -> S.ByteString -> ByteString | ||
15 | (<.++>) :: S.ByteString -> ByteString -> ByteString | ||
16 | (<.++.>) :: S.ByteString -> S.ByteString -> ByteString | ||
17 | a <++> b = L.append a b | ||
18 | a <++.> b = L.append a (fromChunks [b]) | ||
19 | a <.++> b = L.append (fromChunks [a]) b | ||
20 | a <.++.> b = fromChunks [a,b] | ||
21 | infixr 5 <.++.> | ||
22 | infixr 5 <.++> | ||
23 | infixr 5 <++> | ||
24 | infixr 5 <++.> | ||
25 | |||
26 | a <++$> b = fmap (a<++>) b | ||
27 | a <$++> b = fmap (<++>b) a | ||
28 | a <$++$> b = liftA2 (<++>) a b | ||
29 | infixr 6 <++$> | ||
30 | infixr 6 <$++> | ||
31 | infixr 6 <$++$> | ||
32 | |||
33 | Nothing <?++> b = b | ||
34 | Just a <?++> b = a <++> b | ||
35 | infixr 5 <?++> | ||
36 | |||
37 | a <++?> Nothing = a | ||
38 | a <++?> Just b = a <++> b | ||
39 | infixr 5 <++?> | ||
40 | |||
41 | bshow :: Show a => a -> ByteString | ||
42 | bshow = L.pack . show | ||
43 | |||
44 | |||
45 | #if MIN_VERSION_bytestring(0,10,0) | ||
46 | #else | ||
47 | instance NFData L.ByteString where | ||
48 | rnf L.Empty = () | ||
49 | rnf (L.Chunk _ b) = rnf b | ||
50 | #endif | ||
diff --git a/Presence/ClientState.hs b/Presence/ClientState.hs new file mode 100644 index 00000000..30a53131 --- /dev/null +++ b/Presence/ClientState.hs | |||
@@ -0,0 +1,34 @@ | |||
1 | module ClientState where | ||
2 | |||
3 | import Control.Concurrent.STM | ||
4 | import Data.Text ( Text ) | ||
5 | import Data.Int ( Int8 ) | ||
6 | import Data.Bits ( (.&.) ) | ||
7 | |||
8 | import UTmp ( ProcessID ) | ||
9 | import XMPPServer ( Stanza ) | ||
10 | |||
11 | data ClientState = ClientState | ||
12 | { clientResource :: Text | ||
13 | , clientUser :: Text | ||
14 | , clientPid :: Maybe ProcessID | ||
15 | , clientStatus :: TVar (Maybe Stanza) | ||
16 | , clientFlags :: TVar Int8 | ||
17 | } | ||
18 | |||
19 | cf_available :: Int8 | ||
20 | cf_available = 0x1 | ||
21 | cf_interested :: Int8 | ||
22 | cf_interested = 0x2 | ||
23 | |||
24 | -- | True if the client has sent an initial presence | ||
25 | clientIsAvailable :: ClientState -> STM Bool | ||
26 | clientIsAvailable c = do | ||
27 | flgs <- readTVar (clientFlags c) | ||
28 | return $ flgs .&. cf_available /= 0 | ||
29 | |||
30 | -- | True if the client has requested a roster | ||
31 | clientIsInterested :: ClientState -> STM Bool | ||
32 | clientIsInterested c = do | ||
33 | flgs <- readTVar (clientFlags c) | ||
34 | return $ flgs .&. cf_interested /= 0 | ||
diff --git a/Presence/ConfigFiles.hs b/Presence/ConfigFiles.hs new file mode 100644 index 00000000..808e6dd8 --- /dev/null +++ b/Presence/ConfigFiles.hs | |||
@@ -0,0 +1,130 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | ||
2 | module ConfigFiles where | ||
3 | |||
4 | import Data.ByteString.Lazy.Char8 as L | ||
5 | import System.Posix.User | ||
6 | import System.Posix.Files (fileExist) | ||
7 | import System.FilePath | ||
8 | import System.Directory | ||
9 | import System.IO | ||
10 | -- import System.IO.Strict | ||
11 | import System.IO.Error | ||
12 | import Control.Exception | ||
13 | import Control.Monad | ||
14 | import Control.DeepSeq | ||
15 | import ByteStringOperators () -- For NFData instance | ||
16 | import ControlMaybe | ||
17 | import Data.List (partition) | ||
18 | import Data.Maybe (catMaybes,isJust) | ||
19 | |||
20 | type User = ByteString | ||
21 | |||
22 | configDir = ".presence" | ||
23 | buddyFile = "buddies" | ||
24 | subscriberFile = "subscribers" | ||
25 | otherFile = "others" | ||
26 | pendingFile = "pending" | ||
27 | solicitedFile = "solicited" | ||
28 | |||
29 | |||
30 | configPath :: User -> String -> IO String | ||
31 | configPath user filename = do | ||
32 | ue <- getUserEntryForName (unpack user) | ||
33 | return $ (++("/"++configDir++"/"++filename)) $ homeDirectory ue | ||
34 | |||
35 | |||
36 | createConfigFile tag path = do | ||
37 | let dir = dropFileName path | ||
38 | doesDirectoryExist dir >>= flip unless (do | ||
39 | createDirectory dir | ||
40 | ) | ||
41 | withFile path WriteMode $ \h -> do | ||
42 | L.hPutStrLn h tag | ||
43 | |||
44 | addItem item tag path = | ||
45 | let doit = do | ||
46 | handle (\e -> when (isDoesNotExistError e) | ||
47 | (createConfigFile tag path >> doit)) | ||
48 | $ do exists <- fileExist path | ||
49 | if exists | ||
50 | then withFile path AppendMode $ \h -> | ||
51 | L.hPutStrLn h item | ||
52 | else withFile path WriteMode $ \h -> do | ||
53 | L.hPutStrLn h tag | ||
54 | L.hPutStrLn h item | ||
55 | in doit | ||
56 | |||
57 | |||
58 | modifyFile :: | ||
59 | (ByteString,FilePath) | ||
60 | -> ByteString | ||
61 | -> (ByteString -> IO (Maybe ByteString)) | ||
62 | -> Maybe ByteString | ||
63 | -> IO Bool -- Returns True if test function ever returned Nothing | ||
64 | modifyFile (tag,file) user test appending = configPath user file >>= doit | ||
65 | where | ||
66 | doit path = do | ||
67 | handle (\e -> if (isDoesNotExistError e) | ||
68 | then (createConfigFile tag path >> doit path) | ||
69 | else return False) | ||
70 | $ do exists <- fileExist path | ||
71 | if exists | ||
72 | then do | ||
73 | xs <- withFile path ReadMode $ \h -> do | ||
74 | contents <- L.hGetContents h | ||
75 | case L.lines contents of | ||
76 | x:xs -> mapM test xs | ||
77 | _ -> return [] | ||
78 | let (keepers,deleted) = partition isJust xs | ||
79 | withFile path WriteMode $ \h -> do | ||
80 | L.hPutStrLn h tag | ||
81 | forM_ (catMaybes keepers) (L.hPutStrLn h) | ||
82 | withJust appending (L.hPutStrLn h) | ||
83 | return . not . Prelude.null $ deleted | ||
84 | else do | ||
85 | withFile path WriteMode $ \h -> do | ||
86 | L.hPutStrLn h tag | ||
87 | withJust appending (L.hPutStrLn h) | ||
88 | return False | ||
89 | |||
90 | |||
91 | modifySolicited = modifyFile ("<? solicited ?>" , solicitedFile) | ||
92 | modifyBuddies = modifyFile ("<? buddies ?>" , buddyFile) | ||
93 | modifyOthers = modifyFile ("<? others ?>" , otherFile) | ||
94 | modifyPending = modifyFile ("<? pending ?>" , pendingFile) | ||
95 | modifySubscribers = modifyFile ("<? subscribers ?>", subscriberFile) | ||
96 | |||
97 | addBuddy :: User -> ByteString -> IO () | ||
98 | addBuddy user buddy = | ||
99 | configPath user buddyFile >>= addItem buddy "<? buddies ?>" | ||
100 | |||
101 | addSubscriber :: User -> ByteString -> IO () | ||
102 | addSubscriber user subscriber = | ||
103 | configPath user subscriberFile >>= addItem subscriber "<? subscribers ?>" | ||
104 | |||
105 | addSolicited :: User -> ByteString -> IO () | ||
106 | addSolicited user solicited = | ||
107 | configPath user solicitedFile >>= addItem solicited "<? solicited ?>" | ||
108 | |||
109 | |||
110 | getConfigList path = | ||
111 | handle (\e -> if isDoesNotExistError e then (return []) else throw e) | ||
112 | $ withFile path ReadMode $ | ||
113 | L.hGetContents | ||
114 | >=> return . Prelude.tail . L.lines | ||
115 | >=> (\a -> seq (rnf a) (return a)) | ||
116 | |||
117 | getBuddies :: User -> IO [ByteString] | ||
118 | getBuddies user = configPath user buddyFile >>= getConfigList | ||
119 | |||
120 | getSubscribers :: User -> IO [ByteString] | ||
121 | getSubscribers user = configPath user subscriberFile >>= getConfigList | ||
122 | |||
123 | getOthers :: User -> IO [ByteString] | ||
124 | getOthers user = configPath user otherFile >>= getConfigList | ||
125 | |||
126 | getPending :: User -> IO [ByteString] | ||
127 | getPending user = configPath user pendingFile >>= getConfigList | ||
128 | |||
129 | getSolicited :: User -> IO [ByteString] | ||
130 | getSolicited user = configPath user solicitedFile >>= getConfigList | ||
diff --git a/Presence/ConnectionKey.hs b/Presence/ConnectionKey.hs new file mode 100644 index 00000000..944f4f6f --- /dev/null +++ b/Presence/ConnectionKey.hs | |||
@@ -0,0 +1,10 @@ | |||
1 | module ConnectionKey where | ||
2 | |||
3 | import Network.Socket ( SockAddr(..) ) | ||
4 | import SockAddr () | ||
5 | |||
6 | data ConnectionKey | ||
7 | = PeerKey { callBackAddress :: SockAddr } | ||
8 | | ClientKey { localAddress :: SockAddr } | ||
9 | deriving (Show, Ord, Eq) | ||
10 | |||
diff --git a/Presence/ConsoleWriter.hs b/Presence/ConsoleWriter.hs new file mode 100644 index 00000000..e755b27f --- /dev/null +++ b/Presence/ConsoleWriter.hs | |||
@@ -0,0 +1,411 @@ | |||
1 | {-# LANGUAGE CPP #-} | ||
2 | {-# LANGUAGE OverloadedStrings #-} | ||
3 | {-# LANGUAGE RankNTypes #-} | ||
4 | module ConsoleWriter | ||
5 | ( ConsoleWriter(cwPresenceChan) | ||
6 | , newConsoleWriter | ||
7 | , writeActiveTTY | ||
8 | , writeAllPty | ||
9 | , cwClients | ||
10 | ) where | ||
11 | |||
12 | import Control.Monad | ||
13 | -- import Control.Applicative | ||
14 | import Control.Concurrent | ||
15 | import Control.Concurrent.STM | ||
16 | import Data.Monoid | ||
17 | import Data.Char | ||
18 | import Data.Maybe | ||
19 | import System.Environment hiding (setEnv) | ||
20 | import System.Process ( rawSystem ) | ||
21 | import System.Exit ( ExitCode(ExitSuccess) ) | ||
22 | import System.Posix.Env ( setEnv ) | ||
23 | import System.Posix.Process ( forkProcess, exitImmediately, executeFile ) | ||
24 | import System.Posix.User ( setUserID, getUserEntryForName, userID ) | ||
25 | import System.Posix.Files ( getFileStatus, fileMode ) | ||
26 | import System.INotify ( initINotify, EventVariety(Modify), addWatch ) | ||
27 | import Data.Word ( Word8 ) | ||
28 | import Data.Text ( Text ) | ||
29 | import Data.Map ( Map ) | ||
30 | import Data.List ( foldl', groupBy ) | ||
31 | import Data.Bits ( (.&.) ) | ||
32 | import qualified Data.Map as Map | ||
33 | import qualified Data.Traversable as Traversable | ||
34 | import qualified Data.Text as Text | ||
35 | -- import qualified Data.Text.IO as Text | ||
36 | import qualified Network.BSD as BSD | ||
37 | |||
38 | import UTmp ( users2, utmp_file, UtmpRecord(..), UT_Type(..) ) | ||
39 | import FGConsole ( monitorTTY ) | ||
40 | import XMPPServer ( Stanza, makePresenceStanza, JabberShow(..), stanzaType | ||
41 | , LangSpecificMessage(..), msgLangMap, cloneStanza, stanzaFrom ) | ||
42 | import ControlMaybe ( handleIO_ ) | ||
43 | import ClientState | ||
44 | |||
45 | data ConsoleWriter = ConsoleWriter | ||
46 | { cwPresenceChan :: TMVar (ClientState,Stanza) | ||
47 | -- ^ tty switches and logins are announced on this mvar | ||
48 | , csActiveTTY :: TVar Word8 | ||
49 | , csUtmp :: TVar (Map Text (TVar (Maybe UtmpRecord))) | ||
50 | , cwClients :: TVar (Map Text ClientState) | ||
51 | -- ^ This 'TVar' holds a map from resource id (tty name) | ||
52 | -- to ClientState for all active TTYs and PTYs. | ||
53 | } | ||
54 | |||
55 | tshow :: forall a. Show a => a -> Text | ||
56 | tshow x = Text.pack . show $ x | ||
57 | |||
58 | retryWhen :: forall b. STM b -> (b -> Bool) -> STM b | ||
59 | retryWhen var pred = do | ||
60 | value <- var | ||
61 | if pred value then retry | ||
62 | else return value | ||
63 | |||
64 | |||
65 | onLogin :: | ||
66 | forall t. | ||
67 | ConsoleWriter | ||
68 | -> (STM (Word8, Maybe UtmpRecord) | ||
69 | -> TVar (Maybe UtmpRecord) -> IO ()) | ||
70 | -> t | ||
71 | -> IO () | ||
72 | onLogin cs start = \e -> do | ||
73 | us <- UTmp.users2 | ||
74 | let (m,cruft) = | ||
75 | foldl' (\(m,cruft) x -> | ||
76 | case utmpType x of | ||
77 | USER_PROCESS | ||
78 | -> (Map.insert (utmpTty x) x m,cruft) | ||
79 | DEAD_PROCESS | utmpPid x /= 0 | ||
80 | -> (m,Map.insert (utmpTty x) x cruft) | ||
81 | _ -> (m,cruft)) | ||
82 | (Map.empty,Map.empty) | ||
83 | us | ||
84 | forM_ (Map.elems cruft) $ \c -> do | ||
85 | putStrLn $ "cruft " ++ show (utmpTty c, utmpPid c,utmpHost c, utmpRemoteAddr c) | ||
86 | newborn <- atomically $ do | ||
87 | old <- readTVar (csUtmp cs) -- swapTVar (csUtmp cs) m | ||
88 | newborn <- flip Traversable.mapM (m Map.\\ old) | ||
89 | $ newTVar . Just | ||
90 | updated <- let upd v u = writeTVar v $ Just u | ||
91 | in Traversable.sequence $ Map.intersectionWith upd old m | ||
92 | let dead = old Map.\\ m | ||
93 | Traversable.mapM (flip writeTVar Nothing) dead | ||
94 | writeTVar (csUtmp cs) $ (old `Map.union` newborn) Map.\\ dead | ||
95 | return newborn | ||
96 | let getActive = do | ||
97 | tty <- readTVar $ csActiveTTY cs | ||
98 | utmp <- readTVar $ csUtmp cs | ||
99 | flip (maybe $ return (tty,Nothing)) | ||
100 | (Map.lookup ("tty"<>tshow tty) utmp) | ||
101 | $ \tuvar -> do | ||
102 | tu <- readTVar tuvar | ||
103 | return (tty,tu) | ||
104 | |||
105 | forM_ (Map.elems newborn) $ | ||
106 | forkIO . start getActive | ||
107 | -- forM_ (Map.elems dead ) $ putStrLn . ("gone: "++) . show | ||
108 | |||
109 | -- | Sets up threads to monitor tty switches and logins that are | ||
110 | -- written to the system utmp file and returns a 'ConsoleWriter' | ||
111 | -- object for interacting with that information. | ||
112 | newConsoleWriter :: IO ConsoleWriter | ||
113 | newConsoleWriter = do | ||
114 | chan <- atomically $ newEmptyTMVar | ||
115 | cs <- atomically $ do | ||
116 | ttyvar <- newTVar 0 | ||
117 | utmpvar <- newTVar Map.empty | ||
118 | clients <- newTVar Map.empty | ||
119 | return $ ConsoleWriter { cwPresenceChan = chan | ||
120 | , csActiveTTY = ttyvar | ||
121 | , csUtmp = utmpvar | ||
122 | , cwClients = clients | ||
123 | } | ||
124 | outvar <- atomically $ newTMVar () | ||
125 | let logit outvar s = do | ||
126 | {- | ||
127 | atomically $ takeTMVar outvar | ||
128 | Text.putStrLn s | ||
129 | atomically $ putTMVar outvar () | ||
130 | -} | ||
131 | return () | ||
132 | onTTY outvar cs vtnum = do | ||
133 | logit outvar $ "switch: " <> tshow vtnum | ||
134 | atomically $ writeTVar (csActiveTTY cs) vtnum | ||
135 | |||
136 | inotify <- initINotify | ||
137 | |||
138 | -- get active tty | ||
139 | mtty <- monitorTTY (onTTY outvar cs) | ||
140 | atomically $ retryWhen (readTVar $ csActiveTTY cs) (==0) | ||
141 | |||
142 | -- read utmp | ||
143 | onLogin cs (newCon (logit outvar) cs) Modify | ||
144 | |||
145 | -- monitor utmp | ||
146 | wd <- addWatch | ||
147 | inotify | ||
148 | [Modify] -- [CloseWrite,Open,Close,Access,Modify,Move] | ||
149 | utmp_file | ||
150 | (onLogin cs (newCon (logit outvar) cs)) | ||
151 | return cs | ||
152 | |||
153 | -- Transforms a string of form language[_territory][.codeset][@modifier] | ||
154 | -- typically used in LC_ locale variables into the BCP 47 | ||
155 | -- language codes used in xml:lang attributes. | ||
156 | toBCP47 :: [Char] -> [Char] | ||
157 | toBCP47 lang = map hyphen $ takeWhile (/='.') lang | ||
158 | where hyphen '_' = '-' | ||
159 | hyphen c = c | ||
160 | |||
161 | #if MIN_VERSION_base(4,6,0) | ||
162 | #else | ||
163 | lookupEnv k = fmap (lookup k) getEnvironment | ||
164 | #endif | ||
165 | |||
166 | getPreferedLang :: IO Text | ||
167 | getPreferedLang = do | ||
168 | lang <- do | ||
169 | lc_all <- lookupEnv "LC_ALL" | ||
170 | lc_messages <- lookupEnv "LC_MESSAGES" | ||
171 | lang <- lookupEnv "LANG" | ||
172 | return $ lc_all `mplus` lc_messages `mplus` lang | ||
173 | return $ maybe "en" (Text.pack . toBCP47) lang | ||
174 | |||
175 | cimatch :: Text -> Text -> Bool | ||
176 | cimatch w t = Text.toLower w == Text.toLower t | ||
177 | |||
178 | cimatches :: Text -> [Text] -> [Text] | ||
179 | cimatches w ts = dropWhile (not . cimatch w) ts | ||
180 | |||
181 | -- rfc4647 lookup of best match language tag | ||
182 | lookupLang :: [Text] -> [Text] -> Maybe Text | ||
183 | lookupLang (w:ws) tags | ||
184 | | Text.null w = lookupLang ws tags | ||
185 | | otherwise = case cimatches w tags of | ||
186 | (t:_) -> Just t | ||
187 | [] -> lookupLang (reduce w:ws) tags | ||
188 | where | ||
189 | reduce w = Text.concat $ reverse nopriv | ||
190 | where | ||
191 | rparts = reverse . init $ Text.groupBy (\_ c -> c/='-') w | ||
192 | nopriv = dropWhile ispriv rparts | ||
193 | ispriv t = Text.length t == 2 && Text.head t == '-' | ||
194 | |||
195 | lookupLang [] tags | "" `elem` tags = Just "" | ||
196 | | otherwise = listToMaybe $ tags | ||
197 | |||
198 | |||
199 | messageText :: Stanza -> IO Text | ||
200 | messageText msg = do | ||
201 | pref <- getPreferedLang | ||
202 | let m = msgLangMap (stanzaType msg) | ||
203 | key = lookupLang [pref] (map fst m) | ||
204 | choice = do | ||
205 | k <- key | ||
206 | lookup k m | ||
207 | flip (maybe $ return "") choice $ \choice -> do | ||
208 | let subj = fmap ("Subject: " <>) $ msgSubject choice | ||
209 | ts = catMaybes [subj, msgBody choice] | ||
210 | return $ Text.intercalate "\n\n" ts | ||
211 | |||
212 | readEnvFile :: String -> FilePath -> IO (Maybe String) | ||
213 | readEnvFile var file = fmap parse $ readFile file | ||
214 | where | ||
215 | parse xs = listToMaybe $ map (drop 1 . concat . drop 1) $ filter ofinterest bs | ||
216 | where | ||
217 | bs = map (groupBy (\_ x -> x/='=')) $ split (/='\0') xs | ||
218 | ofinterest (k:vs) | k==var = True | ||
219 | ofinterest _ = False | ||
220 | |||
221 | split pred xs = take 1 gs ++ map (drop 1) (drop 1 gs) | ||
222 | where | ||
223 | gs = groupBy (\_ x -> pred x) xs | ||
224 | |||
225 | -- | Delivers an XMPP message stanza to the currently active | ||
226 | -- tty. If that is a linux console, it will write to it similar | ||
227 | -- to the manner of the BSD write command. If that is an X11 | ||
228 | -- display, it will attempt to notify the user via a libnotify | ||
229 | -- interface. | ||
230 | writeActiveTTY :: ConsoleWriter -> Stanza -> IO Bool | ||
231 | writeActiveTTY cw msg = do | ||
232 | putStrLn $ "writeActiveTTY" | ||
233 | -- TODO: Do not deliver if the detination user does not own the active tty! | ||
234 | (tty, mbu) <- atomically $ do | ||
235 | num <- readTVar $ csActiveTTY cw | ||
236 | utmp <- readTVar $ csUtmp cw | ||
237 | mbu <- maybe (return Nothing) readTVar | ||
238 | $ Map.lookup ("tty"<>tshow num) utmp | ||
239 | return ( "/dev/tty" <> tshow num | ||
240 | , mbu ) | ||
241 | flip (maybe $ return False) mbu $ \utmp -> do | ||
242 | display <- fmap (fmap Text.pack) | ||
243 | $ readEnvFile "DISPLAY" ("/proc/" ++ show (utmpPid utmp) ++ "/environ") | ||
244 | case fmap (==utmpHost utmp) display of | ||
245 | Just True -> deliverGUIMessage cw tty utmp msg | ||
246 | _ -> deliverTerminalMessage cw tty utmp msg | ||
247 | |||
248 | deliverGUIMessage :: | ||
249 | forall t t1. t -> t1 -> UtmpRecord -> Stanza -> IO Bool | ||
250 | deliverGUIMessage cw tty utmp msg = do | ||
251 | text <- do | ||
252 | t <- messageText msg | ||
253 | return $ Text.unpack | ||
254 | $ case stanzaFrom msg of | ||
255 | Just from -> from <> ": " <> t | ||
256 | Nothing -> t | ||
257 | putStrLn $ "deliverGUI: " ++ text | ||
258 | handleIO_ (return False) $ do | ||
259 | uentry <- getUserEntryForName (Text.unpack $ utmpUser utmp) | ||
260 | let display = Text.unpack $ utmpHost utmp | ||
261 | pid <- forkProcess $ do | ||
262 | setUserID (userID uentry) | ||
263 | setEnv "DISPLAY" display True | ||
264 | -- rawSystem "/usr/bin/notify-send" [text] | ||
265 | executeFile "/usr/bin/notify-send" False [text] (Just [("DISPLAY",display)]) | ||
266 | exitImmediately ExitSuccess | ||
267 | return True | ||
268 | |||
269 | crlf :: Text -> Text | ||
270 | crlf t = Text.unlines $ map cr (Text.lines t) | ||
271 | where | ||
272 | cr t | Text.last t == '\r' = t | ||
273 | | otherwise = t <> "\r" | ||
274 | |||
275 | deliverTerminalMessage :: | ||
276 | forall t t1. t -> Text -> t1 -> Stanza -> IO Bool | ||
277 | deliverTerminalMessage cw tty utmp msg = do | ||
278 | mode <- fmap fileMode (getFileStatus $ Text.unpack tty) | ||
279 | let mesgy = mode .&. 0o020 /= 0 -- verify mode g+w | ||
280 | if not mesgy then return False else do | ||
281 | text <- do | ||
282 | t <- messageText msg | ||
283 | return $ Text.unpack | ||
284 | $ case stanzaFrom msg of | ||
285 | Just from -> "\r\n" <> from <> " says...\r\n" <> crlf t <> "\r\n" | ||
286 | Nothing -> crlf t <> "\r\n" | ||
287 | writeFile (Text.unpack tty) text | ||
288 | return True -- return True if a message was delivered | ||
289 | |||
290 | -- | Deliver the given message to all a user's PTYs. | ||
291 | writeAllPty :: ConsoleWriter -> Stanza -> IO Bool | ||
292 | writeAllPty cw msg = do | ||
293 | -- TODO: filter only ptys owned by the destination user. | ||
294 | us <- atomically $ readTVar (csUtmp cw) | ||
295 | let ptys = Map.filterWithKey ispty us | ||
296 | ispty k _ = "pts/" `Text.isPrefixOf` k | ||
297 | && Text.all isDigit (Text.drop 4 k) | ||
298 | bs <- forM (Map.toList ptys) $ \(tty,utmp) -> do | ||
299 | deliverTerminalMessage cw ("/dev/" <> tty) utmp msg | ||
300 | return $ or bs | ||
301 | |||
302 | resource :: UtmpRecord -> Text | ||
303 | resource u = | ||
304 | case utmpTty u of | ||
305 | s | Text.take 3 s == "tty" -> s | ||
306 | s | Text.take 4 s == "pts/" -> "pty" <> Text.drop 4 s <> ":" <> utmpHost u | ||
307 | s -> escapeR s <> ":" <> utmpHost u | ||
308 | where | ||
309 | escapeR s = s | ||
310 | |||
311 | textHostName :: IO Text | ||
312 | textHostName = fmap Text.pack BSD.getHostName | ||
313 | |||
314 | ujid :: UtmpRecord -> IO Text | ||
315 | ujid u = do | ||
316 | h <- textHostName | ||
317 | return $ utmpUser u <> "@" <> h <> "/" <> resource u | ||
318 | |||
319 | newCon :: (Text -> IO ()) | ||
320 | -> ConsoleWriter | ||
321 | -> STM (Word8,Maybe UtmpRecord) | ||
322 | -> TVar (Maybe UtmpRecord) | ||
323 | -> IO () | ||
324 | newCon log cw activeTTY utmp = do | ||
325 | ((tty,tu),u) <- atomically $ | ||
326 | liftM2 (,) activeTTY | ||
327 | (readTVar utmp) | ||
328 | flip (maybe $ return ()) u $ \u -> do | ||
329 | jid <- ujid u | ||
330 | log $ status (resource u) tty tu <> " " <> jid <> " pid=" <> tshow (utmpPid u) | ||
331 | <> (if istty (resource u) | ||
332 | then " host=" <> tshow (utmpHost u) | ||
333 | else "") | ||
334 | <> " session=" <> tshow (utmpSession u) | ||
335 | <> " addr=" <> tshow (utmpRemoteAddr u) | ||
336 | let r = resource u | ||
337 | stanza <- makePresenceStanza | ||
338 | "jabber:client" | ||
339 | (Just jid) | ||
340 | (jstatus r tty tu) | ||
341 | statusv <- atomically $ newTVar (Just stanza) | ||
342 | flgs <- atomically $ newTVar 0 | ||
343 | let client = ClientState { clientResource = r | ||
344 | , clientUser = utmpUser u | ||
345 | , clientPid = Nothing | ||
346 | , clientStatus = statusv | ||
347 | , clientFlags = flgs } | ||
348 | atomically $ do | ||
349 | modifyTVar (cwClients cw) $ Map.insert r client | ||
350 | putTMVar (cwPresenceChan cw) (client,stanza) | ||
351 | loop client tty tu (Just u) | ||
352 | where | ||
353 | bstatus r ttynum mtu | ||
354 | = r == ttystr | ||
355 | || match mtu | ||
356 | where ttystr = "tty" <> tshow ttynum | ||
357 | searchstr mtu = maybe ttystr utmpHost $ do | ||
358 | tu <- mtu | ||
359 | guard (not $ Text.null $ utmpHost tu) | ||
360 | return tu | ||
361 | match mtu = searchstr mtu `Text.isInfixOf` Text.dropWhile (/=':') r | ||
362 | jstatus r ttynum tu = | ||
363 | if bstatus r ttynum tu | ||
364 | then Available | ||
365 | else Away | ||
366 | status r ttynum tu = tshow $ jstatus r ttynum tu | ||
367 | |||
368 | istty r = fst3 == "tty" && Text.all isDigit rst | ||
369 | where | ||
370 | (fst3,rst) = Text.splitAt 3 r | ||
371 | |||
372 | loop client tty tu u = do | ||
373 | what <- atomically $ foldr1 orElse | ||
374 | [ do (tty',tu') <- retryWhen activeTTY | ||
375 | (\ttyu -> bstatus r tty tu == uncurry (bstatus r) ttyu) | ||
376 | return $ ttyChanged tty' tu' | ||
377 | , do u' <- retryWhen (readTVar utmp) (==u) | ||
378 | return $ utmpChanged u' | ||
379 | ] | ||
380 | what | ||
381 | where | ||
382 | r = maybe "" resource u | ||
383 | |||
384 | ttyChanged tty' tu' = do | ||
385 | jid <- maybe (return "") ujid u | ||
386 | stanza <- makePresenceStanza | ||
387 | "jabber:client" | ||
388 | (Just jid) | ||
389 | (jstatus r tty' tu') | ||
390 | dup <- cloneStanza stanza | ||
391 | atomically $ do | ||
392 | writeTVar (clientStatus client) $ Just dup | ||
393 | putTMVar (cwPresenceChan cw) (client,stanza) | ||
394 | log $ status r tty' tu' <> " " <> jid | ||
395 | loop client tty' tu' u | ||
396 | |||
397 | utmpChanged u' = maybe dead changed u' | ||
398 | where | ||
399 | changed u' = do | ||
400 | jid0 <- maybe (return "") ujid u | ||
401 | jid <- ujid u' | ||
402 | log $ "changed: " <> jid0 <> " --> " <> jid | ||
403 | loop client tty tu (Just u') | ||
404 | dead = do | ||
405 | jid <- maybe (return "") ujid u | ||
406 | stanza <- makePresenceStanza "jabber:client" (Just jid) Offline | ||
407 | atomically $ do | ||
408 | modifyTVar (cwClients cw) $ Map.delete (clientResource client) | ||
409 | putTMVar (cwPresenceChan cw) (client,stanza) | ||
410 | log $ "Offline " <> jid | ||
411 | |||
diff --git a/Presence/ControlMaybe.hs b/Presence/ControlMaybe.hs new file mode 100644 index 00000000..659dab74 --- /dev/null +++ b/Presence/ControlMaybe.hs | |||
@@ -0,0 +1,29 @@ | |||
1 | {-# LANGUAGE ScopedTypeVariables #-} | ||
2 | module ControlMaybe where | ||
3 | |||
4 | -- import GHC.IO.Exception (IOException(..)) | ||
5 | import Control.Exception as Exception (IOException(..),catch) | ||
6 | |||
7 | |||
8 | withJust :: Monad m => Maybe x -> (x -> m ()) -> m () | ||
9 | withJust (Just x) f = f x | ||
10 | withJust Nothing f = return () | ||
11 | |||
12 | whenJust :: Monad m => m (Maybe x) -> (x -> m ()) -> m () | ||
13 | whenJust acn f = do | ||
14 | x <- acn | ||
15 | withJust x f | ||
16 | |||
17 | |||
18 | catchIO_ :: IO a -> IO a -> IO a | ||
19 | catchIO_ a h = Exception.catch a (\(_ :: IOException) -> h) | ||
20 | |||
21 | catchIO :: IO a -> (IOException -> IO a) -> IO a | ||
22 | catchIO body handler = Exception.catch body handler | ||
23 | |||
24 | handleIO_ :: IO a -> IO a -> IO a | ||
25 | handleIO_ = flip catchIO_ | ||
26 | |||
27 | |||
28 | handleIO :: (IOException -> IO a) -> IO a -> IO a | ||
29 | handleIO = flip catchIO | ||
diff --git a/Presence/DNSCache.hs b/Presence/DNSCache.hs new file mode 100644 index 00000000..4a936d57 --- /dev/null +++ b/Presence/DNSCache.hs | |||
@@ -0,0 +1,261 @@ | |||
1 | -- | Both 'getAddrInfo' and 'getHostByAddr' have hard-coded timeouts for | ||
2 | -- waiting upon network queries that can be a little too long for some use | ||
3 | -- cases. This module wraps both of them so that they block for at most one | ||
4 | -- second. It caches late-arriving results so that they can be returned by | ||
5 | -- repeated timed-out queries. | ||
6 | -- | ||
7 | -- In order to achieve the shorter timeout, it is likely that the you will need | ||
8 | -- to build with GHC's -threaded option. Otherwise, if the wrapped FFI calls | ||
9 | -- to resolve the address will block Haskell threads. Note: I didn't verify | ||
10 | -- this. | ||
11 | {-# LANGUAGE TupleSections #-} | ||
12 | {-# LANGUAGE RankNTypes #-} | ||
13 | module DNSCache | ||
14 | ( DNSCache | ||
15 | , reverseResolve | ||
16 | , forwardResolve | ||
17 | , newDNSCache | ||
18 | , parseAddress | ||
19 | , strip_brackets | ||
20 | , withPort | ||
21 | ) where | ||
22 | |||
23 | import Control.Concurrent | ||
24 | import Control.Concurrent.STM | ||
25 | import Data.Text ( Text ) | ||
26 | import Network.Socket ( SockAddr(..), AddrInfoFlag(..), defaultHints, getAddrInfo, AddrInfo(..) ) | ||
27 | import Data.Time.Clock ( UTCTime, getCurrentTime, diffUTCTime ) | ||
28 | import System.IO.Error ( isDoesNotExistError ) | ||
29 | import System.Endian ( fromBE32, toBE32 ) | ||
30 | import Control.Exception ( handle, ErrorCall(..) ) | ||
31 | import Data.Map ( Map ) | ||
32 | import qualified Data.Map as Map | ||
33 | import qualified Network.BSD as BSD | ||
34 | import qualified Data.Text as Text | ||
35 | import Control.Monad | ||
36 | import Data.Function | ||
37 | import Data.List | ||
38 | import Data.Ord | ||
39 | import Data.Maybe | ||
40 | |||
41 | import SockAddr () | ||
42 | import ControlMaybe ( handleIO_ ) | ||
43 | import GetHostByAddr ( getHostByAddr ) | ||
44 | |||
45 | type TimeStamp = UTCTime | ||
46 | |||
47 | data DNSCache = | ||
48 | DNSCache | ||
49 | { fcache :: TVar (Map Text [(TimeStamp, SockAddr)]) | ||
50 | , rcache :: TVar (Map SockAddr [(TimeStamp, Text)]) | ||
51 | } | ||
52 | |||
53 | |||
54 | newDNSCache :: IO DNSCache | ||
55 | newDNSCache = do | ||
56 | fcache <- newTVarIO Map.empty | ||
57 | rcache <- newTVarIO Map.empty | ||
58 | return DNSCache { fcache=fcache, rcache=rcache } | ||
59 | |||
60 | updateCache :: Eq x => | ||
61 | Bool -> TimeStamp -> [x] -> Maybe [(TimeStamp,x)] -> Maybe [(TimeStamp,x)] | ||
62 | updateCache withScrub utc xs mys = do | ||
63 | let ys = maybe [] id mys | ||
64 | ys' = filter scrub ys | ||
65 | ys'' = map (utc,) xs ++ ys' | ||
66 | minute = 60 | ||
67 | scrub (t,x) | withScrub && diffUTCTime utc t < minute = False | ||
68 | scrub (t,x) | x `elem` xs = False | ||
69 | scrub _ = True | ||
70 | guard $ not (null ys'') | ||
71 | return ys'' | ||
72 | |||
73 | dnsObserve :: DNSCache -> Bool -> TimeStamp -> [(Text,SockAddr)] -> STM () | ||
74 | dnsObserve dns withScrub utc obs = do | ||
75 | f <- readTVar $ fcache dns | ||
76 | r <- readTVar $ rcache dns | ||
77 | let obs' = map (\(n,a)->(n,a `withPort` 0)) obs | ||
78 | gs = do | ||
79 | g <- groupBy ((==) `on` fst) $ sortBy (comparing fst) obs' | ||
80 | (n,_) <- take 1 g | ||
81 | return (n,map snd g) | ||
82 | f' = foldl' updatef f gs | ||
83 | hs = do | ||
84 | h <- groupBy ((==) `on` snd) $ sortBy (comparing snd) obs' | ||
85 | (_,a) <- take 1 h | ||
86 | return (a,map fst h) | ||
87 | r' = foldl' updater r hs | ||
88 | writeTVar (fcache dns) f' | ||
89 | writeTVar (rcache dns) r' | ||
90 | where | ||
91 | updatef f (n,addrs) = Map.alter (updateCache withScrub utc addrs) n f | ||
92 | updater r (a,ns) = Map.alter (updateCache withScrub utc ns) a r | ||
93 | |||
94 | make6mapped4 :: SockAddr -> SockAddr | ||
95 | make6mapped4 addr@(SockAddrInet6 {}) = addr | ||
96 | make6mapped4 addr@(SockAddrInet port a) = SockAddrInet6 port 0 (0,0,0xFFFF,fromBE32 a) 0 | ||
97 | |||
98 | -- Attempt to resolve the given domain name. Returns an empty list if the | ||
99 | -- resolve operation takes longer than the timeout, but the 'DNSCache' will be | ||
100 | -- updated when the resolve completes. | ||
101 | -- | ||
102 | -- When the resolve operation does complete, any entries less than a minute old | ||
103 | -- will be overwritten with the new results. Older entries are allowed to | ||
104 | -- persist for reasons I don't understand as of this writing. (See 'updateCache') | ||
105 | rawForwardResolve :: | ||
106 | DNSCache -> (Text -> IO ()) -> Int -> Text -> IO [SockAddr] | ||
107 | rawForwardResolve dns fail timeout addrtext = do | ||
108 | r <- atomically newEmptyTMVar | ||
109 | mvar <- atomically newEmptyTMVar | ||
110 | rt <- forkOS $ resolver r mvar | ||
111 | -- TODO: System.Timeout.timeout might be more appropriate than this | ||
112 | -- hack involving throwTo (ErrorCall "Interrupteddelay"). | ||
113 | tt <- forkIO $ timer (fail addrtext) timeout r rt | ||
114 | atomically $ putTMVar mvar tt | ||
115 | atomically $ readTMVar r | ||
116 | where | ||
117 | resolver r mvar = do | ||
118 | xs <- handle (\e -> let _ = isDoesNotExistError e in return []) | ||
119 | $ do fmap (nub . map (make6mapped4 . addrAddress)) $ | ||
120 | getAddrInfo (Just $ defaultHints { addrFlags = [ AI_CANONNAME, AI_V4MAPPED ]}) | ||
121 | (Just $ Text.unpack $ strip_brackets addrtext) | ||
122 | (Just "5269") | ||
123 | did <- atomically $ tryPutTMVar r xs | ||
124 | when did $ do | ||
125 | tt <- atomically $ readTMVar mvar | ||
126 | throwTo tt (ErrorCall "Interrupted delay") | ||
127 | utc <- getCurrentTime | ||
128 | atomically $ dnsObserve dns True utc $ map (addrtext,) xs | ||
129 | return () | ||
130 | |||
131 | strip_brackets :: Text -> Text | ||
132 | strip_brackets s = | ||
133 | case Text.uncons s of | ||
134 | Just ('[',t) -> Text.takeWhile (/=']') t | ||
135 | _ -> s | ||
136 | |||
137 | |||
138 | reportTimeout :: forall a. Show a => a -> IO () | ||
139 | reportTimeout addrtext = do | ||
140 | putStrLn $ "timeout resolving: "++show addrtext | ||
141 | -- killThread rt | ||
142 | |||
143 | timer :: forall t a. IO () -> Int -> TMVar [a] -> t -> IO () | ||
144 | timer fail timeout r rt = do | ||
145 | handle (\(ErrorCall _)-> return ()) $ do | ||
146 | threadDelay timeout | ||
147 | did <- atomically $ tryPutTMVar r [] | ||
148 | when did fail | ||
149 | |||
150 | unmap6mapped4 :: SockAddr -> SockAddr | ||
151 | unmap6mapped4 addr@(SockAddrInet6 port _ (0,0,0xFFFF,a) _) = | ||
152 | SockAddrInet port (toBE32 a) | ||
153 | unmap6mapped4 addr = addr | ||
154 | |||
155 | rawReverseResolve :: | ||
156 | DNSCache -> (SockAddr -> IO ()) -> Int -> SockAddr -> IO [Text] | ||
157 | rawReverseResolve dns fail timeout addr = do | ||
158 | r <- atomically newEmptyTMVar | ||
159 | mvar <- atomically newEmptyTMVar | ||
160 | rt <- forkOS $ resolver r mvar | ||
161 | tt <- forkIO $ timer (fail addr) timeout r rt | ||
162 | atomically $ putTMVar mvar tt | ||
163 | atomically $ readTMVar r | ||
164 | where | ||
165 | resolver r mvar = | ||
166 | handleIO_ (return ()) $ do | ||
167 | ent <- getHostByAddr (unmap6mapped4 addr) -- AF_UNSPEC addr | ||
168 | let names = BSD.hostName ent : BSD.hostAliases ent | ||
169 | xs = map Text.pack $ nub names | ||
170 | forkIO $ do | ||
171 | utc <- getCurrentTime | ||
172 | atomically $ dnsObserve dns False utc $ map (,addr) xs | ||
173 | atomically $ putTMVar r xs | ||
174 | |||
175 | -- Returns expired (older than a minute) cached reverse-dns results | ||
176 | -- and removes them from the cache. | ||
177 | expiredReverse :: DNSCache -> SockAddr -> IO [Text] | ||
178 | expiredReverse dns addr = do | ||
179 | utc <- getCurrentTime | ||
180 | addr <- return $ addr `withPort` 0 | ||
181 | es <- atomically $ do | ||
182 | r <- readTVar $ rcache dns | ||
183 | let ns = maybe [] id $ Map.lookup addr r | ||
184 | minute = 60 -- seconds | ||
185 | -- XXX: Is this right? flip diffUTCTime utc returns the age of the | ||
186 | -- cache entry? | ||
187 | (es0,ns') = partition ( (>=minute) . flip diffUTCTime utc . fst ) ns | ||
188 | es = map snd es0 | ||
189 | modifyTVar' (rcache dns) $ Map.insert addr ns' | ||
190 | f <- readTVar $ fcache dns | ||
191 | let f' = foldl' (flip $ Map.alter (expire utc)) f es | ||
192 | expire utc Nothing = Nothing | ||
193 | expire utc (Just as) = if null as' then Nothing else Just as' | ||
194 | where as' = filter ( (<minute) . flip diffUTCTime utc . fst) as | ||
195 | writeTVar (fcache dns) f' | ||
196 | return es | ||
197 | return es | ||
198 | |||
199 | cachedReverse :: DNSCache -> SockAddr -> IO [Text] | ||
200 | cachedReverse dns addr = do | ||
201 | utc <- getCurrentTime | ||
202 | addr <- return $ addr `withPort` 0 | ||
203 | atomically $ do | ||
204 | r <- readTVar (rcache dns) | ||
205 | let ns = maybe [] id $ Map.lookup addr r | ||
206 | {- | ||
207 | ns' = filter ( (<minute) . flip diffUTCTime utc . fst) ns | ||
208 | minute = 60 -- seconds | ||
209 | modifyTVar' (rcache dns) $ Map.insert addr ns' | ||
210 | return $ map snd ns' | ||
211 | -} | ||
212 | return $ map snd ns | ||
213 | |||
214 | -- Returns any dns query results for the given name that were observed less | ||
215 | -- than a minute ago and updates the forward-cache to remove any results older | ||
216 | -- than that. | ||
217 | cachedForward :: DNSCache -> Text -> IO [SockAddr] | ||
218 | cachedForward dns n = do | ||
219 | utc <- getCurrentTime | ||
220 | atomically $ do | ||
221 | f <- readTVar (fcache dns) | ||
222 | let as = maybe [] id $ Map.lookup n f | ||
223 | as' = filter ( (<minute) . flip diffUTCTime utc . fst) as | ||
224 | minute = 60 -- seconds | ||
225 | modifyTVar' (fcache dns) $ Map.insert n as' | ||
226 | return $ map snd as' | ||
227 | |||
228 | -- Reverse-resolves an address to a domain name. Returns both the result of a | ||
229 | -- new query and any freshly cached results. Cache entries older than a minute | ||
230 | -- will not be returned, but will be refreshed in spawned threads so that they | ||
231 | -- may be available for the next call. | ||
232 | reverseResolve :: DNSCache -> SockAddr -> IO [Text] | ||
233 | reverseResolve dns addr = do | ||
234 | expired <- expiredReverse dns addr | ||
235 | forM_ expired $ \n -> forkIO $ do | ||
236 | rawForwardResolve dns (const $ return ()) 1000000 n | ||
237 | return () | ||
238 | xs <- rawReverseResolve dns (const $ return ()) 1000000 addr | ||
239 | cs <- cachedReverse dns addr | ||
240 | return $ xs ++ filter (not . flip elem xs) cs | ||
241 | |||
242 | -- Resolves a name, if there's no result within one second, then any cached | ||
243 | -- results that are less than a minute old are returned. | ||
244 | forwardResolve :: DNSCache -> Text -> IO [SockAddr] | ||
245 | forwardResolve dns n = do | ||
246 | as <- rawForwardResolve dns (const $ return ()) 1000000 n | ||
247 | if null as | ||
248 | then cachedForward dns n | ||
249 | else return as | ||
250 | |||
251 | parseAddress :: Text -> IO (Maybe SockAddr) | ||
252 | parseAddress addr_str = do | ||
253 | info <- getAddrInfo (Just $ defaultHints { addrFlags = [ AI_NUMERICHOST ] }) | ||
254 | (Just . Text.unpack $ addr_str) | ||
255 | (Just "0") | ||
256 | return . listToMaybe $ map addrAddress info | ||
257 | |||
258 | |||
259 | withPort :: SockAddr -> Int -> SockAddr | ||
260 | withPort (SockAddrInet _ a) port = SockAddrInet (toEnum port) a | ||
261 | withPort (SockAddrInet6 _ a b c) port = SockAddrInet6 (toEnum port) a b c | ||
diff --git a/Presence/EventUtil.hs b/Presence/EventUtil.hs new file mode 100644 index 00000000..908e09e0 --- /dev/null +++ b/Presence/EventUtil.hs | |||
@@ -0,0 +1,83 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | ||
2 | module EventUtil where | ||
3 | |||
4 | import Control.Monad | ||
5 | import Data.XML.Types as XML | ||
6 | import qualified Data.List as List | ||
7 | import Data.Text (Text) | ||
8 | |||
9 | -- getStreamName (EventBeginElement name _) = name | ||
10 | |||
11 | isEventBeginElement :: Event -> Bool | ||
12 | isEventBeginElement (EventBeginElement {}) = True | ||
13 | isEventBeginElement _ = False | ||
14 | |||
15 | isEventEndElement :: Event -> Bool | ||
16 | isEventEndElement (EventEndElement {}) = True | ||
17 | isEventEndElement _ = False | ||
18 | |||
19 | -- Note: This function ignores name space qualification | ||
20 | elementAttrs :: | ||
21 | MonadPlus m => | ||
22 | Text -> Event -> m [(Name, [Content])] | ||
23 | elementAttrs expected (EventBeginElement name attrs) | ||
24 | | nameLocalName name==expected | ||
25 | = return attrs | ||
26 | elementAttrs _ _ = mzero | ||
27 | |||
28 | streamP :: Text -> Name | ||
29 | streamP name = Name name (Just "http://etherx.jabber.org/streams") (Just "stream") | ||
30 | |||
31 | attr :: Name -> Text -> (Name,[Content]) | ||
32 | attr name value = (name,[ContentText value]) | ||
33 | |||
34 | isServerIQOf :: Event -> Text -> Bool | ||
35 | isServerIQOf (EventBeginElement name attrs) testType | ||
36 | | name=="{jabber:server}iq" | ||
37 | && matchAttrib "type" testType attrs | ||
38 | = True | ||
39 | isServerIQOf _ _ = False | ||
40 | |||
41 | isClientIQOf :: Event -> Text -> Bool | ||
42 | isClientIQOf (EventBeginElement name attrs) testType | ||
43 | | name=="{jabber:client}iq" | ||
44 | && matchAttrib "type" testType attrs | ||
45 | = True | ||
46 | isClientIQOf _ _ = False | ||
47 | |||
48 | matchAttrib :: Name -> Text -> [(Name, [Content])] -> Bool | ||
49 | matchAttrib name value attrs = | ||
50 | case List.find ( (==name) . fst) attrs of | ||
51 | Just (_,[ContentText x]) | x==value -> True | ||
52 | Just (_,[ContentEntity x]) | x==value -> True | ||
53 | _ -> False | ||
54 | |||
55 | lookupAttrib :: Name -> [(Name, [Content])] -> Maybe Text | ||
56 | lookupAttrib name attrs = | ||
57 | case List.find ( (==name) . fst) attrs of | ||
58 | Just (_,[ContentText x]) -> Just x | ||
59 | Just (_,[ContentEntity x]) -> Just x | ||
60 | _ -> Nothing | ||
61 | |||
62 | tagAttrs :: Event -> [(Name, [Content])] | ||
63 | tagAttrs (EventBeginElement _ xs) = xs | ||
64 | tagAttrs _ = [] | ||
65 | |||
66 | |||
67 | {- | ||
68 | iqTypeSet = "set" | ||
69 | iqTypeGet = "get" | ||
70 | iqTypeResult = "result" | ||
71 | iqTypeError = "error" | ||
72 | -} | ||
73 | |||
74 | |||
75 | tagName :: Event -> Name | ||
76 | tagName (EventBeginElement n _) = n | ||
77 | tagName _ = "" | ||
78 | |||
79 | closerFor :: Event -> Event | ||
80 | closerFor (EventBeginElement n _) = EventEndElement n | ||
81 | closerFor _ = error "closerFor: unsupported event" | ||
82 | |||
83 | |||
diff --git a/Presence/FGConsole.hs b/Presence/FGConsole.hs new file mode 100644 index 00000000..623fb493 --- /dev/null +++ b/Presence/FGConsole.hs | |||
@@ -0,0 +1,62 @@ | |||
1 | {-# LANGUAGE ForeignFunctionInterface #-} | ||
2 | {-# LANGUAGE ScopedTypeVariables #-} | ||
3 | module FGConsole where | ||
4 | |||
5 | import Data.Word | ||
6 | import System.Posix.IO | ||
7 | import System.Posix.Types | ||
8 | import Control.Concurrent | ||
9 | -- import GHC.IO.Handle | ||
10 | import Unsafe.Coerce | ||
11 | import Control.Exception as E | ||
12 | -- import Prelude as E | ||
13 | import Control.Monad | ||
14 | import Foreign.C.Error | ||
15 | import Foreign.C | ||
16 | |||
17 | import Logging | ||
18 | import System.Posix.Signals | ||
19 | |||
20 | -- c_monitorTTY fd = trace "c_monitorTTY" (return ()) -- (trace "WTF" todo) | ||
21 | |||
22 | foreign import ccall "monitorTTY" c_monitorTTY :: Fd -> IO () | ||
23 | foreign import ccall "closeTTY" c_closeTTY :: IO () | ||
24 | |||
25 | monitorTTY :: (Word8 -> IO ()) -> IO (Fd,ThreadId) | ||
26 | monitorTTY handler = do | ||
27 | (rfd,wfd) <- createPipe | ||
28 | thread <- forkIO $ do | ||
29 | let cleanup = do | ||
30 | trace "quitting monitorTTY thread." (return ()) | ||
31 | closeFd wfd `E.catch` \(e::IOException) -> return () | ||
32 | closeFd rfd `E.catch` \(e::IOException) -> return () | ||
33 | c_closeTTY | ||
34 | -- rh <- fdToHandle rfd | ||
35 | c_monitorTTY wfd | ||
36 | let monitor = | ||
37 | (do | ||
38 | threadWaitRead rfd | ||
39 | (cs,cnt) <- fdRead rfd 1 | ||
40 | forM_ cs (handler . unsafeCoerce {- . trace "read byte" -}) | ||
41 | monitor) | ||
42 | `E.catch` | ||
43 | \(e :: IOException) -> do | ||
44 | err <- getErrno | ||
45 | case () of | ||
46 | _ | err==eAGAIN -> monitor | ||
47 | _ | otherwise -> cleanup | ||
48 | `E.catch` | ||
49 | \(e :: AsyncException) -> cleanup | ||
50 | monitor | ||
51 | return (rfd,thread) | ||
52 | |||
53 | unmonitorTTY :: (Fd, ThreadId) -> IO () | ||
54 | unmonitorTTY (rfd,thread) = do | ||
55 | closeFd rfd | ||
56 | yield | ||
57 | killThread thread | ||
58 | raiseSignal sigUSR1 | ||
59 | -- threadDelay 1000000 | ||
60 | |||
61 | |||
62 | -- vim:ft=haskell: | ||
diff --git a/Presence/GetHostByAddr.hs b/Presence/GetHostByAddr.hs new file mode 100644 index 00000000..45bca5e9 --- /dev/null +++ b/Presence/GetHostByAddr.hs | |||
@@ -0,0 +1,77 @@ | |||
1 | {-# LANGUAGE ForeignFunctionInterface #-} | ||
2 | module GetHostByAddr where | ||
3 | |||
4 | import Network.BSD | ||
5 | import Foreign.Ptr | ||
6 | import Foreign.C.Types | ||
7 | import Foreign.Storable (Storable(..)) | ||
8 | import Foreign.Marshal.Utils (with) | ||
9 | import Foreign.Marshal.Alloc | ||
10 | import Control.Concurrent | ||
11 | import System.IO.Unsafe | ||
12 | import System.IO.Error (ioeSetErrorString, mkIOError) | ||
13 | import Network.Socket | ||
14 | import GHC.IO.Exception | ||
15 | |||
16 | |||
17 | throwNoSuchThingIfNull :: String -> String -> IO (Ptr a) -> IO (Ptr a) | ||
18 | throwNoSuchThingIfNull loc desc act = do | ||
19 | ptr <- act | ||
20 | if (ptr == nullPtr) | ||
21 | then ioError (ioeSetErrorString (mkIOError NoSuchThing loc Nothing Nothing) desc) | ||
22 | else return ptr | ||
23 | |||
24 | {-# NOINLINE lock #-} | ||
25 | lock :: MVar () | ||
26 | lock = unsafePerformIO $ newMVar () | ||
27 | |||
28 | withLock :: IO a -> IO a | ||
29 | withLock act = withMVar lock (\_ -> act) | ||
30 | |||
31 | trySysCall :: IO a -> IO a | ||
32 | trySysCall act = act | ||
33 | |||
34 | {- | ||
35 | -- The locking of gethostbyaddr is similar to gethostbyname. | ||
36 | -- | Get a 'HostEntry' corresponding to the given address and family. | ||
37 | -- Note that only IPv4 is currently supported. | ||
38 | getHostByAddr :: Family -> SockAddr -> IO HostEntry | ||
39 | getHostByAddr family addr = do | ||
40 | withSockAddr addr $ \ ptr_addr len -> withLock $ do | ||
41 | throwNoSuchThingIfNull "getHostByAddr" "no such host entry" | ||
42 | $ trySysCall $ c_gethostbyaddr ptr_addr (fromIntegral len) (packFamily family) | ||
43 | >>= peek | ||
44 | -} | ||
45 | |||
46 | |||
47 | -- The locking of gethostbyaddr is similar to gethostbyname. | ||
48 | -- | Get a 'HostEntry' corresponding to the given address and family. | ||
49 | -- Note that only IPv4 is currently supported. | ||
50 | -- getHostByAddr :: Family -> HostAddress -> IO HostEntry | ||
51 | -- getHostByAddr family addr = do | ||
52 | getHostByAddr :: SockAddr -> IO HostEntry | ||
53 | getHostByAddr (SockAddrInet port addr ) = do | ||
54 | let family = AF_INET | ||
55 | with addr $ \ ptr_addr -> withLock $ do | ||
56 | throwNoSuchThingIfNull "getHostByAddr" "no such host entry" | ||
57 | $ trySysCall $ c_gethostbyaddr ptr_addr (fromIntegral (sizeOf addr)) (packFamily family) | ||
58 | >>= peek | ||
59 | getHostByAddr (SockAddrInet6 port flow (a,b,c,d) scope) = do | ||
60 | let family = AF_INET6 | ||
61 | allocaBytes 16 $ \ ptr_addr -> do | ||
62 | pokeElemOff ptr_addr 0 a | ||
63 | pokeElemOff ptr_addr 1 b | ||
64 | pokeElemOff ptr_addr 2 c | ||
65 | pokeElemOff ptr_addr 3 d | ||
66 | withLock $ do | ||
67 | throwNoSuchThingIfNull "getHostByAddr" "no such host entry" | ||
68 | $ trySysCall $ c_gethostbyaddr ptr_addr 16 (packFamily family) | ||
69 | >>= peek | ||
70 | |||
71 | |||
72 | foreign import ccall safe "gethostbyaddr" | ||
73 | c_gethostbyaddr :: Ptr a -> CInt -> CInt -> IO (Ptr HostEntry) | ||
74 | |||
75 | |||
76 | |||
77 | -- vim:ft=haskell: | ||
diff --git a/Presence/IDMangler.hs b/Presence/IDMangler.hs new file mode 100644 index 00000000..664d4f54 --- /dev/null +++ b/Presence/IDMangler.hs | |||
@@ -0,0 +1,68 @@ | |||
1 | --------------------------------------------------------------------------- | ||
2 | -- | | ||
3 | -- Module : IDMangler | ||
4 | -- | ||
5 | -- This library is useful for generating id attributes for use in an XMPP | ||
6 | -- application. It conveniently encodes a key value for looking up context and | ||
7 | -- an original id attribute in case of forwarded messages. | ||
8 | -- | ||
9 | -- For example, an id attribute with an embedded 'XMPPServer.ConnectionKey' | ||
10 | -- for a forwarded message with an original id attribute of \"purplecfa6168a\" | ||
11 | -- might look something like this: | ||
12 | -- | ||
13 | -- > AAAAAAAAAAIBksnqOQiYmtmupcLxbXakI9zcmUl4:purplecfa6168a | ||
14 | -- | ||
15 | {-# LANGUAGE OverloadedStrings #-} | ||
16 | module IDMangler | ||
17 | ( IDMangler | ||
18 | , newIDMangler | ||
19 | , generateUniqueID | ||
20 | , unmangleId | ||
21 | ) where | ||
22 | |||
23 | import Control.Monad.STM | ||
24 | import Control.Concurrent.STM | ||
25 | import Data.Text (Text) | ||
26 | import qualified Data.Text as Text | ||
27 | import qualified Data.ByteString.Lazy as LazyByteString | ||
28 | import Data.Binary | ||
29 | import qualified Codec.Binary.Base64 as Base64 | ||
30 | import Control.Monad | ||
31 | import Data.Monoid ( (<>) ) | ||
32 | |||
33 | |||
34 | data IDMangler k | ||
35 | = IDMangler { idmCounter :: TVar Int } | ||
36 | |||
37 | newIDMangler :: IO (IDMangler k) | ||
38 | newIDMangler = do | ||
39 | nv <- atomically $ newTVar 0 | ||
40 | return $ IDMangler nv | ||
41 | |||
42 | -- | Use the given state and optional data to generate a unique id attribute | ||
43 | -- suitable for xml. To recover the optional encoded data, see 'unmangleId'. | ||
44 | generateUniqueID :: Binary k => | ||
45 | IDMangler k -- ^ the state (a counter) for ensuring uniqueness | ||
46 | -> Maybe k -- ^ optional recoverable key for context | ||
47 | -> Maybe Text -- ^ optional recoverable auxilary id attribute | ||
48 | -> IO Text -- ^ unique id attribute with encoded data | ||
49 | generateUniqueID mangler mkey mid = do | ||
50 | n <- atomically $ do | ||
51 | modifyTVar' (idmCounter mangler) (+1) | ||
52 | readTVar (idmCounter mangler) | ||
53 | let bs = encode (n,mkey) | ||
54 | base64 = Base64.encode (LazyByteString.unpack bs) | ||
55 | suf = maybe "" (":" <>) mid | ||
56 | return $ Text.pack base64 <> suf | ||
57 | |||
58 | -- | Recover data from an encoded id attribute. | ||
59 | unmangleId :: Binary k => Text -> (Maybe k, Maybe Text) | ||
60 | unmangleId encoded = (k,mid) | ||
61 | where | ||
62 | (e,postcolon) = Text.span (/=':') encoded | ||
63 | bytes = Base64.decode (Text.unpack e) | ||
64 | decoded = fmap (decode . LazyByteString.pack) bytes | ||
65 | k = decoded >>= (\(n,k) -> let _ = n :: Int in k) | ||
66 | mid = do guard (not . Text.null $ postcolon) | ||
67 | return $ Text.drop 1 postcolon | ||
68 | |||
diff --git a/Presence/LocalPeerCred.hs b/Presence/LocalPeerCred.hs new file mode 100644 index 00000000..6f7735dd --- /dev/null +++ b/Presence/LocalPeerCred.hs | |||
@@ -0,0 +1,213 @@ | |||
1 | {-# LANGUAGE ViewPatterns #-} | ||
2 | {-# LANGUAGE TupleSections #-} | ||
3 | module LocalPeerCred where | ||
4 | |||
5 | import System.Endian | ||
6 | import qualified Data.ByteString.Lazy.Char8 as L | ||
7 | -- hiding (map,putStrLn,tail,splitAt,tails,filter) | ||
8 | -- import qualified Data.ByteString.Lazy.Char8 as L (splitAt) | ||
9 | import qualified Data.ByteString.Lazy as W8 | ||
10 | import Data.List as List (tails,groupBy) | ||
11 | import System.IO ( withFile, IOMode(..)) | ||
12 | import System.Directory | ||
13 | import Control.Arrow (first) | ||
14 | import Data.Char | ||
15 | import Data.Maybe | ||
16 | import Data.Binary | ||
17 | import Data.Bits | ||
18 | import System.Posix.Types | ||
19 | import System.Posix.Files | ||
20 | import Logging | ||
21 | import SocketLike | ||
22 | import ControlMaybe | ||
23 | |||
24 | xs ?? n | n < 0 = Nothing | ||
25 | [] ?? _ = Nothing | ||
26 | (x:_) ?? 0 = Just x | ||
27 | (_:xs) ?? n = xs ?? (n-1) | ||
28 | |||
29 | parseHex bs = L.concat . parseHex' $ bs | ||
30 | where | ||
31 | parseHex' bs = | ||
32 | let (dnib,ts) = L.splitAt 2 bs | ||
33 | parseNibble x = W8.pack $ group2 toW8 (W8.unpack $ W8.map hexDigit x) | ||
34 | hexDigit d = d - (if d>0x39 then 0x37 else 0x30) | ||
35 | group2 f (x:y:ys) = f x y : group2 f ys | ||
36 | group2 _ _ = [] | ||
37 | toW8 a b = shift a 4 .|. b | ||
38 | in parseNibble dnib : | ||
39 | if L.null ts | ||
40 | then [] | ||
41 | else parseHex' ts | ||
42 | |||
43 | getLocalPeerCred' (unmap6mapped4 -> SockAddrInet portn host) = do | ||
44 | let port = fromEnum portn | ||
45 | {- trace ("tcp4 "++show(port,host)) $ -} | ||
46 | withFile "/proc/net/tcp" ReadMode (parseProcNet port host) | ||
47 | |||
48 | getLocalPeerCred' (unmap6mapped4 -> SockAddrInet6 portn flow host scope) = do | ||
49 | let port = fromEnum portn | ||
50 | (a,b,c,d) = host | ||
51 | host' = (toBE32 a, toBE32 b, toBE32 c, toBE32 d) | ||
52 | withFile "/proc/net/tcp6" ReadMode (parseProcNet port host') | ||
53 | |||
54 | getLocalPeerCred' (unmap6mapped4 -> addr@(SockAddrUnix _)) = | ||
55 | -- TODO: parse /proc/net/unix | ||
56 | -- see also: Network.Socket.getPeerCred | ||
57 | return Nothing | ||
58 | |||
59 | getLocalPeerCred sock = do | ||
60 | addr <- getPeerName sock | ||
61 | muid <- getLocalPeerCred' addr | ||
62 | case muid of | ||
63 | Just (uid,inode) -> return (Just uid) | ||
64 | Nothing -> trace "proc failed." $ fmap (validate . CUid . fromIntegral . sndOf3) (getPeerCred sock) | ||
65 | where sndOf3 (pid,uid,gid) = uid | ||
66 | where | ||
67 | validate uid = Just uid -- TODO | ||
68 | |||
69 | from16 :: Word16 -> Int | ||
70 | from16 = fromEnum | ||
71 | |||
72 | as16 :: Word16 -> Word16 | ||
73 | as16 = id | ||
74 | |||
75 | parseProcNet port host h = do | ||
76 | tcp <- L.hGetContents h -- Failed: tcp <- hFileSize h >>= hGet h . fromIntegral | ||
77 | let u = do | ||
78 | ls <- listToMaybe . tail . tails . L.lines $ tcp | ||
79 | let ws = map L.words ls | ||
80 | let rs = ( catMaybes . flip map ws $ \xs -> do | ||
81 | let ys = snd (Prelude.splitAt 1 xs) | ||
82 | localaddr <- listToMaybe ys | ||
83 | let zs = L.splitWith (==':') localaddr | ||
84 | addr <- fmap parseHex $ listToMaybe zs | ||
85 | port <- fmap (fromIntegral . as16 . decode . parseHex) $ listToMaybe $ snd (Prelude.splitAt 1 zs) | ||
86 | let ys' = snd (Prelude.splitAt 5 (tail ys)) | ||
87 | ys'' = snd (Prelude.splitAt 2 ys') | ||
88 | uid <- listToMaybe ys' | ||
89 | inode <- listToMaybe ys'' | ||
90 | let peer = (port,decode addr) | ||
91 | user = toEnum (read (L.unpack uid) ::Int) ::UserID -- CUid . fromIntegral $ (read (unpack uid)::Int) | ||
92 | return $ {-trace ("peer:"++show(peer,user,inode))-} (peer,(user,inode)) | ||
93 | ) | ||
94 | fmap snd . listToMaybe $ filter ((==(port,host)).fst) rs | ||
95 | {- trace ("found: "++show u) -} | ||
96 | u `seq` return u | ||
97 | {- | ||
98 | where | ||
99 | a === b = let r= a==b in trace ("Comparing "++show (a,b)++"-->"++show r) r | ||
100 | -} | ||
101 | |||
102 | |||
103 | -- PEER NAME: [::ffff:127.0.0.1]:34307 | ||
104 | unmap6mapped4 addr@(SockAddrInet6 port _ (0,0,0xFFFF,a) _) = SockAddrInet port (toBE32 a) | ||
105 | unmap6mapped4 addr = addr | ||
106 | |||
107 | identifyTTY :: | ||
108 | [(W8.ByteString, ProcessID)] | ||
109 | -> UserID -> W8.ByteString -> IO (Maybe W8.ByteString, Maybe CPid) | ||
110 | identifyTTY tty_pids uid inode = do | ||
111 | pid <- scanProc (show uid) (L.unpack inode) | ||
112 | -- putStrLn $ "scanProc --> "++show pid | ||
113 | flip (maybe (return (Nothing,Nothing))) pid $ \(pid,ttydev) -> do | ||
114 | tty <- ttyOrDisplay pid ttydev | ||
115 | -- putStrLn $ "users = " ++ show tty_pids | ||
116 | dts <- ttyToXorgs tty_pids | ||
117 | -- putStrLn $ "displays = " ++ show dts | ||
118 | -- putStrLn $ "tty = " ++ show tty | ||
119 | -- -- displays = [(":5",Chunk "tty7" Empty)] | ||
120 | let tty' = if take 3 tty=="tty" | ||
121 | then Just (L.pack tty) | ||
122 | else lookup (parseTty tty) (map (first parseTty) dts) | ||
123 | return (tty',Just pid) | ||
124 | where | ||
125 | parseTty :: String -> Float | ||
126 | parseTty = read . tail . dropWhile (/=':') | ||
127 | |||
128 | ttyToXorgs :: Show a => [(t, a)] -> IO [([Char], t)] | ||
129 | ttyToXorgs tty_pids = do | ||
130 | dts' <- flip mapM tty_pids $ \(tty,pid) -> do | ||
131 | cmd' <- readFile $ "/proc/"++show pid++"/cmdline" | ||
132 | case listToMaybe . words . takeWhile (/='\0') $ cmd' of | ||
133 | Nothing -> return Nothing | ||
134 | Just cmd -> do | ||
135 | if notElem cmd ["gdm-session-worker"] | ||
136 | then return Nothing | ||
137 | else do | ||
138 | display <- readDisplayVariable pid | ||
139 | return (fmap ( (,tty) . snd ) display) | ||
140 | let dts = catMaybes dts' | ||
141 | return dts | ||
142 | |||
143 | |||
144 | scanProc uid inode = do | ||
145 | contents <- getDirectoryContents "/proc" `catchIO_` return [] | ||
146 | let pids = reverse $ filter (\n -> not (null n) && isDigit (head n)) contents | ||
147 | let searchPids [] = return Nothing | ||
148 | searchPids (pid:pids) = do | ||
149 | loginuid <- fmap makeUidStr $ readFile $ "/proc/"++pid++"/loginuid" | ||
150 | if False -- (uid/=loginuid) -- this check proved bad when mcabber ran on tty3 | ||
151 | then searchPids pids | ||
152 | else do | ||
153 | -- putStrLn $ "pid "++show pid ++ " --> uid "++show loginuid | ||
154 | let loop [] = return Nothing | ||
155 | loop ("0":fds) = loop fds | ||
156 | loop (fd:fds) = do | ||
157 | handleIO_ (loop fds) $ do | ||
158 | what <- readSymbolicLink $ "/proc/"++pid++"/fd/"++fd | ||
159 | -- putStrLn $ " what= "++show what | ||
160 | if (what=="socket:["++inode++"]") | ||
161 | then do | ||
162 | tty <- readSymbolicLink $ "/proc/"++pid++"/fd/0" | ||
163 | return (Just (pid,tty)) | ||
164 | else loop fds | ||
165 | -- requires root (or same user as for pid)... | ||
166 | fds <- getDirectoryContents ("/proc/"++pid++"/fd") `catchIO_` return [] | ||
167 | mb <- loop fds | ||
168 | maybe (searchPids pids) (return . Just) mb | ||
169 | |||
170 | fmap (fmap (first (read :: String -> CPid))) $ searchPids pids | ||
171 | |||
172 | ttyOrDisplay pid ttydev = do | ||
173 | ptty <- searchParentsForTTY (show pid) ttydev | ||
174 | case ptty of | ||
175 | Just tty -> return tty | ||
176 | Nothing -> do | ||
177 | display <- readDisplayVariable pid | ||
178 | -- putStrLn $ "display = " ++ show display | ||
179 | case display of | ||
180 | Just (_,disp) -> return disp | ||
181 | _ -> return ttydev | ||
182 | |||
183 | |||
184 | readDisplayVariable pid = do | ||
185 | env <- handleIO_ (return "") | ||
186 | . readFile $ "/proc/"++show pid++"/environ" | ||
187 | let vs = unzero $ List.groupBy (\_ c->c/='\0') env | ||
188 | unzero [] = [] | ||
189 | unzero (v:vs) = v:map tail vs | ||
190 | keyvalue xs = (key,value) | ||
191 | where | ||
192 | (key,ys) = break (=='=') xs | ||
193 | value = case ys of { [] -> []; (_:ys') -> ys' } | ||
194 | display = listToMaybe | ||
195 | . filter ((=="DISPLAY").fst) | ||
196 | . map keyvalue | ||
197 | $ vs | ||
198 | return display | ||
199 | |||
200 | |||
201 | makeUidStr "4294967295" = "invalid" | ||
202 | makeUidStr uid = uid | ||
203 | |||
204 | |||
205 | searchParentsForTTY pid ttydev | take 8 ttydev == "/dev/tty" = return . Just $ drop 5 ttydev | ||
206 | searchParentsForTTY "1" ttydev | otherwise = return Nothing | ||
207 | searchParentsForTTY pid ttydev = do | ||
208 | stat <- handleIO_ (return "") . readFile $ "/proc/"++pid++"/stat" | ||
209 | case words stat ?? 3 of | ||
210 | Nothing -> return Nothing | ||
211 | Just ppid -> do | ||
212 | tty <- handleIO_ (return "") $ readSymbolicLink $ "/proc/"++ppid++"/fd/0" | ||
213 | searchParentsForTTY ppid tty | ||
diff --git a/Presence/LockedChan.hs b/Presence/LockedChan.hs new file mode 100644 index 00000000..eac2b5ad --- /dev/null +++ b/Presence/LockedChan.hs | |||
@@ -0,0 +1,78 @@ | |||
1 | {-# LANGUAGE CPP #-} | ||
2 | module LockedChan | ||
3 | ( LockedChan | ||
4 | , cloneLChan | ||
5 | , newLockedChan | ||
6 | , peekLChan | ||
7 | , unlockChan | ||
8 | , writeLChan ) | ||
9 | where | ||
10 | |||
11 | |||
12 | import Control.Monad.STM | ||
13 | import Control.Concurrent.STM | ||
14 | |||
15 | data LockedChan a = LockedChan | ||
16 | { lock :: TVar Bool | ||
17 | , chan :: TChan a | ||
18 | } | ||
19 | |||
20 | unlockChan :: LockedChan a -> IO (TChan a) | ||
21 | unlockChan c = do | ||
22 | waslocked <- atomically $ swapTVar (lock c) False | ||
23 | if waslocked | ||
24 | then return (chan c) | ||
25 | else error "Attempt to read unlocked channel" | ||
26 | |||
27 | writeLChan :: LockedChan a -> a -> STM () | ||
28 | writeLChan c a = writeTChan (chan c) a | ||
29 | |||
30 | -- This one blocks rather than throwing an exception... | ||
31 | -- todo: probably this should be changed to conform to the rest | ||
32 | -- of the api. | ||
33 | peekLChan :: LockedChan a -> STM a | ||
34 | peekLChan c = do | ||
35 | readTVar (lock c) >>= check | ||
36 | peekTChan (chan c) | ||
37 | |||
38 | newLockedChan :: STM (LockedChan a) | ||
39 | newLockedChan = do | ||
40 | lock <- newTVar True | ||
41 | chan <- newTChan | ||
42 | return $ LockedChan lock chan | ||
43 | |||
44 | cloneLChan :: LockedChan a -> IO (LockedChan a) | ||
45 | cloneLChan c = do | ||
46 | mchan <- atomically $ do | ||
47 | locked <- readTVar (lock c) | ||
48 | if locked | ||
49 | then fmap Just $ do | ||
50 | c2 <- cloneTChan (chan c) | ||
51 | l2 <- newTVar True | ||
52 | return $ LockedChan l2 c2 | ||
53 | else return Nothing | ||
54 | maybe (do putStrLn "LockedChan: Attempt to clone unlocked channel" | ||
55 | error "Attempt to clone unlocked channel") | ||
56 | return | ||
57 | mchan | ||
58 | |||
59 | #if MIN_VERSION_stm(2,4,0) | ||
60 | #else | ||
61 | -- |Clone a 'TChan': similar to dupTChan, but the cloned channel starts with the | ||
62 | -- same content available as the original channel. | ||
63 | -- | ||
64 | -- Terrible inefficient implementation provided to build against older libraries. | ||
65 | cloneTChan :: TChan a -> STM (TChan a) | ||
66 | cloneTChan chan = do | ||
67 | contents <- chanContents' chan | ||
68 | chan2 <- dupTChan chan | ||
69 | mapM_ (writeTChan chan) contents | ||
70 | return chan2 | ||
71 | where | ||
72 | chanContents' chan = do | ||
73 | b <- isEmptyTChan chan | ||
74 | if b then return [] else do | ||
75 | x <- readTChan chan | ||
76 | xs <- chanContents' chan | ||
77 | return (x:xs) | ||
78 | #endif | ||
diff --git a/Presence/Logging.hs b/Presence/Logging.hs new file mode 100644 index 00000000..b997d341 --- /dev/null +++ b/Presence/Logging.hs | |||
@@ -0,0 +1,25 @@ | |||
1 | {-# LANGUAGE RankNTypes #-} | ||
2 | module Logging where | ||
3 | |||
4 | import qualified Data.ByteString.Lazy.Char8 as L | ||
5 | import qualified Data.ByteString.Char8 as S | ||
6 | import qualified Data.Text.IO as Text | ||
7 | import qualified Data.Text as Text | ||
8 | import qualified Debug.Trace as Debug | ||
9 | |||
10 | debugL :: L.ByteString -> IO () | ||
11 | debugS :: S.ByteString -> IO () | ||
12 | debugStr :: String -> IO () | ||
13 | debugText :: Text.Text -> IO () | ||
14 | trace :: forall a. String -> a -> a | ||
15 | |||
16 | |||
17 | debugStr str = putStrLn str | ||
18 | |||
19 | debugL bs = L.putStrLn bs | ||
20 | |||
21 | debugS bs = S.putStrLn bs | ||
22 | |||
23 | debugText text = Text.putStrLn text | ||
24 | |||
25 | trace str a = Debug.trace str a | ||
diff --git a/Presence/Nesting.hs b/Presence/Nesting.hs new file mode 100644 index 00000000..720237fd --- /dev/null +++ b/Presence/Nesting.hs | |||
@@ -0,0 +1,88 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | ||
2 | {-# LANGUAGE FlexibleContexts #-} | ||
3 | module Nesting where | ||
4 | |||
5 | import Data.Conduit | ||
6 | import Data.Conduit.Lift | ||
7 | import Data.XML.Types | ||
8 | import qualified Data.Text as S | ||
9 | import Control.Monad.State.Strict | ||
10 | import qualified Data.List as List | ||
11 | |||
12 | type Lang = S.Text | ||
13 | |||
14 | data StrictList a = a :! !(StrictList a) | StrictNil | ||
15 | |||
16 | data XMLState = XMLState { | ||
17 | nestingLevel :: Int, | ||
18 | langStack :: StrictList (Int,Lang) | ||
19 | } | ||
20 | |||
21 | type NestingXML o m a = ConduitM Event o (StateT XMLState m) a | ||
22 | |||
23 | doNestingXML :: Monad m => NestingXML o m r -> ConduitM Event o m r | ||
24 | doNestingXML m = | ||
25 | evalStateC (XMLState 0 StrictNil) (trackNesting =$= m) | ||
26 | |||
27 | nesting :: Monad m => NestingXML o m Int | ||
28 | nesting = lift $ (return . nestingLevel) =<< get | ||
29 | |||
30 | xmlLang :: Monad m => NestingXML o m (Maybe Lang) | ||
31 | xmlLang = fmap (fmap snd . top . langStack) (lift get) | ||
32 | where | ||
33 | top ( a :! _as ) = Just a | ||
34 | top _ = Nothing | ||
35 | |||
36 | trackNesting :: Monad m => Conduit Event (StateT XMLState m) Event | ||
37 | trackNesting = awaitForever doit | ||
38 | where | ||
39 | doit xml = do | ||
40 | XMLState lvl langs <- lift get | ||
41 | lift . put $ case xml of | ||
42 | EventBeginElement _ attrs -> | ||
43 | case lookupLang attrs of | ||
44 | Nothing -> XMLState (lvl+1) langs | ||
45 | Just lang -> XMLState (lvl+1) ( (lvl+1,lang) :! langs) | ||
46 | EventEndElement _ -> | ||
47 | case langs of | ||
48 | (llvl,_) :! ls | llvl==lvl -> XMLState (lvl-1) ls | ||
49 | _ | otherwise -> XMLState (lvl-1) langs | ||
50 | _ -> XMLState lvl langs | ||
51 | yield xml | ||
52 | |||
53 | |||
54 | lookupLang :: [(Name, [Content])] -> Maybe S.Text | ||
55 | lookupLang attrs = | ||
56 | case List.find ( (=="xml:lang") . fst) attrs of | ||
57 | Just (_,[ContentText x]) -> Just x | ||
58 | Just (_,[ContentEntity x]) -> Just x | ||
59 | _ -> Nothing | ||
60 | |||
61 | |||
62 | awaitCloser :: Monad m => Int -> NestingXML o m () | ||
63 | awaitCloser lvl = | ||
64 | fix $ \loop -> do | ||
65 | lvl' <- nesting | ||
66 | when (lvl' >= lvl) $ do | ||
67 | xml <- await | ||
68 | maybe (return ()) (const loop) xml | ||
69 | |||
70 | withXML :: | ||
71 | Monad m => | ||
72 | (i -> ConduitM i o m ()) -> ConduitM i o m () | ||
73 | withXML f = await >>= maybe (return ()) f | ||
74 | |||
75 | nextElement :: Monad m => NestingXML o m (Maybe Event) | ||
76 | nextElement = do | ||
77 | lvl <- nesting | ||
78 | fix $ \loop -> do | ||
79 | xml <- await | ||
80 | case xml of | ||
81 | Nothing -> return Nothing | ||
82 | Just (EventBeginElement _ _) -> return xml | ||
83 | Just _ -> do | ||
84 | lvl' <- nesting | ||
85 | if (lvl'>=lvl) then loop | ||
86 | else return Nothing | ||
87 | |||
88 | |||
diff --git a/Presence/Paths.hs b/Presence/Paths.hs new file mode 100644 index 00000000..9d51b66e --- /dev/null +++ b/Presence/Paths.hs | |||
@@ -0,0 +1,62 @@ | |||
1 | {-# LANGUAGE CPP #-} | ||
2 | module Paths where | ||
3 | |||
4 | #include <paths.h> | ||
5 | |||
6 | bshell :: String | ||
7 | console :: String | ||
8 | cshell :: String | ||
9 | devdb :: String | ||
10 | devnull :: String | ||
11 | drum :: String | ||
12 | gshadow :: String | ||
13 | klog :: String | ||
14 | kmem :: String | ||
15 | lastlog :: String | ||
16 | maildir :: String | ||
17 | man :: String | ||
18 | mem :: String | ||
19 | mnttab :: String | ||
20 | mounted :: String | ||
21 | nologin :: String | ||
22 | preserve :: String | ||
23 | rwhodir :: String | ||
24 | sendmail :: String | ||
25 | shadow :: String | ||
26 | shells :: String | ||
27 | tty :: String | ||
28 | unix :: String | ||
29 | utmp :: String | ||
30 | vi :: String | ||
31 | wtmp :: String | ||
32 | |||
33 | |||
34 | |||
35 | bshell = _PATH_BSHELL | ||
36 | console = _PATH_CONSOLE | ||
37 | cshell = _PATH_CSHELL | ||
38 | devdb = _PATH_DEVDB | ||
39 | devnull = _PATH_DEVNULL | ||
40 | drum = _PATH_DRUM | ||
41 | gshadow = _PATH_GSHADOW | ||
42 | klog = _PATH_KLOG | ||
43 | kmem = _PATH_KMEM | ||
44 | lastlog = _PATH_LASTLOG | ||
45 | maildir = _PATH_MAILDIR | ||
46 | man = _PATH_MAN | ||
47 | mem = _PATH_MEM | ||
48 | mnttab = _PATH_MNTTAB | ||
49 | mounted = _PATH_MOUNTED | ||
50 | nologin = _PATH_NOLOGIN | ||
51 | preserve = _PATH_PRESERVE | ||
52 | rwhodir = _PATH_RWHODIR | ||
53 | sendmail = _PATH_SENDMAIL | ||
54 | shadow = _PATH_SHADOW | ||
55 | shells = _PATH_SHELLS | ||
56 | tty = _PATH_TTY | ||
57 | unix = _PATH_UNIX | ||
58 | utmp = _PATH_UTMP | ||
59 | vi = _PATH_VI | ||
60 | wtmp = _PATH_WTMP | ||
61 | |||
62 | |||
diff --git a/Presence/PeerResolve.hs b/Presence/PeerResolve.hs new file mode 100644 index 00000000..0854b365 --- /dev/null +++ b/Presence/PeerResolve.hs | |||
@@ -0,0 +1,39 @@ | |||
1 | module PeerResolve | ||
2 | ( peerKeyToResolvedNames | ||
3 | , resolvePeer | ||
4 | , parseAddress | ||
5 | , strip_brackets | ||
6 | , withPort | ||
7 | ) where | ||
8 | |||
9 | import Data.List ( nub ) | ||
10 | import Data.Text ( Text ) | ||
11 | import Network.Socket ( SockAddr(..) ) | ||
12 | import System.Endian ( fromBE32, toBE32 ) | ||
13 | import System.IO.Error ( isDoesNotExistError ) | ||
14 | import Control.Exception ( handle, ErrorCall(..) ) | ||
15 | import qualified Network.BSD as BSD | ||
16 | import qualified Data.Text as Text | ||
17 | import Control.Concurrent | ||
18 | import Control.Concurrent.STM | ||
19 | import Control.Monad | ||
20 | import Data.Maybe | ||
21 | import System.IO.Unsafe | ||
22 | |||
23 | import GetHostByAddr | ||
24 | import DNSCache | ||
25 | import ConnectionKey | ||
26 | import ControlMaybe | ||
27 | |||
28 | {-# NOINLINE global_dns_cache #-} | ||
29 | global_dns_cache :: DNSCache | ||
30 | global_dns_cache = unsafePerformIO $ newDNSCache | ||
31 | |||
32 | resolvePeer :: Text -> IO [SockAddr] | ||
33 | resolvePeer addrtext = forwardResolve global_dns_cache addrtext | ||
34 | |||
35 | peerKeyToResolvedNames :: ConnectionKey -> IO [Text] | ||
36 | peerKeyToResolvedNames k@(ClientKey { localAddress=addr }) = return [] | ||
37 | peerKeyToResolvedNames k@(PeerKey { callBackAddress=addr }) = do | ||
38 | reverseResolve global_dns_cache addr | ||
39 | |||
diff --git a/Presence/Server.hs b/Presence/Server.hs new file mode 100644 index 00000000..f7f99907 --- /dev/null +++ b/Presence/Server.hs | |||
@@ -0,0 +1,851 @@ | |||
1 | {-# OPTIONS_HADDOCK prune #-} | ||
2 | {-# LANGUAGE CPP #-} | ||
3 | {-# LANGUAGE StandaloneDeriving #-} | ||
4 | {-# LANGUAGE OverloadedStrings #-} | ||
5 | {-# LANGUAGE TupleSections #-} | ||
6 | {-# LANGUAGE FlexibleInstances #-} | ||
7 | ----------------------------------------------------------------------------- | ||
8 | -- | | ||
9 | -- Module : Server | ||
10 | -- | ||
11 | -- Maintainer : joe@jerkface.net | ||
12 | -- Stability : experimental | ||
13 | -- | ||
14 | -- A TCP client/server library. | ||
15 | -- | ||
16 | -- TODO: XXX: A newer version of this code is in the server.git repo. XXX | ||
17 | -- | ||
18 | -- * interface tweaks | ||
19 | -- | ||
20 | {-# LANGUAGE DoAndIfThenElse #-} | ||
21 | module Server where | ||
22 | |||
23 | import Data.ByteString (ByteString,hGetNonBlocking) | ||
24 | import qualified Data.ByteString.Char8 as S -- ( hPutStrLn, hPutStr, pack) | ||
25 | #if MIN_VERSION_containers(0,5,0) | ||
26 | import qualified Data.Map.Strict as Map | ||
27 | import Data.Map.Strict (Map) | ||
28 | #else | ||
29 | import qualified Data.Map as Map | ||
30 | import Data.Map (Map) | ||
31 | #endif | ||
32 | import Data.Monoid ( (<>) ) | ||
33 | import Control.Concurrent | ||
34 | import Control.Concurrent.STM | ||
35 | -- import Control.Concurrent.STM.TMVar | ||
36 | -- import Control.Concurrent.STM.TChan | ||
37 | -- import Control.Concurrent.STM.Delay | ||
38 | import Control.Exception ({-evaluate,-}handle,SomeException(..),bracketOnError,ErrorCall(..)) | ||
39 | import Control.Monad | ||
40 | import Control.Monad.Fix | ||
41 | -- import Control.Monad.STM | ||
42 | import Control.Monad.Trans.Resource | ||
43 | import Control.Monad.IO.Class (MonadIO (liftIO)) | ||
44 | import System.IO.Error (ioeGetErrorType,isDoesNotExistError) | ||
45 | import System.IO | ||
46 | ( IOMode(..) | ||
47 | , hSetBuffering | ||
48 | , BufferMode(..) | ||
49 | , hWaitForInput | ||
50 | , hClose | ||
51 | , hIsEOF | ||
52 | , stderr | ||
53 | , stdout | ||
54 | , Handle | ||
55 | , hFlush | ||
56 | ) | ||
57 | import Network.Socket | ||
58 | import Network.BSD | ||
59 | ( getProtocolNumber | ||
60 | ) | ||
61 | import Debug.Trace | ||
62 | import Data.Time.Clock (UTCTime,getCurrentTime,diffUTCTime) | ||
63 | import Data.Time.Format (formatTime) | ||
64 | import SockAddr () | ||
65 | -- import System.Locale (defaultTimeLocale) | ||
66 | |||
67 | todo = error "unimplemented" | ||
68 | |||
69 | type Microseconds = Int | ||
70 | type Miliseconds = Int | ||
71 | type TimeOut = Miliseconds | ||
72 | type PingInterval = Miliseconds | ||
73 | |||
74 | -- | This object is passed with the 'Listen' and 'Connect' | ||
75 | -- instructions in order to control the behavior of the | ||
76 | -- connections that are established. It is parameterized | ||
77 | -- by a user-suplied type @conkey@ that is used as a lookup | ||
78 | -- key for connections. | ||
79 | data ConnectionParameters conkey u = | ||
80 | ConnectionParameters | ||
81 | { pingInterval :: PingInterval | ||
82 | -- ^ The miliseconds of idle to allow before a 'RequiresPing' | ||
83 | -- event is signaled. | ||
84 | , timeout :: TimeOut | ||
85 | -- ^ The miliseconds of idle after 'RequiresPing' is signaled | ||
86 | -- that are necessary for the connection to be considered | ||
87 | -- lost and signalling 'EOF'. | ||
88 | , makeConnKey :: (Socket,SockAddr) -> IO (conkey,u) | ||
89 | -- ^ This action creates a lookup key for a new connection. If 'duplex' | ||
90 | -- is 'True' and the result is already assocatied with an established | ||
91 | -- connection, then an 'EOF' will be forced before the the new | ||
92 | -- connection becomes active. | ||
93 | -- | ||
94 | , duplex :: Bool | ||
95 | -- ^ If True, then the connection will be treated as a normal | ||
96 | -- two-way socket. Otherwise, a readable socket is established | ||
97 | -- with 'Listen' and a writable socket is established with | ||
98 | -- 'Connect' and they are associated when 'makeConnKey' yields | ||
99 | -- same value for each. | ||
100 | } | ||
101 | |||
102 | -- | Use this function to select appropriate default values for | ||
103 | -- 'ConnectionParameters' other than 'makeConnKey'. | ||
104 | -- | ||
105 | -- Current defaults: | ||
106 | -- | ||
107 | -- * 'pingInterval' = 28000 | ||
108 | -- | ||
109 | -- * 'timeout' = 2000 | ||
110 | -- | ||
111 | -- * 'duplex' = True | ||
112 | -- | ||
113 | connectionDefaults | ||
114 | :: ((Socket, SockAddr) -> IO (conkey,u)) -> ConnectionParameters conkey u | ||
115 | connectionDefaults f = ConnectionParameters | ||
116 | { pingInterval = 28000 | ||
117 | , timeout = 2000 | ||
118 | , makeConnKey = f | ||
119 | , duplex = True | ||
120 | } | ||
121 | |||
122 | -- | Instructions for a 'Server' object | ||
123 | -- | ||
124 | -- To issue a command, put it into the 'serverCommand' TMVar. | ||
125 | data ServerInstruction conkey u | ||
126 | = Quit | ||
127 | -- ^ kill the server. This command is automatically issued when | ||
128 | -- the server is released. | ||
129 | | Listen PortNumber (ConnectionParameters conkey u) | ||
130 | -- ^ listen for incomming connections | ||
131 | | Connect SockAddr (ConnectionParameters conkey u) | ||
132 | -- ^ connect to addresses | ||
133 | | ConnectWithEndlessRetry SockAddr | ||
134 | (ConnectionParameters conkey u) | ||
135 | Miliseconds | ||
136 | -- ^ keep retrying the connection | ||
137 | | Ignore PortNumber | ||
138 | -- ^ stop listening on specified port | ||
139 | | Send conkey ByteString | ||
140 | -- ^ send bytes to an established connection | ||
141 | |||
142 | #ifdef TEST | ||
143 | deriving instance Show conkey => Show (ServerInstruction conkey) | ||
144 | instance Show (a -> b) where show _ = "<function>" | ||
145 | deriving instance Show conkey => Show (ConnectionParameters conkey) | ||
146 | #endif | ||
147 | |||
148 | -- | This type specifies which which half of a half-duplex | ||
149 | -- connection is of interest. | ||
150 | data InOrOut = In | Out | ||
151 | deriving (Enum,Eq,Ord,Show,Read) | ||
152 | |||
153 | -- | These events may be read from 'serverEvent' TChannel. | ||
154 | -- | ||
155 | data ConnectionEvent b | ||
156 | = Got b | ||
157 | -- ^ Arrival of data from a socket | ||
158 | | Connection (STM Bool) (IO (Maybe ByteString)) (ByteString -> IO Bool) | ||
159 | -- ^ A new connection was established | ||
160 | | ConnectFailure SockAddr | ||
161 | -- ^ A 'Connect' command failed. | ||
162 | | HalfConnection InOrOut | ||
163 | -- ^ Half of a half-duplex connection is avaliable. | ||
164 | | EOF | ||
165 | -- ^ A connection was terminated | ||
166 | | RequiresPing | ||
167 | -- ^ 'pingInterval' miliseconds of idle was experienced | ||
168 | |||
169 | #ifdef TEST | ||
170 | instance Show (IO a) where show _ = "<IO action>" | ||
171 | instance Show (STM a) where show _ = "<STM action>" | ||
172 | instance Eq (ByteString -> IO Bool) where (==) _ _ = True | ||
173 | instance Eq (IO (Maybe ByteString)) where (==) _ _ = True | ||
174 | instance Eq (STM Bool) where (==) _ _ = True | ||
175 | deriving instance Show b => Show (ConnectionEvent b) | ||
176 | deriving instance Eq b => Eq (ConnectionEvent b) | ||
177 | #endif | ||
178 | |||
179 | -- | This is the per-connection state. | ||
180 | data ConnectionRecord u | ||
181 | = ConnectionRecord { ckont :: TMVar (STM (IO ())) -- ^ used to pass a continuation to update the eof-handler | ||
182 | , cstate :: ConnectionState -- ^ used to send/receive data to the connection | ||
183 | , cdata :: u -- ^ user data, stored in the connection map for convenience | ||
184 | } | ||
185 | |||
186 | -- | This object accepts commands and signals events and maintains | ||
187 | -- the list of currently listening ports and established connections. | ||
188 | data Server a u | ||
189 | = Server { serverCommand :: TMVar (ServerInstruction a u) | ||
190 | , serverEvent :: TChan ((a,u), ConnectionEvent ByteString) | ||
191 | , serverReleaseKey :: ReleaseKey | ||
192 | , conmap :: TVar (Map a (ConnectionRecord u)) | ||
193 | , listenmap :: TVar (Map PortNumber (ThreadId,Socket)) | ||
194 | , retrymap :: TVar (Map SockAddr (TVar Bool,InterruptableDelay)) | ||
195 | } | ||
196 | |||
197 | control sv = atomically . putTMVar (serverCommand sv) | ||
198 | |||
199 | -- | Construct a 'Server' object. Use 'Control.Monad.Trans.Resource.ResourceT' | ||
200 | -- to ensure proper cleanup. For example, | ||
201 | -- | ||
202 | -- > import Server | ||
203 | -- > import Control.Monad.Trans.Resource (runResourceT) | ||
204 | -- > import Control.Monad.IO.Class (liftIO) | ||
205 | -- > import Control.Monad.STM (atomically) | ||
206 | -- > import Control.Concurrent.STM.TMVar (putTMVar) | ||
207 | -- > import Control.Concurrent.STM.TChan (readTChan) | ||
208 | -- > | ||
209 | -- > main = runResourceT $ do | ||
210 | -- > sv <- server | ||
211 | -- > let params = connectionDefaults (return . snd) | ||
212 | -- > liftIO . atomically $ putTMVar (serverCommand sv) (Listen 2942 params) | ||
213 | -- > let loop = do | ||
214 | -- > (_,event) <- atomically $ readTChan (serverEvent sv) | ||
215 | -- > case event of | ||
216 | -- > Got bytes -> putStrLn $ "got: " ++ show bytes | ||
217 | -- > _ -> return () | ||
218 | -- > case event of EOF -> return () | ||
219 | -- > _ -> loop | ||
220 | -- > liftIO loop | ||
221 | server :: (Show a,Ord a, MonadIO m, MonadResource m) => m (Server a u) | ||
222 | server = do | ||
223 | (key,cmds) <- allocate (atomically newEmptyTMVar) | ||
224 | (atomically . flip putTMVar Quit) | ||
225 | server <- liftIO . atomically $ do | ||
226 | tchan <- newTChan | ||
227 | conmap <- newTVar Map.empty | ||
228 | listenmap<- newTVar Map.empty | ||
229 | retrymap <- newTVar Map.empty | ||
230 | return Server { serverCommand = cmds | ||
231 | , serverEvent = tchan | ||
232 | , serverReleaseKey = key | ||
233 | , conmap = conmap | ||
234 | , listenmap = listenmap | ||
235 | , retrymap = retrymap | ||
236 | } | ||
237 | liftIO $ do | ||
238 | forkIO $ fix $ \loop -> do | ||
239 | instr <- atomically $ takeTMVar cmds | ||
240 | -- warn $ "instr = " <> bshow instr | ||
241 | let again = do doit server instr | ||
242 | -- warn $ "finished " <> bshow instr | ||
243 | loop | ||
244 | case instr of Quit -> closeAll server | ||
245 | _ -> again | ||
246 | return server | ||
247 | where | ||
248 | closeAll server = liftIO $ do | ||
249 | listening <- atomically . readTVar $ listenmap server | ||
250 | mapM_ killListener (Map.elems listening) | ||
251 | let stopRetry (v,d) = do atomically $ writeTVar v False | ||
252 | interruptDelay d | ||
253 | retriers <- atomically $ do | ||
254 | rmap <- readTVar $ retrymap server | ||
255 | writeTVar (retrymap server) Map.empty | ||
256 | return rmap | ||
257 | mapM_ stopRetry (Map.elems retriers) | ||
258 | cons <- atomically . readTVar $ conmap server | ||
259 | atomically $ mapM_ (connClose . cstate) (Map.elems cons) | ||
260 | atomically $ mapM_ (connWait . cstate) (Map.elems cons) | ||
261 | atomically $ writeTVar (conmap server) Map.empty | ||
262 | |||
263 | |||
264 | doit server (Listen port params) = liftIO $ do | ||
265 | |||
266 | listening <- Map.member port | ||
267 | `fmap` atomically (readTVar $ listenmap server) | ||
268 | when (not listening) $ do | ||
269 | |||
270 | let family = AF_INET6 | ||
271 | |||
272 | sock <- socket family Stream 0 | ||
273 | setSocketOption sock ReuseAddr 1 | ||
274 | let address = | ||
275 | case family of | ||
276 | AF_INET -> SockAddrInet port iNADDR_ANY | ||
277 | AF_INET6 -> SockAddrInet6 port 0 iN6ADDR_ANY 0 | ||
278 | fix $ \loop -> do | ||
279 | handle (\(SomeException e)-> do | ||
280 | warn $ "BIND-ERROR:"<>bshow address <> " " <> bshow e | ||
281 | threadDelay 5000000 | ||
282 | loop) | ||
283 | $ bindSocket sock address | ||
284 | listen sock 2 | ||
285 | thread <- forkIO $ acceptLoop server params sock | ||
286 | atomically $ listenmap server `modifyTVar'` Map.insert port (thread,sock) | ||
287 | |||
288 | doit server (Ignore port) = liftIO $ do | ||
289 | mb <- atomically $ do | ||
290 | map <- readTVar $ listenmap server | ||
291 | modifyTVar' (listenmap server) $ Map.delete port | ||
292 | return $ Map.lookup port map | ||
293 | maybe (return ()) killListener $ mb | ||
294 | |||
295 | doit server (Send con bs) = liftIO $ do -- . void . forkIO $ do | ||
296 | map <- atomically $ readTVar (conmap server) | ||
297 | let post False = (trace ("cant send: "++show bs) $ return ()) | ||
298 | post True = return () | ||
299 | maybe (post False) | ||
300 | (post <=< flip connWrite bs . cstate) | ||
301 | $ Map.lookup con map | ||
302 | |||
303 | doit server (Connect addr params) = liftIO $ do | ||
304 | mb <- atomically $ do | ||
305 | rmap <- readTVar (retrymap server) | ||
306 | return $ Map.lookup addr rmap | ||
307 | maybe forkit | ||
308 | (\(v,d) -> do b <- atomically $ readTVar v | ||
309 | interruptDelay d | ||
310 | when (not b) forkit) | ||
311 | mb | ||
312 | where | ||
313 | forkit = void . forkIO $ do | ||
314 | proto <- getProtocolNumber "tcp" | ||
315 | sock <- socket (socketFamily addr) Stream proto | ||
316 | handle (\e -> do -- let t = ioeGetErrorType e | ||
317 | when (isDoesNotExistError e) $ return () -- warn "GOTCHA" | ||
318 | -- warn $ "connect-error: " <> bshow e | ||
319 | (conkey,u) <- makeConnKey params (sock,addr) -- XXX: ? | ||
320 | sClose sock | ||
321 | atomically | ||
322 | $ writeTChan (serverEvent server) | ||
323 | $ ((conkey,u),ConnectFailure addr)) | ||
324 | $ do | ||
325 | connect sock addr | ||
326 | me <- getSocketName sock | ||
327 | (conkey,u) <- makeConnKey params (sock,me) | ||
328 | h <- socketToHandle sock ReadWriteMode | ||
329 | newConnection server params conkey u h Out | ||
330 | return () | ||
331 | |||
332 | doit server (ConnectWithEndlessRetry addr params interval) = liftIO $ do | ||
333 | proto <- getProtocolNumber "tcp" | ||
334 | void . forkIO $ do | ||
335 | resultVar <- atomically newEmptyTMVar | ||
336 | timer <- interruptableDelay | ||
337 | (retryVar,action) <- atomically $ do | ||
338 | let noop = return () | ||
339 | map <- readTVar (retrymap server) | ||
340 | let mb = Map.lookup addr map | ||
341 | action <- | ||
342 | maybe (return noop) | ||
343 | (\(v,d) -> do writeTVar v False | ||
344 | return $ interruptDelay d) | ||
345 | mb | ||
346 | v <- newTVar True | ||
347 | writeTVar (retrymap server) (Map.insert addr (v,timer) map) | ||
348 | return (v,action) | ||
349 | action | ||
350 | fix $ \retryLoop -> do | ||
351 | utc <- getCurrentTime | ||
352 | forkIO $ do | ||
353 | sock <- socket (socketFamily addr) Stream proto | ||
354 | handle (\e -> do -- let t = ioeGetErrorType e | ||
355 | when (isDoesNotExistError e) $ return () -- warn "GOTCHA" | ||
356 | -- warn $ "connect-error: " <> bshow e | ||
357 | -- Weird hack: puting the would-be peer address | ||
358 | -- instead of local socketName | ||
359 | (conkey,u) <- makeConnKey params (sock,addr) -- XXX: ? | ||
360 | sClose sock | ||
361 | atomically $ do | ||
362 | writeTChan (serverEvent server) | ||
363 | $ ((conkey,u),ConnectFailure addr) | ||
364 | retry <- readTVar retryVar | ||
365 | putTMVar resultVar retry) | ||
366 | $ do | ||
367 | connect sock addr | ||
368 | me <- getSocketName sock | ||
369 | (conkey,u) <- makeConnKey params (sock,me) | ||
370 | h <- socketToHandle sock ReadWriteMode | ||
371 | threads <- newConnection server params conkey u h Out | ||
372 | atomically $ do threadsWait threads | ||
373 | retry <- readTVar retryVar | ||
374 | putTMVar resultVar retry | ||
375 | retry <- atomically $ takeTMVar resultVar | ||
376 | fin_utc <- getCurrentTime | ||
377 | when retry $ do | ||
378 | let elapsed = 1000.0 * (fin_utc `diffUTCTime` utc) | ||
379 | expected = fromIntegral interval | ||
380 | when (retry && elapsed < expected) $ do | ||
381 | debugNoise $ "Waiting to retry " <> bshow addr | ||
382 | void $ startDelay timer (round $ 1000 * (expected-elapsed)) | ||
383 | debugNoise $ "retry " <> bshow (retry,addr) | ||
384 | when retry $ retryLoop | ||
385 | |||
386 | |||
387 | -- INTERNAL ---------------------------------------------------------- | ||
388 | |||
389 | {- | ||
390 | hWriteUntilNothing h outs = | ||
391 | fix $ \loop -> do | ||
392 | mb <- atomically $ takeTMVar outs | ||
393 | case mb of Just bs -> do S.hPutStrLn h bs | ||
394 | warn $ "wrote " <> bs | ||
395 | loop | ||
396 | Nothing -> do warn $ "wrote Nothing" | ||
397 | hClose h | ||
398 | |||
399 | -} | ||
400 | connRead :: ConnectionState -> IO (Maybe ByteString) | ||
401 | connRead (WriteOnlyConnection w) = do | ||
402 | -- atomically $ discardContents (threadsChannel w) | ||
403 | return Nothing | ||
404 | connRead conn = do | ||
405 | c <- atomically $ getThreads | ||
406 | threadsRead c | ||
407 | where | ||
408 | getThreads = | ||
409 | case conn of SaneConnection c -> return c | ||
410 | ReadOnlyConnection c -> return c | ||
411 | ConnectionPair c w -> do | ||
412 | -- discardContents (threadsChannel w) | ||
413 | return c | ||
414 | |||
415 | socketFamily (SockAddrInet _ _) = AF_INET | ||
416 | socketFamily (SockAddrInet6 _ _ _ _) = AF_INET6 | ||
417 | socketFamily (SockAddrUnix _) = AF_UNIX | ||
418 | |||
419 | killListener (thread,sock) = do sClose sock | ||
420 | -- killThread thread | ||
421 | |||
422 | |||
423 | conevent con = Connection pingflag read write | ||
424 | where | ||
425 | pingflag = swapTVar (pingFlag (connPingTimer con)) False | ||
426 | read = connRead con | ||
427 | write = connWrite con | ||
428 | |||
429 | newConnection server params conkey u h inout = do | ||
430 | hSetBuffering h NoBuffering | ||
431 | let (forward,idle_ms,timeout_ms) = | ||
432 | case (inout,duplex params) of | ||
433 | (Out,False) -> ( const $ return () | ||
434 | , 0 | ||
435 | , 0 ) | ||
436 | _ -> ( announce . ((conkey,u),) . Got | ||
437 | , pingInterval params | ||
438 | , timeout params ) | ||
439 | |||
440 | new <- do pinglogic <- pingMachine idle_ms timeout_ms | ||
441 | connectionThreads h pinglogic | ||
442 | started <- atomically $ newEmptyTMVar | ||
443 | kontvar <- atomically newEmptyTMVar | ||
444 | forkIO $ do | ||
445 | getkont <- atomically $ takeTMVar kontvar | ||
446 | kont <- atomically getkont | ||
447 | kont | ||
448 | |||
449 | atomically $ do | ||
450 | current <- fmap (Map.lookup conkey) $ readTVar (conmap server) | ||
451 | case current of | ||
452 | Nothing -> do | ||
453 | (newCon,e) <- return $ | ||
454 | if duplex params | ||
455 | then let newcon = SaneConnection new | ||
456 | in ( newcon, ((conkey,u), conevent newcon) ) | ||
457 | else ( case inout of | ||
458 | In -> ReadOnlyConnection new | ||
459 | Out -> WriteOnlyConnection new | ||
460 | , ((conkey,u), HalfConnection inout) ) | ||
461 | modifyTVar' (conmap server) $ Map.insert conkey | ||
462 | ConnectionRecord { ckont = kontvar | ||
463 | , cstate = newCon | ||
464 | , cdata = u } | ||
465 | announce e | ||
466 | putTMVar kontvar $ return $ do | ||
467 | atomically $ putTMVar started () | ||
468 | handleEOF conkey u kontvar newCon | ||
469 | Just what@ConnectionRecord { ckont =mvar }-> do | ||
470 | putTMVar kontvar $ return $ return () | ||
471 | putTMVar mvar $ do | ||
472 | kont <- updateConMap conkey u new what | ||
473 | putTMVar started () | ||
474 | return kont | ||
475 | #ifdef TEST | ||
476 | -- enable this for 'Got' events | ||
477 | forkIO $ do -- inout==In || duplex params then forkIO $ do | ||
478 | warn $ "waiting gots thread: " <> bshow (conkey,inout) | ||
479 | atomically $ takeTMVar started | ||
480 | -- pingBump pinglogic -- start the ping timer | ||
481 | fix $ \loop -> do | ||
482 | -- warn $ "read thread: " <> bshow (conkey,inout) | ||
483 | mb <- threadsRead new | ||
484 | -- pingBump pinglogic | ||
485 | warn $ "got: " <> bshow (mb,(conkey,inout)) | ||
486 | maybe (return ()) | ||
487 | (atomically . forward >=> const loop) | ||
488 | mb | ||
489 | warn $ "quit-gots: " <> bshow (conkey,inout) | ||
490 | #endif | ||
491 | return new | ||
492 | where | ||
493 | |||
494 | announce e = writeTChan (serverEvent server) e | ||
495 | |||
496 | handleEOF conkey u mvar newCon = do | ||
497 | action <- atomically . foldr1 orElse $ | ||
498 | [ takeTMVar mvar >>= id -- passed continuation | ||
499 | , connWait newCon >> return eof | ||
500 | , connWaitPing newCon >>= return . sendPing | ||
501 | -- , pingWait pingTimer >>= return . sendPing | ||
502 | ] | ||
503 | action :: IO () | ||
504 | where | ||
505 | eof = do | ||
506 | -- warn $ "EOF " <>bshow conkey | ||
507 | connCancelPing newCon | ||
508 | atomically $ do connFlush newCon | ||
509 | announce ((conkey,u),EOF) | ||
510 | modifyTVar' (conmap server) | ||
511 | $ Map.delete conkey | ||
512 | -- warn $ "fin-EOF "<>bshow conkey | ||
513 | |||
514 | sendPing PingTimeOut = do | ||
515 | {- | ||
516 | utc <- getCurrentTime | ||
517 | let utc' = formatTime defaultTimeLocale "%s" utc | ||
518 | warn $ "ping:TIMEOUT " <> bshow utc' | ||
519 | -} | ||
520 | atomically (connClose newCon) | ||
521 | eof | ||
522 | |||
523 | sendPing PingIdle = do | ||
524 | {- | ||
525 | utc <- getCurrentTime | ||
526 | let utc' = formatTime defaultTimeLocale "%s" utc | ||
527 | -- warn $ "ping:IDLE " <> bshow utc' | ||
528 | -} | ||
529 | atomically $ announce ((conkey,u),RequiresPing) | ||
530 | handleEOF conkey u mvar newCon | ||
531 | |||
532 | |||
533 | updateConMap conkey u new (ConnectionRecord { ckont=mvar, cstate=replaced, cdata=u0 }) = do | ||
534 | new' <- | ||
535 | if duplex params then do | ||
536 | announce ((conkey,u),EOF) | ||
537 | connClose replaced | ||
538 | let newcon = SaneConnection new | ||
539 | announce $ ((conkey,u),conevent newcon) | ||
540 | return $ newcon | ||
541 | else | ||
542 | case replaced of | ||
543 | WriteOnlyConnection w | inout==In -> | ||
544 | do let newcon = ConnectionPair new w | ||
545 | announce ((conkey,u),conevent newcon) | ||
546 | return newcon | ||
547 | ReadOnlyConnection r | inout==Out -> | ||
548 | do let newcon = ConnectionPair r new | ||
549 | announce ((conkey,u),conevent newcon) | ||
550 | return newcon | ||
551 | _ -> do -- connFlush todo | ||
552 | announce ((conkey,u0), EOF) | ||
553 | connClose replaced | ||
554 | announce ((conkey,u), HalfConnection inout) | ||
555 | return $ case inout of | ||
556 | In -> ReadOnlyConnection new | ||
557 | Out -> WriteOnlyConnection new | ||
558 | modifyTVar' (conmap server) $ Map.insert conkey | ||
559 | ConnectionRecord { ckont = mvar | ||
560 | , cstate = new' | ||
561 | , cdata = u } | ||
562 | return $ handleEOF conkey u mvar new' | ||
563 | |||
564 | acceptLoop server params sock = handle (acceptException server params sock) $ do | ||
565 | con <- accept sock | ||
566 | (conkey,u) <- makeConnKey params con | ||
567 | h <- socketToHandle (fst con) ReadWriteMode | ||
568 | newConnection server params conkey u h In | ||
569 | acceptLoop server params sock | ||
570 | |||
571 | acceptException server params sock ioerror = do | ||
572 | sClose sock | ||
573 | case show (ioeGetErrorType ioerror) of | ||
574 | "resource exhausted" -> do -- try again | ||
575 | warn ("acceptLoop: resource exhasted") | ||
576 | threadDelay 500000 | ||
577 | acceptLoop server params sock | ||
578 | "invalid argument" -> do -- quit on closed socket | ||
579 | return () | ||
580 | message -> do -- unexpected exception | ||
581 | warn ("acceptLoop: "<>bshow message) | ||
582 | return () | ||
583 | |||
584 | |||
585 | |||
586 | getPacket h = do hWaitForInput h (-1) | ||
587 | hGetNonBlocking h 1024 | ||
588 | |||
589 | |||
590 | |||
591 | -- | 'ConnectionThreads' is an interface to a pair of threads | ||
592 | -- that are reading and writing a 'Handle'. | ||
593 | data ConnectionThreads = ConnectionThreads | ||
594 | { threadsWriter :: TMVar (Maybe ByteString) | ||
595 | , threadsChannel :: TChan ByteString | ||
596 | , threadsWait :: STM () -- ^ waits for a 'ConnectionThreads' object to close | ||
597 | , threadsPing :: PingMachine | ||
598 | } | ||
599 | |||
600 | -- | This spawns the reader and writer threads and returns a newly | ||
601 | -- constructed 'ConnectionThreads' object. | ||
602 | connectionThreads :: Handle -> PingMachine -> IO ConnectionThreads | ||
603 | connectionThreads h pinglogic = do | ||
604 | |||
605 | (donew,outs) <- atomically $ liftM2 (,) newEmptyTMVar newEmptyTMVar | ||
606 | |||
607 | (doner,incomming) <- atomically $ liftM2 (,) newEmptyTMVar newTChan | ||
608 | readerThread <- forkIO $ do | ||
609 | let finished e = do | ||
610 | hClose h | ||
611 | -- warn $ "finished read: " <> bshow (fmap ioeGetErrorType e) | ||
612 | -- let _ = fmap ioeGetErrorType e -- type hint | ||
613 | let _ = fmap what e where what (SomeException _) = undefined | ||
614 | atomically $ do tryTakeTMVar outs | ||
615 | putTMVar outs Nothing -- quit writer | ||
616 | putTMVar doner () | ||
617 | handle (finished . Just) $ do | ||
618 | pingBump pinglogic -- start the ping timer | ||
619 | fix $ \loop -> do | ||
620 | packet <- getPacket h | ||
621 | -- warn $ "read: " <> S.take 60 packet | ||
622 | atomically $ writeTChan incomming packet | ||
623 | pingBump pinglogic | ||
624 | -- warn $ "bumped: " <> S.take 60 packet | ||
625 | isEof <- liftIO $ hIsEOF h | ||
626 | if isEof then finished Nothing else loop | ||
627 | |||
628 | writerThread <- forkIO . fix $ \loop -> do | ||
629 | let finished = do -- warn $ "finished write" | ||
630 | -- hClose h -- quit reader | ||
631 | throwTo readerThread (ErrorCall "EOF") | ||
632 | atomically $ putTMVar donew () | ||
633 | mb <- atomically $ readTMVar outs | ||
634 | case mb of Just bs -> handle (\(SomeException e)->finished) | ||
635 | (do -- warn $ "writing: " <> S.take 60 bs | ||
636 | S.hPutStr h bs | ||
637 | -- warn $ "wrote: " <> S.take 60 bs | ||
638 | atomically $ takeTMVar outs | ||
639 | loop) | ||
640 | Nothing -> finished | ||
641 | |||
642 | let wait = do readTMVar donew | ||
643 | readTMVar doner | ||
644 | return () | ||
645 | return ConnectionThreads { threadsWriter = outs | ||
646 | , threadsChannel = incomming | ||
647 | , threadsWait = wait | ||
648 | , threadsPing = pinglogic } | ||
649 | |||
650 | |||
651 | -- | 'threadsWrite' writes the given 'ByteString' to the | ||
652 | -- 'ConnectionThreads' object. It blocks until the ByteString | ||
653 | -- is written and 'True' is returned, or the connection is | ||
654 | -- interrupted and 'False' is returned. | ||
655 | threadsWrite :: ConnectionThreads -> ByteString -> IO Bool | ||
656 | threadsWrite c bs = atomically $ | ||
657 | orElse (const False `fmap` threadsWait c) | ||
658 | (const True `fmap` putTMVar (threadsWriter c) (Just bs)) | ||
659 | |||
660 | -- | 'threadsClose' signals for the 'ConnectionThreads' object | ||
661 | -- to quit and close the associated 'Handle'. This operation | ||
662 | -- is non-blocking, follow it with 'threadsWait' if you want | ||
663 | -- to wait for the operation to complete. | ||
664 | threadsClose :: ConnectionThreads -> STM () | ||
665 | threadsClose c = do | ||
666 | let mvar = threadsWriter c | ||
667 | v <- tryReadTMVar mvar | ||
668 | case v of | ||
669 | Just Nothing -> return () -- already closed | ||
670 | _ -> putTMVar mvar Nothing | ||
671 | |||
672 | -- | 'threadsRead' blocks until a 'ByteString' is available which | ||
673 | -- is returned to the caller, or the connection is interrupted and | ||
674 | -- 'Nothing' is returned. | ||
675 | threadsRead :: ConnectionThreads -> IO (Maybe ByteString) | ||
676 | threadsRead c = atomically $ | ||
677 | orElse (const Nothing `fmap` threadsWait c) | ||
678 | (Just `fmap` readTChan (threadsChannel c)) | ||
679 | |||
680 | -- | A 'ConnectionState' is an interface to a single 'ConnectionThreads' | ||
681 | -- or to a pair of 'ConnectionThreads' objects that are considered as one | ||
682 | -- connection. | ||
683 | data ConnectionState = | ||
684 | SaneConnection ConnectionThreads | ||
685 | -- ^ ordinary read/write connection | ||
686 | | WriteOnlyConnection ConnectionThreads | ||
687 | | ReadOnlyConnection ConnectionThreads | ||
688 | | ConnectionPair ConnectionThreads ConnectionThreads | ||
689 | -- ^ Two 'ConnectionThreads' objects, read operations use the | ||
690 | -- first, write operations use the second. | ||
691 | |||
692 | |||
693 | |||
694 | connWrite :: ConnectionState -> ByteString -> IO Bool | ||
695 | connWrite (ReadOnlyConnection _) bs = return False | ||
696 | connWrite conn bs = threadsWrite c bs | ||
697 | where | ||
698 | c = case conn of SaneConnection c -> c | ||
699 | WriteOnlyConnection c -> c | ||
700 | ConnectionPair _ c -> c | ||
701 | |||
702 | |||
703 | mapConn :: Bool -> | ||
704 | (ConnectionThreads -> STM ()) -> ConnectionState -> STM () | ||
705 | mapConn both action c = | ||
706 | case c of | ||
707 | SaneConnection rw -> action rw | ||
708 | ReadOnlyConnection r -> action r | ||
709 | WriteOnlyConnection w -> action w | ||
710 | ConnectionPair r w -> do | ||
711 | rem <- orElse (const w `fmap` action r) | ||
712 | (const r `fmap` action w) | ||
713 | when both $ action rem | ||
714 | |||
715 | connClose :: ConnectionState -> STM () | ||
716 | connClose c = mapConn True threadsClose c | ||
717 | |||
718 | connWait :: ConnectionState -> STM () | ||
719 | connWait c = doit -- mapConn False threadsWait c | ||
720 | where | ||
721 | action = threadsWait | ||
722 | doit = | ||
723 | case c of | ||
724 | SaneConnection rw -> action rw | ||
725 | ReadOnlyConnection r -> action r | ||
726 | WriteOnlyConnection w -> action w | ||
727 | ConnectionPair r w -> do | ||
728 | rem <- orElse (const w `fmap` action r) | ||
729 | (const r `fmap` action w) | ||
730 | threadsClose rem | ||
731 | |||
732 | connPingTimer c = | ||
733 | case c of | ||
734 | SaneConnection rw -> threadsPing rw | ||
735 | ReadOnlyConnection r -> threadsPing r | ||
736 | WriteOnlyConnection w -> threadsPing w -- should be disabled. | ||
737 | ConnectionPair r w -> threadsPing r | ||
738 | |||
739 | connCancelPing c = pingCancel (connPingTimer c) | ||
740 | connWaitPing c = pingWait (connPingTimer c) | ||
741 | |||
742 | |||
743 | connFlush c = | ||
744 | case c of | ||
745 | SaneConnection rw -> waitChan rw | ||
746 | ReadOnlyConnection r -> waitChan r | ||
747 | WriteOnlyConnection w -> return () | ||
748 | ConnectionPair r w -> waitChan r | ||
749 | where | ||
750 | waitChan t = do | ||
751 | b <- isEmptyTChan (threadsChannel t) | ||
752 | when (not b) retry | ||
753 | |||
754 | bshow e = S.pack . show $ e | ||
755 | warn str = S.hPutStrLn stderr str >> hFlush stderr | ||
756 | debugNoise str = return () | ||
757 | |||
758 | |||
759 | data PingEvent = PingIdle | PingTimeOut | ||
760 | |||
761 | data PingMachine = PingMachine | ||
762 | { pingFlag :: TVar Bool | ||
763 | , pingInterruptable :: InterruptableDelay | ||
764 | , pingEvent :: TMVar PingEvent | ||
765 | , pingStarted :: TMVar Bool | ||
766 | } | ||
767 | |||
768 | pingMachine :: PingInterval -> TimeOut -> IO PingMachine | ||
769 | pingMachine idle timeout = do | ||
770 | d <- interruptableDelay | ||
771 | flag <- atomically $ newTVar False | ||
772 | canceled <- atomically $ newTVar False | ||
773 | event <- atomically newEmptyTMVar | ||
774 | started <- atomically $ newEmptyTMVar | ||
775 | when (idle/=0) $ void . forkIO $ do | ||
776 | (>>=) (atomically (readTMVar started)) $ flip when $ do | ||
777 | fix $ \loop -> do | ||
778 | atomically $ writeTVar flag False | ||
779 | fin <- startDelay d (1000*idle) | ||
780 | (>>=) (atomically (readTMVar started)) $ flip when $ do | ||
781 | if (not fin) then loop | ||
782 | else do | ||
783 | -- Idle event | ||
784 | atomically $ do | ||
785 | tryTakeTMVar event | ||
786 | putTMVar event PingIdle | ||
787 | writeTVar flag True | ||
788 | fin <- startDelay d (1000*timeout) | ||
789 | (>>=) (atomically (readTMVar started)) $ flip when $ do | ||
790 | me <- myThreadId | ||
791 | if (not fin) then loop | ||
792 | else do | ||
793 | -- Timeout event | ||
794 | atomically $ do | ||
795 | tryTakeTMVar event | ||
796 | writeTVar flag False | ||
797 | putTMVar event PingTimeOut | ||
798 | return PingMachine | ||
799 | { pingFlag = flag | ||
800 | , pingInterruptable = d | ||
801 | , pingEvent = event | ||
802 | , pingStarted = started | ||
803 | } | ||
804 | |||
805 | pingCancel :: PingMachine -> IO () | ||
806 | pingCancel me = do | ||
807 | atomically $ do tryTakeTMVar (pingStarted me) | ||
808 | putTMVar (pingStarted me) False | ||
809 | interruptDelay (pingInterruptable me) | ||
810 | |||
811 | pingBump :: PingMachine -> IO () | ||
812 | pingBump me = do | ||
813 | atomically $ do | ||
814 | b <- tryReadTMVar (pingStarted me) | ||
815 | when (b/=Just False) $ do | ||
816 | tryTakeTMVar (pingStarted me) | ||
817 | putTMVar (pingStarted me) True | ||
818 | interruptDelay (pingInterruptable me) | ||
819 | |||
820 | pingWait :: PingMachine -> STM PingEvent | ||
821 | pingWait me = takeTMVar (pingEvent me) | ||
822 | |||
823 | |||
824 | data InterruptableDelay = InterruptableDelay | ||
825 | { delayThread :: TMVar ThreadId | ||
826 | } | ||
827 | |||
828 | interruptableDelay :: IO InterruptableDelay | ||
829 | interruptableDelay = do | ||
830 | fmap InterruptableDelay | ||
831 | $ atomically newEmptyTMVar | ||
832 | |||
833 | startDelay :: InterruptableDelay -> Microseconds -> IO Bool | ||
834 | startDelay d interval = do | ||
835 | thread <- myThreadId | ||
836 | handle (\(ErrorCall _)-> do | ||
837 | debugNoise $ "delay interrupted" | ||
838 | return False) $ do | ||
839 | atomically $ putTMVar (delayThread d) thread | ||
840 | threadDelay interval | ||
841 | void . atomically $ takeTMVar (delayThread d) | ||
842 | return True | ||
843 | |||
844 | interruptDelay :: InterruptableDelay -> IO () | ||
845 | interruptDelay d = do | ||
846 | mthread <- atomically $ do | ||
847 | tryTakeTMVar (delayThread d) | ||
848 | flip (maybe $ return ()) mthread $ \thread -> do | ||
849 | throwTo thread (ErrorCall "Interrupted delay") | ||
850 | |||
851 | |||
diff --git a/Presence/SockAddr.hs b/Presence/SockAddr.hs new file mode 100644 index 00000000..91a03870 --- /dev/null +++ b/Presence/SockAddr.hs | |||
@@ -0,0 +1,13 @@ | |||
1 | {-# LANGUAGE CPP #-} | ||
2 | {-# LANGUAGE StandaloneDeriving #-} | ||
3 | module SockAddr () where | ||
4 | |||
5 | import Network.Socket ( SockAddr(..) ) | ||
6 | |||
7 | #if MIN_VERSION_network(2,4,0) | ||
8 | #else | ||
9 | deriving instance Ord SockAddr | ||
10 | #endif | ||
11 | |||
12 | |||
13 | |||
diff --git a/Presence/SocketLike.hs b/Presence/SocketLike.hs new file mode 100644 index 00000000..af0249ae --- /dev/null +++ b/Presence/SocketLike.hs | |||
@@ -0,0 +1,76 @@ | |||
1 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
2 | module SocketLike | ||
3 | ( SocketLike | ||
4 | , getSocketName | ||
5 | , getPeerName | ||
6 | , getPeerCred | ||
7 | , socketPort | ||
8 | , sIsConnected | ||
9 | , sIsBound | ||
10 | , sIsListening | ||
11 | , sIsReadable | ||
12 | , sIsWritable | ||
13 | , sClose | ||
14 | , RestrictedSocket | ||
15 | , restrictSocket | ||
16 | , restrictHandleSocket | ||
17 | , PortNumber | ||
18 | , SockAddr(..) | ||
19 | , CUInt | ||
20 | ) where | ||
21 | |||
22 | import Network.Socket | ||
23 | ( PortNumber | ||
24 | , SockAddr | ||
25 | ) | ||
26 | import Foreign.C.Types ( CUInt ) | ||
27 | |||
28 | import qualified Network.Socket as NS | ||
29 | import System.IO (Handle,hClose) | ||
30 | |||
31 | class SocketLike sock where | ||
32 | getSocketName :: sock -> IO SockAddr | ||
33 | getPeerName :: sock -> IO SockAddr | ||
34 | getPeerCred :: sock -> IO (CUInt, CUInt, CUInt) | ||
35 | socketPort :: sock -> IO PortNumber | ||
36 | sIsConnected :: sock -> IO Bool | ||
37 | sIsBound :: sock -> IO Bool | ||
38 | sIsListening :: sock -> IO Bool | ||
39 | sIsReadable :: sock -> IO Bool | ||
40 | sIsWritable :: sock -> IO Bool | ||
41 | sClose :: sock -> IO () | ||
42 | |||
43 | instance SocketLike NS.Socket where | ||
44 | getSocketName = NS.getSocketName | ||
45 | getPeerName = NS.getPeerName | ||
46 | getPeerCred = NS.getPeerCred | ||
47 | socketPort = NS.socketPort | ||
48 | sIsConnected = NS.sIsConnected -- warning: this is always False if the socket | ||
49 | -- was converted to a Handle | ||
50 | sIsBound = NS.sIsBound | ||
51 | sIsListening = NS.sIsListening | ||
52 | sIsReadable = NS.sIsReadable | ||
53 | sIsWritable = NS.sIsWritable | ||
54 | |||
55 | sClose = NS.sClose | ||
56 | |||
57 | -- newtype RestrictedSocket = Restricted NS.Socket deriving (SocketLike,Show) | ||
58 | data RestrictedSocket = Restricted (Maybe Handle) NS.Socket deriving Show | ||
59 | |||
60 | instance SocketLike RestrictedSocket where | ||
61 | getSocketName (Restricted mb sock) = NS.getSocketName sock | ||
62 | getPeerName (Restricted mb sock) = NS.getPeerName sock | ||
63 | getPeerCred (Restricted mb sock) = NS.getPeerCred sock | ||
64 | socketPort (Restricted mb sock) = NS.socketPort sock | ||
65 | sIsConnected (Restricted mb sock) = NS.sIsConnected sock | ||
66 | sIsBound (Restricted mb sock) = NS.sIsBound sock | ||
67 | sIsListening (Restricted mb sock) = NS.sIsListening sock | ||
68 | sIsReadable (Restricted mb sock) = NS.sIsReadable sock | ||
69 | sIsWritable (Restricted mb sock) = NS.sIsWritable sock | ||
70 | sClose (Restricted mb sock) = maybe (NS.sClose sock) (\h -> hClose h >> NS.sClose sock) mb | ||
71 | |||
72 | restrictSocket :: NS.Socket -> RestrictedSocket | ||
73 | restrictSocket socket = Restricted Nothing socket | ||
74 | |||
75 | restrictHandleSocket :: Handle -> NS.Socket -> RestrictedSocket | ||
76 | restrictHandleSocket h socket = Restricted (Just h) socket | ||
diff --git a/Presence/UTmp.hs b/Presence/UTmp.hs new file mode 100644 index 00000000..b43278da --- /dev/null +++ b/Presence/UTmp.hs | |||
@@ -0,0 +1,249 @@ | |||
1 | {-# LANGUAGE TemplateHaskell #-} | ||
2 | {-# LANGUAGE RankNTypes #-} | ||
3 | module UTmp | ||
4 | ( users | ||
5 | , users2 | ||
6 | , utmp_file | ||
7 | , UserName | ||
8 | , Tty | ||
9 | , ProcessID | ||
10 | , UtmpRecord(..) | ||
11 | , UT_Type(..) | ||
12 | ) where | ||
13 | |||
14 | import qualified Data.ByteString as S | ||
15 | import qualified Data.ByteString.Char8 as C | ||
16 | import qualified Data.ByteString.Lazy.Char8 as L | ||
17 | import Data.BitSyntax | ||
18 | import Data.Functor.Identity | ||
19 | import Data.Maybe | ||
20 | import System.Posix.Signals | ||
21 | import System.Posix.Types | ||
22 | import Control.Monad | ||
23 | import Data.Word | ||
24 | import Data.Int | ||
25 | import Control.Monad.Error.Class | ||
26 | import System.IO.Error | ||
27 | import qualified Paths | ||
28 | import Data.Text ( Text ) | ||
29 | import Unsafe.Coerce ( unsafeCoerce ) | ||
30 | import Network.Socket ( SockAddr(..) ) | ||
31 | import qualified Data.Text.Encoding as Text | ||
32 | import SockAddr () | ||
33 | |||
34 | |||
35 | utmp_file :: String | ||
36 | utmp_file = Paths.utmp -- "/var/run/utmp" | ||
37 | |||
38 | utmp_bs :: IO C.ByteString | ||
39 | utmp_bs = S.readFile utmp_file | ||
40 | |||
41 | decode_utmp_bytestring :: | ||
42 | C.ByteString | ||
43 | -> (Word32, | ||
44 | Word32, | ||
45 | C.ByteString, | ||
46 | C.ByteString, | ||
47 | C.ByteString, | ||
48 | C.ByteString, | ||
49 | Word16, | ||
50 | Word16, | ||
51 | Word32, | ||
52 | C.ByteString, | ||
53 | Word32, | ||
54 | Word32, | ||
55 | Word32, | ||
56 | Word32) | ||
57 | decode_utmp_bytestring = | ||
58 | runIdentity | ||
59 | . $(bitSyn [ UnsignedLE 4 -- type | ||
60 | , UnsignedLE 4 -- pid | ||
61 | , Fixed 32 -- tty | ||
62 | , Fixed 4 -- inittab id | ||
63 | , Fixed 32 -- username | ||
64 | , Fixed 256 -- remote host | ||
65 | , UnsignedLE 2 -- termination status | ||
66 | , UnsignedLE 2 -- exit status (int) | ||
67 | , UnsignedLE 4 -- session id (int) | ||
68 | , Fixed 8 -- time entry was made | ||
69 | , Unsigned 4 -- remote addr v6 addr[0] | ||
70 | , Unsigned 4 -- remote addr v6 addr[1] | ||
71 | , Unsigned 4 -- remote addr v6 addr[2] | ||
72 | , Unsigned 4 -- remote addr v6 addr[3] | ||
73 | , Skip 20 -- reserved | ||
74 | ]) | ||
75 | |||
76 | utmp_size :: Int | ||
77 | utmp_size = 384 -- 768 | ||
78 | |||
79 | |||
80 | utmp_records :: C.ByteString -> [C.ByteString] | ||
81 | utmp_records bs | S.length bs >= utmp_size | ||
82 | = u:utmp_records us | ||
83 | where | ||
84 | (u,us) = S.splitAt utmp_size bs | ||
85 | |||
86 | utmp_records bs = [bs] | ||
87 | |||
88 | utmp :: | ||
89 | IO | ||
90 | [(Word32, | ||
91 | Word32, | ||
92 | C.ByteString, | ||
93 | C.ByteString, | ||
94 | C.ByteString, | ||
95 | C.ByteString, | ||
96 | Word16, | ||
97 | Word16, | ||
98 | Word32, | ||
99 | C.ByteString, | ||
100 | Word32, | ||
101 | Word32, | ||
102 | Word32, | ||
103 | Word32)] | ||
104 | utmp = fmap (map decode_utmp_bytestring . utmp_records) utmp_bs | ||
105 | |||
106 | toStr :: C.ByteString -> [Char] | ||
107 | toStr = takeWhile (/='\0') . C.unpack | ||
108 | |||
109 | interp_utmp_record :: | ||
110 | forall t t1 t2 t3 t4 t5 t6 t7 t8 a. | ||
111 | Integral a => | ||
112 | (a, | ||
113 | Word32, | ||
114 | C.ByteString, | ||
115 | t, | ||
116 | C.ByteString, | ||
117 | C.ByteString, | ||
118 | t1, | ||
119 | t2, | ||
120 | t3, | ||
121 | t4, | ||
122 | t5, | ||
123 | t6, | ||
124 | t7, | ||
125 | t8) | ||
126 | -> (UT_Type, [Char], [Char], CPid, [Char]) | ||
127 | interp_utmp_record (typ,pid,tty,inittab,user,hostv4,term,exit,session,time | ||
128 | ,addr0,addr1,addr2,addr3) = | ||
129 | ( (toEnum . fromIntegral) typ :: UT_Type | ||
130 | , toStr user, toStr tty, processId pid, toStr hostv4 ) | ||
131 | where | ||
132 | processId = CPid . coerceToSigned | ||
133 | |||
134 | coerceToSigned :: Word32 -> Int32 | ||
135 | coerceToSigned = unsafeCoerce | ||
136 | |||
137 | |||
138 | data UT_Type | ||
139 | = EMPTY -- No valid user accounting information. */ | ||
140 | |||
141 | | RUN_LVL -- The system's runlevel. */ | ||
142 | | BOOT_TIME -- Time of system boot. */ | ||
143 | | NEW_TIME -- Time after system clock changed. */ | ||
144 | | OLD_TIME -- Time when system clock changed. */ | ||
145 | |||
146 | | INIT_PROCESS -- Process spawned by the init process. */ | ||
147 | | LOGIN_PROCESS -- Session leader of a logged in user. */ | ||
148 | | USER_PROCESS -- Normal process. */ | ||
149 | | DEAD_PROCESS -- Terminated process. */ | ||
150 | |||
151 | | ACCOUNTING | ||
152 | |||
153 | deriving (Enum,Show,Eq,Ord,Read) | ||
154 | |||
155 | processAlive :: ProcessID -> IO Bool | ||
156 | processAlive pid = do | ||
157 | catchError (do { signalProcess nullSignal pid ; return True }) | ||
158 | $ \e -> do { return (not ( isDoesNotExistError e)); } | ||
159 | |||
160 | type UserName = L.ByteString | ||
161 | type Tty = L.ByteString | ||
162 | |||
163 | users :: IO [(UserName, Tty, ProcessID)] | ||
164 | users = fmap (map only3) $ do | ||
165 | us <- utmp | ||
166 | let us' = map interp_utmp_record us | ||
167 | us'' = mapMaybe user_proc us' | ||
168 | user_proc (USER_PROCESS, u,tty,pid, hostv4) | ||
169 | = Just (L.pack u,L.pack tty,pid,hostv4) | ||
170 | user_proc _ = Nothing | ||
171 | onThrd f (_,_,pid,_) = f pid | ||
172 | us3 <- filterM (onThrd processAlive) us'' | ||
173 | return us3 | ||
174 | |||
175 | only3 :: forall t t1 t2 t3. (t1, t2, t3, t) -> (t1, t2, t3) | ||
176 | only3 (a,b,c,_) = (a,b,c) | ||
177 | |||
178 | data UtmpRecord = UtmpRecord | ||
179 | { utmpType :: UT_Type | ||
180 | , utmpUser :: Text | ||
181 | , utmpTty :: Text | ||
182 | , utmpPid :: CPid | ||
183 | , utmpHost :: Text | ||
184 | , utmpSession :: Int32 | ||
185 | , utmpRemoteAddr :: Maybe SockAddr | ||
186 | } | ||
187 | deriving ( Show, Eq, Ord ) | ||
188 | |||
189 | toText :: C.ByteString -> Text | ||
190 | toText bs = Text.decodeUtf8 $ C.takeWhile (/='\0') bs | ||
191 | |||
192 | interp_utmp_record2 :: | ||
193 | forall t t1 t2 t3 a. | ||
194 | Integral a => | ||
195 | (a, | ||
196 | Word32, | ||
197 | C.ByteString, | ||
198 | t, | ||
199 | C.ByteString, | ||
200 | C.ByteString, | ||
201 | t1, | ||
202 | t2, | ||
203 | Word32, | ||
204 | t3, | ||
205 | Word32, | ||
206 | Word32, | ||
207 | Word32, | ||
208 | Word32) | ||
209 | -> UtmpRecord | ||
210 | interp_utmp_record2 (typ,pid,tty,inittab,user,hostv4 | ||
211 | ,term,exit,session,time,addr0,addr1,addr2,addr3) = | ||
212 | UtmpRecord | ||
213 | { utmpType = toEnum (fromIntegral typ) :: UT_Type | ||
214 | , utmpUser = toText user | ||
215 | , utmpTty = toText tty | ||
216 | , utmpPid = processId pid | ||
217 | , utmpHost = toText hostv4 | ||
218 | , utmpSession = coerceToSigned session | ||
219 | , utmpRemoteAddr = | ||
220 | if all (==0) [addr1,addr2,addr3] | ||
221 | then do guard (addr0/=0) | ||
222 | Just $ SockAddrInet6 0 0 (0,0,0xFFFF,addr0) 0 | ||
223 | else Just $ SockAddrInet6 0 0 (addr0,addr1,addr2,addr3) 0 | ||
224 | } | ||
225 | where | ||
226 | processId = CPid . coerceToSigned | ||
227 | |||
228 | users2 :: IO [UtmpRecord] | ||
229 | users2 = do | ||
230 | us <- utmp | ||
231 | let us' = map interp_utmp_record2 us | ||
232 | us3 <- filterM (processAlive . utmpPid) us' | ||
233 | return us3 | ||
234 | |||
235 | {- | ||
236 | - This is how the w command reports idle time: | ||
237 | /* stat the device file to get an idle time */ | ||
238 | static time_t idletime(const char *restrict const tty) | ||
239 | { | ||
240 | struct stat sbuf; | ||
241 | if (stat(tty, &sbuf) != 0) | ||
242 | return 0; | ||
243 | return time(NULL) - sbuf.st_atime; | ||
244 | } | ||
245 | - THis might be useful fo rimplementing | ||
246 | - xep-0012 Last Activity | ||
247 | - iq get {jabber:iq:last}query | ||
248 | - | ||
249 | -} | ||
diff --git a/Presence/XMPP.hs b/Presence/XMPP.hs new file mode 100644 index 00000000..eab57da5 --- /dev/null +++ b/Presence/XMPP.hs | |||
@@ -0,0 +1,1461 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | ||
2 | {-# LANGUAGE FlexibleContexts #-} | ||
3 | {-# LANGUAGE ViewPatterns #-} | ||
4 | {-# LANGUAGE TypeFamilies #-} | ||
5 | {-# LANGUAGE CPP #-} | ||
6 | module XMPP | ||
7 | ( module XMPPTypes | ||
8 | , listenForXmppClients | ||
9 | , listenForRemotePeers | ||
10 | , newServerConnections | ||
11 | , seekRemotePeers | ||
12 | , quitListening | ||
13 | , OutBoundMessage(..) | ||
14 | , OutgoingConnections | ||
15 | , CachedMessages | ||
16 | , toPeer | ||
17 | , newOutgoingConnections | ||
18 | , sendMessage | ||
19 | ) where | ||
20 | |||
21 | import ServerC | ||
22 | import XMPPTypes | ||
23 | import ByteStringOperators | ||
24 | import ControlMaybe | ||
25 | import XMLToByteStrings | ||
26 | import SendMessage | ||
27 | import Logging | ||
28 | import Todo | ||
29 | |||
30 | import Data.Maybe (catMaybes) | ||
31 | import Data.HList hiding (hHead) | ||
32 | import Network.Socket ( Family ) | ||
33 | import Control.Concurrent.STM | ||
34 | import Control.Concurrent.STM.Delay | ||
35 | import Data.Conduit | ||
36 | import Data.Maybe | ||
37 | import Data.ByteString (ByteString) | ||
38 | import qualified Data.ByteString.Lazy.Char8 as L | ||
39 | ( fromChunks | ||
40 | ) | ||
41 | import Control.Concurrent.Async | ||
42 | import Control.Exception as E ( finally ) | ||
43 | import System.IO.Error (isDoesNotExistError) | ||
44 | import Control.Monad.IO.Class | ||
45 | import Control.Monad.Trans.Class | ||
46 | import Control.Monad.Trans.Maybe | ||
47 | import Text.XML.Stream.Parse (def,parseBytes,content) | ||
48 | import Data.XML.Types as XML | ||
49 | import qualified Data.Text as S (Text,takeWhile) | ||
50 | import Data.Text.Encoding as S (decodeUtf8,encodeUtf8) | ||
51 | import Data.Text.Lazy.Encoding as L (decodeUtf8) | ||
52 | import Data.Text.Lazy (toStrict) | ||
53 | import qualified Data.Sequence as Seq | ||
54 | import Data.Foldable (toList) | ||
55 | import Data.List (find) | ||
56 | import qualified Text.Show.ByteString as L | ||
57 | import NestingXML | ||
58 | import Data.Set as Set (Set,(\\)) | ||
59 | import qualified Data.Set as Set | ||
60 | import qualified Data.Map as Map | ||
61 | import Data.Map as Map (Map) | ||
62 | |||
63 | #if MIN_VERSION_HList(0,3,0) | ||
64 | #define HCONS HCons' | ||
65 | #else | ||
66 | #define HCONS HCons | ||
67 | #endif | ||
68 | |||
69 | hHead (HCONS x _) = x | ||
70 | |||
71 | textToByteString x = L.fromChunks [S.encodeUtf8 x] | ||
72 | |||
73 | |||
74 | |||
75 | xmlifyPresenceForClient :: Presence -> IO [XML.Event] | ||
76 | xmlifyPresenceForClient (Presence jid stat) = do | ||
77 | let n = name jid | ||
78 | rsc = resource jid | ||
79 | names <- getNamesForPeer (peer jid) | ||
80 | let tostr p = L.decodeUtf8 $ n <$++> "@" <?++> L.fromChunks [p] <++?> "/" <++$> rsc | ||
81 | jidstrs = fmap (toStrict . tostr) names | ||
82 | return (concatMap presenceEvents jidstrs) | ||
83 | where | ||
84 | presenceEvents jidstr = | ||
85 | [ EventBeginElement "{jabber:client}presence" (("from",[ContentText jidstr]):typ stat) ] | ||
86 | ++ ( shw stat >>= jabberShow ) ++ | ||
87 | [ EventEndElement "{jabber:client}presence" ] | ||
88 | typ Offline = [("type",[ContentText "unavailable"])] | ||
89 | typ _ = [] | ||
90 | shw ExtendedAway = ["xa"] | ||
91 | shw Chatty = ["chat"] | ||
92 | shw Away = ["away"] | ||
93 | shw DoNotDisturb = ["dnd"] | ||
94 | shw _ = [] | ||
95 | jabberShow stat = | ||
96 | [ EventBeginElement "{jabber:client}show" [] | ||
97 | , EventContent (ContentText stat) | ||
98 | , EventEndElement "{jabber:client}show" ] | ||
99 | |||
100 | prefix ## name = Name name Nothing (Just prefix) | ||
101 | |||
102 | streamP name = Name name (Just "http://etherx.jabber.org/streams") (Just "stream") | ||
103 | |||
104 | greet host = | ||
105 | [ EventBeginDocument | ||
106 | , EventBeginElement (streamP "stream") | ||
107 | [("from",[ContentText host]) | ||
108 | ,("id",[ContentText "someid"]) | ||
109 | ,("xmlns",[ContentText "jabber:client"]) | ||
110 | ,("xmlns:stream",[ContentText "http://etherx.jabber.org/streams"]) | ||
111 | ,("version",[ContentText "1.0"]) | ||
112 | ] | ||
113 | , EventBeginElement (streamP "features") [] | ||
114 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" [] | ||
115 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" | ||
116 | |||
117 | {- | ||
118 | -- , " <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>" | ||
119 | , " <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" | ||
120 | -- , " <mechanism>DIGEST-MD5</mechanism>" | ||
121 | , " <mechanism>PLAIN</mechanism>" | ||
122 | , " </mechanisms> " | ||
123 | -} | ||
124 | |||
125 | , EventEndElement (streamP "features") | ||
126 | ] | ||
127 | |||
128 | |||
129 | -- type Consumer i m r = forall o. ConduitM i o m r | ||
130 | mawait :: Monad m => MaybeT (ConduitM i o m) i | ||
131 | mawait = MaybeT await | ||
132 | |||
133 | -- Note: This function ignores name space qualification | ||
134 | elementAttrs expected (EventBeginElement name attrs) | ||
135 | | nameLocalName name==expected | ||
136 | = return attrs | ||
137 | elementAttrs _ _ = mzero | ||
138 | |||
139 | eventIsBeginElement (EventBeginElement _ _) = True | ||
140 | eventIsBeginElement _ = False | ||
141 | |||
142 | eventIsEndElement (EventEndElement _) = True | ||
143 | eventIsEndElement _ = False | ||
144 | |||
145 | filterMapElement:: | ||
146 | (Monad m, MonadPlus mp) => | ||
147 | (Event -> mp a) -> Event -> mp a -> MaybeT (ConduitM Event o m) (mp a) | ||
148 | filterMapElement ret opentag empty = loop (empty `mplus` ret opentag) 1 | ||
149 | where | ||
150 | loop ts 0 = return ts | ||
151 | loop ts cnt = do | ||
152 | tag <- mawait | ||
153 | let ts' = mplus ts (ret tag) | ||
154 | case () of | ||
155 | _ | eventIsEndElement tag -> loop ts' (cnt-1) | ||
156 | _ | eventIsBeginElement tag -> loop ts' (cnt+1) | ||
157 | _ -> loop ts' cnt | ||
158 | |||
159 | gatherElement :: | ||
160 | (Monad m, MonadPlus mp) => | ||
161 | Event -> mp Event -> NestingXML o m (mp Event) | ||
162 | gatherElement opentag empty = loop (empty `mplus` return opentag) 1 | ||
163 | where | ||
164 | loop ts 0 = return ts | ||
165 | loop ts cnt = do | ||
166 | maybeXML (return ts) $ \tag -> do | ||
167 | let ts' = mplus ts (return tag) | ||
168 | case () of | ||
169 | _ | eventIsEndElement tag -> loop ts' (cnt-1) | ||
170 | _ | eventIsBeginElement tag -> loop ts' (cnt+1) | ||
171 | _ -> loop ts' cnt | ||
172 | |||
173 | |||
174 | voidMaybeT body = (>> return ()) . runMaybeT $ body | ||
175 | fixMaybeT f = (>> return ()) . runMaybeT . fix $ f | ||
176 | |||
177 | iq_bind_reply id jid = | ||
178 | [ EventBeginElement "{jabber:client}iq" [("type",[ContentText "result"]),("id",[ContentText id])] | ||
179 | |||
180 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" | ||
181 | [("xmlns",[ContentText "urn:ietf:params:xml:ns:xmpp-bind"])] | ||
182 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}jid" [] | ||
183 | , EventContent (ContentText jid) | ||
184 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}jid" | ||
185 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" | ||
186 | , EventEndElement "{jabber:client}iq" | ||
187 | ] | ||
188 | |||
189 | uncontent cs = head $ map getText cs | ||
190 | where | ||
191 | getText (ContentText x) = x | ||
192 | getText (ContentEntity x ) = x | ||
193 | |||
194 | tagAttrs (EventBeginElement _ xs) = xs | ||
195 | tagAttrs _ = [] | ||
196 | |||
197 | tagName (EventBeginElement n _) = n | ||
198 | tagName _ = "" | ||
199 | |||
200 | handleIQSetBind session cmdChan stanza_id = do | ||
201 | mchild <- nextElement | ||
202 | rsc <- case mchild of | ||
203 | Just child -> do | ||
204 | let unhandledBind = do | ||
205 | liftIO $ debugStr $ "unhandled-bind: "++show child | ||
206 | return "" | ||
207 | case tagName child of | ||
208 | "{urn:ietf:params:xml:ns:xmpp-bind}resource" | ||
209 | -> do | ||
210 | rsc <- lift content | ||
211 | return . textToByteString $ rsc | ||
212 | _ -> unhandledBind | ||
213 | Nothing -> do | ||
214 | liftIO $ debugStr $ "empty bind request!" | ||
215 | return "" | ||
216 | liftIO $ do | ||
217 | debugL $ "iq-set-bind-resource " <++> rsc | ||
218 | setResource session rsc | ||
219 | jid <- getJID session | ||
220 | atomically $ do | ||
221 | writeTChan cmdChan (Send $ iq_bind_reply stanza_id (toStrict $ L.decodeUtf8 $ L.show jid) ) | ||
222 | writeTChan cmdChan BoundToResource | ||
223 | forCachedPresence session $ \presence -> do | ||
224 | xs <- xmlifyPresenceForClient presence | ||
225 | atomically . writeTChan cmdChan . Send $ xs | ||
226 | |||
227 | |||
228 | iq_session_reply host stanza_id = | ||
229 | [ EventBeginElement "{jabber:client}iq" | ||
230 | [("id",[ContentText stanza_id]) | ||
231 | ,("from",[ContentText host]) | ||
232 | ,("type",[ContentText "result"]) | ||
233 | ] | ||
234 | , EventEndElement "{jabber:client}iq" | ||
235 | ] | ||
236 | |||
237 | handleIQSetSession session cmdChan stanza_id = do | ||
238 | host <- liftIO $ do | ||
239 | jid <- getJID session | ||
240 | names <- getNamesForPeer (peer jid) | ||
241 | return (S.decodeUtf8 . head $ names) | ||
242 | liftIO . atomically . writeTChan cmdChan . Send $ iq_session_reply host stanza_id | ||
243 | |||
244 | handleIQSet session cmdChan tag = do | ||
245 | withJust (lookupAttrib "id" (tagAttrs tag)) $ \stanza_id -> do | ||
246 | whenJust nextElement $ \child -> do | ||
247 | let unhandledSet = liftIO $ debugStr ("iq-set: "++show (stanza_id,child)) | ||
248 | case tagName child of | ||
249 | "{urn:ietf:params:xml:ns:xmpp-bind}bind" | ||
250 | -> handleIQSetBind session cmdChan stanza_id | ||
251 | "{urn:ietf:params:xml:ns:xmpp-session}session" | ||
252 | -> handleIQSetSession session cmdChan stanza_id | ||
253 | _ -> unhandledSet | ||
254 | |||
255 | matchAttrib name value attrs = | ||
256 | case find ( (==name) . fst) attrs of | ||
257 | Just (_,[ContentText x]) | x==value -> True | ||
258 | Just (_,[ContentEntity x]) | x==value -> True | ||
259 | _ -> False | ||
260 | |||
261 | lookupAttrib name attrs = | ||
262 | case find ( (==name) . fst) attrs of | ||
263 | Just (_,[ContentText x]) -> Just x | ||
264 | Just (_,[ContentEntity x]) -> Just x | ||
265 | _ -> Nothing | ||
266 | |||
267 | iqTypeSet = "set" | ||
268 | iqTypeGet = "get" | ||
269 | iqTypeResult = "result" | ||
270 | iqTypeError = "error" | ||
271 | |||
272 | isIQOf (EventBeginElement name attrs) testType | ||
273 | | name=="{jabber:client}iq" | ||
274 | && matchAttrib "type" testType attrs | ||
275 | = True | ||
276 | isIQOf _ _ = False | ||
277 | |||
278 | isServerIQOf (EventBeginElement name attrs) testType | ||
279 | | name=="{jabber:server}iq" | ||
280 | && matchAttrib "type" testType attrs | ||
281 | = True | ||
282 | isServerIQOf _ _ = False | ||
283 | |||
284 | iq_service_unavailable host iq_id mjid req = | ||
285 | [ EventBeginElement "{jabber:client}iq" | ||
286 | [("type",[ContentText "error"]) | ||
287 | ,("id",[ContentText iq_id]) | ||
288 | -- , TODO: set "from" if isJust mjid | ||
289 | ] | ||
290 | , EventBeginElement req [] | ||
291 | , EventEndElement req | ||
292 | , EventBeginElement "{jabber:client}error" [("type",[ContentText "cancel"])] | ||
293 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable" [] | ||
294 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable" | ||
295 | , EventEndElement "{jabber:client}error" | ||
296 | , EventEndElement "{jabber:client}iq" | ||
297 | ] | ||
298 | |||
299 | attr name value = (name,[ContentText value]) | ||
300 | attrbs name value = (name,[ContentText (toStrict . L.decodeUtf8 $ value)]) | ||
301 | |||
302 | |||
303 | getRoster session iqid = do | ||
304 | let getlist f = do | ||
305 | bs <- f session | ||
306 | -- js <- mapM parseHostNameJID bs | ||
307 | return (Set.fromList bs) -- js) | ||
308 | buddies <- getlist getMyBuddies | ||
309 | subscribers <- getlist getMySubscribers | ||
310 | solicited <- getlist getMySolicited | ||
311 | subnone0 <- getlist getMyOthers | ||
312 | let subnone = subnone0 \\ (Set.union buddies subscribers) | ||
313 | let subto = buddies \\ subscribers | ||
314 | let subfrom = subscribers \\ buddies | ||
315 | let subboth = Set.intersection buddies subscribers | ||
316 | -- solicited -> ask='subscribe' | ||
317 | jid <- getJID session | ||
318 | let dest = toStrict . L.decodeUtf8 . bshow $ jid | ||
319 | let items= (xmlify solicited "to" subto) | ||
320 | ++(xmlify solicited "from" subfrom) | ||
321 | ++(xmlify solicited "both" subboth) | ||
322 | ++(xmlify solicited "none" subnone) | ||
323 | openiq = [EventBeginElement "{jabber:client}iq" | ||
324 | [ attr "id" iqid | ||
325 | , attr "to" dest | ||
326 | , attr "type" "result" ] | ||
327 | ,EventBeginElement "{jabber:iq:roster}query" | ||
328 | [] -- todo: ver? | ||
329 | ] | ||
330 | closeiq = [EventEndElement "{jabber:iq:roster}query" | ||
331 | ,EventEndElement "{jabber:client}iq"] | ||
332 | return $ openiq ++ items ++ closeiq | ||
333 | where | ||
334 | xmlify solicited stype set = flip concatMap (Set.toList set) | ||
335 | $ \jid -> | ||
336 | [ EventBeginElement "item" | ||
337 | ([ attr "jid" (toStrict . L.decodeUtf8 $ jid) | ||
338 | , attr "subscription" stype | ||
339 | ]++if Set.member jid solicited | ||
340 | then [attr "ask" "subscribe"] | ||
341 | else [] ) | ||
342 | , EventEndElement "item" | ||
343 | ] | ||
344 | |||
345 | handleIQGet session cmdChan tag = do | ||
346 | withJust (lookupAttrib "id" (tagAttrs tag)) $ \stanza_id -> do | ||
347 | whenJust nextElement $ \child -> do | ||
348 | host <- liftIO $ do | ||
349 | jid <- getJID session | ||
350 | names <- getNamesForPeer (peer jid) | ||
351 | return (S.decodeUtf8 . head $ names) | ||
352 | let unhandledGet req = do | ||
353 | liftIO $ debugStr ("iq-get: "++show (stanza_id,child)) | ||
354 | liftIO . atomically . writeTChan cmdChan . Send $ iq_service_unavailable host stanza_id Nothing req | ||
355 | case tagName child of | ||
356 | -- "{http://jabber.org/protocol/disco#items}query" -> liftIO $ debugStr "iq-get-query-items" | ||
357 | "{urn:xmpp:ping}ping" -> liftIO $ do | ||
358 | let mjid = lookupAttrib "from" (tagAttrs tag) | ||
359 | let pong = [ EventBeginElement "{jabber:client}iq" | ||
360 | $ (case mjid of | ||
361 | Just jid -> (attr "to" jid :) | ||
362 | _ -> id ) | ||
363 | [ attr "type" "result" | ||
364 | , attr "id" stanza_id | ||
365 | , attr "from" host | ||
366 | ] | ||
367 | , EventEndElement "{jabber:client}iq" | ||
368 | ] | ||
369 | atomically . writeTChan cmdChan . Send $ pong | ||
370 | "{jabber:iq:roster}query" -> liftIO $ do | ||
371 | debugStr $ "REQUESTED ROSTER " ++ show tag | ||
372 | roster <- getRoster session stanza_id | ||
373 | atomically $ do | ||
374 | writeTChan cmdChan InterestedInRoster | ||
375 | writeTChan cmdChan . Send $ roster | ||
376 | sendPending session | ||
377 | req -> unhandledGet req | ||
378 | |||
379 | |||
380 | handleClientPresence session stanza = do | ||
381 | -- online (Available or Away) | ||
382 | let log = liftIO . debugL . ("(C) " <++>) | ||
383 | log $ "handleClientPresence "<++>bshow stanza | ||
384 | jid <- liftIO $ getJID session | ||
385 | -- cjid <- liftIO $ parseAddressJID (textToByteString jid) | ||
386 | let parseChildren stat = do | ||
387 | child <- nextElement | ||
388 | log $ " child: "<++> bshow child | ||
389 | case child of | ||
390 | Just tag | tagName tag=="{jabber:client}show" | ||
391 | -> fmap toStat (lift content) | ||
392 | Just tag | otherwise -> parseChildren stat | ||
393 | Nothing -> return stat | ||
394 | toStat "away" = Away | ||
395 | toStat "xa" = ExtendedAway | ||
396 | toStat "dnd" = DoNotDisturb | ||
397 | toStat "chat" = Chatty | ||
398 | |||
399 | stat' <- parseChildren Available | ||
400 | liftIO $ setPresence session stat' | ||
401 | log $ "requesting presence: "<++>bshow stat' | ||
402 | return () | ||
403 | |||
404 | |||
405 | fromClient :: (MonadThrow m,MonadIO m, JabberClientSession session) => | ||
406 | session -> TChan ClientCommands -> Sink XML.Event m () | ||
407 | fromClient session cmdChan = doNestingXML $ do | ||
408 | let log = liftIO . debugL . ("(C) " <++>) | ||
409 | send = liftIO . atomically . writeTChan cmdChan . Send | ||
410 | withXML $ \begindoc -> do | ||
411 | when (begindoc==EventBeginDocument) $ do | ||
412 | log "begin-doc" | ||
413 | withXML $ \xml -> do | ||
414 | withJust (elementAttrs "stream" xml) $ \stream_attrs -> do | ||
415 | log $ "stream atributes: " <++> bshow stream_attrs | ||
416 | host <- liftIO $ do | ||
417 | jid <- getJID session | ||
418 | names <- getNamesForPeer (peer jid) | ||
419 | return (S.decodeUtf8 . head $ names) | ||
420 | send $ greet host | ||
421 | |||
422 | fix $ \loop -> do | ||
423 | log "waiting for stanza." | ||
424 | whenJust nextElement $ \stanza -> do | ||
425 | stanza_lvl <- nesting | ||
426 | |||
427 | liftIO . debugStr $ "stanza: "++show stanza | ||
428 | |||
429 | let unhandledStanza = do | ||
430 | xs <- gatherElement stanza Seq.empty | ||
431 | prettyPrint "unhandled-C: " (toList xs) | ||
432 | case () of | ||
433 | _ | stanza `isIQOf` iqTypeSet -> handleIQSet session cmdChan stanza | ||
434 | _ | stanza `isIQOf` iqTypeGet -> handleIQGet session cmdChan stanza | ||
435 | _ | stanza `isClientPresenceOf` presenceTypeSubscribe | ||
436 | -> clientRequestsSubscription session cmdChan stanza | ||
437 | _ | stanza `isClientPresenceOf` presenceTypeSubscribed | ||
438 | -> clientApprovesSubscription session stanza | ||
439 | _ | stanza `isClientPresenceOf` presenceTypeUnsubscribed | ||
440 | -> clientRejectsSubscription session stanza | ||
441 | _ | stanza `isClientPresenceOf` presenceTypeOnline | ||
442 | -> handleClientPresence session stanza | ||
443 | _ | isMessageStanza stanza -> handleClientMessage session stanza | ||
444 | _ | otherwise -> unhandledStanza | ||
445 | |||
446 | awaitCloser stanza_lvl | ||
447 | loop | ||
448 | |||
449 | log $ "end of stream" | ||
450 | withXML $ \xml -> do | ||
451 | log $ "end-of-document: " <++> bshow xml | ||
452 | |||
453 | |||
454 | rosterPush to contact attrs = do | ||
455 | let n = name to | ||
456 | rsc = resource to | ||
457 | names <- getNamesForPeer (peer to) | ||
458 | let tostr p = L.decodeUtf8 $ n <$++> "@" <?++> L.fromChunks [p] <++?> "/" <++$> rsc | ||
459 | jidstrs = fmap (toStrict . tostr) names | ||
460 | tojid = head jidstrs | ||
461 | return | ||
462 | [ EventBeginElement "{jabber:client}iq" | ||
463 | [ attr "to" tojid | ||
464 | , attr "id" "someid" | ||
465 | , attr "type" "set" | ||
466 | ] | ||
467 | , EventBeginElement "{jabber:iq:roster}query" [] | ||
468 | , EventBeginElement "{jabber:iq:roster}item" (attr "jid" contact : attrs) | ||
469 | , EventEndElement "{jabber:iq:roster}item" | ||
470 | , EventEndElement "{jabber:iq:roster}query" | ||
471 | , EventEndElement "{jabber:client}iq" | ||
472 | ] | ||
473 | |||
474 | data EventsForClient = CmdChan ClientCommands | ||
475 | | PChan Presence | ||
476 | | RChan RosterEvent | ||
477 | |||
478 | toClient :: (MonadIO m, JabberClientSession session ) => | ||
479 | session -> TChan Presence -> TChan ClientCommands -> TChan RosterEvent -> Source m [XML.Event] | ||
480 | toClient session pchan cmdChan rchan = toClient' False False | ||
481 | where | ||
482 | toClient' isBound isInterested = do | ||
483 | let loop = toClient' isBound isInterested | ||
484 | send xs = yield xs >> prettyPrint ">C: " xs | ||
485 | event <- liftIO . atomically $ | ||
486 | foldr1 orElse [fmap CmdChan $ readTChan cmdChan | ||
487 | ,fmap RChan $ readTChan rchan | ||
488 | ,fmap PChan $ readTChan pchan | ||
489 | ] | ||
490 | case event of | ||
491 | CmdChan QuitThread -> return () | ||
492 | CmdChan (Send xs) -> send xs >> loop | ||
493 | CmdChan BoundToResource -> toClient' True isInterested | ||
494 | CmdChan InterestedInRoster -> do | ||
495 | liftIO . debugStr $ "Roster: interested" | ||
496 | toClient' isBound True | ||
497 | CmdChan (Chat msg) -> do | ||
498 | xs <- liftIO $ xmlifyMessageForClient msg | ||
499 | send xs | ||
500 | loop | ||
501 | -- CmdChan cmd -> liftIO (debugStr $ "unhandled event: "++show cmd) >> loop | ||
502 | RChan (RequestedSubscription who contact) -> do | ||
503 | jid <- liftIO $ getJID session | ||
504 | when (isInterested && Just who==name jid) $ do | ||
505 | r <- liftIO $ rosterPush jid (toStrict . L.decodeUtf8 $ contact) [attr "ask" "subscribe"] | ||
506 | send r | ||
507 | loop | ||
508 | RChan (NewBuddy who contact) -> do | ||
509 | liftIO . debugStr $ "Roster push: NewBuddy "++show (isInterested,who,contact) | ||
510 | (jid,me) <- liftIO $ do | ||
511 | jid <- getJID session | ||
512 | me <- asHostNameJID jid | ||
513 | return (jid,me) | ||
514 | withJust me $ \me -> do | ||
515 | when (isInterested && Just who==name jid) $ do | ||
516 | send [ EventBeginElement "{jabber:client}presence" | ||
517 | [ attrbs "from" contact | ||
518 | , attrbs "to" me | ||
519 | , attr "type" "subscribed" | ||
520 | ] | ||
521 | , EventEndElement "{jabber:client}presence" ] | ||
522 | let f True = "both" | ||
523 | f False = "to" | ||
524 | subscription <- fmap f (liftIO $ isSubscribed session contact) | ||
525 | r <- liftIO . handleIO (\e -> debugStr ("Roster NewBuddy error: "++show e) >> return []) $ do | ||
526 | rosterPush jid | ||
527 | (toStrict . L.decodeUtf8 $ contact) | ||
528 | [attr "subscription" subscription] | ||
529 | send r | ||
530 | loop | ||
531 | RChan (RemovedBuddy who contact) -> do | ||
532 | liftIO . debugStr $ "Roster push: RemovedBuddy "++show (isInterested,who,contact) | ||
533 | (jid,me) <- liftIO $ do | ||
534 | jid <- getJID session | ||
535 | me <- asHostNameJID jid | ||
536 | return (jid,me) | ||
537 | withJust me $ \me -> do | ||
538 | when (isInterested && Just who==name jid) $ do | ||
539 | send [ EventBeginElement "{jabber:client}presence" | ||
540 | [ attrbs "from" contact | ||
541 | , attrbs "to" me | ||
542 | , attr "type" "unsubscribed" | ||
543 | ] | ||
544 | , EventEndElement "{jabber:client}presence" ] | ||
545 | let f True = "from" | ||
546 | f False = "none" | ||
547 | subscription <- fmap f (liftIO $ isSubscribed session contact) | ||
548 | r <- liftIO . handleIO (\e -> debugStr ("Roster RemovedBuddy error: "++show e) >> return []) $ do | ||
549 | rosterPush jid | ||
550 | (toStrict . L.decodeUtf8 $ contact) | ||
551 | [attr "subscription" subscription] | ||
552 | send r | ||
553 | loop | ||
554 | RChan (NewSubscriber who contact) -> do | ||
555 | liftIO . debugStr $ "Roster push: NewSubscriber "++show (isInterested,who,contact) | ||
556 | (jid,me) <- liftIO $ do | ||
557 | jid <- getJID session | ||
558 | me <- asHostNameJID jid | ||
559 | return (jid,me) | ||
560 | withJust me $ \me -> do | ||
561 | when (isInterested && Just who==name jid) $ do | ||
562 | let f True = "both" | ||
563 | f False = "from" | ||
564 | subscription <- fmap f (liftIO $ isBuddy session contact) | ||
565 | r <- liftIO . handleIO (\e -> debugStr ("Roster NewSubscriber error: "++show e) >> return []) $ do | ||
566 | rosterPush jid | ||
567 | (toStrict . L.decodeUtf8 $ contact) | ||
568 | [attr "subscription" subscription] | ||
569 | send r | ||
570 | loop | ||
571 | RChan (RejectSubscriber who contact) -> do | ||
572 | liftIO . debugStr $ "Roster push: RejectSubscriber "++show (isInterested,who,contact) | ||
573 | (jid,me) <- liftIO $ do | ||
574 | jid <- getJID session | ||
575 | me <- asHostNameJID jid | ||
576 | return (jid,me) | ||
577 | withJust me $ \me -> do | ||
578 | when (isInterested && Just who==name jid) $ do | ||
579 | let f True = "to" | ||
580 | f False = "none" | ||
581 | subscription <- fmap f (liftIO $ isBuddy session contact) | ||
582 | r <- liftIO . handleIO (\e -> debugStr ("Roster RejectSubscriber error: "++show e) >> return []) $ do | ||
583 | rosterPush jid | ||
584 | (toStrict . L.decodeUtf8 $ contact) | ||
585 | [attr "subscription" subscription] | ||
586 | send r | ||
587 | loop | ||
588 | RChan (PendingSubscriber who contact) -> do | ||
589 | liftIO . debugStr $ "Roster: Pending buddy "++show (isInterested,who,contact) | ||
590 | (jid,me) <- liftIO $ do | ||
591 | jid <- getJID session | ||
592 | me <- asHostNameJID jid | ||
593 | return (jid,me) | ||
594 | withJust me $ \me -> do | ||
595 | when (isInterested && Just who==name jid) $ do | ||
596 | send [ EventBeginElement "{jabber:client}presence" | ||
597 | [ attrbs "from" contact | ||
598 | , attrbs "to" me | ||
599 | , attr "type" "subscribe" | ||
600 | ] | ||
601 | , EventEndElement "{jabber:client}presence" ] | ||
602 | loop | ||
603 | PChan presence -> do | ||
604 | when isBound $ do | ||
605 | xs <- liftIO $ xmlifyPresenceForClient presence | ||
606 | send xs | ||
607 | loop | ||
608 | |||
609 | |||
610 | {- | ||
611 | handleClient | ||
612 | :: (SocketLike sock, HHead l (XMPPClass session), | ||
613 | JabberClientSession session) => | ||
614 | HCONS sock (HCONS t l) -> Source IO ByteString -> Sink ByteString IO () -> IO () | ||
615 | -} | ||
616 | handleClient st src snk = do | ||
617 | #if MIN_VERSION_HList(0,3,0) | ||
618 | let HCons' sock (HCons' _ st') = st | ||
619 | #else | ||
620 | let HCons sock (HCons _ st') = st | ||
621 | #endif | ||
622 | session_factory = hHead st' | ||
623 | pname <- getPeerName sock | ||
624 | session <- newSession session_factory sock | ||
625 | debugStr $ "PEER NAME: "++Prelude.show pname | ||
626 | pchan <- subscribe session Nothing | ||
627 | rchan <- subscribeToRoster session | ||
628 | let cmdChan = clientChannel session | ||
629 | |||
630 | writer <- async ( toClient session pchan cmdChan rchan `xmlToByteStrings` snk ) | ||
631 | finally ( src $= parseBytes def $$ fromClient session cmdChan ) | ||
632 | $ do | ||
633 | atomically $ writeTChan cmdChan QuitThread | ||
634 | wait writer | ||
635 | closeSession session | ||
636 | |||
637 | {- | ||
638 | listenForXmppClients :: | ||
639 | (HList l, HHead l (XMPPClass session), HExtend e1 l2 l1, | ||
640 | HExtend e l1 (HCONS PortNumber l), JabberClientSession session) => | ||
641 | Family -> e1 -> e -> l2 -> IO ServerHandle | ||
642 | -} | ||
643 | listenForXmppClients addr_family session_factory port st = do | ||
644 | #if MIN_VERSION_HList(0,3,0) | ||
645 | doServer (HCons' addr_family $ HCons' port $ HCons' session_factory st) handleClient | ||
646 | #else | ||
647 | doServer (HCons addr_family $ HCons port $ HCons session_factory st) handleClient | ||
648 | #endif | ||
649 | |||
650 | |||
651 | {- | ||
652 | listenForRemotePeers | ||
653 | :: (HList l, HHead l (XMPPPeerClass session), | ||
654 | HExtend e l1 (HCONS PortNumber l), HExtend e1 l2 l1, | ||
655 | JabberPeerSession session) => | ||
656 | Family -> e1 -> e -> l2 -> IO ServerHandle | ||
657 | -} | ||
658 | listenForRemotePeers addrfamily session_factory port st = do | ||
659 | #if MIN_VERSION_HList(0,3,0) | ||
660 | doServer (HCons' addrfamily $ HCons' port $ HCons' session_factory st) handlePeer | ||
661 | #else | ||
662 | doServer (HCons addrfamily $ HCons port $ HCons session_factory st) handlePeer | ||
663 | #endif | ||
664 | |||
665 | {- | ||
666 | handlePeer | ||
667 | :: (HHead l (XMPPPeerClass session), | ||
668 | JabberPeerSession session) => | ||
669 | HCONS RestrictedSocket (HCONS t1 l) -> Source IO ByteString -> t -> IO () | ||
670 | -} | ||
671 | handlePeer st src snk = do | ||
672 | #if MIN_VERSION_HList(0,3,0) | ||
673 | let HCons' sock (HCons' _ st') = st | ||
674 | #else | ||
675 | let HCons sock (HCons _ st') = st | ||
676 | #endif | ||
677 | session_factory = hHead st' | ||
678 | name <- fmap bshow $ getPeerName sock | ||
679 | debugL $ "(P) connected " <++> name | ||
680 | session <- newPeerSession session_factory sock | ||
681 | |||
682 | didClose <- newTVarIO False | ||
683 | finally ( src $= parseBytes def $$ fromPeer sock session didClose ) | ||
684 | $ do | ||
685 | debugL $ "(P) disconnected " <++> name | ||
686 | didc <- readTVarIO didClose | ||
687 | when (not didc) $ closePeerSession session | ||
688 | |||
689 | |||
690 | handlePeerPresence session stanza False = do | ||
691 | -- Offline | ||
692 | liftIO . debugStr $ "PEER-OFFLINE: "++show stanza | ||
693 | withJust (lookupAttrib "from" (tagAttrs stanza)) $ \jid -> do | ||
694 | peer_jid <- liftIO $ parseAddressJID (textToByteString jid) | ||
695 | liftIO . debugStr $ "PEER-OFFLINE-JID: "++show peer_jid | ||
696 | liftIO $ announcePresence session (Presence peer_jid Offline) | ||
697 | handlePeerPresence session stanza True = do | ||
698 | -- online (Available or Away) | ||
699 | let log = liftIO . debugL . ("(P) " <++>) | ||
700 | withJust (lookupAttrib "from" (tagAttrs stanza)) $ \jid -> do | ||
701 | pjid <- liftIO $ parseAddressJID (textToByteString jid) | ||
702 | -- stat <- show element content | ||
703 | let parseChildren stat = do | ||
704 | child <- nextElement | ||
705 | case child of | ||
706 | Just tag | tagName tag=="{jabber:server}show" | ||
707 | -> fmap toStat (lift content) | ||
708 | Just tag | otherwise -> parseChildren stat | ||
709 | Nothing -> return stat | ||
710 | toStat "away" = Away | ||
711 | toStat "xa" = ExtendedAway | ||
712 | toStat "dnd" = DoNotDisturb | ||
713 | toStat "chat" = Chatty | ||
714 | |||
715 | stat' <- parseChildren Available | ||
716 | liftIO . debugStr $ "announcing peer online: "++show (pjid,stat') | ||
717 | liftIO $ announcePresence session (Presence pjid stat') | ||
718 | log $ bshow (Presence pjid stat') | ||
719 | |||
720 | handlePeerMessage session stanza = do | ||
721 | withJust (lookupAttrib "from" (tagAttrs stanza)) $ \fromstr-> do | ||
722 | withJust (lookupAttrib "to" (tagAttrs stanza)) $ \tostr -> do | ||
723 | fromjid <- liftIO $ parseAddressJID (textToByteString fromstr) | ||
724 | tojid <- liftIO $ parseAddressJID (textToByteString tostr) | ||
725 | let log = liftIO . debugL . ("(P) " <++>) | ||
726 | log $ "handlePeerMessage "<++>bshow stanza | ||
727 | msg <- parseMessage ("{jabber:server}body" | ||
728 | ,"{jabber:server}subject" | ||
729 | ,"{jabber:server}thread" | ||
730 | ) | ||
731 | log | ||
732 | fromjid | ||
733 | tojid | ||
734 | stanza | ||
735 | liftIO $ sendChatToClient session msg | ||
736 | |||
737 | matchAttribMaybe name (Just value) attrs = | ||
738 | case find ( (==name) . fst) attrs of | ||
739 | Just (_,[ContentText x]) | x==value -> True | ||
740 | Just (_,[ContentEntity x]) | x==value -> True | ||
741 | _ -> False | ||
742 | matchAttribMaybe name Nothing attrs | ||
743 | | find ( (==name) . fst) attrs==Nothing | ||
744 | = True | ||
745 | matchAttribMaybe name Nothing attrs | ||
746 | | otherwise | ||
747 | = False | ||
748 | |||
749 | presenceTypeOffline = Just "unavailable" | ||
750 | presenceTypeOnline = Nothing | ||
751 | presenceTypeProbe = Just "probe" | ||
752 | presenceTypeSubscribe = Just "subscribe" | ||
753 | presenceTypeSubscribed = Just "subscribed" | ||
754 | presenceTypeUnsubscribed = Just "unsubscribed" | ||
755 | |||
756 | isPresenceOf (EventBeginElement name attrs) testType | ||
757 | | name=="{jabber:server}presence" | ||
758 | && matchAttribMaybe "type" testType attrs | ||
759 | = True | ||
760 | isPresenceOf _ _ = False | ||
761 | |||
762 | isMessageStanza (EventBeginElement name attrs) | ||
763 | | name=="{jabber:client}message" | ||
764 | = True | ||
765 | isMessageStanza (EventBeginElement name attrs) | ||
766 | | name=="{jabber:server}message" | ||
767 | = True | ||
768 | isMessageStanza _ = False | ||
769 | |||
770 | isClientPresenceOf (EventBeginElement name attrs) testType | ||
771 | | name=="{jabber:client}presence" | ||
772 | && matchAttribMaybe "type" testType attrs | ||
773 | = True | ||
774 | isClientPresenceOf _ _ = False | ||
775 | |||
776 | |||
777 | handlePresenceProbe session stanza = do | ||
778 | withJust (lookupAttrib "to" (tagAttrs stanza)) $ \to -> do | ||
779 | -- withJust (lookupAttrib "from" (tagAttrs stanza)) $ \from -> do | ||
780 | jid <- liftIO $ parseAddressJID $ textToByteString to | ||
781 | withJust (name jid) $ \user -> do | ||
782 | liftIO $ debugL $ "RECEIVED PROBE "<++>bshow (peerAddress session,to) | ||
783 | liftIO $ do | ||
784 | subs <- getSubscribers (peerSessionFactory session) user | ||
785 | liftIO $ debugL $ "subscribers for "<++>bshow user<++>": " <++>bshow subs | ||
786 | forM_ subs $ \jidstr -> do | ||
787 | handleIO_ (return ()) $ do | ||
788 | debugL $ "parsing " <++>jidstr | ||
789 | sub <- parseHostNameJID jidstr | ||
790 | debugStr $ "comparing " ++show (peer sub , peerAddress session) | ||
791 | when (peer sub == discardPort (peerAddress session)) $ do | ||
792 | ps <- userStatus session user | ||
793 | -- todo: Consider making this a directed presence | ||
794 | forM_ ps $ \p -> do | ||
795 | debugStr ("PROBE-REPLY: "++show p) | ||
796 | mapM_ (sendPeerMessage session . OutBoundPresence) ps | ||
797 | return () | ||
798 | |||
799 | subscribeToPresence subscribers peer_jid user = do | ||
800 | pjid <- parseAddressJID peer_jid | ||
801 | if Set.member pjid subscribers | ||
802 | then return () | ||
803 | else return () | ||
804 | |||
805 | bare (JID n host _) = JID n host Nothing | ||
806 | |||
807 | presenceErrorRemoteNotFound iqid from to = return | ||
808 | [ EventBeginElement "{stream:client}presence" | ||
809 | ( case iqid of { Nothing -> id; Just iqid -> ( attr "id" iqid :) } | ||
810 | $ [ attr "from" to | ||
811 | , attr "type" "error" | ||
812 | ] ) | ||
813 | , EventBeginElement "{stream:client}error" | ||
814 | [ attr "type" "modify"] | ||
815 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-stanzas}remote-server-not-found" | ||
816 | [] | ||
817 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-stanzas}remote-server-not-found" | ||
818 | , EventEndElement "{stream:client}error" | ||
819 | , EventEndElement "{stream:client}presence" | ||
820 | ] | ||
821 | |||
822 | presenceSubscribed from = return | ||
823 | [ EventBeginElement "{stream:client}presence" | ||
824 | [ attr "from" from | ||
825 | , attr "type" "subscribed" | ||
826 | ] | ||
827 | , EventEndElement "{stream:client}presence" | ||
828 | ] | ||
829 | |||
830 | clientRequestsSubscription session cmdChan stanza = do | ||
831 | liftIO $ do | ||
832 | debugStr $ "CLIENT PRESENCE SUBSCRIBE " ++ show stanza | ||
833 | withJust (lookupAttrib "to" (tagAttrs stanza)) $ \to_str0 -> do | ||
834 | let to_str = S.takeWhile (/='/') to_str0 | ||
835 | from = lookupAttrib "from" (tagAttrs stanza) | ||
836 | iqid = lookupAttrib "id" (tagAttrs stanza) | ||
837 | let handleError e | isDoesNotExistError e = do | ||
838 | debugStr $ "remote-server-not-found" | ||
839 | r <- presenceErrorRemoteNotFound iqid from to_str | ||
840 | atomically $ writeTChan cmdChan (Send r) | ||
841 | handleError e = do | ||
842 | debugStr $ "ERROR: "++ show e | ||
843 | handleIO handleError $ do | ||
844 | let to_str' = textToByteString to_str | ||
845 | to_jid <- fmap bare $ parseHostNameJID to_str' | ||
846 | if (is_remote . peer) to_jid | ||
847 | then do | ||
848 | addSolicited session to_str' to_jid | ||
849 | debugStr $ "added to solicited: " ++ show to_jid | ||
850 | else do | ||
851 | -- addLocalSubscriber session to_str | ||
852 | -- self <- getJID session | ||
853 | r <- presenceSubscribed to_str -- self | ||
854 | atomically $ writeTChan cmdChan (Send r) | ||
855 | return () | ||
856 | |||
857 | |||
858 | stanzaFromTo :: | ||
859 | JabberPeerSession session => | ||
860 | session -> Event -> IO (Maybe (JID, JID)) | ||
861 | stanzaFromTo session stanza = | ||
862 | let lookup key = fmap textToByteString (lookupAttrib key (tagAttrs stanza)) | ||
863 | parse jidstr = handleIO_ (return Nothing) (fmap Just $ parseAddressJID jidstr) | ||
864 | in case liftM2 (,) (lookup "from") (lookup "to") of | ||
865 | Nothing -> return Nothing | ||
866 | Just (from,to) -> do | ||
867 | mfrom <- parse from | ||
868 | mto <- parse to | ||
869 | case liftM2 (,) mfrom mto of | ||
870 | Nothing -> return Nothing | ||
871 | Just (from,to) -> do | ||
872 | let fromjid = JID (name from) (peerAddress session) Nothing | ||
873 | return $ Just (fromjid,to) | ||
874 | |||
875 | peerRequestsSubsription session stanza = do | ||
876 | liftIO $ debugStr $ "PEER PRESENCE SUBSCRIBE " ++ show stanza | ||
877 | |||
878 | whenJust (liftIO . handleIO (\e -> debugStr ("peerRequestsSubsription: "++show e) >> return Nothing) | ||
879 | $ stanzaFromTo session stanza) $ \(fromjid,tojid) -> do | ||
880 | withJust (name tojid) $ \user -> do | ||
881 | |||
882 | subs <- liftIO $ do | ||
883 | subs <- getSubscribers (peerSessionFactory session) user | ||
884 | msubs <- flip mapM subs $ \str -> do | ||
885 | handleIO_ (return Nothing) | ||
886 | (fmap Just $ parseHostNameJID str) | ||
887 | return (catMaybes msubs) | ||
888 | if elem fromjid subs | ||
889 | then do | ||
890 | liftIO . debugL $ bshow fromjid <++> " already subscribed to " <++> user | ||
891 | -- if already subscribed, reply | ||
892 | liftIO $ do | ||
893 | sendPeerMessage session (Approval tojid fromjid) | ||
894 | ps <- userStatus session user | ||
895 | -- todo: consider making this a directed presence | ||
896 | mapM_ (sendPeerMessage session . OutBoundPresence) ps | ||
897 | else | ||
898 | liftIO $ processRequest session user fromjid | ||
899 | |||
900 | clientApprovesSubscription session stanza = do | ||
901 | liftIO $ debugStr $ "CLIENT APPROVES SUBSCRIPTION" | ||
902 | withJust (lookupAttrib "to" (tagAttrs stanza)) $ \to_str -> do | ||
903 | liftIO $ approveSubscriber session (textToByteString to_str) | ||
904 | |||
905 | clientRejectsSubscription session stanza = do | ||
906 | liftIO $ debugStr $ "CLIENT REJECTS SUBSCRIPTION" | ||
907 | withJust (lookupAttrib "to" (tagAttrs stanza)) $ \to_str -> do | ||
908 | liftIO $ rejectSubscriber session (textToByteString to_str) | ||
909 | |||
910 | peerApprovesSubscription session stanza = do | ||
911 | liftIO $ debugStr $ "PEER APPROVES SUBSCRIPTION" | ||
912 | whenJust (liftIO . handleIO (\e -> debugStr ("peerApprovesSubscription: "++show e) >> return Nothing) | ||
913 | $ stanzaFromTo session stanza) $ \(fromjid,tojid) -> do | ||
914 | withJust (name tojid) $ \user -> do | ||
915 | liftIO $ processApproval session user fromjid | ||
916 | |||
917 | peerRejectsSubscription session stanza = do | ||
918 | liftIO $ debugStr $ "PEER REJECTS SUBSCRIPTION" | ||
919 | whenJust (liftIO . handleIO (\e -> debugStr ("peerRejectsSubscription: "++show e) >> return Nothing) | ||
920 | $ stanzaFromTo session stanza) $ \(fromjid,tojid) -> do | ||
921 | withJust (name tojid) $ \user -> do | ||
922 | liftIO $ processRejection session user fromjid | ||
923 | |||
924 | handlePeerIQGet :: (JabberPeerSession session, MonadIO m) => | ||
925 | session -> XML.Event -> NestingXML o m () | ||
926 | handlePeerIQGet session tag = do | ||
927 | -- TODO: Pings should not require an id field. | ||
928 | withJust (lookupAttrib "id" (tagAttrs tag)) $ \stanza_id -> do | ||
929 | whenJust nextElement $ \child -> do | ||
930 | let unhandledGet req = do | ||
931 | liftIO $ debugStr ("iq-peer-get: "++show (stanza_id,child)) | ||
932 | liftIO $ | ||
933 | sendPeerMessage session (Unsupported (JID Nothing LocalHost Nothing) | ||
934 | (JID Nothing (peerAddress session) Nothing) | ||
935 | (Just (ContentText stanza_id)) | ||
936 | req) | ||
937 | -- Client equiv: liftIO . atomically . writeTChan cmdChan . Send $ iq_service_unavailable host stanza_id Nothing req | ||
938 | case tagName child of | ||
939 | -- "{http://jabber.org/protocol/disco#items}query" -> liftIO $ debugStr "iq-get-query-items" | ||
940 | "{urn:xmpp:ping}ping" -> liftIO $ do | ||
941 | sendPeerMessage session (Pong (JID Nothing LocalHost Nothing) | ||
942 | (JID Nothing (peerAddress session) Nothing) | ||
943 | (Just (ContentText stanza_id))) | ||
944 | -- Client equiv: atomically . writeTChan cmdChan . Send $ pong | ||
945 | return () | ||
946 | |||
947 | req -> unhandledGet req | ||
948 | |||
949 | fromPeer :: (MonadThrow m,MonadIO m, JabberPeerSession session) => | ||
950 | RestrictedSocket -> session -> TVar Bool -> Sink XML.Event m () | ||
951 | fromPeer sock session didClose = doNestingXML $ do | ||
952 | let log = liftIO . debugL . ("(P) " <++>) | ||
953 | withXML $ \begindoc -> do | ||
954 | when (begindoc==EventBeginDocument) $ do | ||
955 | log "begin-doc" | ||
956 | withXML $ \xml -> do | ||
957 | withJust (elementAttrs "stream" xml) $ \stream_attrs -> do | ||
958 | log $ "stream atributes: " <++> bshow stream_attrs | ||
959 | |||
960 | let doTimeout = Thunk $ do | ||
961 | atomically $ writeTVar didClose True | ||
962 | closePeerSession session | ||
963 | |||
964 | fix $ \loop -> do | ||
965 | log "waiting for stanza." | ||
966 | whenJust nextElement $ \stanza -> do | ||
967 | stanza_lvl <- nesting | ||
968 | |||
969 | liftIO $ sendPeerMessage session (ActivityBump doTimeout) -- reset ping timer | ||
970 | |||
971 | let unhandledStanza = do | ||
972 | xs <- gatherElement stanza Seq.empty | ||
973 | prettyPrint "P: " (toList xs) | ||
974 | case () of | ||
975 | _ | stanza `isServerIQOf` iqTypeGet -> handlePeerIQGet session stanza | ||
976 | _ | stanza `isPresenceOf` presenceTypeOnline | ||
977 | -> handlePeerPresence session stanza True | ||
978 | _ | stanza `isPresenceOf` presenceTypeOffline | ||
979 | -> handlePeerPresence session stanza False | ||
980 | _ | stanza `isPresenceOf` presenceTypeProbe | ||
981 | -> handlePresenceProbe session stanza | ||
982 | _ | stanza `isPresenceOf` presenceTypeSubscribe | ||
983 | -> peerRequestsSubsription session stanza | ||
984 | _ | stanza `isPresenceOf` presenceTypeSubscribed | ||
985 | -> peerApprovesSubscription session stanza | ||
986 | _ | stanza `isPresenceOf` presenceTypeUnsubscribed | ||
987 | -> peerRejectsSubscription session stanza | ||
988 | _ | isMessageStanza stanza | ||
989 | -> handlePeerMessage session stanza | ||
990 | _ -> unhandledStanza | ||
991 | |||
992 | awaitCloser stanza_lvl | ||
993 | loop | ||
994 | |||
995 | log $ "end of stream" | ||
996 | withXML $ \xml -> do | ||
997 | log $ "end-of-document: " <++> bshow xml | ||
998 | |||
999 | |||
1000 | |||
1001 | |||
1002 | newServerConnections = newTVar Map.empty | ||
1003 | |||
1004 | data CachedMessages = CachedMessages | ||
1005 | { presences :: Map JID JabberShow | ||
1006 | , probes :: Map JID (Set (Bool,JID)) -- False means solicitation rather than probe | ||
1007 | , approvals :: Map JID (Set (Bool,JID) ) -- False means rejection rather than approval | ||
1008 | } | ||
1009 | |||
1010 | instance CommandCache CachedMessages where | ||
1011 | type CacheableCommand CachedMessages = OutBoundMessage | ||
1012 | emptyCache = CachedMessages Map.empty Map.empty Map.empty | ||
1013 | |||
1014 | updateCache (OutBoundPresence (Presence jid Offline)) cache = | ||
1015 | cache { presences=Map.delete jid . presences $ cache } | ||
1016 | updateCache (OutBoundPresence p@(Presence jid st)) cache = | ||
1017 | cache { presences=Map.insert jid st . presences $ cache } | ||
1018 | updateCache (PresenceProbe from to) cache = | ||
1019 | cache { probes = mmInsert (True,from) to $ probes cache } | ||
1020 | updateCache (Solicitation from to) cache = | ||
1021 | cache { probes= mmInsert (False,from) to $ probes cache } | ||
1022 | updateCache (Approval from to) cache = | ||
1023 | cache { approvals= mmInsert (True,from) to $ approvals cache } | ||
1024 | updateCache (Rejection from to) cache = | ||
1025 | cache { approvals= mmInsert (False,from) to $ approvals cache } | ||
1026 | updateCache (OutBoundMessage msg) cache = cache -- TODO: cache chat? | ||
1027 | updateCache (Pong _ _ _) cache = trace "(DISCARDING Pong)" cache -- pings are not cached | ||
1028 | updateCache (Unsupported _ _ _ _) cache = cache -- error messages are not cached | ||
1029 | updateCache (ActivityBump sock) cache = cache | ||
1030 | |||
1031 | instance ThreadChannelCommand OutBoundMessage where | ||
1032 | isQuitCommand Disconnect = True | ||
1033 | isQuitCommand _ = False | ||
1034 | |||
1035 | mmInsert val key mm = Map.alter f key mm | ||
1036 | where | ||
1037 | f Nothing = Just $ Set.singleton val | ||
1038 | f (Just set) = Just $ Set.insert val set | ||
1039 | |||
1040 | |||
1041 | greetPeer = | ||
1042 | [ EventBeginDocument | ||
1043 | , EventBeginElement (streamP "stream") | ||
1044 | [ attr "xmlns" "jabber:server" | ||
1045 | , attr "version" "1.0" | ||
1046 | ] | ||
1047 | ] | ||
1048 | |||
1049 | goodbyePeer = | ||
1050 | [ EventEndElement (streamP "stream") | ||
1051 | , EventEndDocument | ||
1052 | ] | ||
1053 | |||
1054 | peerJidTextLocal sock jid = do | ||
1055 | addr <- getSocketName sock | ||
1056 | return . toStrict . L.decodeUtf8 | ||
1057 | $ name jid <$++> "@" | ||
1058 | <?++> showPeer (RemotePeer addr) | ||
1059 | <++?> "/" <++$> resource jid | ||
1060 | |||
1061 | peerJidTextRemote sock jid = do | ||
1062 | addr <- getPeerName sock | ||
1063 | return . toStrict . L.decodeUtf8 | ||
1064 | $ name jid <$++> "@" | ||
1065 | <?++> showPeer (RemotePeer addr) | ||
1066 | <++?> "/" <++$> resource jid | ||
1067 | |||
1068 | presenceStanza sock fromjid tojid typ = do | ||
1069 | from <- peerJidTextLocal sock fromjid | ||
1070 | let to = toStrict . L.decodeUtf8 | ||
1071 | $ name tojid <$++> "@" | ||
1072 | <?++> showPeer (peer tojid) | ||
1073 | return | ||
1074 | [ EventBeginElement "{jabber:server}presence" | ||
1075 | [ attr "from" from | ||
1076 | , attr "to" to | ||
1077 | , attr "type" typ | ||
1078 | ] | ||
1079 | , EventEndElement "{jabber:server}presence" | ||
1080 | ] | ||
1081 | |||
1082 | |||
1083 | toPeer | ||
1084 | :: SocketLike sock => | ||
1085 | sock | ||
1086 | -> CachedMessages | ||
1087 | -> TChan OutBoundMessage | ||
1088 | -> (Maybe OutBoundMessage -> IO ()) | ||
1089 | -> ConduitM i [Event] IO () | ||
1090 | toPeer sock cache chan fail = do | ||
1091 | let -- log = liftIO . debugL . ("(>P) " <++>) | ||
1092 | send xs = yield xs >> prettyPrint ">P: " xs -- >> return (3::Int) | ||
1093 | checkConnection cmd = do | ||
1094 | liftIO $ catchIO (getPeerName sock >> return ()) | ||
1095 | (\_ -> fail . Just $ cmd) | ||
1096 | sendOrFail getXML cmd = do | ||
1097 | checkConnection cmd | ||
1098 | r <- liftIO $ getXML | ||
1099 | -- handleIO (\e -> debugStr ("ERROR: "++show e) >> return []) getXML | ||
1100 | yieldOr r (fail . Just $ cmd) | ||
1101 | prettyPrint ">P: " r | ||
1102 | sendPresence presence = | ||
1103 | sendOrFail (xmlifyPresenceForPeer sock presence) | ||
1104 | (OutBoundPresence presence) | ||
1105 | sendProbe from to = | ||
1106 | sendOrFail (presenceStanza sock from to "probe") | ||
1107 | (PresenceProbe from to) | ||
1108 | sendSolicitation from to = | ||
1109 | sendOrFail (presenceStanza sock from to "subscribe") | ||
1110 | (Solicitation from to) | ||
1111 | sendApproval approve from to = | ||
1112 | sendOrFail (presenceStanza sock from to | ||
1113 | (if approve then "subscribed" else "unsubscribed")) | ||
1114 | (if approve then Approval from to | ||
1115 | else Rejection from to) | ||
1116 | sendMessage msg = | ||
1117 | sendOrFail (xmlifyMessageForPeer sock msg) | ||
1118 | (OutBoundMessage msg) | ||
1119 | |||
1120 | sendPong from to mid = do | ||
1121 | liftIO . debugL $ "SEND PONG" | ||
1122 | sendOrFail (xmlifyPong sock from to mid) | ||
1123 | (Pong from to mid) | ||
1124 | where | ||
1125 | xmlifyPong sock from to mid = do | ||
1126 | fromjid <- peerJidTextLocal sock to | ||
1127 | tojid <- peerJidTextRemote sock to | ||
1128 | return $ [ EventBeginElement "{jabber:server}iq" | ||
1129 | $ (case mid of | ||
1130 | Just c -> (("id",[c]):) | ||
1131 | _ -> id ) | ||
1132 | [ attr "type" "result" | ||
1133 | , attr "to" tojid | ||
1134 | , attr "from" fromjid | ||
1135 | ] | ||
1136 | , EventEndElement "{jabber:server}iq" | ||
1137 | ] | ||
1138 | sendUnsupported from to mid tag = | ||
1139 | sendOrFail (xmlifyUnsupported sock from to mid tag) | ||
1140 | (Unsupported from to mid tag) | ||
1141 | where | ||
1142 | xmlifyUnsupported sock from to mid req = do | ||
1143 | fromjid <- peerJidTextLocal sock to | ||
1144 | tojid <- peerJidTextRemote sock to | ||
1145 | return $ | ||
1146 | [ EventBeginElement "{jabber:server}iq" | ||
1147 | $ (case mid of | ||
1148 | Just c -> (("id",[c]):) | ||
1149 | _ -> id ) | ||
1150 | [("type",[ContentText "error"]) | ||
1151 | , attr "to" tojid | ||
1152 | , attr "from" fromjid | ||
1153 | ] | ||
1154 | , EventBeginElement req [] | ||
1155 | , EventEndElement req | ||
1156 | , EventBeginElement "{jabber:server}error" [("type",[ContentText "cancel"])] | ||
1157 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable" [] | ||
1158 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable" | ||
1159 | , EventEndElement "{jabber:server}error" | ||
1160 | , EventEndElement "{jabber:server}iq" | ||
1161 | ] | ||
1162 | |||
1163 | |||
1164 | send greetPeer | ||
1165 | forM_ (Map.assocs . approvals $ cache) $ \(to,froms) -> do | ||
1166 | forM_ (Set.toList froms) $ \(approve,from) -> do | ||
1167 | liftIO $ debugL "sending cached approval/rejection..." | ||
1168 | sendApproval approve from to | ||
1169 | forM_ (Map.assocs . presences $ cache) $ \(jid,st) -> do | ||
1170 | sendPresence (Presence jid st) | ||
1171 | forM_ (Map.assocs . probes $ cache) $ \(to,froms) -> do | ||
1172 | forM_ (Set.toList froms) $ \(got,from) -> do | ||
1173 | if got | ||
1174 | then do | ||
1175 | liftIO $ debugL "sending cached probe..." | ||
1176 | sendProbe from to | ||
1177 | else do | ||
1178 | liftIO $ debugL "sending cached solicitation..." | ||
1179 | sendSolicitation from to | ||
1180 | |||
1181 | |||
1182 | let five_sec = 5 * 1000000 :: Int | ||
1183 | pingref <- liftIO $ do | ||
1184 | ping_timer <- liftIO $ newDelay five_sec | ||
1185 | newTVarIO (ping_timer,0::Int) | ||
1186 | |||
1187 | sockref <- liftIO $ atomically newEmptyTMVar | ||
1188 | let bump fromsock = do | ||
1189 | remote <- liftIO $ catchIO (fmap Just $ getPeerName sock) | ||
1190 | (\_ -> return Nothing) | ||
1191 | debugL $ "PING BUMP" <++?> fmap (showPeer . RemotePeer) remote | ||
1192 | timer <- atomically $ do | ||
1193 | tryTakeTMVar sockref | ||
1194 | putTMVar sockref fromsock | ||
1195 | (timer,v) <- readTVar pingref | ||
1196 | writeTVar pingref (timer,0) | ||
1197 | return timer | ||
1198 | updateDelay timer five_sec | ||
1199 | waitPing = do | ||
1200 | (timer,v) <- readTVar pingref | ||
1201 | waitDelay timer | ||
1202 | return v | ||
1203 | |||
1204 | fix $ \loop -> do | ||
1205 | liftIO . debugStr $ "LOOP waiting..." | ||
1206 | event <- lift . atomically $ orElse (Left `fmap` readTChan chan) | ||
1207 | (Right `fmap` waitPing) | ||
1208 | liftIO . debugStr $ "LOOP event = " ++ show event | ||
1209 | let sendPing n = do | ||
1210 | case n of | ||
1211 | 0 -> do | ||
1212 | ping <- liftIO makePing | ||
1213 | yield ping | ||
1214 | liftIO . debugL $ "SEND PING" | ||
1215 | prettyPrint ">P: " ping | ||
1216 | ping_timer <- liftIO $ newDelay five_sec | ||
1217 | liftIO . atomically $ writeTVar pingref (ping_timer,1) | ||
1218 | loop | ||
1219 | 1 -> do | ||
1220 | remote <- liftIO $ getPeerName sock | ||
1221 | liftIO . debugL $ "PING TIMEOUT: " <++> showPeer (RemotePeer remote) | ||
1222 | fromsock <- liftIO $ atomically $ readTMVar sockref | ||
1223 | -- liftIO $ sClose fromsock | ||
1224 | liftIO $ runThunk fromsock | ||
1225 | |||
1226 | return () -- PING TIMEOUT (loop quits) | ||
1227 | x -> error ("What? "++show x) | ||
1228 | where makePing = do | ||
1229 | addr <- getSocketName sock | ||
1230 | remote <- getPeerName sock | ||
1231 | let from = toStrict . L.decodeUtf8 . showPeer $ RemotePeer addr | ||
1232 | to = toStrict . L.decodeUtf8 . showPeer $ RemotePeer remote | ||
1233 | mid = Just (ContentText "iduno") | ||
1234 | return $ | ||
1235 | [ EventBeginElement "{jabber:server}iq" | ||
1236 | $ (case mid of | ||
1237 | Just c -> (("id",[c]):) | ||
1238 | _ -> id ) | ||
1239 | [ ("type",[ContentText "get"]) | ||
1240 | , attr "to" to | ||
1241 | , attr "from" from | ||
1242 | ] | ||
1243 | , EventBeginElement "{urn:xmpp:ping}ping" [] | ||
1244 | , EventEndElement "{urn:xmpp:ping}ping" | ||
1245 | , EventEndElement "{jabber:server}iq" ] | ||
1246 | chanEvent event = do | ||
1247 | case event of | ||
1248 | OutBoundPresence p -> sendPresence p | ||
1249 | PresenceProbe from to -> do | ||
1250 | liftIO $ debugL "sending live probe..." | ||
1251 | sendProbe from to | ||
1252 | Solicitation from to -> do | ||
1253 | liftIO $ debugL "sending live solicitation..." | ||
1254 | sendSolicitation from to | ||
1255 | Approval from to -> do | ||
1256 | liftIO . debugL $ "sending approval "<++>bshow (from,to) | ||
1257 | sendApproval True from to | ||
1258 | Rejection from to -> do | ||
1259 | liftIO . debugL $ "sending rejection "<++>bshow (from,to) | ||
1260 | sendApproval False from to | ||
1261 | OutBoundMessage msg -> sendMessage msg | ||
1262 | Pong from to mid -> do | ||
1263 | liftIO . debugL $ "sending pong "<++>bshow (from,to) | ||
1264 | sendPong from to mid | ||
1265 | Unsupported from to mid tag -> sendUnsupported from to mid tag | ||
1266 | Disconnect -> return () | ||
1267 | ActivityBump fromsock -> liftIO (bump fromsock) | ||
1268 | when (not . isQuitCommand $ event) loop | ||
1269 | either chanEvent sendPing event | ||
1270 | return () | ||
1271 | -- send goodbyePeer -- TODO: why does this cause an exception? | ||
1272 | -- Text/XML/Stream/Render.hs:169:5-15: | ||
1273 | -- Irrefutable pattern failed for pattern (sl : s') | ||
1274 | |||
1275 | |||
1276 | |||
1277 | |||
1278 | seekRemotePeers :: JabberPeerSession config => | ||
1279 | XMPPPeerClass config -> TChan Presence -> OutgoingConnections CachedMessages -> IO b0 | ||
1280 | seekRemotePeers config chan server_connections = do | ||
1281 | fix $ \loop -> do | ||
1282 | event <- atomically $ readTChan chan | ||
1283 | case event of | ||
1284 | p@(Presence jid stat) | not (is_remote (peer jid)) -> do | ||
1285 | -- debugL $ "seekRemotePeers: " <++> L.show jid <++> " " <++> bshow stat | ||
1286 | runMaybeT $ do | ||
1287 | u <- MaybeT . return $ name jid | ||
1288 | subscribers <- liftIO $ do | ||
1289 | subs <- getSubscribers config u | ||
1290 | mapM parseHostNameJID subs | ||
1291 | -- liftIO . debugL $ "subscribers: " <++> bshow subscribers | ||
1292 | let peers = Set.map peer (Set.fromList subscribers) | ||
1293 | forM_ (Set.toList peers) $ \peer -> do | ||
1294 | when (is_remote peer) $ | ||
1295 | liftIO $ sendMessage server_connections (OutBoundPresence p) peer | ||
1296 | _ -> return (Just ()) | ||
1297 | loop | ||
1298 | |||
1299 | xmlifyPresenceForPeer sock (Presence jid stat) = do | ||
1300 | addr <- getSocketName sock | ||
1301 | let n = name jid | ||
1302 | rsc = resource jid | ||
1303 | jidstr = toStrict . L.decodeUtf8 | ||
1304 | $ n <$++> "@" <?++> showPeer (RemotePeer addr) <++?> "/" <++$> rsc | ||
1305 | return $ | ||
1306 | [ EventBeginElement "{jabber:server}presence" | ||
1307 | (attr "from" jidstr:typ stat) ] | ||
1308 | ++ ( shw stat >>= jabberShow ) ++ | ||
1309 | [ EventEndElement "{jabber:server}presence" ] | ||
1310 | where | ||
1311 | typ Offline = [attr "type" "unavailable"] | ||
1312 | typ _ = [] | ||
1313 | |||
1314 | shw ExtendedAway = ["xa"] | ||
1315 | shw Chatty = ["chat"] | ||
1316 | shw Away = ["away"] | ||
1317 | shw DoNotDisturb = ["dnd"] | ||
1318 | shw _ = [] | ||
1319 | jabberShow stat = | ||
1320 | [ EventBeginElement "{jabber:server}show" [] | ||
1321 | , EventContent (ContentText stat) | ||
1322 | , EventEndElement "{jabber:server}show" ] | ||
1323 | |||
1324 | xmlifyMessageForClient msg = do | ||
1325 | let tojid = msgTo msg | ||
1326 | fromjid = msgFrom msg | ||
1327 | tonames <- getNamesForPeer (peer tojid) | ||
1328 | fromnames <- getNamesForPeer (peer fromjid) | ||
1329 | let mk_str ns jid = toStrict . L.decodeUtf8 $ name jid <$++> "@" <?++> L.fromChunks [head ns] <++?> "/" <++$> resource jid | ||
1330 | to_str = mk_str tonames tojid | ||
1331 | from_str = mk_str fromnames fromjid | ||
1332 | tags = ( "{jabber:client}subject" | ||
1333 | , "{jabber:client}body" | ||
1334 | ) | ||
1335 | return $ | ||
1336 | [ EventBeginElement "{jabber:client}message" | ||
1337 | [ attr "from" from_str | ||
1338 | , attr "to" to_str | ||
1339 | ] | ||
1340 | ] | ||
1341 | ++ xmlifyMsgElements tags (msgLangMap msg) ++ | ||
1342 | [ EventEndElement "{jabber:client}message" ] | ||
1343 | |||
1344 | |||
1345 | xmlifyMessageForPeer sock msg = do | ||
1346 | addr <- getSocketName sock | ||
1347 | remote <- getPeerName sock | ||
1348 | let n = name (msgFrom msg) | ||
1349 | rsc = resource (msgFrom msg) | ||
1350 | jidstr = toStrict . L.decodeUtf8 | ||
1351 | $ n <$++> "@" <?++> showPeer (RemotePeer addr) <++?> "/" <++$> rsc | ||
1352 | tostr = toStrict . L.decodeUtf8 | ||
1353 | $ name (msgTo msg) <$++> "@" | ||
1354 | <?++> showPeer (RemotePeer remote) <++?> "/" | ||
1355 | <++$> resource (msgTo msg) | ||
1356 | tags = ( "{jabber:server}subject" | ||
1357 | , "{jabber:server}body" | ||
1358 | ) | ||
1359 | return $ | ||
1360 | [ EventBeginElement "{jabber:server}message" | ||
1361 | [ attr "from" jidstr | ||
1362 | , attr "to" tostr | ||
1363 | ] | ||
1364 | ] | ||
1365 | ++ xmlifyMsgElements tags (msgLangMap msg) ++ | ||
1366 | [ EventEndElement "{jabber:server}message" ] | ||
1367 | |||
1368 | xmlifyMsgElements tags langmap = concatMap (uncurry (langElements tags)) . Map.toList $ langmap | ||
1369 | |||
1370 | langElements (subjecttag,bodytag) lang msg = | ||
1371 | ( maybeToList (msgSubject msg) | ||
1372 | >>= wrap subjecttag ) | ||
1373 | ++ ( maybeToList (msgBody msg) | ||
1374 | >>= wrap bodytag ) | ||
1375 | ++ ( Set.toList (msgElements msg) | ||
1376 | >>= wrapTriple ) | ||
1377 | where | ||
1378 | wrap name content = | ||
1379 | [ EventBeginElement name | ||
1380 | ( if lang/="" then [attr "xml:lang" lang] | ||
1381 | else [] ) | ||
1382 | , EventContent (ContentText content) | ||
1383 | , EventEndElement name | ||
1384 | ] | ||
1385 | wrapTriple (name,attrs,content) = | ||
1386 | [ EventBeginElement name attrs -- Note: we assume lang specified in attrs | ||
1387 | , EventContent (ContentText content) | ||
1388 | , EventEndElement name | ||
1389 | ] | ||
1390 | |||
1391 | |||
1392 | handleClientMessage session stanza = do | ||
1393 | let log = liftIO . debugL . ("(C) " <++>) | ||
1394 | log $ "handleClientMessage "<++>bshow stanza | ||
1395 | from <- liftIO $ getJID session | ||
1396 | withJust (lookupAttrib "to" (tagAttrs stanza)) $ \to_str -> do | ||
1397 | log $ " to = "<++>bshow to_str | ||
1398 | tojid <- liftIO $ parseHostNameJID (textToByteString to_str) | ||
1399 | msg <- parseMessage ("{jabber:client}body" | ||
1400 | ,"{jabber:client}subject" | ||
1401 | ,"{jabber:client}thread" | ||
1402 | ) | ||
1403 | log | ||
1404 | from | ||
1405 | tojid | ||
1406 | stanza | ||
1407 | liftIO $ sendChat session msg | ||
1408 | |||
1409 | {- | ||
1410 | unhandled-C: <message | ||
1411 | unhandled-C: type="chat" | ||
1412 | unhandled-C: id="purplea0a7fd24" | ||
1413 | unhandled-C: to="user@vm2" | ||
1414 | unhandled-C: xmlns="jabber:client"> | ||
1415 | unhandled-C: <active xmlns="http://jabber.org/protocol/chatstates"/> | ||
1416 | unhandled-C: <body> | ||
1417 | unhandled-C: hello dude | ||
1418 | unhandled-C: </body> | ||
1419 | unhandled-C: </message> | ||
1420 | -} | ||
1421 | parseMessage (bodytag,subjecttag,threadtag) log from tojid stanza = do | ||
1422 | let emptyMsg = LangSpecificMessage { msgBody=Nothing, msgSubject=Nothing, msgElements=Set.empty } | ||
1423 | parseChildren (th,cmap) = do | ||
1424 | child <- nextElement | ||
1425 | lvl <- nesting | ||
1426 | xmllang <- xmlLang | ||
1427 | let lang = maybe "" id xmllang | ||
1428 | let c = maybe emptyMsg id (Map.lookup lang cmap) | ||
1429 | log $ " child: "<++> bshow child | ||
1430 | case child of | ||
1431 | Just tag | tagName tag==bodytag | ||
1432 | -> do | ||
1433 | txt <- lift content | ||
1434 | awaitCloser lvl | ||
1435 | parseChildren (th,Map.insert lang (c { msgBody=Just txt }) cmap) | ||
1436 | Just tag | tagName tag==subjecttag | ||
1437 | -> do | ||
1438 | txt <- lift content | ||
1439 | awaitCloser lvl | ||
1440 | parseChildren (th,Map.insert lang (c { msgSubject=Just txt }) cmap) | ||
1441 | Just tag | tagName tag==threadtag | ||
1442 | -> do | ||
1443 | txt <- lift content | ||
1444 | awaitCloser lvl | ||
1445 | parseChildren (th {msgThreadContent=txt},cmap) | ||
1446 | Just tag -> do | ||
1447 | let nm = tagName tag | ||
1448 | attrs = tagAttrs tag | ||
1449 | elems = msgElements c | ||
1450 | txt <- lift content | ||
1451 | awaitCloser lvl | ||
1452 | parseChildren (th,Map.insert lang (c {msgElements=Set.insert (nm,attrs,txt) elems}) cmap) | ||
1453 | Nothing -> return (th,cmap) | ||
1454 | (th,langmap) <- parseChildren ( MessageThread {msgThreadParent=Nothing, msgThreadContent=""} | ||
1455 | , Map.empty ) | ||
1456 | return Message { | ||
1457 | msgTo = tojid, | ||
1458 | msgFrom = from, | ||
1459 | msgLangMap = langmap, | ||
1460 | msgThread = if msgThreadContent th/="" then Just th else Nothing | ||
1461 | } | ||
diff --git a/Presence/XMPPServer.hs b/Presence/XMPPServer.hs new file mode 100644 index 00000000..2e3c0a37 --- /dev/null +++ b/Presence/XMPPServer.hs | |||
@@ -0,0 +1,1829 @@ | |||
1 | {-# LANGUAGE CPP #-} | ||
2 | {-# LANGUAGE OverloadedStrings #-} | ||
3 | {-# LANGUAGE RankNTypes #-} | ||
4 | {-# LANGUAGE FlexibleInstances #-} -- instance for TChan Event | ||
5 | {-# LANGUAGE DoAndIfThenElse #-} | ||
6 | module XMPPServer | ||
7 | ( xmppServer | ||
8 | , ConnectionKey(..) | ||
9 | , XMPPServerParameters(..) | ||
10 | , XMPPServer | ||
11 | , addPeer | ||
12 | , StanzaWrap(..) | ||
13 | , Stanza(..) | ||
14 | , StanzaType(..) | ||
15 | , StanzaOrigin(..) | ||
16 | , cloneStanza | ||
17 | , LangSpecificMessage(..) | ||
18 | , peerKeyToText | ||
19 | , addrToText | ||
20 | , sendModifiedStanzaToPeer | ||
21 | , sendModifiedStanzaToClient | ||
22 | , presenceProbe | ||
23 | , presenceSolicitation | ||
24 | , makePresenceStanza | ||
25 | , makeInformSubscription | ||
26 | , makeRosterUpdate | ||
27 | , makeMessage | ||
28 | , JabberShow(..) | ||
29 | , Server | ||
30 | ) where | ||
31 | |||
32 | import ConnectionKey | ||
33 | import qualified Control.Concurrent.STM.UpdateStream as Slotted | ||
34 | import Nesting | ||
35 | import Server | ||
36 | import EventUtil | ||
37 | import ControlMaybe | ||
38 | import LockedChan | ||
39 | import PeerResolve | ||
40 | import Blaze.ByteString.Builder (Builder) | ||
41 | |||
42 | import Debug.Trace | ||
43 | import System.IO (hFlush,stdout) | ||
44 | import Control.Monad.Trans.Resource | ||
45 | import Control.Monad.Trans (lift) | ||
46 | import Control.Monad.IO.Class (MonadIO, liftIO) | ||
47 | import Control.Monad.Fix (fix) | ||
48 | import Control.Monad | ||
49 | import Control.Concurrent (forkIO) | ||
50 | import Control.Concurrent.STM | ||
51 | -- import Control.Concurrent.STM.TChan | ||
52 | import Network.Socket | ||
53 | import Text.Printf | ||
54 | import System.Posix.Signals | ||
55 | import Data.ByteString (ByteString) | ||
56 | import qualified Data.ByteString.Char8 as Strict8 | ||
57 | -- import qualified Data.ByteString.Lazy.Char8 as Lazy8 | ||
58 | import Data.Int (Int8) | ||
59 | |||
60 | import Data.Conduit | ||
61 | import qualified Data.Conduit.List as CL | ||
62 | import qualified Data.Conduit.Binary as CB | ||
63 | import Data.Conduit.Blaze (builderToByteStringFlush) | ||
64 | |||
65 | import qualified Text.XML.Stream.Render as XML hiding (content) | ||
66 | import qualified Text.XML.Stream.Parse as XML | ||
67 | import Data.XML.Types as XML | ||
68 | import Data.Maybe | ||
69 | import Data.List (nub) | ||
70 | import Data.Monoid ( (<>) ) | ||
71 | import Data.Text (Text) | ||
72 | import qualified Data.Text as Text (pack,unpack,words,intercalate) | ||
73 | import Data.Char (toUpper,chr,ord) | ||
74 | import Data.Map (Map) | ||
75 | import qualified Data.Map as Map | ||
76 | import Data.Set (Set, (\\) ) | ||
77 | import qualified Data.Set as Set | ||
78 | import Data.String ( IsString(..) ) | ||
79 | import qualified System.Random | ||
80 | import Data.Void (Void) | ||
81 | import System.Endian (toBE32) | ||
82 | import Control.Applicative | ||
83 | |||
84 | peerport :: PortNumber | ||
85 | peerport = 5269 | ||
86 | clientport :: PortNumber | ||
87 | clientport = 5222 | ||
88 | |||
89 | my_uuid :: Text | ||
90 | my_uuid = "154ae29f-98f2-4af4-826d-a40c8a188574" | ||
91 | |||
92 | data JabberShow = Offline | ||
93 | | ExtendedAway | ||
94 | | Away | ||
95 | | DoNotDisturb | ||
96 | | Available | ||
97 | | Chatty | ||
98 | deriving (Show,Enum,Ord,Eq,Read) | ||
99 | |||
100 | data MessageThread = MessageThread { | ||
101 | msgThreadParent :: Maybe Text, | ||
102 | msgThreadContent :: Text | ||
103 | } | ||
104 | deriving (Show,Eq) | ||
105 | |||
106 | data LangSpecificMessage = | ||
107 | LangSpecificMessage { msgBody :: Maybe Text | ||
108 | , msgSubject :: Maybe Text | ||
109 | } | ||
110 | deriving (Show,Eq) | ||
111 | |||
112 | data RosterEventType | ||
113 | = RequestedSubscription | ||
114 | | NewBuddy -- preceded by PresenceInformSubscription True | ||
115 | | RemovedBuddy -- preceded by PresenceInformSubscription False | ||
116 | | PendingSubscriber -- same as PresenceRequestSubscription | ||
117 | | NewSubscriber | ||
118 | | RejectSubscriber | ||
119 | deriving (Show,Read,Ord,Eq,Enum) | ||
120 | |||
121 | data ClientHack = SimulatedChatErrors | ||
122 | deriving (Show,Read,Ord,Eq,Enum) | ||
123 | |||
124 | data StanzaType | ||
125 | = Unrecognized | ||
126 | | Ping | ||
127 | | Pong | ||
128 | | RequestResource (Maybe Text) | ||
129 | | SetResource | ||
130 | | SessionRequest | ||
131 | | UnrecognizedQuery Name | ||
132 | | RequestRoster | ||
133 | | Roster | ||
134 | | RosterEvent { rosterEventType :: RosterEventType | ||
135 | , rosterUser :: Text | ||
136 | , rosterContact :: Text } | ||
137 | | Error StanzaError XML.Event | ||
138 | | PresenceStatus { presenceShow :: JabberShow | ||
139 | , presencePriority :: Maybe Int8 | ||
140 | , presenceStatus :: [(Lang,Text)] | ||
141 | , presenceWhiteList :: [Text] | ||
142 | } | ||
143 | | PresenceInformError | ||
144 | | PresenceInformSubscription Bool | ||
145 | | PresenceRequestStatus | ||
146 | | PresenceRequestSubscription Bool | ||
147 | | Message { msgThread :: Maybe MessageThread | ||
148 | , msgLangMap :: [(Lang,LangSpecificMessage)] | ||
149 | } | ||
150 | | NotifyClientVersion { versionName :: Text | ||
151 | , versionVersion :: Text } | ||
152 | | InternalEnableHack ClientHack | ||
153 | | InternalCacheId Text | ||
154 | deriving (Show,Eq) | ||
155 | |||
156 | data StanzaOrigin = LocalPeer | NetworkOrigin ConnectionKey (TChan Stanza) | ||
157 | |||
158 | data StanzaWrap a = Stanza | ||
159 | { stanzaType :: StanzaType | ||
160 | , stanzaId :: Maybe Text | ||
161 | , stanzaTo :: Maybe Text | ||
162 | , stanzaFrom :: Maybe Text | ||
163 | , stanzaChan :: a | ||
164 | , stanzaClosers :: TVar (Maybe [XML.Event]) | ||
165 | , stanzaInterrupt :: TMVar () | ||
166 | , stanzaOrigin :: StanzaOrigin | ||
167 | } | ||
168 | |||
169 | type Stanza = StanzaWrap (LockedChan XML.Event) | ||
170 | |||
171 | data XMPPServerParameters = | ||
172 | XMPPServerParameters | ||
173 | { -- | Called when a client requests a resource id. The Maybe value is the | ||
174 | -- client's preference. | ||
175 | xmppChooseResourceName :: ConnectionKey -> SockAddr -> Maybe Text -> IO Text | ||
176 | , -- | This should indicate the server's hostname that all client's see. | ||
177 | xmppTellMyNameToClient :: IO Text | ||
178 | , xmppTellMyNameToPeer :: SockAddr -> IO Text | ||
179 | , xmppTellClientHisName :: ConnectionKey -> IO Text | ||
180 | , xmppTellPeerHisName :: ConnectionKey -> IO Text | ||
181 | , xmppNewConnection :: ConnectionKey -> SockAddr -> TChan Stanza -> IO () | ||
182 | , xmppEOF :: ConnectionKey -> IO () | ||
183 | , xmppRosterBuddies :: ConnectionKey -> IO [Text] | ||
184 | , xmppRosterSubscribers :: ConnectionKey -> IO [Text] | ||
185 | , xmppRosterSolicited :: ConnectionKey -> IO [Text] | ||
186 | , xmppRosterOthers :: ConnectionKey -> IO [Text] | ||
187 | , -- | Called when after sending a roster to a client. Usually this means | ||
188 | -- the client status should change from "available" to "interested". | ||
189 | xmppSubscribeToRoster :: ConnectionKey -> IO () | ||
190 | -- , xmppLookupClientJID :: ConnectionKey -> IO Text | ||
191 | , xmppTellClientNameOfPeer :: ConnectionKey -> [Text] -> IO Text | ||
192 | , xmppDeliverMessage :: (IO ()) -> Stanza -> IO () | ||
193 | -- | Called whenever a local client's presence changes. | ||
194 | , xmppInformClientPresence :: ConnectionKey -> Stanza -> IO () | ||
195 | -- | Called whenever a remote peer's presence changes. | ||
196 | , xmppInformPeerPresence :: ConnectionKey -> Stanza -> IO () | ||
197 | , -- | Called when a remote peer requests our status. | ||
198 | xmppAnswerProbe :: ConnectionKey -> Stanza -> TChan Stanza -> IO () | ||
199 | , xmppClientSubscriptionRequest :: IO () -> ConnectionKey -> Stanza -> TChan Stanza -> IO () | ||
200 | , -- | Called when a remote peer sends subscription request. | ||
201 | xmppPeerSubscriptionRequest :: IO () -> ConnectionKey -> Stanza -> TChan Stanza -> IO () | ||
202 | , xmppClientInformSubscription :: IO () -> ConnectionKey -> Stanza -> IO () | ||
203 | , -- | Called when a remote peer informs us of our subscription status. | ||
204 | xmppPeerInformSubscription :: IO () -> ConnectionKey -> Stanza -> IO () | ||
205 | , xmppVerbosity :: IO Int | ||
206 | } | ||
207 | |||
208 | |||
209 | enableClientHacks :: | ||
210 | forall t a. | ||
211 | (Eq a, IsString a) => | ||
212 | a -> t -> TChan Stanza -> IO () | ||
213 | enableClientHacks "Pidgin" version replyto = do | ||
214 | wlog "Enabling hack SimulatedChatErrors for client Pidgin" | ||
215 | donevar <- atomically newEmptyTMVar | ||
216 | sendReply donevar | ||
217 | (InternalEnableHack SimulatedChatErrors) | ||
218 | [] | ||
219 | replyto | ||
220 | enableClientHacks "irssi-xmpp" version replyto = do | ||
221 | wlog "Enabling hack SimulatedChatErrors for client irssi-xmpp" | ||
222 | donevar <- atomically newEmptyTMVar | ||
223 | sendReply donevar | ||
224 | (InternalEnableHack SimulatedChatErrors) | ||
225 | [] | ||
226 | replyto | ||
227 | enableClientHacks _ _ _ = return () | ||
228 | |||
229 | cacheMessageId :: Text -> TChan Stanza -> IO () | ||
230 | cacheMessageId id' replyto = do | ||
231 | wlog $ "Caching id " ++ Text.unpack id' | ||
232 | donevar <- atomically newEmptyTMVar | ||
233 | sendReply donevar | ||
234 | (InternalCacheId id') | ||
235 | [] | ||
236 | replyto | ||
237 | |||
238 | |||
239 | -- TODO: http://xmpp.org/rfcs/rfc6120.html#rules-remote-error | ||
240 | -- client connection | ||
241 | -- socat script to send stanza fragment | ||
242 | -- copyToChannel can keep a stack of closers to append to finish-off a stanza | ||
243 | -- the TMVar () from forkConnection can be passed and with a stanza to detect interruption | ||
244 | |||
245 | addrToText :: SockAddr -> Text | ||
246 | addrToText (addr@(SockAddrInet _ _)) = Text.pack $ stripColon (show addr) | ||
247 | where stripColon s = pre where (pre,port) = break (==':') s | ||
248 | addrToText (addr@(SockAddrInet6 _ _ _ _)) = Text.pack $ stripColon (show addr) | ||
249 | where stripColon s = if null bracket then pre else pre ++ "]" | ||
250 | where | ||
251 | (pre,bracket) = break (==']') s | ||
252 | |||
253 | peerKeyToText :: ConnectionKey -> Text | ||
254 | peerKeyToText (PeerKey { callBackAddress=addr }) = addrToText addr | ||
255 | peerKeyToText (ClientKey { localAddress=addr }) = "ErrorClIeNt0" | ||
256 | |||
257 | |||
258 | wlog :: String -> IO () | ||
259 | wlog s = putStrLn s >> hFlush stdout | ||
260 | |||
261 | wlogb :: ByteString -> IO () | ||
262 | wlogb s = Strict8.putStrLn s >> hFlush stdout | ||
263 | |||
264 | flushPassThrough :: Monad m => Conduit a m b -> Conduit (Flush a) m (Flush b) | ||
265 | flushPassThrough c = getZipConduit $ ZipConduit (onlyChunks =$= mapOutput Chunk c) <* ZipConduit onlyFlushes | ||
266 | where | ||
267 | onlyChunks :: Monad m => Conduit (Flush a) m a | ||
268 | onlyFlushes :: Monad m => Conduit (Flush a) m (Flush b) | ||
269 | onlyChunks = awaitForever yieldChunk | ||
270 | onlyFlushes = awaitForever yieldFlush | ||
271 | yieldFlush Flush = yield Flush | ||
272 | yieldFlush _ = return () | ||
273 | yieldChunk (Chunk x) = yield x | ||
274 | yieldChunk _ = return () | ||
275 | |||
276 | xmlStream :: ReadCommand -> WriteCommand -> ( Source IO XML.Event | ||
277 | , Sink (Flush XML.Event) IO () ) | ||
278 | xmlStream conread conwrite = (xsrc,xsnk) | ||
279 | where | ||
280 | xsrc = src $= XML.parseBytes XML.def | ||
281 | xsnk :: Sink (Flush Event) IO () | ||
282 | xsnk = -- XML.renderBytes XML.def =$ snk | ||
283 | flushPassThrough (XML.renderBuilder XML.def) | ||
284 | =$= builderToByteStringFlush | ||
285 | =$= discardFlush | ||
286 | =$ snk | ||
287 | where | ||
288 | discardFlush :: Monad m => ConduitM (Flush a) a m () | ||
289 | discardFlush = awaitForever yieldChunk | ||
290 | yieldChunk (Chunk x) = yield x | ||
291 | yieldChunk _ = return () | ||
292 | |||
293 | src = do | ||
294 | v <- lift conread | ||
295 | maybe (return ()) -- lift . wlog $ "conread: Nothing") | ||
296 | (yield >=> const src) | ||
297 | v | ||
298 | snk = awaitForever $ liftIO . conwrite | ||
299 | |||
300 | |||
301 | type FlagCommand = STM Bool | ||
302 | type ReadCommand = IO (Maybe ByteString) | ||
303 | type WriteCommand = ByteString -> IO Bool | ||
304 | |||
305 | cloneStanza :: StanzaWrap (LockedChan a) -> IO (StanzaWrap (LockedChan a)) | ||
306 | cloneStanza stanza = do | ||
307 | dupped <- cloneLChan (stanzaChan stanza) | ||
308 | return stanza { stanzaChan = dupped } | ||
309 | |||
310 | copyToChannel | ||
311 | :: MonadIO m => | ||
312 | (Event -> a) -> LockedChan a -> TVar (Maybe [Event]) -> ConduitM Event Event m () | ||
313 | copyToChannel f chan closer_stack = awaitForever copy | ||
314 | where | ||
315 | copy x = do | ||
316 | liftIO . atomically $ writeLChan chan (f x) | ||
317 | case x of | ||
318 | EventBeginDocument {} -> do | ||
319 | let clsr = closerFor x | ||
320 | liftIO . atomically $ | ||
321 | modifyTVar' closer_stack (fmap (clsr:)) | ||
322 | EventEndDocument {} -> do | ||
323 | liftIO . atomically $ | ||
324 | modifyTVar' closer_stack (fmap (drop 1)) | ||
325 | _ -> return () | ||
326 | yield x | ||
327 | |||
328 | |||
329 | prettyPrint :: ByteString -> ConduitM Event Void IO () | ||
330 | prettyPrint prefix = | ||
331 | XML.renderBytes (XML.def { XML.rsPretty=True }) | ||
332 | =$= CB.lines | ||
333 | =$ CL.mapM_ (wlogb . (prefix <>)) | ||
334 | |||
335 | swapNamespace :: Monad m => Text -> Text -> ConduitM Event Event m () | ||
336 | swapNamespace old new = awaitForever (yield . swapit old new) | ||
337 | |||
338 | swapit :: Text -> Text -> Event -> Event | ||
339 | swapit old new (EventBeginElement n as) | nameNamespace n==Just old = | ||
340 | EventBeginElement (n { nameNamespace = Just new }) as | ||
341 | swapit old new (EventEndElement n) | nameNamespace n==Just old = | ||
342 | EventEndElement (n { nameNamespace = Just new }) | ||
343 | swapit old new x = x | ||
344 | |||
345 | fixHeaders :: Monad m => Stanza -> ConduitM Event Event m () | ||
346 | fixHeaders Stanza { stanzaType=typ, stanzaTo=mto, stanzaFrom=mfrom } = do | ||
347 | x <- await | ||
348 | maybe (return ()) f x | ||
349 | where | ||
350 | f (EventBeginElement n as) = do yield $ EventBeginElement n (update n as) | ||
351 | awaitForever yield | ||
352 | f x = yield x >> awaitForever yield | ||
353 | update n as = as3 | ||
354 | where | ||
355 | as' = maybe as (setAttrib "to" as) mto | ||
356 | as'' = maybe as' (setAttrib "from" as') mfrom | ||
357 | as3 = case typ of | ||
358 | PresenceStatus {} | nameNamespace n == Just "jabber:client" | ||
359 | -> delAttrib "whitelist" as'' | ||
360 | PresenceStatus {} | otherwise | ||
361 | -> case presenceWhiteList typ of | ||
362 | [] -> delAttrib "whitelist" as'' | ||
363 | ws -> setAttrib "whitelist" as'' (Text.intercalate " " ws) | ||
364 | _ -> as'' | ||
365 | |||
366 | setAttrib akey as aval = attr akey aval:filter ((/=akey) . fst) as | ||
367 | delAttrib akey as = filter ((/=akey) . fst) as | ||
368 | |||
369 | conduitToChan | ||
370 | :: Conduit () IO Event | ||
371 | -> IO (LockedChan Event, TVar (Maybe [Event]), TMVar a) | ||
372 | conduitToChan c = do | ||
373 | chan <- atomically newLockedChan | ||
374 | clsrs <- atomically $ newTVar (Just []) | ||
375 | quitvar <- atomically $ newEmptyTMVar | ||
376 | forkIO $ do | ||
377 | c =$= copyToChannel id chan clsrs $$ awaitForever (const $ return ()) | ||
378 | atomically $ writeTVar clsrs Nothing | ||
379 | return (chan,clsrs,quitvar) | ||
380 | |||
381 | conduitToStanza | ||
382 | :: StanzaType | ||
383 | -> Maybe Text -- ^ id | ||
384 | -> Maybe Text -- ^ from | ||
385 | -> Maybe Text -- ^ to | ||
386 | -> Conduit () IO Event | ||
387 | -> IO Stanza | ||
388 | conduitToStanza stype mid from to c = do | ||
389 | (chan,clsrs,quitvar) <- conduitToChan c | ||
390 | return | ||
391 | Stanza { stanzaType = stype | ||
392 | , stanzaId = mid | ||
393 | , stanzaTo = to | ||
394 | , stanzaFrom = from | ||
395 | , stanzaChan = chan | ||
396 | , stanzaClosers = clsrs | ||
397 | , stanzaInterrupt = quitvar | ||
398 | , stanzaOrigin = LocalPeer | ||
399 | } | ||
400 | |||
401 | |||
402 | ioWriteChan :: MonadIO m => TChan a -> a -> m () | ||
403 | ioWriteChan c v = liftIO . atomically $ writeTChan c v | ||
404 | |||
405 | stanzaToConduit :: MonadIO m => Stanza -> ConduitM i Event m () | ||
406 | stanzaToConduit stanza = do | ||
407 | let xchan = stanzaChan stanza | ||
408 | xfin = stanzaClosers stanza | ||
409 | rdone = stanzaInterrupt stanza | ||
410 | loop = return () | ||
411 | xchan <- liftIO $ unlockChan xchan | ||
412 | fix $ \inner -> do | ||
413 | what <- liftIO . atomically $ foldr1 orElse | ||
414 | [readTChan xchan >>= \xml -> return $ do | ||
415 | yield xml -- atomically $ Slotted.push slots Nothing xml | ||
416 | inner | ||
417 | ,do mb <- readTVar xfin | ||
418 | cempty <- isEmptyTChan xchan | ||
419 | if isNothing mb | ||
420 | then if cempty then return loop else retry | ||
421 | else do done <- tryReadTMVar rdone | ||
422 | check (isJust done) | ||
423 | trace "todo: send closers" retry | ||
424 | ,do isEmptyTChan xchan >>= check | ||
425 | readTMVar rdone | ||
426 | return (return ())] | ||
427 | what | ||
428 | |||
429 | |||
430 | sendModifiedStanzaToPeer :: Stanza -> TChan Stanza -> IO () | ||
431 | sendModifiedStanzaToPeer stanza chan = do | ||
432 | (echan,clsrs,quitvar) <- conduitToChan c | ||
433 | ioWriteChan chan | ||
434 | stanza { stanzaChan = echan | ||
435 | , stanzaClosers = clsrs | ||
436 | , stanzaInterrupt = quitvar | ||
437 | , stanzaType = processedType (stanzaType stanza) | ||
438 | -- TODO id? origin? | ||
439 | } | ||
440 | where | ||
441 | old = "jabber:client" | ||
442 | new = "jabber:server" | ||
443 | c = stanzaToConduit stanza =$= swapNamespace old new =$= fixHeaders stanza | ||
444 | processedType (Error cond tag) = Error cond (swapit old new tag) | ||
445 | processedType x = x | ||
446 | |||
447 | |||
448 | sendModifiedStanzaToClient :: Stanza -> TChan Stanza -> IO () | ||
449 | sendModifiedStanzaToClient stanza chan = do | ||
450 | (echan,clsrs,quitvar) <- conduitToChan c | ||
451 | -- wlog $ "send-to-client " ++ show (stanzaId stanza) | ||
452 | ioWriteChan chan | ||
453 | stanza { stanzaChan = echan | ||
454 | , stanzaClosers = clsrs | ||
455 | , stanzaInterrupt = quitvar | ||
456 | , stanzaType = processedType (stanzaType stanza) | ||
457 | -- TODO id? origin? | ||
458 | } | ||
459 | where | ||
460 | old = "jabber:server" | ||
461 | new = "jabber:client" | ||
462 | c = stanzaToConduit stanza =$= swapNamespace old new =$= fixHeaders stanza | ||
463 | processedType (Error cond tag) = Error cond (swapit old new tag) | ||
464 | processedType x = x | ||
465 | |||
466 | |||
467 | -- id,to, and from are taken as-is from reply list | ||
468 | -- todo: this should probably be restricted to IO monad | ||
469 | sendReply :: (Functor m, MonadIO m) => TMVar () -> StanzaType -> [Event] -> TChan Stanza -> m () | ||
470 | sendReply donevar stype reply replychan = do | ||
471 | let stanzaTag = listToMaybe reply | ||
472 | mid = stanzaTag >>= lookupAttrib "id" . tagAttrs | ||
473 | mfrom = stanzaTag >>= lookupAttrib "from" . tagAttrs | ||
474 | mto = stanzaTag >>= lookupAttrib "to" . tagAttrs | ||
475 | isInternal (InternalEnableHack {}) = True | ||
476 | isInternal (InternalCacheId {}) = True | ||
477 | isInternal _ = False | ||
478 | flip (maybe $ return ()) | ||
479 | (fmap (const ()) stanzaTag `mplus` guard (isInternal stype)) | ||
480 | . const $ do | ||
481 | replyStanza <- liftIO . atomically $ do | ||
482 | replyChan <- newLockedChan | ||
483 | replyClsrs <- newTVar (Just []) | ||
484 | return Stanza { stanzaType = stype | ||
485 | , stanzaId = mid | ||
486 | , stanzaTo = mto -- as-is from reply list | ||
487 | , stanzaFrom = mfrom -- as-is from reply list | ||
488 | , stanzaChan = replyChan | ||
489 | , stanzaClosers = replyClsrs | ||
490 | , stanzaInterrupt = donevar | ||
491 | , stanzaOrigin = LocalPeer | ||
492 | } | ||
493 | ioWriteChan replychan replyStanza | ||
494 | void . liftIO . forkIO $ do | ||
495 | mapM_ (liftIO . atomically . writeLChan (stanzaChan replyStanza)) reply | ||
496 | liftIO . atomically $ writeTVar (stanzaClosers replyStanza) Nothing | ||
497 | -- liftIO $ wlog "finished reply stanza" | ||
498 | |||
499 | stanzaFromList :: StanzaType -> [Event] -> IO Stanza | ||
500 | stanzaFromList stype reply = do | ||
501 | let stanzaTag = listToMaybe reply | ||
502 | mid = stanzaTag >>= lookupAttrib "id" . tagAttrs | ||
503 | mfrom = stanzaTag >>= lookupAttrib "from" . tagAttrs | ||
504 | mto = stanzaTag >>= lookupAttrib "to" . tagAttrs | ||
505 | {- | ||
506 | isInternal (InternalEnableHack {}) = True | ||
507 | isInternal (InternalCacheId {}) = True | ||
508 | isInternal _ = False | ||
509 | -} | ||
510 | (donevar,replyChan,replyClsrs) <- atomically $ do | ||
511 | donevar <- newEmptyTMVar -- TMVar () | ||
512 | replyChan <- newLockedChan | ||
513 | replyClsrs <- newTVar (Just []) | ||
514 | return (donevar,replyChan, replyClsrs) | ||
515 | forkIO $ do | ||
516 | forM_ reply $ atomically . writeLChan replyChan | ||
517 | atomically $ do putTMVar donevar () | ||
518 | writeTVar replyClsrs Nothing | ||
519 | return Stanza { stanzaType = stype | ||
520 | , stanzaId = mid | ||
521 | , stanzaTo = mto -- as-is from reply list | ||
522 | , stanzaFrom = mfrom -- as-is from reply list | ||
523 | , stanzaChan = replyChan | ||
524 | , stanzaClosers = replyClsrs | ||
525 | , stanzaInterrupt = donevar | ||
526 | , stanzaOrigin = LocalPeer | ||
527 | } | ||
528 | |||
529 | grokStanzaIQGet :: Monad m => XML.Event -> NestingXML o m (Maybe StanzaType) | ||
530 | grokStanzaIQGet stanza = do | ||
531 | mtag <- nextElement | ||
532 | flip (maybe $ return Nothing) mtag $ \tag -> do | ||
533 | case tagName tag of | ||
534 | "{urn:xmpp:ping}ping" -> return $ Just Ping | ||
535 | "{jabber:iq:roster}query" -> return $ Just RequestRoster | ||
536 | name -> return . Just $ UnrecognizedQuery name | ||
537 | |||
538 | parseClientVersion :: NestingXML o IO (Maybe StanzaType) | ||
539 | parseClientVersion = parseit Nothing Nothing | ||
540 | where | ||
541 | reportit mname mver = return $ do | ||
542 | name <- mname | ||
543 | ver <- mver | ||
544 | return NotifyClientVersion { versionName=name, versionVersion=ver } | ||
545 | parseit :: Maybe Text -> Maybe Text -> NestingXML o IO (Maybe StanzaType) | ||
546 | parseit mname mver = do | ||
547 | mtag <- nextElement | ||
548 | flip (maybe $ reportit mname mver) mtag $ \tag -> do | ||
549 | case tagName tag of | ||
550 | "{jabber:iq:version}name" -> do | ||
551 | x <- XML.content | ||
552 | parseit (Just x) mver | ||
553 | "{jabber:iq:version}version" -> do | ||
554 | x <- XML.content | ||
555 | parseit mname (Just x) | ||
556 | _ -> parseit mname mver | ||
557 | |||
558 | |||
559 | grokStanzaIQResult :: XML.Event -> NestingXML o IO (Maybe StanzaType) | ||
560 | grokStanzaIQResult stanza = do | ||
561 | mtag <- nextElement | ||
562 | flip (maybe $ return (Just Pong)) mtag $ \tag -> do | ||
563 | case tagName tag of | ||
564 | "{jabber:iq:version}query" | nameNamespace (tagName stanza)==Just "jabber:client" | ||
565 | -> parseClientVersion | ||
566 | _ -> return Nothing | ||
567 | |||
568 | grokStanzaIQSet :: XML.Event -> NestingXML o IO (Maybe StanzaType) | ||
569 | grokStanzaIQSet stanza = do | ||
570 | mtag <- nextElement | ||
571 | flip (maybe $ return Nothing) mtag $ \tag -> do | ||
572 | case tagName tag of | ||
573 | "{urn:ietf:params:xml:ns:xmpp-bind}bind" -> do | ||
574 | mchild <- nextElement | ||
575 | case fmap tagName mchild of | ||
576 | Just "{urn:ietf:params:xml:ns:xmpp-bind}resource" -> do | ||
577 | rsc <- XML.content -- TODO: MonadThrow??? | ||
578 | return . Just $ RequestResource (Just rsc) | ||
579 | Just _ -> return Nothing | ||
580 | Nothing -> return . Just $ RequestResource Nothing | ||
581 | "{urn:ietf:params:xml:ns:xmpp-session}session" -> do | ||
582 | return $ Just SessionRequest | ||
583 | _ -> return Nothing | ||
584 | |||
585 | |||
586 | {- | ||
587 | C->Unrecognized <iq | ||
588 | C->Unrecognized type="set" | ||
589 | C->Unrecognized id="purpleae62d88f" | ||
590 | C->Unrecognized xmlns="jabber:client"> | ||
591 | C->Unrecognized <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/> | ||
592 | C->Unrecognized </iq> | ||
593 | -} | ||
594 | chanContents :: TChan x -> IO [x] | ||
595 | chanContents ch = do | ||
596 | x <- atomically $ do | ||
597 | bempty <- isEmptyTChan ch | ||
598 | if bempty | ||
599 | then return Nothing | ||
600 | else fmap Just $ readTChan ch | ||
601 | maybe (return []) | ||
602 | (\x -> do | ||
603 | xs <- chanContents ch | ||
604 | return (x:xs)) | ||
605 | x | ||
606 | |||
607 | |||
608 | parsePresenceStatus | ||
609 | :: ( MonadThrow m | ||
610 | , MonadIO m | ||
611 | ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) | ||
612 | parsePresenceStatus ns stanzaTag = do | ||
613 | |||
614 | let toStat "away" = Away | ||
615 | toStat "xa" = ExtendedAway | ||
616 | toStat "dnd" = DoNotDisturb | ||
617 | toStat "chat" = Chatty | ||
618 | |||
619 | showv <- liftIO . atomically $ newTVar Available | ||
620 | priov <- liftIO . atomically $ newTVar Nothing | ||
621 | statusv <- liftIO . atomically $ newTChan | ||
622 | fix $ \loop -> do | ||
623 | mtag <- nextElement | ||
624 | flip (maybe $ return ()) mtag $ \tag -> do | ||
625 | when (nameNamespace (tagName tag) == Just ns) $ do | ||
626 | case nameLocalName (tagName tag) of | ||
627 | "show" -> do t <- XML.content | ||
628 | liftIO . atomically $ writeTVar showv (toStat t) | ||
629 | "priority" -> do t <- XML.content | ||
630 | liftIO . handleIO_ (return ()) $ do | ||
631 | prio <- readIO (Text.unpack t) | ||
632 | atomically $ writeTVar priov (Just prio) | ||
633 | "status" -> do t <- XML.content | ||
634 | lang <- xmlLang | ||
635 | ioWriteChan statusv (maybe "" id lang,t) | ||
636 | _ -> return () | ||
637 | loop | ||
638 | show <- liftIO . atomically $ readTVar showv | ||
639 | prio <- liftIO . atomically $ readTVar priov | ||
640 | status <- liftIO $ chanContents statusv -- Could use unsafeInterleaveIO to | ||
641 | -- avoid multiple passes, but whatever. | ||
642 | let wlist = do | ||
643 | w <- maybeToList $ lookupAttrib "whitelist" (tagAttrs stanzaTag) | ||
644 | Text.words w | ||
645 | return . Just $ PresenceStatus { presenceShow = show | ||
646 | , presencePriority = prio | ||
647 | , presenceStatus = status | ||
648 | , presenceWhiteList = wlist | ||
649 | } | ||
650 | grokPresence | ||
651 | :: ( MonadThrow m | ||
652 | , MonadIO m | ||
653 | ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) | ||
654 | grokPresence ns stanzaTag = do | ||
655 | let typ = lookupAttrib "type" (tagAttrs stanzaTag) | ||
656 | case typ of | ||
657 | Nothing -> parsePresenceStatus ns stanzaTag | ||
658 | Just "unavailable" -> fmap (fmap (\p -> p {presenceShow=Offline})) | ||
659 | $ parsePresenceStatus ns stanzaTag | ||
660 | Just "error" -> return . Just $ PresenceInformError | ||
661 | Just "unsubscribed" -> return . Just $ PresenceInformSubscription False | ||
662 | Just "subscribed" -> return . Just $ PresenceInformSubscription True | ||
663 | Just "probe" -> return . Just $ PresenceRequestStatus | ||
664 | Just "unsubscribe" -> return . Just $ PresenceRequestSubscription False | ||
665 | Just "subscribe" -> return . Just $ PresenceRequestSubscription True | ||
666 | _ -> return Nothing | ||
667 | |||
668 | parseMessage | ||
669 | :: ( MonadThrow m | ||
670 | , MonadIO m | ||
671 | ) => Text -> XML.Event -> NestingXML o m StanzaType | ||
672 | parseMessage ns stanza = do | ||
673 | let bodytag = Name { nameNamespace = Just ns | ||
674 | , nameLocalName = "body" | ||
675 | , namePrefix = Nothing } | ||
676 | subjecttag = Name { nameNamespace = Just ns | ||
677 | , nameLocalName = "subject" | ||
678 | , namePrefix = Nothing } | ||
679 | threadtag = Name { nameNamespace = Just ns | ||
680 | , nameLocalName = "thread" | ||
681 | , namePrefix = Nothing } | ||
682 | let emptyMsg = LangSpecificMessage { msgBody=Nothing, msgSubject=Nothing } | ||
683 | parseChildren (th,cmap) = do | ||
684 | child <- nextElement | ||
685 | lvl <- nesting | ||
686 | xmllang <- xmlLang | ||
687 | let lang = maybe "" id xmllang | ||
688 | let c = maybe emptyMsg id (Map.lookup lang cmap) | ||
689 | -- log $ " child: "<> bshow child | ||
690 | case child of | ||
691 | Just tag | tagName tag==bodytag | ||
692 | -> do | ||
693 | txt <- XML.content | ||
694 | awaitCloser lvl | ||
695 | parseChildren (th,Map.insert lang (c { msgBody=Just txt }) cmap) | ||
696 | Just tag | tagName tag==subjecttag | ||
697 | -> do | ||
698 | txt <- XML.content | ||
699 | awaitCloser lvl | ||
700 | parseChildren (th,Map.insert lang (c { msgSubject=Just txt }) cmap) | ||
701 | Just tag | tagName tag==threadtag | ||
702 | -> do | ||
703 | txt <- XML.content | ||
704 | awaitCloser lvl | ||
705 | parseChildren (th {msgThreadContent=txt},cmap) | ||
706 | Just tag -> do | ||
707 | -- let nm = tagName tag | ||
708 | -- attrs = tagAttrs tag | ||
709 | -- -- elems = msgElements c | ||
710 | -- txt <- XML.content | ||
711 | awaitCloser lvl | ||
712 | parseChildren (th,Map.insert lang c cmap) | ||
713 | Nothing -> return (th,cmap) | ||
714 | (th,langmap) <- parseChildren ( MessageThread {msgThreadParent=Nothing, msgThreadContent=""} | ||
715 | , Map.empty ) | ||
716 | return Message { | ||
717 | msgLangMap = Map.toList langmap, | ||
718 | msgThread = if msgThreadContent th/="" then Just th else Nothing | ||
719 | } | ||
720 | |||
721 | findConditionTag :: Monad m => NestingXML o m (Maybe XML.Event) | ||
722 | findConditionTag = do | ||
723 | x <- nextElement | ||
724 | flip (maybe $ return Nothing) x $ \x -> do | ||
725 | case nameNamespace (tagName x) of | ||
726 | Just "urn:ietf:params:xml:ns:xmpp-stanzas" -> return (Just x) | ||
727 | _ -> findConditionTag | ||
728 | |||
729 | conditionFromText :: Text -> Maybe StanzaError | ||
730 | conditionFromText t = fmap fst $ listToMaybe ss | ||
731 | where | ||
732 | es = [BadRequest .. UnexpectedRequest] | ||
733 | ts = map (\e->(e,errorTagLocalName e)) es | ||
734 | ss = dropWhile ((/=t) . snd) ts | ||
735 | |||
736 | findErrorTag :: Monad m => Text -> NestingXML o m (Maybe StanzaError) | ||
737 | findErrorTag ns = do | ||
738 | x <- nextElement | ||
739 | flip (maybe $ return Nothing) x $ \x -> do | ||
740 | case tagName x of | ||
741 | n | nameNamespace n==Just ns && nameLocalName n=="error" | ||
742 | -> do | ||
743 | mtag <- findConditionTag | ||
744 | return $ do | ||
745 | tag <- {- trace ("mtag = "++show mtag) -} mtag | ||
746 | let t = nameLocalName (tagName tag) | ||
747 | conditionFromText t | ||
748 | _ -> findErrorTag ns | ||
749 | |||
750 | grokMessage | ||
751 | :: ( MonadThrow m | ||
752 | , MonadIO m | ||
753 | ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) | ||
754 | grokMessage ns stanzaTag = do | ||
755 | let typ = lookupAttrib "type" (tagAttrs stanzaTag) | ||
756 | case typ of | ||
757 | Just "error" -> do | ||
758 | mb <- findErrorTag ns | ||
759 | return $ do | ||
760 | e <- mb | ||
761 | return $ Error e stanzaTag | ||
762 | _ -> do t <- parseMessage ns stanzaTag | ||
763 | return $ Just t | ||
764 | |||
765 | |||
766 | |||
767 | grokStanza | ||
768 | :: Text -> XML.Event -> NestingXML o IO (Maybe StanzaType) | ||
769 | grokStanza "jabber:server" stanzaTag = | ||
770 | case () of | ||
771 | _ | stanzaTag `isServerIQOf` "get" -> grokStanzaIQGet stanzaTag | ||
772 | _ | stanzaTag `isServerIQOf` "result" -> grokStanzaIQResult stanzaTag | ||
773 | _ | tagName stanzaTag == "{jabber:server}presence" -> grokPresence "jabber:server" stanzaTag | ||
774 | _ | tagName stanzaTag == "{jabber:server}message" -> grokMessage "jabber:server" stanzaTag | ||
775 | _ -> return $ Just Unrecognized | ||
776 | |||
777 | grokStanza "jabber:client" stanzaTag = | ||
778 | case () of | ||
779 | _ | stanzaTag `isClientIQOf` "get" -> grokStanzaIQGet stanzaTag | ||
780 | _ | stanzaTag `isClientIQOf` "set" -> grokStanzaIQSet stanzaTag | ||
781 | _ | stanzaTag `isClientIQOf` "result" -> grokStanzaIQResult stanzaTag | ||
782 | _ | tagName stanzaTag == "{jabber:client}presence" -> grokPresence "jabber:client" stanzaTag | ||
783 | _ | tagName stanzaTag == "{jabber:client}message" -> grokMessage "jabber:client" stanzaTag | ||
784 | _ -> return $ Just Unrecognized | ||
785 | |||
786 | mkname :: Text -> Text -> XML.Name | ||
787 | mkname namespace name = (Name name (Just namespace) Nothing) | ||
788 | |||
789 | makeMessage :: Text -> Text -> Text -> Text -> IO Stanza | ||
790 | makeMessage namespace from to bod = | ||
791 | stanzaFromList typ | ||
792 | $ [ EventBeginElement (mkname namespace "message") | ||
793 | [ attr "from" from | ||
794 | , attr "to" to | ||
795 | ] | ||
796 | , EventBeginElement (mkname namespace "body") [] | ||
797 | , EventContent (ContentText bod) | ||
798 | , EventEndElement (mkname namespace "body") | ||
799 | , EventEndElement (mkname namespace "message") ] | ||
800 | where | ||
801 | typ = Message { msgThread = Nothing | ||
802 | , msgLangMap = [("", lsm)] | ||
803 | } | ||
804 | lsm = LangSpecificMessage | ||
805 | { msgBody = Just bod | ||
806 | , msgSubject = Nothing } | ||
807 | |||
808 | makeInformSubscription :: Text -> Text -> Text -> Bool -> IO Stanza | ||
809 | makeInformSubscription namespace from to approved = | ||
810 | stanzaFromList (PresenceInformSubscription approved) | ||
811 | $ [ EventBeginElement (mkname namespace "presence") | ||
812 | [ attr "from" from | ||
813 | , attr "to" to | ||
814 | , attr "type" $ if approved then "subscribed" | ||
815 | else "unsubscribed" ] | ||
816 | , EventEndElement (mkname namespace "presence")] | ||
817 | |||
818 | makePresenceStanza :: Text -> Maybe Text -> JabberShow -> IO Stanza | ||
819 | makePresenceStanza namespace mjid pstat = do | ||
820 | stanzaFromList PresenceStatus { presenceShow = pstat | ||
821 | , presencePriority = Nothing | ||
822 | , presenceStatus = [] | ||
823 | , presenceWhiteList = [] | ||
824 | } | ||
825 | $ [ EventBeginElement (mkname namespace "presence") | ||
826 | (setFrom $ typ pstat) ] | ||
827 | ++ (shw pstat >>= jabberShow) ++ | ||
828 | [ EventEndElement (mkname namespace "presence")] | ||
829 | where | ||
830 | setFrom = maybe id | ||
831 | (\jid -> (attr "from" jid :) ) | ||
832 | mjid | ||
833 | typ Offline = [attr "type" "unavailable"] | ||
834 | typ _ = [] | ||
835 | shw ExtendedAway = ["xa"] | ||
836 | shw Chatty = ["chat"] | ||
837 | shw Away = ["away"] | ||
838 | shw DoNotDisturb = ["dnd"] | ||
839 | shw _ = [] | ||
840 | jabberShow stat = | ||
841 | [ EventBeginElement "{jabber:client}show" [] | ||
842 | , EventContent (ContentText stat) | ||
843 | , EventEndElement "{jabber:client}show" ] | ||
844 | |||
845 | makeRosterUpdate :: Text -> Text -> [(Name, Text)] -> IO Stanza | ||
846 | makeRosterUpdate tojid contact as = do | ||
847 | let attrs = map (uncurry attr) as | ||
848 | stanzaFromList Unrecognized | ||
849 | [ EventBeginElement "{jabber:client}iq" | ||
850 | [ attr "to" tojid | ||
851 | , attr "id" "someid" | ||
852 | , attr "type" "set" | ||
853 | ] | ||
854 | , EventBeginElement "{jabber:iq:roster}query" [] | ||
855 | , EventBeginElement "{jabber:iq:roster}item" (attr "jid" contact : attrs) | ||
856 | , EventEndElement "{jabber:iq:roster}item" | ||
857 | , EventEndElement "{jabber:iq:roster}query" | ||
858 | , EventEndElement "{jabber:client}iq" | ||
859 | ] | ||
860 | |||
861 | makePong :: Text -> Maybe Text -> Text -> Text -> [XML.Event] | ||
862 | makePong namespace mid to from = | ||
863 | -- Note: similar to session reply | ||
864 | [ EventBeginElement (mkname namespace "iq") | ||
865 | $(case mid of | ||
866 | Just c -> (("id",[ContentText c]):) | ||
867 | _ -> id) | ||
868 | [ attr "type" "result" | ||
869 | , attr "to" to | ||
870 | , attr "from" from | ||
871 | ] | ||
872 | , EventEndElement (mkname namespace "iq") | ||
873 | ] | ||
874 | |||
875 | |||
876 | xmppInbound :: Server ConnectionKey SockAddr | ||
877 | -> XMPPServerParameters | ||
878 | -> ConnectionKey | ||
879 | -> SockAddr | ||
880 | -> FlagCommand -- ^ action to check whether the connection needs a ping | ||
881 | -> TChan Stanza -- ^ channel to announce incomming stanzas on | ||
882 | -> TChan Stanza -- ^ channel used to send stanzas | ||
883 | -> TMVar () -- ^ mvar that is filled when the connection quits | ||
884 | -> Sink XML.Event IO () | ||
885 | xmppInbound sv xmpp k laddr pingflag stanzas output donevar = doNestingXML $ do | ||
886 | let (namespace,tellmyname,tellyourname) = case k of | ||
887 | ClientKey {} -> ( "jabber:client" | ||
888 | , xmppTellMyNameToClient xmpp | ||
889 | , xmppTellClientHisName xmpp k | ||
890 | ) | ||
891 | PeerKey {} -> ( "jabber:server" | ||
892 | , xmppTellMyNameToPeer xmpp laddr | ||
893 | , xmppTellPeerHisName xmpp k | ||
894 | ) | ||
895 | me <- liftIO tellmyname | ||
896 | withXML $ \begindoc -> do | ||
897 | when (begindoc==EventBeginDocument) $ do | ||
898 | whenJust nextElement $ \xml -> do | ||
899 | withJust (elementAttrs "stream" xml) $ \stream_attrs -> do | ||
900 | fix $ \loop -> do | ||
901 | -- liftIO . wlog $ "waiting for stanza." | ||
902 | (chan,clsrs) <- liftIO . atomically $ | ||
903 | liftM2 (,) newLockedChan (newTVar (Just [])) | ||
904 | whenJust nextElement $ \stanzaTag -> do | ||
905 | stanza_lvl <- nesting | ||
906 | liftIO . atomically $ do | ||
907 | writeLChan chan stanzaTag | ||
908 | modifyTVar' clsrs (fmap (closerFor stanzaTag:)) | ||
909 | copyToChannel id chan clsrs =$= do | ||
910 | let mid = lookupAttrib "id" (tagAttrs stanzaTag) | ||
911 | mfrom = lookupAttrib "from" (tagAttrs stanzaTag) | ||
912 | mto = lookupAttrib "to" (tagAttrs stanzaTag) | ||
913 | dispatch <- grokStanza namespace stanzaTag | ||
914 | let unrecog = do | ||
915 | let stype = Unrecognized | ||
916 | s <- liftIO . atomically $ do | ||
917 | return Stanza | ||
918 | { stanzaType = stype | ||
919 | , stanzaId = mid | ||
920 | , stanzaTo = mto | ||
921 | , stanzaFrom = mfrom | ||
922 | , stanzaChan = chan | ||
923 | , stanzaClosers = clsrs | ||
924 | , stanzaInterrupt = donevar | ||
925 | , stanzaOrigin = NetworkOrigin k output | ||
926 | } | ||
927 | ioWriteChan stanzas s | ||
928 | you <- liftIO tellyourname | ||
929 | flip (maybe $ unrecog) dispatch $ \dispatch -> | ||
930 | case dispatch of | ||
931 | -- Checking that the to-address matches this server. | ||
932 | -- Otherwise it could be a client-to-client ping or a | ||
933 | -- client-to-server for some other server. | ||
934 | -- For now, assuming its for the immediate connection. | ||
935 | Ping | mto==Just me || mto==Nothing -> do | ||
936 | let pongto = maybe you id mfrom | ||
937 | pongfrom = maybe me id mto | ||
938 | pong = makePong namespace mid pongto pongfrom | ||
939 | sendReply donevar Pong pong output | ||
940 | #ifdef PINGNOISE | ||
941 | -- TODO: Remove this, it is only to generate a debug print | ||
942 | ioWriteChan stanzas Stanza | ||
943 | { stanzaType = Ping | ||
944 | , stanzaId = mid | ||
945 | , stanzaTo = mto | ||
946 | , stanzaFrom = mfrom | ||
947 | , stanzaChan = chan | ||
948 | , stanzaClosers = clsrs | ||
949 | , stanzaInterrupt = donevar | ||
950 | , stanzaOrigin = NetworkOrigin k output | ||
951 | } | ||
952 | #endif | ||
953 | stype -> ioWriteChan stanzas Stanza | ||
954 | { stanzaType = stype | ||
955 | , stanzaId = mid | ||
956 | , stanzaTo = mto | ||
957 | , stanzaFrom = mfrom | ||
958 | , stanzaChan = chan | ||
959 | , stanzaClosers = clsrs | ||
960 | , stanzaInterrupt = donevar | ||
961 | , stanzaOrigin = NetworkOrigin k output | ||
962 | } | ||
963 | awaitCloser stanza_lvl | ||
964 | liftIO . atomically $ writeTVar clsrs Nothing | ||
965 | loop | ||
966 | |||
967 | |||
968 | while :: IO Bool -> IO a -> IO [a] | ||
969 | while cond body = do | ||
970 | b <- cond | ||
971 | if b then do x <- body | ||
972 | xs <- while cond body | ||
973 | return (x:xs) | ||
974 | else return [] | ||
975 | |||
976 | readUntilNothing :: TChan (Maybe x) -> IO [x] | ||
977 | readUntilNothing ch = do | ||
978 | x <- atomically $ readTChan ch | ||
979 | maybe (return []) | ||
980 | (\x -> do | ||
981 | xs <- readUntilNothing ch | ||
982 | return (x:xs)) | ||
983 | x | ||
984 | |||
985 | |||
986 | streamFeatures :: Text -> [XML.Event] | ||
987 | streamFeatures "jabber:client" = | ||
988 | [ EventBeginElement (streamP "features") [] | ||
989 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" [] | ||
990 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" | ||
991 | |||
992 | {- | ||
993 | -- , " <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>" | ||
994 | , " <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" | ||
995 | -- , " <mechanism>DIGEST-MD5</mechanism>" | ||
996 | , " <mechanism>PLAIN</mechanism>" | ||
997 | , " </mechanisms> " | ||
998 | -} | ||
999 | |||
1000 | , EventEndElement (streamP "features") | ||
1001 | ] | ||
1002 | streamFeatures "jabber:server" = | ||
1003 | [] | ||
1004 | |||
1005 | |||
1006 | greet' :: Text -> Text -> [XML.Event] | ||
1007 | greet' namespace host = EventBeginDocument : greet'' namespace host | ||
1008 | |||
1009 | greet'' :: Text -> Text -> [Event] | ||
1010 | greet'' namespace host = | ||
1011 | [ EventBeginElement (streamP "stream") | ||
1012 | [("from",[ContentText host]) | ||
1013 | ,("id",[ContentText "someid"]) | ||
1014 | ,("xmlns",[ContentText namespace]) | ||
1015 | ,("xmlns:stream",[ContentText "http://etherx.jabber.org/streams"]) | ||
1016 | ,("version",[ContentText "1.0"]) | ||
1017 | ] | ||
1018 | ] ++ streamFeatures namespace | ||
1019 | |||
1020 | consid :: Maybe Text -> [(Name, [Content])] -> [(Name, [Content])] | ||
1021 | consid Nothing = id | ||
1022 | consid (Just sid) = (("id",[ContentText sid]):) | ||
1023 | |||
1024 | |||
1025 | data XMPPState | ||
1026 | = PingSlot | ||
1027 | deriving (Eq,Ord) | ||
1028 | |||
1029 | makePing :: Text -> Maybe Text -> Text -> Text -> [XML.Event] | ||
1030 | makePing namespace mid to from = | ||
1031 | [ EventBeginElement (mkname namespace "iq") | ||
1032 | $ (case mid of | ||
1033 | Just c -> (("id",[ContentText c]):) | ||
1034 | _ -> id ) | ||
1035 | [ ("type",[ContentText "get"]) | ||
1036 | , attr "to" to | ||
1037 | , attr "from" from | ||
1038 | ] | ||
1039 | , EventBeginElement "{urn:xmpp:ping}ping" [] | ||
1040 | , EventEndElement "{urn:xmpp:ping}ping" | ||
1041 | , EventEndElement $ mkname namespace "iq"] | ||
1042 | |||
1043 | iq_bind_reply :: Maybe Text -> Text -> [XML.Event] | ||
1044 | iq_bind_reply mid jid = | ||
1045 | [ EventBeginElement "{jabber:client}iq" (consid mid [("type",[ContentText "result"])]) | ||
1046 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" | ||
1047 | [("xmlns",[ContentText "urn:ietf:params:xml:ns:xmpp-bind"])] | ||
1048 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}jid" [] | ||
1049 | , EventContent (ContentText jid) | ||
1050 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}jid" | ||
1051 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" | ||
1052 | , EventEndElement "{jabber:client}iq" | ||
1053 | |||
1054 | {- | ||
1055 | -- query for client version | ||
1056 | , EventBeginElement "{jabber:client}iq" | ||
1057 | [ attr "to" jid | ||
1058 | , attr "from" hostname | ||
1059 | , attr "type" "get" | ||
1060 | , attr "id" "version"] | ||
1061 | , EventBeginElement "{jabber:iq:version}query" [] | ||
1062 | , EventEndElement "{jabber:iq:version}query" | ||
1063 | , EventEndElement "{jabber:client}iq" | ||
1064 | -} | ||
1065 | ] | ||
1066 | |||
1067 | iq_session_reply :: Maybe Text -> Text -> [XML.Event] | ||
1068 | iq_session_reply mid host = | ||
1069 | -- Note: similar to Pong | ||
1070 | [ EventBeginElement "{jabber:client}iq" | ||
1071 | (consid mid [("from",[ContentText host]) | ||
1072 | ,("type",[ContentText "result"]) | ||
1073 | ]) | ||
1074 | , EventEndElement "{jabber:client}iq" | ||
1075 | ] | ||
1076 | |||
1077 | iq_service_unavailable :: Maybe Text -> Text -> XML.Name -> [XML.Event] | ||
1078 | iq_service_unavailable mid host {- mjid -} req = | ||
1079 | [ EventBeginElement "{jabber:client}iq" | ||
1080 | (consid mid [attr "type" "error" | ||
1081 | ,attr "from" host]) | ||
1082 | , EventBeginElement req [] | ||
1083 | , EventEndElement req | ||
1084 | , EventBeginElement "{jabber:client}error" | ||
1085 | [ attr "type" "cancel" | ||
1086 | , attr "code" "503" ] | ||
1087 | , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable" [] | ||
1088 | , EventEndElement "{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable" | ||
1089 | , EventEndElement "{jabber:client}error" | ||
1090 | , EventEndElement "{jabber:client}iq" | ||
1091 | ] | ||
1092 | |||
1093 | |||
1094 | wrapStanzaList :: [XML.Event] -> STM [Either (StanzaWrap XML.Event) XML.Event] | ||
1095 | wrapStanzaList xs = do | ||
1096 | wrap <- do | ||
1097 | clsrs <- newTVar Nothing | ||
1098 | donev <- newTMVar () | ||
1099 | return $ \ x -> | ||
1100 | Stanza { stanzaType = Unrecognized | ||
1101 | , stanzaId = mid | ||
1102 | , stanzaTo = mto | ||
1103 | , stanzaFrom = mfrom | ||
1104 | , stanzaClosers = clsrs | ||
1105 | , stanzaInterrupt = donev | ||
1106 | , stanzaOrigin = LocalPeer | ||
1107 | , stanzaChan = x | ||
1108 | } | ||
1109 | return $ map (Left . wrap) (take 1 xs) ++ map Right (drop 1 xs) | ||
1110 | where | ||
1111 | m = listToMaybe xs | ||
1112 | mto = m >>= lookupAttrib "to" . tagAttrs | ||
1113 | mfrom = m >>= lookupAttrib "from" . tagAttrs | ||
1114 | mid = m >>= lookupAttrib "id" . tagAttrs | ||
1115 | |||
1116 | wrapStanzaConduit :: Monad m => StanzaWrap a -> ConduitM Event (Either (StanzaWrap Event) Event) m () | ||
1117 | wrapStanzaConduit stanza = do | ||
1118 | mfirst <- await | ||
1119 | flip (maybe $ return ()) mfirst $ \first -> do | ||
1120 | yield . Left $ stanza { stanzaChan = first } | ||
1121 | awaitForever $ yield . Right | ||
1122 | |||
1123 | |||
1124 | |||
1125 | {- | ||
1126 | greet namespace = | ||
1127 | [ EventBeginDocument | ||
1128 | , EventBeginElement (streamP "stream") | ||
1129 | [ attr "xmlns" namespace | ||
1130 | , attr "version" "1.0" | ||
1131 | ] | ||
1132 | ] | ||
1133 | -} | ||
1134 | |||
1135 | goodbye :: [XML.Event] | ||
1136 | goodbye = | ||
1137 | [ EventEndElement (streamP "stream") | ||
1138 | , EventEndDocument | ||
1139 | ] | ||
1140 | |||
1141 | simulateChatError :: StanzaError -> Maybe Text -> [Event] | ||
1142 | simulateChatError err mfrom = | ||
1143 | [ EventBeginElement "{jabber:client}message" | ||
1144 | ((maybe id (\t->(attr "from" t:)) mfrom) | ||
1145 | [attr "type" "normal" ]) | ||
1146 | , EventBeginElement "{jabber:client}body" [] | ||
1147 | , EventContent $ ContentText ("/me " <> errorText err) | ||
1148 | , EventEndElement "{jabber:client}body" | ||
1149 | , EventBeginElement "{http://jabber.org/protocol/xhtml-im}html" [] | ||
1150 | , EventBeginElement "{http://www.w3.org/1999/xhtml}body" [] | ||
1151 | , EventBeginElement "{http://www.w3.org/1999/xhtml}p" | ||
1152 | [ attr "style" "font-weight:bold; color:red" | ||
1153 | ] | ||
1154 | , EventContent $ ContentText ("/me " <> errorText err) | ||
1155 | , EventEndElement "{http://www.w3.org/1999/xhtml}p" | ||
1156 | , EventEndElement "{http://www.w3.org/1999/xhtml}body" | ||
1157 | , EventEndElement "{http://jabber.org/protocol/xhtml-im}html" | ||
1158 | , EventEndElement "{jabber:client}message" | ||
1159 | ] | ||
1160 | |||
1161 | |||
1162 | presenceSolicitation :: Text -> Text -> IO Stanza | ||
1163 | presenceSolicitation = presenceStanza (PresenceRequestSubscription True) "subscribe" | ||
1164 | |||
1165 | presenceProbe :: Text -> Text -> IO Stanza | ||
1166 | presenceProbe = presenceStanza PresenceRequestStatus "probe" | ||
1167 | |||
1168 | presenceStanza :: StanzaType -> Text -> Text -> Text -> IO Stanza | ||
1169 | presenceStanza stanza_type type_attr me jid = | ||
1170 | stanzaFromList stanza_type | ||
1171 | [ EventBeginElement "{jabber:server}presence" | ||
1172 | [ attr "to" jid | ||
1173 | , attr "from" me | ||
1174 | , attr "type" type_attr | ||
1175 | ] | ||
1176 | , EventEndElement "{jabber:server}presence" ] | ||
1177 | |||
1178 | forkConnection :: Server ConnectionKey SockAddr | ||
1179 | -> XMPPServerParameters | ||
1180 | -> ConnectionKey | ||
1181 | -> SockAddr | ||
1182 | -> FlagCommand | ||
1183 | -> Source IO XML.Event | ||
1184 | -> Sink (Flush XML.Event) IO () | ||
1185 | -> TChan Stanza | ||
1186 | -> IO (TChan Stanza) | ||
1187 | forkConnection sv xmpp k laddr pingflag src snk stanzas = do | ||
1188 | let (namespace,tellmyname) = case k of | ||
1189 | ClientKey {} -> ("jabber:client", xmppTellMyNameToClient xmpp) | ||
1190 | PeerKey {} -> ("jabber:server",xmppTellMyNameToPeer xmpp laddr) | ||
1191 | me <- tellmyname | ||
1192 | rdone <- atomically newEmptyTMVar | ||
1193 | let isStarter (Left _) = True | ||
1194 | isStarter (Right e) | isEventBeginElement e = True | ||
1195 | isStarter _ = False | ||
1196 | isStopper (Left _) = False | ||
1197 | isStopper (Right e) | isEventEndElement e = True | ||
1198 | isStopper _ = False | ||
1199 | slots <- atomically $ Slotted.new isStarter isStopper | ||
1200 | needsFlush <- atomically $ newTVar False | ||
1201 | lastStanza <- atomically $ newTVar Nothing | ||
1202 | nesting <- atomically $ newTVar 0 | ||
1203 | let _ = slots :: Slotted.UpdateStream XMPPState (Either (StanzaWrap XML.Event) XML.Event) | ||
1204 | let greet_src = do | ||
1205 | CL.sourceList (greet' namespace me) =$= CL.map Chunk | ||
1206 | yield Flush | ||
1207 | slot_src = do | ||
1208 | what <- lift . atomically $ foldr1 orElse | ||
1209 | [Slotted.pull slots >>= \x -> do | ||
1210 | x <- case x of | ||
1211 | Left wrapped -> do | ||
1212 | writeTVar nesting 1 | ||
1213 | writeTVar lastStanza (Just wrapped) | ||
1214 | return $ stanzaChan wrapped | ||
1215 | Right x -> do | ||
1216 | when (isEventBeginElement x) | ||
1217 | $ modifyTVar' nesting (+1) | ||
1218 | when (isEventEndElement x) $ do | ||
1219 | n <- readTVar nesting | ||
1220 | when (n==1) $ writeTVar lastStanza Nothing | ||
1221 | modifyTVar' nesting (subtract 1) | ||
1222 | return x | ||
1223 | writeTVar needsFlush True | ||
1224 | return $ do | ||
1225 | -- liftIO $ wlog $ "yielding Chunk: " ++ show x | ||
1226 | yield (Chunk x) | ||
1227 | slot_src | ||
1228 | ,do Slotted.isEmpty slots >>= check | ||
1229 | readTVar needsFlush >>= check | ||
1230 | writeTVar needsFlush False | ||
1231 | return $ do | ||
1232 | -- liftIO $ wlog "yielding Flush" | ||
1233 | yield Flush | ||
1234 | slot_src | ||
1235 | ,readTMVar rdone >> return (return ()) | ||
1236 | ] | ||
1237 | what | ||
1238 | forkIO $ do (greet_src >> slot_src) $$ snk | ||
1239 | last <- atomically $ readTVar lastStanza | ||
1240 | es <- while (atomically . fmap not $ Slotted.isEmpty slots) | ||
1241 | (atomically . Slotted.pull $ slots) | ||
1242 | let es' = mapMaybe metadata es | ||
1243 | metadata (Left s) = Just s | ||
1244 | metadata _ = Nothing | ||
1245 | -- TODO: Issuing RecipientUnavailable for all errors is a presence leak | ||
1246 | -- and protocol violation | ||
1247 | -- TODO: IDMangler can be used for better targetted error delivery. | ||
1248 | let fail stanza = do | ||
1249 | wlog $ "failed delivery: " ++ show (stanzaId stanza) | ||
1250 | quitVar <- atomically newEmptyTMVar | ||
1251 | reply <- makeErrorStanza stanza | ||
1252 | tag <- stanzaFirstTag stanza | ||
1253 | -- sendReply quitVar (Error RecipientUnavailable tag) reply replyto | ||
1254 | replystanza <- stanzaFromList (Error RecipientUnavailable tag) reply | ||
1255 | xmppDeliverMessage xmpp (wlog $ "discarded error delivery fail") replystanza | ||
1256 | notError s = case stanzaType s of | ||
1257 | Error {} -> False | ||
1258 | _ -> True | ||
1259 | -- TODO: Probably some stanzas should be queued or saved for re-connect. | ||
1260 | mapM_ fail $ filter notError (maybeToList last ++ es') | ||
1261 | wlog $ "end post-queue fork: " ++ show k | ||
1262 | output <- atomically newTChan | ||
1263 | hacks <- atomically $ newTVar Map.empty | ||
1264 | msgids <- atomically $ newTVar [] | ||
1265 | forkIO $ do | ||
1266 | -- mapM_ (atomically . Slotted.push slots Nothing) greetPeer | ||
1267 | verbosity <- xmppVerbosity xmpp | ||
1268 | fix $ \loop -> do | ||
1269 | what <- atomically $ foldr1 orElse | ||
1270 | [readTChan output >>= \stanza -> return $ do | ||
1271 | let notping f | ||
1272 | | (verbosity==1) = case stanzaType stanza of Pong -> return () | ||
1273 | _ -> f | ||
1274 | | (verbosity>=2) = f | ||
1275 | | otherwise = return () | ||
1276 | -- isempty <- atomically $ isEmptyTChan (stanzaChan stanza) | ||
1277 | -- kwlog $ "queuing: "++show (isempty, stanzaId stanza) | ||
1278 | notping $ do | ||
1279 | dup <- cloneStanza stanza | ||
1280 | let typ = Strict8.pack $ c ++ "<-"++(concat . take 1 . words $ show (stanzaType dup))++" " | ||
1281 | c = case k of | ||
1282 | ClientKey {} -> "C" | ||
1283 | PeerKey {} -> "P" | ||
1284 | wlog "" | ||
1285 | stanzaToConduit dup $$ prettyPrint typ | ||
1286 | -- wlog $ "hacks: "++show (stanzaId stanza) | ||
1287 | case stanzaType stanza of | ||
1288 | InternalEnableHack hack -> do | ||
1289 | -- wlog $ "enable hack: " ++ show hack | ||
1290 | atomically $ modifyTVar' hacks (Map.insert hack ()) | ||
1291 | InternalCacheId x -> do | ||
1292 | -- wlog $ "cache id thread: " ++ show x | ||
1293 | atomically $ modifyTVar' msgids (take 3 . (x:)) | ||
1294 | _ -> return () | ||
1295 | stanzaToConduit stanza =$= wrapStanzaConduit stanza | ||
1296 | $$ awaitForever | ||
1297 | -- TODO: PresenceStatus stanzas should be pushed to appropriate slots | ||
1298 | $ liftIO . atomically . Slotted.push slots Nothing | ||
1299 | case stanzaType stanza of | ||
1300 | Error err tag | tagName tag=="{jabber:client}message" -> do | ||
1301 | wlog $ "handling Error hacks" | ||
1302 | b <- atomically $ do m <- readTVar hacks | ||
1303 | cached <- readTVar msgids | ||
1304 | flip (maybe $ return False) (stanzaId stanza) $ \id' -> do | ||
1305 | return $ Map.member SimulatedChatErrors m | ||
1306 | && elem id' cached | ||
1307 | ids <- atomically $ readTVar msgids | ||
1308 | wlog $ "ids = " ++ show (b,stanzaId stanza, ids) | ||
1309 | when b $ do | ||
1310 | let sim = simulateChatError err (stanzaFrom stanza) | ||
1311 | wlog $ "sending simulated chat for error message." | ||
1312 | CL.sourceList sim =$= wrapStanzaConduit stanza -- not quite right, but whatever | ||
1313 | $$ awaitForever | ||
1314 | $ liftIO . atomically . Slotted.push slots Nothing | ||
1315 | Error e _ -> do | ||
1316 | wlog $ "no hacks for error: " ++ show e | ||
1317 | _ -> return () | ||
1318 | loop | ||
1319 | ,do pingflag >>= check | ||
1320 | return $ do | ||
1321 | to <- xmppTellPeerHisName xmpp k -- addrToText (callBackAddress k) | ||
1322 | let from = me -- Look it up from Server object | ||
1323 | -- or pass it with Connection event. | ||
1324 | mid = Just "ping" | ||
1325 | ping0 = makePing namespace mid to from | ||
1326 | ping <- atomically $ wrapStanzaList ping0 | ||
1327 | mapM_ (atomically . Slotted.push slots (Just $ PingSlot)) | ||
1328 | ping | ||
1329 | #ifdef PINGNOISE | ||
1330 | wlog "" | ||
1331 | CL.sourceList ping0 $$ prettyPrint $ case k of | ||
1332 | ClientKey {} -> "C<-Ping" | ||
1333 | PeerKey {} -> "P<-Ping " | ||
1334 | #endif | ||
1335 | loop | ||
1336 | ,readTMVar rdone >> return (return ()) | ||
1337 | ] | ||
1338 | what | ||
1339 | wlog $ "end pre-queue fork: " ++ show k | ||
1340 | forkIO $ do | ||
1341 | -- src $$ awaitForever (lift . putStrLn . takeWhile (/=' ') . show) | ||
1342 | src $$ xmppInbound sv xmpp k laddr pingflag stanzas output rdone | ||
1343 | atomically $ putTMVar rdone () | ||
1344 | wlog $ "end reader fork: " ++ show k | ||
1345 | return output | ||
1346 | |||
1347 | {- | ||
1348 | data Peer = Peer | ||
1349 | { peerWanted :: TVar Bool -- ^ False when this peer is on a you-call-me basis | ||
1350 | , peerState :: TVar PeerState | ||
1351 | } | ||
1352 | data PeerState | ||
1353 | = PeerPendingConnect UTCTime | ||
1354 | | PeerPendingAccept UTCTime | ||
1355 | | PeerConnected (TChan Stanza) | ||
1356 | -} | ||
1357 | |||
1358 | peerKey :: (Socket,SockAddr) -> IO (ConnectionKey,SockAddr) | ||
1359 | peerKey (sock,addr) = do | ||
1360 | peer <- | ||
1361 | sIsConnected sock >>= \c -> | ||
1362 | if c then getPeerName sock -- addr is normally socketName | ||
1363 | else return addr -- Weird hack: addr is would-be peer name | ||
1364 | laddr <- getSocketName sock | ||
1365 | return $ (PeerKey (peer `withPort` fromIntegral peerport),laddr) | ||
1366 | |||
1367 | clientKey :: (Socket,SockAddr) -> IO (ConnectionKey,SockAddr) | ||
1368 | clientKey (sock,addr) = do | ||
1369 | paddr <- getPeerName sock | ||
1370 | return $ (ClientKey addr,paddr) | ||
1371 | |||
1372 | xmlifyRosterItems :: Monad m => Set Text -> Text -> Set Text -> ConduitM i Event m () | ||
1373 | xmlifyRosterItems solicited stype set = mapM_ item (Set.toList set) | ||
1374 | where | ||
1375 | item jid = do yield $ EventBeginElement "{jabber:iq:roster}item" | ||
1376 | ([ attr "jid" jid | ||
1377 | , attr "subscription" stype | ||
1378 | ]++if Set.member jid solicited | ||
1379 | then [attr "ask" "subscribe"] | ||
1380 | else [] ) | ||
1381 | yield $ EventEndElement "{jabber:iq:roster}item" | ||
1382 | |||
1383 | sendRoster :: | ||
1384 | StanzaWrap a | ||
1385 | -> XMPPServerParameters | ||
1386 | -> TChan Stanza | ||
1387 | -> IO () | ||
1388 | sendRoster query xmpp replyto = do | ||
1389 | let k = case stanzaOrigin query of | ||
1390 | NetworkOrigin k _ -> Just k | ||
1391 | LocalPeer -> Nothing -- local peer requested roster? | ||
1392 | flip (maybe $ return ()) k $ \k -> do | ||
1393 | hostname <- xmppTellMyNameToClient xmpp | ||
1394 | let getlist f = do | ||
1395 | bs <- f xmpp k | ||
1396 | return (Set.fromList bs) -- js) | ||
1397 | buddies <- getlist xmppRosterBuddies | ||
1398 | subscribers <- getlist xmppRosterSubscribers | ||
1399 | solicited <- getlist xmppRosterSolicited | ||
1400 | subnone0 <- getlist xmppRosterOthers | ||
1401 | jid <- case k of | ||
1402 | ClientKey {} -> xmppTellClientHisName xmpp k -- LookupClientJID xmpp k | ||
1403 | PeerKey {} -> xmppTellClientNameOfPeer xmpp k (Set.toList buddies) | ||
1404 | let subnone = Set.union solicited subnone0 \\ Set.union buddies subscribers | ||
1405 | let subto = buddies \\ subscribers | ||
1406 | let subfrom = subscribers \\ buddies | ||
1407 | let subboth = Set.intersection buddies subscribers | ||
1408 | let roster = do | ||
1409 | yield $ EventBeginElement "{jabber:client}iq" | ||
1410 | (consid (stanzaId query) | ||
1411 | [ attr "to" jid | ||
1412 | , attr "type" "result" ]) | ||
1413 | yield $ EventBeginElement "{jabber:iq:roster}query" [] -- todo: ver? | ||
1414 | xmlifyRosterItems solicited "to" subto | ||
1415 | xmlifyRosterItems solicited "from" subfrom | ||
1416 | xmlifyRosterItems solicited "both" subboth | ||
1417 | xmlifyRosterItems solicited "none" subnone | ||
1418 | yield $ EventEndElement "{jabber:iq:roster}query" | ||
1419 | yield $ EventEndElement "{jabber:client}iq" | ||
1420 | |||
1421 | conduitToStanza Roster | ||
1422 | (stanzaId query) | ||
1423 | Nothing | ||
1424 | (Just jid) | ||
1425 | roster >>= ioWriteChan replyto | ||
1426 | {- | ||
1427 | let debugpresence = | ||
1428 | [ EventBeginElement "{jabber:client}presence" | ||
1429 | [ attr "from" "guest@oxio4inifatsetlx.onion" | ||
1430 | , attr "to" jid] | ||
1431 | , EventEndElement "{jabber:client}presence" | ||
1432 | ] | ||
1433 | quitvar <- atomically newEmptyTMVar | ||
1434 | sendReply quitvar Unrecognized debugpresence replyto | ||
1435 | -} | ||
1436 | |||
1437 | |||
1438 | socketFromKey :: Server ConnectionKey SockAddr -> ConnectionKey -> IO SockAddr | ||
1439 | socketFromKey sv k = do | ||
1440 | map <- atomically $ readTVar (conmap sv) | ||
1441 | let mcd = Map.lookup k map | ||
1442 | case mcd of | ||
1443 | Nothing -> case k of | ||
1444 | ClientKey addr -> return addr | ||
1445 | PeerKey addr -> return addr | ||
1446 | -- XXX: ? wrong address | ||
1447 | -- Shouldnt happen anyway. | ||
1448 | Just cd -> return $ cdata cd | ||
1449 | |||
1450 | class StanzaFirstTag a where | ||
1451 | stanzaFirstTag :: StanzaWrap a -> IO XML.Event | ||
1452 | instance StanzaFirstTag (TChan XML.Event) where | ||
1453 | stanzaFirstTag stanza = do | ||
1454 | e <-atomically $ peekTChan (stanzaChan stanza) | ||
1455 | return e | ||
1456 | instance StanzaFirstTag (LockedChan XML.Event) where | ||
1457 | stanzaFirstTag stanza = do | ||
1458 | e <-atomically $ peekLChan (stanzaChan stanza) | ||
1459 | return e | ||
1460 | instance StanzaFirstTag XML.Event where | ||
1461 | stanzaFirstTag stanza = return (stanzaChan stanza) | ||
1462 | |||
1463 | data StanzaError | ||
1464 | = BadRequest | ||
1465 | | Conflict | ||
1466 | | FeatureNotImplemented | ||
1467 | | Forbidden | ||
1468 | | Gone | ||
1469 | | InternalServerError | ||
1470 | | ItemNotFound | ||
1471 | | JidMalformed | ||
1472 | | NotAcceptable | ||
1473 | | NotAllowed | ||
1474 | | NotAuthorized | ||
1475 | | PaymentRequired | ||
1476 | | RecipientUnavailable | ||
1477 | | Redirect | ||
1478 | | RegistrationRequired | ||
1479 | | RemoteServerNotFound | ||
1480 | | RemoteServerTimeout | ||
1481 | | ResourceConstraint | ||
1482 | | ServiceUnavailable | ||
1483 | | SubscriptionRequired | ||
1484 | | UndefinedCondition | ||
1485 | | UnexpectedRequest | ||
1486 | deriving (Show,Enum,Ord,Eq) | ||
1487 | |||
1488 | xep0086 :: | ||
1489 | forall t t1. (Num t1, IsString t) => StanzaError -> (t, t1) | ||
1490 | xep0086 e = | ||
1491 | case e of | ||
1492 | BadRequest -> ("modify", 400) | ||
1493 | Conflict -> ("cancel", 409) | ||
1494 | FeatureNotImplemented -> ("cancel", 501) | ||
1495 | Forbidden -> ("auth", 403) | ||
1496 | Gone -> ("modify", 302) | ||
1497 | InternalServerError -> ("wait", 500) | ||
1498 | ItemNotFound -> ("cancel", 404) | ||
1499 | JidMalformed -> ("modify", 400) | ||
1500 | NotAcceptable -> ("modify", 406) | ||
1501 | NotAllowed -> ("cancel", 405) | ||
1502 | NotAuthorized -> ("auth", 401) | ||
1503 | PaymentRequired -> ("auth", 402) | ||
1504 | RecipientUnavailable -> ("wait", 404) | ||
1505 | Redirect -> ("modify", 302) | ||
1506 | RegistrationRequired -> ("auth", 407) | ||
1507 | RemoteServerNotFound -> ("cancel", 404) | ||
1508 | RemoteServerTimeout -> ("wait", 504) | ||
1509 | ResourceConstraint -> ("wait", 500) | ||
1510 | ServiceUnavailable -> ("cancel", 503) | ||
1511 | SubscriptionRequired -> ("auth", 407) | ||
1512 | UndefinedCondition -> ("", 500) | ||
1513 | UnexpectedRequest -> ("wait", 400) | ||
1514 | |||
1515 | errorText :: StanzaError -> Text | ||
1516 | errorText e = | ||
1517 | case e of | ||
1518 | BadRequest -> "Bad request" | ||
1519 | Conflict -> "Conflict" | ||
1520 | FeatureNotImplemented -> "This feature is not implemented" | ||
1521 | Forbidden -> "Forbidden" | ||
1522 | Gone -> "Recipient can no longer be contacted" | ||
1523 | InternalServerError -> "Internal server error" | ||
1524 | ItemNotFound -> "Item not found" | ||
1525 | JidMalformed -> "JID Malformed" | ||
1526 | NotAcceptable -> "Message was rejected" | ||
1527 | NotAllowed -> "Not allowed" | ||
1528 | NotAuthorized -> "Not authorized" | ||
1529 | PaymentRequired -> "Payment is required" | ||
1530 | RecipientUnavailable -> "Recipient is unavailable" | ||
1531 | Redirect -> "Redirect" | ||
1532 | RegistrationRequired -> "Registration required" | ||
1533 | RemoteServerNotFound -> "Recipient's server not found" | ||
1534 | RemoteServerTimeout -> "Remote server timeout" | ||
1535 | ResourceConstraint -> "The server is low on resources" | ||
1536 | ServiceUnavailable -> "The service is unavailable" | ||
1537 | SubscriptionRequired -> "A subscription is required" | ||
1538 | UndefinedCondition -> "Undefined condition" | ||
1539 | UnexpectedRequest -> "Unexpected request" | ||
1540 | |||
1541 | eventContent :: Maybe [Content] -> Text | ||
1542 | eventContent cs = maybe "" (foldr1 (<>) . map content1) cs | ||
1543 | where content1 (ContentText t) = t | ||
1544 | content1 (ContentEntity t) = t | ||
1545 | |||
1546 | errorTagLocalName :: forall a. Show a => a -> Text | ||
1547 | errorTagLocalName e = Text.pack . drop 1 $ do | ||
1548 | c <- show e | ||
1549 | if 'A' <= c && c <= 'Z' | ||
1550 | then [ '-', chr( ord c - ord 'A' + ord 'a') ] | ||
1551 | else return c | ||
1552 | |||
1553 | makeErrorStanza :: StanzaFirstTag a => StanzaWrap a -> IO [XML.Event] | ||
1554 | makeErrorStanza stanza = do | ||
1555 | startTag <- stanzaFirstTag stanza | ||
1556 | let n = tagName startTag | ||
1557 | endTag = EventEndElement n | ||
1558 | amap0 = Map.fromList (tagAttrs startTag) | ||
1559 | mto = Map.lookup "to" amap0 | ||
1560 | mfrom = Map.lookup "from" amap0 | ||
1561 | mtype = Map.lookup "type" amap0 | ||
1562 | mid = Map.lookup "id" amap0 | ||
1563 | amap1 = Map.alter (const mto) "from" amap0 | ||
1564 | -- amap2 = Map.alter (const $ Just $ [ContentText "blackbird"]) {-mfrom)-} "to" amap1 | ||
1565 | amap2 = Map.alter (const mfrom) "to" amap1 | ||
1566 | amap3 = Map.insert "type" [XML.ContentText "error"] amap2 | ||
1567 | startTag' = EventBeginElement | ||
1568 | (tagName startTag) | ||
1569 | (Map.toList amap3) | ||
1570 | -- err = Gone -- FeatureNotImplemented -- UndefinedCondition -- RecipientUnavailable | ||
1571 | err = RecipientUnavailable | ||
1572 | errname = n { nameLocalName = "error" } | ||
1573 | -- errattrs = [attr "type" "wait"] -- "modify"] | ||
1574 | errorAttribs e xs = ys ++ xs -- todo replace instead of append | ||
1575 | where (typ,code) = xep0086 e | ||
1576 | ys = [attr "type" typ, attr "code" (Text.pack . show $ code)] | ||
1577 | errorTagName = Name { nameNamespace = Just "urn:ietf:params:xml:ns:xmpp-stanzas" | ||
1578 | , nameLocalName = errorTagLocalName err | ||
1579 | , namePrefix = Nothing } | ||
1580 | errattrs = errorAttribs err [] | ||
1581 | let wlogd v s = do | ||
1582 | wlog $ "error "++show (lookupAttrib "id" $ tagAttrs startTag)++" " ++ v ++ " = " ++ show s | ||
1583 | {- | ||
1584 | wlogd "amap0" amap0 | ||
1585 | wlogd "mto" mto | ||
1586 | wlogd "mfrom" mfrom | ||
1587 | wlogd "amap3" amap3 | ||
1588 | -} | ||
1589 | if eventContent mtype=="error" then return [] else do | ||
1590 | return [ startTag' | ||
1591 | , EventBeginElement errname errattrs | ||
1592 | , EventBeginElement errorTagName [] | ||
1593 | , EventEndElement errorTagName | ||
1594 | , EventEndElement errname | ||
1595 | {- | ||
1596 | , EventBeginElement "{jabber:client}body" [] | ||
1597 | , EventContent (ContentText "what?") | ||
1598 | , EventEndElement "{jabber:client}body" | ||
1599 | -} | ||
1600 | {- | ||
1601 | , EventBeginElement "{154ae29f-98f2-4af4-826d-a40c8a188574}dummy" [] | ||
1602 | , EventEndElement "{154ae29f-98f2-4af4-826d-a40c8a188574}dummy" | ||
1603 | -} | ||
1604 | , endTag | ||
1605 | ] | ||
1606 | |||
1607 | monitor :: | ||
1608 | Server ConnectionKey SockAddr | ||
1609 | -> ConnectionParameters ConnectionKey SockAddr | ||
1610 | -> XMPPServerParameters | ||
1611 | -> IO b | ||
1612 | monitor sv params xmpp = do | ||
1613 | chan <- return $ serverEvent sv | ||
1614 | stanzas <- atomically newTChan | ||
1615 | quitVar <- atomically newEmptyTMVar | ||
1616 | fix $ \loop -> do | ||
1617 | action <- atomically $ foldr1 orElse | ||
1618 | [ readTChan chan >>= \((k,u),e) -> return $ do | ||
1619 | case e of | ||
1620 | Connection pingflag conread conwrite -> do | ||
1621 | wlog $ tomsg k "Connection" | ||
1622 | let (xsrc,xsnk) = xmlStream conread conwrite | ||
1623 | outs <- forkConnection sv xmpp k u pingflag xsrc xsnk stanzas | ||
1624 | xmppNewConnection xmpp k u outs | ||
1625 | return () | ||
1626 | ConnectFailure addr -> return () -- wlog $ tomsg k "ConnectFailure" | ||
1627 | EOF -> do wlog $ tomsg k "EOF" | ||
1628 | xmppEOF xmpp k | ||
1629 | HalfConnection In -> do | ||
1630 | wlog $ tomsg k "ReadOnly" | ||
1631 | control sv (Connect (callBackAddress k) params) | ||
1632 | HalfConnection Out -> wlog $ tomsg k "WriteOnly" | ||
1633 | RequiresPing -> return () -- wlog $ tomsg k "RequiresPing" | ||
1634 | _ -> return () | ||
1635 | , readTChan stanzas >>= \stanza -> return $ do | ||
1636 | {- | ||
1637 | dup <- case stanzaType stanza of | ||
1638 | -- Must dup anything that is going to be delivered... | ||
1639 | Message {} -> do | ||
1640 | dup <- cloneStanza stanza -- dupped so we can make debug print | ||
1641 | return dup | ||
1642 | Error {} -> do | ||
1643 | dup <- cloneStanza stanza -- dupped so we can make debug print | ||
1644 | return dup | ||
1645 | _ -> return stanza | ||
1646 | -} | ||
1647 | dup <- cloneStanza stanza | ||
1648 | |||
1649 | forkIO $ do | ||
1650 | case stanzaOrigin stanza of | ||
1651 | NetworkOrigin k@(ClientKey {}) replyto -> | ||
1652 | case stanzaType stanza of | ||
1653 | RequestResource wanted -> do | ||
1654 | sockaddr <- socketFromKey sv k | ||
1655 | rsc <- xmppChooseResourceName xmpp k sockaddr wanted | ||
1656 | let reply = iq_bind_reply (stanzaId stanza) rsc | ||
1657 | -- sendReply quitVar SetResource reply replyto | ||
1658 | hostname <- xmppTellMyNameToClient xmpp | ||
1659 | let requestVersion :: Producer IO XML.Event | ||
1660 | requestVersion = do | ||
1661 | yield $ EventBeginElement "{jabber:client}iq" | ||
1662 | [ attr "to" rsc | ||
1663 | , attr "from" hostname | ||
1664 | , attr "type" "get" | ||
1665 | , attr "id" "version"] | ||
1666 | yield $ EventBeginElement "{jabber:iq:version}query" [] | ||
1667 | yield $ EventEndElement "{jabber:iq:version}query" | ||
1668 | yield $ EventEndElement "{jabber:client}iq" | ||
1669 | {- | ||
1670 | -- XXX Debug chat: | ||
1671 | yield $ EventBeginElement "{jabber:client}message" | ||
1672 | [ attr "from" $ eventContent (Just [ContentText rsc]) | ||
1673 | , attr "type" "normal" ] -- "blackbird" ] | ||
1674 | yield $ EventBeginElement "{jabber:client}body" [] | ||
1675 | yield $ EventContent $ ContentText ("hello?") | ||
1676 | yield $ EventEndElement "{jabber:client}body" | ||
1677 | yield $ EventEndElement "{jabber:client}message" | ||
1678 | -} | ||
1679 | sendReply quitVar SetResource reply replyto | ||
1680 | conduitToStanza (UnrecognizedQuery "{jabber:iq:version}query") | ||
1681 | Nothing -- id | ||
1682 | (Just hostname) -- from | ||
1683 | (Just rsc) -- to | ||
1684 | requestVersion | ||
1685 | >>= ioWriteChan replyto | ||
1686 | SessionRequest -> do | ||
1687 | me <- xmppTellMyNameToClient xmpp | ||
1688 | let reply = iq_session_reply (stanzaId stanza) me | ||
1689 | sendReply quitVar Pong reply replyto | ||
1690 | RequestRoster -> do | ||
1691 | sendRoster stanza xmpp replyto | ||
1692 | xmppSubscribeToRoster xmpp k | ||
1693 | PresenceStatus {} -> do | ||
1694 | xmppInformClientPresence xmpp k stanza | ||
1695 | PresenceRequestSubscription {} -> do | ||
1696 | let fail = return () -- todo | ||
1697 | xmppClientSubscriptionRequest xmpp fail k stanza replyto | ||
1698 | PresenceInformSubscription {} -> do | ||
1699 | let fail = return () -- todo | ||
1700 | xmppClientInformSubscription xmpp fail k stanza | ||
1701 | NotifyClientVersion name version -> do | ||
1702 | enableClientHacks name version replyto | ||
1703 | UnrecognizedQuery query -> do | ||
1704 | me <- xmppTellMyNameToClient xmpp | ||
1705 | let reply = iq_service_unavailable (stanzaId stanza) me query | ||
1706 | sendReply quitVar (Error ServiceUnavailable (head reply)) reply replyto | ||
1707 | Message {} -> do | ||
1708 | -- wlog $ "LANGMAP "++show (stanzaId stanza, msgLangMap (stanzaType stanza)) | ||
1709 | maybe (return ()) (flip cacheMessageId replyto) $ do | ||
1710 | guard . not . null . mapMaybe (msgBody . snd) $ msgLangMap (stanzaType stanza) | ||
1711 | stanzaId stanza | ||
1712 | _ -> return () | ||
1713 | NetworkOrigin k@(PeerKey {}) replyto -> | ||
1714 | case stanzaType stanza of | ||
1715 | PresenceRequestStatus {} -> do | ||
1716 | xmppAnswerProbe xmpp k stanza replyto | ||
1717 | PresenceStatus {} -> do | ||
1718 | xmppInformPeerPresence xmpp k stanza | ||
1719 | PresenceRequestSubscription {} -> do | ||
1720 | let fail = return () -- todo | ||
1721 | xmppPeerSubscriptionRequest xmpp fail k stanza replyto | ||
1722 | PresenceInformSubscription {} -> do | ||
1723 | let fail = return () -- todo | ||
1724 | xmppPeerInformSubscription xmpp fail k stanza | ||
1725 | _ -> return () | ||
1726 | _ -> return () | ||
1727 | let deliver replyto = do | ||
1728 | -- TODO: Issuing RecipientUnavailable for all errors is a presence leak | ||
1729 | -- and protocol violation | ||
1730 | let fail = do | ||
1731 | wlog $ "Failed delivery id="++show (stanzaId stanza) -- TODO | ||
1732 | reply <- makeErrorStanza stanza | ||
1733 | tag <- stanzaFirstTag stanza | ||
1734 | sendReply quitVar (Error RecipientUnavailable tag) reply replyto | ||
1735 | xmppDeliverMessage xmpp fail stanza | ||
1736 | -- -- bad idea: | ||
1737 | -- let newStream = greet'' "jabber:client" "blackbird" | ||
1738 | -- sendReply quitVar Error newStream replyto | ||
1739 | case stanzaType stanza of | ||
1740 | Message {} -> do | ||
1741 | case stanzaOrigin stanza of | ||
1742 | LocalPeer {} -> return () | ||
1743 | NetworkOrigin _ replyto -> deliver replyto | ||
1744 | Error {} -> do | ||
1745 | case stanzaOrigin stanza of | ||
1746 | LocalPeer {} -> return () | ||
1747 | NetworkOrigin k replyto -> do | ||
1748 | -- wlog $ "delivering error: " ++show (stanzaId stanza) | ||
1749 | -- wlog $ " from: " ++ show k | ||
1750 | deliver replyto | ||
1751 | _ -> return () | ||
1752 | -- We need to clone in the case the stanza is passed on as for Message. | ||
1753 | verbosity <- xmppVerbosity xmpp | ||
1754 | let notping f | (verbosity==1) = case stanzaType stanza of Pong -> return () | ||
1755 | _ -> f | ||
1756 | | (verbosity>=2) = f | ||
1757 | | otherwise = return () | ||
1758 | notping $ do | ||
1759 | let typ = Strict8.pack $ c ++ "->"++(concat . take 1 . words $ show (stanzaType stanza))++" " | ||
1760 | c = case stanzaOrigin stanza of | ||
1761 | LocalPeer -> "*" | ||
1762 | NetworkOrigin (ClientKey {}) _ -> "C" | ||
1763 | NetworkOrigin (PeerKey {}) _ -> "P" | ||
1764 | wlog "" | ||
1765 | stanzaToConduit dup $$ prettyPrint typ | ||
1766 | |||
1767 | ] | ||
1768 | action | ||
1769 | loop | ||
1770 | where | ||
1771 | tomsg k str = printf "%12s %s" str (show k) | ||
1772 | where | ||
1773 | _ = str :: String | ||
1774 | |||
1775 | data XMPPServer | ||
1776 | = XMPPServer { _xmpp_sv :: Server ConnectionKey SockAddr | ||
1777 | , _xmpp_peer_params :: ConnectionParameters ConnectionKey SockAddr | ||
1778 | } | ||
1779 | |||
1780 | addPeer :: XMPPServer -> SockAddr -> IO () | ||
1781 | addPeer sv addr = do | ||
1782 | control (_xmpp_sv sv) (ConnectWithEndlessRetry addr (_xmpp_peer_params sv) 10000) | ||
1783 | |||
1784 | xmppServer :: ( MonadResource m | ||
1785 | , MonadIO m | ||
1786 | ) => XMPPServerParameters -> m XMPPServer | ||
1787 | xmppServer xmpp = do | ||
1788 | sv <- server | ||
1789 | -- some fuzz helps avoid simultaneity | ||
1790 | pingfuzz <- liftIO $ do | ||
1791 | gen <- System.Random.getStdGen | ||
1792 | let (r,gen') = System.Random.next gen | ||
1793 | return $ r `mod` 2000 -- maximum 2 seconds of fuzz | ||
1794 | liftIO . wlog $ "pingfuzz = " ++ show pingfuzz | ||
1795 | let peer_params = (connectionDefaults peerKey) | ||
1796 | { pingInterval = 15000 + pingfuzz | ||
1797 | , timeout = 2000 | ||
1798 | , duplex = False } | ||
1799 | client_params = (connectionDefaults clientKey) | ||
1800 | { pingInterval = 0 | ||
1801 | , timeout = 0 | ||
1802 | } | ||
1803 | liftIO $ do | ||
1804 | forkIO $ monitor sv peer_params xmpp | ||
1805 | control sv (Listen peerport peer_params) | ||
1806 | control sv (Listen clientport client_params) | ||
1807 | return XMPPServer { _xmpp_sv = sv, _xmpp_peer_params = peer_params } | ||
1808 | |||
1809 | #if MIN_VERSION_stm(2,4,0) | ||
1810 | #else | ||
1811 | -- |Clone a 'TChan': similar to dupTChan, but the cloned channel starts with the | ||
1812 | -- same content available as the original channel. | ||
1813 | -- | ||
1814 | -- Terrible inefficient implementation provided to build against older libraries. | ||
1815 | cloneTChan :: TChan a -> STM (TChan a) | ||
1816 | cloneTChan chan = do | ||
1817 | contents <- chanContents' chan | ||
1818 | chan2 <- dupTChan chan | ||
1819 | mapM_ (writeTChan chan) contents | ||
1820 | return chan2 | ||
1821 | where | ||
1822 | chanContents' chan = do | ||
1823 | b <- isEmptyTChan chan | ||
1824 | if b then return [] else do | ||
1825 | x <- readTChan chan | ||
1826 | xs <- chanContents' chan | ||
1827 | return (x:xs) | ||
1828 | #endif | ||
1829 | |||
diff --git a/Presence/monitortty.c b/Presence/monitortty.c new file mode 100644 index 00000000..a9a095cf --- /dev/null +++ b/Presence/monitortty.c | |||
@@ -0,0 +1,173 @@ | |||
1 | // monitortty.c | ||
2 | |||
3 | #include <unistd.h> | ||
4 | #include <pthread.h> | ||
5 | #include <stdio.h> | ||
6 | #include <string.h> | ||
7 | #include <stdint.h> | ||
8 | #include <errno.h> | ||
9 | #include <linux/vt.h> | ||
10 | #include <sys/ioctl.h> | ||
11 | #include <fcntl.h> | ||
12 | #include <linux/kd.h> | ||
13 | #include <stdlib.h> | ||
14 | |||
15 | static char *conspath[] = { | ||
16 | "/proc/self/fd/0", | ||
17 | "/dev/tty", | ||
18 | "/dev/tty0", | ||
19 | "/dev/vc/0", | ||
20 | "/dev/systty", | ||
21 | "/dev/console", | ||
22 | NULL | ||
23 | }; | ||
24 | |||
25 | static int | ||
26 | is_a_console(int fd) { | ||
27 | char arg; | ||
28 | |||
29 | arg = 0; | ||
30 | return (isatty (fd) | ||
31 | && ioctl(fd, KDGKBTYPE, &arg) == 0 | ||
32 | && ((arg == KB_101) || (arg == KB_84))); | ||
33 | } | ||
34 | |||
35 | static int | ||
36 | open_a_console(const char *fnam) { | ||
37 | int fd; | ||
38 | |||
39 | /* | ||
40 | * For ioctl purposes we only need some fd and permissions | ||
41 | * do not matter. But setfont:activatemap() does a write. | ||
42 | */ | ||
43 | fd = open(fnam, O_RDWR); | ||
44 | if (fd < 0) | ||
45 | fd = open(fnam, O_WRONLY); | ||
46 | if (fd < 0) | ||
47 | fd = open(fnam, O_RDONLY); | ||
48 | if (fd < 0) | ||
49 | return -1; | ||
50 | return fd; | ||
51 | } | ||
52 | |||
53 | int ttyfd() { | ||
54 | // We try several things because opening /dev/console will fail | ||
55 | // if someone else used X (which does a chown on /dev/console). | ||
56 | int i; | ||
57 | int fd; | ||
58 | for (i = 0; conspath[i]; i++) { | ||
59 | if ((fd = open_a_console(conspath[i])) >= 0) { | ||
60 | if (is_a_console(fd)) { | ||
61 | printf("using %s\n",conspath[i]); | ||
62 | return fd; | ||
63 | } | ||
64 | close(fd); | ||
65 | } | ||
66 | } | ||
67 | for (fd = 0; fd < 3; fd++) | ||
68 | if (is_a_console(fd)) | ||
69 | return fd; | ||
70 | printf("failed to find console fd\n"); | ||
71 | return -1; | ||
72 | } | ||
73 | |||
74 | void vt_wait(int tty_fd) { | ||
75 | struct vt_event vt; | ||
76 | memset(&vt,'\0',sizeof(vt)); | ||
77 | vt.event = VT_EVENT_SWITCH; | ||
78 | int res; | ||
79 | // printf("started wait\n"); | ||
80 | res = ioctl (tty_fd, VT_WAITEVENT, &vt); | ||
81 | if (res==-1) { | ||
82 | printf("vt_wait error fd=%i\n",tty_fd); | ||
83 | perror("vt_wait"); | ||
84 | // printf("vt_wait: %u - %s\n", errno, errmsg(errno)); | ||
85 | sleep(1); | ||
86 | } | ||
87 | // printf("finished wait\n"); | ||
88 | } | ||
89 | |||
90 | int8_t get_active(int tty_fd) { | ||
91 | struct vt_stat vtstat; | ||
92 | memset(&vtstat,'\0',sizeof(vtstat)); | ||
93 | if (ioctl(tty_fd, VT_GETSTATE, &vtstat)) { | ||
94 | perror ("get_active: VT_GETSTATE"); | ||
95 | return 7; | ||
96 | } | ||
97 | return vtstat.v_active; | ||
98 | } | ||
99 | |||
100 | void chvt(int tty_fd, int n) { | ||
101 | if (ioctl(tty_fd, VT_ACTIVATE, n)) { | ||
102 | perror ("chvt: VT_ACTIVATE"); | ||
103 | } | ||
104 | |||
105 | } | ||
106 | |||
107 | pthread_mutex_t mu; | ||
108 | pthread_t mt; | ||
109 | int tty = -1; | ||
110 | |||
111 | void *write_vtch(void *pfd) { | ||
112 | int fd = (int)(intptr_t)pfd; | ||
113 | printf("started VT_WAITEVENT loop fd=%i\n",fd); | ||
114 | pthread_mutex_lock(&mu); | ||
115 | tty = ttyfd(); | ||
116 | pthread_mutex_unlock(&mu); | ||
117 | int8_t active_tty = get_active(tty); | ||
118 | int8_t reported_tty; | ||
119 | ssize_t e; | ||
120 | |||
121 | pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); | ||
122 | for (;;) { | ||
123 | // ssize_t write(int fd, const void *buf, size_t count); | ||
124 | e = write(fd, &active_tty, 1); | ||
125 | if (e<0 ) { | ||
126 | if( errno==EAGAIN) continue; | ||
127 | break; | ||
128 | } | ||
129 | else if(e==1) { | ||
130 | reported_tty = active_tty; | ||
131 | } | ||
132 | do { | ||
133 | vt_wait(tty); | ||
134 | // printf("vt_wait() finished. tty=%d fd=%d\n",tty,fd); | ||
135 | active_tty = get_active(tty); | ||
136 | } while (active_tty==reported_tty); | ||
137 | } | ||
138 | |||
139 | // TODO: | ||
140 | // use VT_GETSTATE | ||
141 | // use VT_WAITEVENT | ||
142 | printf("stopped VT_WAITEVENT loop\n"); | ||
143 | tty = -1; | ||
144 | pthread_mutex_destroy(&mu); | ||
145 | return NULL; | ||
146 | } | ||
147 | |||
148 | |||
149 | void monitorTTY(int fd) { | ||
150 | pthread_mutex_init(&mu,NULL); | ||
151 | // printf ("Hello world.\n"); | ||
152 | pthread_create (&mt, NULL, write_vtch, (void*)(intptr_t)fd); | ||
153 | } | ||
154 | |||
155 | void closeTTY() { | ||
156 | int fd = -1; | ||
157 | int active = 7; | ||
158 | pthread_mutex_lock(&mu); | ||
159 | active = get_active(tty); | ||
160 | fd = tty; | ||
161 | pthread_mutex_unlock(&mu); | ||
162 | #ifndef VTHACK | ||
163 | pthread_cancel(mt); | ||
164 | #endif | ||
165 | char cmd[40]; cmd[39] = '\0'; | ||
166 | // Hack to wake up from VT_WAITEVENT ioctl | ||
167 | #ifdef VTHACK | ||
168 | snprintf(cmd,39,"chvt %i;chvt %i",active+1,active); | ||
169 | system(cmd); | ||
170 | pthread_join(mt,NULL); | ||
171 | #endif | ||
172 | close(fd); | ||
173 | } | ||