How to make a NSURLSesssion GET request with cookies - ios

I'm using the Pinterest SDK to download a Pinterest Pin's link, (sample link that I get back from the server: https://www.pinterest.com/r/pin/186195765822871832/4801566892554728205/77314e40aeb26c0dc412e9cfa82f8dccc401fdb2b9806a3fe17ba8bafdb50510).
About 5 days ago I started getting 404 errors in my NSURLSesssion when trying to access similar links that I'd pulled down from Pinterest.
A friend said that he believes Pinterest must now require cookies to access that link.
How can I configure my session so that I can use cookies and get a 200 response back from Pinterest?
UPDATED CODE:
import UIKit
import PlaygroundSupport
var url = URL(string: "https://www.pinterest.com/r/pin/186195765822871832/4801566892554728205/77314e40aeb26c0dc412e9cfa82f8dccc401fdb2b9806a3fe17ba8bafdb50510")
var getSourceURLFromPinterest: URLSessionDataTask? = nil
let sessionConfig: URLSessionConfiguration = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 30.0
sessionConfig.timeoutIntervalForResource = 30.0
let cookieJar = HTTPCookieStorage.shared
let cookieHeaderField = ["Set-Cookie": "key=value, key2=value2"]
let cookies = HTTPCookie.cookies(withResponseHeaderFields: cookieHeaderField, for: url!)
HTTPCookieStorage.shared.setCookies(cookies, for: url, mainDocumentURL: url)
let session = URLSession(configuration: sessionConfig)
getSourceURLFromPinterest = session.dataTask(with: url! as URL) { (data: Data?, response: URLResponse?, error: Error?) -> Void in
if error != nil {
print("error is \(error)")
}
if response == nil {
print("no response")
} else if let _ = data {
//Config Request
let request = NSMutableURLRequest(
url: (response?.url)!,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 30.0)
request.httpMethod = "HEAD"
var statusCode = Int()
let session = URLSession.shared
let checkURLForResponse = session.dataTask(with: request as URLRequest) {urlData, myResponse, responseError in
if let httpResponse = myResponse as? HTTPURLResponse {
statusCode = httpResponse.statusCode
switch statusCode {
case _ where statusCode < 500 && statusCode > 299 && statusCode != 405: //whitelisted 405 to exclude Amazon.com false errors
print("status code \(statusCode) for \(url)")
default:
break;
}
} else { print("***NO httpResponse for \(url)") }
}
checkURLForResponse.resume()
}
}
getSourceURLFromPinterest!.resume()
PlaygroundPage.current.needsIndefiniteExecution = true

The other answers may work generally, but specifically for me this is how I coded the request in order to get a response from Pinterest's server. Note that specifically what I am doing I think is related to a possible bug in Pinterest's server, see: https://github.com/pinterest/ios-pdk/issues/124
I commented out my personal Pinterest session ID
var cookieSession = String()
var cookieCSRFToken = String()
var myWebServiceUrl = URL(string: "https://www.pinterest.com/r/pin/186195765821344905/4801566892554728205/a9bb098fcbd6b73c4f38a127caca17491dafc57135e9bbf6a0fdd61eab4ba885")
let requestOne = URLRequest(url: myWebServiceUrl!)
let sessionOne = URLSession.shared
let taskOne = sessionOne.dataTask(with: requestOne, completionHandler: { (data, response, error) in
if let error = error {
print("ERROR: \(error)")
}
else {
print("RESPONSE: \(response)")
if let data = data, let dataString = String(data: data, encoding: .utf8) {
print("DATA: " + dataString)
} // end: if
var cookies:[HTTPCookie] = HTTPCookieStorage.shared.cookies! as [HTTPCookie]
print("Cookies Count = \(cookies.count)")
for cookie:HTTPCookie in cookies as [HTTPCookie] {
// Get the _pinterest_sess ID
if cookie.name as String == "_pinterest_sess" {
//var cookieValue : String = "CookieName=" + cookie.value as String
cookieSession = cookie.value as String
print(cookieSession)
}
// Get the csrftoken
if cookie.name as String == "csrftoken" {
cookieCSRFToken = cookie.value
print(cookieCSRFToken)
}
} // end: for
} // end: if
})
taskOne.resume()
var requestTwo = URLRequest(url: myWebServiceUrl!)
cookieSession = "XXXXXXXX"
cookieCSRFToken = "JHDylCCKKNToE4VXgofq1ad3hg06uKKl"
var cookieRequest = "_auth=1; _b=\"AR4XTkMmqo9JKradOZyuMoSWcMdsBMuBHHIM21wj2RPInwdkbl2yuy56yQR4iqxJ+N4=\"; _pinterest_pfob=disabled; _pinterest_sess=\"" + cookieSession + "\"; csrftoken=" + cookieCSRFToken as String
requestTwo.setValue(cookieRequest as String, forHTTPHeaderField: "Cookie")
let taskTwo = sessionOne.dataTask(with: requestTwo, completionHandler: { (data, response, error) in
if let error = error {
print("ERROR: \(error)")
}
else {
print("RESPONSE: \(response)")
} // end: if
})
taskTwo.resume()
PlaygroundPage.current.needsIndefiniteExecution = true

You can configure a cookie based session in the following way. Please let me know if you need any help. The below is just an example
let session: URLSession = URLSession.shared
session.dataTask(with: myUrlRequest { urlData, response, responseError in
let httpRes: HTTPURLResponse = (response as? HTTPURLResponse)!
let cookies:[HTTPCookie] = HTTPCookie.cookies(withResponseHeaderFields: httpRes.allHeaderFields as! [String : String], for: httpRes.url!)
HTTPCookieStorage.shared.setCookies(cookies, for: response?.url!, mainDocumentURL: nil)
if responseError == nil {
}else {
}
}.resume()
Feel free to suggest edits to make it better. Please let me know if the below doesn't work.

When you do an authentication with the server, the server gives out the cookies, which you receives in the header of the response. You can extract that and set as a cookie in the shared storage of cookies. So everytime you make a call, for those domain, the cookies will be shared and checked, and if valid, it will let you in.
let resp: HTTPURLResponse = (response as? HTTPURLResponse)!
let cookies:[HTTPCookie] = HTTPCookie.cookies(withResponseHeaderFields: resp.allHeaderFields as! [String : String], for: resp.url!)
HTTPCookieStorage.shared.setCookies(cookies, for: response?.url!, mainDocumentURL: nil)
In this, the cookies will be an array, which contain cookies in an array. An response may contain more than one cookies.
In case, if you are using third party framework to manage the HTTP requests like Alamofire then it will take cares of the cookie management itself.

Related

`WKWebsiteDataStore.default()` crash if the code is access before any webview is accessed. Why?

I have the following code
let url = URL(string: "http://127.0.0.1/set_simple.php")!
let request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
let session = URLSession.shared
session.configuration.httpCookieAcceptPolicy = .always
session.configuration.httpCookieStorage = HTTPCookieStorage.shared
session.configuration.httpShouldSetCookies = true
let dataTask = session.dataTask(with: request) { (data, response, error) in
if error == nil {
print("Success fetch!")
guard
let url = response?.url,
let httpResponse = response as? HTTPURLResponse,
let fields = httpResponse.allHeaderFields as? [String: String]
else { return }
let cookies = HTTPCookie.cookies(withResponseHeaderFields: fields, for: url)
HTTPCookieStorage.shared.setCookies(cookies, for: url, mainDocumentURL: nil)
for cookie in cookies {
var cookieProperties = [HTTPCookiePropertyKey: Any]()
cookieProperties[.name] = cookie.name
cookieProperties[.value] = cookie.value
cookieProperties[.domain] = cookie.domain
cookieProperties[.path] = cookie.path
cookieProperties[.version] = cookie.version
cookieProperties[.expires] = Date().addingTimeInterval(31536000)
let newCookie = HTTPCookie(properties: cookieProperties)
// HTTPCookieStorage.shared.setCookie(newCookie!)
WKWebsiteDataStore.default().httpCookieStore.setCookie(newCookie!, completionHandler: nil)
print("name: \(cookie.name) value: \(cookie.value)")
}
} else {
print("Ops! \(error.debugDescription)")
}
}
dataTask.resume()
It works well if I only access it, after accessing a webview.
However, if I start the app and access this code before accessing any webview, it will crash at WKWebsiteDataStore.default(). Is it because I need to setup the WKWebsiteDataStore default?
Looks like I need to set it on the mainThread
DispatchQueue.main.async {
WKWebsiteDataStore.default().httpCookieStore.setCookie(newCookie!, completionHandler: nil)
}

Swift iOS15 async/await + basic authentication + http request slow(er)

I am using the below code for basic http authentication. It is noticeably slower than when I wasn't using authentication (the below is called around 30 times)
Are there any speed optimization changes that could be made to the code ?
Thanks
struct PrixJSONService {
let passwordString = "user:password"
let configuration = URLSessionConfiguration.default
enum PrixJSONServiceError: Error {
case failed
case failedToDecode
case invalidStatusCode
}
func fetchPrix(for stationId:String) async throws -> [Prix] {
let passwordData = passwordString.data(using:String.Encoding.utf8)!
let base64EncodedCredential = passwordData.base64EncodedString()
let authString = "Basic \(base64EncodedCredential)"
let session = URLSession(configuration: configuration)
configuration.httpAdditionalHeaders = ["Authorization" : authString]
let dataUrl = "https://xxxx.xx/~xx/xxxxxxxx/prix/\(stationId)/price.json"
let url = URL(string: dataUrl)!
var urlRequest = URLRequest(url: url)
urlRequest.setValue("Basic \(base64EncodedCredential)", forHTTPHeaderField: "Authorization")
urlRequest.httpMethod = "GET"
let (data, response) = try await session.data(for: urlRequest)
guard let response = response as? HTTPURLResponse,
response.statusCode == 200 else {
throw PrixJSONServiceError.invalidStatusCode
}
let decodedData = try JSONDecoder().decode([Price].self, from: data)
return decodedData
}
}

(Application.Errors.invalidResponse) Cant fix invalidResponse error

I don't get it why its showing invalidResponse as error when I think I did it all right the Token is correct the api URL is correct and tested on Postman, but when debugging its showing error at invalidResponse or invalidData after the response error, I haven't yet tried to fill TableView with the incoming api data fields because I want to try first if its coming before proceeding :( :)
class APICaller {
static let shared = APICaller()
private let baseURL = "http://000.000.000.000:3030/api/"
private init() {}
func getVehicles(for id: String, completed: #escaping (Result<[Vehicles],OnTraErrors>) -> Void ){
let endpoint = baseURL + "GetVehicles?UserIdentificationValue=\(id)"
guard let url = URL(string: endpoint) else {
completed(.failure(.invalidURL))
return
}
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 120)
request.httpMethod = "GET"
request.allHTTPHeaderFields = [
"content-type": "application/json",
"authorizetoken": "Bearer NjQzODM2N0NDNDM4NDhCNDk3RkU0NjA0QUY0NjVFS3GE=",
"cache-control": "no-cache",
]
let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
if let _ = error {
completed(.failure(.unableToComplete))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completed(.failure(.invalidResponse))
return
}
guard let data = data else {
completed(.failure(.invalidData))
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let vehicles = try decoder.decode([Vehicles].self, from: data)
completed(.success(vehicles))
} catch {
completed(.failure(.invalidData))
}
}
task.resume()
}

Spotify API Authorization Error (Swift)

I've made a little App in Swift where a user can search the Spotify database for songs. I am using the Web API Console > Search for an Item. My problem is the new OAuth system where you have to sign-in and all that stuff. My authorization is ok, but when I'm trying to get an access token with the following code, it's returning me the following error: {"error":"server_error","error_description":"Unexpected status: 400"}. My code is:
let keys = "<MY_APPLICATION_KEYS>"
let url = NSURL(string: "https://accounts.spotify.com/api/token")
let session = URLSession.shared
let request = NSMutableURLRequest(url: url! as URL)
request.httpMethod = "POST"
request.setValue("Basic \(keys)", forHTTPHeaderField: "Authorization")
request.setValue("client_credentials", forHTTPHeaderField: "grant_type")
let task = session.dataTask(with: request as URLRequest) { (data, response, error) in
guard let _: Data = data, let _: URLResponse = response, error == nil else {
print(error!)
return
}
let dataString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("Data: \(dataString!)")
self.parseData(JSONData: data!)
}
task.resume()
}
var accessToken = ""
func parseData(JSONData : Data) {
do {
var readableJSON = try JSONSerialization.jsonObject(with: JSONData, options: .mutableContainers) as! JSONStandard
if let token = readableJSON["access_token"] as? String {
accessToken = token
}
print("Access Token: \(accessToken)")
updateTokenInFirebase()
}
catch{
print(error)
}
Any help would be very appreciated, thank you very much in advance!
Documentation of the Web API: Web API Link
I am using on the Client Credentials Flow the first method.
I know it's been ~1 year since you posted this but I had the same issue and after a few tries was able to get it. You can test this in Playground.
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
if let url = URL(string: "https://accounts.spotify.com/api/token") {
var postRequest = URLRequest(url: url)
postRequest.httpMethod = "POST"
let bodyParams = "grant_type=client_credentials"
postRequest.httpBody = bodyParams.data(using: String.Encoding.ascii, allowLossyConversion: true)
let id = "your client id"
let secret = "your secret"
let combined = "\(id):\(secret)"
let combo = "\(id):\(secret)".toBase64()
postRequest.addValue("Basic \(combo)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: postRequest) { (data, response, error) in
guard let data = data else {
return
}
print(String(data: data, encoding: String.Encoding.utf8)!)
}
task.resume()
}
extension String {
func fromBase64() -> String? {
guard let data = Data(base64Encoded: self) else {
return nil
}
return String(data: data, encoding: .utf8)
}
func toBase64() -> String {
return Data(self.utf8).base64EncodedString()
}
}
I know this is really late, but the issue is with this line:
request.setValue("client_credentials", forHTTPHeaderField: "grant_type")
According to the authorization guide, this should be in the body of the request, not the headers.

Make a http request with basic authentication

I want to make an http request with basic auth. I tried to follow this topic : click here. But Xcode tell me
Bad Request after httpResponse
And I don't know why since this morning. Maybe anyone got a idea more interesting than I can find on the web ? :)
Here is my code :
func topRefresh(sender:AnyObject){
var list=Array<Notification>()
//credential
let credentialLogin = NSUserDefaults.standardUserDefaults().objectForKey("login") as! String
let credentialPassword = NSUserDefaults.standardUserDefaults().objectForKey("password") as! String
// set up the base64-encoded credentials
let loginString = NSString(format: "%#:%#", credentialLogin, credentialPassword)
let loginData: NSData = loginString.dataUsingEncoding(NSUTF8StringEncoding)!
let base64LoginString = loginData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
// create the request
let url = NSURL(string: jsonLink)!
let request = NSMutableURLRequest(URL: url)
request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringCacheData
request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
request.HTTPMethod="GET"
let paramString="login="+credentialLogin
request.HTTPBody = paramString.dataUsingEncoding(NSUTF8StringEncoding)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) {
(data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
if (httpResponse.statusCode == 200) {
do{
if (data != nil){
self.notificationsDisplayed.removeAll()
let jsonDict = try NSJSONSerialization.JSONObjectWithData(data!,options: .AllowFragments)
list=self.parseJson(jsonDict)
if (self.notifications.count==0){
self.notifications=self.copyArray(list)
list.removeAll()
}else{
//compare new data with past data
while(list.count>0){
print(list.count)
let tmp=list.last
for notification in self.notifications {
if(tmp!.id==notification.id){
list.removeLast()
break
}else{
self.notifications.insert(tmp!, atIndex: 0)
list.removeLast()
break
}
}
}
}
self.orderByDate(&self.notifications)
self.notificationsDisplayed=self.copyArray(self.notifications)
self.applyFilter()
print("Data parsed")
}else{
print("Data is empty")
}
}catch {
print("Error with Json: \(error)")
}
}else{
print("HTTP Error")
}
self.refreshControl?.endRefreshing()
print("Finished")
}
task.resume()
}
I use the following in my code to create a default session that include the authentication:
static func defaultURLSession(username : String, password:String) -> NSURLSession {
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let userPasswordString = "\(username):\(password)"
let userPasswordData = userPasswordString.dataUsingEncoding(NSUTF8StringEncoding)
let base64EncodedCredential = userPasswordData!.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.EncodingEndLineWithCarriageReturn)
let authString = "Basic \(base64EncodedCredential)"
config.HTTPAdditionalHeaders = ["Authorization" : authString]
return NSURLSession(configuration: config)
}

Resources