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