How to validate HTTP request in Laravel - ios

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.

Related

How to debug URLSession POST Authentication Login Failure

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!

iOS: How to access and use API response data from REST SwiftUi

firstly I am really new to iOS development and Swift (2 weeks coming here from PHP :))
I am creating a application that has a callout to my very simple api. I am using the below code but an unable to access the 'if let response' part of the code and unable to get the value of 'Comments' from the returned data from api. Wondering if anyone can help on this.
The data come back fine as
//Print out fine
print(str)
will print my response of {"Comments":"success"}
so just wondering how I can properly use this data and check if success etc
Thanks
func loadData() {
guard let url = URL(string: "myapi.php") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
print("hello1")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
print("hello2")
let str = String(decoding: data, as: UTF8.self)
//Print out fine
print(str)
// Cannot enter this if statement
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
print("hello3")
print(response)
}
return
}
}
}.resume()
}
struct TaskEntry: Codable {
public var Comments: String
}
With the description provided by you, the JSON that returns your API is the following:
{
"Comments": "success"
}
Is that right?
If it is, this part of your code
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data)
Marks that you are decoding data fetched from your app as Array. You specify an array of TaskEntry right here [TaskEntry].self for your data that's not true, you need to decode json with try? JSONDecoder().decode(TaskEntry.self, from: data)
If you hope for an array your api must to return something like this
[
{
"Comments": "success"
}
]
For check if it is right with status code I provide a restructuration of your code explained.
First, protocol Codable it's used to decode and encode data with Encoders and Decoders. In that case, you are working with JSONs so you use JSONDecoder to decode data. You only need decode, so with Decodable protocol It's enough. You don't need to mark Comments with public, because you are working with TaskEntry in your project, so by default (no specified) It's internal. Moreover you don't need var, because, you are not making changes of the property.
Also, properties as standard starts with lowercase, so you can use CodingKeys to keep a quality code standard.
You can check it in Apple Developer site
struct TaskEntry: Decodable {
let comments: String
enum CodingKeys: String, CodingKey {
case comments = "Comments"
}
}
Then, you should to think about your Api errors and defined it. I give you an example:
enum NetworkErrors: Error {
case responseIsNotHTTP
case noDataProvided
case unknown
}
Your load function, should to communicate to external class or struct the TaskEntry or the error. You could use a closure with a Result. Please check the link Hacking with Swift. This has two cases with associated values, failure and success.
Further the response of your UrlSessionTask need to be cast to HTTPURLResponse to get the response http status code. I provide an example of that with your code.
func loadData(result: #escaping (Result<TaskEntry, Error>) -> Void) {
guard let url = URL(string: "myapi.php") else {
result(.failure(URLError(.badURL)))
return
}
let request = URLRequest(url: url) // your method is Get, if not you need to set the http method of the request
URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response as? HTTPURLResponse else {
if let error = error {
print(error.localizedDescription)
result(.failure(error))
} else {
result(.failure(NetworkErrors.responseIsNotHTTP))
}
return
}
switch response.statusCode {
case 200...299:
if let data = data {
do {
let taskEntry = try JSONDecoder().decode(TaskEntry.self, from: data)
result(.success(taskEntry))
} catch {
result(.failure(error))
}
} else {
result(.failure(NetworkErrors.noDataProvided))
}
default:
if let error = error {
print(error.localizedDescription)
result(.failure(error))
} else {
result(.failure(NetworkErrors.unknown))
}
}
}.resume()
}
Additionally, you could pass this code to async/await form with a new function in iOS 15 or MacOS 12 (async/await it's a feature of swift 5.5, but the urlsession function that i've used it's only from ios 15 and macos 12 to up):
#available(macOS 12.0, iOS 15.0, *)
func loadData() async throws -> Result<TaskEntry,Error> {
guard let url = URL(string: "myapi.php") else {
throw URLError(.badURL)
}
do {
let (data, urlResponse) = try await URLSession.shared.data(from: url, delegate: nil)
guard let response = urlResponse as? HTTPURLResponse else {
return .failure(NetworkErrors.responseIsNotHTTP)
}
switch response.statusCode {
case 200...299:
do {
let taskEntry = try JSONDecoder().decode(TaskEntry.self, from: data)
return .success(taskEntry)
} catch {
return .failure(error)
}
default:
return .failure(NetworkErrors.unknown)
}
} catch {
return .failure(error)
}
}
Please, vote up if it's clarify you.
Have a good day :)

