mirror of https://github.com/zulip/zulip.git
outgoing_http: Use OutgoingSession subclasses in more places.
This adds the X-Smokescreen-Role header to proxy connections, to track usage from various codepaths, and enforces a timeout. Timeouts were kept consistent with their previous values, or set to 5s if they had none previously.
This commit is contained in:
parent
191b1ac2be
commit
4d428490fd
|
@ -28,6 +28,7 @@ from typing_extensions import TypedDict
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
from zerver.lib.avatar_hash import gravatar_hash
|
from zerver.lib.avatar_hash import gravatar_hash
|
||||||
|
from zerver.lib.github import GithubSession
|
||||||
from zproject.config import get_secret
|
from zproject.config import get_secret
|
||||||
|
|
||||||
duplicate_commits_file = os.path.join(os.path.dirname(__file__), "duplicate_commits.json")
|
duplicate_commits_file = os.path.join(os.path.dirname(__file__), "duplicate_commits.json")
|
||||||
|
@ -69,9 +70,13 @@ def fetch_contributors(repo_name: str, max_retries: int) -> List[Contributor]:
|
||||||
if personal_access_token is not None:
|
if personal_access_token is not None:
|
||||||
headers = {"Authorization": f"token {personal_access_token}"}
|
headers = {"Authorization": f"token {personal_access_token}"}
|
||||||
|
|
||||||
|
session = GithubSession()
|
||||||
while True:
|
while True:
|
||||||
response: requests.Response = requests.get(
|
response: requests.Response = session.get(
|
||||||
api_link, {**api_data, "page": f"{page_index}"}, verify=certificates, headers=headers
|
api_link,
|
||||||
|
params={**api_data, "page": f"{page_index}"},
|
||||||
|
verify=certificates,
|
||||||
|
headers=headers,
|
||||||
)
|
)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
|
@ -4,14 +4,20 @@ import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from zerver.lib.cache import cache_with_key
|
from zerver.lib.cache import cache_with_key
|
||||||
|
from zerver.lib.outgoing_http import OutgoingSession
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubSession(OutgoingSession):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(role="github", timeout=5)
|
||||||
|
|
||||||
|
|
||||||
def get_latest_github_release_version_for_repo(repo: str) -> str:
|
def get_latest_github_release_version_for_repo(repo: str) -> str:
|
||||||
api_url = f"https://api.github.com/repos/zulip/{repo}/releases/latest"
|
api_url = f"https://api.github.com/repos/zulip/{repo}/releases/latest"
|
||||||
try:
|
try:
|
||||||
return requests.get(api_url).json()["tag_name"]
|
return GithubSession().get(api_url).json()["tag_name"]
|
||||||
except (requests.RequestException, json.JSONDecodeError, KeyError):
|
except (requests.RequestException, json.JSONDecodeError, KeyError):
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Unable to fetch the latest release version from GitHub %s", api_url, stack_info=True
|
"Unable to fetch the latest release version from GitHub %s", api_url, stack_info=True
|
||||||
|
@ -21,7 +27,7 @@ def get_latest_github_release_version_for_repo(repo: str) -> str:
|
||||||
|
|
||||||
def verify_release_download_link(link: str) -> bool:
|
def verify_release_download_link(link: str) -> bool:
|
||||||
try:
|
try:
|
||||||
requests.head(link).raise_for_status()
|
GithubSession().head(link).raise_for_status()
|
||||||
return True
|
return True
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
logger.error("App download link is broken %s", link)
|
logger.error("App download link is broken %s", link)
|
||||||
|
|
|
@ -53,6 +53,7 @@ from zerver.lib.exceptions import MarkdownRenderingException
|
||||||
from zerver.lib.markdown import fenced_code
|
from zerver.lib.markdown import fenced_code
|
||||||
from zerver.lib.markdown.fenced_code import FENCE_RE
|
from zerver.lib.markdown.fenced_code import FENCE_RE
|
||||||
from zerver.lib.mention import MentionData, get_stream_name_info
|
from zerver.lib.mention import MentionData, get_stream_name_info
|
||||||
|
from zerver.lib.outgoing_http import OutgoingSession
|
||||||
from zerver.lib.subdomains import is_static_or_current_realm_url
|
from zerver.lib.subdomains import is_static_or_current_realm_url
|
||||||
from zerver.lib.tex import render_tex
|
from zerver.lib.tex import render_tex
|
||||||
from zerver.lib.thumbnail import user_uploads_or_external
|
from zerver.lib.thumbnail import user_uploads_or_external
|
||||||
|
@ -468,12 +469,17 @@ def fetch_tweet_data(tweet_id: str) -> Optional[Dict[str, Any]]:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class OpenGraphSession(OutgoingSession):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(role="markdown", timeout=1)
|
||||||
|
|
||||||
|
|
||||||
def fetch_open_graph_image(url: str) -> Optional[Dict[str, Any]]:
|
def fetch_open_graph_image(url: str) -> Optional[Dict[str, Any]]:
|
||||||
og = {"image": None, "title": None, "desc": None}
|
og = {"image": None, "title": None, "desc": None}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with requests.get(
|
with OpenGraphSession().get(
|
||||||
url, headers={"Accept": "text/html,application/xhtml+xml"}, stream=True, timeout=1
|
url, headers={"Accept": "text/html,application/xhtml+xml"}, stream=True
|
||||||
) as res:
|
) as res:
|
||||||
if res.status_code != requests.codes.ok:
|
if res.status_code != requests.codes.ok:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -12,9 +12,15 @@ from analytics.models import InstallationCount, RealmCount
|
||||||
from version import ZULIP_VERSION
|
from version import ZULIP_VERSION
|
||||||
from zerver.lib.exceptions import JsonableError
|
from zerver.lib.exceptions import JsonableError
|
||||||
from zerver.lib.export import floatify_datetime_fields
|
from zerver.lib.export import floatify_datetime_fields
|
||||||
|
from zerver.lib.outgoing_http import OutgoingSession
|
||||||
from zerver.models import RealmAuditLog
|
from zerver.models import RealmAuditLog
|
||||||
|
|
||||||
|
|
||||||
|
class PushBouncerSession(OutgoingSession):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(role="push_bouncer", timeout=30)
|
||||||
|
|
||||||
|
|
||||||
class PushNotificationBouncerException(Exception):
|
class PushNotificationBouncerException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -55,8 +61,8 @@ def send_to_push_bouncer(
|
||||||
headers.update(extra_headers)
|
headers.update(extra_headers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = requests.request(
|
res = PushBouncerSession().request(
|
||||||
method, url, data=post_data, auth=api_auth, timeout=30, verify=True, headers=headers
|
method, url, data=post_data, auth=api_auth, verify=True, headers=headers
|
||||||
)
|
)
|
||||||
except (
|
except (
|
||||||
requests.exceptions.Timeout,
|
requests.exceptions.Timeout,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
from pyoembed import PyOembedException, oEmbed
|
from pyoembed import PyOembedException, oEmbed
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,7 +10,7 @@ def get_oembed_data(
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
data = oEmbed(url, maxwidth=maxwidth, maxheight=maxheight)
|
data = oEmbed(url, maxwidth=maxwidth, maxheight=maxheight)
|
||||||
except (PyOembedException, json.decoder.JSONDecodeError):
|
except (PyOembedException, json.decoder.JSONDecodeError, requests.exceptions.ConnectionError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
oembed_resource_type = data.get("type", "")
|
oembed_resource_type = data.get("type", "")
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.utils.encoding import smart_str
|
||||||
|
|
||||||
from version import ZULIP_VERSION
|
from version import ZULIP_VERSION
|
||||||
from zerver.lib.cache import cache_with_key, get_cache_with_key, preview_url_cache_key
|
from zerver.lib.cache import cache_with_key, get_cache_with_key, preview_url_cache_key
|
||||||
|
from zerver.lib.outgoing_http import OutgoingSession
|
||||||
from zerver.lib.pysa import mark_sanitized
|
from zerver.lib.pysa import mark_sanitized
|
||||||
from zerver.lib.url_preview.oembed import get_oembed_data
|
from zerver.lib.url_preview.oembed import get_oembed_data
|
||||||
from zerver.lib.url_preview.parsers import GenericParser, OpenGraphParser
|
from zerver.lib.url_preview.parsers import GenericParser, OpenGraphParser
|
||||||
|
@ -36,6 +37,11 @@ HEADERS = {"User-Agent": ZULIP_URL_PREVIEW_USER_AGENT}
|
||||||
TIMEOUT = 15
|
TIMEOUT = 15
|
||||||
|
|
||||||
|
|
||||||
|
class PreviewSession(OutgoingSession):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(role="preview", timeout=TIMEOUT, headers=HEADERS)
|
||||||
|
|
||||||
|
|
||||||
def is_link(url: str) -> Optional[Match[str]]:
|
def is_link(url: str) -> Optional[Match[str]]:
|
||||||
return link_regex.match(smart_str(url))
|
return link_regex.match(smart_str(url))
|
||||||
|
|
||||||
|
@ -51,7 +57,7 @@ def guess_mimetype_from_content(response: requests.Response) -> str:
|
||||||
|
|
||||||
def valid_content_type(url: str) -> bool:
|
def valid_content_type(url: str) -> bool:
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, stream=True, headers=HEADERS, timeout=TIMEOUT)
|
response = PreviewSession().get(url, stream=True)
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -94,7 +100,7 @@ def get_link_embed_data(
|
||||||
if data.get("oembed"):
|
if data.get("oembed"):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
response = requests.get(mark_sanitized(url), stream=True, headers=HEADERS, timeout=TIMEOUT)
|
response = PreviewSession().get(mark_sanitized(url), stream=True)
|
||||||
if response.ok:
|
if response.ok:
|
||||||
og_data = OpenGraphParser(
|
og_data = OpenGraphParser(
|
||||||
response.content, response.headers.get("Content-Type")
|
response.content, response.headers.get("Content-Type")
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.core.management.base import CommandError
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
|
||||||
from zerver.lib.management import ZulipBaseCommand, check_config
|
from zerver.lib.management import ZulipBaseCommand, check_config
|
||||||
|
from zerver.lib.remote_server import PushBouncerSession
|
||||||
|
|
||||||
if settings.DEVELOPMENT:
|
if settings.DEVELOPMENT:
|
||||||
SECRETS_FILENAME = "zproject/dev-secrets.conf"
|
SECRETS_FILENAME = "zproject/dev-secrets.conf"
|
||||||
|
@ -82,8 +83,9 @@ class Command(ZulipBaseCommand):
|
||||||
registration_url = (
|
registration_url = (
|
||||||
settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/server/register"
|
settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/server/register"
|
||||||
)
|
)
|
||||||
|
session = PushBouncerSession()
|
||||||
try:
|
try:
|
||||||
response = requests.post(registration_url, params=request)
|
response = session.post(registration_url, params=request)
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
raise CommandError(
|
raise CommandError(
|
||||||
"Network error connecting to push notifications service "
|
"Network error connecting to push notifications service "
|
||||||
|
|
|
@ -95,6 +95,7 @@ class OembedTestCase(ZulipTestCase):
|
||||||
"width": 658,
|
"width": 658,
|
||||||
"height": 400,
|
"height": 400,
|
||||||
}
|
}
|
||||||
|
# pyoembed.providers.imgur only works with http:// URLs, not https:// (!)
|
||||||
url = "http://imgur.com/photo/158727223"
|
url = "http://imgur.com/photo/158727223"
|
||||||
reconstructed_url = reconstruct_url(url)
|
reconstructed_url = reconstruct_url(url)
|
||||||
responses.add(
|
responses.add(
|
||||||
|
@ -140,13 +141,29 @@ class OembedTestCase(ZulipTestCase):
|
||||||
self.assertEqual(data["title"], response_data["title"])
|
self.assertEqual(data["title"], response_data["title"])
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_error_request(self) -> None:
|
def test_connect_error_request(self) -> None:
|
||||||
|
url = "http://instagram.com/p/BLtI2WdAymy"
|
||||||
|
reconstructed_url = reconstruct_url(url)
|
||||||
|
responses.add(responses.GET, reconstructed_url, body=ConnectionError())
|
||||||
|
data = get_oembed_data(url)
|
||||||
|
self.assertIsNone(data)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_400_error_request(self) -> None:
|
||||||
url = "http://instagram.com/p/BLtI2WdAymy"
|
url = "http://instagram.com/p/BLtI2WdAymy"
|
||||||
reconstructed_url = reconstruct_url(url)
|
reconstructed_url = reconstruct_url(url)
|
||||||
responses.add(responses.GET, reconstructed_url, status=400)
|
responses.add(responses.GET, reconstructed_url, status=400)
|
||||||
data = get_oembed_data(url)
|
data = get_oembed_data(url)
|
||||||
self.assertIsNone(data)
|
self.assertIsNone(data)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_500_error_request(self) -> None:
|
||||||
|
url = "http://instagram.com/p/BLtI2WdAymy"
|
||||||
|
reconstructed_url = reconstruct_url(url)
|
||||||
|
responses.add(responses.GET, reconstructed_url, status=500)
|
||||||
|
data = get_oembed_data(url)
|
||||||
|
self.assertIsNone(data)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_invalid_json_in_response(self) -> None:
|
def test_invalid_json_in_response(self) -> None:
|
||||||
url = "http://instagram.com/p/BLtI2WdAymy"
|
url = "http://instagram.com/p/BLtI2WdAymy"
|
||||||
|
|
|
@ -24,6 +24,7 @@ from requests_oauthlib import OAuth2Session
|
||||||
from zerver.decorator import zulip_login_required
|
from zerver.decorator import zulip_login_required
|
||||||
from zerver.lib.actions import do_set_zoom_token
|
from zerver.lib.actions import do_set_zoom_token
|
||||||
from zerver.lib.exceptions import ErrorCode, JsonableError
|
from zerver.lib.exceptions import ErrorCode, JsonableError
|
||||||
|
from zerver.lib.outgoing_http import OutgoingSession
|
||||||
from zerver.lib.pysa import mark_sanitized
|
from zerver.lib.pysa import mark_sanitized
|
||||||
from zerver.lib.request import REQ, has_request_variables
|
from zerver.lib.request import REQ, has_request_variables
|
||||||
from zerver.lib.response import json_success
|
from zerver.lib.response import json_success
|
||||||
|
@ -33,6 +34,11 @@ from zerver.lib.validator import check_dict, check_string
|
||||||
from zerver.models import UserProfile, get_realm
|
from zerver.models import UserProfile, get_realm
|
||||||
|
|
||||||
|
|
||||||
|
class VideoCallSession(OutgoingSession):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(role="video_calls", timeout=5)
|
||||||
|
|
||||||
|
|
||||||
class InvalidZoomTokenError(JsonableError):
|
class InvalidZoomTokenError(JsonableError):
|
||||||
code = ErrorCode.INVALID_ZOOM_TOKEN
|
code = ErrorCode.INVALID_ZOOM_TOKEN
|
||||||
|
|
||||||
|
@ -218,7 +224,7 @@ def join_bigbluebutton(
|
||||||
raise JsonableError(_("BigBlueButton is not configured."))
|
raise JsonableError(_("BigBlueButton is not configured."))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = VideoCallSession().get(
|
||||||
add_query_to_redirect_url(
|
add_query_to_redirect_url(
|
||||||
settings.BIG_BLUE_BUTTON_URL + "api/create",
|
settings.BIG_BLUE_BUTTON_URL + "api/create",
|
||||||
urlencode(
|
urlencode(
|
||||||
|
|
Loading…
Reference in New Issue