Analisi di Twisted Proxy

Flusso dei dati

La classe Proxy non fa altro che ereditare da HTTPChannel ed impostare requestFactory=ProxyRequest:

class Proxy(HTTPChannel):
    requestFactory = ProxyRequest

La classe HTTPChannel è un ricevitore di richieste: in essa viene creata la coda delle richieste, e il callback lineReceived(self, line) viene chiamato ogni volta che viene ricevuta una linea, creando una request se la linea è la “prima ricevuta”, oppure completando una request se è una linea successiva.
Quando tutta la request è stata ricevuta dal client, viene eseguito il metodo allContentReceived(): questo a sua volta chiama il metodo request.requestReceived(command, path, version) della richiesta, il quale vedremo in seguito eseguirà la richiesta.
Quando la richiesta avrà finito, essa chiamerà il metodo requestDone di HTTPChannel: se la connessione del client non è persistente verrà chiusa.

class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin):
    ....
    requestFactory = Request
    ....
    def __init__(self):
        # the request queue
        self.requests = []
        ....
    def lineReceived(self, line):
        if self.__first_line:
        .... # fa qualche pulizia di dati precedenti
            request = self.requestFactory(self, len(self.requests))
            self.requests.append(request)

            self.__first_line = 0
            ....
        elif line == b'':
            ....
            self.allHeadersReceived()
            if self.length == 0:
                self.allContentReceived()
            ....
        elif line[0] in b' \t':
            ....
        else:
            ....
            self.__header = line
    ....
    def allContentReceived(self):
        command = self._command
        path = self._path
        version = self._version
        ....
        req = self.requests[-1] # prende la richiesta e
        req.requestReceived(command, path, version) # chiama il suo metodo
    ....
    def requestDone(self, request):
        """
        Called by first request in queue when it is done.
        """
        if request != self.requests[0]: raise TypeError
        del self.requests[0]
        if self.persistent:
            ....
        else:
            self.transport.loseConnection()

A questo punto bisogna ricordare che Proxy (erede di HTTPChannel) ha la proprietà requestFactory impostata a ProxyRequest.
La classe ProxyRequest eredita da Request e ne implementa il metodo astratto process(), oltre a dichiarare il protocollo da adoperare per eseguire la richiesta:

class ProxyRequest(Request):
    protocols = {'http': ProxyClientFactory}
    ports = {'http': 80}
    ....
    def process(self):
        .... # varie inizializzazioni
        port = self.ports[protocol]
        class_ = self.protocols[protocol]
        s = self.content.read()
        clientFactory = class_(self.method, rest, self.clientproto, headers,
                               s, self)
        self.reactor.connectTCP(host, port, clientFactory)

Nella classe genitrice Request ci sono alcuni metodi adoperati da HTTPChannel:

class Request:
    ....
    def requestReceived(self, command, path, version):
        # inizializzazioni varie
        self.process()
    ....
    def process(self):
        pass
    ....
    def notifyFinish(self):
        self.notifications.append(Deferred())
        return self.notifications[-1]
    def finish(self):
        if self._disconnected:
            raise RuntimeError(
                "Request.finish called on a request after its connection was lost; "
                "use Request.notifyFinish to keep track of this.")
        ....
        self.finished = 1
    def write(self, data):
        if self.finished:
            raise RuntimeError('Request.write called on a request after '
                               'Request.finish was called.')
        if not self.startedWriting: # intanto invia la parte HEAD
            self.startedWriting = 1
            version = self.clientproto
            l = []
            l.append(
                version + b" " +
                intToBytes(self.code) + b" " +
                networkString(self.code_message) + b"\r\n")
            ....
            l.append(b"\r\n")
            self.transport.writeSequence(l)
            ....
           # if this is a "HEAD" request, we shouldn't return any data
            if self.method == b"HEAD":
                self.write = lambda data: None
                return

        self.sentLength = self.sentLength + len(data)
        if data:
            if self.chunked:
                self.transport.writeSequence(toChunk(data))
            else:
                self.transport.write(data)
    ....
    def connectionLost(self, reason):
        """
        There is no longer a connection for this request to respond over.
        Clean up anything which can't be useful anymore.
        """
        self._disconnected = True
        self.channel = None
        if self.content is not None:
            self.content.close()
        for d in self.notifications:
            d.errback(reason)
        self.notifications = []

