]>
Commit | Line | Data |
---|---|---|
99844c15 | 1 | from twisted.web import resource, server, static, error as http_error |
9e769c12 | 2 | from twisted.names import client |
265f5ce3 | 3 | from twisted.internet import reactor, error |
ace37679 | 4 | from authgateengine import login_optional, getSessionData |
930be88a | 5 | import simplejson, md5, sys, os, time, config, qwebirc.config_options as config_options, traceback, socket |
85f01e3f CP |
6 | import qwebirc.ircclient as ircclient |
7 | from adminengine import AdminEngineAction | |
8 | from qwebirc.util import HitCounter | |
28c4ad01 | 9 | import qwebirc.dns as qdns |
9e769c12 CP |
10 | Sessions = {} |
11 | ||
12 | def get_session_id(): | |
4e4bbf26 | 13 | return md5.md5(os.urandom(16)).hexdigest() |
8dc46dfa CP |
14 | |
15 | class BufferOverflowException(Exception): | |
16 | pass | |
17 | ||
f59585a7 CP |
18 | class AJAXException(Exception): |
19 | pass | |
20 | ||
4094890f CP |
21 | class IDGenerationException(Exception): |
22 | pass | |
23 | ||
99844c15 CP |
24 | class PassthruException(Exception): |
25 | pass | |
26 | ||
bdd008f9 CP |
27 | NOT_DONE_YET = None |
28 | ||
9e769c12 CP |
29 | def 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 |
44 | def cleanupSession(id): |
45 | try: | |
46 | del Sessions[id] | |
47 | except KeyError: | |
48 | pass | |
49 | ||
9e769c12 CP |
50 | class 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! |
144 | def connect_notice(line): | |
145 | return "c", "NOTICE", "", ("AUTH", "*** (qwebirc) %s" % line) | |
146 | ||
9e769c12 CP |
147 | class Channel: |
148 | def __init__(self, request): | |
149 | self.request = request | |
150 | ||
151 | class 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 |
160 | class MultipleUseChannel(Channel): |
161 | def write(self, data): | |
162 | self.request.write(data) | |
163 | return True | |
164 | ||
165 | class 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 | |
6ce70043 | 193 | password = request.args.get("password") |
2f74dea9 CP |
194 | if password is not None: |
195 | password = ircclient.irc_decode(password[0]) | |
196 | ||
f59585a7 CP |
197 | for i in xrange(10): |
198 | id = get_session_id() | |
199 | if not Sessions.get(id): | |
200 | break | |
201 | else: | |
202 | raise IDGenerationException() | |
9e769c12 | 203 | |
f59585a7 | 204 | session = IRCSession(id) |
9e769c12 | 205 | |
ace37679 CP |
206 | qticket = getSessionData(request).get("qticket") |
207 | if qticket is None: | |
208 | perform = None | |
209 | else: | |
348574ee CP |
210 | service_mask = config.AUTH_SERVICE |
211 | msg_mask = service_mask.split("!")[0] + "@" + service_mask.split("@", 1)[1] | |
212 | perform = ["PRIVMSG %s :TICKETAUTH %s" % (msg_mask, qticket)] | |
ace37679 | 213 | |
b5c84380 | 214 | ident, realname = config.IDENT, config.REALNAME |
930be88a | 215 | if ident is config_options.IDENT_HEX or ident is None: # latter is legacy |
b5c84380 | 216 | ident = socket.inet_aton(ip).encode("hex") |
930be88a CP |
217 | elif ident is config_options.IDENT_NICKNAME: |
218 | ident = nick | |
b5c84380 | 219 | |
85f01e3f | 220 | self.__connect_hit() |
28c4ad01 CP |
221 | |
222 | def proceed(hostname): | |
2f74dea9 CP |
223 | kwargs = dict(nick=nick, ident=ident, ip=ip, realname=realname, perform=perform, hostname=hostname) |
224 | if password is not None: | |
225 | kwargs["password"] = password | |
226 | ||
227 | client = ircclient.createIRC(session, **kwargs) | |
28c4ad01 CP |
228 | session.client = client |
229 | ||
930be88a CP |
230 | if not hasattr(config, "WEBIRC_MODE") or config.WEBIRC_MODE == "hmac": |
231 | proceed(None) | |
232 | elif config.WEBIRC_MODE != "hmac": | |
28c4ad01 CP |
233 | notice = lambda x: session.event(connect_notice(x)) |
234 | notice("Looking up your hostname...") | |
235 | def callback(hostname): | |
236 | notice("Found your hostname.") | |
237 | proceed(hostname) | |
238 | def errback(failure): | |
239 | notice("Couldn't look up your hostname!") | |
240 | proceed(ip) | |
241 | qdns.lookupAndVerifyPTR(ip, timeout=[config.DNS_TIMEOUT]).addCallbacks(callback, errback) | |
28c4ad01 | 242 | |
f59585a7 CP |
243 | Sessions[id] = session |
244 | ||
245 | return id | |
246 | ||
247 | def getSession(self, request): | |
71afd444 CP |
248 | bad_session_message = "Invalid session, this most likely means the server has restarted; close this dialog and then try refreshing the page." |
249 | ||
f59585a7 CP |
250 | sessionid = request.args.get("s") |
251 | if sessionid is None: | |
71afd444 | 252 | raise AJAXException, bad_session_message |
9e769c12 | 253 | |
f59585a7 CP |
254 | session = Sessions.get(sessionid[0]) |
255 | if not session: | |
71afd444 | 256 | raise AJAXException, bad_session_message |
f59585a7 | 257 | return session |
8dc46dfa | 258 | |
f59585a7 | 259 | def subscribe(self, request): |
1d924d97 | 260 | request.channel.cancelTimeout() |
265f5ce3 | 261 | self.getSession(request).subscribe(SingleUseChannel(request), request.notifyFinish()) |
bdd008f9 | 262 | return NOT_DONE_YET |
9e769c12 | 263 | |
f59585a7 CP |
264 | def push(self, request): |
265 | command = request.args.get("c") | |
266 | if command is None: | |
99844c15 | 267 | raise AJAXException, "No command specified." |
85f01e3f CP |
268 | self.__total_hit() |
269 | ||
c70a7ff6 | 270 | decoded = ircclient.irc_decode(command[0]) |
f59585a7 CP |
271 | |
272 | session = self.getSession(request) | |
273 | ||
f59585a7 CP |
274 | if len(decoded) > config.MAXLINELEN: |
275 | session.disconnect() | |
99844c15 | 276 | raise AJAXException, "Line too long." |
f59585a7 CP |
277 | |
278 | try: | |
279 | session.push(decoded) | |
280 | except AttributeError: # occurs when we haven't noticed an error | |
281 | session.disconnect() | |
99844c15 | 282 | raise AJAXException, "Connection closed by server; try reconnecting by reloading the page." |
f59585a7 CP |
283 | except Exception, e: # catch all |
284 | session.disconnect() | |
285 | traceback.print_exc(file=sys.stderr) | |
71afd444 | 286 | raise AJAXException, "Unknown error." |
f59585a7 CP |
287 | |
288 | return True | |
289 | ||
85f01e3f CP |
290 | def closeById(self, k): |
291 | s = Sessions.get(k) | |
292 | if s is None: | |
293 | return | |
294 | s.client.client.error("Closed by admin interface") | |
295 | ||
296 | @property | |
297 | def adminEngine(self): | |
298 | return { | |
299 | "Sessions": [(str(v.client.client), AdminEngineAction("close", self.closeById, k)) for k, v in Sessions.iteritems() if not v.closed], | |
300 | "Connections": [(self.__connect_hit,)], | |
301 | "Total hits": [(self.__total_hit,)], | |
302 | } | |
303 | ||
f59585a7 | 304 | COMMANDS = dict(p=push, n=newConnection, s=subscribe) |
b5c84380 | 305 |