Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
SOAP.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
9#include "Stroika/Foundation/Containers/Mapping.h"
10#include "Stroika/Foundation/DataExchange/BadFormatException.h"
14#include "Stroika/Foundation/DataExchange/XML/WriterUtils.h"
16#include "Stroika/Foundation/Execution/Throw.h"
17#include "Stroika/Foundation/IO/Network/HTTP/Headers.h"
18#include "Stroika/Foundation/IO/Network/HTTP/Methods.h"
19#include "Stroika/Foundation/IO/Network/HTTP/Status.h"
20#include "Stroika/Foundation/IO/Network/Transfer/Connection.h"
21#include "Stroika/Foundation/IO/Network/Transfer/Exception.h"
22#include "Stroika/Foundation/Streams/MemoryStream.h"
23#if qStroika_Foundation_DataExchange_XML_SupportDOM
24#include "Stroika/Foundation/DataExchange/XML/DOM.h"
25#endif
26
27#include "SOAP.h"
28
29using namespace Stroika::Foundation;
33
34using namespace Stroika::Frameworks;
35using namespace Stroika::Frameworks::WebService;
37
38namespace {
39 constexpr string_view kEnvelopeNamespace_ = "http://schemas.xmlsoap.org/soap/envelope/"sv;
40
41 optional<String> Lookup_ (const Arguments& arguments, const String& name)
42 {
43 for (const KeyValuePair<String, String>& a : arguments) {
44 if (a.fKey == name) {
45 return a.fValue;
46 }
47 }
48 return nullopt;
49 }
50
51#if qStroika_Foundation_DataExchange_XML_SupportDOM
52 namespace WriteElement = XML::DOM::Element;
53
54 constexpr string_view kEnvelopePrefix_ = "s"sv;
55 constexpr string_view kMethodPrefix_ = "m"sv;
56 const IO::Network::URI kEnvelopeNamespaceURI_{String{kEnvelopeNamespace_}};
57 const IO::Network::URI kEncodingNamespaceURI_{"http://schemas.xmlsoap.org/soap/encoding/"sv};
58
59 /*
60 * The SOAP envelope round a body (section 4), and that Body to put the one element of the message in. One struct, with
61 * the Document FIRST, so the Body - a node of it - is destroyed before the document it points into.
62 */
63 struct Envelope_ {
65 WriteElement::Ptr fBody;
66
67 Envelope_ (const Options& options)
68 {
69 /*
70 * s: - the prefix everyone writes SOAP with - said at the root, so that it and everything under it are
71 * written with it, and not left to whatever a provider would invent. A Fault's code NAMES the prefix in
72 * text (@see Serialize (const Fault&)), so it has to be one we chose; and declaring it rather than a
73 * default namespace is what lets a Fault's unqualified children simply be unqualified, with no xmlns=""
74 * to escape a default that is not there.
75 */
76 fDoc = XML::DOM::Document::New (options.fXMLOptions);
77 WriteElement::Ptr envelope =
78 fDoc.ReplaceRootElement (XML::NameWithNamespace{kEnvelopeNamespaceURI_, "Envelope"sv}, String{kEnvelopePrefix_});
79 fBody = envelope.Append ("Body"sv);
80 // where options say - by default nowhere (@see EncodingStyleAttribute)
81 const XML::NameWithNamespace kEncodingStyle_{kEnvelopeNamespaceURI_, "encodingStyle"sv};
82 switch (options.fEncodingStyleAttribute) {
83 case EncodingStyleAttribute::eOnBody:
84 fBody.SetAttribute (kEncodingStyle_, kEncodingNamespaceURI_.As<String> ());
85 break;
86 case EncodingStyleAttribute::eOnEnvelope:
87 envelope.SetAttribute (kEncodingStyle_, kEncodingNamespaceURI_.As<String> ());
88 break;
89 case EncodingStyleAttribute::eOmitted:
90 break;
91 }
92 }
93 Memory::BLOB Write () const
94 {
95 Streams::MemoryStream::Ptr<std::byte> out = Streams::MemoryStream::New<std::byte> ();
96 fDoc.Write (out);
97 return out.As<Memory::BLOB> ();
98 }
99 };
100
101 // a call's or answer's element: elementName, in the method's namespace, its arguments its elements (section 7.1) - which
102 // are unqualified, where the method's namespace would otherwise be inherited
103 void AppendMethodElement_ (WriteElement::Ptr body, const String& elementName, const String& methodNamespace, const Arguments& arguments)
104 {
105 // m: - what SOAP's own RPC examples call a method's namespace - declared before the element is made, so that
106 // making it finds the prefix rather than inventing one
107 body.SetAttribute (XML::NameWithNamespace{XML::kXMLNS.fNamespace, kMethodPrefix_}, methodNamespace);
108 WriteElement::Ptr method = body.Append (XML::NameWithNamespace{IO::Network::URI{methodNamespace}, elementName});
109 for (const KeyValuePair<String, String>& a : arguments) {
110 method.Append (XML::NameWithNamespace{XML::kNoNamespace, a.fKey}, a.fValue);
111 }
112 }
113#endif
114
115#if qStroika_Foundation_DataExchange_XML_SupportDOM
116 namespace Element = XML::DOM::Element;
117
118 // f (the Body's one element - a call, an answer, or a fault), while its document is kept: its elements do not keep it
119 template <typename RESULT>
120 RESULT WithBodyElement_ (const Memory::BLOB& b, const Options& options, const function<RESULT (const Element::Ptr&)>& f)
121 {
122 using namespace XML::DOM;
123 auto child = [] (const Element::Ptr& e, const optional<String>& name) -> Element::Ptr {
124 for (const Element::Ptr& c : e.GetChildElements ()) {
125 if (not name or c.GetName ().fName == *name) {
126 return c;
127 }
128 }
129 return nullptr;
130 };
131 // a schema, where the caller named one, is checked AS it reads - so a message that does not hold throws here
132 // rather than being half understood. Named, never reached for: @see Options::fValidateReadAgainst on why
133 Document::Ptr doc = Document::New (b.As<Streams::InputStream::Ptr<std::byte>> (), options.fValidateReadAgainst, options.fXMLOptions);
134 Element::Ptr envelope = doc.GetRootElement ();
135 Element::Ptr body = envelope == nullptr or envelope.GetName ().fName != "Envelope"sv ? nullptr : child (envelope, "Body"sv);
136 Element::Ptr top = body == nullptr ? nullptr : child (body, nullopt);
137 if (top == nullptr) {
138 Execution::Throw (BadFormatException{"not a SOAP message: no element in an Envelope's Body"sv});
139 }
140 return f (top);
141 }
142 String NamespaceOf_ (const Element::Ptr& e)
143 {
144 optional<URI> ns = e.GetName ().fNamespace;
145 return ns ? ns->As<String> () : String{};
146 }
147 bool IsFault_ (const Element::Ptr& e)
148 {
149 return e.GetName ().fName == "Fault"sv and NamespaceOf_ (e) == kEnvelopeNamespace_;
150 }
151 // its child elements, each its name and text: a call's or answer's arguments
152 Arguments ChildValues_ (const Element::Ptr& e)
153 {
154 Arguments result;
155 for (const Element::Ptr& c : e.GetChildElements ()) {
156 result.Append ({c.GetName ().fName, c.GetValue ()});
157 }
158 return result;
159 }
160#else
161 [[noreturn]] void NoXMLDOM_ ()
162 {
163 Execution::Throw (Execution::Exception<runtime_error>{"SOAP messages cannot be read or written: this build has no XML DOM"sv});
164 }
165#endif
166}
167
168/*
169 ********************************************************************************
170 ************************* WebService::SOAP::Request ****************************
171 ********************************************************************************
172 */
173optional<String> SOAP::Request::LookupArgument (const String& name) const
174{
175 return Lookup_ (fArguments, name);
176}
177
179{
180 StringBuilder sb;
181 sb << "{"sv;
182 sb << "Namespace: "sv << fNamespace;
183 sb << ", Method: "sv << fMethod;
184 sb << ", Arguments: "sv << fArguments;
185 sb << "}"sv;
186 return sb;
187}
188
189/*
190 ********************************************************************************
191 ************************** WebService::SOAP::Response **************************
192 ********************************************************************************
193 */
194optional<String> SOAP::Response::LookupArgument (const String& name) const
195{
196 return Lookup_ (fArguments, name);
197}
198
200{
201 StringBuilder sb;
202 sb << "{"sv;
203 sb << "Namespace: "sv << fNamespace;
204 sb << ", Method: "sv << fMethod;
205 sb << ", Arguments: "sv << fArguments;
206 sb << "}"sv;
207 return sb;
208}
209
210/*
211 ********************************************************************************
212 **************************** WebService::SOAP::Fault ***************************
213 ********************************************************************************
214 */
216{
217 StringBuilder sb;
218 sb << "{"sv;
219 sb << "Fault-Code: "sv << fFaultCode;
220 sb << ", Fault-String: "sv << fFaultString;
221 if (fDetail) {
222 sb << ", Detail: "sv << *fDetail;
223 }
224 sb << "}"sv;
225 return sb;
226}
227
228/*
229 ********************************************************************************
230 ************************ WebService::SOAP::FaultException **********************
231 ********************************************************************************
232 */
233FaultException::FaultException (const Fault& fault)
234 : Execution::Exception<runtime_error>{"SOAP fault {}: {}"_f(fault.fFaultCode, fault.fFaultString)}
235 , fFault_{fault}
236{
237}
238
239Fault FaultException::GetFault () const
240{
241 return fFault_;
242}
243
244/*
245 ********************************************************************************
246 ************************** WebService::SOAP::Serialize *************************
247 ********************************************************************************
248 */
249Memory::BLOB SOAP::Serialize ([[maybe_unused]] const Request& request, [[maybe_unused]] const Options& options)
250{
251#if qStroika_Foundation_DataExchange_XML_SupportDOM
252 Envelope_ envelope{options};
253 AppendMethodElement_ (envelope.fBody, request.fMethod, request.fNamespace, request.fArguments);
254 return envelope.Write ();
255#else
256 NoXMLDOM_ ();
257#endif
258}
259
260Memory::BLOB SOAP::Serialize ([[maybe_unused]] const Response& response, [[maybe_unused]] const Options& options)
261{
262#if qStroika_Foundation_DataExchange_XML_SupportDOM
263 Envelope_ envelope{options};
264 AppendMethodElement_ (envelope.fBody, response.fMethod + "Response"sv, response.fNamespace, response.fArguments);
265 return envelope.Write ();
266#else
267 NoXMLDOM_ ();
268#endif
269}
270
271Memory::BLOB SOAP::Serialize ([[maybe_unused]] const Fault& fault, [[maybe_unused]] const Options& options)
272{
273#if qStroika_Foundation_DataExchange_XML_SupportDOM
274 Require (XML::IsNCName (fault.fFaultCode)); // it is written as a QName's local part - @see Fault::fFaultCode
275 Envelope_ envelope{options};
276 WriteElement::Ptr faultElt = envelope.fBody.Append ("Fault"sv);
277 // SOAP 1.1 has a Fault's own children unqualified (section 4.4), where the envelope's namespace would be inherited
278 // a QName, naming the envelope namespace through the prefix declared on it (section 4.4)
279 faultElt.Append (XML::NameWithNamespace{XML::kNoNamespace, "faultcode"sv}, String{kEnvelopePrefix_} + ":"sv + fault.fFaultCode);
280 faultElt.Append (XML::NameWithNamespace{XML::kNoNamespace, "faultstring"sv}, fault.fFaultString);
281 if (fault.fDetail) {
282 WriteElement::Ptr detail = faultElt.Append (XML::NameWithNamespace{XML::kNoNamespace, "detail"sv});
283 // the detail is the application's own XML: parsed, so a malformed one is caught here rather than written out as a
284 // malformed envelope, and imported as ELEMENTS - escaping it into text would change what the receiver reads
285 // the SAME provider the envelope is being built with, since ImportNode requires that of both documents - with the
286 // default here instead, a caller naming a provider got a failed Require rather than a fault
287 XML::DOM::Document::Ptr detailDoc = XML::DOM::Document::New (*fault.fDetail, nullptr, options.fXMLOptions);
288 detail.ImportNode (detailDoc.GetRootElement ());
289 }
290 return envelope.Write ();
291#else
292 NoXMLDOM_ ();
293#endif
294}
295
296/*
297 ********************************************************************************
298 ************************* WebService::SOAP::DeSerialize ************************
299 ********************************************************************************
300 */
301void SOAP::DeSerialize ([[maybe_unused]] const Memory::BLOB& b, Request* request, [[maybe_unused]] const Options& options)
302{
303 RequireNotNull (request);
304#if qStroika_Foundation_DataExchange_XML_SupportDOM
305 *request = WithBodyElement_<Request> (b, options, [] (const Element::Ptr& e) {
306 if (IsFault_ (e)) {
307 Execution::Throw (BadFormatException{"not a SOAP call: a fault"sv});
308 }
309 return Request{.fNamespace = NamespaceOf_ (e), .fMethod = e.GetName ().fName, .fArguments = ChildValues_ (e)};
310 });
311#else
312 NoXMLDOM_ ();
313#endif
314}
315
316void SOAP::DeSerialize ([[maybe_unused]] const Memory::BLOB& b, Response* response, [[maybe_unused]] const Options& options)
317{
318 RequireNotNull (response);
319#if qStroika_Foundation_DataExchange_XML_SupportDOM
320 *response = WithBodyElement_<Response> (b, options, [] (const Element::Ptr& e) {
321 const String name = e.GetName ().fName;
322 if (IsFault_ (e) or not name.EndsWith ("Response"sv)) {
323 Execution::Throw (BadFormatException{"not a SOAP call's answer: {}"_f(name)});
324 }
325 return Response{.fNamespace = NamespaceOf_ (e), .fMethod = name.SubString (0, name.size () - "Response"sv.size ()), .fArguments = ChildValues_ (e)};
326 });
327#else
328 NoXMLDOM_ ();
329#endif
330}
331
332void SOAP::DeSerialize ([[maybe_unused]] const Memory::BLOB& b, Fault* fault, [[maybe_unused]] const Options& options)
333{
334 RequireNotNull (fault);
335#if qStroika_Foundation_DataExchange_XML_SupportDOM
336 *fault = WithBodyElement_<Fault> (b, options, [] (const Element::Ptr& e) {
337 if (not IsFault_ (e)) {
338 Execution::Throw (BadFormatException{"not a SOAP fault: {}"_f(e.GetName ().fName)});
339 }
340 Fault result;
341 for (const Element::Ptr& c : e.GetChildElements ()) {
342 const String name = c.GetName ().fName;
343 if (name == "faultcode"sv) {
344 const String code = c.GetValue ().Trim ();
345 optional<size_t> colon = code.Find (':');
346 String local = colon ? code.SubString (*colon + 1) : code; // its prefix is the envelope's, whatever it is
347 if (not XML::IsNCName (local)) {
348 // not a QName, as a faultcode must be - read leniently (checked, the schema has refused it already): as its
349 // longest leading part that is a name, or Server, so that what is read can always be written back
350 size_t n = local.size ();
351 while (n > 0 and not XML::IsNCName (local.SubString (0, n))) {
352 --n;
353 }
354 const String readAs = n == 0 ? Fault::kServer : local.SubString (0, n);
355 DbgTrace ("faultcode '{}' is not a QName: read as '{}'"_f, code, readAs);
356 local = readAs;
357 }
358 result.fFaultCode = local;
359 }
360 else if (name == "faultstring"sv) {
361 result.fFaultString = c.GetValue ();
362 }
363 else if (name == "detail"sv) {
364 StringBuilder sb;
365 for (const Element::Ptr& d : c.GetChildElements ()) {
366 sb << d.ToString ();
367 }
368 result.fDetail = sb.str ();
369 }
370 }
371 return result;
372 });
373#else
374 NoXMLDOM_ ();
375#endif
376}
377
378/*
379 ********************************************************************************
380 **************************** WebService::SOAP::Invoke **************************
381 ********************************************************************************
382 */
383SOAP::Response SOAP::Invoke (const URI& url, const String& soapAction, const Request& request, const Options& options)
384{
385 return Invoke (Transfer::Connection::New (), url, soapAction, request, options);
386}
387
388SOAP::Response SOAP::Invoke (Transfer::Connection::Ptr c, const URI& url, const String& soapAction, const Request& request, const Options& options)
389{
392 r.fMethod = HTTP::Methods::kPost;
393 r.fAuthorityRelativeURL = url.GetAuthorityRelativeResource<URI> ();
394 r.fOverrideHeaders = Containers::Mapping<String, String>{{String{HTTP::HeaderName::kSOAPAction}, soapAction}};
395 r.SetTypedBLOB (TypedBLOB{.fData = Serialize (request, options), .fType = kContentType});
396 Transfer::Response answer = c.Send (r); // not SendAndThrowOnFailure: a fault's answer - status 500 - says what the fault was
397 if (answer.GetSucceeded ()) {
398 Response response;
399 DeSerialize (answer.GetData (), &response, options);
400 return response;
401 }
402 if (answer.GetStatus () == HTTP::StatusCodes::kInternalError) {
403 optional<Fault> fault;
404 try {
405 Fault f;
406 DeSerialize (answer.GetData (), &f, options);
407 fault = f;
408 }
409 catch (const BadFormatException&) {
410 // a 500 that says nothing SOAP: as any other failed answer
411 }
412 if (fault) {
414 }
415 }
416 Execution::Throw (Transfer::Exception{answer});
417}
418
419/*
420 ********************************************************************************
421 ************************ WebService::SOAP::HandleRequest ***********************
422 ********************************************************************************
423 */
424void SOAP::HandleRequest (WebServer::Message& m, const function<Response (const Request&)>& handler, const Options& options)
425{
426 static const InternetMediaType kTextXML_{"text/xml"sv};
427 WebServer::Response& response = m.rwResponse ();
428 // its body must be text/xml (with any charset), or it is refused: 415
429 if (optional<InternetMediaType> ct = m.request ().headers ().contentType (); not ct or not InternetMediaTypeRegistry::sThe->IsA (kTextXML_, *ct)) {
430 response.status = HTTP::StatusCodes::kUnsupportedMediaType;
431 return;
432 }
433 response.contentType = kContentType;
434 auto answerFault = [&] (const Fault& fault) {
435 response.status = HTTP::StatusCodes::kInternalError; // how SOAP says it failed: section 6.2
436 response.write (Serialize (fault, options));
437 };
438 Request request;
439 try {
440 DeSerialize (m.rwRequest ().GetBody (), &request, options);
441 }
442 catch (const BadFormatException& e) {
443 answerFault (Fault{.fFaultCode = Fault::kClient, .fFaultString = "not a SOAP call: {}"_f(e)});
444 return;
445 }
446 try {
447 response.write (Serialize (handler (request), options));
448 }
449 catch (const FaultException& e) {
450 answerFault (e.GetFault ());
451 }
452}
#define RequireNotNull(p)
Definition Assertions.h:350
#define DbgTrace
Definition Trace.h:327
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
Characters::String ToString() const
same as str (): so a StringBuilder formats as its text, with _f and std::format alike
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual size_t size() const noexcept
Definition String.inl:536
nonvirtual bool EndsWith(const Character &c, CompareOptions co=eWithCase) const
Definition String.cpp:1147
nonvirtual String SubString(SZ from) const
nonvirtual String Trim(bool(*shouldBeTrimmed)(Character)=Character::IsWhitespace) const
Definition String.cpp:1651
nonvirtual optional< size_t > Find(Character c, CompareOptions co=eWithCase) const
Definition String.inl:687
A Mapping uniquely associates two elements: a key and a value (use Assocation to allow multiple value...
A generalization of a vector: a container whose elements are keyed by the natural numbers.
nonvirtual void Append(ArgByValueType< value_type > item)
Definition Sequence.inl:615
static Execution::Synchronized< InternetMediaTypeRegistry > sThe
TypedBLOB is a named tuple<Memory::BLOB, optional<InternetMediaType>> - with friendlier names,...
Definition TypedBLOB.h:48
Document::Ptr is create with Document::New, and is a smart pointer to a DOM document object.
Definition DOM.h:721
nonvirtual void Write(const Streams::OutputStream::Ptr< byte > &to, const SerializationOptions &options={}) const
The document as XML: to a byte stream as UTF-8, with no byte order mark - or as a String.
Definition DOM.inl:380
nonvirtual Element::Ptr GetRootElement() const
always returns Node of eElement or return nullptr if none
Definition DOM.inl:395
nonvirtual Element::Ptr ReplaceRootElement(const NameWithNamespace &newEltName, const optional< String > &namespacePrefix=nullopt) const
Make this the document's root element - and say HOW its namespace is declared on it,...
Definition DOM.inl:405
Exception<> is a replacement (subclass) for any std c++ exception class (e.g. the default 'std::excep...
Definition Exceptions.h:219
nonvirtual URI GetSchemeAndAuthority() const
Definition URI.inl:90
nonvirtual RETURN_TYPE GetAuthorityRelativeResource() const
Return the (PCT etc encoded if a string) data AFTER the authority, but not including the fragment.
InputStream<>::Ptr is Smart pointer (with abstract Rep) class defining the interface to reading from ...
Common::ReadOnlyProperty< Response & > rwResponse
Definition Message.h:93
Common::ReadOnlyProperty< const Request & > request
Definition Message.h:72
Common::ReadOnlyProperty< Request & > rwRequest
Definition Message.h:80
A Fault, thrown: Invoke throws the one a server answers with, and a HandleRequest handler throws one ...
Definition SOAP.h:184
void Throw(T &&e2Throw)
like the builtin C++ 'throw', but stamps in the current Activity context and does helpful,...
Definition Throw.inl:54
const InternetMediaType kContentType
Definition SOAP.h:73
void DeSerialize(const Memory::BLOB &b, Request *request, const Options &options={})
The SOAP message read: what Serialize writes, and any other's. Throws DataExchange::BadFormatExceptio...
Definition SOAP.cpp:301
Memory::BLOB Serialize(const Request &request, const Options &options={})
The SOAP message - its XML, UTF-8 - which sends it: a call, its answer, or a fault.
Definition SOAP.cpp:249
Response Invoke(const URI &url, const String &soapAction, const Request &request, const Options &options={})
Call a method: POST request to url, with soapAction (in quotes, as SOAPAction headers are: section 6....
Definition SOAP.cpp:383
void HandleRequest(WebServer::Message &m, const function< Response(const Request &)> &handler, const Options &options={})
Answer a call, at its URL's route: 415 (Unsupported Media Type) if its body is not text/xml; else its...
An element's or attribute's name (local-name, not including a prefix), plus the namespace that name i...
Definition Namespace.h:63
optional< URI > fNamespace
The namespace fName is in, however the document said so - nullopt for a name in no namespace.
Definition Namespace.h:72
A call's answer, the method having not been done: why not (section 4.4) - sent in an HTTP response wh...
Definition SOAP.h:143
nonvirtual String ToString() const
Definition SOAP.cpp:215
What SOAP must be told that has a sensible default - taken by Serialize and DeSerialize,...
Definition SOAP.h:252
EncodingStyleAttribute fEncodingStyleAttribute
Where Serialize writes encodingStyle - nowhere, the default, so what is written holds to SOAP's schem...
Definition SOAP.h:297
DataExchange::XML::Schema::Ptr fValidateReadAgainst
Check a message being read against this schema - EnvelopeSchema - throwing where it does not hold; nu...
Definition SOAP.h:288
DataExchange::XML::Options fXMLOptions
How XML is read and written - by default, with Stroika's default provider; nearly always left as is (...
Definition SOAP.h:257
A call of a method (section 7.1): the Body's one element, named the method, in its namespace - its in...
Definition SOAP.h:84
nonvirtual optional< String > LookupArgument(const String &name) const
Definition SOAP.cpp:173
A call's answer, the method having been done (section 7.1): the Body's one element,...
Definition SOAP.h:112
nonvirtual optional< String > LookupArgument(const String &name) const
Definition SOAP.cpp:194