1 /****************************************************************************
2 ** Copyright (C) 2010-2020 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com.
3 ** All rights reserved.
4 **
5 ** This file is part of the KD Soap library.
6 **
7 ** Licensees holding valid commercial KD Soap licenses may use this file in
8 ** accordance with the KD Soap Commercial License Agreement provided with
9 ** the Software.
10 **
11 **
12 ** This file may be distributed and/or modified under the terms of the
13 ** GNU Lesser General Public License version 2.1 and version 3 as published by the
14 ** Free Software Foundation and appearing in the file LICENSE.LGPL.txt included.
15 **
16 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
17 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
18 **
19 ** Contact info@kdab.com if any conditions of this licensing are not
20 ** clear to you.
21 **
22 **********************************************************************/
23 #include "KDSoapClientInterface.h"
24 #include "KDSoapClientInterface_p.h"
25 #include "KDSoapNamespaceManager.h"
26 #include "KDSoapMessageWriter_p.h"
27 #ifndef QT_NO_OPENSSL
28 #include "KDSoapSslHandler.h"
29 #include "KDSoapReplySslHandler_p.h"
30 #endif
31 #include "KDSoapPendingCall_p.h"
32 #include <QSslConfiguration>
33 #include <QNetworkRequest>
34 #include <QNetworkReply>
35 #include <QAuthenticator>
36 #include <QDebug>
37 #include <QBuffer>
38 #include <QNetworkProxy>
39 #include <QTimer>
40
KDSoapClientInterface(const QString & endPoint,const QString & messageNamespace)41 KDSoapClientInterface::KDSoapClientInterface(const QString &endPoint, const QString &messageNamespace)
42 : d(new KDSoapClientInterfacePrivate)
43 {
44 d->m_endPoint = endPoint;
45 d->m_messageNamespace = messageNamespace;
46 d->m_version = KDSoap::SOAP1_1;
47 }
48
~KDSoapClientInterface()49 KDSoapClientInterface::~KDSoapClientInterface()
50 {
51 d->m_thread.stop();
52 d->m_thread.wait();
53 delete d;
54 }
55
setSoapVersion(KDSoapClientInterface::SoapVersion version)56 void KDSoapClientInterface::setSoapVersion(KDSoapClientInterface::SoapVersion version)
57 {
58 d->m_version = static_cast<KDSoap::SoapVersion>(version);
59 }
60
soapVersion() const61 KDSoapClientInterface::SoapVersion KDSoapClientInterface::soapVersion() const
62 {
63 return static_cast<KDSoapClientInterface::SoapVersion>(d->m_version);
64 }
65
KDSoapClientInterfacePrivate()66 KDSoapClientInterfacePrivate::KDSoapClientInterfacePrivate()
67 : m_accessManager(nullptr),
68 m_authentication(),
69 m_version(KDSoap::SOAP1_1),
70 m_style(KDSoapClientInterface::RPCStyle),
71 m_ignoreSslErrors(false),
72 m_timeout(30 * 60 * 1000) // 30 minutes, as documented
73 {
74 #ifndef QT_NO_OPENSSL
75 m_sslHandler = nullptr;
76 #endif
77 }
78
~KDSoapClientInterfacePrivate()79 KDSoapClientInterfacePrivate::~KDSoapClientInterfacePrivate()
80 {
81 #ifndef QT_NO_OPENSSL
82 delete m_sslHandler;
83 #endif
84 }
85
accessManager()86 QNetworkAccessManager *KDSoapClientInterfacePrivate::accessManager()
87 {
88 if (!m_accessManager) {
89 m_accessManager = new QNetworkAccessManager(this);
90 connect(m_accessManager, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)),
91 this, SLOT(_kd_slotAuthenticationRequired(QNetworkReply*,QAuthenticator*)));
92 }
93 return m_accessManager;
94 }
95
prepareRequest(const QString & method,const QString & action)96 QNetworkRequest KDSoapClientInterfacePrivate::prepareRequest(const QString &method, const QString &action)
97 {
98 QNetworkRequest request(QUrl(this->m_endPoint));
99
100 QString soapAction = action;
101
102 if (soapAction.isNull()) {
103 // The automatic generation of SoapAction done in this block is a mistake going back to KDSoap 1.0.
104 // The spec says "there is no default value for SoapAction" (https://www.w3.org/TR/wsdl#_soap:operation)
105 // but we keep this for compatibility, when nothing was passed as argument (see the webcalls unittest)
106 soapAction = this->m_messageNamespace;
107 if (!soapAction.endsWith(QLatin1Char('/'))) {
108 soapAction += QLatin1Char('/');
109 }
110 soapAction += method;
111 }
112 //qDebug() << "soapAction=" << soapAction;
113
114 QString soapHeader;
115 if (m_version == KDSoap::SOAP1_1) {
116 soapHeader += QString::fromLatin1("text/xml;charset=utf-8");
117 request.setRawHeader("SoapAction", '\"' + soapAction.toUtf8() + '\"');
118 } else if (m_version == KDSoap::SOAP1_2) {
119 soapHeader += QString::fromLatin1("application/soap+xml;charset=utf-8;action=") + soapAction;
120 }
121
122 request.setHeader(QNetworkRequest::ContentTypeHeader, soapHeader.toUtf8());
123
124 // FIXME need to find out which version of Qt this is no longer necessary
125 // without that the server might respond with gzip compressed data and
126 // Qt 4.6.2 fails to decode that properly
127 //
128 // happens with retrieval calls in against SugarCRM 5.5.1 running on Apache 2.2.15
129 // when the response seems to reach a certain size threshold
130 request.setRawHeader("Accept-Encoding", "compress");
131
132 for (QMap<QByteArray, QByteArray>::const_iterator it = m_httpHeaders.constBegin(); it != m_httpHeaders.constEnd(); ++it) {
133 request.setRawHeader(it.key(), it.value());
134 }
135
136 #ifndef QT_NO_OPENSSL
137 if (!m_sslConfiguration.isNull()) {
138 request.setSslConfiguration(m_sslConfiguration);
139 }
140 #endif
141
142 return request;
143 }
144
prepareRequestBuffer(const QString & method,const KDSoapMessage & message,const KDSoapHeaders & headers)145 QBuffer *KDSoapClientInterfacePrivate::prepareRequestBuffer(const QString &method, const KDSoapMessage &message, const KDSoapHeaders &headers)
146 {
147 KDSoapMessageWriter msgWriter;
148 msgWriter.setMessageNamespace(m_messageNamespace);
149 msgWriter.setVersion(m_version);
150 const QByteArray data = msgWriter.messageToXml(message, (m_style == KDSoapClientInterface::RPCStyle) ? method : QString(), headers, m_persistentHeaders, m_authentication);
151 QBuffer *buffer = new QBuffer;
152 buffer->setData(data);
153 buffer->open(QIODevice::ReadOnly);
154 return buffer;
155 }
156
asyncCall(const QString & method,const KDSoapMessage & message,const QString & soapAction,const KDSoapHeaders & headers)157 KDSoapPendingCall KDSoapClientInterface::asyncCall(const QString &method, const KDSoapMessage &message, const QString &soapAction, const KDSoapHeaders &headers)
158 {
159 QBuffer *buffer = d->prepareRequestBuffer(method, message, headers);
160 QNetworkRequest request = d->prepareRequest(method, soapAction);
161 QNetworkReply *reply = d->accessManager()->post(request, buffer);
162 d->setupReply(reply);
163 maybeDebugRequest(buffer->data(), reply->request(), reply);
164 KDSoapPendingCall call(reply, buffer);
165 call.d->soapVersion = d->m_version;
166 return call;
167 }
168
call(const QString & method,const KDSoapMessage & message,const QString & soapAction,const KDSoapHeaders & headers)169 KDSoapMessage KDSoapClientInterface::call(const QString &method, const KDSoapMessage &message, const QString &soapAction, const KDSoapHeaders &headers)
170 {
171 d->accessManager()->cookieJar(); // create it in the right thread, the secondary thread will use it
172 // Problem is: I don't want a nested event loop here. Too dangerous for GUI programs.
173 // I wanted a socket->waitFor... but we don't have access to the actual socket in QNetworkAccess.
174 // So the only option that remains is a thread and acquiring a semaphore...
175 KDSoapThreadTaskData *task = new KDSoapThreadTaskData(this, method, message, soapAction, headers);
176 task->m_authentication = d->m_authentication;
177 d->m_thread.enqueue(task);
178 if (!d->m_thread.isRunning()) {
179 d->m_thread.start();
180 }
181 task->waitForCompletion();
182 KDSoapMessage ret = task->response();
183 d->m_lastResponseHeaders = task->responseHeaders();
184 delete task;
185 return ret;
186 }
187
callNoReply(const QString & method,const KDSoapMessage & message,const QString & soapAction,const KDSoapHeaders & headers)188 void KDSoapClientInterface::callNoReply(const QString &method, const KDSoapMessage &message, const QString &soapAction, const KDSoapHeaders &headers)
189 {
190 QBuffer *buffer = d->prepareRequestBuffer(method, message, headers);
191 QNetworkRequest request = d->prepareRequest(method, soapAction);
192 QNetworkReply *reply = d->accessManager()->post(request, buffer);
193 d->setupReply(reply);
194 maybeDebugRequest(buffer->data(), reply->request(), reply);
195 QObject::connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater()));
196 QObject::connect(reply, SIGNAL(finished()), buffer, SLOT(deleteLater()));
197 }
198
_kd_slotAuthenticationRequired(QNetworkReply * reply,QAuthenticator * authenticator)199 void KDSoapClientInterfacePrivate::_kd_slotAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator)
200 {
201 m_authentication.handleAuthenticationRequired(reply, authenticator);
202 }
203
setAuthentication(const KDSoapAuthentication & authentication)204 void KDSoapClientInterface::setAuthentication(const KDSoapAuthentication &authentication)
205 {
206 d->m_authentication = authentication;
207 }
208
endPoint() const209 QString KDSoapClientInterface::endPoint() const
210 {
211 return d->m_endPoint;
212 }
213
setEndPoint(const QString & endPoint)214 void KDSoapClientInterface::setEndPoint(const QString &endPoint)
215 {
216 d->m_endPoint = endPoint;
217 }
218
setHeader(const QString & name,const KDSoapMessage & header)219 void KDSoapClientInterface::setHeader(const QString &name, const KDSoapMessage &header)
220 {
221 d->m_persistentHeaders[name] = header;
222 d->m_persistentHeaders[name].setQualified(true);
223 }
224
ignoreSslErrors()225 void KDSoapClientInterface::ignoreSslErrors()
226 {
227 d->m_ignoreSslErrors = true;
228 }
229
230 #ifndef QT_NO_OPENSSL
ignoreSslErrors(const QList<QSslError> & errors)231 void KDSoapClientInterface::ignoreSslErrors(const QList<QSslError> &errors)
232 {
233 d->m_ignoreErrorsList = errors;
234 }
235 #endif
236
237 // Workaround for lack of connect-to-lambdas in Qt4
238 // The pure Qt5 code could read like
239 /*
240 QTimer *timeoutTimer = new QTimer(reply);
241 timeoutTimer->setSingleShot(true);
242 connect(timeoutTimer, &QTimer::timeout, reply, [reply]() { contents_of_the_slot });
243 */
244 class TimeoutHandler : public QTimer // this way a single QObject is needed
245 {
246 Q_OBJECT
247 public:
TimeoutHandler(QNetworkReply * reply)248 TimeoutHandler(QNetworkReply *reply)
249 : QTimer(reply)
250 {
251 setSingleShot(true);
252 }
253 public Q_SLOTS:
replyTimeout()254 void replyTimeout()
255 {
256 QNetworkReply *reply = qobject_cast<QNetworkReply *>(parent());
257 Q_ASSERT(reply);
258
259 // contents_of_the_slot:
260 reply->setProperty("kdsoap_reply_timed_out", true); // see KDSoapPendingCall.cpp
261 reply->abort();
262 }
263 };
264
setupReply(QNetworkReply * reply)265 void KDSoapClientInterfacePrivate::setupReply(QNetworkReply *reply)
266 {
267 if (m_ignoreSslErrors) {
268 QObject::connect(reply, SIGNAL(sslErrors(QList<QSslError>)), reply, SLOT(ignoreSslErrors()));
269 } else {
270 #ifndef QT_NO_OPENSSL
271 reply->ignoreSslErrors(m_ignoreErrorsList);
272 if (m_sslHandler) {
273 // create a child object of the reply, which will forward to m_sslHandler.
274 // this is a workaround for the lack of the reply pointer in the signal,
275 // and sender() doesn't work for sync calls (from another thread) (SOAP-79/issue29)
276 new KDSoapReplySslHandler(reply, m_sslHandler);
277 }
278 #endif
279 }
280 if (m_timeout >= 0) {
281 TimeoutHandler *timeoutHandler = new TimeoutHandler(reply);
282 connect(timeoutHandler, SIGNAL(timeout()), timeoutHandler, SLOT(replyTimeout()));
283 timeoutHandler->start(m_timeout);
284 }
285 }
286
lastResponseHeaders() const287 KDSoapHeaders KDSoapClientInterface::lastResponseHeaders() const
288 {
289 return d->m_lastResponseHeaders;
290 }
291
setStyle(KDSoapClientInterface::Style style)292 void KDSoapClientInterface::setStyle(KDSoapClientInterface::Style style)
293 {
294 d->m_style = style;
295 }
296
style() const297 KDSoapClientInterface::Style KDSoapClientInterface::style() const
298 {
299 return d->m_style;
300 }
301
cookieJar() const302 QNetworkCookieJar *KDSoapClientInterface::cookieJar() const
303 {
304 return d->accessManager()->cookieJar();
305 }
306
setCookieJar(QNetworkCookieJar * jar)307 void KDSoapClientInterface::setCookieJar(QNetworkCookieJar *jar)
308 {
309 QObject *oldParent = jar->parent();
310 d->accessManager()->setCookieJar(jar);
311 jar->setParent(oldParent); // see comment in QNAM::setCookieJar...
312 }
313
setRawHTTPHeaders(const QMap<QByteArray,QByteArray> & headers)314 void KDSoapClientInterface::setRawHTTPHeaders(const QMap<QByteArray, QByteArray> &headers)
315 {
316 d->m_httpHeaders = headers;
317 }
318
proxy() const319 QNetworkProxy KDSoapClientInterface::proxy() const
320 {
321 return d->accessManager()->proxy();
322 }
323
setProxy(const QNetworkProxy & proxy)324 void KDSoapClientInterface::setProxy(const QNetworkProxy &proxy)
325 {
326 d->accessManager()->setProxy(proxy);
327 }
328
timeout() const329 int KDSoapClientInterface::timeout() const
330 {
331 return d->m_timeout;
332 }
333
setTimeout(int msecs)334 void KDSoapClientInterface::setTimeout(int msecs)
335 {
336 d->m_timeout = msecs;
337 }
338
339 #ifndef QT_NO_OPENSSL
sslConfiguration() const340 QSslConfiguration KDSoapClientInterface::sslConfiguration() const
341 {
342 return d->m_sslConfiguration;
343 }
344
setSslConfiguration(const QSslConfiguration & config)345 void KDSoapClientInterface::setSslConfiguration(const QSslConfiguration &config)
346 {
347 d->m_sslConfiguration = config;
348 }
349
sslHandler() const350 KDSoapSslHandler *KDSoapClientInterface::sslHandler() const
351 {
352 if (!d->m_sslHandler) {
353 d->m_sslHandler = new KDSoapSslHandler;
354 }
355 return d->m_sslHandler;
356 }
357 #endif
358
359 #include "moc_KDSoapClientInterface_p.cpp"
360 #include "KDSoapClientInterface.moc"
361