How can I connect to a hard coded IP address using http in iOS 12+

I am trying to make a simple prototype with both a server (EC2 dev server) and a client. Where the server just has a hard IPv4 address (numbers) and send http results upon requests and the client is a prototype iOS 12+ app where I simply want to retrieve data from the server that is using http. How can I do so? Thanks in advance!
Read up on URLSession and how to fetch webside data into memory.
func startLoad() {
let url = URL(string: "http://IPADDRESS/")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
self.handleClientError(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
self.handleServerError(response)
return
}
// Data is in the data-variable.
if let mimeType = httpResponse.mimeType, mimeType == "text/html",
let data = data,
let string = String(data: data, encoding: .utf8) {
// Avoid using the main-thread to return any data.
DispatchQueue.main.async {
// Return data to use in view/viewcontroller
self.webView.loadHTMLString(string, baseURL: url)
}
}
}
task.resume()
}
Other things you probably will want to get into is MVVM/MVC for managing data between views and Codables.

How to pass bearer token to make Yelp API call with URLSessoin

U P D A T E D... The function with what works!
I would like to incorporate the yelp api into an app but can't successfully pass my authorization token on the URL string. Do I need to do something to connect the URLRequest to the URLSessoin call and its not using the header? Maybe the key value pairs is wrong? The below function returns:
error = {
code = "TOKEN_MISSING";
description = "An access token must be supplied in order to use this endpoint.";
};
I was able to use postman to get the yelp API call working, but only by clicking the "Header" section on postman and putting in Bearer and then my yelp key. I googled around a bit and found some links that indicate that you can add a header to the URLSession that I assume would work the way postman does but I haven't been able to get it to work.
I know there are some githubs with yelp API repos but I am trying to not install a large set of code that I don't understand into my app, when all I want is the JSON that I can see is coming through on postman. Can anyone help me understand how I would edit code similar to the Here example below so that I can get the Authorization/Bearer that yelp requires?
func getYelp() {
let appSecret = "Bearer <YELP APIKEY>"
let link = "https://api.yelp.com/v3/businesses/search?latitude=37.786882&longitude=-122.399972"
if let url = URL(string: link) {
// Set headers
var request = URLRequest(url: url)
request.setValue("Accept-Language", forHTTPHeaderField: "en-us")
request.setValue(appSecret, forHTTPHeaderField: "Authorization")
print("Attempting to get places around location from Yelp")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
} else {
if let urlContent = data {
do {
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject // Added "as anyObject" to fix syntax error in Xcode 8 Beta 6
print("Printing all JSON/n/n//n--------------------------")
print(jsonResult)
print("Printing from results/n/n//n--------------------------")
if let description = ((jsonResult["search"] as? NSDictionary)?["context"] as? NSDictionary)?["href"] as? String {
} else {
print("JSON pull failed/n/n//n--------------------------")
}
} catch {
print("JSON Processing Failed/n/n//n--------------------------")
}
}
}
}
task.resume()
} else {
resultLabel.text = "Couldn't get results from Here"
}
}
You're mixing up between the headers and the url, you need to set your headers correctly
if let url = URL(string: "https://places.cit.api.here.com/places/v1/discover/around?at=37.776169%2C-122.421267&app_id=\(app_id)&app_code=\(app_code)") {
var request = URLRequest(url: url)
// Set headers
request.setValue("Accept-Language", forHTTPHeaderField: "en-us")
request.setValue("Authorization", forHTTPHeaderField: "Bearer " + token // Token here)
print("Attempting to get places around location")
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
// ...
Lets say you have an api with "https://google.com" (this is just an example with fake keys)
and an api key that is "ApiKey: 92927839238293d92d98d98d92".
You would then take this information and do this.
let uri = URL(string:"https://google.com")
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL)request.addValue("92927839238293d92d98d98d92", forHTTPHeaderField: "ApiKey")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
// you should put in error handling code, too
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
// HERE'S WHERE YOUR DATA IS
print(json)
} catch {
print(error.localizedDescription)
}
}
}
dataTask.resume()
}
Please remember that you would replace the google.com with your GET address and the APIKey header with your own api key values.
Also, this will print out all the JSON like in PostMan.
If this works for you, then I also have a link on accessing the JSON Objects.

