]> code.delx.au - pymsnt/blob - src/main.py
* PyMSNt now supports Twisted 2.5
[pymsnt] / src / main.py
1 # Copyright 2004-2006 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
3
4 import os, os.path, time, sys, codecs, getopt
5 reload(sys)
6 sys.setdefaultencoding("utf-8")
7 sys.stdout = codecs.lookup('utf-8')[-1](sys.stdout)
8
9
10 # Find the best reactor
11 selectWarning = "Unable to install any good reactors (kqueue, epoll, poll).\nWe fell back to using select. You may have scalability problems.\nThis reactor will not support more than 1024 connections at a time."
12 try:
13 from twisted.internet import epollreactor as bestreactor
14 except:
15 #try:
16 #from twisted.internet import kqreactor as bestreactor
17 #except:
18 try:
19 from twisted.internet import pollreactor as bestreactor
20 except:
21 try:
22 from twisted.internet import selectreactor as bestreactor
23 print selectWarning
24 except:
25 try:
26 from twisted.internet import default as bestreactor
27 print selectWarning
28 except:
29 print "Unable to find a reactor. Please make sure you have Twisted properly installed.\nExiting..."
30 sys.exit(1)
31 bestreactor.install()
32
33 import twistfix
34 twistfix.main()
35
36
37 # Must load config before everything else
38 import config
39 import xmlconfig
40 configFile = "config.xml"
41 configOptions = {}
42 opts, args = getopt.getopt(sys.argv[1:], "bc:o:dDgtlp:h", ["background", "config=", "option=", "debug", "Debug", "garbage", "traceback", "log=", "pid=", "help"])
43 for o, v in opts:
44 if o in ("-c", "--config"):
45 configFile = v
46 elif o in ("-b", "--background"):
47 config.background = True
48 elif o in ("-d", "--debug"):
49 config.debugLevel = "2"
50 elif o in ("-D", "--Debug"):
51 config.debugLevel = "3"
52 elif o in ("-g", "--garbage"):
53 import gc
54 gc.set_debug(gc.DEBUG_LEAK|gc.DEBUG_STATS)
55 elif o in ("-t", "--traceback"):
56 config.debugLevel = "1"
57 elif o in ("-l", "--log"):
58 config.debugFile = v
59 elif o in ("-p", "--pid"):
60 config.pid = v
61 elif o in ("-o", "--option"):
62 var, setting = v.split("=", 2)
63 configOptions[var] = setting
64 elif o in ("-h", "--help"):
65 print "%s [options]" % sys.argv[0]
66 print " -h print this help"
67 print " -b daemonize/background transport"
68 print " -c <file> read configuration from this file"
69 print " -d print debugging output"
70 print " -D print extended debugging output"
71 print " -g print garbage collection output"
72 print " -t print debugging only on traceback"
73 print " -l <file> write debugging output to file"
74 print " -p <file> write process ID to file"
75 print " -o <var>=<setting> set config var to setting"
76 sys.exit(0)
77
78 xmlconfig.reloadConfig(configFile, configOptions)
79
80 if config.reactor:
81 # They picked their own reactor. Lets install it.
82 del sys.modules["twisted.internet.reactor"]
83 if config.reactor == "epoll":
84 from twisted.internet import epollreactor
85 epollreactor.install()
86 elif config.reactor == "poll":
87 from twisted.internet import pollreactor
88 pollreactor.install()
89 elif config.reactor == "kqueue":
90 from twisted.internet import kqreactor
91 kqreactor.install()
92 elif len(config.reactor) > 0:
93 print "Unknown reactor: ", config.reactor, ". Using select(), reactor."
94
95
96 from twisted.internet import reactor, task
97 from twisted.internet.defer import Deferred
98 from twisted.words.xish.domish import Element
99 from twisted.words.protocols.jabber import component
100 from twisted.words.protocols.jabber.jid import internJID
101
102 from debug import LogEvent, INFO, WARN, ERROR
103
104 import debug
105 import svninfo
106 import utils
107 import xdb
108 import avatar
109 import session
110 import jabw
111 import disco
112 import register
113 import misciq
114 import ft
115 import lang
116 import legacy
117 import housekeep
118
119
120
121 class PyTransport(component.Service):
122 def __init__(self):
123 LogEvent(INFO)
124 try:
125 LogEvent(INFO, msg="SVN r" + str(svninfo.getSVNVersion()))
126 except:
127 pass
128 LogEvent(INFO, msg="Reactor: " + str(reactor))
129
130 # Discovery, as well as some builtin features
131 self.discovery = disco.ServerDiscovery(self)
132 self.discovery.addIdentity("gateway", legacy.id, config.discoName, config.jid)
133 self.discovery.addIdentity("conference", "text", config.discoName + " Chatrooms", config.jid)
134 self.discovery.addFeature(disco.XCONFERENCE, None, config.jid) # So that clients know you can create groupchat rooms on the server
135 self.discovery.addFeature("jabber:iq:conference", None, config.jid) # We don't actually support this, but Psi has a bug where it looks for this instead of the above
136 self.discovery.addIdentity("client", "pc", "MSN Messenger", "USER")
137
138 self.xdb = xdb.XDB(config.jid, legacy.mangle)
139 self.avatarCache = avatar.AvatarCache()
140 self.registermanager = register.RegisterManager(self)
141 self.gatewayTranslator = misciq.GatewayTranslator(self)
142 self.versionTeller = misciq.VersionTeller(self)
143 self.pingService = misciq.PingService(self)
144 self.adHocCommands = misciq.AdHocCommands(self)
145 self.vCardFactory = misciq.VCardFactory(self)
146 self.iqAvatarFactor = misciq.IqAvatarFactory(self)
147 self.connectUsers = misciq.ConnectUsers(self)
148 if config.ftJabberPort:
149 self.ftSOCKS5Receive = ft.Proxy65(int(config.ftJabberPort))
150 self.ftSOCKS5Send = misciq.Socks5FileTransfer(self)
151 if config.ftOOBPort:
152 self.ftOOBReceive = ft.FileTransferOOBReceive(int(config.ftOOBPort))
153 self.ftOOBSend = misciq.FileTransferOOBSend(self)
154 self.statistics = misciq.Statistics(self)
155 self.startTime = int(time.time())
156
157 self.xmlstream = None
158 self.sessions = {}
159
160 # Groupchat ID handling
161 self.lastID = 0
162 self.reservedIDs = []
163
164 # Message IDs
165 self.messageID = 0
166
167 self.loopTask = task.LoopingCall(self.loopFunc)
168 self.loopTask.start(60.0)
169
170 def removeMe(self):
171 LogEvent(INFO)
172 for session in self.sessions.copy():
173 self.sessions[session].removeMe()
174
175 def makeMessageID(self):
176 self.messageID += 1
177 return str(self.messageID)
178
179 def makeID(self):
180 newID = "r" + str(self.lastID)
181 self.lastID += 1
182 if self.reservedIDs.count(newID) > 0:
183 # Ack, it's already used.. Try again
184 return self.makeID()
185 else:
186 return newID
187
188 def reserveID(self, ID):
189 self.reservedIDs.append(ID)
190
191 def loopFunc(self):
192 numsessions = len(self.sessions)
193
194 #if config.debugOn and numsessions > 0:
195 # print "Sessions:"
196 # for key in self.sessions:
197 # print "\t" + self.sessions[key].jabberID
198
199 self.statistics.stats["Uptime"] = int(time.time()) - self.startTime
200 self.statistics.stats["OnlineUsers"] = numsessions
201 legacy.updateStats(self.statistics)
202 if numsessions > 0:
203 oldDict = self.sessions.copy()
204 self.sessions = {}
205 for key in oldDict:
206 s = oldDict[key]
207 if not s.alive:
208 LogEvent(WARN, "", "Ghost session found.")
209 # Don't add it to the new dictionary. Effectively removing it
210 else:
211 self.sessions[key] = s
212
213 def componentConnected(self, xmlstream):
214 LogEvent(INFO)
215 self.xmlstream = xmlstream
216 self.xmlstream.addObserver("/iq", self.discovery.onIq)
217 self.xmlstream.addObserver("/presence", self.onPresence)
218 self.xmlstream.addObserver("/message", self.onMessage)
219 self.xmlstream.addObserver("/route", self.onRouteMessage)
220 if config.useXCP:
221 pres = Element((None, "presence"))
222 pres.attributes["to"] = "presence@-internal"
223 pres.attributes["from"] = config.compjid
224 x = pres.addElement("x")
225 x.attributes["xmlns"] = "http://www.jabber.com/schemas/component-presence.xsd"
226 x.attributes["xmlns:config"] = "http://www.jabber.com/config"
227 x.attributes["config:version"] = "1"
228 x.attributes["protocol-version"] = "1.0"
229 x.attributes["config-ns"] = legacy.url + "/component"
230 self.send(pres)
231
232 def componentDisconnected(self):
233 LogEvent(INFO)
234 self.xmlstream = None
235
236 def onRouteMessage(self, el):
237 for child in el.elements():
238 if child.name == "message":
239 self.onMessage(child)
240 elif child.name == "presence":
241 # Ignore any presence broadcasts about other XCP components
242 if child.getAttribute("to") and child.getAttribute("to").find("@-internal") > 0: return
243 self.onPresence(child)
244 elif child.name == "iq":
245 self.discovery.onIq(child)
246
247 def onMessage(self, el):
248 fro = el.getAttribute("from")
249 try:
250 froj = internJID(fro)
251 except Exception, e:
252 LogEvent(WARN, "", "Failed stringprep.")
253 return
254 mtype = el.getAttribute("type")
255 s = self.sessions.get(froj.userhost(), None)
256 if mtype == "error" and s:
257 LogEvent(INFO, s.jabberID, "Removing session because of message type=error")
258 s.removeMe()
259 elif s:
260 s.onMessage(el)
261 elif mtype != "error":
262 to = el.getAttribute("to")
263 ulang = utils.getLang(el)
264 body = None
265 for child in el.elements():
266 if child.name == "body":
267 body = child.__str__()
268 LogEvent(INFO, "", "Sending error response to a message outside of session.")
269 jabw.sendErrorMessage(self, fro, to, "auth", "not-authorized", lang.get(ulang).notLoggedIn, body)
270 jabw.sendPresence(self, fro, to, ptype="unavailable")
271
272 def onPresence(self, el):
273 fro = el.getAttribute("from")
274 to = el.getAttribute("to")
275 try:
276 froj = internJID(fro)
277 toj = internJID(to)
278 except Exception, e:
279 LogEvent(WARN, "", "Failed stringprep.")
280 return
281
282 ptype = el.getAttribute("type")
283 s = self.sessions.get(froj.userhost())
284 if ptype == "error" and s:
285 LogEvent(INFO, s.jabberID, "Removing session because of message type=error")
286 s.removeMe()
287 elif s:
288 s.onPresence(el)
289 else:
290 ulang = utils.getLang(el)
291 ptype = el.getAttribute("type")
292 if to.find('@') < 0:
293 # If the presence packet is to the transport (not a user) and there isn't already a session
294 if not el.getAttribute("type"): # Don't create a session unless they're sending available presence
295 LogEvent(INFO, "", "Attempting to create a new session.")
296 s = session.makeSession(self, froj.userhost(), ulang)
297 if s:
298 self.statistics.stats["TotalUsers"] += 1
299 self.sessions[froj.userhost()] = s
300 LogEvent(INFO, "", "New session created.")
301 # Send the first presence
302 s.onPresence(el)
303 else:
304 LogEvent(INFO, "", "Failed to create session")
305 jabw.sendMessage(self, to=froj.userhost(), fro=config.jid, body=lang.get(ulang).notRegistered)
306
307 elif el.getAttribute("type") != "error":
308 LogEvent(INFO, "", "Sending unavailable presence to non-logged in user.")
309 jabw.sendPresence(self, fro, to, ptype="unavailable")
310 return
311
312 elif ptype and (ptype.startswith("subscribe") or ptype.startswith("unsubscribe")):
313 # They haven't logged in, and are trying to change subscription to a user
314 # No, lets not log them in. Lets send an error :)
315 jabw.sendPresence(self, fro, to, ptype="error")
316
317 # Lets log them in and then do it
318 #LogEvent(INFO, "", "Attempting to create a session to do subscription stuff.")
319 #s = session.makeSession(self, froj.userhost(), ulang)
320 #if s:
321 # self.sessions[froj.userhost()] = s
322 # LogEvent(INFO, "", "New session created.")
323 # # Tell the session there's a new resource
324 # s.handleResourcePresence(froj.userhost(), froj.resource, toj.userhost(), toj.resource, 0, None, None, None)
325 # # Send this subscription
326 # s.onPresence(el)
327
328
329 class App:
330 def __init__(self):
331 # Check for any other instances
332 if config.pid and os.name != "posix":
333 config.pid = ""
334 if config.pid:
335 twistd.checkPID(config.pid)
336
337 # Do any auto-update stuff
338 housekeep.init()
339
340 # Daemonise the process and write the PID file
341 if config.background and os.name == "posix":
342 twistd.daemonize()
343 if config.pid:
344 self.writePID()
345
346 # Initialise debugging, and do the other half of SIGHUPstuff
347 debug.reloadConfig()
348 legacy.reloadConfig()
349
350 # Start the service
351 jid = config.jid
352 if config.useXCP and config.compjid:
353 jid = config.compjid
354 self.c = component.buildServiceManager(jid, config.secret, "tcp:%s:%s" % (config.mainServer, config.port))
355 self.transportSvc = PyTransport()
356 self.transportSvc.setServiceParent(self.c)
357 self.c.startService()
358 reactor.addSystemEventTrigger('before', 'shutdown', self.shuttingDown)
359
360 def writePID(self):
361 # Create a PID file
362 pid = str(os.getpid())
363 pf = open(config.pid, "w")
364 pf.write("%s\n" % pid)
365 pf.close()
366
367 def shuttingDown(self):
368 self.transportSvc.removeMe()
369 # Keep the transport running for another 3 seconds
370 def cb(ignored=None):
371 if config.pid:
372 twistd.removePID(config.pid)
373 d = Deferred()
374 d.addCallback(cb)
375 reactor.callLater(3.0, d.callback, None)
376 return d
377
378
379
380 def SIGHUPstuff(*args):
381 global configFile, configOptions
382 xmlconfig.reloadConfig(configFile, configOptions)
383 if config.pid and os.name != "posix":
384 config.pid = ""
385 debug.reloadConfig()
386 legacy.reloadConfig()
387
388 if os.name == "posix":
389 import signal
390 # Set SIGHUP to reload the config file & close & open debug file
391 signal.signal(signal.SIGHUP, SIGHUPstuff)
392 # Load some scripts for PID and daemonising
393 from twisted.scripts import _twistd_unix as twistd
394
395
396 def main():
397 # Create the application
398 app = App()
399 reactor.run()
400
401 if __name__ == "__main__":
402 main()
403