| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
"""I am a virtual hosts implementation. |
|---|
| 6 |
""" |
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
import urlparse |
|---|
| 10 |
from zope.interface import implements |
|---|
| 11 |
import urllib |
|---|
| 12 |
import warnings |
|---|
| 13 |
|
|---|
| 14 |
from twisted.internet import address |
|---|
| 15 |
from twisted.python import log |
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
from twisted.web2 import resource |
|---|
| 19 |
from twisted.web2 import responsecode |
|---|
| 20 |
from twisted.web2 import iweb |
|---|
| 21 |
from twisted.web2 import http |
|---|
| 22 |
|
|---|
| 23 |
class NameVirtualHost(resource.Resource): |
|---|
| 24 |
"""Resource in charge of dispatching requests to other resources based on |
|---|
| 25 |
the value of the HTTP 'Host' header. |
|---|
| 26 |
|
|---|
| 27 |
@param supportNested: If True domain segments will be chopped off until the |
|---|
| 28 |
TLD is reached or a matching virtual host is found. (In which case the |
|---|
| 29 |
child resource can do its own more specific virtual host lookup.) |
|---|
| 30 |
""" |
|---|
| 31 |
|
|---|
| 32 |
supportNested = True |
|---|
| 33 |
|
|---|
| 34 |
def __init__(self, default=None): |
|---|
| 35 |
""" |
|---|
| 36 |
@param default: The default resource to be served when encountering an |
|---|
| 37 |
unknown hostname. |
|---|
| 38 |
@type default: L{twisted.web2.iweb.IResource} or C{None} |
|---|
| 39 |
""" |
|---|
| 40 |
resource.Resource.__init__(self) |
|---|
| 41 |
self.hosts = {} |
|---|
| 42 |
|
|---|
| 43 |
self.default = default |
|---|
| 44 |
|
|---|
| 45 |
def addHost(self, name, resrc): |
|---|
| 46 |
"""Add a host to this virtual host. - The Fun Stuff(TM) |
|---|
| 47 |
|
|---|
| 48 |
This associates a host named 'name' with a resource 'resrc':: |
|---|
| 49 |
|
|---|
| 50 |
nvh.addHost('nevow.com', nevowDirectory) |
|---|
| 51 |
nvh.addHost('divmod.org', divmodDirectory) |
|---|
| 52 |
nvh.addHost('twistedmatrix.com', twistedMatrixDirectory) |
|---|
| 53 |
|
|---|
| 54 |
I told you that was fun. |
|---|
| 55 |
|
|---|
| 56 |
@param name: The FQDN to be matched to the 'Host' header. |
|---|
| 57 |
@type name: C{str} |
|---|
| 58 |
|
|---|
| 59 |
@param resrc: The L{twisted.web2.iweb.IResource} to be served as the |
|---|
| 60 |
given hostname. |
|---|
| 61 |
@type resource: L{twisted.web2.iweb.IResource} |
|---|
| 62 |
""" |
|---|
| 63 |
self.hosts[name] = resrc |
|---|
| 64 |
|
|---|
| 65 |
def removeHost(self, name): |
|---|
| 66 |
"""Remove the given host. |
|---|
| 67 |
@param name: The FQDN to remove. |
|---|
| 68 |
@type name: C{str} |
|---|
| 69 |
""" |
|---|
| 70 |
del self.hosts[name] |
|---|
| 71 |
|
|---|
| 72 |
def locateChild(self, req, segments): |
|---|
| 73 |
"""It's a NameVirtualHost, do you know where your children are? |
|---|
| 74 |
|
|---|
| 75 |
This uses locateChild magic so you don't have to mutate the request. |
|---|
| 76 |
""" |
|---|
| 77 |
|
|---|
| 78 |
host = req.host.lower() |
|---|
| 79 |
|
|---|
| 80 |
if self.supportNested: |
|---|
| 81 |
while not self.hosts.has_key(host) and len(host.split('.')) > 1: |
|---|
| 82 |
host = '.'.join(host.split('.')[1:]) |
|---|
| 83 |
|
|---|
| 84 |
|
|---|
| 85 |
return self.hosts.get(host, self.default), segments |
|---|
| 86 |
|
|---|
| 87 |
|
|---|
| 88 |
class AutoVHostURIRewrite(object): |
|---|
| 89 |
""" |
|---|
| 90 |
I do request mangling to insure that children know what host they are being |
|---|
| 91 |
accessed from behind apache2. |
|---|
| 92 |
|
|---|
| 93 |
Usage: |
|---|
| 94 |
|
|---|
| 95 |
- Twisted:: |
|---|
| 96 |
|
|---|
| 97 |
root = MyResource() |
|---|
| 98 |
vur = vhost.AutoVHostURIRewrite(root) |
|---|
| 99 |
|
|---|
| 100 |
- Apache2:: |
|---|
| 101 |
|
|---|
| 102 |
<Location /whatever/> |
|---|
| 103 |
ProxyPass http://localhost:8538/ |
|---|
| 104 |
RequestHeader set X-App-Location /whatever/ |
|---|
| 105 |
</Location> |
|---|
| 106 |
|
|---|
| 107 |
If the trailing / is ommitted in the second argument to ProxyPass |
|---|
| 108 |
VHostURIRewrite will return a 404 response code. |
|---|
| 109 |
|
|---|
| 110 |
If proxying HTTPS, add this to the Apache config:: |
|---|
| 111 |
|
|---|
| 112 |
RequestHeader set X-App-Scheme https |
|---|
| 113 |
""" |
|---|
| 114 |
implements(iweb.IResource) |
|---|
| 115 |
|
|---|
| 116 |
def __init__(self, resource, sendsRealHost=False): |
|---|
| 117 |
""" |
|---|
| 118 |
@param resource: The resource to serve after mutating the request. |
|---|
| 119 |
@type resource: L{twisted.web2.iweb.IResource} |
|---|
| 120 |
|
|---|
| 121 |
@param sendsRealHost: If True then the proxy will be expected to send the |
|---|
| 122 |
HTTP 'Host' header that was sent by the requesting client. |
|---|
| 123 |
@type sendsRealHost: C{bool} |
|---|
| 124 |
""" |
|---|
| 125 |
|
|---|
| 126 |
self.resource=resource |
|---|
| 127 |
self.sendsRealHost = sendsRealHost |
|---|
| 128 |
|
|---|
| 129 |
def renderHTTP(self, req): |
|---|
| 130 |
return http.Response(responsecode.NOT_FOUND) |
|---|
| 131 |
|
|---|
| 132 |
def locateChild(self, req, segments): |
|---|
| 133 |
scheme = req.headers.getRawHeaders('x-app-scheme') |
|---|
| 134 |
|
|---|
| 135 |
if self.sendsRealHost: |
|---|
| 136 |
host = req.headers.getRawHeaders('host') |
|---|
| 137 |
else: |
|---|
| 138 |
host = req.headers.getRawHeaders('x-forwarded-host') |
|---|
| 139 |
|
|---|
| 140 |
app_location = req.headers.getRawHeaders('x-app-location') |
|---|
| 141 |
remote_ip = req.headers.getRawHeaders('x-forwarded-for') |
|---|
| 142 |
|
|---|
| 143 |
if not (host and remote_ip): |
|---|
| 144 |
if not host: |
|---|
| 145 |
warnings.warn( |
|---|
| 146 |
("No host was obtained either from Host or " |
|---|
| 147 |
"X-Forwarded-Host headers. If your proxy does not " |
|---|
| 148 |
"send either of these headers use VHostURIRewrite. " |
|---|
| 149 |
"If your proxy sends the real host as the Host header " |
|---|
| 150 |
"use " |
|---|
| 151 |
"AutoVHostURIRewrite(resrc, sendsRealHost=True)")) |
|---|
| 152 |
|
|---|
| 153 |
|
|---|
| 154 |
raise http.HTTPError(responsecode.BAD_REQUEST) |
|---|
| 155 |
host = host[0] |
|---|
| 156 |
remote_ip = remote_ip[0] |
|---|
| 157 |
if app_location: |
|---|
| 158 |
app_location = app_location[0] |
|---|
| 159 |
else: |
|---|
| 160 |
app_location = '/' |
|---|
| 161 |
if scheme: |
|---|
| 162 |
scheme = scheme[0] |
|---|
| 163 |
else: |
|---|
| 164 |
scheme='http' |
|---|
| 165 |
|
|---|
| 166 |
req.host, req.port = http.splitHostPort(scheme, host) |
|---|
| 167 |
req.scheme = scheme |
|---|
| 168 |
|
|---|
| 169 |
req.remoteAddr = address.IPv4Address('TCP', remote_ip, 0) |
|---|
| 170 |
|
|---|
| 171 |
req.prepath = app_location[1:].split('/')[:-1] |
|---|
| 172 |
req.path = '/'+('/'.join([urllib.quote(s, '') for s in (req.prepath + segments)])) |
|---|
| 173 |
|
|---|
| 174 |
return self.resource, segments |
|---|
| 175 |
|
|---|
| 176 |
class VHostURIRewrite(object): |
|---|
| 177 |
""" |
|---|
| 178 |
I do request mangling to insure that children know what host they are being |
|---|
| 179 |
accessed from behind mod_proxy. |
|---|
| 180 |
|
|---|
| 181 |
Usage: |
|---|
| 182 |
|
|---|
| 183 |
- Twisted:: |
|---|
| 184 |
|
|---|
| 185 |
root = MyResource() |
|---|
| 186 |
vur = vhost.VHostURIRewrite(uri='http://hostname:port/path', resource=root) |
|---|
| 187 |
server.Site(vur) |
|---|
| 188 |
|
|---|
| 189 |
- Apache:: |
|---|
| 190 |
|
|---|
| 191 |
<VirtualHost hostname:port> |
|---|
| 192 |
ProxyPass /path/ http://localhost:8080/ |
|---|
| 193 |
Servername hostname |
|---|
| 194 |
</VirtualHost> |
|---|
| 195 |
|
|---|
| 196 |
If the trailing / is ommitted in the second argument to ProxyPass |
|---|
| 197 |
VHostURIRewrite will return a 404 response code. |
|---|
| 198 |
|
|---|
| 199 |
uri must be a fully specified uri complete with scheme://hostname/path/ |
|---|
| 200 |
""" |
|---|
| 201 |
|
|---|
| 202 |
implements(iweb.IResource) |
|---|
| 203 |
|
|---|
| 204 |
def __init__(self, uri, resource): |
|---|
| 205 |
""" |
|---|
| 206 |
@param uri: The URI to be used for mutating the request. This MUST |
|---|
| 207 |
include scheme://hostname/path. |
|---|
| 208 |
@type uri: C{str} |
|---|
| 209 |
|
|---|
| 210 |
@param resource: The resource to serve after mutating the request. |
|---|
| 211 |
@type resource: L{twisted.web2.iweb.IResource} |
|---|
| 212 |
""" |
|---|
| 213 |
|
|---|
| 214 |
self.resource = resource |
|---|
| 215 |
|
|---|
| 216 |
(self.scheme, self.host, self.path, |
|---|
| 217 |
params, querystring, fragment) = urlparse.urlparse(uri) |
|---|
| 218 |
if params or querystring or fragment: |
|---|
| 219 |
raise ValueError("Must not specify params, query args, or fragment to VHostURIRewrite") |
|---|
| 220 |
self.path = map(urllib.unquote, self.path[1:].split('/'))[:-1] |
|---|
| 221 |
self.host, self.port = http.splitHostPort(self.scheme, self.host) |
|---|
| 222 |
|
|---|
| 223 |
def renderHTTP(self, req): |
|---|
| 224 |
return http.Response(responsecode.NOT_FOUND) |
|---|
| 225 |
|
|---|
| 226 |
def locateChild(self, req, segments): |
|---|
| 227 |
req.scheme = self.scheme |
|---|
| 228 |
req.host = self.host |
|---|
| 229 |
req.port = self.port |
|---|
| 230 |
req.prepath=self.path[:] |
|---|
| 231 |
req.path = '/'+('/'.join([urllib.quote(s, '') for s in (req.prepath + segments)])) |
|---|
| 232 |
|
|---|
| 233 |
|
|---|
| 234 |
return self.resource, segments |
|---|
| 235 |
|
|---|
| 236 |
__all__ = ['VHostURIRewrite', 'AutoVHostURIRewrite', 'NameVirtualHost'] |
|---|