PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
retrieve_envelope.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
10#include "retrieve_envelope.h"
11
12#include <pugixml.hpp>
13
14#include <chrono>
15#include <cstdint>
16#include <random>
17#include <sstream>
18#include <string>
19#include <string_view>
20
22
23namespace {
24
25constexpr const char* kSoapEnv = "http://www.w3.org/2003/05/soap-envelope";
26constexpr const char* kWsa = "http://www.w3.org/2005/08/addressing";
27constexpr const char* kWsse =
28 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
29constexpr const char* kWsu =
30 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
31constexpr const char* kXdsb = "urn:ihe:iti:xds-b:2007";
32
33// Per-envelope id generator; not RFC 4122 compliant. See the matching
34// helper in soap_envelope.cpp for rationale.
35std::string make_xml_id(std::string_view prefix) {
36 static thread_local std::mt19937_64 rng(
37 static_cast<std::uint64_t>(
38 std::chrono::steady_clock::now().time_since_epoch().count()));
39 std::uniform_int_distribution<std::uint64_t> dist;
40 std::ostringstream oss;
41 oss << prefix << '-' << std::hex << dist(rng) << dist(rng);
42 return oss.str();
43}
44
45std::string utc_iso8601_now() {
46 const auto now = std::chrono::system_clock::now();
47 const auto tt = std::chrono::system_clock::to_time_t(now);
48 std::tm tm{};
49#ifdef _WIN32
50 gmtime_s(&tm, &tt);
51#else
52 gmtime_r(&tt, &tm);
53#endif
54 char buf[32];
55 std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
56 return std::string(buf);
57}
58
59struct xml_string_writer : pugi::xml_writer {
60 std::string out;
61 void write(const void* data, std::size_t size) override {
62 out.append(static_cast<const char*>(data), size);
63 }
64};
65
66} // namespace
67
69 const retrieve_request& req) {
70 if (req.document_unique_id.empty()) {
71 return kcenon::common::make_error<built_envelope>(
72 static_cast<int>(
74 "retrieve_request.document_unique_id is empty",
75 std::string(error_source));
76 }
77 if (req.repository_unique_id.empty()) {
78 return kcenon::common::make_error<built_envelope>(
79 static_cast<int>(
81 "retrieve_request.repository_unique_id is empty",
82 std::string(error_source));
83 }
84
85 pugi::xml_document doc;
86 auto decl = doc.append_child(pugi::node_declaration);
87 decl.append_attribute("version") = "1.0";
88 decl.append_attribute("encoding") = "UTF-8";
89
90 auto env = doc.append_child("soap:Envelope");
91 env.append_attribute("xmlns:soap") = kSoapEnv;
92 env.append_attribute("xmlns:wsa") = kWsa;
93 env.append_attribute("xmlns:wsse") = kWsse;
94 env.append_attribute("xmlns:wsu") = kWsu;
95 env.append_attribute("xmlns:xdsb") = kXdsb;
96
97 auto header = env.append_child("soap:Header");
98
99 auto action = header.append_child("wsa:Action");
100 action.append_attribute("soap:mustUnderstand") = "true";
101 action.text().set("urn:ihe:iti:2007:RetrieveDocumentSet");
102
103 auto message_id = header.append_child("wsa:MessageID");
104 message_id.text().set(("urn:uuid:" + make_xml_id("msg")).c_str());
105
106 auto security = header.append_child("wsse:Security");
107 security.append_attribute("soap:mustUnderstand") = "true";
108
110 out.timestamp_id = make_xml_id("ts");
111 out.binary_security_token_id = make_xml_id("bst");
112 out.body_id = make_xml_id("body");
113
114 auto timestamp = security.append_child("wsu:Timestamp");
115 timestamp.append_attribute("wsu:Id") = out.timestamp_id.c_str();
116 auto created = timestamp.append_child("wsu:Created");
117 const std::string now = utc_iso8601_now();
118 created.text().set(now.c_str());
119
120 auto bst = security.append_child("wsse:BinarySecurityToken");
121 bst.append_attribute("wsu:Id") = out.binary_security_token_id.c_str();
122 bst.append_attribute("EncodingType") =
123 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-"
124 "security-1.0#Base64Binary";
125 bst.append_attribute("ValueType") =
126 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-"
127 "profile-1.0#X509v3";
128
129 auto body = env.append_child("soap:Body");
130 body.append_attribute("wsu:Id") = out.body_id.c_str();
131
132 auto rq = body.append_child("xdsb:RetrieveDocumentSetRequest");
133 auto doc_req = rq.append_child("xdsb:DocumentRequest");
134
135 // TODO(#1129-follow-up): emit xdsb:HomeCommunityId here when XCA
136 // Cross-Community Access support lands. ITI-43 intra-community
137 // retrieve (the scope of this PR) omits the element.
138 doc_req.append_child("xdsb:RepositoryUniqueId")
139 .text()
140 .set(req.repository_unique_id.c_str());
141 doc_req.append_child("xdsb:DocumentUniqueId")
142 .text()
143 .set(req.document_unique_id.c_str());
144
145 xml_string_writer writer;
146 doc.save(writer, "", pugi::format_raw | pugi::format_no_declaration,
147 pugi::encoding_utf8);
148 if (writer.out.empty()) {
149 return kcenon::common::make_error<built_envelope>(
151 "pugixml produced an empty envelope",
152 std::string(error_source));
153 }
154
155 out.xml = R"(<?xml version="1.0" encoding="UTF-8"?>)";
156 out.xml += writer.out;
157 return out;
158}
159
160} // namespace kcenon::pacs::ihe::xds::detail
kcenon::common::Result< built_envelope > build_iti43_envelope(const retrieve_request &req)
Build a SOAP envelope for ITI-43 from a retrieve_request.
constexpr std::string_view error_source
Short identifier used as the "source" field of make_error.
Definition errors.h:95
SOAP 1.2 envelope builder for IHE XDS.b ITI-43 RetrieveDocumentSet.
std::string out
Result of envelope construction.
ITI-43 RetrieveDocumentSet request target.