2017-03-08 12:41:46 +01:00
|
|
|
import importlib
|
|
|
|
import os
|
2020-06-11 00:54:34 +02:00
|
|
|
from typing import List, Optional
|
2017-03-08 12:41:46 +01:00
|
|
|
|
2018-01-30 06:05:25 +01:00
|
|
|
import django.urls.resolvers
|
2020-08-07 01:09:47 +02:00
|
|
|
import orjson
|
2020-07-01 04:19:54 +02:00
|
|
|
from django.test import Client
|
2017-03-08 12:41:46 +01:00
|
|
|
|
|
|
|
from zerver.lib.test_classes import ZulipTestCase
|
2020-06-17 14:25:25 +02:00
|
|
|
from zerver.models import Realm, Stream
|
2016-02-08 04:00:12 +01:00
|
|
|
from zproject import urls
|
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2017-03-08 12:41:46 +01:00
|
|
|
class PublicURLTest(ZulipTestCase):
|
|
|
|
"""
|
|
|
|
Account creation URLs are accessible even when not logged in. Authenticated
|
|
|
|
URLs redirect to a page.
|
|
|
|
"""
|
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def fetch(self, method: str, urls: List[str], expected_status: int) -> None:
|
2017-03-08 12:41:46 +01:00
|
|
|
for url in urls:
|
|
|
|
# e.g. self.client_post(url) if method is "post"
|
|
|
|
response = getattr(self, method)(url)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertEqual(
|
|
|
|
response.status_code,
|
|
|
|
expected_status,
|
|
|
|
msg=f"Expected {expected_status}, received {response.status_code} for {method} to {url}",
|
|
|
|
)
|
2017-03-08 12:41:46 +01:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_public_urls(self) -> None:
|
2017-03-08 12:41:46 +01:00
|
|
|
"""
|
|
|
|
Test which views are accessible when not logged in.
|
|
|
|
"""
|
|
|
|
# FIXME: We should also test the Tornado URLs -- this codepath
|
|
|
|
# can't do so because this Django test mechanism doesn't go
|
|
|
|
# through Tornado.
|
2021-02-12 08:20:45 +01:00
|
|
|
denmark_stream_id = Stream.objects.get(name="Denmark").id
|
2021-02-12 08:19:30 +01:00
|
|
|
get_urls = {
|
|
|
|
200: [
|
|
|
|
"/accounts/home/",
|
|
|
|
"/accounts/login/",
|
|
|
|
"/en/accounts/home/",
|
|
|
|
"/ru/accounts/home/",
|
|
|
|
"/en/accounts/login/",
|
|
|
|
"/ru/accounts/login/",
|
|
|
|
"/help/",
|
|
|
|
"/",
|
|
|
|
"/en/",
|
|
|
|
"/ru/",
|
|
|
|
],
|
|
|
|
400: [
|
|
|
|
"/json/messages",
|
|
|
|
],
|
|
|
|
401: [
|
|
|
|
f"/json/streams/{denmark_stream_id}/members",
|
|
|
|
"/api/v1/users/me/subscriptions",
|
|
|
|
"/api/v1/messages",
|
|
|
|
"/api/v1/streams",
|
|
|
|
],
|
|
|
|
404: ["/help/nonexistent", "/help/include/admin", "/help/" + "z" * 1000],
|
|
|
|
}
|
2017-03-08 12:41:46 +01:00
|
|
|
|
|
|
|
# Add all files in 'templates/zerver/help' directory (except for 'main.html' and
|
|
|
|
# 'index.md') to `get_urls['200']` list.
|
2021-02-12 08:20:45 +01:00
|
|
|
for doc in os.listdir("./templates/zerver/help"):
|
|
|
|
if doc.startswith(".") or "~" in doc or "#" in doc:
|
2017-03-08 12:41:46 +01:00
|
|
|
continue # nocoverage -- just here for convenience
|
2021-02-12 08:20:45 +01:00
|
|
|
if doc not in {"main.html", "index.md", "include", "missing.md"}:
|
|
|
|
get_urls[200].append("/help/" + os.path.splitext(doc)[0]) # Strip the extension.
|
2017-03-08 12:41:46 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
post_urls = {
|
|
|
|
200: ["/accounts/login/"],
|
|
|
|
302: ["/accounts/logout/"],
|
|
|
|
401: [
|
|
|
|
"/json/messages",
|
|
|
|
"/json/invites",
|
|
|
|
"/json/subscriptions/exists",
|
|
|
|
"/api/v1/users/me/subscriptions/properties",
|
|
|
|
"/json/fetch_api_key",
|
|
|
|
"/json/users/me/subscriptions",
|
|
|
|
"/api/v1/users/me/subscriptions",
|
|
|
|
"/json/export/realm",
|
|
|
|
],
|
|
|
|
400: [
|
|
|
|
"/api/v1/external/github",
|
|
|
|
"/api/v1/fetch_api_key",
|
|
|
|
],
|
|
|
|
}
|
2017-07-31 20:44:52 +02:00
|
|
|
patch_urls = {
|
|
|
|
401: ["/json/settings"],
|
|
|
|
}
|
2020-06-26 19:51:10 +02:00
|
|
|
|
2017-09-27 10:11:59 +02:00
|
|
|
for status_code, url_set in get_urls.items():
|
2017-03-08 12:41:46 +01:00
|
|
|
self.fetch("client_get", url_set, status_code)
|
2017-09-27 10:11:59 +02:00
|
|
|
for status_code, url_set in post_urls.items():
|
2017-03-08 12:41:46 +01:00
|
|
|
self.fetch("client_post", url_set, status_code)
|
2017-09-27 10:11:59 +02:00
|
|
|
for status_code, url_set in patch_urls.items():
|
2017-07-31 20:44:52 +02:00
|
|
|
self.fetch("client_patch", url_set, status_code)
|
2017-03-08 12:41:46 +01:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_get_gcid_when_not_configured(self) -> None:
|
2017-03-08 12:41:46 +01:00
|
|
|
with self.settings(GOOGLE_CLIENT_ID=None):
|
|
|
|
resp = self.client_get("/api/v1/fetch_google_client_id")
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertEqual(
|
|
|
|
400,
|
|
|
|
resp.status_code,
|
|
|
|
msg=f"Expected 400, received {resp.status_code} for GET /api/v1/fetch_google_client_id",
|
|
|
|
)
|
2021-02-12 08:20:45 +01:00
|
|
|
self.assertEqual("error", resp.json()["result"])
|
2017-03-08 12:41:46 +01:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_get_gcid_when_configured(self) -> None:
|
2017-03-08 12:41:46 +01:00
|
|
|
with self.settings(GOOGLE_CLIENT_ID="ABCD"):
|
|
|
|
resp = self.client_get("/api/v1/fetch_google_client_id")
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertEqual(
|
|
|
|
200,
|
|
|
|
resp.status_code,
|
|
|
|
msg=f"Expected 200, received {resp.status_code} for GET /api/v1/fetch_google_client_id",
|
|
|
|
)
|
2020-08-07 01:09:47 +02:00
|
|
|
data = orjson.loads(resp.content)
|
2021-02-12 08:20:45 +01:00
|
|
|
self.assertEqual("success", data["result"])
|
|
|
|
self.assertEqual("ABCD", data["google_client_id"])
|
2017-03-08 12:41:46 +01:00
|
|
|
|
2020-06-17 14:25:25 +02:00
|
|
|
def test_config_error_endpoints_dev_env(self) -> None:
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-06-17 14:25:25 +02:00
|
|
|
The content of these pages is tested separately.
|
|
|
|
Here we simply sanity-check that all the URLs load
|
|
|
|
correctly.
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-06-17 14:25:25 +02:00
|
|
|
auth_types = [auth.lower() for auth in Realm.AUTHENTICATION_FLAGS]
|
2021-02-12 08:19:30 +01:00
|
|
|
for auth in [
|
2021-02-12 08:20:45 +01:00
|
|
|
"azuread",
|
|
|
|
"email",
|
|
|
|
"remoteuser",
|
2021-02-12 08:19:30 +01:00
|
|
|
]: # We do not have configerror pages for AzureAD and Email.
|
2020-06-17 14:25:25 +02:00
|
|
|
auth_types.remove(auth)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
auth_types += [
|
2021-02-12 08:20:45 +01:00
|
|
|
"smtp",
|
|
|
|
"remoteuser/remote_user_backend_disabled",
|
|
|
|
"remoteuser/remote_user_header_missing",
|
2021-02-12 08:19:30 +01:00
|
|
|
]
|
2021-02-12 08:20:45 +01:00
|
|
|
urls = [f"/config-error/{auth_type}" for auth_type in auth_types]
|
2020-06-17 14:25:25 +02:00
|
|
|
with self.settings(DEVELOPMENT=True):
|
|
|
|
for url in urls:
|
|
|
|
response = self.client_get(url)
|
2021-02-12 08:20:45 +01:00
|
|
|
self.assert_in_success_response(["Configuration error"], response)
|
2020-06-17 14:25:25 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-07-01 04:19:54 +02:00
|
|
|
class URLResolutionTest(ZulipTestCase):
|
2018-02-02 05:43:18 +01:00
|
|
|
def get_callback_string(self, pattern: django.urls.resolvers.URLPattern) -> Optional[str]:
|
2021-02-12 08:20:45 +01:00
|
|
|
callback_str = hasattr(pattern, "lookup_str") and "lookup_str"
|
|
|
|
callback_str = callback_str or "_callback_str"
|
2017-03-09 09:20:38 +01:00
|
|
|
return getattr(pattern, callback_str, None)
|
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def check_function_exists(self, module_name: str, view: str) -> None:
|
2016-02-08 04:00:12 +01:00
|
|
|
module = importlib.import_module(module_name)
|
2020-06-10 06:41:04 +02:00
|
|
|
self.assertTrue(hasattr(module, view), f"View {module_name}.{view} does not exist")
|
2016-02-08 04:00:12 +01:00
|
|
|
|
|
|
|
# Tests function-based views declared in urls.urlpatterns for
|
|
|
|
# whether the function exists. We at present do not test the
|
|
|
|
# class-based views.
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_non_api_url_resolution(self) -> None:
|
2016-02-08 04:00:12 +01:00
|
|
|
for pattern in urls.urlpatterns:
|
2017-03-09 09:20:38 +01:00
|
|
|
callback_str = self.get_callback_string(pattern)
|
|
|
|
if callback_str:
|
|
|
|
(module_name, base_view) = callback_str.rsplit(".", 1)
|
|
|
|
self.check_function_exists(module_name, base_view)
|
errors: Force a super-simpler handler for 400 errors.
This works around a bug in Django in handling the error case of a
client sending an inappropriate HTTP `Host:` header. Various
internal Django machinery expects to be able to casually call
`request.get_host()`, which will attempt to parse that header, so an
exception will be raised. The exception-handling machinery attempts
to catch that exception and just turn it into a 400 response... but
in a certain case, that machinery itself ends up trying to call
`request.get_host()`, and we end up with an uncaught exception that
causes a 500 response, a chain of tracebacks in the logs, and an email
to the server admins. See example below.
That `request.get_host` call comes in the midst of some CSRF-related
middleware, which doesn't even serve any function unless you have a
form in your 400 response page that you want CSRF protection for.
We use the default 400 response page, which is a 26-byte static
HTML error message. So, just send that with no further ado.
Example exception from server logs (lightly edited):
2017-10-08 09:51:50.835 ERR [django.security.DisallowedHost] Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
2017-10-08 09:51:50.835 ERR [django.request] Internal Server Error: /loginWithSetCookie
Traceback (most recent call last):
File ".../django/core/handlers/exception.py", line 41, in inner
response = get_response(request)
File ".../django/utils/deprecation.py", line 138, in __call__
response = self.process_request(request)
File ".../django/middleware/common.py", line 57, in process_request
host = request.get_host()
File ".../django/http/request.py", line 113, in get_host
raise DisallowedHost(msg)
django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../django/core/handlers/exception.py", line 109, in get_exception_response
response = callback(request, **dict(param_dict, exception=exception))
File ".../django/utils/decorators.py", line 145, in _wrapped_view
result = middleware.process_view(request, view_func, args, kwargs)
File ".../django/middleware/csrf.py", line 276, in process_view
good_referer = request.get_host()
File ".../django/http/request.py", line 113, in get_host
raise DisallowedHost(msg)
django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
2017-10-10 06:39:36 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-07-01 04:19:54 +02:00
|
|
|
class ErrorPageTest(ZulipTestCase):
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_bogus_http_host(self) -> None:
|
errors: Force a super-simpler handler for 400 errors.
This works around a bug in Django in handling the error case of a
client sending an inappropriate HTTP `Host:` header. Various
internal Django machinery expects to be able to casually call
`request.get_host()`, which will attempt to parse that header, so an
exception will be raised. The exception-handling machinery attempts
to catch that exception and just turn it into a 400 response... but
in a certain case, that machinery itself ends up trying to call
`request.get_host()`, and we end up with an uncaught exception that
causes a 500 response, a chain of tracebacks in the logs, and an email
to the server admins. See example below.
That `request.get_host` call comes in the midst of some CSRF-related
middleware, which doesn't even serve any function unless you have a
form in your 400 response page that you want CSRF protection for.
We use the default 400 response page, which is a 26-byte static
HTML error message. So, just send that with no further ado.
Example exception from server logs (lightly edited):
2017-10-08 09:51:50.835 ERR [django.security.DisallowedHost] Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
2017-10-08 09:51:50.835 ERR [django.request] Internal Server Error: /loginWithSetCookie
Traceback (most recent call last):
File ".../django/core/handlers/exception.py", line 41, in inner
response = get_response(request)
File ".../django/utils/deprecation.py", line 138, in __call__
response = self.process_request(request)
File ".../django/middleware/common.py", line 57, in process_request
host = request.get_host()
File ".../django/http/request.py", line 113, in get_host
raise DisallowedHost(msg)
django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../django/core/handlers/exception.py", line 109, in get_exception_response
response = callback(request, **dict(param_dict, exception=exception))
File ".../django/utils/decorators.py", line 145, in _wrapped_view
result = middleware.process_view(request, view_func, args, kwargs)
File ".../django/middleware/csrf.py", line 276, in process_view
good_referer = request.get_host()
File ".../django/http/request.py", line 113, in get_host
raise DisallowedHost(msg)
django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
2017-10-10 06:39:36 +02:00
|
|
|
# This tests that we've successfully worked around a certain bug in
|
|
|
|
# Django's exception handling. The enforce_csrf_checks=True,
|
|
|
|
# secure=True, and HTTP_REFERER with an `https:` scheme are all
|
|
|
|
# there to get us down just the right path for Django to blow up
|
|
|
|
# when presented with an HTTP_HOST that's not a valid DNS name.
|
|
|
|
client = Client(enforce_csrf_checks=True)
|
2021-02-12 08:19:30 +01:00
|
|
|
result = client.post(
|
2021-02-12 08:20:45 +01:00
|
|
|
"/json/users", secure=True, HTTP_REFERER="https://somewhere", HTTP_HOST="$nonsense"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
errors: Force a super-simpler handler for 400 errors.
This works around a bug in Django in handling the error case of a
client sending an inappropriate HTTP `Host:` header. Various
internal Django machinery expects to be able to casually call
`request.get_host()`, which will attempt to parse that header, so an
exception will be raised. The exception-handling machinery attempts
to catch that exception and just turn it into a 400 response... but
in a certain case, that machinery itself ends up trying to call
`request.get_host()`, and we end up with an uncaught exception that
causes a 500 response, a chain of tracebacks in the logs, and an email
to the server admins. See example below.
That `request.get_host` call comes in the midst of some CSRF-related
middleware, which doesn't even serve any function unless you have a
form in your 400 response page that you want CSRF protection for.
We use the default 400 response page, which is a 26-byte static
HTML error message. So, just send that with no further ado.
Example exception from server logs (lightly edited):
2017-10-08 09:51:50.835 ERR [django.security.DisallowedHost] Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
2017-10-08 09:51:50.835 ERR [django.request] Internal Server Error: /loginWithSetCookie
Traceback (most recent call last):
File ".../django/core/handlers/exception.py", line 41, in inner
response = get_response(request)
File ".../django/utils/deprecation.py", line 138, in __call__
response = self.process_request(request)
File ".../django/middleware/common.py", line 57, in process_request
host = request.get_host()
File ".../django/http/request.py", line 113, in get_host
raise DisallowedHost(msg)
django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../django/core/handlers/exception.py", line 109, in get_exception_response
response = callback(request, **dict(param_dict, exception=exception))
File ".../django/utils/decorators.py", line 145, in _wrapped_view
result = middleware.process_view(request, view_func, args, kwargs)
File ".../django/middleware/csrf.py", line 276, in process_view
good_referer = request.get_host()
File ".../django/http/request.py", line 113, in get_host
raise DisallowedHost(msg)
django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.
2017-10-10 06:39:36 +02:00
|
|
|
self.assertEqual(result.status_code, 400)
|