]> jfr.im git - irc/quakenet/qwebirc.git/blame - qwebirc/ajaxengine.py
Authgate integration stage 1.
[irc/quakenet/qwebirc.git] / qwebirc / ajaxengine.py
CommitLineData
9e769c12
CP
1from twisted.web import resource, server, static
2from twisted.names import client
3from twisted.internet import reactor
f065bc69
CP
4from authgateengine import login_optional
5import simplejson, md5, sys, os, ircclient, time, config, weakref, traceback
9e769c12
CP
6
7Sessions = {}
8
9def get_session_id():
4e4bbf26 10 return md5.md5(os.urandom(16)).hexdigest()
8dc46dfa
CP
11
12class BufferOverflowException(Exception):
13 pass
14
f59585a7
CP
15class AJAXException(Exception):
16 pass
17
4094890f
CP
18class IDGenerationException(Exception):
19 pass
20
bdd008f9
CP
21NOT_DONE_YET = None
22
9e769c12
CP
23def jsondump(fn):
24 def decorator(*args, **kwargs):
f59585a7
CP
25 try:
26 x = fn(*args, **kwargs)
bdd008f9
CP
27 if x is None:
28 return server.NOT_DONE_YET
29 x = (True, x)
f59585a7 30 except AJAXException, e:
bdd008f9 31 x = (False, e[0])
f59585a7
CP
32
33 return simplejson.dumps(x)
9e769c12
CP
34 return decorator
35
8dc46dfa
CP
36def cleanupSession(id):
37 try:
38 del Sessions[id]
39 except KeyError:
40 pass
41
9e769c12
CP
42class IRCSession:
43 def __init__(self, id):
44 self.id = id
45 self.subscriptions = []
46 self.buffer = []
47 self.throttle = 0
48 self.schedule = None
8dc46dfa
CP
49 self.closed = False
50 self.cleanupschedule = None
51
9e769c12 52 def subscribe(self, channel):
0df6faa6 53 if len(self.subscriptions) >= config.MAXSUBSCRIPTIONS:
4e221566 54 self.subscriptions.pop(0).close()
0df6faa6 55
9e769c12
CP
56 self.subscriptions.append(channel)
57 self.flush()
58
59 def flush(self, scheduled=False):
60 if scheduled:
61 self.schedule = None
62
63 if not self.buffer or not self.subscriptions:
64 return
65
66 t = time.time()
67
68 if t < self.throttle:
69 if not self.schedule:
70 self.schedule = reactor.callLater(self.throttle - t, self.flush, True)
71 return
72 else:
73 # process the rest of the packet
74 if not scheduled:
75 if not self.schedule:
76 self.schedule = reactor.callLater(0, self.flush, True)
77 return
78
79 self.throttle = t + config.UPDATE_FREQ
80
81 encdata = simplejson.dumps(self.buffer)
82 self.buffer = []
83
84 newsubs = []
85 for x in self.subscriptions:
86 if x.write(encdata):
87 newsubs.append(x)
88
89 self.subscriptions = newsubs
8dc46dfa
CP
90 if self.closed and not self.subscriptions:
91 cleanupSession(self.id)
92
9e769c12 93 def event(self, data):
8dc46dfa
CP
94 bufferlen = sum(map(len, self.buffer))
95 if bufferlen + len(data) > config.MAXBUFLEN:
96 self.buffer = []
97 self.client.error("Buffer overflow")
98 return
99
9e769c12
CP
100 self.buffer.append(data)
101 self.flush()
102
103 def push(self, data):
8dc46dfa
CP
104 if not self.closed:
105 self.client.write(data)
106
107 def disconnect(self):
108 # keep the session hanging around for a few seconds so the
109 # client has a chance to see what the issue was
110 self.closed = True
111
112 reactor.callLater(5, cleanupSession, self.id)
113
9e769c12
CP
114class Channel:
115 def __init__(self, request):
116 self.request = request
117
118class SingleUseChannel(Channel):
119 def write(self, data):
120 self.request.write(data)
121 self.request.finish()
122 return False
123
4e221566
CP
124 def close(self):
125 self.request.finish()
126
9e769c12
CP
127class MultipleUseChannel(Channel):
128 def write(self, data):
129 self.request.write(data)
130 return True
131
132class AJAXEngine(resource.Resource):
133 isLeaf = True
134
135 def __init__(self, prefix):
136 self.prefix = prefix
137
138 @jsondump
57ea572e 139 def render_POST(self, request):
9e769c12 140 path = request.path[len(self.prefix):]
f59585a7
CP
141 if path[0] == "/":
142 handler = self.COMMANDS.get(path[1:])
143 if handler is not None:
144 return handler(self, request)
145 raise AJAXException("404")
146
147# def render_GET(self, request):
148# return self.render_POST(request)
149
150 def newConnection(self, request):
f065bc69
CP
151 ticket = login_optional(request)
152
f59585a7 153 _, ip, port = request.transport.getPeer()
9e769c12 154
f065bc69
CP
155 nick, ident, realname = request.args.get("nick"), "webchat", config.REALNAME
156
157 if not ticket is None:
158 realname = "%s (%s:%d:%s)" % (realname, ticket.username, ticket.id, ticket.authflags)
159
f59585a7
CP
160 if not nick:
161 raise AJAXException("Nickname not supplied")
9e769c12 162
f59585a7 163 nick = nick[0]
57ea572e 164
f59585a7
CP
165 for i in xrange(10):
166 id = get_session_id()
167 if not Sessions.get(id):
168 break
169 else:
170 raise IDGenerationException()
9e769c12 171
f59585a7 172 session = IRCSession(id)
9e769c12 173
f065bc69 174 client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=realname)
f59585a7
CP
175 session.client = client
176
177 Sessions[id] = session
178
179 return id
180
181 def getSession(self, request):
182 sessionid = request.args.get("s")
183 if sessionid is None:
184 raise AJAXException("Bad session ID")
9e769c12 185
f59585a7
CP
186 session = Sessions.get(sessionid[0])
187 if not session:
188 raise AJAXException("Bad session ID")
189 return session
8dc46dfa 190
f59585a7
CP
191 def subscribe(self, request):
192 self.getSession(request).subscribe(SingleUseChannel(request))
bdd008f9 193 return NOT_DONE_YET
9e769c12 194
f59585a7
CP
195 def push(self, request):
196 command = request.args.get("c")
197 if command is None:
198 raise AJAXException("No command specified")
199
200 command = command[0]
201
202 session = self.getSession(request)
203
204 try:
205 decoded = command.decode("utf-8")
206 except UnicodeDecodeError:
207 decoded = command.decode("iso-8859-1", "ignore")
208
209 if len(decoded) > config.MAXLINELEN:
210 session.disconnect()
211 raise AJAXException("Line too long")
212
213 try:
214 session.push(decoded)
215 except AttributeError: # occurs when we haven't noticed an error
216 session.disconnect()
217 raise AJAXException("Connection closed by server.")
218 except Exception, e: # catch all
219 session.disconnect()
220 traceback.print_exc(file=sys.stderr)
221 raise AJAXException("Unknown error.")
222
223 return True
224
225 COMMANDS = dict(p=push, n=newConnection, s=subscribe)
226