]> jfr.im git - irc/quakenet/qwebirc.git/blobdiff - qwebirc/ajaxengine.py
Make channel, nick and auth links less obvious.
[irc/quakenet/qwebirc.git] / qwebirc / ajaxengine.py
index 92b6228cdae669afd7c977eac28031e5029cc9a9..0aae808802aa34711150cc1e3f2a069077bc2679 100644 (file)
@@ -1,21 +1,44 @@
 from twisted.web import resource, server, static
 from twisted.names import client
 from twisted.internet import reactor
-import simplejson, md5, sys, os, ircclient, time, config
+from authgateengine import login_optional
+import simplejson, md5, sys, os, ircclient, time, config, weakref, traceback
 
 Sessions = {}
 
 def get_session_id():
   return md5.md5(os.urandom(16)).hexdigest()
+
+class BufferOverflowException(Exception):
+  pass
+
+class AJAXException(Exception):
+  pass
   
+class IDGenerationException(Exception):
+  pass
+
+NOT_DONE_YET = None
+
 def jsondump(fn):
   def decorator(*args, **kwargs):
-    x = fn(*args, **kwargs)
-    if isinstance(x, list):
-      return simplejson.dumps(x)
-    return x
+    try:
+      x = fn(*args, **kwargs)
+      if x is None:
+        return server.NOT_DONE_YET
+      x = (True, x)
+    except AJAXException, e:
+      x = (False, e[0])
+      
+    return simplejson.dumps(x)
   return decorator
 
+def cleanupSession(id):
+  try:
+    del Sessions[id]
+  except KeyError:
+    pass
+
 class IRCSession:
   def __init__(self, id):
     self.id = id
@@ -23,8 +46,13 @@ class IRCSession:
     self.buffer = []
     self.throttle = 0
     self.schedule = None
-    
+    self.closed = False
+    self.cleanupschedule = None
+
   def subscribe(self, channel):
+    if len(self.subscriptions) >= config.MAXSUBSCRIPTIONS:
+      self.subscriptions.pop(0).close()
+
     self.subscriptions.append(channel)
     self.flush()
       
@@ -59,14 +87,30 @@ class IRCSession:
         newsubs.append(x)
 
     self.subscriptions = newsubs
-     
+    if self.closed and not self.subscriptions:
+      cleanupSession(self.id)
+
   def event(self, data):
+    bufferlen = sum(map(len, self.buffer))
+    if bufferlen + len(data) > config.MAXBUFLEN:
+      self.buffer = []
+      self.client.error("Buffer overflow")
+      return
+
     self.buffer.append(data)
     self.flush()
     
   def push(self, data):
-    self.client.write(data)
+    if not self.closed:
+      self.client.write(data)
+
+  def disconnect(self):
+    # keep the session hanging around for a few seconds so the
+    # client has a chance to see what the issue was
+    self.closed = True
+
+    reactor.callLater(5, cleanupSession, self.id)
+
 class Channel:
   def __init__(self, request):
     self.request = request
@@ -77,6 +121,9 @@ class SingleUseChannel(Channel):
     self.request.finish()
     return False
     
+  def close(self):
+    self.request.finish()
+    
 class MultipleUseChannel(Channel):
   def write(self, data):
     self.request.write(data)
@@ -89,56 +136,91 @@ class AJAXEngine(resource.Resource):
     self.prefix = prefix
 
   @jsondump
-  def render_GET(self, request):
+  def render_POST(self, request):
     path = request.path[len(self.prefix):]
-    if path == "/n":
-      ip = request.transport.getPeer()
-      ip = ip[1]
+    if path[0] == "/":
+      handler = self.COMMANDS.get(path[1:])
+      if handler is not None:
+        return handler(self, request)
+    raise AJAXException("404")
 
-      nick, ident = request.args.get("nick"), "webchat"
-      if not nick:
-        return [False, "Nickname not supplied"]
-        
-      nick = nick[0]
-      
-      id = get_session_id()
-      
-      session = IRCSession(id)
+#  def render_GET(self, request):
+#    return self.render_POST(request)
+  
+  def newConnection(self, request):
+    ticket = login_optional(request)
+    
+    _, ip, port = request.transport.getPeer()
 
-      client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=nick)
-      session.client = client
-      
-      Sessions[id] = session
+    nick, ident, realname = request.args.get("nick"), "webchat", config.REALNAME
+    
+    if not ticket is None:
+      realname = "%s (%s:%d:%s)" % (realname, ticket.username, ticket.id, ticket.authflags)
       
-      return [True, id]
-          
-    if path.startswith("/s/"):
-      sessionid = path[3:]
-      session = Sessions.get(sessionid)
+    if not nick:
+      raise AJAXException("Nickname not supplied")
       
-      if not session:
-        return [False, "Bad session ID"]
+    nick = nick[0]
+
+    for i in xrange(10):
+      id = get_session_id()
+      if not Sessions.get(id):
+        break
+    else:
+      raise IDGenerationException()
 
-      session.subscribe(SingleUseChannel(request))
-      return server.NOT_DONE_YET
-    if path.startswith("/p/"):
-      command = request.args.get("c")
-      if not command:
-        return [False, "No command specified"]
+    session = IRCSession(id)
 
-      command = command[0]
+    client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=realname)
+    session.client = client
+    
+    Sessions[id] = session
+    
+    return id
+  
+  def getSession(self, request):
+    sessionid = request.args.get("s")
+    if sessionid is None:
+      raise AJAXException("Bad session ID")
       
-      sessionid = path[3:]
-      session = Sessions.get(sessionid)
-      if not session:
-        return [False, "Bad session ID"]
-
-      try:
-        decoded = command.decode("utf-8")
-      except UnicodeDecodeError:
-        decoded = command.decode("iso-8859-1", "ignore")
-      session.push(decoded)
-      return [True]
+    session = Sessions.get(sessionid[0])
+    if not session:
+      raise AJAXException("Bad session ID")
+    return session
+    
+  def subscribe(self, request):
+    self.getSession(request).subscribe(SingleUseChannel(request))
+    return NOT_DONE_YET
+
+  def push(self, request):
+    command = request.args.get("c")
+    if command is None:
+      raise AJAXException("No command specified")
+
+    command = command[0]
+    
+    session = self.getSession(request)
 
-    return [False, "404"]
+    try:
+      decoded = command.decode("utf-8")
+    except UnicodeDecodeError:
+      decoded = command.decode("iso-8859-1", "ignore")
 
+    if len(decoded) > config.MAXLINELEN:
+      session.disconnect()
+      raise AJAXException("Line too long")
+
+    try:
+      session.push(decoded)
+    except AttributeError: # occurs when we haven't noticed an error
+      session.disconnect()
+      raise AJAXException("Connection closed by server.")
+    except Exception, e: # catch all
+      session.disconnect()        
+      traceback.print_exc(file=sys.stderr)
+      raise AJAXException("Unknown error.")
+  
+    return True
+  
+  COMMANDS = dict(p=push, n=newConnection, s=subscribe)
+  
\ No newline at end of file