Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async #3

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f3d902f
Changes for cargoo
chadgates Jan 11, 2023
47e62fa
Adding Async Support for Callbacks
chadgates Mar 10, 2024
91fe057
Adding Async Support for Callbacks
chadgates Mar 18, 2024
c2d44c2
Merge branch 'master' into async
chadgates Mar 18, 2024
2e4c253
Add Changelog entries
chadgates Mar 18, 2024
0b39518
Adding key encryption algo option to support rsaes_oaep.
chadgates Mar 18, 2024
43b84e3
Adding key encryption algo option to support rsaes_oaep.
chadgates Mar 18, 2024
e02be38
Adding key encryption algo option to support rsaes_oaep.
chadgates Mar 18, 2024
5dd639f
Formatting
chadgates Mar 18, 2024
c050829
Add pytest-asyncio testing capabilities
chadgates Mar 18, 2024
a93d4c1
Add Async callback to MDN
chadgates Mar 18, 2024
23a8c39
Async Tests
chadgates Mar 18, 2024
c292302
Adding Tests
chadgates Mar 19, 2024
625e479
Adding Tests
chadgates Mar 19, 2024
e362211
Remove obsolete checks
chadgates Mar 19, 2024
cbf0659
https://github.com/abhishek-ram/pyas2-lib/issues/60 and also making h…
chadgates Mar 20, 2024
adbd933
https://github.com/abhishek-ram/pyas2-lib/issues/60
chadgates Apr 17, 2024
0d6765a
https://github.com/abhishek-ram/pyas2-lib/issues/62
chadgates May 1, 2024
5a29efb
Asserting error messages and _encrypted_data_with_faulty_key_algo
chadgates May 2, 2024
69b13d5
Add specific error when MDN received, but Original Message was not fo…
chadgates May 3, 2024
ca98059
Merge branch 'refs/heads/feature/signing_algorithms' into async
chadgates May 3, 2024
3af9cbe
Merge branch 'refs/heads/feature/rsaes_oaep' into async
chadgates May 3, 2024
b7f56f0
Merge branch 'refs/heads/feature/message_not_found' into async
chadgates May 3, 2024
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
2 changes: 1 addition & 1 deletion AUTHORS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
- Abhishek Ram <[email protected]> @abhishek-ram
- Chad Gates @chadgates
- Wassilios Lytras @chadgates
- Bruno Ribeiro da Silva <[email protected]> @loop0
- Robin C Samuel @robincsamuel
- Brandon Joyce @brandonjoyce
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# Release History

## 1.4.4 - 2024-

* feat: added partnership lookup function
* feat: added support for async callback functions
* feat: added support for optional key encryption algorithm rsaes_oaep for encryption and decryption

## 1.4.3 - 2023-01-25

* fix: update pyopenssl version to resovle pyca/cryptography#7959
* fix: update pyopenssl version to resolve pyca/cryptography#7959

## 1.4.2 - 2022-12-11

Expand Down
124 changes: 108 additions & 16 deletions pyas2lib/as2.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Define the core functions/classes of the pyas2 package."""
import logging
import hashlib
import asyncio
import binascii
import hashlib
import inspect
import logging
import traceback
from dataclasses import dataclass
from email import encoders
from email import message as email_message
from email import message_from_bytes as parse_mime
from email import utils as email_utils
from email.mime.multipart import MIMEMultipart

from oscrypto import asymmetric

from pyas2lib.cms import (
Expand Down Expand Up @@ -564,26 +567,41 @@ def _decompress_data(self, payload):

return False, payload

def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None):
async def aparse(
self,
raw_content,
find_org_cb=None,
find_partner_cb=None,
find_message_cb=None,
find_org_partner_cb=None,
):
"""Function parses the RAW AS2 message; decrypts, verifies and
decompresses it and extracts the payload.

:param raw_content:
A byte string of the received HTTP headers followed by the body.

:param find_org_cb:
A callback the returns an Organization object if exists. The
as2-to header value is passed as an argument to it.
A conditional callback the returns an Organization object if exists. The
as2-to header value is passed as an argument to it. Must be provided
when find_partner_cb is provided and find_org_partner_cb is None

:param find_partner_cb:
A callback the returns an Partner object if exists. The
as2-from header value is passed as an argument to it.
An conditional callback the returns an Partner object if exists. The
as2-from header value is passed as an argument to it. Must be provided
when find_org_cb is provided and find_org_partner_cb is None.

:param find_message_cb:
An optional callback the returns an Message object if exists in
order to check for duplicates. The message id and partner id is
passed as arguments to it.

:param find_org_partner_cb:
A conditional callback that return Organization object and
Partner object if exist. The as2-to and as2-from header value
are passed as an argument to it. Must be provided
when find_org_cb and find_org_partner_cb is None.

