]> jfr.im git - irc/quakenet/qwebirc.git/commitdiff
Add admin engine and reorganise a lot of directory structure.
authorChris Porter <redacted>
Sat, 31 Jan 2009 22:25:40 +0000 (22:25 +0000)
committerChris Porter <redacted>
Sat, 31 Jan 2009 22:25:40 +0000 (22:25 +0000)
Also add GZip support for static files.

15 files changed:
config.py.example
pagegen.py
qwebirc/engines/__init__.py [new file with mode: 0644]
qwebirc/engines/adminengine.py [new file with mode: 0644]
qwebirc/engines/ajaxengine.py [moved from qwebirc/ajaxengine.py with 88% similarity]
qwebirc/engines/authgateengine.py [moved from qwebirc/authgateengine.py with 80% similarity]
qwebirc/engines/feedbackengine.py [moved from qwebirc/feedbackengine.py with 90% similarity]
qwebirc/engines/staticengine.py [new file with mode: 0644]
qwebirc/ircclient.py
qwebirc/root.py
qwebirc/util/__init__.py [new file with mode: 0644]
qwebirc/util/ciphers.py [moved from qwebirc/ciphers.py with 100% similarity]
qwebirc/util/gziprequest.py [new file with mode: 0644]
qwebirc/util/hitcounter.py [new file with mode: 0644]
qwebirc/util/rijndael.py [moved from qwebirc/rijndael.py with 100% similarity]

index 7d4d34fd22a58e0939f6d24b471228893f3d5556..45f36773cc1bf3df3ce84e92dc52508f4817599f 100644 (file)
@@ -13,3 +13,4 @@ FEEDBACK_FROM = "moo@moo.com"
 FEEDBACK_TO = "moo@moo.com"
 FEEDBACK_SMTP_HOST = "127.0.0.1"
 FEEDBACK_SMTP_PORT = 25
