1///////////////////////////////////////////////////////////////////////////////
2// Name:        src/osx/webrequest_urlsession.mm
3// Purpose:     wxWebRequest implementation using URLSession
4// Author:      Tobias Taschner
5// Created:     2018-10-25
6// Copyright:   (c) 2018 wxWidgets development team
7// Licence:     wxWindows licence
8///////////////////////////////////////////////////////////////////////////////
9
10// For compilers that support precompilation, includes "wx.h".
11#include "wx/wxprec.h"
12
13#include "wx/webrequest.h"
14
15#if wxUSE_WEBREQUEST && wxUSE_WEBREQUEST_URLSESSION
16
17#import <Foundation/Foundation.h>
18
19#include "wx/osx/private/webrequest_urlsession.h"
20#include "wx/osx/private.h"
21
22#ifndef WX_PRECOMP
23    #include "wx/log.h"
24    #include "wx/utils.h"
25#endif
26
27@interface wxWebSessionDelegate : NSObject <NSURLSessionDataDelegate>
28{
29    wxWebSessionURLSession* m_session;
30    NSMapTable* m_requests;
31}
32
33@end
34
35@implementation wxWebSessionDelegate
36
37- (id)initWithSession:(wxWebSessionURLSession*)session
38{
39    m_session = session;
40    m_requests = [[NSMapTable weakToStrongObjectsMapTable] retain];
41    return self;
42}
43
44- (void)dealloc
45{
46    [m_requests release];
47    [super dealloc];
48}
49
50- (void)registerRequest:(wxWebRequestURLSession*)request task:(NSURLSessionTask*)task
51{
52    [m_requests setObject:[NSValue valueWithPointer:request] forKey:task];
53}
54
55- (wxWebRequestURLSession*)requestForTask:(NSURLSessionTask*)task
56{
57    wxWebRequestURLSession* request = NULL;
58    NSValue* val = [m_requests objectForKey:task];
59    if (val)
60        request = static_cast<wxWebRequestURLSession*>(val.pointerValue);
61
62    return request;
63}
64
65- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
66{
67    wxUnusedVar(session);
68
69    wxWebRequestURLSession* request = [self requestForTask:dataTask];
70
71    wxLogTrace(wxTRACE_WEBREQUEST, "Request %p: didReceiveData", request);
72
73    if (request)
74        request->GetResponseImplPtr()->HandleData(data);
75}
76
77- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
78{
79    wxUnusedVar(session);
80
81    wxWebRequestURLSession* request = [self requestForTask:task];
82    if (error)
83    {
84        wxLogTrace(wxTRACE_WEBREQUEST, "Request %p: didCompleteWithError, error=%s",
85                   request, wxCFStringRefFromGet([error description]).AsString());
86
87        if ( error.code == NSURLErrorCancelled )
88            request->SetState(wxWebRequest::State_Cancelled);
89        else
90            request->SetState(wxWebRequest::State_Failed, wxCFStringRefFromGet(error.localizedDescription).AsString());
91    }
92    else
93    {
94        wxLogTrace(wxTRACE_WEBREQUEST, "Request %p: completed successfully", request);
95
96        request->HandleCompletion();
97    }
98
99    // After the task is completed it no longer needs to be mapped
100    [m_requests removeObjectForKey:task];
101}
102
103- (void)URLSession:(NSURLSession *)session
104        task:(NSURLSessionTask *)task
105        didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
106        completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
107{
108    wxUnusedVar(session);
109
110    wxWebRequestURLSession* request = [self requestForTask:task];
111    wxCHECK_RET( request, "received authentication challenge for an unknown task" );
112
113    NSURLProtectionSpace* const space = [challenge protectionSpace];
114
115    wxLogTrace(wxTRACE_WEBREQUEST, "Request %p: didReceiveChallenge for %s",
116               request,
117               wxCFStringRefFromGet([space description]).AsString());
118
119    // We need to distinguish between session-wide and task-specific
120    // authentication challenges, we're only really interested in the latter
121    // ones here (but apparently there is no way to get just them, even though
122    // the documentation seems to imply that session-wide challenges shouldn't
123    // be sent to this task-specific delegate -- but they're, at least under
124    // 10.14).
125    const auto authMethod = space.authenticationMethod;
126    if ( authMethod == NSURLAuthenticationMethodHTTPBasic ||
127            authMethod == NSURLAuthenticationMethodHTTPDigest )
128    {
129        if ( auto* const authChallenge = request->GetAuthChallengeImplPtr() )
130        {
131            // We're going to get called until we don't provide the correct
132            // credentials, so don't use them again (and again, and again...)
133            // if we had already used them unsuccessfully.
134            if ( !challenge.previousFailureCount )
135            {
136                wxLogTrace(wxTRACE_WEBREQUEST, "Request %p: using credentials", request);
137
138                completionHandler(NSURLSessionAuthChallengeUseCredential,
139                                  authChallenge->GetURLCredential());
140
141                return;
142            }
143
144            wxLogTrace(wxTRACE_WEBREQUEST, "Request %p: not using failing credentials again", request);
145        }
146
147        request->HandleChallenge(new wxWebAuthChallengeURLSession(
148            [space isProxy] ? wxWebAuthChallenge::Source_Proxy
149                            : wxWebAuthChallenge::Source_Server,
150            *request
151        ));
152    }
153    else if ( authMethod == NSURLAuthenticationMethodServerTrust )
154    {
155        if (request->IsPeerVerifyDisabled())
156            completionHandler(NSURLSessionAuthChallengeUseCredential,
157                              [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
158    }
159
160    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
161}
162
163@end
164
165//
166// wxWebRequestURLSession
167//
168wxWebRequestURLSession::wxWebRequestURLSession(wxWebSession& session,
169                                               wxWebSessionURLSession& sessionImpl,
170                                               wxEvtHandler* handler,
171                                               const wxString& url,
172                                               int winid):
173    wxWebRequestImpl(session, sessionImpl, handler, winid),
174    m_sessionImpl(sessionImpl),
175    m_url(url)
176{
177}
178
179wxWebRequestURLSession::~wxWebRequestURLSession()
180{
181    [m_task release];
182}
183
184void wxWebRequestURLSession::Start()
185{
186    wxString method = m_method;
187    if ( method.empty() )
188        method = m_dataSize ? wxASCII_STR("POST") : wxASCII_STR("GET");
189
190    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:
191                                [NSURL URLWithString:wxCFStringRef(m_url).AsNSString()]];
192    req.HTTPMethod = wxCFStringRef(method).AsNSString();
193
194    // Set request headers
195    for (wxWebRequestHeaderMap::const_iterator it = m_headers.begin(); it != m_headers.end(); ++it)
196    {
197        [req setValue:wxCFStringRef(it->second).AsNSString() forHTTPHeaderField:
198         wxCFStringRef(it->first).AsNSString()];
199    }
200
201    if (m_dataSize)
202    {
203        // Read all upload data to memory buffer
204        void* const buf = malloc(m_dataSize);
205        m_dataStream->Read(buf, m_dataSize);
206
207        // Create NSData from memory buffer, passing it ownership of the data.
208        NSData* data = [NSData dataWithBytesNoCopy:buf length:m_dataSize];
209        m_task = [[m_sessionImpl.GetSession() uploadTaskWithRequest:req fromData:data] retain];
210    }
211    else
212    {
213        // Create data task
214        m_task = [[m_sessionImpl.GetSession() dataTaskWithRequest:req] retain];
215    }
216
217    wxLogTrace(wxTRACE_WEBREQUEST, "Request %p: start \"%s %s\"",
218               this, method, m_url);
219
220    // The session delegate needs to know which task is wrapped in which request
221    [m_sessionImpl.GetDelegate() registerRequest:this task:m_task];
222
223    m_response.reset(new wxWebResponseURLSession(*this, m_task));
224
225    SetState(wxWebRequest::State_Active);
226    [m_task resume];
227}
228
229void wxWebRequestURLSession::DoCancel()
230{
231    [m_task cancel];
232}
233
234void wxWebRequestURLSession::HandleCompletion()
235{
236    switch ( m_response->GetStatus() )
237    {
238        case 401:
239        case 407:
240            SetState(wxWebRequest::State_Unauthorized, m_response->GetStatusText());
241            break;
242
243        default:
244            SetFinalStateFromStatus();
245    }
246}
247
248void wxWebRequestURLSession::HandleChallenge(wxWebAuthChallengeURLSession* challenge)
249{
250    m_authChallenge.reset(challenge);
251}
252
253wxFileOffset wxWebRequestURLSession::GetBytesSent() const
254{
255    return m_task.countOfBytesSent;
256}
257
258wxFileOffset wxWebRequestURLSession::GetBytesExpectedToSend() const
259{
260    return m_task.countOfBytesExpectedToSend;
261}
262
263wxFileOffset wxWebRequestURLSession::GetBytesReceived() const
264{
265    return m_task.countOfBytesReceived;
266}
267
268wxFileOffset wxWebRequestURLSession::GetBytesExpectedToReceive() const
269{
270    return m_task.countOfBytesExpectedToReceive;
271}
272
273//
274// wxWebAuthChallengeURLSession
275//
276
277wxWebAuthChallengeURLSession::~wxWebAuthChallengeURLSession()
278{
279    [m_cred release];
280}
281
282void wxWebAuthChallengeURLSession::SetCredentials(const wxWebCredentials& cred)
283{
284    wxLogTrace(wxTRACE_WEBREQUEST, "Request %p: setting credentials", &m_request);
285
286    [m_cred release];
287
288    m_cred = [NSURLCredential
289        credentialWithUser:wxCFStringRef(cred.GetUser()).AsNSString()
290        password:wxCFStringRef(wxSecretString(cred.GetPassword())).AsNSString()
291        persistence:NSURLCredentialPersistenceNone
292    ];
293
294    [m_cred retain];
295
296    m_request.Start();
297}
298
299
300//
301// wxWebResponseURLSession
302//
303
304wxWebResponseURLSession::wxWebResponseURLSession(wxWebRequestURLSession& request,
305                                                 WX_NSURLSessionTask task):
306    wxWebResponseImpl(request)
307{
308    m_task = [task retain];
309
310    Init();
311}
312
313wxWebResponseURLSession::~wxWebResponseURLSession()
314{
315    [m_task release];
316}
317
318void wxWebResponseURLSession::HandleData(WX_NSData data)
319{
320    void* buf = GetDataBuffer(data.length);
321    [data getBytes:buf length:data.length];
322    ReportDataReceived(data.length);
323}
324
325wxFileOffset wxWebResponseURLSession::GetContentLength() const
326{
327    return m_task.response.expectedContentLength;
328}
329
330wxString wxWebResponseURLSession::GetURL() const
331{
332    return wxCFStringRefFromGet(m_task.response.URL.absoluteString).AsString();
333}
334
335wxString wxWebResponseURLSession::GetHeader(const wxString& name) const
336{
337    NSHTTPURLResponse* httpResp = (NSHTTPURLResponse*) m_task.response;
338    NSString* value = [httpResp.allHeaderFields objectForKey:wxCFStringRef(name).AsNSString()];
339    if (value)
340        return wxCFStringRefFromGet(value).AsString();
341    else
342        return wxString();
343}
344
345int wxWebResponseURLSession::GetStatus() const
346{
347    NSHTTPURLResponse* httpResp = (NSHTTPURLResponse*) m_task.response;
348    return httpResp.statusCode;
349}
350
351wxString wxWebResponseURLSession::GetStatusText() const
352{
353    return wxCFStringRefFromGet([NSHTTPURLResponse localizedStringForStatusCode:GetStatus()]).AsString();
354}
355
356wxString wxWebResponseURLSession::GetSuggestedFileName() const
357{
358    return wxCFStringRefFromGet(m_task.response.suggestedFilename).AsString();
359}
360
361//
362// wxWebSessionURLSession
363//
364
365wxWebSessionURLSession::wxWebSessionURLSession()
366{
367    m_delegate = [[wxWebSessionDelegate alloc] initWithSession:this];
368
369    m_session = [[NSURLSession sessionWithConfiguration:
370                  [NSURLSessionConfiguration defaultSessionConfiguration]
371                                               delegate:m_delegate delegateQueue:nil] retain];
372}
373
374wxWebSessionURLSession::~wxWebSessionURLSession()
375{
376    [m_session release];
377    [m_delegate release];
378}
379
380wxWebRequestImplPtr
381wxWebSessionURLSession::CreateRequest(wxWebSession& session,
382                                      wxEvtHandler* handler,
383                                      const wxString& url,
384                                      int winid)
385{
386    return wxWebRequestImplPtr(new wxWebRequestURLSession(session, *this, handler, url, winid));
387}
388
389wxVersionInfo wxWebSessionURLSession::GetLibraryVersionInfo()
390{
391    int verMaj, verMin, verMicro;
392    wxGetOsVersion(&verMaj, &verMin, &verMicro);
393    return wxVersionInfo("URLSession", verMaj, verMin, verMicro);
394}
395
396#endif // wxUSE_WEBREQUEST_URLSESSION
397