| 1 | |
|---|
| 2 | |
|---|
| 3 | |
|---|
| 4 | |
|---|
| 5 | """ |
|---|
| 6 | Simplistic HTTP proxy support. |
|---|
| 7 | |
|---|
| 8 | This comes in two main variants - the Proxy and the ReverseProxy. |
|---|
| 9 | |
|---|
| 10 | When a Proxy is in use, a browser trying to connect to a server (say, |
|---|
| 11 | www.yahoo.com) will be intercepted by the Proxy, and the proxy will covertly |
|---|
| 12 | connect to the server, and return the result. |
|---|
| 13 | |
|---|
| 14 | When 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, |
|---|
| 16 | and returns the result. |
|---|
| 17 | |
|---|
| 18 | Normally, a Proxy is used on the client end of an Internet connection, while a |
|---|
| 19 | ReverseProxy is used on the server end. |
|---|
| 20 | """ |
|---|
| 21 | |
|---|
| 22 | import urlparse |
|---|
| 23 | from urllib import quote as urlquote |
|---|
| 24 | |
|---|
| 25 | from twisted.internet import reactor |
|---|
| 26 | from twisted.internet.protocol import ClientFactory |
|---|
| 27 | from twisted.web.resource import Resource |
|---|
| 28 | from twisted.web.server import NOT_DONE_YET |
|---|
| 29 | from twisted.web.http import HTTPClient, Request, HTTPChannel |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | class 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 | |
|---|
| 68 | |
|---|
| 69 | |
|---|
| 70 | |
|---|
| 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 | |
|---|
| 93 | class 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 | |
|---|
| 127 | class 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 | |
|---|
| 166 | class 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 | |
|---|
| 185 | class 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 | |
|---|
| 219 | class 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 | |
|---|
| 230 | class 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 | |
|---|
| 287 | |
|---|
| 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 |
|---|