A questo punto la situazione è la seguente:

  • Proxy(HTTPChannel) riceve la connessione con lineReceived e crea una ProxyRequest(Request), inserendola nella sua coda
  • Quando la request è completa, sempre in lineReceived viene chiamato allContentReceived
  • allContentReceived chiama ProxyRequest.requestReceived
  • requestReceived chiama ProxyRequest.process()
  • ProxyRequest.process() crea un clientFactory e lo connette alla destinazione con self.reactor.connectTCP(host, port, clientFactory)

Si è visto che ProxyRequest.process() istanzia una ProxyClientFactory e la connette al server di destinazione della richiesta, per ricevere la risposta.
La factory esegue la connessione e imposta il transport, poi crea il protocollo ProxyClient che esegue la comunicazione vera e propria con il server.
Quando ProxyClient viene collegato al server, viene chiamato il suo callback connectionMade che invia headers e dati attraverso il transport.
Quando il server risponde, ci sono diversi handlers che ricevono i dati.
In particolare, handleResponseEnd() esegue la “pulizia” finale chiamando ProxyRequest.finish() e chiudendo la connessione con il server.
Attenzione: handleResponseEnd viene chiamato da HTTPClient.connectionLost().

class ProxyClient(HTTPClient):
    _finished = False
    ....
    def connectionMade(self):
        self.sendCommand(self.command, self.rest)
        for header, value in self.headers.items():
            self.sendHeader(header, value)
        self.endHeaders()
        self.transport.write(self.data)
    def handleHeader(self, key, value):
    .... # si può riusare per processare ogni header ricevuto
    def handleResponsePart(self, buffer):
        self.father.write(buffer) # usa il metodo write di Request
    def handleResponseEnd(self):
        if not self._finished:
            self._finished = True
            self.father.finish() # chiama il metodo finish di Request
            self.transport.loseConnection() #chiude la connessione

Gestione delle connessioni

Dal lato browser troviamo Proxy e ProxyRequest.
Proxy eredita da Protocol: il metodo connectionLost() è chiamato quando il browser si disconnette dal proxy, e notifica l’ evento a tutte le ProxyRequests ancora in coda

def connectionLost(self, reason):
    self.setTimeout(None)
    for request in self.requests:
        request.connectionLost(reason)

La ProxyRequest.connectionLost fa le pulizie: imposta _disconnected=True, cancella il puntatore al canale di comunicazione con il client, lancia gli errbacks delle notifiche.

def connectionLost(self, reason):
    self._disconnected = True
    self.channel = None
    if self.content is not None:
        self.content.close()
    for d in self.notifications:
        d.errback(reason)
    self.notifications = []

Dal lato opposto, quello che comunica con il server di destinazione, troviamo ProxyClient, che eredita da HTTPClient.
Se il server chiude la comunicazione, viene eseguito il metodo HTTPClient.connectionLost, che a sua volta chiama handleResponseEnd, implementato da ProxyClient:

def handleResponseEnd(self):
    if not self._finished:
        self._finished = True
        self.father.finish()
        self.transport.loseConnection()

Per controllare questo comportamento, basta imitare ciò che avviene nel codice sorgente:
basta cioè fare un test su self._finished

Due volte handleResponseEnd

ProxyClient riceve i dati e in alcuni casi (non so bene quali) esegue una prima volta handleResponseEnd().
Poi però c’è la disconnessione dal server: HTTPClient.connectionLost() chiama self.handleResponseEnd una seconda volta.

Lascia un commento

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...