root/trunk/twisted/web/proxy.py

Revision 28824, 9.3 KB (checked in by exarkun, 5 months ago)

Apply patch to propagate reactor from a ReverseProxyResource to its children

Author: aschmolck
Reviewer: exarkun
Fixes: #4403

When creating a new ReverseProxyResource in ReverseProxyResource.getChild, pass
on the reactor the parent was created with to the new child.

Line 
1# -*- test-case-name: twisted.web.test.test_proxy -*-
2# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Simplistic HTTP proxy support.
7
8This comes in two main variants - the Proxy and the ReverseProxy.
9
10When a Proxy is in use, a browser trying to connect to a server (say,
11www.yahoo.com) will be intercepted by the Proxy, and the proxy will covertly
12connect to the server, and return the result.
13
14When a ReverseProxy is in use, the client connects directly to the ReverseProxy
15(say, www.yahoo.com) which farms off the request to one of a pool of servers,
16and returns the result.
17
18Normally, a Proxy is used on the client end of an Internet connection, while a
19ReverseProxy is used on the server end.
20"""
21
22import urlparse
23from urllib import quote as urlquote
24
25from twisted.internet import reactor
26from twisted.internet.protocol import ClientFactory
27from twisted.web.resource import Resource
28from twisted.web.server import NOT_DONE_YET
29from twisted.web.http import HTTPClient, Request, HTTPChannel
30
31
32
33class ProxyClient(HTTPClient):
34    """
35    Used by ProxyClientFactory to implement a simple web proxy.
36
37    @ivar _finished: A flag which indicates whether or not the original request
38        has been finished yet.
39    """
40    _finished = False
41
42    def __init__(self, command, rest, version, headers, data, father):
43        self.father = father
44        self.command = command
45        self.rest = rest
46        if "proxy-connection" in headers:
47            del headers["proxy-connection"]
48        headers["connection"] = "close"
49        headers.pop('keep-alive', None)
50        self.headers = headers
51        self.data = data
52
53
54    def connectionMade(self):
55        self.sendCommand(self.command, self.rest)
56        for header, value in self.headers.items():
57            self.sendHeader(header, value)
58        self.endHeaders()
59        self.transport.write(self.data)
60
61
62    def handleStatus(self, version, code, message):
63        self.father.setResponseCode(int(code), message)
64
65
66    def handleHeader(self, key, value):
67        # t.web.server.Request sets default values for these headers in its
68        # 'process' method. When these headers are received from the remote
69        # server, they ought to override the defaults, rather than append to
70        # them.
71        if key.lower() in ['server', 'date', 'content-type']:
72            self.father.responseHeaders.setRawHeaders(key, [value])
73        else:
74            self.father.responseHeaders.addRawHeader(key, value)
75
76
77    def handleResponsePart(self, buffer):
78        self.father.write(buffer)
79
80
81    def handleResponseEnd(self):
82        """
83        Finish the original request, indicating that the response has been
84        completely written to it, and disconnect the outgoing transport.
85        """
86        if not self._finished:
87            self._finished = True
88            self.father.finish()
89            self.transport.loseConnection()
90
91
92
93class ProxyClientFactory(ClientFactory):
94    """
95    Used by ProxyRequest to implement a simple web proxy.
96    """
97
98    protocol = ProxyClient
99
100
101    def __init__(self, command, rest, version, headers, data, father):
102        self.father = father
103        self.command = command
104        self.rest = rest
105        self.headers = headers
106        self.data = data
107        self.version = version
108
109
110    def buildProtocol(self, addr):
111        return self.protocol(self.command, self.rest, self.version,
112                             self.headers, self.data, self.father)
113
114
115    def clientConnectionFailed(self, connector, reason):
116        """
117        Report a connection failure in a response to the incoming request as
118        an error.
119        """
120        self.father.setResponseCode(501, "Gateway error")
121        self.father.responseHeaders.addRawHeader("Content-Type", "text/html")
122        self.father.write("<H1>Could not connect</H1>")
123        self.father.finish()
124
125
126
127class ProxyRequest(Request):
128    """
129    Used by Proxy to implement a simple web proxy.
130
131    @ivar reactor: the reactor used to create connections.
132    @type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
133    """
134
135    protocols = {'http': ProxyClientFactory}
136    ports = {'http': 80}
137
138    def __init__(self, channel, queued, reactor=reactor):
139        Request.__init__(self, channel, queued)
140        self.reactor = reactor
141
142
143    def process(self):
144        parsed = urlparse.urlparse(self.uri)
145        protocol = parsed[0]
146        host = parsed[1]
147        port = self.ports[protocol]
148        if ':' in host:
149            host, port = host.split(':')
150            port = int(port)
151        rest = urlparse.urlunparse(('', '') + parsed[2:])
152        if not rest:
153            rest = rest + '/'
154        class_ = self.protocols[protocol]
155        headers = self.getAllHeaders().copy()
156        if 'host' not in headers:
157            headers['host'] = host
158        self.content.seek(0, 0)
159        s = self.content.read()
160        clientFactory = class_(self.method, rest, self.clientproto, headers,
161                               s, self)
162        self.reactor.connectTCP(host, port, clientFactory)
163
164
165
166class Proxy(HTTPChannel):
167    """
168    This class implements a simple web proxy.
169
170    Since it inherits from L{twisted.protocols.http.HTTPChannel}, to use it you
171    should do something like this::
172
173        from twisted.web import http
174        f = http.HTTPFactory()
175        f.protocol = Proxy
176
177    Make the HTTPFactory a listener on a port as per usual, and you have
178    a fully-functioning web proxy!
179    """
180
181    requestFactory = ProxyRequest
182
183
184
185class ReverseProxyRequest(Request):
186    """
187    Used by ReverseProxy to implement a simple reverse proxy.
188
189    @ivar proxyClientFactoryClass: a proxy client factory class, used to create
190        new connections.
191    @type proxyClientFactoryClass: L{ClientFactory}
192
193    @ivar reactor: the reactor used to create connections.
194    @type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
195    """
196
197    proxyClientFactoryClass = ProxyClientFactory
198
199    def __init__(self, channel, queued, reactor=reactor):
200        Request.__init__(self, channel, queued)
201        self.reactor = reactor
202
203
204    def process(self):
205        """
206        Handle this request by connecting to the proxied server and forwarding
207        it there, then forwarding the response back as the response to this
208        request.
209        """
210        self.received_headers['host'] = self.factory.host
211        clientFactory = self.proxyClientFactoryClass(
212            self.method, self.uri, self.clientproto, self.getAllHeaders(),
213            self.content.read(), self)
214        self.reactor.connectTCP(self.factory.host, self.factory.port,
215                                clientFactory)
216
217
218
219class ReverseProxy(HTTPChannel):
220    """
221    Implements a simple reverse proxy.
222
223    For details of usage, see the file examples/proxy.py.
224    """
225
226    requestFactory = ReverseProxyRequest
227
228
229
230class ReverseProxyResource(Resource):
231    """
232    Resource that renders the results gotten from another server
233
234    Put this resource in the tree to cause everything below it to be relayed
235    to a different server.
236
237    @ivar proxyClientFactoryClass: a proxy client factory class, used to create
238        new connections.
239    @type proxyClientFactoryClass: L{ClientFactory}
240
241    @ivar reactor: the reactor used to create connections.
242    @type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
243    """
244
245    proxyClientFactoryClass = ProxyClientFactory
246
247
248    def __init__(self, host, port, path, reactor=reactor):
249        """
250        @param host: the host of the web server to proxy.
251        @type host: C{str}
252
253        @param port: the port of the web server to proxy.
254        @type port: C{port}
255
256        @param path: the base path to fetch data from. Note that you shouldn't
257            put any trailing slashes in it, it will be added automatically in
258            request. For example, if you put B{/foo}, a request on B{/bar} will
259            be proxied to B{/foo/bar}.  Any required encoding of special
260            characters (such as " " or "/") should have been done already.
261
262        @type path: C{str}
263        """
264        Resource.__init__(self)
265        self.host = host
266        self.port = port
267        self.path = path
268        self.reactor = reactor
269
270
271    def getChild(self, path, request):
272        """
273        Create and return a proxy resource with the same proxy configuration
274        as this one, except that its path also contains the segment given by
275        C{path} at the end.
276        """
277        return ReverseProxyResource(
278            self.host, self.port, self.path + '/' + urlquote(path, safe=""),
279            self.reactor)
280
281
282    def render(self, request):
283        """
284        Render a request by forwarding it to the proxied server.
285        """
286        # RFC 2616 tells us that we can omit the port if it's the default port,
287        # but we have to provide it otherwise
288        if self.port == 80:
289            host = self.host
290        else:
291            host = "%s:%d" % (self.host, self.port)
292        request.received_headers['host'] = host
293        request.content.seek(0, 0)
294        qs = urlparse.urlparse(request.uri)[4]
295        if qs:
296            rest = self.path + '?' + qs
297        else:
298            rest = self.path
299        clientFactory = self.proxyClientFactoryClass(
300            request.method, rest, request.clientproto,
301            request.getAllHeaders(), request.content.read(), request)
302        self.reactor.connectTCP(self.host, self.port, clientFactory)
303        return NOT_DONE_YET
Note: See TracBrowser for help on using the browser.