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 = "fake123@gmail.com"
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!