Commit 8cfaf8d2 authored by Vincent Pelletier's avatar Vincent Pelletier

stack/erp5: Add support for path-based routing.

When the outside world path does not match the Zope path (typically: Web
Site).
parent 20c1b326
...@@ -3,6 +3,28 @@ ...@@ -3,6 +3,28 @@
"description": "Parameters to instantiate ERP5", "description": "Parameters to instantiate ERP5",
"additionalProperties": false, "additionalProperties": false,
"definitions": { "definitions": {
"routing-rule-list": {
"description": "Maps the path received in requests to given zope path. Rules are applied in the order they are given. This requires the path received from the outside world (typically: frontend) to have its root correspond to Zope's root (for frontend: 'path' parameter must be empty), with the customary VirtualHostMonster construct (for frontend: 'type' must be 'zope').",
"type": "array",
"default": [["/", "/"]],
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"title": "External path",
"description": "Path as received from the outside world, based on VirtualHostRoot element.",
"type": "string"
},
{
"title": "Internal path",
"description": "Zope path, based on Zope root object, the external path should correspond to. '%(site-id)s' is replaced by the site-id value, and '%%' replaced by '%'.",
"type": "string"
}
]
}
},
"tcpv4port": { "tcpv4port": {
"$ref": "./schemas-definitions.json#/tcpv4port" "$ref": "./schemas-definitions.json#/tcpv4port"
} }
...@@ -452,6 +474,20 @@ ...@@ -452,6 +474,20 @@
"balancer": { "balancer": {
"description": "HTTP(S) load balancer proxy parameters", "description": "HTTP(S) load balancer proxy parameters",
"properties": { "properties": {
"path-routing-list": {
"$ref": "#/definitions/routing-rule-list",
"title": "Global path routing rules"
},
"family-path-routing-dict": {
"type": "object",
"title": "Family-specific path routing rules",
"description": "Applied, only for the eponymous family, before global path routing rules.",
"patternProperties": {
".+": {
"$ref": "#/definitions/routing-rule-list"
}
}
},
"ssl": { "ssl": {
"description": "HTTPS certificate generation parameters", "description": "HTTPS certificate generation parameters",
"properties": { "properties": {
......
...@@ -8,6 +8,7 @@ import shutil ...@@ -8,6 +8,7 @@ import shutil
import subprocess import subprocess
import tempfile import tempfile
import time import time
import urllib
import urlparse import urlparse
from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import BaseHTTPRequestHandler
from typing import Dict from typing import Dict
...@@ -166,7 +167,9 @@ class BalancerTestCase(ERP5InstanceTestCase): ...@@ -166,7 +167,9 @@ class BalancerTestCase(ERP5InstanceTestCase):
'ssl-authentication-dict': {}, 'ssl-authentication-dict': {},
'ssl': { 'ssl': {
'caucase-url': cls.getManagedResource("caucase", CaucaseService).url, 'caucase-url': cls.getManagedResource("caucase", CaucaseService).url,
} },
'family-path-routing-dict': {},
'path-routing-list': [],
} }
@classmethod @classmethod
...@@ -877,3 +880,93 @@ class TestClientTLS(BalancerTestCase): ...@@ -877,3 +880,93 @@ class TestClientTLS(BalancerTestCase):
with self.assertRaisesRegexp(Exception, 'certificate revoked'): with self.assertRaisesRegexp(Exception, 'certificate revoked'):
_make_request() _make_request()
class TestPathBasedRouting(BalancerTestCase):
"""Check path-based routing rewrites URLs as expected.
"""
__partition_reference__ = 'pbr'
@classmethod
def _getInstanceParameterDict(cls):
# type: () -> Dict
parameter_dict = super(
TestPathBasedRouting,
cls,
)._getInstanceParameterDict()
parameter_dict['zope-family-dict'][
'second'
] = parameter_dict['zope-family-dict'][
'default'
]
# Routing rules outermost slashes mean nothing. They are internally
# stripped and rebuilt in order to correctly represent the request's URL.
parameter_dict['family-path-routing-dict'] = {
'default': [
['foo/bar', 'erp5/boo/far/faz'], # no outermost slashes
['/foo', '/erp5/somewhere'],
['/foo/shadowed', '/foo_shadowed'], # unreachable
['/next', '/erp5/web_site_module/another_next_website'],
],
}
parameter_dict['path-routing-list'] = [
['/next', '/erp5/web_site_module/the_next_website'],
['/next2', '/erp5/web_site_module/the_next2_website'],
['//', '//erp5/web_site_module/123//'], # extraneous slashes
]
return parameter_dict
def test_routing(self):
# type: () -> None
published_dict = json.loads(self.computer_partition.getConnectionParameterDict()['_'])
scheme = 'scheme'
netloc = 'example.com:8080'
prefix = '/VirtualHostBase/' + scheme + '//' + urllib.quote(
netloc,
safe='',
)
# For easier reading of test data, visualy separating the virtual host
# base from the virtual host root
vhr = '/VirtualHostRoot'
def assertRoutingEqual(family, path, expected_path):
# sanity check: unlike the rules, this test is sensitive to outermost
# slashes, and paths must be absolute-ish for code simplicity.
assert path.startswith('/')
# Frontend is expected to provide URLs with the following path structure:
# /VirtualHostBase/<scheme>//<netloc>/VirtualHostRoot<path>
# where:
# - scheme is the user-input scheme
# - netloc is the user-input netloc
# - path is the user-input path
# Someday, frontends will instead propagate scheme and netloc via other
# means (likely: HTTP headers), in which case this test and the SR will
# need to be amended to reconstruct Virtual Host urls itself, and this
# test will need to be updated accordingly.
self.assertEqual(
requests.get(
urlparse.urljoin(published_dict[family], prefix + vhr + path),
verify=False,
).json()['Path'],
expected_path,
)
# Trailing slash presence is preserved.
assertRoutingEqual('default', '/foo/bar', prefix + '/erp5/boo/far/faz' + vhr + '/_vh_foo/bar')
assertRoutingEqual('default', '/foo/bar/', prefix + '/erp5/boo/far/faz' + vhr + '/_vh_foo/bar/')
# Subpaths are preserved.
assertRoutingEqual('default', '/foo/bar/hey', prefix + '/erp5/boo/far/faz' + vhr + '/_vh_foo/bar/hey')
# Rule precedence: later less-specific rules are applied.
assertRoutingEqual('default', '/foo', prefix + '/erp5/somewhere' + vhr + '/_vh_foo')
assertRoutingEqual('default', '/foo/', prefix + '/erp5/somewhere' + vhr + '/_vh_foo/')
assertRoutingEqual('default', '/foo/baz', prefix + '/erp5/somewhere' + vhr + '/_vh_foo/baz')
# Rule precedence: later more-specific rules are meaningless.
assertRoutingEqual('default', '/foo/shadowed', prefix + '/erp5/somewhere' + vhr + '/_vh_foo/shadowed')
# Rule precedence: family rules applied before general rules.
assertRoutingEqual('default', '/next', prefix + '/erp5/web_site_module/another_next_website' + vhr + '/_vh_next')
# Fallback on general rules when no family-specific rule matches
# Note: the root is special in that there is aways a trailing slash in the
# produced URL.
assertRoutingEqual('default', '/', prefix + '/erp5/web_site_module/123' + vhr + '/')
# Rule-less family reach general rules.
assertRoutingEqual('second', '/foo/bar', prefix + '/erp5/web_site_module/123' + vhr + '/foo/bar') # Rules match whole-elements, so the rule order does not matter to
# elements which share a common prefix.
assertRoutingEqual('second', '/next', prefix + '/erp5/web_site_module/the_next_website' + vhr + '/_vh_next')
assertRoutingEqual('second', '/next2', prefix + '/erp5/web_site_module/the_next2_website' + vhr + '/_vh_next2')
...@@ -74,7 +74,7 @@ md5sum = b5ac16fdeed8863e465e955ba6d1e12a ...@@ -74,7 +74,7 @@ md5sum = b5ac16fdeed8863e465e955ba6d1e12a
[template-erp5] [template-erp5]
filename = instance-erp5.cfg.in filename = instance-erp5.cfg.in
md5sum = 548d99118afa736e5a7c428b0c8ed560 md5sum = 5fd080be58e2489be777af3cea15a768
[template-zeo] [template-zeo]
filename = instance-zeo.cfg.in filename = instance-zeo.cfg.in
...@@ -86,11 +86,11 @@ md5sum = c03f93f95333e6a61b857dcfab7f9c0e ...@@ -86,11 +86,11 @@ md5sum = c03f93f95333e6a61b857dcfab7f9c0e
[template-balancer] [template-balancer]
filename = instance-balancer.cfg.in filename = instance-balancer.cfg.in
md5sum = 8ad9137310ae0403d433bb3c0d93be9f md5sum = 8d3694226b6cbed961f6d608b6d6d294
[template-haproxy-cfg] [template-haproxy-cfg]
filename = haproxy.cfg.in filename = haproxy.cfg.in
md5sum = 8de18a61607bd66341a44b95640d293f md5sum = 452c502fabd5a6066c9dee533dfb1c77
[template-rsyslogd-cfg] [template-rsyslogd-cfg]
filename = rsyslogd.cfg.in filename = rsyslogd.cfg.in
......
...@@ -147,6 +147,8 @@ defaults ...@@ -147,6 +147,8 @@ defaults
log {{ parameter_dict['log-socket'] }} local0 info log {{ parameter_dict['log-socket'] }} local0 info
{% set bind_ssl_crt = 'ssl crt ' ~ parameter_dict['cert'] ~ ' alpn h2,http/1.1' %} {% set bind_ssl_crt = 'ssl crt ' ~ parameter_dict['cert'] ~ ' alpn h2,http/1.1' %}
{% set family_path_routing_dict = parameter_dict['family-path-routing-dict'] %}
{% set path_routing_list = parameter_dict['path-routing-list'] %}
{% for name, (port, _, certificate_authentication, backend_list) in sorted(parameter_dict['backend-dict'].iteritems()) -%} {% for name, (port, _, certificate_authentication, backend_list) in sorted(parameter_dict['backend-dict'].iteritems()) -%}
listen family_{{ name }} listen family_{{ name }}
...@@ -172,6 +174,11 @@ listen family_{{ name }} ...@@ -172,6 +174,11 @@ listen family_{{ name }}
capture request header User-Agent len 512 capture request header User-Agent len 512
log-format "%{+Q}o %{-Q}ci - - [%trg] %r %ST %B %{+Q}[capture.req.hdr(0)] %{+Q}[capture.req.hdr(1)] %Tt" log-format "%{+Q}o %{-Q}ci - - [%trg] %r %ST %B %{+Q}[capture.req.hdr(0)] %{+Q}[capture.req.hdr(1)] %Tt"
{% for outer_prefix, inner_prefix in family_path_routing_dict.get(name, []) + path_routing_list %}
{% set outer_prefix = outer_prefix.strip('/') -%}
http-request replace-path ^(/+VirtualHostBase/+[^/]+/+[^/]+)/+VirtualHostRoot/+({% if outer_prefix %}{{ outer_prefix }}($|/.*){% else %}.*{% endif %}) \1/{{ inner_prefix.strip('/') }}/VirtualHostRoot/{% if outer_prefix %}_vh_{% endif %}\2
{% endfor %}
{% set has_webdav = [] -%} {% set has_webdav = [] -%}
{% for address, connection_count, webdav in backend_list -%} {% for address, connection_count, webdav in backend_list -%}
{% if webdav %}{% do has_webdav.append(None) %}{% endif -%} {% if webdav %}{% do has_webdav.append(None) %}{% endif -%}
......
...@@ -190,6 +190,8 @@ ca-cert = ${haproxy-conf-ssl:ca-cert} ...@@ -190,6 +190,8 @@ ca-cert = ${haproxy-conf-ssl:ca-cert}
crl = ${haproxy-conf-ssl:crl} crl = ${haproxy-conf-ssl:crl}
{% endif %} {% endif %}
stats-socket = ${directory:run}/haproxy.sock stats-socket = ${directory:run}/haproxy.sock
path-routing-list = {{ dumps(slapparameter_dict['path-routing-list']) }}
family-path-routing-dict = {{ dumps(slapparameter_dict['family-path-routing-dict']) }}
pidfile = ${directory:run}/haproxy.pid pidfile = ${directory:run}/haproxy.pid
log-socket = ${rsyslogd-cfg-parameter-dict:log-socket} log-socket = ${rsyslogd-cfg-parameter-dict:log-socket}
server-check-path = {{ dumps(slapparameter_dict['haproxy-server-check-path']) }} server-check-path = {{ dumps(slapparameter_dict['haproxy-server-check-path']) }}
......
...@@ -344,6 +344,31 @@ config-{{ name }}-test-runner-address-list = {{ ' ${' ~ zope_section_id ~ ':conn ...@@ -344,6 +344,31 @@ config-{{ name }}-test-runner-address-list = {{ ' ${' ~ zope_section_id ~ ':conn
{% endfor -%} {% endfor -%}
# XXX: should those really be same for all families ? # XXX: should those really be same for all families ?
config-haproxy-server-check-path = {{ dumps(balancer_dict.get('haproxy-server-check-path', '/') % {'site-id': site_id}) }} config-haproxy-server-check-path = {{ dumps(balancer_dict.get('haproxy-server-check-path', '/') % {'site-id': site_id}) }}
{% set routing_path_template_field_dict = {"site-id": site_id} -%}
{% macro expandRoutingPath(output, input) -%}
{% for outer_prefix, inner_prefix in input -%}
{% do output.append((outer_prefix, inner_prefix % routing_path_template_field_dict)) -%}
{% endfor -%}
{% endmacro -%}
{% set path_routing_list = [] -%}
{% do expandRoutingPath(
path_routing_list,
balancer_dict.get(
'path-routing-list',
(('/', '/'), ),
),
) -%}
config-path-routing-list = {{ dumps(path_routing_list) }}
{% set family_path_routing_dict = {} -%}
{% for name, family_path_routing_list in balancer_dict.get(
'family-path-routing-dict',
{},
).items() -%}
{% set path_routing_list = [] -%}
{% do expandRoutingPath(path_routing_list, family_path_routing_list) -%}
{% do family_path_routing_dict.__setitem__(name, path_routing_list) -%}
{% endfor -%}
config-family-path-routing-dict = {{ dumps(family_path_routing_dict) }}
config-monitor-passwd = ${monitor-htpasswd:passwd} config-monitor-passwd = ${monitor-htpasswd:passwd}
config-ssl = {{ dumps(balancer_dict['ssl']) }} config-ssl = {{ dumps(balancer_dict['ssl']) }}
config-name = ${:name} config-name = ${:name}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment