I need to open an external link in Safari to a page but the server I'm hitting with the url requires that I need to verify the IP address in a header. How can I pass header data in with UIApplication.shared.open?
There's an option parameter but I can't find any examples or documentation if this is the correct parameter to use to pass header data like ['header': _ipAddress].
Below is an example of where I'm setting headers for another kind of request that just opens in the app itself but I don't know how I can do this for UIApplication.shared.open.
let newRequest = (self.request as NSURLRequest).mutableCopy() as! NSMutableURLRequest
if let _ip = EDataManager.shared.ipAddressOfTheUser, _ip.length() > 0 {
newRequest.setValue(_ip, forHTTPHeaderField: "ex_header")
}
URLProtocol.setProperty("true", forKey: EWebViewAssetDownloadProtocol.CustomKey, in: newRequest)
let defaultConfigObj = URLSessionConfiguration.default
let defaultSession = URLSession(configuration: defaultConfigObj, delegate: self, delegateQueue: nil)
self.dataTask = defaultSession.dataTask(with: newRequest as URLRequest)
self.dataTask!.resume()
Unfortunately this is not supported for UIApplication.openURL(:options:completion:). Additionally, you can't open a URLRequest out to another application, so there really is no way to pass header fields into an external link to open in Safari.
If you have control of the external API, the best way to handle this is to pass URL query parameters into the url you open. However you have to be careful with that, because URL query parameters are viewable by users, so you can't pass any sensitive data that way.
Related
I am currently expressing JSON data by loading the information from a URL, the URL is from an API that comes in two forms:
test.example.com and example.com
Full links throughout the applications will always end differently after the forward slash:
test.example.com/example1 ... test.example.com/example2
But the beginning will always be one of the 2 forms above
I would like to the ability to easily switch between the two URL by changing it in one place using perhaps an extension that is available for all view controller.
So for example I have:
private func JSON() {
guard let url = URL(string: "https://test.example.com/example"),
let sample = value1.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "example1=\(example)".data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return }
do {
self.JStruct = try JSONDecoder().decode([exampleStruct].self,from:data)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
print(error)
}
}.resume()
}
What would be the best approach for switching between the two urls?
UPDATE:
Is it wrong to just use:
struct URLVar {
static var url = "https://example1.com/example/"
}
and reference URLVar.url when needed?
What I normally do is maintain an api.plist file (a key value file store) and an environment.plist file. The ending part of the APIs will be stored in the api.plist, while the domain parts example.com will be stored in the environment.plist.
Note that this way, I can have multiple environment.plist files pointing to different places (a production server, a staging server, etc...). I can simply switch in the necessary environment.plist using a build script phase.
To read, I use the normal plist reading mechanisms provided by apple and combine the domainUrl (in environment.plist) with the required resource path (in api.plist). The following link will explore reading options for you:
https://learnappmaking.com/plist-property-list-swift-how-to/
You can also look at the following library that adds some beautiful code generation capabilities for .plist files among others:
https://github.com/SwiftGen/SwiftGen
I created an macOS ShareExtension which I want to use to upload pictures.
I'm still testing this so any requests will be sent to https://beeceptor.com.
The share extension works fine and it shows up in Preview, once I run it:
I add some text and hit "Post"
But the image is then not uploaded.
This is my code that initiates the background upload:
let sc_uploadURL = "https://xyz.free.beeceptor.com/api/posts" // https://beeceptor.com/console/xyz
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
let configName = "com.shinobicontrols.ShareAlike.BackgroundSessionConfig"
let sessionConfig = URLSessionConfiguration.background(withIdentifier: configName)
// Extensions aren't allowed their own cache disk space. Need to share with application
sessionConfig.sharedContainerIdentifier = "group.CreateDaily"
let session = URLSession(configuration: sessionConfig)
// Prepare the URL Request
let request = urlRequestWithImage(image: attachedImage, text: contentText)
// Create the task, and kick it off
let task = session.dataTask(with: request! as URLRequest)
task.resume()
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
extensionContext?.completeRequest(returningItems: [AnyObject](), completionHandler: nil)
}
private func urlRequestWithImage(image: NSImage?, text: String) -> NSURLRequest? {
let url = URL(string: sc_uploadURL)!
let request: NSMutableURLRequest? = NSMutableURLRequest(url: url as URL)
request?.addValue("application/json", forHTTPHeaderField: "Content-Type")
request?.addValue("application/json", forHTTPHeaderField: "Accept")
request?.httpMethod = "POST"
let jsonObject = NSMutableDictionary()
jsonObject["text"] = text
if let image = image {
jsonObject["image_details"] = extractDetailsFromImage(image: image)
}
// Create the JSON payload
let jsonData = try! JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions.prettyPrinted)
request?.httpBody = jsonData
return request
}
Please note that the sharedContainerIdentifier is present in the entitlements of the app as well as in the sharing extensions entitlements.
The ShareExtensions is in the respective App Group and has outgoing connections enabled.
Performing a background upload
Once the user has completed their entry, and clicks the Post button, then the extension should upload the content to some web service somewhere. For the purposes of this example, the URL of the endpoint is contained within a property on the view controller:
let sc_uploadURL = "http://requestb.in/oha28noh"
This is a URL for the Request Bin service, which gives you a temporary URL to allow you to test network operations. The above URL (and the one in the sample code) won’t work for you, but if you visit requestb.in then you can get hold of your own URL for testing.
As mentioned previously, it’s important that extensions put very little strain on the limited system resources. Therefore, at the point the Post button is tapped, there is no time to perform a synchronous, foreground network operation. Luckily, NSURLSession provides a simple API for creating background network operations, and that’s what you’ll need here.
The method which gets called when the user taps post is didSelectPost(), and in its simplest form it must look like this:
override func didSelectPost() {
// Perform upload
...
// Inform the host that we're done, so it un-blocks its UI.
extensionContext?.completeRequestReturningItems(nil, completionHandler: nil)
}
Setting up an NSURLSession is pretty standard:
let configName = "com.shinobicontrols.ShareAlike.BackgroundSessionConfig"
let sessionConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configName)
// Extensions aren't allowed their own cache disk space. Need to share with application
sessionConfig.sharedContainerIdentifier = "group.ShareAlike"
let session = NSURLSession(configuration: sessionConfig)
The important part to note of the above code segment is the line which sets the sharedContainerIdentifier on the session configuration. This specifies the name of the container that NSURLSession can use as a cache (since extensions don’t have their own writable disc access). This container needs to be set up as part of the host application (i.e. ShareAlike in this demo), and can be done through Xcode:
Go to the capabilities tab of the app’s target
Enable App Groups
Create a new app group, entitled something appropriate. It must
start with group.. In the demo the group is called group.ShareAlike
Let Xcode go through the process of creating this group for you.
Then you need to go to the extension’s target, and follow the same process. Note that you won’t need to create a new app group, but instead select the one that you created for your host application.
These app groups are registered against your developer ID, and the signing process ensures that only your apps are able to access these shared containers.
Xcode will have created an entitlements file for each of your projects, and this will contain the name of the shared container it has access to.
Now that you’ve got your session set up correctly, you need to create a URL request to perform:
// Prepare the URL Request
let request = urlRequestWithImage(attachedImage, text: contentText)
This calls a method which constructs a URL request which uses HTTP POST to send some JSON, which includes the string content, and some metadata properties about the image:
func urlRequestWithImage(image: UIImage?, text: String) -> NSURLRequest? {
let url = NSURL.URLWithString(sc_uploadURL)
let request = NSMutableURLRequest(URL: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.HTTPMethod = "POST"
var jsonObject = NSMutableDictionary()
jsonObject["text"] = text
if let image = image {
jsonObject["image_details"] = extractDetailsFromImage(image)
}
// Create the JSON payload
var jsonError: NSError?
let jsonData = NSJSONSerialization.dataWithJSONObject(jsonObject, options: nil, error: &jsonError)
if jsonData {
request.HTTPBody = jsonData
} else {
if let error = jsonError {
println("JSON Error: \(error.localizedDescription)")
}
}
return request
}
This method doesn’t actually create a request which uploads the image, although it could be adapted to do so. Instead, it extracts some details about the image using the following method:
func extractDetailsFromImage(image: UIImage) -> NSDictionary {
var resultDict = [String : AnyObject]()
resultDict["height"] = image.size.height
resultDict["width"] = image.size.width
resultDict["orientation"] = image.imageOrientation.toRaw()
resultDict["scale"] = image.scale
resultDict["description"] = image.description
return resultDict
}
Finally, you can ask the session to create a task associated with the request you’ve built, and then call resume() on it to kick it off in the background:
// Create the task, and kick it off
let task = session.dataTaskWithRequest(request!)
task.resume()
If you run through this process now, with your own requestb.in URL in place, then you can expect to see results like this:
An App Group identifier must start with "group." and must match everywhere it is used - in the entitlements files, in your code, and on the Apple Dev portal.
In your app and share extension entitlement definitions, you have $(TeamIdentifierPrefix).group.CreateDaily. This is not valid, since it does not begin with "group.".
In your code, you just have "group.CreateDaily". This would be fine if it matched what was in your entitlement files, though Apple recommends using reverse domain name notation to avoid conflicts.
My recommendation would be to go to the Apple Dev portal under Certificates, Identifiers & Profiles/ Identifiers/ AppGroups and define your app groups. Apple will not let you enter something that does not begin with "group.". Once that has been setup, make sure that what you have in your entitlement files and code (config.sharedContainerIdentifier) match and then everything should work.
When I use contentsof:url it truncates the url before retrieving the content, resulting in different html content than the displayed in the WKWebView.
For example
contents = try String(contentsOf: https://www.amazon.com/gp/aw/d/B00BECJ4R8/ref=mp_s_a_1_1?ie=UTF8&qid=1531620716&sr=8-1-spons&pi=AC_SX236_SY340_QL65&keywords=cole+haan&psc=1)
returns the contents of this page: https://www.amazon.com/gp/aw/d/B00BECJ4R8/
Why is this happening? Is there an alternative method that allow you to read the content of the actual URL not the truncated URL?
Any advice if very much appreciated.
Thank you.
You shouldn't be using String(contentsOf:) to load a website. You should use the URL Loading System for this work then passing that object back to your webView.load(_:) method in viewDidLoad()
let urlString = "https://www.amazon.com/dp/B00BECJ4R8/?tag=stackoverflow17-20"
// URL construct may fail in case of the String not being a properly formatted URL, so unwrap it.
if let url = URL(string: urlString) {
// Create a URLRequest instance with the new url.
let request = URLRequest(url: url)
// Load the request.
webView.load(request)
}
In my app I'm trying to login user through UIWebView. On successful login cookies are set in NSHTTPCookieStorage. Few of my app pages are open in UIWebView.
When request for particular web page is send, it check whether user is logged in or not based on the cookies.
I checked that cookies are present in NSHTTPCookieStorage, but are not valid cookies on server. That is, it consider user as logged out user.
My code for loading UIWebView is as below:
let url = serverURL + urlString
let urlRequest = NSMutableURLRequest(URL: NSURL(string: url)!)
webPage.loadRequest(urlRequest)
Even I tried with NSURLSession and setting cookies as HTTPHeaderField. Below is my code for that too:
let URLRequest: NSMutableURLRequest = NSMutableURLRequest(URL: NSURL(string: url)!)
let cookies = NSHTTPCookieStorage.sharedHTTPCookieStorage().cookiesForURL(NSURL(string: serverURL)!)
for cookie in cookies!{
URLRequest.setValue(cookie.value, forHTTPHeaderField: cookie.name)
}
let sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithRequest(URLRequest)
task.resume()
It's working properly for few cases. I'm unable to find what can be an issue with the cookies.
Any help will be appreciated.
Thanks in advance
The following code does not do what you intend:
for cookie in cookies!{
URLRequest.setValue(cookie.value, forHTTPHeaderField: cookie.name)
}
When cookies are sent over HTTP, they are sent in a Cookie header of the format:
Cookie: cookie1=value1; cookie2=value2
You are creating multiple headers named the cookie name, so your request looks like:
cookie1: value1
cookie2: value2
The easiest way to create the correct headers is with NSHTTPCookie.requestHeaderFieldsWithCookies(_:[NSHTTPCookie]). Since you've added no other headers, you could simply do:
if let cookies = NSHTTPCookieStorage.sharedHTTPCookieStorage().cookiesForURL(NSURL(string: serverURL)!) {
URLRequest.allHTTPHeaderFields = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies)
}
As an aside, you are using different URLs for your request and for looking up the cookies. They should be the same as cookies can be scoped to specific paths in a domain.
For a simple iOS (swift) application for my university I try to login to one of their pages to retrieve the amount of money currently on my card. However when doing my http request I can't get the data I need.
This is my code:
let url = NSURL(string: "https://campuscard.hhs.nl/portal/j_spring_security_check")
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let data : NSData = ("?j_username=USERNAME&j_password=PASSWORD").dataUsingEncoding(NSUTF8StringEncoding)!;
request.HTTPBody = data;
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {(response, data, error) in
println(NSString(data: data, encoding: NSUTF8StringEncoding))
}
It gives me the error that I have enterred the wrong credentials and when I print my request it says:
<NSMutableURLRequest: 0x7f8d7b53bd30> { URL: https://campuscard.hhs.nl/portal/j_spring_security_check, headers: {
"Content-Type" = "application/x-www-form-urlencoded";
} }
So I think it doesn't include the username and password.
Does anyone have an idea?
It would be much appreciated by me and other students on my university!
added
Me and a friend of my class we see the attributes in the request through Charles thanks to you, however since we both never tried working with this we don't know how to handle those attributes. We simply added all we can find to the request and tried it but we still get the ArrayOutOfBoundsException on the server.
var dataString = "j_username=USERNAME&j_password=PASSWORD"
var request : NSMutableURLRequest = NSMutableURLRequest()
request.URL = NSURL(string: "https://campuscard.hhs.nl/portal/j_spring_security_check")
var postString = (dataString as NSString).dataUsingEncoding(NSUTF8StringEncoding)
request.HTTPMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
//request.setValue("JSESSIONID=C78C688403A836968EC1FEAED9AE9126", forHTTPHeaderField: "Cookie")
request.setValue("campuscard.hhs.nl", forHTTPHeaderField: "Host");
request.setValue("keep-alive", forHTTPHeaderField: "Connection");
request.setValue("41", forHTTPHeaderField: "Content-Length");
request.setValue("max-age=0", forHTTPHeaderField: "Cache-Controle");
request.setValue("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", forHTTPHeaderField: "Accept");
request.setValue("https://campuscard.hhs.nl", forHTTPHeaderField: "Origin");
request.setValue("https://campuscard.hhs.nl/portal/login", forHTTPHeaderField: "Referer");
request.setValue("gzip,deflate", forHTTPHeaderField: "Accept-Encoding");
request.setValue("nl-NL,nl;q=0.8,en-US;q=0.6,en;q=0.4", forHTTPHeaderField: "Accept-Language");
request.HTTPBody = postString
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {(response, data, error) in
println(NSString(data: data, encoding: NSUTF8StringEncoding))
}
I'm sorry to place such a big piece of code on you, but maybe there is something you can see is wrong. Thank you for your time
The body of the x-www-form-urlencoded request should not contain the ?.
As an aside, you should be percent encoding USERNAME and PASSWORD. Right now, if either (more likely, the password) contained certain reserved characters, your request would fail. I use a extension like this in Swift 2:
extension String {
/// Percent escape value to be added to a HTTP request
///
/// This percent-escapes all characters besize the alphanumeric character set and "-", ".", "_", and "*".
/// This will also replace spaces with the "+" character as outlined in the application/x-www-form-urlencoded spec:
///
/// http://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm
///
/// - returns: Return percent escaped string.
func stringByAddingPercentEncodingForFormUrlencoded() -> String? {
let allowedCharacters = NSCharacterSet(charactersInString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._* ")
return stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters)?.stringByReplacingOccurrencesOfString(" ", withString: "+")
}
}
I use this stringByAddingPercentEncodingForFormUrlencoded function on the USERNAME and PASSWORD values (but not the whole string).
Or, in Swift 3:
extension String {
/// Percent escape value to be added to a HTTP request
///
/// This percent-escapes all characters besize the alphanumeric character set and "-", ".", "_", and "*".
/// This will also replace spaces with the "+" character as outlined in the application/x-www-form-urlencoded spec:
///
/// http://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm
///
/// - returns: Return percent escaped string.
func addingPercentEncodingForFormUrlencoded() -> String? {
let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._* ")
return addingPercentEncoding(withAllowedCharacters: allowedCharacters)?.replacingOccurrences(of: " ", with: "+")
}
}
The absence of the username and password when examining NSURLRequest is not at all worrying (I wouldn't have expected it to include the body of the request when you log it like that). If you want to check, run this through Charles or something like that.
If you're using Charles, if you want to inspect HTTPS interaction, you have to enable SSL proxying, and add your domain to the list of location. See "Proxy settings..." on "Proxy" menu, and click on the "SSL" tab. See Charles Web Debugging Proxy.
This will show you the full request in all of its glory. If you're trying to have your app log on like you would from a web browser, you can use Charles to watch the web browser exchange and compare and contrast that to your app.
In your revised question, you are now showing all the various headers that you're trying to set. (You don't have to set some of these: Watch existing app request in Charles and you'll see some of these are already set.) I'd be surprised if any of these are needed.
Ironically, the only one that's probably critical is the one you've commented out, JSESSIONID. lol. Many of these web sites will provide some session ID in the login HTML. Then when you then try to submit the login request, you have to pass the same JSESSIONID that was provided to you by the login HTML page.
So the model is usually (a) get the login page; (b) parse it for whatever header fields that need to be set in subsequent requests (e.g. looks like it might be JSESSIONID, on the basis of your example); and (c) supply that session id for all subsequent requests.
This is supposition, as I haven't been able to actually see the full conversation b/w the web browser and your particular web server, but this is the sort of pattern I've seen before. Just watch web browser requests/responses, paying special attention to cryptic id numbers buried in the HTML that might be provided in subsequent requests (either in the body or the headers).