-
-
Notifications
You must be signed in to change notification settings - Fork 0
Add/adapters #1
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
Merged
Merged
Add/adapters #1
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
cd22058
add `IPv6HTTPAdapter`, refactor `IPv4HTTPAdapter`
AHReccese 541f646
add testcases for both adapters
AHReccese 1bceb09
minor refactoring
AHReccese 04e5dd2
update `pytest` command
AHReccese dfa3f6b
expose both adapters
AHReccese 1bc2124
update `README.md`
AHReccese 4b51566
update `CHANGELOG.md`
AHReccese afb92bf
update test
AHReccese 07eb29e
update `.coveragerc`
AHReccese 34d8338
apply suggested renamings
AHReccese c56b95d
update `CHANGELOG.md`
AHReccese ae9805d
update `CHANGELOG.md`
AHReccese b64b023
fix docstring issues
AHReccese cf89d7a
refactor IPv4 and IPv6 adapters to override send method for DNS resol…
AHReccese 99e7cd1
enhance tests for IPv4 and IPv6 adapters to verify address filtering …
AHReccese 53df88b
update os image, add python 3.14, and update checkout and install pyt…
AHReccese 631fd18
update GitHub Actions workflow to disable CI failure on Codecov error…
AHReccese 5c07f94
update name
AHReccese 1727fa9
`README.md` updated
AHReccese 28d464e
update namings
AHReccese 22abdae
update versions
AHReccese 3855af4
update naming from `set_up` to `setup`.
AHReccese 33a2826
`README.md` updated
AHReccese File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """ipforce modules.""" | ||
| from .params import IPFORCE_VERSION | ||
| from .adapters import IPv4HTTPAdapter | ||
| from .adapters import IPv4TransportAdapter, IPv6TransportAdapter | ||
|
|
||
| __version__ = IPFORCE_VERSION |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,61 +1,65 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """IPForce Adapters to force IPv4 or IPv6 for requests.""" | ||
| import socket | ||
| from typing import List, Tuple | ||
| from urllib3 import PoolManager | ||
| from typing import Any, List, Tuple | ||
| from requests.adapters import HTTPAdapter | ||
| from requests.sessions import Session | ||
|
|
||
| class IPv4HTTPAdapter(HTTPAdapter): | ||
|
|
||
| class IPv4TransportAdapter(HTTPAdapter): | ||
| """A custom HTTPAdapter that enforces the use of IPv4 for DNS resolution during HTTP(S) requests using the requests library.""" | ||
|
|
||
| def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None: | ||
| """ | ||
| Initialize the connection pool manager using a temporary override of socket.getaddrinfo to ensure only IPv4 addresses are used. | ||
| This is necessary to ensure that the requests library uses IPv4 addresses for DNS resolution, which is required for some APIs. | ||
| :param connections: the number of connection pools to cache | ||
| :param maxsize: the maximum number of connections to save in the pool | ||
| :param block: whether the connections should block when reaching the max size | ||
| :param kwargs: additional keyword arguments for the PoolManager | ||
| def send(self, *args: list, **kwargs: dict) -> Any: | ||
| """ | ||
| self.poolmanager = PoolManager( | ||
| num_pools=connections, | ||
| maxsize=maxsize, | ||
| block=block, | ||
| socket_options=self._ipv4_socket_options(), | ||
| **kwargs | ||
| ) | ||
|
|
||
| def _ipv4_socket_options(self) -> list: | ||
| """ | ||
| Temporarily patches socket.getaddrinfo to filter only IPv4 addresses (AF_INET). | ||
| Override send method to apply the monkey patch only during the request. | ||
|
|
||
| :return: an empty list of socket options; DNS patching occurs here | ||
| :param args: additional list arguments for the send method | ||
| :param kwargs: additional keyword arguments for the send method | ||
| """ | ||
| original_getaddrinfo = socket.getaddrinfo | ||
|
|
||
| def ipv4_only_getaddrinfo(*args: list, **kwargs: dict) -> List[Tuple]: | ||
| results = original_getaddrinfo(*args, **kwargs) | ||
| def ipv4_only_getaddrinfo(*gargs: list, **gkwargs: dict) -> List[Tuple]: | ||
| """ | ||
| Filter getaddrinfo to return only IPv4 addresses. | ||
|
|
||
| :param gargs: additional list arguments for the original_getaddrinfo function | ||
| :param gkwargs: additional keyword arguments for the original_getaddrinfo function | ||
| """ | ||
| results = original_getaddrinfo(*gargs, **gkwargs) | ||
| return [res for res in results if res[0] == socket.AF_INET] | ||
|
|
||
| self._original_getaddrinfo = socket.getaddrinfo | ||
| socket.getaddrinfo = ipv4_only_getaddrinfo | ||
| try: | ||
| response = super().send(*args, **kwargs) | ||
| finally: | ||
| socket.getaddrinfo = original_getaddrinfo | ||
| return response | ||
|
|
||
| return [] | ||
|
|
||
| def __del__(self) -> None: | ||
| """Restores the original socket.getaddrinfo function upon adapter deletion.""" | ||
| if hasattr(self, "_original_getaddrinfo"): | ||
| socket.getaddrinfo = self._original_getaddrinfo | ||
| class IPv6TransportAdapter(HTTPAdapter): | ||
| """A custom HTTPAdapter that enforces the use of IPv6 for DNS resolution during HTTP(S) requests using the requests library.""" | ||
|
|
||
| @staticmethod | ||
| def get_ipv4_enforced_session() -> Session: | ||
| def send(self, *args: list, **kwargs: dict) -> Any: | ||
| """ | ||
| Returns a requests.Session with IPv4HTTPAdapter mounted for both HTTP and HTTPS. | ||
| All requests made with this session will use IPv4 for DNS resolution. | ||
| Override send method to apply the monkey patch only during the request. | ||
|
|
||
| :return: requests.Session object with IPv4 enforced | ||
| :param args: additional list arguments for the send method | ||
| :param kwargs: additional keyword arguments for the send method | ||
| """ | ||
| session = Session() | ||
| adapter = IPv4HTTPAdapter() | ||
| session.mount("http://", adapter) | ||
| session.mount("https://", adapter) | ||
| return session | ||
| original_getaddrinfo = socket.getaddrinfo | ||
|
|
||
| def ipv6_only_getaddrinfo(*gargs: list, **gkwargs: dict) -> List[Tuple]: | ||
| """ | ||
| Filter getaddrinfo to return only IPv6 addresses. | ||
|
|
||
| :param gargs: additional list arguments for the original_getaddrinfo function | ||
| :param gkwargs: additional keyword arguments for the original_getaddrinfo function | ||
| """ | ||
| results = original_getaddrinfo(*gargs, **gkwargs) | ||
| return [res for res in results if res[0] == socket.AF_INET6] | ||
|
|
||
| socket.getaddrinfo = ipv6_only_getaddrinfo | ||
| try: | ||
| response = super().send(*args, **kwargs) | ||
| finally: | ||
| socket.getaddrinfo = original_getaddrinfo | ||
| return response |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,4 +4,3 @@ | |
| IPFORCE_VERSION = "0.1" | ||
| IPFORCE_OVERVIEW = '''OVERVIEW''' | ||
| IPFORCE_REPO = "https://github.com/openscilab/ipforce" | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| import unittest | ||
| import socket | ||
| from unittest.mock import patch, MagicMock | ||
| from ipforce.adapters import IPv4TransportAdapter, IPv6TransportAdapter | ||
|
|
||
|
|
||
| class TestIPv4Adapter(unittest.TestCase): | ||
| """Test cases for IPv4TransportAdapter.""" | ||
|
|
||
| def set_up(self): | ||
|
AHReccese marked this conversation as resolved.
Outdated
|
||
| """Set up test fixtures.""" | ||
| self.adapter = IPv4TransportAdapter() | ||
|
|
||
| def test_ipv4_filtering_during_send(self): | ||
| """Test that IPv4 adapter filters only IPv4 addresses during send.""" | ||
| self.set_up() | ||
| mock_results = [ | ||
| (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 | ||
| (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 | ||
| (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('10.0.0.1', 80)), # IPv4 | ||
| ] | ||
|
|
||
| original_getaddrinfo = socket.getaddrinfo | ||
| captured_results = [] | ||
|
|
||
| def mock_super_send(*args, **kwargs): | ||
| # Capture the filtered results during send | ||
| captured_results.extend(socket.getaddrinfo('example.com', 80)) | ||
| return MagicMock() | ||
|
|
||
| with patch('socket.getaddrinfo', return_value=mock_results): | ||
| with patch.object(IPv4TransportAdapter.__bases__[0], 'send', mock_super_send): | ||
| self.adapter.send(MagicMock()) | ||
|
|
||
| # Only IPv4 results should be captured | ||
| self.assertEqual(len(captured_results), 2) | ||
| for result in captured_results: | ||
| self.assertEqual(result[0], socket.AF_INET) | ||
|
|
||
| def test_cleanup_after_send(self): | ||
| """Test that the adapter properly restores original getaddrinfo after send.""" | ||
| self.set_up() | ||
| original_getaddrinfo = socket.getaddrinfo | ||
|
|
||
| with patch.object(IPv4TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): | ||
| self.adapter.send(MagicMock()) | ||
|
|
||
| # Verify it was restored after send | ||
| self.assertEqual(socket.getaddrinfo, original_getaddrinfo) | ||
|
|
||
| def test_cleanup_on_exception(self): | ||
| """Test that the adapter restores original getaddrinfo even if send raises.""" | ||
| self.set_up() | ||
| original_getaddrinfo = socket.getaddrinfo | ||
|
|
||
| with patch.object(IPv4TransportAdapter.__bases__[0], 'send', side_effect=Exception("Test error")): | ||
| with self.assertRaises(Exception): | ||
| self.adapter.send(MagicMock()) | ||
|
|
||
| # Verify it was restored even after exception | ||
| self.assertEqual(socket.getaddrinfo, original_getaddrinfo) | ||
|
|
||
|
|
||
| class TestIPv6Adapter(unittest.TestCase): | ||
| """Test cases for IPv6TransportAdapter.""" | ||
|
|
||
| def set_up(self): | ||
|
AHReccese marked this conversation as resolved.
Outdated
|
||
| """Set up test fixtures.""" | ||
| self.adapter = IPv6TransportAdapter() | ||
|
|
||
| def test_ipv6_filtering_during_send(self): | ||
| """Test that IPv6 adapter filters only IPv6 addresses during send.""" | ||
| self.set_up() | ||
| mock_results = [ | ||
| (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 | ||
| (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 | ||
| (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('2001:db8::1', 80)), # IPv6 | ||
| ] | ||
|
|
||
| captured_results = [] | ||
|
|
||
| def mock_super_send(*args, **kwargs): | ||
| # Capture the filtered results during send | ||
| captured_results.extend(socket.getaddrinfo('example.com', 80)) | ||
| return MagicMock() | ||
|
|
||
| with patch('socket.getaddrinfo', return_value=mock_results): | ||
| with patch.object(IPv6TransportAdapter.__bases__[0], 'send', mock_super_send): | ||
| self.adapter.send(MagicMock()) | ||
|
|
||
| # Only IPv6 results should be captured | ||
| self.assertEqual(len(captured_results), 2) | ||
| for result in captured_results: | ||
| self.assertEqual(result[0], socket.AF_INET6) | ||
|
|
||
| def test_cleanup_after_send(self): | ||
| """Test that the adapter properly restores original getaddrinfo after send.""" | ||
| self.set_up() | ||
| original_getaddrinfo = socket.getaddrinfo | ||
|
|
||
| with patch.object(IPv6TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): | ||
| self.adapter.send(MagicMock()) | ||
|
|
||
| # Verify it was restored after send | ||
| self.assertEqual(socket.getaddrinfo, original_getaddrinfo) | ||
|
|
||
| def test_cleanup_on_exception(self): | ||
| """Test that the adapter restores original getaddrinfo even if send raises.""" | ||
| self.set_up() | ||
| original_getaddrinfo = socket.getaddrinfo | ||
|
|
||
| with patch.object(IPv6TransportAdapter.__bases__[0], 'send', side_effect=Exception("Test error")): | ||
| with self.assertRaises(Exception): | ||
| self.adapter.send(MagicMock()) | ||
|
|
||
| # Verify it was restored even after exception | ||
| self.assertEqual(socket.getaddrinfo, original_getaddrinfo) | ||
|
|
||
|
|
||
| class TestAdapterIntegration(unittest.TestCase): | ||
| """Integration tests for both adapters.""" | ||
|
|
||
| def test_both_adapters_independent(self): | ||
| """Test that both adapters can coexist without interference.""" | ||
| ipv4_adapter = IPv4TransportAdapter() | ||
| ipv6_adapter = IPv6TransportAdapter() | ||
| original_getaddrinfo = socket.getaddrinfo | ||
|
|
||
| with patch.object(IPv4TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): | ||
| ipv4_adapter.send(MagicMock()) | ||
|
|
||
| with patch.object(IPv6TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): | ||
| ipv6_adapter.send(MagicMock()) | ||
|
|
||
| # Verify original is still intact | ||
| self.assertEqual(socket.getaddrinfo, original_getaddrinfo) | ||
|
|
||
| def test_sequential_sends_restore_correctly(self): | ||
| """Test that multiple sequential sends properly restore getaddrinfo.""" | ||
| adapter = IPv4TransportAdapter() | ||
| original_getaddrinfo = socket.getaddrinfo | ||
|
|
||
| with patch.object(IPv4TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): | ||
| adapter.send(MagicMock()) | ||
| adapter.send(MagicMock()) | ||
| adapter.send(MagicMock()) | ||
|
|
||
| # Verify original is still intact after multiple sends | ||
| self.assertEqual(socket.getaddrinfo, original_getaddrinfo) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.