I want to integrate offline HLS in iOS through AVFoundation.
I have an encrypted HLS with simple AES-128 and it doesn't want to play in offline mode, I was trying to integrate AVAssetResourceLoaderDelegate, but don't know how to integrate applicationCertificate and contentKeyFromKeyServerModuleWithSPCData that are in https://developer.apple.com/streaming/fps/ examples. I have a feeling that I am doing something wrong. It is a sample AES-128 encryption, not even DRM.
Without the internet, AVPlayer is still trying to get encryption key through GET request.
It would be great if someone succeeded to save the encrypted key locally and somehow gave it to AVPlayer together with AVURLAsset.
Did someone manage to integrate this?
I have written to apple support and their responses weren't new for me. Information that they provided to me I got from wwdc videos and documentation before I started a conversation with them. (https://developer.apple.com/streaming/fps/)
Further, I will describe how I achieve to play HLS in offline mode with AES-128 encryption. The Example On Github describes the below process.
Take care AVDownloadTask doesn’t work on the simulator so you should have a device for this implementation.
At the beginning, you need a stream URL.
Step 1:
Before creating AVURLAsset we should take stream URL and change scheme to an invalid one (example: https -> fakehttps, I did it through URLComponents) and assign AVAssetResourceLoaderDelegate to the new created url asset. All this changes force AVAssetDownloadTask to call:
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
}
(it is calling because AVFoundation see an invalid URL and doesn’t know what to do with it)
Step 2:
When delegate is called we should check that url is that one that we had before. We need to change back scheme to valid one and create a simple URLSession with it. We will get first .m3u8 file that should be like:
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1697588,RESOLUTION=1280x720,FRAME-RATE=23.980,CODECS="mp4a"
https://avid.avid.net/avid/information_about_stream1
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1132382,RESOLUTION=848x480,FRAME-RATE=23.980,CODECS="mp4a"
https://avid.avid.net/avid/information_about_stream2
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=690409,RESOLUTION=640x360,FRAME-RATE=23.980,CODECS="mp4a"
https://avid.avid.net/avid/information_about_stream3
Step 3:
Parse all needed information from this data and change all https schemes to invalid one fakehttps
Now you should setup AVAssetResourceLoadingRequest from shouldWaitForLoadingOfRequestedResource delegate like:
loadingRequest.contentInformationRequest?.contentType = response.mimeType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = response.expectedContentLength
loadingRequest.dataRequest?.respond(with: modifiedData)
loadingRequest.finishLoading()
downloadTask?.resume()
where: response -> response from URLSession, modifiedData -> data with changed URL’s
Resume your download task and return true in shouldWaitForLoadingOfRequestedResource delegate
Step 4:
If everything will be ok AVAssetDownloadDelegate will fire with:
- (void)URLSession:(NSURLSession *)session assetDownloadTask:(AVAssetDownloadTask *)assetDownloadTask didResolveMediaSelection:(AVMediaSelection *)resolvedMediaSelection NS_AVAILABLE_IOS(9_0) {
}
Step 5:
We have changed all https to fakehttps when AVFoundation will select best media stream URL, shouldWaitForLoadingOfRequestedResource will trigger again with one of the URL from first .m3u8
Step 6:
When delegate is called again we should check that url is that one that we needed. Change again fake scheme to a valid one and create a simple URLSession with this url. We will get second .m3u8 file:
#EXTM3U
#EXT-X-TARGETDURATION:12
#EXT-X-ALLOW-CACHE:YES
#EXT-X-KEY:METHOD=AES-128,URI="https://avid.avid.net/avid/key”
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:6.006,
https://avid.avid.net/avid/information_about_stream1
#EXTINF:4.713,
https://avid.avid.net/avid/information_about_stream2
#EXTINF:10.093,
https://avid.avid.net/avid/information_about_stream3
#EXT-X-ENDLIST
Step 7:
Parse second .m3u8 file and take all information that you need from it, also take a look on
#EXT-X-KEY:METHOD=AES-128,URI="https://avid.avid.net/avid/key”
We have URL for encryption key
Step 8:
Before sending some information back to AVAssetDownloadDelegate we need to download the key from the server and save it locally on the device. After this you should change URI=https://avid.avid.net/avid/key from second .m3u8 to an invalid URI=fakehttps://avid.avid.net/avid/key, or maybe a local file path where you have saved your local key.
Now you should setup AVAssetResourceLoadingRequest from shouldWaitForLoadingOfRequestedResource delegate smth. like:
loadingRequest.contentInformationRequest?.contentType = response.mimeType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = response.expectedContentLength
loadingRequest.dataRequest?.respond(with: modifiedData)
loadingRequest.finishLoading()
downloadTask?.resume()
where: response -> response from URLSession, modifiedData -> data with changed URL’s
Resume your download task and return true in shouldWaitForLoadingOfRequestedResource delegate (Same as on Step 3)
Step 9:
Of course, when download task will try to create request with modified URI= that again is not a valid one shouldWaitForLoadingOfRequestedResource will trigger again. In this case, you should detect this and create new data with your persistent key(the key that you saved locally. Take care here contentType should be AVStreamingKeyDeliveryPersistentContentKeyType without it AVFoundation doesn’t understand that this contains key).
loadingRequest.contentInformationRequest?.contentType = AVStreamingKeyDeliveryPersistentContentKeyType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = keyData.count
loadingRequest.dataRequest?.respond(with: keyData)
loadingRequest.finishLoading()
downloadTask?.resume()
Step 10:
Chunks will be downloaded automatically by AVFoudnation.
When download is finished this delegate will be called:
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
}
You should save location somewhere, when you want to play stream from device you should create AVURLAsset from this location URL
All this information is saved locally by AVFoundation so next time when you will try to play local content in offline AVURLAsset delegate will be called because of URI=fakehttps://avid.avid.net/avid/key, that is an invalid link, here you will do Step 9 again and video will play in offline mode.
This works for me if anyone knows better implementation I will be glad to know.
Example On Github
#Cyklet Answer helped me a ton! Thanks.
There is however an additional step I had to do to get hls streaming to work.
When using shouldWaitForLoadingOfRequestedResource apples documentation states:
contentType
Before finishing loading an AVAssetResourceLoadingRequest instance, if its contentInformationRequest property is not nil, set the value of this property to a UTI indicating the type of data contained by the requested resource.
When trying to implement HLS Streaming there are two UTIs that may be used (as far as I know...).
AVStreamingKeyDeliveryPersistentContentKeyType
"com.apple.streamingkeydelivery.persistentcontentkey"
AVStreamingKeyDeliveryContentKeyType
"com.apple.streamingkeydelivery.contentkey"
Check allowedContentTypes what UTI to use. See possible implementation below:
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
....
var contentType = AVStreamingKeyDeliveryPersistentContentKeyType
if let allowedContentType = contentInformationRequest.allowedContentTypes?.first{
if allowedContentType == AVStreamingKeyDeliveryContentKeyType{
contentType = AVStreamingKeyDeliveryContentKeyType
}
}
....
}
Related
I want obtain all cookies from WKWebView. Why? I have been started a project that use web-based auth. As result, I should intercept cookies to be sure that user is logged in and for some other purposes. Another case - imagine if user logged in, and than he "kill" the app - due to some delay in storing this cookie session will be lost :(.
The problem seems to be that the cookies are cached and not saved out
to a file immediately.
(#Kemenaran from here - p.5 below)
The point where I try to catch them -
webView:decidePolicyForNavigationResponse:decisionHandler:,
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
if let httpResponse = navigationResponse.response as? NSHTTPURLResponse {
if let headers = httpResponse.allHeaderFields as? [String: String], url = httpResponse.URL {
let cookies = NSHTTPCookie.cookiesWithResponseHeaderFields(headers, forURL: url {
for cookie in cookies {
NSHTTPCookieStorage.shared.set(cookie)
}
}
}
}
but not all request are navigation, so one cookie (in my case) is skipped, see details below
Few words about other option I tried...
Yes, i Know that starting from iOS 11, we can use WKHTTPCookieStore as mention here. But my project should support iOS 9+
I for 100% sure, that after 5-10 sec from login, required cookie will be saved to NSHttpCookieStorage (at least all my tests during few days confirm that)
I try to use provided observer NSHTTPCookieManagerCookiesChangedNotification, but it provide me callback only for cookies that comes within webView:decidePolicyForNavigationResponse:decisionHandler
I also try to get cookies using some JS like mentioned here and also test all suggestion from here - really great article by the way. Result - negative
I also found this radar bug, and this SO question, and Sample project, but I want to prevent even this case. (described in this post applicable not only for remove but and for save) Also this situation true and when user kill the app, so case when user login, kill app and relaunch, may be present. And preventing this (simply by checking NSHttpCookieStorage for required cookies are also not good idea, because exactly after login required cookie can be stored with some delay, so this approach requires some bool-powered solution, that looks like weird..
I also read few more SO post for some related problem, and the most usefull are
This one
Another one
One more
But still without good solution...
So, is any way exist to obtain or at least force to immediately store cookies?
I ended with simple "force-like" saving Cookie from webpage.
To get all cookie i use
stringByEvaluatingJavaScriptFromString
with JS string like document.cookie();. As result i able to receive all cookies as a string with ; separator. All i need to do - parse string, create cookie and set it to NSHttpSharedStorage
I want to encrypt/decrypt all cached data from a NSURLSession using AES256. I'm new using Alamofire but I think it is possible to do it without involving the library itself.
I don't know exactly what is the most seamless way to encrypt the data before caching and decrypt it after being retrieved from cache.
I see I can use Alamofire's SessionDelegate and the methods dataTaskWillCacheResponse and dataTaskWillCacheResponseWithCompletion to encrypt but I don't see anything related with the data being extracted from the cache to do the decrypting.
On the other hand I was thinking about a custom NSURLProtocol to override cachedResponse but I don't see anything related with the caching of that response, only with the extracted data.
In summary, I don't know if it is possible to accomplish this, or I have to use a mix between the NSURLSessionDelegate/SessionDelegate and NSURLProtocol, or maybe subclass NSURLCache to do the job and pass it to the Alamofire session, or there is something simpler out there, or I'm terribly wrong :P
Any help will be really appreciated.
EDIT
I'm trying to achieve it with the next implementation. First of all a very simple subclass of the cache:
class EncryptedURLCache: URLCache {
let encryptionKey: String
init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String? = nil, encryptionKey: String) {
guard !encryptionKey.isEmpty else {
fatalError("No encryption key provided")
}
self.encryptionKey = encryptionKey
super.init(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: path)
}
override func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
return super.cachedResponse(for: request)?.cloneDecryptingData(withKey: encryptionKey)
}
override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
super.storeCachedResponse(cachedResponse.cloneEncryptingData(withKey: encryptionKey), for: request)
}
}
And an extension of the cached response to return the encrypted/decrypted data
extension CachedURLResponse {
func cloneEncryptingData(withKey key: String) -> CachedURLResponse {
return clone(withData: data.aes256Encrypted(withKey: key))
}
func cloneDecryptingData(withKey key: String) -> CachedURLResponse {
return clone(withData: data.aes256Decrypted(withKey: key) ?? data)
}
private func clone(withData data: Data) -> CachedURLResponse {
return CachedURLResponse(
response: response,
data: data,
userInfo: userInfo,
storagePolicy: storagePolicy
)
}
}
This is working but only for a mockable.io that I mounted with the header Cache-Control: max-age=60. I'm also testing against the SWAPI http://swapi.co/api/people/1/ and against Google Books https://www.googleapis.com/books/v1/volumes?q=swift+programming.
In all three cases the responses are correctly encrypted and cached. I'm doing my testing cutting off the Internet connection and setting the session configuration's requestCachePolicy = .returnCacheDataDontLoad.
In this scenario, the request made to mockable.io is correctly decrypted and returned from cache but the others say NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline.". This is VERY strange because, with that policy, it has to say NSURLErrorDomain Code=-1008 "resource unavailable" if there is no possibility to return the cached data. If there is an error decrypting then it says it was an error serializing to a JSON object.
I've also tested with the common shared cache and it works as expected, with that policy the data is returned. I thought it could be something related with the absence of cache headers in the SWAPI and GBooks responses but this test works, it returns the cached data.
Then I made another test: using my cache but without encrypting/decrypting data, simply cloning the returned cached response with the data as is, with no results. Then I tried a final and very stupid test: to avoid cloning the response, just return the cachedResponse and then IT WORKED. How the h*** is that possible? If I clone the cachedResponse to inject my encrypted/decrypted data it does not work! Even in examples from Apple they are creating new cached responses with no fear.
I don't know where is the error but I'm going to jump over the window in a minute or two.
Please, any help? Thank you so much.
EDIT 2
I was changing emails with a DTS engineer from Apple and the conclusion is that this is not possible to achieve this because the backing CF type is doing more logic than the Foundation object, in this case it is doing a validation against the URLRequest that is passed to it when the system caches the response, but I cannot pass it when make the clone with the regular NSCachedURLResponse.
When the system validates against the request, there is none to match with.
There is no way to intercept cache retrieval calls from the delegate side that I'm aware of, and I don't think that a custom protocol will even be asked to handle the request if it comes out of the cache, but I could be wrong. So probably your options are:
Explicitly ask the cache for the data before you make the URL request.
Add code in the code that actually handles the response so that it recognizes that the data is encrypted and decrypt it.
For example, you could insert an additional header into the headers as you store it into the cache to indicate that the cached data is encrypted. Then, when you see that magic header value on the way back out, decrypt it.
Write a subclass of NSURLCache and handle the decryption there (and ideally, store the on-disk data in a different file to avoid breaking any requests in your app that use the normal cache).
I want to download audio files (mp3, m4a, wma...) with Google Drive API. Trying following code, I got the error on simulator.
Error:
The operation could't be completed. (The user has not granted the app xxxx read access to the file alEnlflseiFNSEi.)
Code:
let url = "https://www.googleapis.com/drive/v3/files/\(identifier)?alt=media"
service.fetchObject(
with: URL(string: url)!,
delegate: self,
didFinish: #selector(GoogleDriveFileListTableViewController.downloadWithTicket(ticket:finishedWithObject:error:))
)
// Parse results and display
func downloadWithTicket(ticket : GTLServiceTicket, finishedWithObject response : GTLDriveFileList, error : NSError?) {
if let error = error {
showAlert(title: "Error", message: error.localizedDescription)
return
}
}
With regard to your "read access" problem here's what the Download files states:
Downloading the file requires the user to have at least read access.
Additionally, your app must be authorized with a scope that allows
reading of file content. For example, an app using the
drive.readonly.metadata scope would not be authorized to download the
file contents. Users with edit permission may restrict downloading by
read-only users by setting the viewersCanCopyContent field to true.
Try obtaining the webContentLink of the audio file using Files.get. When this link is clicked or opened in a new window, the audio file will be downloaded.
It looks something like this:
https://drive.google.com/uc?id=0Bzgk4zccNwI7TzBxZTFzbHFhdUU&export=download
Now i'm using UIWebView and with canInitWithRequest: of NSURLProtocol i can intercept all requests and do with it what I want.
In the new WKWebView this method there isn't, and i not found something similar.
Has someone resolved this problem?
I see that after 5 years this question still generates curiosity, so I describe how I solved it and about some main problems I faced up.
As many who answered here, I have implemented WKURLSchemeHandler and used new schemes.
First of all the URL that wkwebview launches must not be HTTP (or HTTPS) but one of yours new schemes.
Example
mynewscheme://your-server-application.com
In you WKWebViewConfiguration conf, I set the handler:
[conf setURLSchemeHandler:[CustomSchemeHandler new] forURLScheme:#"mynewscheme"];
[conf setURLSchemeHandler:[CustomSchemeHandler new] forURLScheme:#"mynewschemesecure"];
In CustomSchemeHandler I have implemented webView:startURLSchemeTask: and webView:stopURLSchemeTask:.
In my case I check if the request is for a file that I just saved locally, otherwise I change actual protocol ("mynewscheme or "mynewschemesecure") with http (or https) and I make request by myself.
At this point I solved the "interception problem".
In this new way we have the webview "location" (location.href via javascript) with my new scheme and with it new problems started.
First problem is that my applications work mainly with javascript,
and document.cookie has stopped working. I'm using Cordova
framework, so I've develeped a plugin to set and get cookie to
replace document.cookie (I had to do this, because, obviously, I
have also http header set-cookie).
Second problem is that I've got a lot of "cross-origin" problems, then
I changed all my urls in relative url (or with new schemes)
Third problem is that browser automatically handle server port 80
and 443, omitting them, but has now stopped (maybe because of "not
http location"). In my server code I had to handle this.
Writing down these few rows I admit that it seems to was an easy problem to solve, but I ensure that find out a workaround, how to solve it and integrate with the infinite amount of code has been hard. Every step towards the solution corresponded to a new problem.
You can intercept requests on WKWebView since iOS 8.0 by implementing the decidePolicyFor: navigationAction: method for the WKNavigationDelegate
func webView(_ webView: WKWebView, decidePolicyFor
navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Swift.Void) {
//link to intercept www.example.com
// navigation types: linkActivated, formSubmitted,
// backForward, reload, formResubmitted, other
if navigationAction.navigationType == .linkActivated {
if navigationAction.request.url!.absoluteString == "http://www.example.com" {
//do stuff
//this tells the webview to cancel the request
decisionHandler(.cancel)
return
}
}
//this tells the webview to allow the request
decisionHandler(.allow)
}
there are many ways to implement intercepter request.
setup a local proxy, use WKNavigationDelegate
-[ViewController webView:decidePolicyForNavigationAction:decisionHandler:]
load new request and forward to local server, and at the local server side you can do something(eg. cache).
private api. use WKBrowsingContextController and custom URLProtocol
Class cls = NSClassFromString(#"WKBrowsingContextController");
SEL sel = NSSelectorFromString(#"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 把 http 和 https 请求交给 NSURLProtocol 处理
[(id)cls performSelector:sel withObject:#"http"];
[(id)cls performSelector:sel withObject:#"https"];
}
use KVO to get system http/https handler.(ios 11, *)
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
URLSchemeHandler *schemeHandler = [[URLSchemeHandler alloc] init];
[configuration setURLSchemeHandler:schemeHandler forURLScheme:#"test"];
NSMutableDictionary *handlers = [configuration valueForKey:#"_urlSchemeHandlers"];
handlers[#"http"] = schemeHandler;
handlers[#"https"] = schemeHandler;
all the three ways you probably need handle CORS & post bodies to be stripped,you overwrite change browser`s option request(
Preflighted_requests),you can custom http header to replace http body for post bodies to be stripped.
in iOS 11 WKWebView has come up with Custom Scheme Handler called WKURLSchemeHandler, which you can use to intercept the custom events.
for more info check out this project.
https://github.com/BKRApps/KRWebView
I know I am late but I am able to solve this problem. I can intercept each and every request even your http/https call using below trick. I can also trace the call made from html to server calls. I can also use this to render html with offline content.
Download the html of the website that we want to render in offline or online to intercept the request.
Either place the html in document directory of the user or place it inside the archive. But we should know the path of the html file.
Place all your js, cs, woff, font of our website at the same level as our base html. We need to given permission while loading the web view.
Then we have to register our own custom handler scheme with WKWebView. When wkwebview see the pattern "myhandler-webview" then it will give you control and you will get the callback to 'func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask)' delegate implementation. You can play around with url in this delegate like mentioned in point 6.
let configuration = WKWebViewConfiguration();
configuration.setURLSchemeHandler(self, forURLScheme: "myhandler-webview");
webView = WKWebView(frame: view.bounds, configuration: configuration);
Convert file scheme to the custom scheme (myhandler-webview) then load it with WKWebView
let htmlPath = Bundle.main.path(forResource: "index", ofType: "html")
var htmlURL = URL(fileURLWithPath: htmlPath!, isDirectory: false)
htmlURL = self.changeURLScheme(newScheme: "myhandler-webview", forURL: htmlURL)
self.webView.load(URLRequest(url: htmlURL))
Implement below methods of WKURLSchemeHandler protocol and handle didReceiveResponse, didReceiveData, didFinish delegate methods of WKURLSchemeTask.
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
print("Function: \(#function), line: \(#line)")
print("==> \(urlSchemeTask.request.url?.absoluteString ?? "")\n")
// You can find the url pattern by using urlSchemeTask.request.url. and create NSData from your local resource and send the data using 3 delegate method like done below.
// You can also call server api from this native code and return the data to the task.
// You can also cache the data coming from server and use it during offline access of this html.
// When you are returning html the the mime type should be 'text/html'. When you are trying to return Json data then we should change the mime type to 'application/json'.
// For returning json data you need to return NSHTTPURLResponse which has base classs of NSURLResponse with status code 200.
// Handle WKURLSchemeTask delegate methods
let url = changeURLScheme(newScheme: "file", forURL: urlSchemeTask.request.url!)
do {
let data = try Data(contentsOf: url)
urlSchemeTask.didReceive(URLResponse(url: urlSchemeTask.request.url!, mimeType: "text/html", expectedContentLength: data.count, textEncodingName: nil))
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
} catch {
print("Unexpected error when get data from URL: \(url)")
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
print("Function: \(#function), line: \(#line)")
print("==> \(urlSchemeTask.request.url?.absoluteString ?? "")\n")
}
Let me know if this explanation is not enough.
Objective c example mentioned below
intercepting request with wkwebview
One can use WKURLSchemeHandler to intercept each and every request to be loaded in WKWebView,
Only disadvantage is that you cannot register http or https scheme for interception,
Solution over that is,
Replace your http/https scheme of url with custom scheme url like xyz://
for e.g. https://google.com can be loaded like xyz://google.com
Now you will get a callback in WKURLSchemeHandler there you again replace it back to https and load data programmatically and call urlSchemeTask.didReceive(response)
This way each and every https request will come to your handler.
I am blindly taking guesses since I only have my windows computer with me. By reading the Apple Developer documentation here is information I gathered that might lead to some ideas on how to solve the question.
Based on WKWebView,
Set the delegate property to an object conforming to the WKUIDelegate protocol to track the loading of web content.
Also, I see we can set our navigationDelegate with the,
weak var navigationDelegate: WKNavigationDelegate? { get set }
The methods of the WKNavigationDelegate protocol help you implement custom behaviors that are triggered during a web view's process of accepting, loading, and completing a navigation request.
Then after we create and set our custom WKNavigationDelegate, we would override some methods to intercept something we might be looking for. I found the Responding to Server Actions section of some interest since they receive a WKNavigation as parameter. Moreover, you might want to skim through WKNavigationAction and WKNavigationResponse see if there is perhaps something which might help us achieve our goal.
BTW, I am just giving some ideas on what to try so that we can solve this question, ideas which might be 100% wrong cause I have not tried them myself.
I have been trying to set up a web view wrapper app that will load the content of a website (still to be launched). Currently, the website is in development mode, and the only endpoints for the website are protected behind a http authentication.
I have been looking at this solution: Swift webview xcode post data
However, I do not want to make a POST request each time, but I'd rather want to authenticate against the website once and keep the connection.
What I'm looking is for a clean and stable solution, one that would allow me to be able to have control of edge cases such as bad credentials provided.
I am not comfortable with using the NSURLConnection because that solution is deprecated in iOS9. I need a solution with NSURLSession.
Let me know if I'm missing something within the above linked solution. I am sure someone had this issue as well. Additionally, the website has SSL protection.
Kind regards
I'm not entirely sure this fulfils your demands in the best way, but if you can use a WKWebView, maybe you can simply rely on the authentication challenge delegate method? See my answer here as well, the relevant code snippet would be:
func webView(webView: WKWebView, didReceiveAuthenticationChallenge
challenge: NSURLAuthenticationChallenge,
completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
let creds = NSURLCredential(user:"username", password:"password", persistence: NSURLCredentialPersistence.ForSession)
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, creds)
}
I haven't tried it yet myself, but the documentation says the credentials should be used for the session, so additional requests resulting from links should work. Even if not, that just results for the method to be called again and you can provide the credentials once more.
This is just a rump, you'd have to get name and password from an alert or the like (also you can store the credentials more elegantly to make subsequent calls to the delegate method more elegant).
You also wrote you're using SSL, so I take it you're familiar with the App Transport Security flags (since the question title just has "HTTP" in it and not "HTTPS", which you probably want for it to work smoothly, otherwise see the question I linked).
Okay here is the Swift 3 code.
extension MyController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let u = self.webuser, let p = self.webp else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let creds = URLCredential.init(user: u, password: p, persistence: .forSession)
completionHandler(.useCredential, creds)
}
}
You shouldn't need to use POST or a connection or session. You should just create a mutable URL request and set an auth header. Then ask the web view to load the request.
Auth header details from Wikipedia:
The username and password are combined with a single colon.
The resulting string is encoded using the RFC2045-MIME variant of Base64, except not limited to 76 char/line.
The authorization method and a space i.e. "Basic " is then put before the encoded string.
For example, if the user agent uses Aladdin as the username and OpenSesame as the password then the field is formed as follows:
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l