+ADMIN_ENGINE_HOSTS = ["127.0.0.1"]
index 833c64b249ca15866ac4a06112a5bf07a8078c9c..0745c2ebe16074064f728c1561972601822d1c84 100644 (file)
@@ -5,22 +5,21 @@ UI_BASE = ["baseui", "baseuiwindow", "colour", "url", "theme", "hilightcontrolle
 
 DEBUG_BASE = ["qwebirc", "version", "jslib", "crypto", "md5", ["irc/%s" % x for x in IRC_BASE], ["ui/%s" % x for x in UI_BASE], "qwebircinterface", "auth", "sound"]
 BUILD_BASE = ["qwebirc"]
-JS_BASE = ["mootools-1.2.1-core"]
+JS_BASE = ["mootools-1.2.1-core", "mootools-1.2-more"]
 
 UIs = {
   "qui": {
     "class": "QUI",
     "uifiles": ["qui"],
     "extra": ["mootools-1.2-more"],
-    "buildextra": ["mootools-1.2-more"],
     "doctype": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"" + "\n" \
       "  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
   },
   "mochaui": {
     "class": "MochaUI",
     "uifiles": ["mochaui"],
-    "extra": ["mootools-1.2-more", "mochaui/mocha"],
-    "buildextra": ["mootools-1.2-more", "mochaui/mocha-compressed"],
+    "extra": ["mochaui/mocha"],
+    "buildextra": ["mochaui/mocha-compressed"],
     "doctype": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">",
     "div": """
     <div id="desktop">
diff --git a/qwebirc/engines/__init__.py b/qwebirc/engines/__init__.py
new file mode 100644 (file)
index 0000000..7259497
--- /dev/null
@@ -0,0 +1,5 @@
+from ajaxengine import AJAXEngine
+from adminengine import AdminEngine
+from staticengine import StaticEngine
+from feedbackengine import FeedbackEngine
+from authgateengine import AuthgateEngine
diff --git a/qwebirc/engines/adminengine.py b/qwebirc/engines/adminengine.py
new file mode 100644 (file)
index 0000000..b0972d9
--- /dev/null
@@ -0,0 +1,123 @@
+from twisted.web import resource, server, static
+from cgi import escape
+from urllib import urlencode
+import config, copy, time
+
+HEADER = """
+<html><head><link rel="stylesheet" href="/css/qui.css"></link><link rel="stylesheet" href="/css/dialogs.css"></link></head><body class="qwebirc-qui">
+<div class="qwebirc-aboutpane lines" style="bottom: 0px; top: 0px; position: absolute; right: 0px; left: 0px;">
+<div class="header"> 
+  <table> 
+    <tr> 
+      <td><img src="/images/qwebircsmall.png" alt="qwebirc" title="qwebirc"/></td> 
+      <td>&nbsp;&nbsp;&nbsp;&nbsp;</td> 
+      <td align="center"><div class="title">qwebirc</div><div class="version">AdminEngine</div></td> 
+    </tr> 
+  </table> 
+</div> 
+<div class="mainbody" style="bottom: 100px">
+"""
+
+FOOTER = """</div></body></html>"""
+
+class AdminEngineException(Exception):
+  pass
+  
+class AdminEngineAction:
+  def __init__(self, link_text, function, uniqid=None):
+    self._link_text = link_text
+    self.function = function
+    self.uniqid = uniqid
+
+  def get_link(self, **kwargs):
+    kwargs = copy.deepcopy(kwargs)
+    if self.uniqid is not None:
+      kwargs["uniqid"] = self.uniqid
+    return "<a href=\"?%s\">%s</a>" % (urlencode(kwargs), escape(self._link_text))
+  
+class AdminEngine(resource.Resource):
+  def __init__(self, path, services):
+    self.__services = services
+    self.__path = path
+    self.__creation_time = time.time()
+
+  @property
+  def adminEngine(self):
+    return {
+      "Permitted hosts": (config.ADMIN_ENGINE_HOSTS,),
+      "Started": ((time.asctime(time.localtime(self.__creation_time)),),),
+      "Running for": (("%d seconds" % int(time.time() - self.__creation_time),),),
+      "CPU time used (UNIX only)": (("%.2f seconds" % time.clock(),),)
+    }
+      
+  def process_action(self, args):
+    try:  
+      engine = args["engine"][0]
+      heading = args["heading"][0]
+      pos = int(args["pos"][0])
+      pos2 = int(args["pos2"][0])
+      
+      uniqid = args.get("uniqid", [None])[0]
+      
+      obj = self.__services[engine].adminEngine[heading][pos]
+    except KeyError:
+      raise AdminEngineException("Bad action description.")
+      
+    if uniqid is None:
+      obj[pos2].function()
+    else:
+      for x in obj:
+        if not isinstance(x, AdminEngineAction):
+          continue
+        if x.uniqid == uniqid:
+          x.function(uniqid)
+          break
+      else:
+        raise AdminEngineException("Action does not exist.")
+    
+  def render_GET(self, request):
+    _, ip, port = request.transport.getPeer()
+    if ip not in config.ADMIN_ENGINE_HOSTS:
+      raise AdminEngineException("Access denied")
+  
+    args = request.args.get("engine")
+    if args:
+      self.process_action(request.args)
+      request.redirect("?")
+      request.finish()
+      return server.NOT_DONE_YET
+      
+    data = [HEADER]
+    
+    def add_list(lines):
+      data.append("<ul>")
+      data.extend(["<li>" + escape(x) + "</li>" for x in lines])
+      data.append("</ul>")
+      
+    def add_text(text, block="p"):
+      data.append("<%s>%s</%s>" % (block, escape(text), block))
+      
+    def brescape(text):
+      return escape(text).replace("\n", "<br/>")
+      
+    for engine, obj in self.__services.items():
+      if not hasattr(obj, "adminEngine"):
+        continue
+      add_text(engine, "h2")
+      
+      for heading, obj2 in obj.adminEngine.items():
+        add_text(heading, "h3")
+
+        for pos, obj3 in enumerate(obj2):
+          elements = []
+          for pos2, obj4 in enumerate(obj3):
+            if isinstance(obj4, AdminEngineAction):
+              elements.append(obj4.get_link(engine=engine, heading=heading, pos=pos, pos2=pos2))
+            else:
+              elements.append(brescape(str(obj4)))
+
+          data+=["<p>", " ".join(elements), "</p>"]
+
+    data.append(FOOTER)
+    
+    return "".join(data)
similarity index 88%
rename from qwebirc/ajaxengine.py
rename to qwebirc/engines/ajaxengine.py
index d74bd39fb45bc61ef7cf8186d4d6a99beffd0e06..7390f2ebe017292d4cb08cd566874ee6c1a4f690 100644 (file)
@@ -2,7 +2,10 @@ from twisted.web import resource, server, static
 from twisted.names import client
 from twisted.internet import reactor
 from authgateengine import login_optional, getSessionData
-import simplejson, md5, sys, os, ircclient, time, config, weakref, traceback
+import simplejson, md5, sys, os, time, config, weakref, traceback
+import qwebirc.ircclient as ircclient
+from adminengine import AdminEngineAction
+from qwebirc.util import HitCounter
 
 Sessions = {}
 
@@ -134,7 +137,9 @@ class AJAXEngine(resource.Resource):
   
   def __init__(self, prefix):
     self.prefix = prefix
-
+    self.__connect_hit = HitCounter()
+    self.__total_hit = HitCounter()
+    
   @jsondump
   def render_POST(self, request):
     path = request.path[len(self.prefix):]
@@ -174,6 +179,7 @@ class AJAXEngine(resource.Resource):
     else:
       perform = ["PRIVMSG %s :TICKETAUTH %s" % (config.QBOT, qticket)]
 
+    self.__connect_hit()
     client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=realname, perform=perform)
     session.client = client
     
@@ -199,7 +205,8 @@ class AJAXEngine(resource.Resource):
     command = request.args.get("c")
     if command is None:
       raise AJAXException("No command specified")
-
+    self.__total_hit()
+    
     command = command[0]
     
     session = self.getSession(request)
@@ -225,5 +232,19 @@ class AJAXEngine(resource.Resource):
   
     return True
   
+  def closeById(self, k):
+    s = Sessions.get(k)
+    if s is None:
+      return
+    s.client.client.error("Closed by admin interface")
+    
+  @property
+  def adminEngine(self):
+    return {
+      "Sessions": [(str(v.client.client), AdminEngineAction("close", self.closeById, k)) for k, v in Sessions.iteritems() if not v.closed],
+      "Connections": [(self.__connect_hit,)],
+      "Total hits": [(self.__total_hit,)],
+    }
+    
   COMMANDS = dict(p=push, n=newConnection, s=subscribe)
   
\ No newline at end of file
similarity index 80%
rename from qwebirc/authgateengine.py
rename to qwebirc/engines/authgateengine.py
index 8765bc8b3ddd6069c5492ab7c513f93d37815a88..46bbd0ca0a7b43fa813fad4f83c4d762aed65234 100644 (file)
@@ -1,6 +1,8 @@
-from authgate import twisted as authgate
+from qwebirc.authgate import twisted as authgate
 from twisted.web import resource, server, static
-import config, urlparse, urllib, rijndael, ciphers, hashlib, re
+import config, urlparse, urllib, hashlib, re
+import qwebirc.util.rijndael, qwebirc.util.ciphers
+import qwebirc.util
 
 BLOCK_SIZE = 128/8
 
@@ -9,6 +11,7 @@ class AuthgateEngine(resource.Resource):
   
   def __init__(self, prefix):
     self.__prefix = prefix
+    self.__hit = qwebirc.util.HitCounter()
     
   def deleteCookie(self, request, key):
     request.addCookie(key, "", path="/", expires="Sat, 29 Jun 1996 01:44:48 GMT")
@@ -31,6 +34,7 @@ class AuthgateEngine(resource.Resource):
       if not qt is None:
         getSessionData(request)["qticket"] = decodeQTicket(qt)
       
+      self.__hit()
       location = request.getCookie("redirect")
       if location is None:
         location = "/"
@@ -43,15 +47,19 @@ class AuthgateEngine(resource.Resource):
       request.finish()
       
     return server.NOT_DONE_YET
+  
+  @property  
+  def adminEngine(self):
+    return dict(Logins=((self.__hit,),))
     
-def decodeQTicket(qticket, p=re.compile("\x00*$"), cipher=rijndael.rijndael(hashlib.sha256(config.QTICKETKEY).digest()[:16])):
+def decodeQTicket(qticket, p=re.compile("\x00*$"), cipher=qwebirc.util.rijndael.rijndael(hashlib.sha256(config.QTICKETKEY).digest()[:16])):
   def decrypt(data):
     l = len(data)
     if l < BLOCK_SIZE * 2 or l % BLOCK_SIZE != 0:
       raise Exception("Bad qticket.")
     
     iv, data = data[:16], data[16:]
-    cbc = ciphers.CBC(cipher, iv)
+    cbc = qwebirc.util.ciphers.CBC(cipher, iv)
   
     # technically this is a flawed padding algorithm as it allows chopping at BLOCK_SIZE, we don't
     # care about that though!
@@ -59,10 +67,8 @@ def decodeQTicket(qticket, p=re.compile("\x00*$"), cipher=rijndael.rijndael(hash
     for i, v in enumerate(b):
       q = cbc.decrypt(data[v:v+BLOCK_SIZE])
       if i == len(b) - 1:
-        print repr(q), re.sub(p, "", q)
         yield re.sub(p, "", q)
       else:
-        print repr(q)
         yield q
   return "".join(decrypt(qticket))
   
similarity index 90%
rename from qwebirc/feedbackengine.py
rename to qwebirc/engines/feedbackengine.py
index df35d8850ffcd5f1f13c657e834499cb7494c7b3..48a1b20d920f5932e8dd0a221a08030fa983670f 100644 (file)
@@ -3,6 +3,7 @@ from twisted.mail.smtp import SMTPSenderFactory, ESMTPSenderFactory
 from twisted.internet import defer, reactor
 from StringIO import StringIO
 from email.mime.text import MIMEText
+import qwebirc.util as util
 import config
 
 class FeedbackException(Exception):
@@ -13,7 +14,12 @@ class FeedbackEngine(resource.Resource):
   
   def __init__(self, prefix):
     self.prefix = prefix
-
+    self.__hit = util.HitCounter()
+    
+  @property
+  def adminEngine(self):
+    return dict(Sent=[(self.__hit,)])
+    
   def render_POST(self, request):
     text = request.args.get("feedback")
     if text is None:
@@ -43,4 +49,5 @@ class FeedbackEngine(resource.Resource):
     factorytype = SMTPSenderFactory
     factory = factorytype(fromEmail=config.FEEDBACK_FROM, toEmail=config.FEEDBACK_TO, file=email, deferred=defer.Deferred())
     reactor.connectTCP(config.FEEDBACK_SMTP_HOST, config.FEEDBACK_SMTP_PORT, factory)
+    self.__hit()
     return "1"
\ No newline at end of file
diff --git a/qwebirc/engines/staticengine.py b/qwebirc/engines/staticengine.py
new file mode 100644 (file)
index 0000000..6349690
--- /dev/null
@@ -0,0 +1,44 @@
+from twisted.web import resource, server, static
+from qwebirc.util.gziprequest import GZipRequest
+import qwebirc.util as util
+import pprint
+from adminengine import AdminEngineAction
+
+# TODO, cache gzip stuff
+cache = {}
+def clear_cache():
+  global cache
+  cache = {}
+
+def apply_gzip(request):
+  accept_encoding = request.getHeader('accept-encoding')
+  if accept_encoding:
+    encodings = accept_encoding.split(',')
+    for encoding in encodings:
+      name = encoding.split(';')[0].strip()
+      if name == 'gzip':
+        request = GZipRequest(request)
+  return request
+
+class StaticEngine(static.File):
+  isLeaf = False
+  hit = util.HitCounter()
+  
+  def __init__(self, *args, **kwargs):
+    static.File.__init__(self, *args, **kwargs)
+    
+  def render(self, request):
+    self.hit(request)
+    request = apply_gzip(request)
+    return static.File.render(self, request)
+    
+  @property
+  def adminEngine(self):
+    return {
+      #"GZip cache": [
+        #("Contents: %s" % pprint.pformat(list(cache.keys())),)# AdminEngineAction("clear", d))
+      #],
+      "Hits": [
+        (self.hit,),
+      ]
+    }
index e7475d8334726fe1bb2dc5cdc4b00cf0e6f336dc..75736252ad7d099d5600ce2fbdd525e502fa9650 100644 (file)
@@ -15,7 +15,9 @@ def hmacfn(*args):
 
 class QWebIRCClient(basic.LineReceiver):
   delimiter = "\n"
-
+  def __init__(self, *args, **kwargs):
+    self.__nickname = "(unregistered)"
+    
   def dataReceived(self, data):
     basic.LineReceiver.dataReceived(self, data.replace("\r", ""))
 
@@ -32,10 +34,18 @@ class QWebIRCClient(basic.LineReceiver):
     except irc.IRCBadMessage:
       self.badMessage(line, *sys.exc_info())
       
-    if command == "001" and self.__perform:
-      for x in self.__perform:
-        self.write(x)
+    if command == "001":
+      self.__nickname = params[0]
       
+      if self.__perform is not None:
+        for x in self.__perform:
+          self.write(x)
+        self.__perform = None
+    elif command == "NICK":
+      nick = prefix.split("!", 1)[0]
+      if nick == self.__nickname:
+        self.__nickname = params[0]
+        
   def badMessage(self, args):
     self("badmessage", args)
   
@@ -54,6 +64,7 @@ class QWebIRCClient(basic.LineReceiver):
     self.lastError = None
     f = self.factory.ircinit
     nick, ident, ip, realname = f["nick"], f["ident"], f["ip"], f["realname"]
+    self.__nickname = nick
     self.__perform = f.get("perform")
     
     hmac = hmacfn(ident, ip)
@@ -63,6 +74,9 @@ class QWebIRCClient(basic.LineReceiver):
     self.factory.client = self
     self("connect")
 
+  def __str__(self):
+    return "<QWebIRCClient: %s!%s@%s>" % (self.__nickname, self.factory.ircinit["ident"], self.factory.ircinit["ip"])
+    
   def connectionLost(self, reason):
     if self.lastError:
       self.disconnect("Connection to IRC server lost: %s" % self.lastError)
index 20fd66f45769acf90ecdb38effccbf2a30da6e28..0d552c45228d556fabe2dd82246809df0e1bbaab 100644 (file)
@@ -1,8 +1,6 @@
-from ajaxengine import AJAXEngine
-from authgateengine import AuthgateEngine
-from feedbackengine import FeedbackEngine
-import mimetypes
+import engines
 from twisted.web import resource, server, static
+import mimetypes
 
 class RootResource(resource.Resource):
   def getChild(self, name, request):
@@ -15,10 +13,17 @@ class RootSite(server.Site):
     root = RootResource()
     server.Site.__init__(self, root, *args, **kwargs)
 
-    root.primaryChild = static.File(path)
-    root.putChild("e", AJAXEngine("/e"))
-    root.putChild("feedback", FeedbackEngine("/feedback"))
-    root.putChild("auth", AuthgateEngine("/auth"))
+    services = {}
+    services["StaticEngine"] = root.primaryChild = engines.StaticEngine(path)
 
+    def register(service, path, *args, **kwargs):
+      sobj = service("/" + path, *args, **kwargs)
+      services[service.__name__] = sobj
+      root.putChild(path, sobj)
+      
+    register(engines.AJAXEngine, "e")
+    register(engines.FeedbackEngine, "feedback")
+    register(engines.AuthgateEngine, "auth")
+    register(engines.AdminEngine, "adminengine", services)
+    
 mimetypes.types_map[".ico"] = "image/vnd.microsoft.icon"
-
diff --git a/qwebirc/util/__init__.py b/qwebirc/util/__init__.py
new file mode 100644 (file)
index 0000000..d16e8dc
--- /dev/null
@@ -0,0 +1 @@
+from hitcounter import HitCounter
similarity index 100%
rename from qwebirc/ciphers.py
rename to qwebirc/util/ciphers.py
diff --git a/qwebirc/util/gziprequest.py b/qwebirc/util/gziprequest.py
new file mode 100644 (file)
index 0000000..8f6fba2
--- /dev/null
@@ -0,0 +1,51 @@
+import struct, zlib
+
+class GZipRequest(object):
+  """Wrapper for a request that applies a gzip content encoding"""
+
+  def __init__(self, request, compressLevel=6):
+    self.request = request
+    self.request.setHeader('Content-Encoding', 'gzip')
+    # Borrowed from twisted.web2 gzip filter
+    self.compress = zlib.compressobj(compressLevel, zlib.DEFLATED, -zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL,0)
+
+  def __getattr__(self, attr):
+    if 'request' in self.__dict__:
+      return getattr(self.request, attr)
+      
+    raise AttributeError, attr
+
+  def __setattr__(self, attr, value):
+    if 'request' in self.__dict__:
+      return setattr(self.request, attr, value)
+      
+    self.__dict__[attr] = value
+
+  def write(self, data):
+    if not self.request.startedWriting:
+      self.crc = zlib.crc32('')
+      self.size = self.csize = 0
+      # XXX: Zap any length for now since we don't know final size
+      if 'content-length' in self.request.headers:
+        del self.request.headers['content-length']
+        # Borrow header information from twisted.web2 gzip filter
+      self.request.write('\037\213\010\000' '\0\0\0\0' '\002\377')
+
+    self.crc = zlib.crc32(data, self.crc)
+    self.size += len(data)
+    cdata = self.compress.compress(data)
+    self.csize += len(cdata)
+    if cdata:
+      self.request.write(cdata)
+    elif self.request.producer:
+      # Simulate another pull even though it hasn't really made it out to the consumer yet.
+      self.request.producer.resumeProducing()
+
+  def finish(self):
+    remain = self.compress.flush()
+    self.csize += len(remain)
+    if remain:
+      self.request.write(remain)
+      
+    self.request.write(struct.pack('<LL', self.crc & 0xFFFFFFFFL, self.size & 0xFFFFFFFFL))
+    self.request.finish()
diff --git a/qwebirc/util/hitcounter.py b/qwebirc/util/hitcounter.py
new file mode 100644 (file)
index 0000000..7bdc45d
--- /dev/null
@@ -0,0 +1,15 @@
+import time
+
+class HitCounter:
+  def __init__(self):
+    self.__hits = 0
+    self.__start_time = time.time()
+    
+  def __call__(self, *args):
+    self.__hits+=1
+
+  def __str__(self):
+    delta = time.time() - self.__start_time
+    
+    return "Total: %d hits/s: %.2f" % (self.__hits, self.__hits / delta)
+    
\ No newline at end of file
similarity index 100%
rename from qwebirc/rijndael.py
rename to qwebirc/util/rijndael.py