Data in HTTPBody with a PUT method fails, while it works with a POST?

first of all i would like to say i got the exact same problem as the following question: How to add data to HTTPBody with PUT method in NSURLSession?. But it wasn't answered so i made my own question.
We have written a node API for a school assignment. We've tested the whole API. (The chances of being something wrong there are slim.)
After that i went working on a iOS client to CRUD users.
Making a user is going perfectly, but whenever i try to edit a user something strange happens. The data on the server arrives as undefined.
I use the following code to save a user:
func saveUser(user: User, completionHandler: (String?, User?) -> Void) {
let url = NSURL(string: "https://pokeapi9001.herokuapp.com/api/users/")
let request = NSMutableURLRequest(URL:url!)
request.HTTPMethod = "POST"
let postString = "email=\(user.email)&password=\(user.password!)&role=\(user.role)"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringCacheData
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil {
print("error: \(error)")
}
do {
guard let data = data else {
throw JSONError.NoData
}
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? NSDictionary else {
throw JSONError.ConversionFailed
}
//do specific things
} catch let error as JSONError {
completionHandler(error.rawValue, nil)
} catch let error as NSError {
completionHandler(error.debugDescription, nil)
}
}
task.resume()
}
keep in mind, this is working perfectly (don't know if it is intended to be used like this)
To edit a user i use the following code:
func editUser(user: User, completionHandler: (String?, User?) -> Void) {
let url = NSURL(string: "https://pokeapi9001.herokuapp.com/api/users/\(user.id!)")
let request = NSMutableURLRequest(URL:url!)
request.HTTPMethod = "PUT"
let postString = "email=\(user.email)&password=\(user.password!)&role=\(user.role)"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringCacheData
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil {
print("error: \(error)")
}
do {
guard let data = data else {
throw JSONError.NoData
}
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? NSDictionary else {
throw JSONError.ConversionFailed
}
//do specific things
} catch let error as JSONError {
completionHandler(error.rawValue, nil)
} catch let error as NSError {
completionHandler(error.debugDescription, nil)
}
}
task.resume()
}
(The original code is a bit longer but i removed parts that had nothing to do with the actual posting of the data)
I have really no idea what i'm doing wrong, could be something small and stupid. Please help.
edit after input from #fiks
To be clear, the problem i am having is that I fill the "postString" the same way in the editUser method as I do in the saveUser method.(At least I think I do)
However in the saveUser method the postString seems to be correctly passed through to the API (it creates a new user with the given values).
The editUser method does not pass the values through.
If I put a console log on the server it shows all values are "undefined".
To test if the postString was correct on the iOS part I printed both strings out. Both of them outputted email=user#test.com&password=test&role=admin
From what I see in the postman request, you are sending a x-www-form-urlencoded request.
You have to specify it in the code. See example: POST request using application/x-www-form-urlencoded
Regarding Charles: since you are using https, you have to enable proxy for the host. More info here: https://www.charlesproxy.com/documentation/proxying/ssl-proxying/

Resources