Skip to content

Commit

Permalink
Merge pull request #195 from nnsnodnb/voip-cert-key
Browse files Browse the repository at this point in the history
Support cert & key separated for VoIP client
  • Loading branch information
nnsnodnb authored Jan 24, 2024
2 parents fe07cb3 + 06199e0 commit 84cecc4
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 23 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ asyncio.run(
import asyncio
from pathlib import Path

from kalyke import VoIPApnsConfig, ApnsPushType, VoIPClient
from kalyke import VoIPApnsConfig, VoIPClient

client = VoIPClient(
use_sandbox=True,
Expand Down
4 changes: 2 additions & 2 deletions examples/voip.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import asyncio
from pathlib import Path

from kalyke import ApnsConfig, ApnsPushType, VoIPClient
from kalyke import VoIPApnsConfig, VoIPClient

client = VoIPClient(
use_sandbox=True,
auth_key_filepath=Path("/") / "path" / "to" / "YOUR_VOIP_CERTIFICATE.pem",
)

payload = {"key": "value"}
config = ApnsConfig(topic="com.example.App.voip", push_type=ApnsPushType.VOIP)
config = VoIPApnsConfig(topic="com.example.App.voip")

# Send single VoIP notification

Expand Down
8 changes: 8 additions & 0 deletions kalyke/clients/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import importlib
import urllib.parse
from pathlib import Path
from typing import Any, Dict, Union

from httpx import AsyncClient, Response
Expand All @@ -10,6 +11,7 @@

class __Client(object):
use_sandbox: bool
auth_key_filepath: Union[str, Path]

async def send_message(
self,
Expand Down Expand Up @@ -55,3 +57,9 @@ def _handle_error(self, error_json: Dict[str, Any]) -> ApnsProviderException:
exception_class = getattr(exceptions_module, reason)

return exception_class(error=error_json)

def _get_auth_key_filepath(self) -> Path:
if isinstance(self.auth_key_filepath, Path):
return self.auth_key_filepath
else:
return Path(self.auth_key_filepath)
11 changes: 2 additions & 9 deletions kalyke/clients/apns.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,12 @@
from . import __Client as BaseClient


@dataclass
@dataclass(frozen=True)
class ApnsClient(BaseClient):
use_sandbox: bool
team_id: str
auth_key_id: str
auth_key_filepath: Union[str, Path]
_auth_key_filepath: Path = field(init=False)

def __post_init__(self):
if isinstance(self.auth_key_filepath, Path):
self._auth_key_filepath = self.auth_key_filepath
else:
self._auth_key_filepath = Path(self.auth_key_filepath)

def _init_client(self, apns_config: ApnsConfig) -> AsyncClient:
headers = apns_config.make_headers()
Expand All @@ -31,7 +24,7 @@ def _init_client(self, apns_config: ApnsConfig) -> AsyncClient:
return client

def _make_authorization(self) -> str:
auth_key = self._auth_key_filepath.read_text()
auth_key = self._get_auth_key_filepath().read_text()
token = jwt.encode(
payload={
"iss": self.team_id,
Expand Down
2 changes: 1 addition & 1 deletion kalyke/clients/live_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .apns import ApnsClient


@dataclass
@dataclass(frozen=True)
class LiveActivityClient(ApnsClient):
async def send_message(
self,
Expand Down
22 changes: 13 additions & 9 deletions kalyke/clients/voip.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, Union
from typing import Any, Dict, Optional, Union

import httpx
from httpx import AsyncClient
Expand All @@ -9,17 +10,16 @@
from . import __Client as BaseClient


@dataclass
@dataclass(frozen=True)
class VoIPClient(BaseClient):
use_sandbox: bool
auth_key_filepath: Union[str, Path]
_auth_key_filepath: Path = field(init=False)
key_filepath: Optional[Union[str, Path]] = field(default=None)
password: Optional[str] = field(default=None)

def __post_init__(self):
if isinstance(self.auth_key_filepath, Path):
self._auth_key_filepath = self.auth_key_filepath
else:
self._auth_key_filepath = Path(self.auth_key_filepath)
def __post_init__(self) -> None:
if self.key_filepath is None and self.password is not None:
warnings.warn(UserWarning("password is ignored because key_filepath is None."))

async def send_message(
self,
Expand All @@ -36,6 +36,10 @@ async def send_message(
def _init_client(self, apns_config: VoIPApnsConfig) -> AsyncClient:
headers = apns_config.make_headers()
context = httpx.create_ssl_context()
context.load_cert_chain(self._auth_key_filepath)
context.load_cert_chain(
certfile=self._get_auth_key_filepath(),
keyfile=self.key_filepath,
password=self.password,
)
client = AsyncClient(headers=headers, verify=context, http2=True)
return client
19 changes: 18 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pytest-cov = "^4.0.0"
coveralls = "^3.3.1"
pytest-httpx = ">=0.21.3,<0.29.0"
pytest-asyncio = ">=0.20.3,<0.24.0"
pem = "^23.1.0"

[tool.poetry-dynamic-versioning]
enable = true
Expand Down
22 changes: 22 additions & 0 deletions tests/clients/client/test_get_auth_key_filepath.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pathlib import Path

import pytest

from kalyke.clients import __Client


@pytest.mark.parametrize(
"auth_key_filepath",
[
Path(__file__).parent.parent / "dummy.p8",
"../dummy.p8",
],
ids=["Path object", "str"],
)
def test_get_auth_key_filepath(auth_key_filepath):
client = __Client()
client.auth_key_filepath = auth_key_filepath

actual = client._get_auth_key_filepath()

assert isinstance(actual, Path)
54 changes: 54 additions & 0 deletions tests/clients/voip/test_init.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import tempfile
from pathlib import Path

import pem
import pytest

from kalyke import VoIPClient


Expand All @@ -17,3 +23,51 @@ def test_initialize_with_str(auth_key_filepath):
)

assert isinstance(client, VoIPClient)


def test_initialize_with_key_filepath(auth_key_filepath):
cer, key = pem.parse_file(auth_key_filepath)
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
cer_path = tmpdir_path / "cer.pem"
cer_path.write_bytes(cer.as_bytes())
key_path = tmpdir_path / "key.pem"
key_path.write_bytes(key.as_bytes())

client = VoIPClient(
use_sandbox=True,
auth_key_filepath=cer_path,
key_filepath=key_path,
)

assert isinstance(client, VoIPClient)


def test_initialize_with_key_filepath_and_password(auth_key_filepath):
cer, key = pem.parse_file(auth_key_filepath)
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
cer_path = tmpdir_path / "cer.pem"
cer_path.write_bytes(cer.as_bytes())
key_path = tmpdir_path / "key.pem"
key_path.write_bytes(key.as_bytes())

client = VoIPClient(
use_sandbox=True,
auth_key_filepath=cer_path,
key_filepath=key_path,
password="password",
)

assert isinstance(client, VoIPClient)


def test_initialize_with_password(auth_key_filepath):
with pytest.warns(UserWarning, match="password is ignored because key_filepath is None."):
client = VoIPClient(
use_sandbox=True,
auth_key_filepath=auth_key_filepath,
password="password",
)

assert isinstance(client, VoIPClient)

0 comments on commit 84cecc4

Please sign in to comment.