Also add GZip support for static files.
FEEDBACK_TO = "moo@moo.com"
FEEDBACK_SMTP_HOST = "127.0.0.1"
FEEDBACK_SMTP_PORT = 25
+ADMIN_ENGINE_HOSTS = ["127.0.0.1"]
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">
--- /dev/null
+from ajaxengine import AJAXEngine
+from adminengine import AdminEngine
+from staticengine import StaticEngine
+from feedbackengine import FeedbackEngine
+from authgateengine import AuthgateEngine
--- /dev/null
+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> </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)
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 = {}
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):]
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
command = request.args.get("c")
if command is None:
raise AJAXException("No command specified")
-
+ self.__total_hit()
+
command = command[0]
session = self.getSession(request)
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
-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
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")
if not qt is None:
getSessionData(request)["qticket"] = decodeQTicket(qt)
+ self.__hit()
location = request.getCookie("redirect")
if location is None:
location = "/"
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!
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))
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):
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:
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
--- /dev/null
+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,),
+ ]
+ }
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", ""))
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)
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)
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)
-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):
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"
-
--- /dev/null
+from hitcounter import HitCounter
--- /dev/null
+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()
--- /dev/null
+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