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