I am trying to mimic an HTTP Form POST in iOS to login to a server.
When I use Safari or Chrome and submit the login form outside of my app, I can login without issue. I am using Safari and Chrome dev tools to record/review the "correct" request and response headers, cookies and body during the GET and POST required to login.
When I run my app in Xcode, I use debug print statements or Instruments to review the headers, cookies and body.
Is there a tool/method that will allow me to compare my app's GET and POST header and body vs. what a web browser does? I want an "apples to apples" comparison that will allow me to determine what I am doing wrong...
My code is below. The POST header returns status code = 419. The post body includes the text "Page Expired", which leads me to believe I am not handling tokens or cookies correctly.
Code overview:
I press a UI button to invoke login(). This does a GET of login page, and saves the hidden _token form input from the response body. Cookies are saved to cookieStorage.
I press a UI button to invoke loginPost(). This submits a form with a bogus email and password. I format headers and body. I expect to get an error indicating email is not registered. POST adds _token to body. This body seems to match Chrome dev tools for urlencode formatting. Status code 419 is returned..
Code
class LoginAPI {
public let avLogin = "https://someDomain.com/login"
// save response, data from last getHTMLPage() GET
fileprivate var lastGetResponse: HTTPURLResponse? = nil
fileprivate var lastGetData: String? = nil
// test POST with saved values
var loginToken = ""
var cookies: [HTTPCookie] = []
// MARK: Login
func login() async -> String {
// GET login page,
let loginGetHTML = await self.getHTMLPage(url: self.avLogin)
let loginToken = self.scrapeLoginToken(html: loginGetHTML)
let cookies = self.getCookiesFromResponse(response: self.lastGetResponse)
if let lastResponse = self.lastGetResponse,
let lastURL = lastResponse.url {
HTTPCookieStorage.shared.setCookies(cookies,
for: lastURL, mainDocumentURL: nil)
}
// allow testing of Login, then POST
self.loginToken = loginToken
self.cookies = cookies
// TO DO: add delay, then call loginPost(), and return Data as String
return ""
}
// MARK: POST Login form
func loginPost(url: String, loginToken: String, cookies: [HTTPCookie]) async {
guard let loginURL = URL(string: url) else {return}
let email = "[email protected]"
let password = "pass123"
var request = URLRequest(url: loginURL)
request.httpMethod = "POST"
request.url = loginURL
// header
request.httpShouldHandleCookies = true
// body
let loginInfo = [
("_token" , loginToken),
("email" , email),
("password", password)
]
let body = urlEncode(loginInfo)
request.httpBody = Data(body.utf8)
let session = URLSession.shared
session.configuration.httpCookieStorage = HTTPCookieStorage.shared
session.configuration.httpCookieAcceptPolicy = .always
session.configuration.httpShouldSetCookies = true
let task = session.dataTask(with: request) { (data, response, error) in
if let error = error {
print ("POST error: \(error)")
}
guard let response = response as? HTTPURLResponse else {
print("invalid POST response")
return
}
print("response")
let statusCode = response.statusCode
let headerFields = response.allHeaderFields
let cookies = headerFields["Set-Cookie"]
// let cookie = response.value(forKey: "Set-Cookie")
print(" status code = \(statusCode)")
print(" cookies = \(cookies.debugDescription)")
print(response)
if let mimeType = response.mimeType,
let data = data,
let page = String(data: data, encoding: .utf8) {
print("mimeType \(mimeType)")
print("page as UTF-8")
print(page)
}
}
task.resume()
}
// MARK: GET
public func getHTMLPage(url urlString: String) async -> String {
var statusCode = 0 // HTTP Response status code
// void prior cached response, data
self.lastGetResponse = nil
self.lastGetData = nil
guard let url = URL(string: urlString) else {
print("Error: Invalid URL: '\(urlString)'")
return ""
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse {
statusCode = httpResponse.statusCode
self.lastGetResponse = httpResponse
print("GET response")
print(response)
} else {
print("Error: couldn't get HTTP Response")
return ""
}
guard statusCode == 200 else {
print("Error: Bad HTTP status code. code=\(statusCode)")
return ""
}
let page = String(decoding: data, as: UTF8.self)
self.lastGetData = page
return page
} catch {
print("Error: catch triggerred")
return ""
}
}
// MARK: Login Helper Functions
private func getCookiesFromResponse(response: HTTPURLResponse?) -> [HTTPCookie] {
guard let response = response,
let responseURL = response.url else {
return []
}
guard let responseHeaderFields = response.allHeaderFields as? [String : String] else {
return []
}
let cookies = HTTPCookie.cookies(
withResponseHeaderFields: responseHeaderFields,
for: responseURL)
return cookies
}
// MARK: Login token
public func scrapeLoginToken(html: String) -> String {
look for name="_token", value="40-char-string"
return <40-char-string
}
// MARK: Login urlEncode
public func urlEncode(_ params: [(String, String)]) -> String {
var paramArray: [String] = []
for param in params {
let (name, value) = param
let valueEnc = urlEncode(value)
paramArray.append("\(name)=\(valueEnc)")
}
let body = paramArray.joined(separator: "&")
return body
}
private func urlEncode(_ string: String) -> String {
let allowedCharacters = CharacterSet.alphanumerics
return string.addingPercentEncoding(
withAllowedCharacters: allowedCharacters) ?? ""
}
}
Any debug help or direction would be appreciated!
CodePudding user response:
I was able to resolve this issue with Proxyman, a Mac app that places itself between your mac and the internet, and saves all activity from apps, like Safari and Chrome, as well as apps being developed in Xcode.
Proxyman allowed me to make a simple "apples to apples" comparison, showing me all internet activity made by a browser, my app, or any other app running on my mac, in a common easy to read format.
I am not a web developer, so I didn't realize how easy it would be to debug URLSession headers, cookies, responses, etc. using a proxy server app.
My real problem: I had a simple typo in the web form I was sending to the server, so the server wasn't receiving all necessary form fields.
Days of debug looking into caches, cookies, headers, url encoding, responses, etc. I just didn't see the typo!