:return:
A three element tuple containing (status, (exception, traceback)
, mdn). The status is a string indicating the status of the
Expand All @@ -592,6 +610,18 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None)
the partner did not request it.
"""

# Validate passed arguments
if not any(
[
find_org_cb and find_partner_cb and not find_org_partner_cb,
find_org_partner_cb and not find_partner_cb and not find_org_cb,
]
):
raise TypeError(
"Incorrect arguments passed: either find_org_cb and find_partner_cb "
"or only find_org_partner_cb must be passed."
)

# Parse the raw MIME message and extract its content and headers
status, detailed_status, exception, mdn = "processed", None, (None, None), None
self.payload = parse_mime(raw_content)
Expand All @@ -605,19 +635,42 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None)
try:
# Get the organization and partner for this transmission
org_id = unquote_as2name(as2_headers["as2-to"])
self.receiver = find_org_cb(org_id)
partner_id = unquote_as2name(as2_headers["as2-from"])

if find_org_partner_cb:
if inspect.iscoroutinefunction(find_org_partner_cb):
self.receiver, self.sender = await find_org_partner_cb(
org_id, partner_id
)
else:
self.receiver, self.sender = find_org_partner_cb(org_id, partner_id)

elif find_org_cb and find_partner_cb:
if inspect.iscoroutinefunction(find_org_cb):
self.receiver = await find_org_cb(org_id)
else:
self.receiver = find_org_cb(org_id)

if inspect.iscoroutinefunction(find_partner_cb):
self.sender = await find_partner_cb(partner_id)
else:
self.sender = find_partner_cb(partner_id)

if not self.receiver:
raise PartnerNotFound(f"Unknown AS2 organization with id {org_id}")

partner_id = unquote_as2name(as2_headers["as2-from"])
self.sender = find_partner_cb(partner_id)
if not self.sender:
raise PartnerNotFound(f"Unknown AS2 partner with id {partner_id}")

if find_message_cb and find_message_cb(self.message_id, partner_id):
raise DuplicateDocument(
"Duplicate message received, message with this ID already processed."
)
if find_message_cb:
if inspect.iscoroutinefunction(find_message_cb):
message_exists = await find_message_cb(self.message_id, partner_id)
else:
message_exists = find_message_cb(self.message_id, partner_id)
if message_exists:
raise DuplicateDocument(
"Duplicate message received, message with this ID already processed."
)

if (
self.sender.encrypt
Expand Down Expand Up @@ -738,6 +791,18 @@ def parse(self, raw_content, find_org_cb, find_partner_cb, find_message_cb=None)

return status, exception, mdn

def parse(self, *args, **kwargs):
"""
A synchronous wrapper for the asynchronous parse method.
It runs the parse coroutine in an event loop and returns the result.
"""
loop = asyncio.get_event_loop()
if loop.is_running():
raise RuntimeError(
"Cannot run synchronous parse within an already running event loop, use aparse."
)
return loop.run_until_complete(self.aparse(*args, **kwargs))


class Mdn:
"""Class for handling AS2 MDNs. Includes functions for both
Expand Down Expand Up @@ -916,7 +981,7 @@ def build(
f"content:\n {mime_to_bytes(self.payload)}"
)

def parse(self, raw_content, find_message_cb):
async def aparse(self, raw_content, find_message_cb):
"""Function parses the RAW AS2 MDN, verifies it and extracts the
processing status of the orginal AS2 message.

Expand All @@ -941,7 +1006,22 @@ def parse(self, raw_content, find_message_cb):
self.orig_message_id, orig_recipient = self.detect_mdn()

# Call the find message callback which should return a Message instance
orig_message = find_message_cb(self.orig_message_id, orig_recipient)
if inspect.iscoroutinefunction(find_message_cb):
orig_message = await find_message_cb(
self.orig_message_id, orig_recipient
)
else:
orig_message = find_message_cb(self.orig_message_id, orig_recipient)

if not orig_message:
status = "failed/Failure"
details_status = "original-message-not-found"
return status, details_status

if not orig_message:
status = "failed/Failure"
details_status = "original-message-not-found"
return status, details_status

# Extract the headers and save it
mdn_headers = {}
Expand Down Expand Up @@ -1019,6 +1099,18 @@ def parse(self, raw_content, find_message_cb):
logger.error(f"Failed to parse AS2 MDN\n: {traceback.format_exc()}")
return status, detailed_status

def parse(self, *args, **kwargs):
"""
A synchronous wrapper for the asynchronous parse method.
It runs the parse coroutine in an event loop and returns the result.
"""
loop = asyncio.get_event_loop()
if loop.is_running():
raise RuntimeError(
"Cannot run synchronous parse within an already running event loop, use aparse."
)
return loop.run_until_complete(self.aparse(*args, **kwargs))

def detect_mdn(self):
"""Function checks if the received raw message is an AS2 MDN or not.

Expand Down
2 changes: 1 addition & 1 deletion pyas2lib/cms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import zlib
from datetime import datetime, timezone

from asn1crypto import cms, core, algos
from asn1crypto import algos, cms, core
from asn1crypto.cms import SMIMECapabilityIdentifier
from oscrypto import asymmetric, symmetric, util

Expand Down
27 changes: 27 additions & 0 deletions pyas2lib/tests/test_advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,33 @@ def test_mdn_not_found(self):
self.assertEqual(status, "failed/Failure")
self.assertEqual(detailed_status, "mdn-not-found")

def test_mdn_original_message_not_found(self):
"""Test that the MDN parser raises MDN not found when a non MDN message is passed."""
self.partner.mdn_mode = as2.SYNCHRONOUS_MDN
self.out_message = as2.Message(self.org, self.partner)
self.out_message.build(self.test_data)

# Parse the generated AS2 message as the partner
raw_out_message = (
self.out_message.headers_str + b"\r\n" + self.out_message.content
)
in_message = as2.Message()
_, _, mdn = in_message.parse(
raw_out_message,
find_org_cb=self.find_org,
find_partner_cb=self.find_partner,
find_message_cb=lambda x, y: False,
)

# Parse the MDN
out_mdn = as2.Mdn()
status, detailed_status = out_mdn.parse(
mdn.headers_str + b"\r\n" + mdn.content, find_message_cb=lambda x, y: False
)

self.assertEqual(status, "failed/Failure")
self.assertEqual(detailed_status, "original-message-not-found")

def test_unsigned_mdn_sent_error(self):
"""Test the case where a signed mdn was expected but unsigned mdn was returned."""
self.partner.mdn_mode = as2.SYNCHRONOUS_MDN
Expand Down
Loading
Loading