]> jfr.im git - irc/quakenet/qwebirc.git/blame - qwebirc/engines/ajaxengine.py
Merge into stable.
[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
85f01e3f
CP
5import simplejson, md5, sys, os, time, config, weakref, traceback
6import qwebirc.ircclient as ircclient
7from adminengine import AdminEngineAction
8from qwebirc.util import HitCounter
9e769c12
CP
9
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 = []
55 self.throttle = 0
56 self.schedule = None
8dc46dfa
CP
57 self.closed = False
58 self.cleanupschedule = None
59
265f5ce3
CP
60 def subscribe(self, channel, notifier):
61 timeout_entry = reactor.callLater(config.HTTP_AJAX_REQUEST_TIMEOUT, self.timeout, channel)
62 def cancel_timeout(result):
63 if channel in self.subscriptions:
64 self.subscriptions.remove(channel)
65 try:
66 timeout_entry.cancel()
67 except error.AlreadyCalled:
68 pass
69 notifier.addCallbacks(cancel_timeout, cancel_timeout)
70
0df6faa6 71 if len(self.subscriptions) >= config.MAXSUBSCRIPTIONS:
4e221566 72 self.subscriptions.pop(0).close()
0df6faa6 73
9e769c12
CP
74 self.subscriptions.append(channel)
75 self.flush()
76
265f5ce3
CP
77 def timeout(self, channel):
78 if self.schedule:
79 return
80
81 channel.write(simplejson.dumps([]))
82 if channel in self.subscriptions:
83 self.subscriptions.remove(channel)
84
9e769c12
CP
85 def flush(self, scheduled=False):
86 if scheduled:
87 self.schedule = None
88
89 if not self.buffer or not self.subscriptions:
90 return
91
92 t = time.time()
93
94 if t < self.throttle:
95 if not self.schedule:
96 self.schedule = reactor.callLater(self.throttle - t, self.flush, True)
97 return
98 else:
99 # process the rest of the packet
100 if not scheduled:
101 if not self.schedule:
102 self.schedule = reactor.callLater(0, self.flush, True)
103 return
104
105 self.throttle = t + config.UPDATE_FREQ
106
107 encdata = simplejson.dumps(self.buffer)
108 self.buffer = []
109
110 newsubs = []
111 for x in self.subscriptions:
112 if x.write(encdata):
113 newsubs.append(x)
114
115 self.subscriptions = newsubs
8dc46dfa
CP
116 if self.closed and not self.subscriptions:
117 cleanupSession(self.id)
118
9e769c12 119 def event(self, data):
8dc46dfa
CP
120 bufferlen = sum(map(len, self.buffer))
121 if bufferlen + len(data) > config.MAXBUFLEN:
122 self.buffer = []
99844c15 123 self.client.error("Buffer overflow.")
8dc46dfa
CP
124 return
125
9e769c12
CP
126 self.buffer.append(data)
127 self.flush()
128
129 def push(self, data):
8dc46dfa
CP
130 if not self.closed:
131 self.client.write(data)
132
133 def disconnect(self):
134 # keep the session hanging around for a few seconds so the
135 # client has a chance to see what the issue was
136 self.closed = True
137
138 reactor.callLater(5, cleanupSession, self.id)
139
9e769c12
CP
140class Channel:
141 def __init__(self, request):
142 self.request = request
143
144class SingleUseChannel(Channel):
145 def write(self, data):
146 self.request.write(data)
147 self.request.finish()
148 return False
149
4e221566
CP
150 def close(self):
151 self.request.finish()
152
9e769c12
CP
153class MultipleUseChannel(Channel):
154 def write(self, data):
155 self.request.write(data)
156 return True
157
158class AJAXEngine(resource.Resource):
159 isLeaf = True
160
161 def __init__(self, prefix):
162 self.prefix = prefix
85f01e3f
CP
163 self.__connect_hit = HitCounter()
164 self.__total_hit = HitCounter()
165
9e769c12 166 @jsondump
57ea572e 167 def render_POST(self, request):
9e769c12 168 path = request.path[len(self.prefix):]
f59585a7
CP
169 if path[0] == "/":
170 handler = self.COMMANDS.get(path[1:])
171 if handler is not None:
172 return handler(self, request)
99844c15
CP
173
174 raise PassthruException, http_error.NoResource().render(request)
f59585a7 175
99844c15
CP
176 #def render_GET(self, request):
177 #return self.render_POST(request)
f59585a7
CP
178
179 def newConnection(self, request):
f065bc69
CP
180 ticket = login_optional(request)
181
f59585a7 182 _, ip, port = request.transport.getPeer()
9e769c12 183
c70a7ff6 184 nick = request.args.get("nick")
f59585a7 185 if not nick:
99844c15 186 raise AJAXException, "Nickname not supplied."
c70a7ff6 187 nick = ircclient.irc_decode(nick[0])
57ea572e 188
c70a7ff6
CP
189 ident, realname = "webchat", config.REALNAME
190
f59585a7
CP
191 for i in xrange(10):
192 id = get_session_id()
193 if not Sessions.get(id):
194 break
195 else:
196 raise IDGenerationException()
9e769c12 197
f59585a7 198 session = IRCSession(id)
9e769c12 199
ace37679
CP
200 qticket = getSessionData(request).get("qticket")
201 if qticket is None:
202 perform = None
203 else:
348574ee
CP
204 service_mask = config.AUTH_SERVICE
205 msg_mask = service_mask.split("!")[0] + "@" + service_mask.split("@", 1)[1]
206 perform = ["PRIVMSG %s :TICKETAUTH %s" % (msg_mask, qticket)]
ace37679 207
85f01e3f 208 self.__connect_hit()
ace37679 209 client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=realname, perform=perform)
f59585a7
CP
210 session.client = client
211
212 Sessions[id] = session
213
214 return id
215
216 def getSession(self, request):
71afd444
CP
217 bad_session_message = "Invalid session, this most likely means the server has restarted; close this dialog and then try refreshing the page."
218
f59585a7
CP
219 sessionid = request.args.get("s")
220 if sessionid is None:
71afd444 221 raise AJAXException, bad_session_message
9e769c12 222
f59585a7
CP
223 session = Sessions.get(sessionid[0])
224 if not session:
71afd444 225 raise AJAXException, bad_session_message
f59585a7 226 return session
8dc46dfa 227
f59585a7 228 def subscribe(self, request):
1d924d97 229 request.channel.cancelTimeout()
265f5ce3 230 self.getSession(request).subscribe(SingleUseChannel(request), request.notifyFinish())
bdd008f9 231 return NOT_DONE_YET
9e769c12 232
f59585a7
CP
233 def push(self, request):
234 command = request.args.get("c")
235 if command is None:
99844c15 236 raise AJAXException, "No command specified."
85f01e3f
CP
237 self.__total_hit()
238
c70a7ff6 239 decoded = ircclient.irc_decode(command[0])
f59585a7
CP
240
241 session = self.getSession(request)
242
f59585a7
CP
243 if len(decoded) > config.MAXLINELEN:
244 session.disconnect()
99844c15 245 raise AJAXException, "Line too long."
f59585a7
CP
246
247 try:
248 session.push(decoded)
249 except AttributeError: # occurs when we haven't noticed an error
250 session.disconnect()
99844c15 251 raise AJAXException, "Connection closed by server; try reconnecting by reloading the page."
f59585a7
CP
252 except Exception, e: # catch all
253 session.disconnect()
254 traceback.print_exc(file=sys.stderr)
71afd444 255 raise AJAXException, "Unknown error."
f59585a7
CP
256
257 return True
258
85f01e3f
CP
259 def closeById(self, k):
260 s = Sessions.get(k)
261 if s is None:
262 return
263 s.client.client.error("Closed by admin interface")
264
265 @property
266 def adminEngine(self):
267 return {
268 "Sessions": [(str(v.client.client), AdminEngineAction("close", self.closeById, k)) for k, v in Sessions.iteritems() if not v.closed],
269 "Connections": [(self.__connect_hit,)],
270 "Total hits": [(self.__total_hit,)],
271 }
272
f59585a7
CP
273 COMMANDS = dict(p=push, n=newConnection, s=subscribe)
274