source: SMSSender/common/network/snetworkhelper.cpp @ 378:7553d82ad234

separation-frontend-backend
Last change on this file since 378:7553d82ad234 was 378:7553d82ad234, checked in by Sämy Zehnder <saemy.zehnder@…>, 7 years ago
  • Reverts the removal of the CA certificates to avoid the disavailability of a service due to the system CA store not containing it's CA.
File size: 15.5 KB
Line 
1/*
2 smssender - A frontend for fast and easy SMS sending over different gateways.
3 Copyright (C) 2007-2014, gorrión. See http://smssender.gorrion.ch
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18#include "snetworkhelper.h"
19
20#include <QCoreApplication>
21#include <QEventLoop>
22#include <QFile>
23#include <QNetworkCookieJar>
24
25#include "common/domain/sversion.h"
26#include "common/exceptions/enetworkexception.h"
27#include "common/network/snetworkreply.h"
28
29const uint SNetworkHelper::MAX_REDIRECT_COUNT = 10;
30
31SNetworkHelper::SNetworkHelper(QObject *parent)
32    : QObject(parent)
33    , MULTIPART_BOUNDARY("---------------------------11677076429873974811095575830")
34    , sharedCookieJar_(new QNetworkCookieJar(this))
35{
36    qRegisterMetaType<QList<QSslError> >("QList<QSslError>");
37
38    initDefaultSSLConfiguration();
39}
40
41SNetworkHelper::~SNetworkHelper() {
42    foreach (QThread* thread, networkAccessManager_.keys()) {
43        networkAccessManager_.take(thread)->deleteLater();
44    }
45}
46
47/**
48 * QNetworkAccessManager can not be accessed from different threads simultaneous. Therefore we create an own instance
49 * for each thread and share the cookie jar.
50 */
51QNetworkAccessManager &SNetworkHelper::networkAccessManager() {
52    if (!networkAccessManager_.contains(QThread::currentThread())) {
53        QNetworkAccessManager* nam = new QNetworkAccessManager;
54        nam->setCookieJar(sharedCookieJar_);
55        sharedCookieJar_->setParent(this); // setCookieJar takes ownership
56
57        connect(QThread::currentThread(), SIGNAL(terminated()), this, SLOT(onThreadTerminated()));
58        networkAccessManager_.insert(QThread::currentThread(), nam);
59
60        connect(nam, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError>&)),
61                this, SLOT(onSslErrors(QNetworkReply*, const QList<QSslError>&)),
62                Qt::DirectConnection);
63    }
64    return *networkAccessManager_.value(QThread::currentThread());
65}
66void SNetworkHelper::onThreadTerminated() {
67    networkAccessManager_.take(QThread::currentThread())->deleteLater();
68}
69
70
71void SNetworkHelper::initDefaultSSLConfiguration() {
72    /* We always trust the gorrión. signing CA */
73    addTrustedCA(":/certs/StartCom_root_CA.crt");
74}
75
76void SNetworkHelper::addTrustedCA(const QString& certFilename, QSsl::EncodingFormat format) {
77    QFile certFile(certFilename);
78    bool canOpen = certFile.open(QIODevice::ReadOnly);
79    Q_ASSERT(canOpen);
80
81    QSslCertificate cert(&certFile, format);
82    Q_ASSERT(cert.isValid() && !cert.isNull());
83
84    QList<QSslCertificate> caCertificates = defaultSSLConfiguration().caCertificates();
85    caCertificates.append(cert);
86    defaultSSLConfiguration().setCaCertificates(caCertificates);
87}
88
89/**
90 * Does a http GET on the given desitnation url and waits for the result. The headers of the request are automatically filled.
91 * If desired, redirects are followed.
92 *
93 * @param destination The destination url
94 * @param redirectHandling If redirects should be handled
95 * @return The network reply
96 */
97SNetworkReply SNetworkHelper::syncGet(const QUrl& destination, SNetworkHelper::RedirectHandling redirectHandling) {
98    QNetworkRequest request = createDefaultRequest(destination);
99    return syncGet(request, redirectHandling);
100}
101/**
102 * Does a http GET for the given request and waits for the result. If desired, redirects are followed.
103 *
104 * @param request The request
105 * @param redirectHandling If redirects should be handled
106 * @return The network reply
107 */
108SNetworkReply SNetworkHelper::syncGet(QNetworkRequest& request, SNetworkHelper::RedirectHandling redirectHandling) {
109    SNetworkReply reply = asyncGet(request);
110    waitFor(reply);
111
112    reply = handleRedirect(reply, redirectHandling);
113
114    return reply;
115}
116
117/**
118 * Does a http GET on the given desitnation url and returns immediately. The headers of the request are automatically filled.
119 *
120 * @param destination The destination url
121 * @return The network reply
122 */
123SNetworkReply SNetworkHelper::asyncGet(const QUrl& destination) {
124    QNetworkRequest request = createDefaultRequest(destination);
125    return asyncGet(request);
126}
127/**
128 * Does a http GET for the given request and returns immediately.
129 *
130 * @param request The request
131 * @return The network reply
132 */
133SNetworkReply SNetworkHelper::asyncGet(QNetworkRequest& request) {
134    lastUrl_.insert(&networkAccessManager(), request.url());
135
136    SNetworkReply reply(networkAccessManager().get(request));
137    return reply;
138}
139
140
141/**
142 * Does a http POST on the given desitnation url and waits for the result. The headers of the request are automatically filled.
143 * If desired, redirects are followed.
144 *
145 * @param destination The destination url
146 * @param posts The post params which should be sent. Ownership is taken.
147 * @param postType If we should send the params urlencoded or with multipart.
148 * @param redirectHandling If redirects should be handled
149 * @return The network reply
150 */
151SNetworkReply SNetworkHelper::syncPost(const QUrl& destination, QList<IParam*>& posts, SNetworkHelper::PostType postType, SNetworkHelper::RedirectHandling redirectHandling) {
152    QNetworkRequest request = createDefaultRequest(destination);
153    return syncPost(request, posts, postType, redirectHandling);
154}
155/**
156 * Does a http POST for the given request and waits for the result. If desired, redirects are followed.
157 *
158 * @param request The request
159 * @param posts The post params which should be sent. Ownership is taken.
160 * @param postType If we should send the params urlencoded or with multipart.
161 * @param redirectHandling If redirects should be handled
162 * @return The network reply
163 */
164SNetworkReply SNetworkHelper::syncPost(QNetworkRequest& request, QList<IParam*>& posts, SNetworkHelper::PostType postType, SNetworkHelper::RedirectHandling redirectHandling) {
165    SNetworkReply reply = asyncPost(request, posts, postType);
166    waitFor(reply);
167
168    reply = handleRedirect(reply, redirectHandling);
169
170    return reply;
171}
172
173/**
174 * Does a http POST on the given desitnation url and returns immediately. The headers of the request are automatically filled.
175 *
176 * @param destination The destination url
177 * @param posts The post params which should be sent. Ownership is taken.
178 * @param postType If we should send the params urlencoded or with multipart.
179 * @return The network reply
180 */
181SNetworkReply SNetworkHelper::asyncPost(const QUrl& destination, QList<IParam*>& posts, SNetworkHelper::PostType postType) {
182    QNetworkRequest request  = createDefaultRequest(destination);
183    return asyncPost(request, posts, postType);
184}
185/**
186 * Does a http POST for the given request and returns immediately.
187 *
188 * @param request The request
189 * @param posts The post params which should be sent. Ownership is taken.
190 * @param postType If we should send the params urlencoded or with multipart.
191 * @return The network reply
192 */
193SNetworkReply SNetworkHelper::asyncPost(QNetworkRequest& request, QList<IParam*>& posts, SNetworkHelper::PostType postType) {
194    lastUrl_.insert(&networkAccessManager(), request.url());
195
196    // Set the content type header
197    if (!request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
198        switch (postType) {
199            case SNetworkHelper::ptUrlEncoded: {
200                request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
201                /* Perhaps you want: application/x-www-form-urlencoded; charset=utf-8
202                 * But do not set this as default because some http-servers are crashing because of that!*/
203                break;
204            }
205
206            case SNetworkHelper::ptMultipart: {
207                request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=" + MULTIPART_BOUNDARY);
208                break;
209            }
210        }
211    }
212
213    // Generate the posts body
214    QByteArray postData = paramsToEncoded(posts, postType);
215    request.setHeader(QNetworkRequest::ContentLengthHeader, postData.length());
216
217    // Cleanup the params
218    foreach (IParam* param, posts) {
219        delete param;
220    }
221    posts.clear();
222
223    // Post the request
224    SNetworkReply reply(networkAccessManager().post(request, postData));
225    return reply;
226}
227
228QUrl SNetworkHelper::getRedirectUrl(const SNetworkReply& reply) {
229    return reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
230}
231
232void SNetworkHelper::waitFor(const SNetworkReply& networkReply) {
233    QEventLoop eventLoop;
234    connect(networkReply, SIGNAL(finished()), &eventLoop, SLOT(quit()));
235    eventLoop.exec(QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents);
236    Q_ASSERT(networkReply->isFinished());
237
238    if (networkReply->error() != QNetworkReply::NoError) {
239        ENetworkException(networkReply->error(), networkReply->errorString())
240                .addDebugInfo("url", networkReply->request().url().toString())
241                .raise();
242    }
243}
244
245SNetworkReply SNetworkHelper::handleRedirect(SNetworkReply reply, SNetworkHelper::RedirectHandling redirectHandling) {
246    switch (redirectHandling) {
247        case SNetworkHelper::rhHandleRedirects: {
248            QUrl redirectUrl = getRedirectUrl(reply);
249            if (!redirectUrl.isEmpty()) {
250                QNetworkRequest request = createDefaultRequest(redirectUrl);
251
252                // Set referer header
253                QByteArray refererUrl = reply->request().url().toEncoded();
254                if (reply->request().hasRawHeader("Referer")) {
255                    refererUrl = reply->request().rawHeader("Referer");
256                }
257                request.setRawHeader("Referer", refererUrl);
258
259                reply = syncGet(request, redirectHandling);
260            }
261
262            break;
263        }
264
265        case SNetworkHelper::rhNone:
266            break;
267    }
268
269    return reply;
270}
271
272void SNetworkHelper::adjustUrl(QUrl& url) {
273    QUrl lstUrl = lastUrl();
274    if (lstUrl.isEmpty()) {
275        return;
276    }
277
278    if (url.scheme().isEmpty()) {
279        url.setScheme(lstUrl.scheme());
280    }
281    if (url.host().isEmpty()) {
282        url.setHost(lstUrl.host());
283    }
284    if (url.port() == -1) {
285        url.setPort(lstUrl.port());
286    }
287}
288QUrl SNetworkHelper::lastUrl() {
289    return lastUrl_.value(&networkAccessManager());
290}
291
292
293QByteArray SNetworkHelper::paramsToEncoded(const QList<IParam*>& posts, SNetworkHelper::PostType postType) {
294    QByteArray delim;
295    QByteArray startMarker;
296    QByteArray endMarker;
297    switch (postType) {
298        case SNetworkHelper::ptUrlEncoded: {
299            delim       = "&";
300            startMarker = "";
301            endMarker   = "";
302
303            break;
304        }
305
306        case SNetworkHelper::ptMultipart: {
307            delim       = QString("\r\n--" + MULTIPART_BOUNDARY + "\r\n").toUtf8();
308            startMarker = QString("--" + MULTIPART_BOUNDARY + "\r\n").toUtf8();
309            endMarker   = QString("\r\n--" + MULTIPART_BOUNDARY + "--\r\n").toUtf8();
310
311            break;
312        }
313    }
314
315    QByteArray result = startMarker;
316    foreach (IParam* param, posts) {
317        result += param->encode(postType) + delim;
318    }
319    result.remove(result.length() - delim.length(), delim.length()); // Remove last delim
320    result += endMarker;
321
322    return result;
323}
324
325
326QNetworkRequest SNetworkHelper::createDefaultRequest(const QUrl& url) {
327    QUrl dest(url);
328    adjustUrl(dest);
329
330    if (dest.host().isEmpty()) {
331        dest.setHost(lastUrl().host());
332        dest.setPort(lastUrl().port());
333        dest.setScheme(lastUrl().scheme());
334    }
335    QNetworkRequest request(dest);
336    attachDefaultHeaders(request);
337    attachDefaultSSLConfiguration(request);
338
339    return request;
340}
341
342void SNetworkHelper::attachDefaultHeaders(QNetworkRequest& request) {
343    foreach (const QByteArray header, defaultHeaders().keys()) {
344        request.setRawHeader(header, defaultHeaders().value(header));
345    }
346
347    if (!request.hasRawHeader("User-Agent")) {
348        if (request.url().host().toLower().endsWith("gorrion.ch")) {
349            SVersion appVersion(qApp->applicationVersion());
350            request.setRawHeader("User-Agent", QString("Mozilla/4.0 (compatible; SMSSender %1; http://www.gorrion.ch/p/smssender)").arg(appVersion.toString(true)).toUtf8());
351        } else {
352            request.setRawHeader("User-Agent", "Mozilla/4.0 (compatible; SMSSender; http://www.gorrion.ch/p/smssender)");
353        }
354    }
355
356    if (!request.hasRawHeader("Host") && !request.url().host().isEmpty()) {
357        request.setRawHeader("Host", request.url().host().toUtf8());
358    }
359
360    if (!request.hasRawHeader("Connection")) {
361        request.setRawHeader("Connection", "Keep-Alive");
362    }
363}
364
365void SNetworkHelper::clearCookies() {
366    networkAccessManager().setCookieJar(new QNetworkCookieJar);
367}
368
369void SNetworkHelper::attachDefaultSSLConfiguration(QNetworkRequest& request) {
370    request.setSslConfiguration(defaultSSLConfiguration());
371}
372
373
374QString certToString(const QSslCertificate &cert, const QString &delim) {
375    QString subjectOrganization = cert.subjectInfo(QSslCertificate::Organization);
376    QString subjectCommonName = cert.subjectInfo(QSslCertificate::CommonName);
377    QString subjectLocalityName = cert.subjectInfo(QSslCertificate::LocalityName);
378    QString subjectOrganizationalUnitName = cert.subjectInfo(QSslCertificate::OrganizationalUnitName);
379
380    QString issuerOrganization = cert.issuerInfo(QSslCertificate::Organization);
381    QString issuerCommonName = cert.issuerInfo(QSslCertificate::CommonName);
382    QString issuerLocalityName = cert.issuerInfo(QSslCertificate::LocalityName);
383    QString issuerOrganizationalUnitName = cert.issuerInfo(QSslCertificate::OrganizationalUnitName);
384
385    return QString("s:[%1]%2i:[%3]")
386            .arg(QString("O=%1,CN=%2,L=%3,OU=%4,S=%5")
387                 .arg(subjectOrganization)
388                 .arg(subjectCommonName)
389                 .arg(subjectLocalityName)
390                 .arg(subjectOrganizationalUnitName)
391                 .arg(QString(cert.serialNumber())))
392            .arg(delim)
393            .arg(QString("O=%1,CN=%2,L=%3,OU=%4")
394                 .arg(issuerOrganization)
395                 .arg(issuerCommonName)
396                 .arg(issuerLocalityName)
397                 .arg(issuerOrganizationalUnitName));
398}
399
400void SNetworkHelper::onSslErrors(QNetworkReply* reply, const QList<QSslError>& errors) {
401    qDebug() << QString("SSL errors occured while connecting to %1:")
402                .arg(reply->request().url().toString());
403
404    foreach (QSslError error, errors) {
405        qDebug() << QString("%1\n  %2")
406                    .arg(error.errorString())
407                    .arg(certToString(error.certificate(), "\n  "));
408    }
409
410    qDebug() << "Installed CAs:";
411    unsigned i = 0;
412    foreach (const QSslCertificate &cert, reply->request().sslConfiguration().caCertificates()) {
413        qDebug() << QString("%1: %2")
414                    .arg(i)
415                    .arg(certToString(cert, "\n   "));
416        ++i;
417    }
418}
419
420HeadersMap& SNetworkHelper::defaultHeaders() {
421    return defaultHeaders_;
422}
423void SNetworkHelper::setDefaultHeaders(const HeadersMap& headers) {
424    defaultHeaders_ = headers;
425}
426
427QSslConfiguration& SNetworkHelper::defaultSSLConfiguration() {
428    return defaultSSLConfiguration_;
429}
430void SNetworkHelper::setDefaultSSLConfiguration(const QSslConfiguration& sslConfiguration) {
431    defaultSSLConfiguration_ = sslConfiguration;
432}
Note: See TracBrowser for help on using the repository browser.