Skip to content

Commit

Permalink
dicom server: added support for MOVE event
Browse files Browse the repository at this point in the history
  • Loading branch information
jstucke committed Apr 26, 2024
1 parent 3bff275 commit eff8676
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 23 deletions.
52 changes: 36 additions & 16 deletions honeypots/dicom_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,30 +141,19 @@ def handle_store(event: evt.Event) -> int:
def handle_get(event):
# C-GET event
# see docs: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_c_get.html#pynetdicom._handlers.doc_handle_c_get
dataset = event.identifier
log_data = {
key: getattr(dataset, key, None)
for key in (
"QueryRetrieveLevel",
"PatientID",
"StudyInstanceUID",
"SeriesInstanceUID",
)
}
_log_event(event, log_data)
_log_get_move_event(event)

if "QueryRetrieveLevel" not in dataset:
if not event.identifier or "QueryRetrieveLevel" not in event.identifier:
# if this is a valid GET request, there should be a retrieve level
yield FAILURE, None
return

# we simply always return the same demo dataset instead of
# checking if anything actually matched the IDs in the request
instances = [GET_REQUEST_DS, GET_REQUEST_DS]
instances = [GET_REQUEST_DS]

# first yield the number of operations
total = len(instances)
yield total
yield len(instances)

# then yield the "matching" instance
for instance in instances:
Expand All @@ -173,6 +162,32 @@ def handle_get(event):
return
yield PENDING, instance

def handle_move(event):
# C-MOVE request event
_log_get_move_event(event)

if not event.identifier or "QueryRetrieveLevel" not in event.identifier:
yield FAILURE, None
return

# we can't actually know the requested destination server (and even if it is the
# same one that send the request we don't know the port), so we yield (None, None)
# which results in the server returning 0xA801 (move destination unknown)
yield None, None

def _log_get_move_event(event):
dataset = event.identifier
log_data = {
key: getattr(dataset, key, None)
for key in (
"QueryRetrieveLevel",
"PatientID",
"StudyInstanceUID",
"SeriesInstanceUID",
)
}
_log_event(event, log_data)

def handle_login(event: evt.Event) -> tuple[bool, bytes | None]:
# USER-ID event
# see https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_userid.html
Expand Down Expand Up @@ -250,6 +265,7 @@ def _log_event(event, additional_data: dict | None = None):
special_handlers = {
evt.EVT_USER_ID.name: handle_login,
evt.EVT_C_GET.name: handle_get,
evt.EVT_C_MOVE.name: handle_move,
}
if _q_s.store_images:
special_handlers[evt.EVT_C_STORE.name] = handle_store
Expand Down Expand Up @@ -284,7 +300,11 @@ def _log_event(event, additional_data: dict | None = None):
with patch("pynetdicom.association.DULServiceProvider", CustomDUL), patch(
"pynetdicom.ae.AssociationServer", CustomAssociationServer
):
app_entity.start_server((self.ip, self.port), block=True, evt_handlers=handlers)
app_entity.start_server(
(self.ip, self.port),
block=True,
evt_handlers=handlers,
)


def _dicom_obj_to_dict(pdu) -> dict[str, str | list[dict[str, str]]]:
Expand Down
54 changes: 47 additions & 7 deletions tests/test_dicom_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
from pynetdicom.sop_class import (
PatientRootQueryRetrieveInformationModelFind,
PatientRootQueryRetrieveInformationModelGet,
PatientRootQueryRetrieveInformationModelMove,
)
from pynetdicom.pdu_primitives import UserIdentityNegotiation
from pynetdicom import AE, tests, build_role, evt
from pynetdicom import AE, tests, build_role, evt, StoragePresentationContexts

from honeypots import QDicomServer
from honeypots.dicom_server import SUCCESS
Expand All @@ -31,6 +32,7 @@
USER_AND_PW = 2
RETRIES = 3
PORT = 61112
MOVE_DESTINATION_UNKNOWN = 0xA801


@pytest.mark.parametrize(
Expand Down Expand Up @@ -158,11 +160,7 @@ def test_get(server_logs):
# the server sends us back the requested images so we become the SCP here
role = build_role(CTImageStorage, scp_role=True)

ds = Dataset()
ds.QueryRetrieveLevel = "SERIES"
ds.PatientID = "1234567"
ds.StudyInstanceUID = "1.2.3"
ds.SeriesInstanceUID = "1.2.3.4"
ds = _get_test_dataset()

with wait_for_server(PORT + 3):
association = _retry_association(ae, PORT + 3, ext_neg=[role], handlers=handlers)
Expand All @@ -177,5 +175,47 @@ def test_get(server_logs):
assert "C-GET" in logs_by_action
assert logs_by_action["C-GET"]["data"]["PatientID"] == ds.PatientID

assert len(responses) == 3
assert len(responses) == 2
assert _event.is_set()


@pytest.mark.parametrize(
"server_logs",
[{"server": QDicomServer, "port": PORT + 4}],
indirect=True,
)
def test_move(server_logs):
ae = AE()
ae.add_requested_context(PatientRootQueryRetrieveInformationModelMove)
ae.supported_contexts = StoragePresentationContexts

ds = _get_test_dataset()

with wait_for_server(PORT + 4):
association = _retry_association(ae, PORT + 4)
responses = list(
association.send_c_move(
ds, "some unknown AE", PatientRootQueryRetrieveInformationModelMove
)
)
association.release()

logs = load_logs_from_file(server_logs)

assert len(logs) > 2
logs_by_action = _get_logs_by_action(logs)
assert "C-MOVE" in logs_by_action
assert logs_by_action["C-MOVE"]["data"]["PatientID"] == ds.PatientID

assert len(responses) == 1
status, _ = responses[0]
assert status.Status == MOVE_DESTINATION_UNKNOWN


def _get_test_dataset():
ds = Dataset()
ds.QueryRetrieveLevel = "SERIES"
ds.PatientID = "1234567"
ds.StudyInstanceUID = "1.2.3"
ds.SeriesInstanceUID = "1.2.3.4"
return ds

0 comments on commit eff8676

Please sign in to comment.