twistedその4

HTMLのフィルタリング機能を実装する。
参考コードはこちら
http://blog.somethingaboutcode.com/?p=155

+-------+
|reactor|
+-------+---------+  +-----+  +-----------------+
+10080 HTTPFactory|->|Proxy|->|UpperProxyRequest|
+-----------------+  +-----+  +-------------------------------------------+
A                                   |process-> FilteringProxyClientFactory|
|                                   +------------------V------------------+----------+
+-------+                           |FilteringProxyClient                            |
|browser|                           |   Proxyから取ってきてHTMLにフィルタをかける    |
+-------+                           +------------------------------------------------+

htmlfilter

# coding: utf-8

import re

RE=re.compile('(<body[^>]*>)', re.IGNORECASE)

def filter(url, html):
    return RE.sub(r'\g<1><h1>Filtered!</h1>', html)

bodyタグの直後にFiltered!と追加する。
後でなんとかするためのプレースホルダ

from twisted.web import http, proxy
from twisted.internet import reactor
from twisted.python import log
import urlparse
import htmlfilter
from cStringIO import StringIO

###############################################################################
# upper proxy
###############################################################################
PROXY_HOST='localhost'
PROXY_PORT=8080

class UpperProxyRequest(proxy.ProxyRequest):

    def process(self):
        parsed = urlparse.urlparse(self.uri)
        protocol = parsed[0]
        host = parsed[1]
        print self.method

        port = self.ports[protocol]
        if ':' in host:
            host, port = host.split(':')
            port = int(port)
        rest = urlparse.urlunparse(('', '') + parsed[2:])
        if not rest:
            rest = rest + '/'
        class_ = self.protocols[protocol]
        headers = self.getAllHeaders().copy()
        if 'host' not in headers:
            headers['host'] = host
        self.content.seek(0, 0)
        s = self.content.read()

        clientFactory = class_(
                self.method, self.uri, self.clientproto, headers, s, self)
        assert(PROXY_HOST)
        assert(PROXY_PORT)
        self.reactor.connectTCP(PROXY_HOST, PROXY_PORT, clientFactory)


proxy.Proxy.requestFactory=UpperProxyRequest

###############################################################################
# filtering
###############################################################################
class FilteringProxyClient(proxy.ProxyClient):
    def __init__(self, *args, **kwargs):
        proxy.ProxyClient.__init__(self, *args, **kwargs)
        self.__buffer=None
 
    def handleHeader(self, key, value):
        if key.lower()=="content-type" and value.startswith("text/html"):
            self.__buffer=StringIO()
        else:
            proxy.ProxyClient.handleHeader(self, key, value)
 
    def handleEndHeaders(self):
        if self.__buffer:
            pass #Need to calculate and send Content-Length first
        else:
            proxy.ProxyClient.handleEndHeaders(self)
 
    def handleResponsePart(self, buffer):
        if self.__buffer:
            self.__buffer.write(buffer)
        else:
            proxy.ProxyClient.handleResponsePart(self, buffer)
 
    def handleResponseEnd(self):
        if self.__buffer:
            b=self.__buffer.getvalue()
            self.__buffer=None
            buffer=htmlfilter.filter(self.father.uri, b)
            self.father.responseHeaders.setRawHeaders(
                    "Content-Length", ["%d" % len(buffer)])
            proxy.ProxyClient.handleEndHeaders(self)
            proxy.ProxyClient.handleResponsePart(self, buffer)
        proxy.ProxyClient.handleResponseEnd(self)

class FilteringProxyClientFactory(proxy.ProxyClientFactory):
    protocol=FilteringProxyClient

proxy.Proxy.requestFactory.protocols={'http': FilteringProxyClientFactory}


if __name__=="__main__":
    import sys
    log.startLogging(sys.stdout)

    f=http.HTTPFactory()
    f.protocol=proxy.Proxy

    reactor.listenTCP(10080, f)
    reactor.run()

ProxyRequest.protocolsのもつProxyClientFactoryに細工して、Filtering機能を持つProxyClientを生成するように改造。FilteringProxyClientは、content-typeがtext/htmlの時だけ本体を確保しておいて全部取得してからHTMLフィルターをかけてcontent-lengthを再計算するようにしている。