NSUrlConnection backend for QNetworkAccessManager
[qt:qtbase.git] / src / network / access / qnetworkreplynsurlconnectionimpl.mm
1 /****************************************************************************
2 **
3 ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/legal
5 **
6 ** This file is part of the QtNetwork module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and Digia.  For licensing terms and
14 ** conditions see http://qt.digia.com/licensing.  For further information
15 ** use the contact form at http://qt.digia.com/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file.  Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** In addition, as a special exception, Digia gives you certain additional
26 ** rights.  These rights are described in the Digia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 **
29 ** GNU General Public License Usage
30 ** Alternatively, this file may be used under the terms of the GNU
31 ** General Public License version 3.0 as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL included in the
33 ** packaging of this file.  Please review the following information to
34 ** ensure the GNU General Public License version 3.0 requirements will be
35 ** met: http://www.gnu.org/copyleft/gpl.html.
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "qnetworkreplynsurlconnectionimpl_p.h"
43
44 #include "QtCore/qdatetime.h"
45 #include <QtCore/qcoreapplication.h>
46 #include <QtCore/qdebug.h>
47 #include <Foundation/Foundation.h>
48
49 QT_BEGIN_NAMESPACE
50
51 // Network reply implementation using NSUrlConnection.
52 //
53 // Class/object structure:
54 //
55 // QNetworkReplyNSURLConnectionImpl
56 //  |- QNetworkReplyNSURLConnectionImplPrivate
57 //      |- (bytes read)
58 //      |- (QIODevice and CFStream for async POST data transfer)
59 //      |- NSURLConnection
60 //      |- QtNSURLConnectionDelegate <NSURLConnectionDataDelegate>
61 //          |- NSURLResponse/NSHTTPURLResponse
62 //          |- (response data)
63 //
64 // The main entry point is the QNetworkReplyNSURLConnectionImpl constructor, which
65 // receives a network request from QNetworkAccessManager. The constructor
66 // creates a NSURLRequest and initiates a NSURLConnection with a QtNSURLConnectionDelegate.
67 // The delegate callbacks are then called asynchronously as the request completes.
68 //
69
70 @class QtNSURLConnectionDelegate;
71 class QNetworkReplyNSURLConnectionImplPrivate: public QNetworkReplyPrivate
72 {
73 public:
74     QNetworkReplyNSURLConnectionImplPrivate();
75     virtual ~QNetworkReplyNSURLConnectionImplPrivate();
76
77     Q_DECLARE_PUBLIC(QNetworkReplyNSURLConnectionImpl)
78     NSURLConnection * urlConnection;
79     QtNSURLConnectionDelegate *urlConnectionDelegate;
80     qint64 bytesRead;
81
82     // Sequental outgiong data streaming
83     QIODevice *outgoingData;
84     CFReadStreamRef readStream;
85     CFWriteStreamRef writeStream;
86     CFIndex transferBufferSize;
87
88     // Forwarding functions to the public class.
89     void setFinished();
90     void setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value);
91     void setRawHeader(const QByteArray &headerName, const QByteArray &value);
92     void setError(QNetworkReply::NetworkError errorCode, const QString &errorString);
93 };
94
95 @interface QtNSURLConnectionDelegate : NSObject
96 {
97     NSURLResponse *response;
98     NSMutableData *responseData;
99     QNetworkReplyNSURLConnectionImplPrivate * replyprivate;
100 }
101
102 - (id)initWithQNetworkReplyNSURLConnectionImplPrivate:(QNetworkReplyNSURLConnectionImplPrivate *)a_replyPrivate ;
103 #if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_7, __IPHONE_3_0)
104 - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
105 #endif
106 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError*)error;
107 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse*)response;
108 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData*)data;
109 - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten
110       totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite;
111 - (NSCachedURLResponse*)connection:(NSURLConnection*)connection willCacheResponse:(NSCachedURLResponse*)cachedResponse;
112 - (NSURLRequest*)connection:(NSURLConnection*)connection willSendRequest:(NSURLRequest*)request redirectResponse:(NSURLResponse*)redirectResponse;
113 - (void)connectionDidFinishLoading:(NSURLConnection*)connection;
114 - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection*)connection;
115
116 @end
117
118 QNetworkReplyNSURLConnectionImplPrivate::QNetworkReplyNSURLConnectionImplPrivate()
119     : QNetworkReplyPrivate()
120     , urlConnection(0)
121     , urlConnectionDelegate(0)
122     , bytesRead(0)
123     , readStream(0)
124     , writeStream(0)
125     , transferBufferSize(4096)
126 {
127 }
128
129 QNetworkReplyNSURLConnectionImplPrivate::~QNetworkReplyNSURLConnectionImplPrivate()
130 {
131     [urlConnection release];
132     [urlConnectionDelegate release];
133     if (readStream)
134         CFRelease(readStream);
135     if (writeStream)
136         CFRelease(writeStream);
137 }
138
139 void QNetworkReplyNSURLConnectionImplPrivate::setFinished()
140 {
141     q_func()->setFinished(true);
142 }
143
144 void QNetworkReplyNSURLConnectionImplPrivate::setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value)
145 {
146     q_func()->setHeader(header, value);
147 }
148
149 void QNetworkReplyNSURLConnectionImplPrivate::setRawHeader(const QByteArray &headerName, const QByteArray &value)
150 {
151     q_func()->setRawHeader(headerName, value);
152 }
153
154 void QNetworkReplyNSURLConnectionImplPrivate::setError(QNetworkReply::NetworkError errorCode, const QString &errorString)
155 {
156     q_func()->setError(errorCode, errorString);
157 }
158
159 void QNetworkReplyNSURLConnectionImpl::readyReadOutgoingData()
160 {
161     Q_D(QNetworkReplyNSURLConnectionImpl);
162     int bytesRead = 0;
163     do {
164         char data[d->transferBufferSize];
165         bytesRead = d->outgoingData->read(data, d->transferBufferSize);
166         if (bytesRead <= 0)
167             break;
168         CFIndex bytesWritten = CFWriteStreamWrite(d->writeStream, reinterpret_cast<unsigned char *>(data), bytesRead);
169         if (bytesWritten != bytesRead) {
170             CFErrorRef err = CFWriteStreamCopyError(d->writeStream);
171             qWarning() << "QNetworkReplyNSURLConnectionImpl: CFWriteStreamWrite error"
172                        << (err ? QString::number(CFErrorGetCode(err)) : QStringLiteral(""));
173         }
174     } while (bytesRead > 0);
175
176     if (d->outgoingData->atEnd())
177         CFWriteStreamClose(d->writeStream);
178 }
179
180 @interface QtNSURLConnectionDelegate ()
181
182 @property (nonatomic, retain) NSURLResponse* response;
183 @property (nonatomic, retain) NSMutableData* responseData;
184
185 @end
186
187 @implementation QtNSURLConnectionDelegate
188
189 @synthesize response;
190 @synthesize responseData;
191
192 - (id)initWithQNetworkReplyNSURLConnectionImplPrivate:(QNetworkReplyNSURLConnectionImplPrivate *)a_replyPrivate
193 {
194     if (self = [super init])
195         replyprivate = a_replyPrivate;
196     return self;
197 }
198
199 - (void)dealloc
200 {
201     [response release];
202     [responseData release];
203     [super dealloc];
204 }
205
206 #if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_7, __IPHONE_3_0)
207 - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
208 {
209     Q_UNUSED(connection)
210     Q_UNUSED(challenge)
211
212     if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
213         SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
214         SecTrustResultType resultType;
215         SecTrustEvaluate(serverTrust, &resultType);
216         if (resultType == kSecTrustResultUnspecified) {
217             // All good
218             [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
219         } else if (resultType == kSecTrustResultRecoverableTrustFailure) {
220             // Certificate verification error, ask user
221             // ### TODO actually ask user
222             // (test site: https://testssl-expire.disig.sk/index.en.html)
223             qWarning()  << "QNetworkReplyNSURLConnection: Certificate verification error handlig is"
224                         << "not implemented. Connection will time out.";
225         } else {
226             // other error, which the default handler will handle
227             [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
228         }
229     }
230
231     [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
232 }
233 #endif
234
235 - (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
236 {
237     Q_UNUSED(connection)
238
239     QNetworkReply::NetworkError qtError = QNetworkReply::UnknownNetworkError;
240     if ([error domain] == NSURLErrorDomain) {
241         switch ([error code]) {
242         case NSURLErrorTimedOut: qtError = QNetworkReply::TimeoutError; break;
243         case NSURLErrorUnsupportedURL: qtError = QNetworkReply::ProtocolUnknownError; break;
244         case NSURLErrorCannotFindHost: qtError = QNetworkReply::HostNotFoundError; break;
245         case NSURLErrorCannotConnectToHost: qtError = QNetworkReply::ConnectionRefusedError; break;
246         case NSURLErrorNetworkConnectionLost: qtError = QNetworkReply::NetworkSessionFailedError; break;
247         case NSURLErrorDNSLookupFailed: qtError = QNetworkReply::HostNotFoundError; break;
248         case NSURLErrorNotConnectedToInternet: qtError = QNetworkReply::NetworkSessionFailedError; break;
249         case NSURLErrorUserAuthenticationRequired: qtError = QNetworkReply::AuthenticationRequiredError; break;
250         default: break;
251         }
252     }
253
254     replyprivate->setError(qtError, QString::fromNSString([error localizedDescription]));
255 }
256
257 - (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)aResponse
258 {
259     Q_UNUSED(connection)
260         self.response = aResponse;
261     self.responseData = [NSMutableData data];
262
263     // copy headers
264     if ([aResponse isKindOfClass:[NSHTTPURLResponse class]]) {
265         NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)aResponse;
266         NSDictionary *headers = [httpResponse allHeaderFields];
267         for (NSString *key in headers) {
268             NSString *value = [headers objectForKey:key];
269             replyprivate->setRawHeader(QString::fromNSString(key).toUtf8(), QString::fromNSString(value).toUtf8());
270         }
271     } else {
272         if ([aResponse expectedContentLength] != NSURLResponseUnknownLength)
273             replyprivate->setHeader(QNetworkRequest::ContentLengthHeader, [aResponse expectedContentLength]);
274     }
275
276     QMetaObject::invokeMethod(replyprivate->q_func(), "metaDataChanged", Qt::QueuedConnection);
277 }
278
279 - (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
280 {
281      Q_UNUSED(connection)
282          [responseData appendData:data];
283
284     if ([response expectedContentLength] != NSURLResponseUnknownLength) {
285         QMetaObject::invokeMethod(replyprivate->q_func(), "downloadProgress", Qt::QueuedConnection,
286             Q_ARG(qint64, qint64([responseData length])),
287             Q_ARG(qint64, qint64([response expectedContentLength])));
288     }
289
290     QMetaObject::invokeMethod(replyprivate->q_func(), "readyRead", Qt::QueuedConnection);
291 }
292
293 - (void)connection:(NSURLConnection*)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten
294   totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
295 {
296     Q_UNUSED(connection)
297     Q_UNUSED(bytesWritten)
298     QMetaObject::invokeMethod(replyprivate->q_func(), "uploadProgress", Qt::QueuedConnection,
299         Q_ARG(qint64, qint64(totalBytesWritten)),
300         Q_ARG(qint64, qint64(totalBytesExpectedToWrite)));
301 }
302
303 - (NSCachedURLResponse*)connection:(NSURLConnection*)connection willCacheResponse:(NSCachedURLResponse*)cachedResponse
304 {
305     Q_UNUSED(connection)
306     return cachedResponse;
307 }
308
309 - (NSURLRequest*)connection:(NSURLConnection*)connection willSendRequest:(NSURLRequest*)request redirectResponse:(NSURLResponse*)redirectResponse
310 {
311     Q_UNUSED(connection)
312     Q_UNUSED(redirectResponse)
313         return request;
314 }
315
316 - (void)connectionDidFinishLoading:(NSURLConnection*)connection
317 {
318     Q_UNUSED(connection)
319         replyprivate->setFinished();
320     QMetaObject::invokeMethod(replyprivate->q_func(), "finished", Qt::QueuedConnection);
321 }
322
323 - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection*)connection
324 {
325     Q_UNUSED(connection)
326     return YES;
327 }
328
329 @end
330
331 QNetworkReplyNSURLConnectionImpl::~QNetworkReplyNSURLConnectionImpl()
332 {
333 }
334
335 QNetworkReplyNSURLConnectionImpl::QNetworkReplyNSURLConnectionImpl(QObject *parent,
336     const QNetworkRequest &request, const QNetworkAccessManager::Operation operation, QIODevice* outgoingData)
337     : QNetworkReply(*new QNetworkReplyNSURLConnectionImplPrivate(), parent)
338 {
339     setRequest(request);
340     setUrl(request.url());
341     setOperation(operation);
342     QNetworkReply::open(QIODevice::ReadOnly);
343
344     QNetworkReplyNSURLConnectionImplPrivate *d = (QNetworkReplyNSURLConnectionImplPrivate*) d_func();
345
346     QUrl url = request.url();
347     if (url.host() == QLatin1String("localhost"))
348         url.setHost(QString());
349
350     if (url.path().isEmpty())
351         url.setPath(QLatin1String("/"));
352     setUrl(url);
353
354     // Create a NSMutableURLRequest from QNetworkRequest
355     NSMutableURLRequest *nsRequest = [NSMutableURLRequest requestWithURL:request.url().toNSURL()
356                                              cachePolicy:NSURLRequestUseProtocolCachePolicy
357                                              timeoutInterval:60.0];
358     // copy headers
359     foreach (const QByteArray &header, request.rawHeaderList()) {
360         QByteArray headerValue = request.rawHeader(header);
361         [nsRequest addValue:QString::fromUtf8(headerValue).toNSString()
362                  forHTTPHeaderField:QString::fromUtf8(header).toNSString()];
363     }
364
365     if (operation == QNetworkAccessManager::GetOperation)
366         [nsRequest setHTTPMethod:@"GET"];
367     else if (operation == QNetworkAccessManager::PostOperation)
368         [nsRequest setHTTPMethod:@"POST"];
369     else if (operation == QNetworkAccessManager::PutOperation)
370         [nsRequest setHTTPMethod:@"PUT"];
371     else if (operation == QNetworkAccessManager::DeleteOperation)
372         [nsRequest setHTTPMethod:@"DELETE"];
373     else
374         qWarning() << "QNetworkReplyNSURLConnection: Unsupported netork operation" << operation;
375
376     if (outgoingData) {
377         d->outgoingData = outgoingData;
378         if (outgoingData->isSequential()) {
379             // set up streaming from outgoingData iodevice to request
380             CFStreamCreateBoundPair(kCFAllocatorDefault, &d->readStream, &d->writeStream, d->transferBufferSize);
381             CFWriteStreamOpen(d->writeStream);
382             [nsRequest setHTTPBodyStream:reinterpret_cast<NSInputStream *>(d->readStream)];
383             connect(outgoingData, SIGNAL(readyRead()), this, SLOT(readyReadOutgoingData()));
384             readyReadOutgoingData();
385         } else {
386             // move all data at once
387             QByteArray data = outgoingData->readAll();
388             [nsRequest setHTTPBody:[NSData dataWithBytes:data.constData() length:data.length()]];
389         }
390     }
391
392     // Create connection
393     d->urlConnectionDelegate = [[QtNSURLConnectionDelegate alloc] initWithQNetworkReplyNSURLConnectionImplPrivate:d];
394     d->urlConnection = [[NSURLConnection alloc] initWithRequest:nsRequest delegate:d->urlConnectionDelegate];
395     if (!d->urlConnection) {
396         // ### what type of error is an initWithRequest fail?
397         setError(QNetworkReply::ProtocolUnknownError, QStringLiteral("QNetworkReplyNSURLConnection internal error"));
398     }
399 }
400
401 void QNetworkReplyNSURLConnectionImpl::close()
402 {
403     // No-op? Network ops should continue (especially POSTs)
404     QNetworkReply::close();
405 }
406
407 void QNetworkReplyNSURLConnectionImpl::abort()
408 {
409     Q_D(QNetworkReplyNSURLConnectionImpl);
410     [d->urlConnection cancel];
411     QNetworkReply::close();
412 }
413
414 qint64 QNetworkReplyNSURLConnectionImpl::bytesAvailable() const
415 {
416     Q_D(const QNetworkReplyNSURLConnectionImpl);
417     qint64 available = QNetworkReply::bytesAvailable() +
418             [[d->urlConnectionDelegate responseData] length] -
419             d->bytesRead;
420
421     return available;
422 }
423
424 bool QNetworkReplyNSURLConnectionImpl::isSequential() const
425 {
426     return true;
427 }
428
429 qint64 QNetworkReplyNSURLConnectionImpl::size() const
430 {
431     Q_D(const QNetworkReplyNSURLConnectionImpl);
432     return [[d->urlConnectionDelegate responseData] length];
433 }
434
435 /*!
436     \internal
437 */
438 qint64 QNetworkReplyNSURLConnectionImpl::readData(char *data, qint64 maxlen)
439 {
440     Q_D(QNetworkReplyNSURLConnectionImpl);
441     qint64 dataSize = [[d->urlConnectionDelegate responseData] length];
442     qint64 canRead = qMin(maxlen, dataSize - d->bytesRead);
443     const char *sourceBase = static_cast<const char *>([[d->urlConnectionDelegate responseData] bytes]);
444     memcpy(data,  sourceBase + d->bytesRead, canRead);
445     d->bytesRead += canRead;
446     return canRead;
447 }
448
449 QT_END_NAMESPACE