Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Jul 21, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/408.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix issue where GCM pushkins behind proxy fail to start.
7 changes: 6 additions & 1 deletion scripts-dev/proxy-test/curl.sh
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
curl -i -H "Content-Type: application/json" --request POST -d @notification.json http://localhost:5000/_matrix/push/v1/notify
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <notification_file>"
exit 1
fi

curl -i -H "Content-Type: application/json" --request POST -d @$1 http://localhost:5000/_matrix/push/v1/notify
3 changes: 2 additions & 1 deletion scripts-dev/proxy-test/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ services:
- ./sygnal.yaml:/sygnal.yaml
- ./service_account.json:/service_account.json:ro
- ./curl.sh:/curl.sh
- ./notification.json:/notification.json
- ./notification-gcm.json:/notification-gcm.json
- ./notification-ios.json:/notification-ios.json
- ./proxy.conf:/etc/apt/apt.conf.d/proxy.conf
ports:
- 5000:5000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"devices": [
{
"app_id": "im.vector.app",
"pushkey": "<PUSHKEY HERE>",
"pushkey": "aaaa",
"pushkey_ts": 12345678,
"data": {},
"tweaks": {
Expand Down
31 changes: 31 additions & 0 deletions scripts-dev/proxy-test/notification-ios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"notification": {
"event_id": "\\$3957tyerfgewrf384",
"room_id": "!slw48wfj34rtnrf:example.org",
"type": "m.room.message",
"sender": "@exampleuser:example.org",
"sender_display_name": "Major Tom",
"room_name": "Mission Control",
"room_alias": "#exampleroom:example.org",
"prio": "high",
"content": {
"msgtype": "m.text",
"body": "I'm floating in a most peculiar way."
},
"counts": {
"unread": 2,
"missed_calls": 1
},
"devices": [
{
"app_id": "im.vector.app.ios",
"pushkey": "aaaa",
"pushkey_ts": 12345678,
"data": {},
"tweaks": {
"sound": "bing"
}
}
]
}
}
6 changes: 6 additions & 0 deletions scripts-dev/proxy-test/sygnal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,9 @@ apps:
api_version: v1
project_id: <PROJECT_ID>
service_account_file: /service_account.json
im.vector.app.ios:
type: apns
keyfile: key.p8
key_id: asdf
team_id: team
topic: topic
51 changes: 24 additions & 27 deletions sygnal/apnspushkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,39 +168,36 @@ def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None:
# use the Sygnal global proxy configuration
proxy_url_str = sygnal.config.get("proxy")

loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
if proxy_url_str:
# this overrides the create_connection method to use a HTTP proxy
loop = ProxyingEventLoopWrapper(loop, proxy_url_str) # type: ignore

async def make_apns() -> aioapns.APNs:
if certfile is not None:
# max_connection_attempts is actually the maximum number of
# additional connection attempts, so =0 means try once only
# (we will retry at a higher level so not worth doing more here)
apns_client = APNs(
client_cert=certfile,
use_sandbox=self.use_sandbox,
max_connection_attempts=0,
)
if certfile is not None:
# max_connection_attempts is actually the maximum number of
# additional connection attempts, so =0 means try once only
# (we will retry at a higher level so not worth doing more here)
apns_client = APNs(
client_cert=certfile,
use_sandbox=self.use_sandbox,
max_connection_attempts=0,
)

self._report_certificate_expiration(certfile)
self._report_certificate_expiration(certfile)

return apns_client
else:
# max_connection_attempts is actually the maximum number of
# additional connection attempts, so =0 means try once only
# (we will retry at a higher level so not worth doing more here)
return APNs(
key=self.get_config("keyfile", str),
key_id=self.get_config("key_id", str),
team_id=self.get_config("team_id", str),
topic=self.get_config("topic", str),
use_sandbox=self.use_sandbox,
max_connection_attempts=0,
)

self.apns_client = loop.run_until_complete(make_apns())
self.apns_client = apns_client
else:
# max_connection_attempts is actually the maximum number of
# additional connection attempts, so =0 means try once only
# (we will retry at a higher level so not worth doing more here)
self.apns_client = APNs(
key=self.get_config("keyfile", str),
key_id=self.get_config("key_id", str),
team_id=self.get_config("team_id", str),
topic=self.get_config("topic", str),
use_sandbox=self.use_sandbox,
max_connection_attempts=0,
)

push_type = self.get_config("push_type", str)
if not push_type:
Expand Down
21 changes: 1 addition & 20 deletions sygnal/gcmpushkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,6 @@ def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None:
f"`service_account_file` must be valid: {str(e)}",
)

# This is instantiated in `self.create`
self.google_auth_request: google.auth.transport._aiohttp_requests.Request

# Use the fcm_options config dictionary as a foundation for the body;
# this lets the Sygnal admin choose custom FCM options
# (e.g. content_available).
Expand All @@ -221,35 +218,19 @@ def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None:
"Config field fcm_options, if set, must be a dictionary of options"
)

@classmethod
async def create(
cls, name: str, sygnal: "Sygnal", config: Dict[str, Any]
) -> "GcmPushkin":
"""
Override this if your pushkin needs to call async code in order to
be constructed. Otherwise, it defaults to just invoking the Python-standard
__init__ constructor.

Returns:
an instance of this Pushkin
"""
session = None
proxy_url = sygnal.config.get("proxy")
if proxy_url:
# `ClientSession` can't directly take the proxy URL, so we need to
# set the usual env var and use `trust_env=True`
os.environ["HTTPS_PROXY"] = proxy_url

# ClientSession must be instantiated by an async function, hence we do this
# here instead of `__init__`.
session = aiohttp.ClientSession(trust_env=True, auto_decompress=False)

