Skip to content

Commit

Permalink
Merge pull request #2 from capcom6/feature/encryption
Browse files Browse the repository at this point in the history
[encryption] add encryption support
  • Loading branch information
capcom6 authored Feb 19, 2024
2 parents c5b5cf3 + 1f38a7a commit e7ee85c
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 58 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
Expand All @@ -25,7 +25,8 @@ jobs:
- name: Install dependencies
run: |
pipenv install --dev
pipenv sync --dev
pipenv sync --categories encryption
- name: Lint with flake8
run: pipenv run flake8 android_sms_gateway tests
Expand Down
9 changes: 6 additions & 3 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ importlib-metadata = "*"
python_version = "3"

[requests]
requests = "*"
requests = "~=2.31"

[httpx]
httpx = "*"
httpx = "~=0.26"

[aiohttp]
aiohttp = "*"
aiohttp = "~=3.9"

[encryption]
pycryptodome = "~=3.20"
73 changes: 45 additions & 28 deletions Pipfile.lock

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

28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ This is a Python client library for interfacing with the [Android SMS Gateway](h
- [aiohttp](https://pypi.org/project/aiohttp/)
- [httpx](https://pypi.org/project/httpx/)

Optional:

- [pycryptodome](https://pypi.org/project/pycryptodome/) - end-to-end encryption support

## Installation

```bash
Expand All @@ -32,27 +36,37 @@ pip install android_sms_gateway[aiohttp]
pip install android_sms_gateway[httpx]
```

## Usage
With encrypted messages support:

```bash
pip install android_sms_gateway[encryption]
```

## Quickstart

Here's an example of using the client:

```python
import asyncio
import os

from android_sms_gateway import client, domain
from android_sms_gateway import client, domain, Encryptor

login = os.getenv("ANDROID_SMS_GATEWAY_LOGIN")
password = os.getenv("ANDROID_SMS_GATEWAY_PASSWORD")

# encryptor = Encryptor('passphrase') # for end-to-end encryption, see https://sms.capcom.me/privacy/encryption/

message = domain.Message(
"Your message text here.",
["+1234567890"],
)

def sync_client():
with client.APIClient(login, password) as c:
with client.APIClient(
login,
password,
# encryptor=encryptor,
) as c:
state = c.send(message)
print(state)

Expand All @@ -61,7 +75,11 @@ def sync_client():


async def async_client():
async with client.AsyncAPIClient(login, password) as c:
async with client.AsyncAPIClient(
login,
password,
# encryptor=encryptor,
) as c:
state = await c.send(message)
print(state)

Expand Down
2 changes: 2 additions & 0 deletions android_sms_gateway/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .client import APIClient, AsyncAPIClient
from .constants import VERSION
from .domain import Message, MessageState, RecipientState
from .encryption import Encryptor
from .http import HttpClient

__all__ = (
Expand All @@ -12,6 +13,7 @@
"Message",
"MessageState",
"RecipientState",
"Encryptor",
)

__version__ = VERSION
97 changes: 78 additions & 19 deletions android_sms_gateway/client.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import abc
import base64
import dataclasses
import logging
import sys
import typing as t

from . import ahttp, domain, http
from .constants import DEFAULT_URL, VERSION
from .encryption import BaseEncryptor

logger = logging.getLogger(__name__)


class BaseClient(abc.ABC):
def __init__(
self, login: str, password: str, *, base_url: str = DEFAULT_URL
self,
login: str,
password: str,
*,
base_url: str = DEFAULT_URL,
encryptor: t.Optional[BaseEncryptor] = None,
) -> None:
credentials = base64.b64encode(f"{login}:{password}".encode("utf-8")).decode(
"utf-8"
Expand All @@ -23,6 +30,44 @@ def __init__(
"User-Agent": f"android-sms-gateway/{VERSION} (client; python {sys.version_info.major}.{sys.version_info.minor})",
}
self.base_url = base_url.rstrip("/")
self.encryptor = encryptor

def _encrypt(self, message: domain.Message) -> domain.Message:
if self.encryptor is None:
return message

if message.is_encrypted:
raise ValueError("Message is already encrypted")

message = dataclasses.replace(
message,
is_encrypted=True,
message=self.encryptor.encrypt(message.message),
phone_numbers=[
self.encryptor.encrypt(phone) for phone in message.phone_numbers
],
)

return message

def _decrypt(self, state: domain.MessageState) -> domain.MessageState:
if state.is_encrypted and self.encryptor is None:
raise ValueError("Message is encrypted but encryptor is not set")

if self.encryptor is None:
return state

return dataclasses.replace(
state,
recipients=[
dataclasses.replace(
recipient,
phone_number=self.encryptor.decrypt(recipient.phone_number),
)
for recipient in state.recipients
],
is_encrypted=False,
)


class APIClient(BaseClient):
Expand All @@ -32,10 +77,11 @@ def __init__(
password: str,
*,
base_url: str = DEFAULT_URL,
http_client: t.Optional[http.HttpClient] = None,
encryptor: t.Optional[BaseEncryptor] = None,
http: t.Optional[http.HttpClient] = None,
) -> None:
super().__init__(login, password, base_url=base_url)
self.http = http_client
super().__init__(login, password, base_url=base_url, encryptor=encryptor)
self.http = http

def __enter__(self):
if self.http is not None:
Expand All @@ -50,17 +96,22 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self.http = None

def send(self, message: domain.Message) -> domain.MessageState:
return domain.MessageState.from_dict(
self.http.post(
f"{self.base_url}/message",
payload=message.asdict(),
headers=self.headers,
message = self._encrypt(message)
return self._decrypt(
domain.MessageState.from_dict(
self.http.post(
f"{self.base_url}/message",
payload=message.asdict(),
headers=self.headers,
)
)
)

def get_state(self, _id: str) -> domain.MessageState:
return domain.MessageState.from_dict(
self.http.get(f"{self.base_url}/message/{_id}", headers=self.headers)
return self._decrypt(
domain.MessageState.from_dict(
self.http.get(f"{self.base_url}/message/{_id}", headers=self.headers)
)
)


Expand All @@ -71,9 +122,10 @@ def __init__(
password: str,
*,
base_url: str = DEFAULT_URL,
encryptor: t.Optional[BaseEncryptor] = None,
http_client: t.Optional[ahttp.AsyncHttpClient] = None,
) -> None:
super().__init__(login, password, base_url=base_url)
super().__init__(login, password, base_url=base_url, encryptor=encryptor)
self.http = http_client

async def __aenter__(self):
Expand All @@ -89,15 +141,22 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
self.http = None

async def send(self, message: domain.Message) -> domain.MessageState:
return domain.MessageState.from_dict(
await self.http.post(
f"{self.base_url}/message",
payload=message.asdict(),
headers=self.headers,
message = self._encrypt(message)
return self._decrypt(
domain.MessageState.from_dict(
await self.http.post(
f"{self.base_url}/message",
payload=message.asdict(),
headers=self.headers,
)
)
)

async def get_state(self, _id: str) -> domain.MessageState:
return domain.MessageState.from_dict(
await self.http.get(f"{self.base_url}/message/{_id}", headers=self.headers)
return self._decrypt(
domain.MessageState.from_dict(
await self.http.get(
f"{self.base_url}/message/{_id}", headers=self.headers
)
)
)
Loading

0 comments on commit e7ee85c

Please sign in to comment.