Skip to content

Commit b0eaac6

Browse files
authored
Merge pull request #2408 from zuo/jk/stomp-and-n6-related-updates-fixes
STOMP-and-n6-related updates, fixes and enhancements, especially adding login-based authentication
2 parents 8c7bef1 + 7c59d49 commit b0eaac6

File tree

8 files changed

+293
-77
lines changed

8 files changed

+293
-77
lines changed

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,65 @@ CHANGELOG
1111
------------------
1212

1313
### Configuration
14+
- Add new optional configuration parameters for `intelmq.bots.collectors.stomp.collector`
15+
and `intelmq.bots.outputs.stomp.output` (PR#2408 by Jan Kaliszewski):
16+
- `auth_by_ssl_client_certificate` (Boolean, default: *true*; if *false* then
17+
`ssl_client_certificate` and `ssl_client_certificate_key` will be ignored);
18+
- `username` (STOMP authentication login, default: "guest"; to be used only
19+
if `auth_by_ssl_client_certificate` is *false*);
20+
- `password` (STOMP authentication passcode, default: "guest"; to be used only
21+
if `auth_by_ssl_client_certificate` is *false*).
1422

1523
### Core
1624
- `intelmq.lib.message`: For invalid message keys, add a hint on the failure to the exception: not allowed by configuration or not matching regular expression (PR#2398 by Sebastian Wagner).
1725
- `intelmq.lib.exceptions.InvalidKey`: Add optional parameter `additional_text` (PR#2398 by Sebastian Wagner).
26+
- `intelmq.lib.mixins`: Add a new class, `StompMixin` (defined in a new submodule: `stomp`),
27+
which provides certain common STOMP-bot-specific operations, factored out from
28+
`intelmq.bots.collectors.stomp.collector` and `intelmq.bots.outputs.stomp.output`
29+
(PR#2408 by Jan Kaliszewski).
1830

1931
### Development
2032

2133
### Data Format
2234

2335
### Bots
2436
#### Collectors
37+
- `intelmq.bots.collectors.stomp.collector` (PR#2408 by Jan Kaliszewski):
38+
- Add support for authentication based on STOMP login and passcode,
39+
introducing 3 new configuration parameters (see above: *Configuration*).
40+
- Update the code to support new versions of `stomp.py`, including the latest (`8.1.0`);
41+
fixes [#2342](https://github.com/certtools/intelmq/issues/2342).
42+
- Fix the reconnection behavior: do not attempt to reconnect after `shutdown`. Also,
43+
never attempt to reconnect if the version of `stomp.py` is older than `4.1.21` (it
44+
did not work properly anyway).
45+
- Add coercion of the `port` config parameter to `int`.
46+
- Add implementation of the `check` hook (verifying, in particular, accessibility
47+
of necessary file(s)).
48+
- Remove undocumented and unused attributes of `StompCollectorBot` instances:
49+
`ssl_ca_cert`, `ssl_cl_cert`, `ssl_cl_cert_key`.
50+
- Minor fixes/improvements and some refactoring (see also above: *Core*...).
2551

2652
#### Parsers
2753

2854
#### Experts
2955

3056
#### Outputs
57+
- `intelmq.bots.outputs.stomp.output` (PR#2408 by Jan Kaliszewski):
58+
- Add support for authentication based on STOMP login and passcode,
59+
introducing 3 new configuration parameters (see above: *Configuration*).
60+
- Update the code to support new versions of `stomp.py`, including the latest (`8.1.0`).
61+
- Fix `AttributeError` caused by attempts to get unset attributes of `StompOutputBot`
62+
(`ssl_ca_cert` et consortes).
63+
- Add coercion of the `port` config parameter to `int`.
64+
- Add implementation of the `check` hook (verifying, in particular, accessibility
65+
of necessary file(s)).
66+
- Add `stomp.py` version check (raise `MissingDependencyError` if not `>=4.1.8`).
67+
- Minor fixes/improvements and some refactoring (see also above: *Core*...).
3168

3269
### Documentation
3370
- Add a readthedocs configuration file to fix the build fail (PR#2403 by Sebastian Wagner).
71+
- Update/fix/improve the stuff related to the STOMP bots and integration with the *n6*'s
72+
Stream API (PR#2408 by Jan Kaliszewski).
3473

3574
### Packaging
3675

docs/user/bots.rst

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -945,12 +945,15 @@ Install the `stomp.py` library from PyPI:
945945
**Configuration Parameters**
946946

947947
* **Feed parameters** (see above)
948-
* `exchange`: exchange point
948+
* `exchange`: STOMP *destination* to subscribe to, e.g. "/exchange/my.org/*.*.*.*"
949949
* `port`: 61614
950-
* `server`: hostname e.g. "n6stream.cert.pl"
950+
* `server`: hostname, e.g. "n6stream.cert.pl"
951951
* `ssl_ca_certificate`: path to CA file
952-
* `ssl_client_certificate`: path to client cert file
953-
* `ssl_client_certificate_key`: path to client cert key file
952+
* `auth_by_ssl_client_certificate`: Boolean, default: true (note: set to false for new *n6* auth)
953+
* `ssl_client_certificate`: path to client cert file, used only if `auth_by_ssl_client_certificate` is true
954+
* `ssl_client_certificate_key`: path to client cert key file, used only if `auth_by_ssl_client_certificate` is true
955+
* `username`: STOMP *login* (e.g., *n6* user login), used only if `auth_by_ssl_client_certificate` is false
956+
* `password`: STOMP *passcode* (e.g., *n6* user API key), used only if `auth_by_ssl_client_certificate` is false
954957

955958

956959
.. _intelmq.bots.collectors.twitter.collector_twitter:
@@ -4305,7 +4308,7 @@ Also you will need a so called "exchange point".
43054308
43064309
**Configuration Parameters**
43074310
4308-
* `exchange`: The exchange to push at
4311+
* `exchange`: STOMP *destination* to push at, e.g. "/exchange/_push"
43094312
* `heartbeat`: default: 60000
43104313
* `message_hierarchical_output`: Boolean, default: false
43114314
* `message_jsondict_as_string`: Boolean, default: false
@@ -4314,8 +4317,11 @@ Also you will need a so called "exchange point".
43144317
* `server`: Host or IP address of the STOMP server
43154318
* `single_key`: Boolean or string (field name), default: false
43164319
* `ssl_ca_certificate`: path to CA file
4317-
* `ssl_client_certificate`: path to client cert file
4318-
* `ssl_client_certificate_key`: path to client cert key file
4320+
* `auth_by_ssl_client_certificate`: Boolean, default: true (note: set to false for new *n6* auth)
4321+
* `ssl_client_certificate`: path to client cert file, used only if `auth_by_ssl_client_certificate` is true
4322+
* `ssl_client_certificate_key`: path to client cert key file, used only if `auth_by_ssl_client_certificate` is true
4323+
* `username`: STOMP *login* (e.g., *n6* user login), used only if `auth_by_ssl_client_certificate` is false
4324+
* `password`: STOMP *passcode* (e.g., *n6* user API key), used only if `auth_by_ssl_client_certificate` is false
43194325
43204326
43214327
.. _intelmq.bots.outputs.tcp.output:

docs/user/n6-integrations.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ n6 is maintained and developed by `CERT.pl <https://www.cert.pl/>`_.
1111

1212
Information about n6 can be found here:
1313

14-
- Website: `n6.cert.pl <https://n6.cert.pl/en/>`_
14+
- Website: `cert.pl/en/n6 <https://cert.pl/en/n6/>`_
1515
- Source Code: `github.com/CERT-Polska/n6 <https://github.com/CERT-Polska/n6/>`_
1616
- n6 documentation: `n6.readthedocs.io <https://n6.readthedocs.io/>`_
17-
- n6sdk developer documentation: `n6sdk.readthedocs.io <https://n6sdk.readthedocs.io/>`_
1817

1918
.. image:: /_static/n6/n6-schemat2.png
2019
:alt: n6 schema

intelmq/bots/collectors/stomp/collector.py

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
# SPDX-License-Identifier: AGPL-3.0-or-later
44

55
# -*- coding: utf-8 -*-
6-
import os.path
76

87
from intelmq.lib.bot import CollectorBot
9-
from intelmq.lib.exceptions import MissingDependencyError
8+
from intelmq.lib.mixins import StompMixin
109

1110
try:
1211
import stomp
12+
import stomp.exception
1313
except ImportError:
1414
stomp = None
1515
else:
@@ -18,9 +18,10 @@ class StompListener(stomp.PrintingListener):
1818
the stomp listener gets called asynchronously for
1919
every STOMP message
2020
"""
21-
def __init__(self, n6stompcollector, conn, destination):
21+
def __init__(self, n6stompcollector, conn, destination, connect_kwargs=None):
2222
self.stompbot = n6stompcollector
2323
self.conn = conn
24+
self.connect_kwargs = connect_kwargs
2425
self.destination = destination
2526
super().__init__()
2627
if stomp.__version__ >= (5, 0, 0):
@@ -29,15 +30,23 @@ def __init__(self, n6stompcollector, conn, destination):
2930

3031
def on_heartbeat_timeout(self):
3132
self.stompbot.logger.info("Heartbeat timeout. Attempting to re-connect.")
32-
connect_and_subscribe(self.conn, self.stompbot.logger, self.destination)
33-
34-
def on_error(self, headers, message):
35-
self.stompbot.logger.error('Received an error: %r.', message)
36-
37-
def on_message(self, headers, message):
38-
self.stompbot.logger.debug('Receive message %r...', message[:500])
33+
if self.stompbot._auto_reconnect:
34+
connect_and_subscribe(self.conn, self.stompbot.logger, self.destination,
35+
connect_kwargs=self.connect_kwargs)
36+
37+
def on_error(self, frame, body=None):
38+
if body is None:
39+
# `stomp.py >= 6.1.0`
40+
body = frame.body
41+
self.stompbot.logger.error('Received an error: %r.', body)
42+
43+
def on_message(self, frame, body=None):
44+
if body is None:
45+
# `stomp.py >= 6.1.0`
46+
body = frame.body
47+
self.stompbot.logger.debug('Receive message %r...', body[:500])
3948
report = self.stompbot.new_report()
40-
report.add("raw", message.rstrip())
49+
report.add("raw", body.rstrip())
4150
report.add("feed.url", "stomp://" +
4251
self.stompbot.server +
4352
":" + str(self.stompbot.port) +
@@ -46,24 +55,31 @@ def on_message(self, headers, message):
4655

4756
def on_disconnected(self):
4857
self.stompbot.logger.debug('Detected disconnect')
49-
connect_and_subscribe(self.conn, self.stompbot.logger, self.destination)
58+
if self.stompbot._auto_reconnect:
59+
connect_and_subscribe(self.conn, self.stompbot.logger, self.destination,
60+
connect_kwargs=self.connect_kwargs)
5061

5162

52-
def connect_and_subscribe(conn, logger, destination, start=False):
63+
def connect_and_subscribe(conn, logger, destination, start=False, connect_kwargs=None):
5364
if start:
5465
conn.start()
55-
conn.connect(wait=True)
66+
if connect_kwargs is None:
67+
connect_kwargs = dict(wait=True)
68+
conn.connect(**connect_kwargs)
5669
conn.subscribe(destination=destination,
5770
id=1, ack='auto')
5871
logger.info('Successfully connected and subscribed.')
5972

6073

61-
class StompCollectorBot(CollectorBot):
74+
class StompCollectorBot(CollectorBot, StompMixin):
6275
"""Collect data from a STOMP Interface"""
6376
""" main class for the STOMP protocol collector """
6477
exchange: str = ''
6578
port: int = 61614
6679
server: str = "n6stream.cert.pl"
80+
auth_by_ssl_client_certificate: bool = True
81+
username: str = 'guest' # ignored if `auth_by_ssl_client_certificate` is true
82+
password: str = 'guest' # ignored if `auth_by_ssl_client_certificate` is true
6783
ssl_ca_certificate: str = 'ca.pem' # TODO pathlib.Path
6884
ssl_client_certificate: str = 'client.pem' # TODO pathlib.Path
6985
ssl_client_certificate_key: str = 'client.key' # TODO pathlib.Path
@@ -73,36 +89,22 @@ class StompCollectorBot(CollectorBot):
7389
__conn = False # define here so shutdown method can check for it
7490

7591
def init(self):
76-
if stomp is None:
77-
raise MissingDependencyError("stomp")
78-
elif stomp.__version__ < (4, 1, 8):
79-
raise MissingDependencyError("stomp", version="4.1.8",
80-
installed=stomp.__version__)
81-
82-
self.ssl_ca_cert = self.ssl_ca_certificate
83-
self.ssl_cl_cert = self.ssl_client_certificate
84-
self.ssl_cl_cert_key = self.ssl_client_certificate_key
85-
86-
# check if certificates exist
87-
for f in [self.ssl_ca_cert, self.ssl_cl_cert, self.ssl_cl_cert_key]:
88-
if not os.path.isfile(f):
89-
raise ValueError("Could not open file %r." % f)
90-
91-
_host = [(self.server, self.port)]
92-
self.__conn = stomp.Connection(host_and_ports=_host, use_ssl=True,
93-
ssl_key_file=self.ssl_cl_cert_key,
94-
ssl_cert_file=self.ssl_cl_cert,
95-
ssl_ca_certs=self.ssl_ca_cert,
96-
heartbeats=(self.heartbeat,
97-
self.heartbeat))
98-
99-
self.__conn.set_listener('', StompListener(self, self.__conn, self.exchange))
92+
self.stomp_bot_runtime_initial_check()
93+
94+
# (note: older versions of `stomp.py` do not play well with reconnects)
95+
self._auto_reconnect = (stomp.__version__ >= (4, 1, 21))
96+
97+
self.__conn, connect_kwargs = self.prepare_stomp_connection()
98+
self.__conn.set_listener('', StompListener(self, self.__conn, self.exchange,
99+
connect_kwargs=connect_kwargs))
100100
connect_and_subscribe(self.__conn, self.logger, self.exchange,
101-
start=stomp.__version__ < (4, 1, 20))
101+
start=stomp.__version__ < (4, 1, 20),
102+
connect_kwargs=connect_kwargs)
102103

103104
def shutdown(self):
104105
if not stomp or not self.__conn:
105106
return
107+
self._auto_reconnect = False
106108
try:
107109
self.__conn.disconnect()
108110
except stomp.exception.NotConnectedException:
@@ -111,5 +113,9 @@ def shutdown(self):
111113
def process(self):
112114
pass
113115

116+
@classmethod
117+
def check(cls, parameters):
118+
return cls.stomp_bot_parameters_check(parameters) or None
119+
114120

115121
BOT = StompCollectorBot

intelmq/bots/outputs/stomp/output.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@
33
# SPDX-License-Identifier: AGPL-3.0-or-later
44

55
# -*- coding: utf-8 -*-
6-
import os.path
76

87
from intelmq.lib.bot import OutputBot
9-
from intelmq.lib.exceptions import MissingDependencyError
10-
8+
from intelmq.lib.mixins import StompMixin
119

1210
try:
1311
import stomp
1412
except ImportError:
1513
stomp = None
1614

1715

18-
class StompOutputBot(OutputBot):
16+
class StompOutputBot(OutputBot, StompMixin):
1917
"""Send events to a STMOP server"""
2018
""" main class for the STOMP protocol output bot """
2119
exchange: str = "/exchange/_push"
@@ -28,36 +26,28 @@ class StompOutputBot(OutputBot):
2826
port: int = 61614
2927
server: str = "127.0.0.1" # TODO: could be ip address
3028
single_key: bool = False
29+
auth_by_ssl_client_certificate: bool = True
30+
username: str = 'guest' # ignored if `auth_by_ssl_client_certificate` is true
31+
password: str = 'guest' # ignored if `auth_by_ssl_client_certificate` is true
3132
ssl_ca_certificate: str = 'ca.pem' # TODO: could be pathlib.Path
3233
ssl_client_certificate: str = 'client.pem' # TODO: pathlib.Path
3334
ssl_client_certificate_key: str = 'client.key' # TODO: patlib.Path
3435

3536
_conn = None
3637

3738
def init(self):
38-
if stomp is None:
39-
raise MissingDependencyError("stomp")
40-
41-
# check if certificates exist
42-
for f in [self.ssl_ca_cert, self.ssl_cl_cert, self.ssl_cl_cert_key]:
43-
if not os.path.isfile(f):
44-
raise ValueError("Could not open SSL (certificate) file '%s'." % f)
45-
46-
_host = [(self.server, self.port)]
47-
self._conn = stomp.Connection(host_and_ports=_host, use_ssl=True,
48-
ssl_key_file=self.ssl_cl_cert_key,
49-
ssl_cert_file=self.ssl_cl_cert,
50-
ssl_ca_certs=self.ssl_ca_cert,
51-
heartbeats=(self.heartbeat,
52-
self.heartbeat))
39+
self.stomp_bot_runtime_initial_check()
40+
(self._conn,
41+
self._connect_kwargs) = self.prepare_stomp_connection()
5342
self.connect()
5443

5544
def connect(self):
5645
self.logger.debug('Connecting.')
5746
# based on the documentation at:
5847
# https://github.com/jasonrbriggs/stomp.py/wiki/Simple-Example
59-
self._conn.start()
60-
self._conn.connect(wait=True)
48+
if stomp.__version__ < (4, 1, 20):
49+
self._conn.start()
50+
self._conn.connect(**self._connect_kwargs)
6151
self.logger.debug('Connected.')
6252

6353
def shutdown(self):
@@ -73,5 +63,9 @@ def process(self):
7363
destination=self.exchange)
7464
self.acknowledge_message()
7565

66+
@classmethod
67+
def check(cls, parameters):
68+
return cls.stomp_bot_parameters_check(parameters) or None
69+
7670

7771
BOT = StompOutputBot

intelmq/etc/feeds.yaml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,20 +1158,19 @@ providers:
11581158
module: intelmq.bots.collectors.stomp.collector
11591159
parameters:
11601160
exchange: "{insert your exchange point as given by CERT.pl}"
1161-
ssl_client_certificate_key: "{insert path to client cert key file for
1162-
CERT.pl's n6}"
11631161
ssl_ca_certificate: "{insert path to CA file for CERT.pl's n6}"
1162+
auth_by_ssl_client_certificate: false
1163+
username: "{insert n6 user's login}"
1164+
password: "{insert n6 user's API key}"
11641165
port: '61614'
1165-
ssl_client_certificate: "{insert path to client cert file for CERTpl's
1166-
n6}"
11671166
server: n6stream.cert.pl
11681167
name: __FEED__
11691168
provider: __PROVIDER__
11701169
parser:
11711170
module: intelmq.bots.parsers.n6.parser_n6stomp
11721171
parameters:
1173-
revision: 2018-01-20
1174-
documentation: https://n6.cert.pl/en/
1172+
revision: 2023-09-23
1173+
documentation: https://n6.readthedocs.io/usage/streamapi/
11751174
public: false
11761175
AlienVault:
11771176
OTX:

intelmq/lib/mixins/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
from intelmq.lib.mixins.http import HttpMixin
66
from intelmq.lib.mixins.cache import CacheMixin
77
from intelmq.lib.mixins.sql import SQLMixin
8+
from intelmq.lib.mixins.stomp import StompMixin
89

9-
__all__ = ['HttpMixin', 'CacheMixin', 'SQLMixin']
10+
__all__ = ['HttpMixin', 'CacheMixin', 'SQLMixin', 'StompMixin']

0 commit comments

Comments
 (0)