From f1bbbd0050f5c573f07481dcf621ad98739434ed Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Tue, 24 Feb 2026 10:33:52 +0100 Subject: [PATCH 01/17] Use ConfigParser --- libtaxii/scripts/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libtaxii/scripts/__init__.py b/libtaxii/scripts/__init__.py index c6574fa..76facf7 100644 --- a/libtaxii/scripts/__init__.py +++ b/libtaxii/scripts/__init__.py @@ -9,7 +9,7 @@ import datetime import libtaxii.clients as tc import six -from six.moves.configparser import SafeConfigParser +from configparser import ConfigParser from six.moves.urllib.parse import urlparse import libtaxii as t @@ -71,7 +71,7 @@ def add_poll_response_args(parser): "to \'clobber\'") -class ArgParserConfig(SafeConfigParser): +class ArgParserConfig(ConfigParser): def as_args(self, section, raw=False, vars=None): """ @@ -102,7 +102,7 @@ class LoadFromFile(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): config_file = ArgParserConfig() with values as f: - config_file.readfp(f) + config_file.read_file(f) # Overrides initial values if present on config file parser.parse_known_args(config_file.as_args('libtaxii'), namespace=namespace) From d79eb4b8f41bb436be6a5392a8c4965e3ab085ac Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Thu, 26 Feb 2026 14:52:14 +0100 Subject: [PATCH 02/17] Added tests for client --- libtaxii/test/test_message_from_response.py | 74 +++++++++++++++++++++ requirements.txt | 3 +- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 libtaxii/test/test_message_from_response.py diff --git a/libtaxii/test/test_message_from_response.py b/libtaxii/test/test_message_from_response.py new file mode 100644 index 0000000..cbbbbc6 --- /dev/null +++ b/libtaxii/test/test_message_from_response.py @@ -0,0 +1,74 @@ +from urllib.error import HTTPError + +import libtaxii +import libtaxii.messages + + +IN_RESPONSE_TO = "test in_response_to value" +TAXII_CONTENT_TYPE = libtaxii.constants.VID_TAXII_XML_11 +TEST_MESSAGE_ID = "test message id" +CONTENT_XML = '' + + +def test_httplib_http_response(httpserver): + httpserver.expect_request("/poll_service_path/").respond_with_data( + CONTENT_XML.encode('windows-1252'), + content_type="application/xml; charset=windows-1252", + headers={"X-TAXII-Content-Type": TAXII_CONTENT_TYPE} + ) + + client = libtaxii.clients.HttpClient() + http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) + message = libtaxii.get_message_from_httplib_http_response(http_response, IN_RESPONSE_TO) + + assert isinstance(message, libtaxii.messages_11.DiscoveryRequest) + assert message.in_response_to is None + assert message.message_id == 'test message id' + + +def test_httplib_http_response_error(httpserver): + httpserver.expect_request("/poll_service_path/").respond_with_data( + CONTENT_XML.encode('windows-1252'), + status=500, + content_type="application/xml; charset=windows-1252", + headers={"X-TAXII-Content-Type": TAXII_CONTENT_TYPE} + ) + + client = libtaxii.clients.HttpClient() + http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) + + assert isinstance(http_response, HTTPError) + + message = libtaxii.get_message_from_httplib_http_response(http_response, IN_RESPONSE_TO) + + assert isinstance(message, libtaxii.messages_11.DiscoveryRequest) + assert message.in_response_to is None + assert message.message_id == 'test message id' + + +def test_httplib_http_response_no_taxii_content_type(httpserver): + httpserver.expect_request("/poll_service_path/").respond_with_data( + CONTENT_XML.encode('windows-1252'), + content_type="application/xml; charset=windows-1252", + ) + + client = libtaxii.clients.HttpClient() + http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) + message = libtaxii.get_message_from_httplib_http_response(http_response, IN_RESPONSE_TO) + + assert isinstance(message, libtaxii.messages_11.StatusMessage) + assert message.in_response_to == IN_RESPONSE_TO + assert message.message_id == '0' + + server = http_response.headers.get("Server") + date = http_response.headers.get("Date") + assert message.message == ( + f'''Server: {server}\r\n''' + f'''Date: {date}\r\n''' + '''Content-Type: application/xml; charset=windows-1252\r\n''' + '''Content-Length: 129\r\n''' + '''Connection: close\r\n''' + '''\r\n''' + '''''' + ) + diff --git a/requirements.txt b/requirements.txt index 28065c1..3f1ce65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Sphinx==1.6.1 sphinx_rtd_theme==0.2.4 -pytest==3.0.7 +pytest==4.6.11 +pytest-httpserver==1.1.1 tox==2.7.0 bumpversion==0.5.3 From c39307f4258435251bc3b9abf7b78c7b697a7776 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Fri, 27 Feb 2026 11:35:45 +0100 Subject: [PATCH 03/17] Test every content type --- libtaxii/test/test_message_from_response.py | 55 ++++++++++++++------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/libtaxii/test/test_message_from_response.py b/libtaxii/test/test_message_from_response.py index cbbbbc6..1896069 100644 --- a/libtaxii/test/test_message_from_response.py +++ b/libtaxii/test/test_message_from_response.py @@ -1,54 +1,73 @@ +from http.client import HTTPResponse from urllib.error import HTTPError +import pytest + import libtaxii +from libtaxii.constants import VID_TAXII_XML_10, VID_TAXII_XML_11, VID_CERT_EU_JSON_10 import libtaxii.messages IN_RESPONSE_TO = "test in_response_to value" TAXII_CONTENT_TYPE = libtaxii.constants.VID_TAXII_XML_11 TEST_MESSAGE_ID = "test message id" -CONTENT_XML = '' +MESSAGES = { + VID_TAXII_XML_10: ( + '', + libtaxii.messages_10.DiscoveryRequest(message_id='1') + ), + VID_TAXII_XML_11: ( + '', + libtaxii.messages_11.DiscoveryRequest(message_id='test message id'), + ), + VID_CERT_EU_JSON_10: ( + '{"extended_headers": {}, "message_type": "Discovery_Request", "message_id": "1"}', + libtaxii.messages_10.DiscoveryRequest(message_id='1') + ) +} + + +@pytest.mark.parametrize('taxii_content_type', MESSAGES.keys()) +def test_httplib_http_response(taxii_content_type, httpserver): + content, expected_message = MESSAGES[taxii_content_type] -def test_httplib_http_response(httpserver): httpserver.expect_request("/poll_service_path/").respond_with_data( - CONTENT_XML.encode('windows-1252'), + content.encode('windows-1252'), content_type="application/xml; charset=windows-1252", - headers={"X-TAXII-Content-Type": TAXII_CONTENT_TYPE} + headers={"X-TAXII-Content-Type": taxii_content_type} ) client = libtaxii.clients.HttpClient() http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) + assert isinstance(http_response, HTTPResponse) + message = libtaxii.get_message_from_httplib_http_response(http_response, IN_RESPONSE_TO) + assert message == expected_message - assert isinstance(message, libtaxii.messages_11.DiscoveryRequest) - assert message.in_response_to is None - assert message.message_id == 'test message id' +@pytest.mark.parametrize('taxii_content_type', MESSAGES.keys()) +def test_httplib_http_response_error(taxii_content_type, httpserver): + content, expected_message = MESSAGES[taxii_content_type] -def test_httplib_http_response_error(httpserver): httpserver.expect_request("/poll_service_path/").respond_with_data( - CONTENT_XML.encode('windows-1252'), + content.encode('windows-1252'), status=500, content_type="application/xml; charset=windows-1252", - headers={"X-TAXII-Content-Type": TAXII_CONTENT_TYPE} + headers={"X-TAXII-Content-Type": taxii_content_type} ) client = libtaxii.clients.HttpClient() http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) - assert isinstance(http_response, HTTPError) message = libtaxii.get_message_from_httplib_http_response(http_response, IN_RESPONSE_TO) - - assert isinstance(message, libtaxii.messages_11.DiscoveryRequest) - assert message.in_response_to is None - assert message.message_id == 'test message id' + assert message == expected_message def test_httplib_http_response_no_taxii_content_type(httpserver): httpserver.expect_request("/poll_service_path/").respond_with_data( - CONTENT_XML.encode('windows-1252'), + "some content".encode('windows-1252'), content_type="application/xml; charset=windows-1252", ) @@ -66,9 +85,9 @@ def test_httplib_http_response_no_taxii_content_type(httpserver): f'''Server: {server}\r\n''' f'''Date: {date}\r\n''' '''Content-Type: application/xml; charset=windows-1252\r\n''' - '''Content-Length: 129\r\n''' + '''Content-Length: 12\r\n''' '''Connection: close\r\n''' '''\r\n''' - '''''' + '''some content''' ) From 3483a7c618cfe44626c7c68f96ffe3510a4de638 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Fri, 27 Feb 2026 12:35:32 +0100 Subject: [PATCH 04/17] Use head method in tests --- libtaxii/test/test_message_from_response.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libtaxii/test/test_message_from_response.py b/libtaxii/test/test_message_from_response.py index 1896069..b28d290 100644 --- a/libtaxii/test/test_message_from_response.py +++ b/libtaxii/test/test_message_from_response.py @@ -42,7 +42,7 @@ def test_httplib_http_response(taxii_content_type, httpserver): http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) assert isinstance(http_response, HTTPResponse) - message = libtaxii.get_message_from_httplib_http_response(http_response, IN_RESPONSE_TO) + message = libtaxii.get_message_from_http_response(http_response, IN_RESPONSE_TO) assert message == expected_message @@ -61,7 +61,7 @@ def test_httplib_http_response_error(taxii_content_type, httpserver): http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) assert isinstance(http_response, HTTPError) - message = libtaxii.get_message_from_httplib_http_response(http_response, IN_RESPONSE_TO) + message = libtaxii.get_message_from_http_response(http_response, IN_RESPONSE_TO) assert message == expected_message @@ -73,7 +73,7 @@ def test_httplib_http_response_no_taxii_content_type(httpserver): client = libtaxii.clients.HttpClient() http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) - message = libtaxii.get_message_from_httplib_http_response(http_response, IN_RESPONSE_TO) + message = libtaxii.get_message_from_http_response(http_response, IN_RESPONSE_TO) assert isinstance(message, libtaxii.messages_11.StatusMessage) assert message.in_response_to == IN_RESPONSE_TO From 45404f48316ccecbdf4f8895aeb5bcce02a59623 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Fri, 27 Feb 2026 12:35:42 +0100 Subject: [PATCH 05/17] Test error with taxii content type --- libtaxii/test/test_message_from_response.py | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/libtaxii/test/test_message_from_response.py b/libtaxii/test/test_message_from_response.py index b28d290..785cd0b 100644 --- a/libtaxii/test/test_message_from_response.py +++ b/libtaxii/test/test_message_from_response.py @@ -91,3 +91,35 @@ def test_httplib_http_response_no_taxii_content_type(httpserver): '''some content''' ) + +def test_httplib_http_response_error_no_taxii_content_type(httpserver): + httpserver.expect_request("/poll_service_path/").respond_with_data( + "some content".encode('windows-1252'), + status=500, + content_type="application/xml; charset=windows-1252", + ) + + client = libtaxii.clients.HttpClient() + http_response = client.call_taxii_service2(httpserver.host, '/poll_service_path/', libtaxii.constants.VID_TAXII_XML_10, b"", port=httpserver.port) + assert isinstance(http_response, HTTPError) + + message = libtaxii.get_message_from_http_response(http_response, IN_RESPONSE_TO) + assert isinstance(message, libtaxii.messages_11.StatusMessage) + assert message.in_response_to == IN_RESPONSE_TO + assert message.message_id == '0' + + server = http_response.headers.get("Server") + date = http_response.headers.get("Date") + assert message.message == ( + f'''HTTP Error 500: INTERNAL SERVER ERROR\r\n''' + f'''Server: {server}\n''' + f'''Date: {date}\n''' + '''Content-Type: application/xml; charset=windows-1252\n''' + '''Content-Length: 12\n''' + '''Connection: close\n''' + '''\n''' + '''\r\n''' + '''some content''' + ) + + From 7b16fef75349a3acf2c37b1afe8d1ce33c316cca Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Mon, 2 Mar 2026 11:51:41 +0100 Subject: [PATCH 06/17] Remove use of cgi and simplify method --- libtaxii/__init__.py | 108 +++----------------- libtaxii/test/test_message_from_response.py | 12 +-- 2 files changed, 18 insertions(+), 102 deletions(-) diff --git a/libtaxii/__init__.py b/libtaxii/__init__.py index 6d4268d..b4dfa86 100644 --- a/libtaxii/__init__.py +++ b/libtaxii/__init__.py @@ -5,16 +5,12 @@ The main libtaxii module """ -import six -from six.moves import urllib +from urllib.error import HTTPError import libtaxii.messages_10 as tm10 import libtaxii.messages_11 as tm11 -import libtaxii.clients as tc from .constants import * -import cgi - from .version import __version__ # noqa @@ -39,70 +35,23 @@ def get_message_from_http_response(http_response, in_response_to): parse in_response_to (str): the default value for in_response_to """ - if isinstance(http_response, six.moves.http_client.HTTPResponse): - return get_message_from_httplib_http_response(http_response, in_response_to) - elif isinstance(http_response, urllib.error.HTTPError): - return get_message_from_urllib2_httperror(http_response, in_response_to) - elif isinstance(http_response, urllib.response.addinfourl): - return get_message_from_urllib_addinfourl(http_response, in_response_to) - else: - raise ValueError('Unsupported response type: %s.' % http_response.__class__.__name__) - -def get_message_from_urllib2_httperror(http_response, in_response_to): - """ This function should not be called by libtaxii users directly. """ - info = http_response.info() - - if hasattr(info, 'getheader'): - taxii_content_type = info.getheader('X-TAXII-Content-Type') - _, params = cgi.parse_header(info.getheader('Content-Type')) - else: - taxii_content_type = info.get('X-TAXII-Content-Type') - _, params = cgi.parse_header(info.get('Content-Type')) + taxii_content_type = http_response.getheader('X-TAXII-Content-Type') + encoding = http_response.headers.get_charset() or 'utf-8' - encoding = params.get('charset', 'utf-8') - response_message = six.ensure_text(http_response.read(), errors='replace') + response_message = http_response.read() if taxii_content_type is None: - m = str(http_response) + '\r\n' + str(http_response.info()) + '\r\n' + response_message - return tm11.StatusMessage(message_id='0', in_response_to=in_response_to, status_type=ST_FAILURE, message=m) - elif taxii_content_type == VID_TAXII_XML_10: # It's a TAXII XML 1.0 message - return tm10.get_message_from_xml(response_message, encoding) - elif taxii_content_type == VID_TAXII_XML_11: # It's a TAXII XML 1.1 message - return tm11.get_message_from_xml(response_message, encoding) - elif taxii_content_type == VID_CERT_EU_JSON_10: - return tm10.get_message_from_json(response_message, encoding) - else: - raise ValueError('Unsupported X-TAXII-Content-Type: %s' % taxii_content_type) - - -def get_message_from_urllib_addinfourl(http_response, in_response_to): - """ This function should not be called by libtaxii users directly. """ - info = http_response.info() - - if hasattr(info, 'getheader'): - taxii_content_type = info.getheader('X-TAXII-Content-Type') - _, params = cgi.parse_header(info.getheader('Content-Type')) - else: - taxii_content_type = info.get('X-TAXII-Content-Type') - _, params = cgi.parse_header(info.get('Content-Type')) - - encoding = params.get('charset', 'utf-8') - response_message = six.ensure_text(http_response.read(), errors='replace') - - if taxii_content_type is None: # Treat it as a Failure Status Message, per the spec - - message = [] - header_dict = six.iteritems(http_response.info().dict) - for k, v in header_dict: - message.append(k + ': ' + v + '\r\n') - message.append('\r\n') - message.append(response_message) - - m = ''.join(message) + if isinstance(http_response, HTTPError): + m = str(http_response) + '\r\n' + else: + m = '' + for header, value in http_response.headers.items(): + m += f'{header}: {value}\r\n' + m += '\r\n' + m += response_message.decode(encoding, 'replace') return tm11.StatusMessage(message_id='0', in_response_to=in_response_to, status_type=ST_FAILURE, message=m) - elif taxii_content_type == VID_TAXII_XML_10: # It's a TAXII XML 1.0 message return tm10.get_message_from_xml(response_message, encoding) elif taxii_content_type == VID_TAXII_XML_11: # It's a TAXII XML 1.1 message @@ -111,36 +60,3 @@ def get_message_from_urllib_addinfourl(http_response, in_response_to): return tm10.get_message_from_json(response_message, encoding) else: raise ValueError('Unsupported X-TAXII-Content-Type: %s' % taxii_content_type) - - -def get_message_from_httplib_http_response(http_response, in_response_to): - """ This function should not be called by libtaxii users directly. """ - if hasattr(http_response, 'getheader'): - taxii_content_type = http_response.getheader('X-TAXII-Content-Type') - _, params = cgi.parse_header(http_response.getheader('Content-Type')) - else: - taxii_content_type = http_response.get('X-TAXII-Content-Type') - _, params = cgi.parse_header(http_response.get('Content-Type')) - - encoding = params.get('charset', 'utf-8') - response_message = six.ensure_text(http_response.read(), errors='replace') - - if taxii_content_type is None: # Treat it as a Failure Status Message, per the spec - - message = [] - header_tuples = http_response.getheaders() - for k, v in header_tuples: - message.append(k + ': ' + v + '\r\n') - message.append('\r\n') - message.append(response_message) - - m = ''.join(message) - - return tm11.StatusMessage(message_id='0', in_response_to=in_response_to, status_type=ST_FAILURE, message=m) - - elif taxii_content_type == VID_TAXII_XML_10: # It's a TAXII XML 1.0 message - return tm10.get_message_from_xml(response_message, encoding) - elif taxii_content_type == VID_TAXII_XML_11: # It's a TAXII XML 1.1 message - return tm11.get_message_from_xml(response_message, encoding) - else: - raise ValueError('Unsupported X-TAXII-Content-Type: %s' % taxii_content_type) diff --git a/libtaxii/test/test_message_from_response.py b/libtaxii/test/test_message_from_response.py index 785cd0b..2d88e43 100644 --- a/libtaxii/test/test_message_from_response.py +++ b/libtaxii/test/test_message_from_response.py @@ -5,6 +5,7 @@ import libtaxii from libtaxii.constants import VID_TAXII_XML_10, VID_TAXII_XML_11, VID_CERT_EU_JSON_10 +import libtaxii.clients import libtaxii.messages @@ -112,12 +113,11 @@ def test_httplib_http_response_error_no_taxii_content_type(httpserver): date = http_response.headers.get("Date") assert message.message == ( f'''HTTP Error 500: INTERNAL SERVER ERROR\r\n''' - f'''Server: {server}\n''' - f'''Date: {date}\n''' - '''Content-Type: application/xml; charset=windows-1252\n''' - '''Content-Length: 12\n''' - '''Connection: close\n''' - '''\n''' + f'''Server: {server}\r\n''' + f'''Date: {date}\r\n''' + '''Content-Type: application/xml; charset=windows-1252\r\n''' + '''Content-Length: 12\r\n''' + '''Connection: close\r\n''' '''\r\n''' '''some content''' ) From f050c4b308f632ce142e51e4183fb46f0c74f182 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Tue, 3 Mar 2026 11:10:34 +0100 Subject: [PATCH 07/17] Fixed charset handling --- libtaxii/__init__.py | 2 +- libtaxii/test/test_message_from_response.py | 25 +++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/libtaxii/__init__.py b/libtaxii/__init__.py index b4dfa86..bea7dd4 100644 --- a/libtaxii/__init__.py +++ b/libtaxii/__init__.py @@ -37,7 +37,7 @@ def get_message_from_http_response(http_response, in_response_to): """ taxii_content_type = http_response.getheader('X-TAXII-Content-Type') - encoding = http_response.headers.get_charset() or 'utf-8' + encoding = http_response.headers.get_content_charset() or 'utf-8' response_message = http_response.read() diff --git a/libtaxii/test/test_message_from_response.py b/libtaxii/test/test_message_from_response.py index 2d88e43..d4cc6e4 100644 --- a/libtaxii/test/test_message_from_response.py +++ b/libtaxii/test/test_message_from_response.py @@ -19,8 +19,8 @@ libtaxii.messages_10.DiscoveryRequest(message_id='1') ), VID_TAXII_XML_11: ( - '', - libtaxii.messages_11.DiscoveryRequest(message_id='test message id'), + '', + libtaxii.messages_11.DiscoveryRequest(message_id='non-ascii message id ÀÁÂÃÄÅ'), ), VID_CERT_EU_JSON_10: ( '{"extended_headers": {}, "message_type": "Discovery_Request", "message_id": "1"}', @@ -34,8 +34,8 @@ def test_httplib_http_response(taxii_content_type, httpserver): content, expected_message = MESSAGES[taxii_content_type] httpserver.expect_request("/poll_service_path/").respond_with_data( - content.encode('windows-1252'), - content_type="application/xml; charset=windows-1252", + content.encode('ISO-8859-1'), + content_type="application/xml; charset=ISO-8859-1", headers={"X-TAXII-Content-Type": taxii_content_type} ) @@ -44,6 +44,7 @@ def test_httplib_http_response(taxii_content_type, httpserver): assert isinstance(http_response, HTTPResponse) message = libtaxii.get_message_from_http_response(http_response, IN_RESPONSE_TO) + print(message.message_id) assert message == expected_message @@ -52,9 +53,9 @@ def test_httplib_http_response_error(taxii_content_type, httpserver): content, expected_message = MESSAGES[taxii_content_type] httpserver.expect_request("/poll_service_path/").respond_with_data( - content.encode('windows-1252'), + content.encode('ISO-8859-1'), status=500, - content_type="application/xml; charset=windows-1252", + content_type="application/xml; charset=ISO-8859-1", headers={"X-TAXII-Content-Type": taxii_content_type} ) @@ -68,8 +69,8 @@ def test_httplib_http_response_error(taxii_content_type, httpserver): def test_httplib_http_response_no_taxii_content_type(httpserver): httpserver.expect_request("/poll_service_path/").respond_with_data( - "some content".encode('windows-1252'), - content_type="application/xml; charset=windows-1252", + "some content".encode('ISO-8859-1'), + content_type="application/xml; charset=ISO-8859-1", ) client = libtaxii.clients.HttpClient() @@ -85,7 +86,7 @@ def test_httplib_http_response_no_taxii_content_type(httpserver): assert message.message == ( f'''Server: {server}\r\n''' f'''Date: {date}\r\n''' - '''Content-Type: application/xml; charset=windows-1252\r\n''' + '''Content-Type: application/xml; charset=ISO-8859-1\r\n''' '''Content-Length: 12\r\n''' '''Connection: close\r\n''' '''\r\n''' @@ -95,9 +96,9 @@ def test_httplib_http_response_no_taxii_content_type(httpserver): def test_httplib_http_response_error_no_taxii_content_type(httpserver): httpserver.expect_request("/poll_service_path/").respond_with_data( - "some content".encode('windows-1252'), + "some content".encode('ISO-8859-1'), status=500, - content_type="application/xml; charset=windows-1252", + content_type="application/xml; charset=ISO-8859-1", ) client = libtaxii.clients.HttpClient() @@ -115,7 +116,7 @@ def test_httplib_http_response_error_no_taxii_content_type(httpserver): f'''HTTP Error 500: INTERNAL SERVER ERROR\r\n''' f'''Server: {server}\r\n''' f'''Date: {date}\r\n''' - '''Content-Type: application/xml; charset=windows-1252\r\n''' + '''Content-Type: application/xml; charset=ISO-8859-1\r\n''' '''Content-Length: 12\r\n''' '''Connection: close\r\n''' '''\r\n''' From 2a51a6b62c5742f21f32e14785a27673f7f3d70b Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 10:23:49 +0100 Subject: [PATCH 08/17] Fixed xee attack test --- libtaxii/test/messages_11_test.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/libtaxii/test/messages_11_test.py b/libtaxii/test/messages_11_test.py index 96e33a1..97284b6 100644 --- a/libtaxii/test/messages_11_test.py +++ b/libtaxii/test/messages_11_test.py @@ -7,11 +7,13 @@ import datetime import io +import re import sys import unittest import warnings import inspect +import pytest from dateutil.tz import tzutc from lxml import etree @@ -1244,12 +1246,9 @@ def test_xee_remote(self): &xxe; """ - # If an XML Syntax Error is received, an attack would have succeeded - - try: - e = parse(xee_remote) - except etree.XMLSyntaxError: - raise ValueError("An XML Syntax Error was raised, meaning a real attack would have succeeded!") + # Undefined xxe entity should be expected, because lxml should not attempt to define it via external reference + with pytest.raises(etree.XMLSyntaxError, match=re.escape("Entity 'xxe' not defined, line 4, column 35 (, line 4)")): + parse(xee_remote) def test_xee_local(self): """ @@ -1263,12 +1262,10 @@ def test_xee_local(self): ]> &xxe; """ - # If an XML Syntax Error is received, an attack would have succeeded - try: - e = parse(xee_local) - except etree.XMLSyntaxError: - raise ValueError("An XML Syntax Error was raised, meaning a real attack would have succeeded!") + # Undefined xxe entity is expected, because lxml should not attempt to define it via external reference + with pytest.raises(etree.XMLSyntaxError, match=re.escape("Entity 'xxe' not defined, line 4, column 35 (, line 4)")): + parse(xee_local) def test_ssrf(self): """ From fa758210aa8968fafee10328741681e2f440b7fd Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 10:24:01 +0100 Subject: [PATCH 09/17] Use less common charset for testing encoding --- libtaxii/test/test_message_from_response.py | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/libtaxii/test/test_message_from_response.py b/libtaxii/test/test_message_from_response.py index d4cc6e4..f280d5c 100644 --- a/libtaxii/test/test_message_from_response.py +++ b/libtaxii/test/test_message_from_response.py @@ -19,8 +19,8 @@ libtaxii.messages_10.DiscoveryRequest(message_id='1') ), VID_TAXII_XML_11: ( - '', - libtaxii.messages_11.DiscoveryRequest(message_id='non-ascii message id ÀÁÂÃÄÅ'), + '', + libtaxii.messages_11.DiscoveryRequest(message_id='non-ascii message id Ħ'), ), VID_CERT_EU_JSON_10: ( '{"extended_headers": {}, "message_type": "Discovery_Request", "message_id": "1"}', @@ -34,8 +34,8 @@ def test_httplib_http_response(taxii_content_type, httpserver): content, expected_message = MESSAGES[taxii_content_type] httpserver.expect_request("/poll_service_path/").respond_with_data( - content.encode('ISO-8859-1'), - content_type="application/xml; charset=ISO-8859-1", + content.encode('ISO-8859-3'), + content_type="application/xml; charset=ISO-8859-3", headers={"X-TAXII-Content-Type": taxii_content_type} ) @@ -53,9 +53,9 @@ def test_httplib_http_response_error(taxii_content_type, httpserver): content, expected_message = MESSAGES[taxii_content_type] httpserver.expect_request("/poll_service_path/").respond_with_data( - content.encode('ISO-8859-1'), + content.encode('ISO-8859-3'), status=500, - content_type="application/xml; charset=ISO-8859-1", + content_type="application/xml; charset=ISO-8859-3", headers={"X-TAXII-Content-Type": taxii_content_type} ) @@ -69,8 +69,8 @@ def test_httplib_http_response_error(taxii_content_type, httpserver): def test_httplib_http_response_no_taxii_content_type(httpserver): httpserver.expect_request("/poll_service_path/").respond_with_data( - "some content".encode('ISO-8859-1'), - content_type="application/xml; charset=ISO-8859-1", + "some content".encode('ISO-8859-3'), + content_type="application/xml; charset=ISO-8859-3", ) client = libtaxii.clients.HttpClient() @@ -86,7 +86,7 @@ def test_httplib_http_response_no_taxii_content_type(httpserver): assert message.message == ( f'''Server: {server}\r\n''' f'''Date: {date}\r\n''' - '''Content-Type: application/xml; charset=ISO-8859-1\r\n''' + '''Content-Type: application/xml; charset=ISO-8859-3\r\n''' '''Content-Length: 12\r\n''' '''Connection: close\r\n''' '''\r\n''' @@ -96,9 +96,9 @@ def test_httplib_http_response_no_taxii_content_type(httpserver): def test_httplib_http_response_error_no_taxii_content_type(httpserver): httpserver.expect_request("/poll_service_path/").respond_with_data( - "some content".encode('ISO-8859-1'), + "some content".encode('ISO-8859-3'), status=500, - content_type="application/xml; charset=ISO-8859-1", + content_type="application/xml; charset=ISO-8859-3", ) client = libtaxii.clients.HttpClient() @@ -116,7 +116,7 @@ def test_httplib_http_response_error_no_taxii_content_type(httpserver): f'''HTTP Error 500: INTERNAL SERVER ERROR\r\n''' f'''Server: {server}\r\n''' f'''Date: {date}\r\n''' - '''Content-Type: application/xml; charset=ISO-8859-1\r\n''' + '''Content-Type: application/xml; charset=ISO-8859-3\r\n''' '''Content-Length: 12\r\n''' '''Connection: close\r\n''' '''\r\n''' From adfe7b4ee1053c4d25ac86cbc861f9ac26c3eb3b Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 12:47:35 +0100 Subject: [PATCH 10/17] Added build-system dependencies --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b1e1a46 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools"] From 3d4b3772689558b7395500478c56e39bb398efa9 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 12:47:52 +0100 Subject: [PATCH 11/17] Update dependencies to support python 3.14 --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3f1ce65..7053c15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -Sphinx==1.6.1 -sphinx_rtd_theme==0.2.4 -pytest==4.6.11 +Sphinx==8.1.3 +sphinx_rtd_theme==3.1.0 +pytest==7.4.4 pytest-httpserver==1.1.1 tox==2.7.0 bumpversion==0.5.3 From a1ecb494c005aaad9ff3c357f4cb17861c2ad9c4 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 12:48:27 +0100 Subject: [PATCH 12/17] Updated tox.ini Only test supported python versions Added setuptools dependency for packaging test --- tox.ini | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/tox.ini b/tox.ini index 9e41056..b324447 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, rhel26, py34, py35, py36, py37, py38, docs, packaging +envlist = 3.10, 3.11, 3.12, 3.13, 3.14, docs, packaging [testenv] commands = @@ -8,20 +8,6 @@ commands = sphinx-build -b html docs docs/_build/html deps = -rrequirements.txt -[testenv:py26] -commands = - pytest libtaxii -deps = pytest - -[testenv:rhel26] -commands = - pytest libtaxii -deps = - lxml==2.2.3 - python-dateutil==1.4.1 - six==1.9.0 - pytest - [testenv:docs] commands = sphinx-build -b doctest docs docs/_build/doctest @@ -30,15 +16,14 @@ commands = [testenv:packaging] deps = readme_renderer + setuptools commands = python setup.py check -r -s [travis] python = - 2.6: py26, rhel26 - 2.7: py27, docs, packaging - 3.4: py34 - 3.5: py35 - 3.6: py36, docs, packaging - 3.7: py37 - 3.8: py38 + 3.10: 3.10 + 3.11: 3.11 + 3.12: 3.12 + 3.13: 3.13 + 3.14: 3.14, docs, packaging From 453695fbabd4d004bb203cad3e2a35baaf9df5b2 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 12:50:17 +0100 Subject: [PATCH 13/17] Removed deprecated get_html_theme_path call --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index e9ff1a2..b2efe69 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,7 +39,6 @@ if not on_rtd: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] else: html_theme = 'default' From defaaea4b8e797f316a12d7e22c67d6a77fb1898 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 12:55:53 +0100 Subject: [PATCH 14/17] Removed deprecated license classifier --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 22b7e5c..92e59b4 100644 --- a/setup.py +++ b/setup.py @@ -91,7 +91,6 @@ def get_long_description(): classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', From a468486ed23da56e15591f654606b66d9bdfbcf0 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 12:57:42 +0100 Subject: [PATCH 15/17] Fixed invalid escape sequences --- libtaxii/validation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libtaxii/validation.py b/libtaxii/validation.py index 8440751..1569cca 100644 --- a/libtaxii/validation.py +++ b/libtaxii/validation.py @@ -19,9 +19,9 @@ RegexTuple = collections.namedtuple('_RegexTuple', ['regex', 'title']) # URI regex per http://tools.ietf.org/html/rfc3986 -uri_regex = RegexTuple("(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?", "URI Format") -message_id_regex_10 = RegexTuple("^[0-9]+$", "Numbers only") -targeting_expression_regex = RegexTuple("^(@?\w+|\*{1,2})(/(@?\w+|\*{1,2}))*$", "Targeting Expression Syntax") +uri_regex = RegexTuple(r"(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?", "URI Format") +message_id_regex_10 = RegexTuple(r"^[0-9]+$", "Numbers only") +targeting_expression_regex = RegexTuple(r"^(@?\w+|\*{1,2})(/(@?\w+|\*{1,2}))*$", "Targeting Expression Syntax") _none_error = "%s is not allowed to be None and the provided value was None" _type_error = "%s must be of type %s. The incorrect value was of type %s" From b7f6043e6ddbd40918b0183b582ae0bba475f458 Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Wed, 4 Mar 2026 13:01:17 +0100 Subject: [PATCH 16/17] Update tox version in requirments.txt 2.7.0 does not run correctly on python 3.14 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7053c15..60cf154 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Sphinx==8.1.3 sphinx_rtd_theme==3.1.0 pytest==7.4.4 pytest-httpserver==1.1.1 -tox==2.7.0 +tox==4.47.3 bumpversion==0.5.3 -e . From 3bc1c15d9162c0e32bcde2e300056f3db9fd558a Mon Sep 17 00:00:00 2001 From: Ilia Novoselov Date: Thu, 5 Mar 2026 11:42:45 +0100 Subject: [PATCH 17/17] Update bumpversion --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 60cf154..4988234 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ sphinx_rtd_theme==3.1.0 pytest==7.4.4 pytest-httpserver==1.1.1 tox==4.47.3 -bumpversion==0.5.3 +bump-my-version==1.2.7 -e .