#####################################################################################
#
# Copyright (c) typedef int GmbH
# SPDX-License-Identifier: EUPL-1.2
#
#####################################################################################
import importlib
import os
import txaio
from autobahn.wamp.exception import ApplicationError
from twisted.internet.defer import inlineCallbacks, maybeDeferred
from twisted.python.compat import urllib_parse, urlquote
from twisted.web import server
from twisted.web._responses import NOT_FOUND
from twisted.web.proxy import ReverseProxyResource
from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET
import crossbar
[docs]
class Resource404(Resource):
"""
Custom error page (404) Twisted Web resource.
"""
def __init__(self, templates, directory):
Resource.__init__(self)
[docs]
self._page = templates.get_template("cb_web_404.html")
[docs]
self._directory = directory
[docs]
self._pid = "{}".format(os.getpid())
[docs]
def render_HEAD(self, request):
request.setResponseCode(NOT_FOUND)
return b""
[docs]
def render_GET(self, request):
request.setResponseCode(NOT_FOUND)
try:
peer = request.transport.getPeer()
peer = "{}:{}".format(peer.host, peer.port)
except:
peer = "?:?"
s = self._page.render(
cbVersion=crossbar.__version__, directory=self._directory.decode("utf8"), workerPid=self._pid, peer=peer
)
return s.encode("utf8")
[docs]
class RootResource(Resource):
"""
Root resource when you want one specific resource be the default serving
resource for a Twisted Web site, but have sub-paths served by different
resources.
"""
def __init__(self, rootResource, children):
"""
:param rootResource: The resource to serve as root resource.
:type rootResource: `twisted.web.resource.Resource <http://twistedmatrix.com/documents/current/api/twisted.web.resource.Resource.html>`_
:param children: A dictionary with string keys constituting URL sub-paths, and Twisted Web resources as values.
:type children: dict
"""
Resource.__init__(self)
[docs]
self._rootResource = rootResource
[docs]
self.children = children
[docs]
def getChild(self, path, request):
request.prepath.pop()
request.postpath.insert(0, path)
return self._rootResource
[docs]
class RouterWebService(object):
"""
A Web service configured on a URL path on a Web transport.
"""
[docs]
log = txaio.make_logger()
def __init__(self, transport, path, config=None, resource=None):
# Web transport this Web service is attached to
[docs]
self._transport = transport
# path on Web transport under which the Web service is attached
# configuration of the Web service (itself)
[docs]
self._config = config or dict(type="page404")
# Twisted Web resource on self._path itself
[docs]
self._resource = resource or Resource404(self._transport._worker._templates)
# RouterWebService children on subpaths
[docs]
def __contains__(self, path):
return path in self._paths
[docs]
def __getitem__(self, path):
return self._paths.get(path, None)
[docs]
def __delitem__(self, path):
if path in self._paths:
deleted = self._paths[path]
web_path = path.encode("utf8")
if web_path in self._resource.children:
del self._resource.children[web_path]
del self._paths[path]
return deleted
[docs]
def __setitem__(self, path, webservice):
if path in self._paths:
del self._paths[path]
self._paths[path] = webservice
web_path = path.encode("utf8")
self._resource.putChild(web_path, webservice._resource)
@property
[docs]
def path(self):
return self._path
@property
[docs]
def config(self):
return self._config
@property
[docs]
def resource(self):
return self._resource
[docs]
class ExtReverseProxyResource(ReverseProxyResource):
[docs]
log = txaio.make_logger()
def __init__(self, host, port, path, forwarded_port=None, forwarded_proto=None):
# host:port/path => target server
[docs]
self._forwarded_port = forwarded_port
[docs]
self._forwarded_proto = forwarded_proto
ReverseProxyResource.__init__(self, host, port, path)
[docs]
def render(self, request):
"""
Render a request by forwarding it to the proxied server.
"""
self.log.info("{klass}.render(): forwarding incoming HTTP request ..", klass=self.__class__.__name__)
# host request by client in incoming HTTP request
requested_host = request.requestHeaders.getRawHeaders("Host")[0]
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
request.requestHeaders.setRawHeaders(b"X-Forwarded-Host", [requested_host.encode("ascii")])
# request.requestHeaders.setRawHeaders(b'X-Forwarded-Server', [requested_host.encode('ascii')])
# crossbar web transport listening IP/port
server_port = request.getHost().port
server_port = "{}".format(server_port).encode("ascii")
# RFC 2616 tells us that we can omit the port if it's the default port,
# but we have to provide it otherwise
if self.port == 80:
host = self.host
else:
host = "%s:%d" % (self.host, self.port)
request.requestHeaders.setRawHeaders(b"Host", [host.encode("utf8")])
# forward originating IP of incoming HTTP request
client_ip = request.getClientAddress().host
if client_ip:
client_ip = client_ip.encode("ascii")
request.requestHeaders.setRawHeaders(b"X-Forwarded-For", [client_ip])
request.requestHeaders.setRawHeaders(b"X-Real-IP", [client_ip])
# forward information of outside listening port and protocol (http vs https)
if self._forwarded_port:
request.requestHeaders.setRawHeaders(
b"X-Forwarded-Port", ["{}".format(self._forwarded_port).encode("ascii")]
)
else:
request.requestHeaders.setRawHeaders(b"X-Forwarded-Port", [server_port])
if self._forwarded_proto:
request.requestHeaders.setRawHeaders(b"X-Forwarded-Proto", [self._forwarded_proto])
else:
request.requestHeaders.setRawHeaders(
b"X-Forwarded-Proto", [("https" if server_port == 443 else "http").encode("ascii")]
)
# rewind cursor to begin of request data
request.content.seek(0, 0)
# reapply query strings to forwarding HTTP request
qs = urllib_parse.urlparse(request.uri)[4]
if qs:
rest = self.path + b"?" + qs
else:
rest = self.path
self.log.info(
'forwarding HTTP request to "{rest}" with HTTP request headers {headers}',
rest=rest,
headers=request.getAllHeaders(),
)
# now issue the forwarded request to the HTTP server that is being reverse-proxied
clientFactory = self.proxyClientFactoryClass(
request.method, rest, request.clientproto, request.getAllHeaders(), request.content.read(), request
)
self.reactor.connectTCP(self.host, self.port, clientFactory)
# the proxy client request created ^ is taking care of actually finishing the request ..
return NOT_DONE_YET
[docs]
def getChild(self, path, request):
return ExtReverseProxyResource(
self.host,
self.port,
self.path + b"/" + urlquote(path, safe=b"").encode("utf-8"),
forwarded_port=self._forwarded_port,
forwarded_proto=self._forwarded_proto,
)
[docs]
class RouterWebServiceReverseWeb(RouterWebService):
"""
Reverse Web proxy service.
"""
@staticmethod
[docs]
def create(transport, path, config):
personality = transport.worker.personality
personality.WEB_SERVICE_CHECKERS["reverseproxy"](personality, config)
# target HTTP server to forward incoming HTTP requests to
host = config["host"]
port = int(config.get("port", 80))
base_path = config.get("path", "").encode("utf-8")
# public listening port and protocol (http vs https) the crossbar
# web transport is listening on. this might be used by the HTTP server
# the request is proxied to to construct correct HTTP links (which need
# to point to the _public_ listening web transport of crossbar)
forwarded_port = int(config.get("forwarded_port", 80))
forwarded_proto = config.get("forwarded_proto", "http").encode("ascii")
resource = ExtReverseProxyResource(
host, port, base_path, forwarded_port=forwarded_port, forwarded_proto=forwarded_proto
)
if path == "/":
resource = RootResource(resource, {})
return RouterWebServiceReverseWeb(transport, base_path, config, resource)
[docs]
class RedirectResource(Resource):
"""
Redirecting Twisted Web resource.
"""
def __init__(self, redirect_url):
Resource.__init__(self)
[docs]
self._redirect_url = redirect_url
[docs]
def render_GET(self, request):
request.redirect(self._redirect_url)
request.finish()
return server.NOT_DONE_YET
[docs]
class RouterWebServiceRedirect(RouterWebService):
"""
Redirecting Web service.
"""
@staticmethod
[docs]
def create(transport, path, config):
personality = transport.worker.personality
personality.WEB_SERVICE_CHECKERS["redirect"](personality, config)
redirect_url = config["url"].encode("ascii", "ignore")
resource = RedirectResource(redirect_url)
return RouterWebServiceRedirect(transport, path, config, resource)
[docs]
class RouterWebServiceTwistedWeb(RouterWebService):
"""
Generic Twisted Web base service.
"""
@staticmethod
[docs]
def create(transport, path, config):
personality = transport.worker.personality
personality.WEB_SERVICE_CHECKERS["resource"](personality, config)
klassname = config["classname"]
try:
c = klassname.split(".")
module_name, klass_name = ".".join(c[:-1]), c[-1]
module = importlib.import_module(module_name)
make = getattr(module, klass_name)
resource = make(config.get("extra", {}))
except Exception as e:
emsg = "Failed to import class '{}' - {}".format(klassname, e)
raise ApplicationError("crossbar.error.class_import_failed", emsg)
return RouterWebServiceTwistedWeb(transport, path, config, resource)
[docs]
class RouterWebServiceNestedPath(RouterWebService):
"""
Nested sub-URL path (pseudo-)service.
"""
@staticmethod
@inlineCallbacks
[docs]
def create(transport, path, config):
personality = transport.worker.personality
personality.WEB_SERVICE_CHECKERS["path"](personality, config)
nested_paths = config.get("paths", {})
if "/" in nested_paths:
root_config = nested_paths["/"]
root_factory = personality.WEB_SERVICE_FACTORIES[root_config["type"]]
root_service = yield maybeDeferred(root_factory.create, transport, "/", root_config)
root_resource = root_service.resource
else:
root_resource = Resource404(transport.templates, b"")
for nested_path in sorted(nested_paths):
if nested_path != "/":
nested_config = nested_paths[nested_path]
nested_factory = personality.WEB_SERVICE_FACTORIES[nested_config["type"]]
nested_service = yield maybeDeferred(nested_factory.create, transport, nested_path, nested_config)
nested_resource = nested_service.resource
root_resource.putChild(nested_path.encode("utf8"), nested_resource)
return RouterWebServiceNestedPath(transport, path, config, root_resource)