]> jfr.im git - irc/quakenet/qwebirc.git/blame - qwebirc/engines/ajaxengine.py
Don't show multiple 'maximum retries exceeded' dialogs.
[irc/quakenet/qwebirc.git] / qwebirc / engines / ajaxengine.py
CommitLineData
99844c15 1from twisted.web import resource, server, static, error as http_error
9e769c12 2from twisted.names import client
265f5ce3 3from twisted.internet import reactor, error
ace37679 4from authgateengine import login_optional, getSessionData
b5c84380 5import simplejson, md5, sys, os, time, config, weakref, traceback, socket
85f01e3f
CP
6import qwebirc.ircclient as ircclient
7from adminengine import AdminEngineAction
8from qwebirc.util import HitCounter
28c4ad01 9import qwebirc.dns as qdns
9e769c12
CP
10Sessions = {}
11
12def get_session_id():
4e4bbf26 13 return md5.md5(os.urandom(16)).hexdigest()
8dc46dfa
CP
14
15class BufferOverflowException(Exception):
16 pass
17
f59585a7
CP
18class AJAXException(Exception):
19 pass
20
4094890f
CP
21class IDGenerationException(Exception):
22 pass
23
99844c15
CP
24class PassthruException(Exception):
25 pass
26
bdd008f9
CP
27NOT_DONE_YET = None
28
9e769c12
CP
29def jsondump(fn):
30 def decorator(*args, **kwargs):
f59585a7
CP
31 try:
32 x = fn(*args, **kwargs)
bdd008f9
CP
33 if x is None:
34 return server.NOT_DONE_YET
35 x = (True, x)
f59585a7 36 except AJAXException, e:
bdd008f9 37 x = (False, e[0])
99844c15
CP
38 except PassthruException, e:
39 return str(e)
f59585a7
CP
40
41 return simplejson.dumps(x)
9e769c12
CP
42 return decorator
43
8dc46dfa
CP
44def cleanupSession(id):
45 try:
46 del Sessions[id]
47 except KeyError:
48 pass
49
9e769c12
CP
50class IRCSession:
51 def __init__(self, id):
52 self.id = id
53 self.subscriptions = []
54 self.buffer = []
8932790b 55 self.buflen = 0
9e769c12
CP
56 self.throttle = 0
57 self.schedule = None
8dc46dfa
CP
58 self.closed = False
59 self.cleanupschedule = None
60
265f5ce3
CP
61 def subscribe(self, channel, notifier):
62 timeout_entry = reactor.callLater(config.HTTP_AJAX_REQUEST_TIMEOUT, self.timeout, channel)
63 def cancel_timeout(result):
64 if channel in self.subscriptions:
65 self.subscriptions.remove(channel)
66 try:
67 timeout_entry.cancel()
68 except error.AlreadyCalled:
69 pass
70 notifier.addCallbacks(cancel_timeout, cancel_timeout)
71
0df6faa6 72 if len(self.subscriptions) >= config.MAXSUBSCRIPTIONS:
4e221566 73 self.subscriptions.pop(0).close()
0df6faa6 74
9e769c12
CP
75 self.subscriptions.append(channel)
76 self.flush()
77
265f5ce3
CP
78 def timeout(self, channel):
79 if self.schedule:
80 return
81
82 channel.write(simplejson.dumps([]))
83 if channel in self.subscriptions:
84 self.subscriptions.remove(channel)
85
9e769c12
CP
86 def flush(self, scheduled=False):
87 if scheduled:
88 self.schedule = None
89
90 if not self.buffer or not self.subscriptions:
91 return
92
93 t = time.time()
94
95 if t < self.throttle:
96 if not self.schedule:
97 self.schedule = reactor.callLater(self.throttle - t, self.flush, True)
98 return
99 else:
100 # process the rest of the packet
101 if not scheduled:
102 if not self.schedule:
103 self.schedule = reactor.callLater(0, self.flush, True)
104 return
105
106 self.throttle = t + config.UPDATE_FREQ
107
108 encdata = simplejson.dumps(self.buffer)
109 self.buffer = []
8932790b
CP
110 self.buflen = 0
111
9e769c12
CP
112 newsubs = []
113 for x in self.subscriptions:
114 if x.write(encdata):
115 newsubs.append(x)
116
117 self.subscriptions = newsubs
8dc46dfa
CP
118 if self.closed and not self.subscriptions:
119 cleanupSession(self.id)
120
9e769c12 121 def event(self, data):
8932790b
CP
122 newbuflen = self.buflen + len(data)
123 if newbuflen > config.MAXBUFLEN:
8dc46dfa 124 self.buffer = []
99844c15 125 self.client.error("Buffer overflow.")
8dc46dfa
CP
126 return
127
9e769c12 128 self.buffer.append(data)
8932790b 129 self.buflen = newbuflen
9e769c12
CP
130 self.flush()
131
132 def push(self, data):
8dc46dfa
CP
133 if not self.closed:
134 self.client.write(data)
135
136 def disconnect(self):
137 # keep the session hanging around for a few seconds so the
138 # client has a chance to see what the issue was
139 self.closed = True
140
141 reactor.callLater(5, cleanupSession, self.id)
142
28c4ad01
CP
143# DANGER! Breach of encapsulation!
144def connect_notice(line):
145 return "c", "NOTICE", "", ("AUTH", "*** (qwebirc) %s" % line)
146
9e769c12
CP
147class Channel:
148 def __init__(self, request):
149 self.request = request
150
151class SingleUseChannel(Channel):
152 def write(self, data):
153 self.request.write(data)
154 self.request.finish()
155 return False
156
4e221566
CP
157 def close(self):
158 self.request.finish()
159
9e769c12
CP
160class MultipleUseChannel(Channel):
161 def write(self, data):
162 self.request.write(data)
163 return True
164
165class AJAXEngine(resource.Resource):
166 isLeaf = True
167
168 def __init__(self, prefix):
169 self.prefix = prefix
85f01e3f
CP
170 self.__connect_hit = HitCounter()
171 self.__total_hit = HitCounter()
172
9e769c12 173 @jsondump
57ea572e 174 def render_POST(self, request):
9e769c12 175 path = request.path[len(self.prefix):]
f59585a7
CP
176 if path[0] == "/":
177 handler = self.COMMANDS.get(path[1:])
178 if handler is not None:
179 return handler(self, request)
99844c15
CP
180
181 raise PassthruException, http_error.NoResource().render(request)
f59585a7 182
f59585a7 183 def newConnection(self, request):
f065bc69
CP
184 ticket = login_optional(request)
185
f59585a7 186 _, ip, port = request.transport.getPeer()
9e769c12 187
c70a7ff6 188 nick = request.args.get("nick")
f59585a7 189 if not nick:
99844c15 190 raise AJAXException, "Nickname not supplied."
c70a7ff6 191 nick = ircclient.irc_decode(nick[0])
57ea572e 192
f59585a7
CP
193 for i in xrange(10):
194 id = get_session_id()
195 if not Sessions.get(id):
196 break
197 else:
198 raise IDGenerationException()
9e769c12 199
f59585a7 200 session = IRCSession(id)
9e769c12 201
ace37679
CP
202 qticket = getSessionData(request).get("qticket")
203 if qticket is None:
204 perform = None
205 else:
348574ee
CP
206 service_mask = config.AUTH_SERVICE
207 msg_mask = service_mask.split("!")[0] + "@" + service_mask.split("@", 1)[1]
208 perform = ["PRIVMSG %s :TICKETAUTH %s" % (msg_mask, qticket)]
ace37679 209
b5c84380
CP
210 ident, realname = config.IDENT, config.REALNAME
211 if ident is None:
212 ident = socket.inet_aton(ip).encode("hex")
213
85f01e3f 214 self.__connect_hit()
28c4ad01
CP
215
216 def proceed(hostname):
217 client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=realname, perform=perform, hostname=hostname)
218 session.client = client
219
220 if config.WEBIRC_MODE != "hmac":
221 notice = lambda x: session.event(connect_notice(x))
222 notice("Looking up your hostname...")
223 def callback(hostname):
224 notice("Found your hostname.")
225 proceed(hostname)
226 def errback(failure):
227 notice("Couldn't look up your hostname!")
228 proceed(ip)
229 qdns.lookupAndVerifyPTR(ip, timeout=[config.DNS_TIMEOUT]).addCallbacks(callback, errback)
230 else:
231 proceed(None) # hmac doesn't care
232
f59585a7
CP
233 Sessions[id] = session
234
235 return id
236
237 def getSession(self, request):
71afd444
CP
238 bad_session_message = "Invalid session, this most likely means the server has restarted; close this dialog and then try refreshing the page."
239
f59585a7
CP
240 sessionid = request.args.get("s")
241 if sessionid is None:
71afd444 242 raise AJAXException, bad_session_message
9e769c12 243
f59585a7
CP
244 session = Sessions.get(sessionid[0])
245 if not session:
71afd444 246 raise AJAXException, bad_session_message
f59585a7 247 return session
8dc46dfa 248
f59585a7 249 def subscribe(self, request):
1d924d97 250 request.channel.cancelTimeout()
265f5ce3 251 self.getSession(request).subscribe(SingleUseChannel(request), request.notifyFinish())
bdd008f9 252 return NOT_DONE_YET
9e769c12 253
f59585a7
CP
254 def push(self, request):
255 command = request.args.get("c")
256 if command is None:
99844c15 257 raise AJAXException, "No command specified."
85f01e3f
CP
258 self.__total_hit()
259
c70a7ff6 260 decoded = ircclient.irc_decode(command[0])
f59585a7
CP
261
262 session = self.getSession(request)
263
f59585a7
CP
264 if len(decoded) > config.MAXLINELEN:
265 session.disconnect()
99844c15 266 raise AJAXException, "Line too long."
f59585a7
CP
267
268 try:
269 session.push(decoded)
270 except AttributeError: # occurs when we haven't noticed an error
271 session.disconnect()
99844c15 272 raise AJAXException, "Connection closed by server; try reconnecting by reloading the page."
f59585a7
CP
273 except Exception, e: # catch all
274 session.disconnect()
275 traceback.print_exc(file=sys.stderr)
71afd444 276 raise AJAXException, "Unknown error."
f59585a7
CP
277
278 return True
279
85f01e3f
CP
280 def closeById(self, k):
281 s = Sessions.get(k)
282 if s is None:
283 return
284 s.client.client.error("Closed by admin interface")
285
286 @property
287 def adminEngine(self):
288 return {
289 "Sessions": [(str(v.client.client), AdminEngineAction("close", self.closeById, k)) for k, v in Sessions.iteritems() if not v.closed],
290 "Connections": [(self.__connect_hit,)],
291 "Total hits": [(self.__total_hit,)],
292 }
293
f59585a7 294 COMMANDS = dict(p=push, n=newConnection, s=subscribe)
b5c84380 295