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!
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!
Related
I'm trying to perform the Firebase App Check validation using REST APIs since it's the only way when developing App Clips as they dont' support sockets. I'm trying to follow Firebase docs. All I'm having trouble with is the decoding of the App Attestation Statement.
So far I've been able to extract the device keyId, make Firebase send me a challenge to be sent to Apple so they can provide me an App Attest Statement using DCAppAttestService.shared.attestKey method.
Swift:
private let dcAppAttestService = DCAppAttestService.shared
private var deviceKeyId = ""
private func generateAppAttestKey() {
// The generateKey method returns an ID associated with the key. The key itself is stored in the Secure Enclave
dcAppAttestService.generateKey(completionHandler: { [self] keyId, error in
guard let keyId = keyId else {
print("key generate failed: \(String(describing: error))")
return
}
deviceKeyId = keyId
print("Key ID: \(deviceKeyId.toBase64())")
})
}
func requestAppAttestChallenge() {
guard let url = URL(string: "\(PROJECT_PREFIX_BETA):generateAppAttestChallenge?key=\(FIREBASE_API_KEY)") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
return
}
guard let data = data, error == nil else {
return
}
let response = try? JSONDecoder().decode(AppAttestChallenge.self, from: data)
if let response = response {
let challenge = response.challenge
print("Response app check challenge: \(challenge)")
print("Response app check keyID: \(deviceKeyId)")
let hash = Data(SHA256.hash(data: Data(base64Encoded: challenge)!))
dcAppAttestService.attestKey(deviceKeyId, clientDataHash: hash, completionHandler: {attestationObj, errorAttest in
let string = String(decoding: attestationObj!, as: UTF8.self)
print("Attestation Object: \(string)")
})
}
}
task.resume()
}
I tried to send the attestation object to Firebase after converting it in a String, although I wasn't able to properly format the String. I see from Apple docs here the format of the attestation, but it isn't really a JSON so I don't know how to handle it. I was trying to send it to Firebase like this:
func exchangeAppAttestAttestation(appAttestation : String, challenge : String) {
guard let url = URL(string: "\(PROJECT_PREFIX_BETA):exchangeAppAttestAttestation?key=\(FIREBASE_API_KEY)") else {
return
}
let requestBody = ExchangeAttestChallenge(attestationStatement: appAttestation.toBase64(), challenge: challenge, keyID: deviceKeyId)
let jsonData = try! JSONEncoder().encode(requestBody)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) {data, response, error in
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
print("Exchange App Attestation \(String(describing: response))")
return
}
guard let data = data, error == nil else {
return
}
print("Exchange App Attestation: \(data)")
}
task.resume()
}
In my iOS App i'm able to download data from a database, but actually all the operations are made in background and the main thread is still active, even the GUI. I also tried to make a 'sleep' with
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { ... }
With this delay everthing works fine, but it's not a good solution. How can i change my code to do this in the main thread? Possibly with loadingIndicator.
This is my code (checking if username exists):
func CheckIfUsernameExists(username : String, passwordFromDb : inout String, errorMsg : inout String)
{
//declare parameter as a dictionary which contains string as key and value combination. considering inputs are valid
var _errorMsg = ""
var _psw = ""
var parameters : [String : Any]?
parameters = ["username": username,
"action": "login"]
print(parameters!)
let session = URLSession.shared
let url = "http://www.thetestiosapp.com/LoginFunctions.php"
let request = NSMutableURLRequest()
request.url = URL(string: url)!
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField:"Accept")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField:"Content-Type")
do{
request.httpBody = try JSONSerialization.data(withJSONObject: parameters!, options: .sortedKeys)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
if let response = response {
let nsHTTPResponse = response as! HTTPURLResponse
let statusCode = nsHTTPResponse.statusCode
print ("status code = \(statusCode)")
}
if let error = error {
print ("\(error)")
}
if let data = data {
do{
_psw = self.parseJSON_CheckIfUsernameExists(data, errorMsg: &_errorMsg)
}
}
})
task.resume()
}catch _ {
print ("Oops something happened buddy")
errorMsg = "Usarname non recuperato (1)"
}
passwordFromDb = _psw
errorMsg = _errorMsg
}
You’re attempting to update passwordFromDb and errorMsg at the end of this method. But this is an asynchronous method and and those local variables _psw and _errorMsg are set inside the closure. Rather than trying to defer the checking of those variables some arbitrary three seconds in the future, move whatever “post request” processing you need inside that closure. E.g.
func CheckIfUsernameExists(username : String, passwordFromDb : inout String, errorMsg : inout String) {
//declare parameter as a dictionary which contains string as key and value combination. considering inputs are valid
let parameters = ...
let session = URLSession.shared
var request = URLRequest()
...
do {
request.httpBody = ...
let task = session.dataTask(with: request) { data, response, error in
if let httpResponse = response as? HTTPURLResponse,
let statusCode = httpResponse.statusCode {
print ("status code = \(statusCode)")
}
guard let data = data else {
print (error ?? "Unknown error")
return
}
let password = self.parseJSON_CheckIfUsernameExists(data, errorMsg: &_errorMsg)
DispatchQueue.main.async {
// USE YOUR PASSWORD AND ERROR MESSAGE HERE, E.G.:
self.passwordFromDb = password
self.errorMsg = _errorMsg
// INITIATE WHATEVER UI UPDATE YOU WANT HERE
}
}
task.resume()
} catch _ {
print ("Oops something happened buddy")
errorMsg = "Usarname non recuperato (1)"
}
}
I have an iOS application that is making an HTTP request to a server/site that uses Laravel, everything is working fine but I would like to validate the HTTP request before returning any information. Right now the way it works is that you type example.com/zip-code/51751and the server responds to the request, what I want, is to make the server respond only to requests coming from the iOS app. What I was thinking is to hard-code a random key/token in both my iOS app and in the Laravel app and then include that key as part of my HTTP request, so that I can check the request to see if the key in the request matches the key in the server (.env file).
Here is the code I currently have that works fine but doesn't do the validation.
Routes File
Route::get('zip-code/{zipCode}', 'MyController#placeDescription');
Controller
class MyController extends Controller
{
public function placeDescription($zipCode)
{
$placeInfo = InfoDB::where('ZipCode', $zipCode)->get();
return ($placeInfo);
}
}
.env File
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:GpyF4XKuxMqCfkylHfdsfFDSjfasdfFDgfsdGfdsFGffhg=
APP_URL=http://localhost
LOG_CHANNEL=stack
DB_CONNECTION=sqlite
...
iOS App / Swift
func descriptionForCurrentLocation(zipCode: String, completion:#escaping(_ placeDescription:String?) -> () ){
let siteLink = "http://example.com/zip-code/" + zipCode
let url = URL(string: siteLink)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
// ERROR
completion(nil)
return
}
guard let data = data else {
// Return, data is empty
completion(nil)
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
guard let jsonArray = json as? [[String: String]] else {
// Return, the object returned by the server is not in the right format.
return
}
if jsonArray.isEmpty{
// Return, array is empty.
return
}else{
let description = jsonArray[0]["CityDescription"]!
completion(description)
}
}
task.resume()
}
How can I validate the HTTP request against a random key?
Edit:
Just for the record here is what I did in my iOS app to add the HTTP header. See Salman Zafars answer for details on how to do the validation in Laravel.
let siteLink = "http://example.com/zip-code/" + zipCode
let url = URL(string: siteLink)
var request = URLRequest(url: url!)
request.setValue("161jshghss", forHTTPHeaderField: "CustomKey")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
...
You can generate a single uuid for validating a request and to validate a request you can use below code
use Illuminate\Http\Request;
class MyController extends Controller
{
public function placeDescription(Request $request, $zipCode)
{
if($request->hasHeader('CustomKey') && $request->header('CustomKey') === "161jshghss")
{
$placeInfo = InfoDB::where('ZipCode', $zipCode)->get();
return ($placeInfo);
}
else
{
return response(['error' => 'Something is missing'], 403);
}
}
}
You have to set Custom Header in your ios app with the HeaderName and Headervalue and then you can use the above code.
Thanks.
I'm trying to make an REST API call to a universal devices hub to turn a switch on. It seems like the call is going through, however I am getting an error that says I need credentials, which makes sense because there are credentials needed to get into the interface. However I am not sure how to make this work.
My code is the following
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBOutlet weak var lightOn: UIButton!
#IBAction func lightOn(_ sender: Any) {
guard let url = URL(string: "http://0.0.0.0/rest/nodes/ZW002_1/cmd/DFON") else { return }
let userCredential = URLCredential(user: "admin",
password: "admin",
persistence: .permanent)
URLCredentialStorage.shared.setDefaultCredential(userCredential, for: protectionSpace)
// create URL session ~ defaulted to GET
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
// optional chaining to make sure value is inside returnables and not not
if let response = response {
print(response)
}
if let data = data {
// assuming the data coming back is Json -> transform bytes into readable json data
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print(json)
} catch {
print("error")
}
}
}.resume() // if this is not called this block of code isnt executed
}
}
I tried piecing together a couple examples online, and the ones I have seen use protectionSpace but when I use it the code returns:
Use of unresolved identifier 'protectionSpace'
Also overall whenever I actually run the simulator I get this exact error:
2017-12-26 13:28:58.656122-0600 hohmtest[6922:1000481] CredStore - performQuery - Error copying matching creds. Error=-25300, query={
atyp = http;
class = inet;
"m_Limit" = "m_LimitAll";
ptcl = http;
"r_Attributes" = 1;
sdmn = "/";
srvr = "192.168.1.73";
sync = syna;
}
<NSHTTPURLResponse: 0x60400042a3e0>
{ URL:
http://192.168.1.73/rest/nodes/ZW002_1/cmd/DON/ } { Status Code: 401,
Headers {
"Cache-Control" = (
"max-age=3600, must-revalidate"
);
Connection = (
"Keep-Alive"
);
"Content-Length" = (
0
);
"Content-Type" = (
"text/html; charset=UTF-8"
);
EXT = (
"UCoS, UPnP/1.0, UDI/1.0"
);
"Last-Modified" = (
"Tue, 26 Dec 2017 11:26:15 GMT"
);
"Www-Authenticate" = (
"Basic realm=\"/\""
);
} }
error
This solution worked for me. This is how I called a REST API that required a username and password. For those wondering, I put this code inside my IBAction button and didn't have to do anything else other than making the button.
let username = "admin"
let password = "admin"
let loginData = String(format: "%#:%#", username, password).data(using: String.Encoding.utf8)!
let base64LoginData = loginData.base64EncodedString()
// create the request
let url = URL(string: "http:/rest/nodes/ZW002_1/cmd/DFON")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Basic \(base64LoginData)", forHTTPHeaderField: "Authorization")
//making the request
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse {
// check status code returned by the http server
print("status code = \(httpStatus.statusCode)")
// process result
}
}
task.resume()
********* EXTRA NOTE *************
If yours does not have a username and password and you are trying to call a REST API call in swift here is some code that can help you! BOTH ARE GET REQUESTS!
#IBAction func onGetTapped(_ sender: Any) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else { return }
// create URL session ~ defaulted to GET
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
// optional chaining to make sure value is inside returnables and not not
if let response = response {
print(response)
}
if let data = data {
// assuming the data coming back is Json -> transform bytes into readable json data
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print(json)
} catch {
print("error")
}
}
}.resume() // if this is not called this block of code isnt executed
}
How to check reachability of particular website?
I am connected to wifi network for internet access, which have blocked some sites. How to check if I have access to those sites or not?
I have checked with Reachability class, but I can not check for particular website.
Currently I am using Reachability.swift
I don't know what is the best practice, but I use HTTP request to do so.
func checkWebsite(completion: #escaping (Bool) -> Void ) {
guard let url = URL(string: "yourURL.com") else { return }
var request = URLRequest(url: url)
request.timeoutInterval = 1.0
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("\(error.localizedDescription)")
completion(false)
}
if let httpResponse = response as? HTTPURLResponse {
print("statusCode: \(httpResponse.statusCode)")
// do your logic here
// if statusCode == 200 ...
completion(true)
}
}
task.resume()
}
func pageExists(at url: URL) async -> Bool {
var headRequest = URLRequest(url: url)
headRequest.httpMethod = "HEAD"
headRequest.timeoutInterval = 3
let headRequestResult = try? await URLSession.shared.data(for: headRequest)
guard let httpURLResponse = headRequestResult?.1 as? HTTPURLResponse
else { return false }
return (200...299).contains(httpURLResponse.statusCode)
}
The initializer you want to use is listed on that page.
You pass the hostname as a parameter:
init?(hostname: String)
// example
Reachability(hostname: "www.mydomain.com")