cls.google_auth_request = google.auth.transport._aiohttp_requests.Request(
self.google_auth_request = google.auth.transport._aiohttp_requests.Request(
session=session
)

return cls(name, sygnal, config)

async def _perform_http_request(
self, body: Dict[str, Any], headers: Dict[AnyStr, List[AnyStr]]
) -> Tuple[IResponse, str]:
Expand Down
2 changes: 1 addition & 1 deletion sygnal/helper/proxy/proxy_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def __init__(
self._sslcontext = sslcontext

# asyncio EventLoop
self._event_loop = loop or asyncio.get_event_loop()
self._event_loop = loop or asyncio.get_running_loop()

# This future is completed when it is safe to take back control of the
# transport.
Expand Down
23 changes: 16 additions & 7 deletions sygnal/sygnal.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
import logging.config
import os
import sys
from typing import Any, Dict, Generator, Set, cast
from typing import Any, Dict, Set, cast

import opentracing
import prometheus_client
import yaml
from opentracing import Tracer
from opentracing.scope_managers.asyncio import AsyncioScopeManager
from twisted.internet import asyncioreactor, defer
from twisted.internet.defer import Deferred, ensureDeferred
from twisted.internet import asyncioreactor
from twisted.internet.defer import ensureDeferred
from twisted.internet.interfaces import (
IReactorCore,
IReactorFDSet,
Expand All @@ -42,6 +42,7 @@

from sygnal.http import PushGatewayApiServer
from sygnal.notifications import Pushkin
from sygnal.utils import twisted_sleep

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -222,10 +223,18 @@ def run(self) -> None:
Attempt to run Sygnal and then exit the application.
"""

@defer.inlineCallbacks
def start() -> Generator[Deferred[Any], Any, Any]:
async def start():
# NOTE: This sleep may seem odd to you, but it is in fact necessary.
# Without this sleep, the code following it will run before Twisted has had
# a chance to fully setup the asyncio event loop.
# Specifically, `callWhenRunning` runs the functions
# before the asyncio event loop has started running.
# ie. asyncio.get_running_loop() will throw because of no running loop.
# Calling twisted_sleep is enough to kickstart Twisted into setting up the
# asyncio event loop for future usage.
await twisted_sleep(0, self.reactor)
try:
yield ensureDeferred(self.make_pushkins_then_start())
await self.make_pushkins_then_start()
except Exception:
# Print the exception and bail out.
print("Error during startup:", file=sys.stderr)
Expand All @@ -236,7 +245,7 @@ def start() -> Generator[Deferred[Any], Any, Any]:
if self.reactor.running:
self.reactor.stop()

self.reactor.callWhenRunning(start)
self.reactor.callWhenRunning(lambda: ensureDeferred(start()))
self.reactor.run()


Expand Down
10 changes: 5 additions & 5 deletions tests/test_httpproxy_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def test_connect_no_credentials(self):
)

# advance event loop because we have to let coroutines be executed
self.loop.advance(1.0)
cast(TimelessEventLoopWrapper, self.loop).advance(1.0)

# *now* we should have switched over from the HTTP CONNECT protocol
# to the user protocol (in our case, a MockProtocol).
Expand Down Expand Up @@ -142,7 +142,7 @@ def test_connect_correct_credentials(self):
)

# advance event loop because we have to let coroutines be executed
self.loop.advance(1.0)
cast(TimelessEventLoopWrapper, self.loop).advance(1.0)

# *now* we should have switched over from the HTTP CONNECT protocol
# to the user protocol (in our case, a MockProtocol).
Expand Down Expand Up @@ -187,7 +187,7 @@ def test_connect_failure(self):
)

# advance event loop because we have to let coroutines be executed
self.loop.advance(1.0)
cast(TimelessEventLoopWrapper, self.loop).advance(1.0)

# *now* this future should have completed
self.assertTrue(switch_over_task.done())
Expand Down Expand Up @@ -301,7 +301,7 @@ def test_connect_no_credentials(self):
)

# Advance event loop because we have to let coroutines be executed
self.loop.advance(1.0)
cast(TimelessEventLoopWrapper, self.loop).advance(1.0)
server_loop.advance(1.0)

# We manually copy the bytes between the fake_proxy transport and our
Expand Down Expand Up @@ -329,7 +329,7 @@ def test_connect_no_credentials(self):
fake_proxy.pretend_to_receive(server_transport.buffer)
server_transport.buffer = b""

self.loop.advance(1.0)
cast(TimelessEventLoopWrapper, self.loop).advance(1.0)

# *now* we should have switched over from the HTTP CONNECT protocol
# to the user protocol (in our case, a MockProtocol).
Expand Down
10 changes: 10 additions & 0 deletions tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import json
from io import BytesIO
from threading import Condition
Expand All @@ -32,6 +33,8 @@

from sygnal.sygnal import CONFIG_DEFAULTS, Sygnal, merge_left_with_defaults

from tests.asyncio_test_helpers import TimelessEventLoopWrapper

REQ_PATH = b"/_matrix/push/v1/notify"


Expand Down Expand Up @@ -73,7 +76,14 @@ def setUp(self):

config = {"apps": {}, "log": logging_config}

self.loop: Union[asyncio.AbstractEventLoop, TimelessEventLoopWrapper] = (
asyncio.new_event_loop()
)
self.config_setup(config)
# Manually set the running loop after calling config_setup since self.loop
# can be modified inside config_setup.
# asyncio doesn't set this itself for some reason when calling `set_event_loop`.
asyncio._set_running_loop(self.loop)

config = merge_left_with_defaults(CONFIG_DEFAULTS, config)

Expand Down