diff --git a/zerver/lib/subdomains.py b/zerver/lib/subdomains.py index 4b74a9c7a7..e05fa2d64f 100644 --- a/zerver/lib/subdomains.py +++ b/zerver/lib/subdomains.py @@ -17,10 +17,10 @@ def get_subdomain(request): # X-Forwarded-Host, it passes right through the same way. So our # logic is a bit complicated to allow for that variation. # - # For EXTERNAL_HOST, we take a missing port to mean that any port - # should be accepted in Host. It's not totally clear that's the - # right behavior, but it keeps compatibility with older versions - # of Zulip, so that's a start. + # For both EXTERNAL_HOST and REALM_HOSTS, we take a missing port + # to mean that any port should be accepted in Host. It's not + # totally clear that's the right behavior, but it keeps + # compatibility with older versions of Zulip, so that's a start. host = request.get_host().lower() @@ -32,6 +32,11 @@ def get_subdomain(request): return Realm.SUBDOMAIN_FOR_ROOT_DOMAIN return subdomain + for subdomain, realm_host in settings.REALM_HOSTS.items(): + if re.search('^%s(:\d+)?$' % (realm_host,), + host): + return subdomain + return Realm.SUBDOMAIN_FOR_ROOT_DOMAIN def is_subdomain_root_or_alias(request): diff --git a/zerver/models.py b/zerver/models.py index f6d477c4a2..b257706ee7 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -280,9 +280,10 @@ class Realm(models.Model): @staticmethod def host_for_subdomain(subdomain): # type: (str) -> str - if subdomain not in [None, ""]: - return "%s.%s" % (subdomain, settings.EXTERNAL_HOST) - return settings.EXTERNAL_HOST + if subdomain == Realm.SUBDOMAIN_FOR_ROOT_DOMAIN: + return settings.EXTERNAL_HOST + default_host = "%s.%s" % (subdomain, settings.EXTERNAL_HOST) + return settings.REALM_HOSTS.get(subdomain, default_host) @property def is_zephyr_mirror_realm(self): diff --git a/zerver/tests/test_subdomains.py b/zerver/tests/test_subdomains.py index cbdb283f30..f0c273027f 100644 --- a/zerver/tests/test_subdomains.py +++ b/zerver/tests/test_subdomains.py @@ -1,6 +1,6 @@ import mock -from typing import Any, List +from typing import Any, Dict, List from django.test import TestCase, override_settings @@ -18,9 +18,10 @@ class SubdomainsTest(TestCase): return request def test(expected, host, *, plusport=True, - external_host='example.org', root_aliases=[]): - # type: (str, str, bool, str, List[str]) -> None + external_host='example.org', realm_hosts={}, root_aliases=[]): + # type: (str, str, bool, str, Dict[str, str], List[str]) -> None with self.settings(EXTERNAL_HOST=external_host, + REALM_HOSTS=realm_hosts, ROOT_SUBDOMAIN_ALIASES=root_aliases): self.assertEqual(get_subdomain(request_mock(host)), expected) if plusport and ':' not in host: @@ -38,11 +39,18 @@ class SubdomainsTest(TestCase): test(ROOT, 'arbitrary.com') test(ROOT, 'foo.example.org.evil.com') - # Any port is fine in Host if there's none in EXTERNAL_HOST + # REALM_HOSTS adds a name, + test('bar', 'chat.barbar.com', realm_hosts={'bar': 'chat.barbar.com'}) + # ... exactly, ... + test(ROOT, 'surchat.barbar.com', realm_hosts={'bar': 'chat.barbar.com'}) + test(ROOT, 'foo.chat.barbar.com', realm_hosts={'bar': 'chat.barbar.com'}) + # ... and leaves the subdomain in place too. + test('bar', 'bar.example.org', realm_hosts={'bar': 'chat.barbar.com'}) + + # Any port is fine in Host if there's none in EXTERNAL_HOST, ... test('foo', 'foo.example.org:443', external_host='example.org') test('foo', 'foo.example.org:12345', external_host='example.org') - - # Explicit port in EXTERNAL_HOST must be explicitly matched in Host + # ... but an explicit port in EXTERNAL_HOST must be explicitly matched in Host. test(ROOT, 'foo.example.org', external_host='example.org:12345') test(ROOT, 'foo.example.org', external_host='example.org:443', plusport=False) test('foo', 'foo.example.org:443', external_host='example.org:443') diff --git a/zproject/settings.py b/zproject/settings.py index d67c722662..065f6d1aab 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -245,6 +245,12 @@ DEFAULT_SETTINGS.update({ # purpose now that the REALMS_HAVE_SUBDOMAINS migration is finished. 'SYSTEM_ONLY_REALMS': {"zulip"}, + # Alternate hostnames to serve particular realms on, in addition to + # their usual subdomains. Keys are realm string_ids (aka subdomains), + # and values are alternate hosts. + # The values will also be added to ALLOWED_HOSTS. + 'REALM_HOSTS': {}, + # Whether the server is using the Pgroonga full-text search # backend. Plan is to turn this on for everyone after further # testing. @@ -411,9 +417,11 @@ USE_X_FORWARDED_HOST = True # Extend ALLOWED_HOSTS with localhost (needed to RPC to Tornado), ALLOWED_HOSTS += ['127.0.0.1', 'localhost'] -# and with hosts corresponding to EXTERNAL_HOST. +# ... with hosts corresponding to EXTERNAL_HOST, ALLOWED_HOSTS += [EXTERNAL_HOST.split(":")[0], '.' + EXTERNAL_HOST.split(":")[0]] +# ... and with the hosts in REALM_HOSTS. +ALLOWED_HOSTS += REALM_HOSTS.values() MIDDLEWARE = ( # With the exception of it's dependencies,