1 from twisted
.web
import resource
, server
, static
, error
as http_error
2 from twisted
.names
import client
3 from twisted
.internet
import reactor
, error
4 from authgateengine
import login_optional
, getSessionData
5 import simplejson
, md5
, sys
, os
, time
, config
, weakref
, traceback
6 import qwebirc
.ircclient
as ircclient
7 from adminengine
import AdminEngineAction
8 from qwebirc
.util
import HitCounter
13 return md5
.md5(os
.urandom(16)).hexdigest()
15 class BufferOverflowException(Exception):
18 class AJAXException(Exception):
21 class IDGenerationException(Exception):
24 class PassthruException(Exception):
30 def decorator(*args
, **kwargs
):
32 x
= fn(*args
, **kwargs
)
34 return server
.NOT_DONE_YET
36 except AJAXException
, e
:
38 except PassthruException
, e
:
41 return simplejson
.dumps(x
)
44 def cleanupSession(id):
51 def __init__(self
, id):
53 self
.subscriptions
= []
58 self
.cleanupschedule
= None
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
)
66 timeout_entry
.cancel()
67 except error
.AlreadyCalled
:
69 notifier
.addCallbacks(cancel_timeout
, cancel_timeout
)
71 if len(self
.subscriptions
) >= config
.MAXSUBSCRIPTIONS
:
72 self
.subscriptions
.pop(0).close()
74 self
.subscriptions
.append(channel
)
77 def timeout(self
, channel
):
81 channel
.write(simplejson
.dumps([]))
82 if channel
in self
.subscriptions
:
83 self
.subscriptions
.remove(channel
)
85 def flush(self
, scheduled
=False):
89 if not self
.buffer or not self
.subscriptions
:
96 self
.schedule
= reactor
.callLater(self
.throttle
- t
, self
.flush
, True)
99 # process the rest of the packet
101 if not self
.schedule
:
102 self
.schedule
= reactor
.callLater(0, self
.flush
, True)
105 self
.throttle
= t
+ config
.UPDATE_FREQ
107 encdata
= simplejson
.dumps(self
.buffer)
111 for x
in self
.subscriptions
:
115 self
.subscriptions
= newsubs
116 if self
.closed
and not self
.subscriptions
:
117 cleanupSession(self
.id)
119 def event(self
, data
):
120 bufferlen
= sum(map(len, self
.buffer))
121 if bufferlen
+ len(data
) > config
.MAXBUFLEN
:
123 self
.client
.error("Buffer overflow.")
126 self
.buffer.append(data
)
129 def push(self
, data
):
131 self
.client
.write(data
)
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
138 reactor
.callLater(5, cleanupSession
, self
.id)
141 def __init__(self
, request
):
142 self
.request
= request
144 class SingleUseChannel(Channel
):
145 def write(self
, data
):
146 self
.request
.write(data
)
147 self
.request
.finish()
151 self
.request
.finish()
153 class MultipleUseChannel(Channel
):
154 def write(self
, data
):
155 self
.request
.write(data
)
158 class AJAXEngine(resource
.Resource
):
161 def __init__(self
, prefix
):
163 self
.__connect
_hit
= HitCounter()
164 self
.__total
_hit
= HitCounter()
167 def render_POST(self
, request
):
168 path
= request
.path
[len(self
.prefix
):]
170 handler
= self
.COMMANDS
.get(path
[1:])
171 if handler
is not None:
172 return handler(self
, request
)
174 raise PassthruException
, http_error
.NoResource().render(request
)
176 #def render_GET(self, request):
177 #return self.render_POST(request)
179 def newConnection(self
, request
):
180 ticket
= login_optional(request
)
182 _
, ip
, port
= request
.transport
.getPeer()
184 nick
= request
.args
.get("nick")
186 raise AJAXException
, "Nickname not supplied."
187 nick
= ircclient
.irc_decode(nick
[0])
189 ident
, realname
= "webchat", config
.REALNAME
192 id = get_session_id()
193 if not Sessions
.get(id):
196 raise IDGenerationException()
198 session
= IRCSession(id)
200 qticket
= getSessionData(request
).get("qticket")
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
)]
209 client
= ircclient
.createIRC(session
, nick
=nick
, ident
=ident
, ip
=ip
, realname
=realname
, perform
=perform
)
210 session
.client
= client
212 Sessions
[id] = session
216 def getSession(self
, request
):
217 bad_session_message
= "Invalid session, this most likely means the server has restarted; close this dialog and then try refreshing the page."
219 sessionid
= request
.args
.get("s")
220 if sessionid
is None:
221 raise AJAXException
, bad_session_message
223 session
= Sessions
.get(sessionid
[0])
225 raise AJAXException
, bad_session_message
228 def subscribe(self
, request
):
229 request
.channel
.cancelTimeout()
230 self
.getSession(request
).subscribe(SingleUseChannel(request
), request
.notifyFinish())
233 def push(self
, request
):
234 command
= request
.args
.get("c")
236 raise AJAXException
, "No command specified."
239 decoded
= ircclient
.irc_decode(command
[0])
241 session
= self
.getSession(request
)
243 if len(decoded
) > config
.MAXLINELEN
:
245 raise AJAXException
, "Line too long."
248 session
.push(decoded
)
249 except AttributeError: # occurs when we haven't noticed an error
251 raise AJAXException
, "Connection closed by server; try reconnecting by reloading the page."
252 except Exception, e
: # catch all
254 traceback
.print_exc(file=sys
.stderr
)
255 raise AJAXException
, "Unknown error."
259 def closeById(self
, k
):
263 s
.client
.client
.error("Closed by admin interface")
266 def adminEngine(self
):
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
,)],
273 COMMANDS
= dict(p
=push
, n
=newConnection
, s
=subscribe
)