2018-12-04 23:22:05 +01:00
|
|
|
import re
|
2019-02-02 23:53:22 +01:00
|
|
|
from typing import List, Optional, Tuple
|
2017-01-30 07:21:13 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2018-12-10 22:42:28 +01:00
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from version import DESKTOP_MINIMUM_VERSION, DESKTOP_WARNING_VERSION
|
2017-01-30 07:21:13 +01:00
|
|
|
from zerver.lib.response import json_error, json_success
|
|
|
|
from zerver.lib.user_agent import parse_user_agent
|
2020-04-20 14:00:03 +02:00
|
|
|
from zerver.signals import get_device_browser
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2017-01-30 07:21:13 +01:00
|
|
|
|
2018-12-05 00:00:56 +01:00
|
|
|
def pop_numerals(ver: str) -> Tuple[List[int], str]:
|
|
|
|
match = re.search(r'^( \d+ (?: \. \d+ )* ) (.*)', ver, re.X)
|
2018-12-04 23:22:05 +01:00
|
|
|
if match is None:
|
2018-12-05 00:00:56 +01:00
|
|
|
return [], ver
|
|
|
|
numerals, rest = match.groups()
|
|
|
|
numbers = [int(n) for n in numerals.split('.')]
|
|
|
|
return numbers, rest
|
2018-12-04 23:22:05 +01:00
|
|
|
|
|
|
|
def version_lt(ver1: str, ver2: str) -> Optional[bool]:
|
|
|
|
'''
|
|
|
|
Compare two Zulip-style version strings.
|
|
|
|
|
|
|
|
Versions are dot-separated sequences of decimal integers,
|
|
|
|
followed by arbitrary trailing decoration. Comparison is
|
|
|
|
lexicographic on the integer sequences, and refuses to
|
|
|
|
guess how any trailing decoration compares to any other,
|
|
|
|
to further numerals, or to nothing.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
True if ver1 < ver2
|
|
|
|
False if ver1 >= ver2
|
|
|
|
None if can't tell.
|
|
|
|
'''
|
2018-12-05 00:00:56 +01:00
|
|
|
num1, rest1 = pop_numerals(ver1)
|
|
|
|
num2, rest2 = pop_numerals(ver2)
|
2018-12-05 00:06:46 +01:00
|
|
|
if not num1 or not num2:
|
|
|
|
return None
|
2018-12-05 00:00:56 +01:00
|
|
|
common_len = min(len(num1), len(num2))
|
|
|
|
common_num1, rest_num1 = num1[:common_len], num1[common_len:]
|
|
|
|
common_num2, rest_num2 = num2[:common_len], num2[common_len:]
|
|
|
|
|
|
|
|
# Leading numbers win.
|
|
|
|
if common_num1 != common_num2:
|
|
|
|
return common_num1 < common_num2
|
|
|
|
|
|
|
|
# More numbers beats end-of-string, but ??? vs trailing text.
|
|
|
|
# (NB at most one of rest_num1, rest_num2 is nonempty.)
|
|
|
|
if not rest1 and rest_num2:
|
|
|
|
return True
|
|
|
|
if rest_num1 and not rest2:
|
|
|
|
return False
|
|
|
|
if rest_num1 or rest_num2:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Trailing text we can only compare for equality.
|
|
|
|
if rest1 == rest2:
|
|
|
|
return False
|
|
|
|
return None
|
2018-12-04 23:22:05 +01:00
|
|
|
|
2018-12-05 00:46:52 +01:00
|
|
|
|
|
|
|
def find_mobile_os(user_agent: str) -> Optional[str]:
|
|
|
|
if re.search(r'\b Android \b', user_agent, re.I | re.X):
|
|
|
|
return 'android'
|
|
|
|
if re.search(r'\b(?: iOS | iPhone\ OS )\b', user_agent, re.I | re.X):
|
|
|
|
return 'ios'
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2018-12-05 00:49:23 +01:00
|
|
|
# Zulip Mobile release 16.2.96 was made 2018-08-22. It fixed a
|
|
|
|
# bug in our Android code that causes spammy, obviously-broken
|
|
|
|
# notifications once the "remove_push_notification" feature is
|
|
|
|
# enabled on the user's Zulip server.
|
|
|
|
android_min_app_version = '16.2.96'
|
|
|
|
|
2018-11-29 03:07:40 +01:00
|
|
|
def check_global_compatibility(request: HttpRequest) -> HttpResponse:
|
2018-12-10 22:42:28 +01:00
|
|
|
if request.META.get('HTTP_USER_AGENT') is None:
|
|
|
|
return json_error(_('User-Agent header missing from request'))
|
|
|
|
|
2018-12-06 00:03:42 +01:00
|
|
|
# This string should not be tagged for translation, since old
|
|
|
|
# clients are checking for an extra string.
|
|
|
|
legacy_compatibility_error_message = "Client is too old"
|
2017-01-30 07:21:13 +01:00
|
|
|
user_agent = parse_user_agent(request.META["HTTP_USER_AGENT"])
|
2018-03-22 20:02:15 +01:00
|
|
|
if user_agent['name'] == "ZulipInvalid":
|
2018-12-06 00:03:42 +01:00
|
|
|
return json_error(legacy_compatibility_error_message)
|
2018-12-05 00:49:23 +01:00
|
|
|
if user_agent['name'] == "ZulipMobile":
|
|
|
|
user_os = find_mobile_os(request.META["HTTP_USER_AGENT"])
|
|
|
|
if (user_os == 'android'
|
|
|
|
and version_lt(user_agent['version'], android_min_app_version)):
|
2018-12-06 00:03:42 +01:00
|
|
|
return json_error(legacy_compatibility_error_message)
|
2017-01-30 07:21:13 +01:00
|
|
|
return json_success()
|
2020-02-28 09:55:29 +01:00
|
|
|
|
2020-03-25 02:00:28 +01:00
|
|
|
def is_outdated_desktop_app(user_agent_str: str) -> Tuple[bool, bool, bool]:
|
2020-04-01 22:11:26 +02:00
|
|
|
# Returns (insecure, banned, auto_update_broken)
|
2020-02-28 09:55:29 +01:00
|
|
|
user_agent = parse_user_agent(user_agent_str)
|
|
|
|
if user_agent['name'] == 'ZulipDesktop':
|
|
|
|
# The deprecated QT/webkit based desktop app, last updated in ~2016.
|
2020-03-25 02:00:28 +01:00
|
|
|
return (True, True, True)
|
2020-02-28 09:55:29 +01:00
|
|
|
|
2020-03-25 02:00:28 +01:00
|
|
|
if user_agent['name'] != 'ZulipElectron':
|
|
|
|
return (False, False, False)
|
|
|
|
|
|
|
|
if version_lt(user_agent['version'], '4.0.0'):
|
|
|
|
# Version 2.3.82 and older (aka <4.0.0) of the modern
|
|
|
|
# Electron-based Zulip desktop app with known security issues.
|
|
|
|
# won't auto-update; we may want a special notice to
|
|
|
|
# distinguish those from modern releases.
|
|
|
|
return (True, True, True)
|
|
|
|
|
2020-04-01 22:11:26 +02:00
|
|
|
if version_lt(user_agent['version'], DESKTOP_MINIMUM_VERSION):
|
|
|
|
# Below DESKTOP_MINIMUM_VERSION, we reject access as well.
|
|
|
|
return (True, True, False)
|
|
|
|
|
|
|
|
if version_lt(user_agent['version'], DESKTOP_WARNING_VERSION):
|
2020-03-25 02:00:28 +01:00
|
|
|
# Other insecure versions should just warn.
|
|
|
|
return (True, False, False)
|
|
|
|
|
|
|
|
return (False, False, False)
|
2020-04-20 14:00:03 +02:00
|
|
|
|
|
|
|
def is_unsupported_browser(user_agent: str) -> Tuple[bool, Optional[str]]:
|
|
|
|
browser_name = get_device_browser(user_agent)
|
|
|
|
if browser_name == "Internet Explorer":
|
|
|
|
return (True, browser_name)
|
|
|
|
return (False, browser_name)
|