I am trying to get rest data to iOS app, and I use:
var rest_url = "http://192.168.0.1:8000/rest/users/"
let url: NSURL = NSURL(string: rest_url)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
if(error != nil) {
println(error.localizedDescription)
}
println(data)
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSDictionary!
But I think I can't access my server like this. Does anyone know how I can access my server from the iOS simulator?
Do you have App Transport Security Settings in your Info.plist file?
If no then for debugging purpose you can set them like
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Such settings allow issuing requests to any server.
But don't do so for a release version. It is insecure.
Make sure that your IP of your Mac is 192.168.0.1. So your url could be
var rest_url = "http://YOUR MAC IP:8000/rest/users/"
If you have a server running on the machine where you iOS simulator is running, then you need to choose 'http://127.0.0.1' as the URL.
In your case it will be :
var rest_url = "http://127.0.0.1:8000/rest/users/"
For people finding this thread because they can't connect to localhost due to an invalid certificate: in your URLSessionDelegate you should respond to the URLAuthenticationChallenge with the following delegate method:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
func defaultAction() { completionHandler(.performDefaultHandling, nil) }
// Due to localhost using an invalid certificate, we need to manually accept it and move on
guard challenge.protectionSpace.host.hasPrefix("localhost") else { return defaultAction() }
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else { return defaultAction() }
guard let trust = challenge.protectionSpace.serverTrust else { return defaultAction() }
completionHandler(.useCredential, URLCredential(trust: trust))
}
Maybe you can replace 192.168.0.1 with localhost, when debugging with iOS simulator (that is, real devices should use your server's IP).
I also cannot access my test server using IP address on simulator. But when I am using localhost or 120.0.0.1, the simulator can work well with my test server.
Related
I'm dealing with a strange issue where I'm not able to hit some API's through iOS simulator in XCode.
Version: Xcode 10.3
I've tried using the following:
https://finnhub.io/api/v1/stock/profile2?symbol=GOOGL&token=
https://www.alphavantage.co/query?function=OVERVIEW&symbol=IBM&apikey=demo
I'm able to hit both endpoints through the browser, replacing both with a completely unrelated api: https://cat-fact.herokuapp.com/facts works, I'm able to see the response immediately as expected.
I've added the following to my Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Code:
let apiBaseUrl = "https://www.alphavantage.co/query?function=OVERVIEW&symbol=IBM&apikey=demo"
func getStock(symbol: String) {
if let url = URL(string: apiBaseUrl) {
let task = URLSession.shared.dataTask(with: url, completionHandler: {
data, res, err in
if let error = err {
print(error)
} else {
var result: Any! = nil
do
{
result = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
}
catch{
print("exception: ")
}
print(result)
}
})
task.resume()
}
}
In my console I see the following:
HTTP load failed (error code: -999 [1:89])
PAC result block not invoked
Received XPC error Connection invalid for message type 3 kCFNetworkAgentXPCMessageTypePACQuery
I solved this issue as below :
I went into
System Preferences > Network > Advanced > Proxies
and unticked "Auto Proxy Discovery"
My calls now work.
I want to perform a certificate or public key pining in my iOS app. I am not using Alamofire only the standard requests Apple provide. How I can make this, I read how to do it with alomofire but I am not using it. I want the standard way apple giving us but if it’s possible someone to give me more examples of what is happening and how. What the is the best way, certificate or public key and how to implement it.
You can use pinning on iOS, is not so terrible but you must search on Appel docs:
(and I Opened a TSI with ADC for details...)
basically You have to implement delegate methods:
func urlSession(_: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
print("got Challenge")
let serverTrust = challenge.protectionSpace.serverTrust
let areEqual = CertificateHelper.certIsValid(CERT, serverTrust: serverTrust!)
// Set SSL policies for domain name check
let policies = NSMutableArray()
policies.add(SecPolicyCreateSSL(true, (challenge.protectionSpace.host as CFString?)))
SecTrustSetPolicies(serverTrust!, policies);
// Evaluate server certificate
var result = SecTrustResultType.invalid
SecTrustEvaluate(serverTrust!, &result)
let isServerTrusted: Bool = (
result == SecTrustResultType.unspecified // unspecified means implicitely go on (see help with alt click on SecTrustEvaluate)
|| result == SecTrustResultType.proceed
)
if isServerTrusted && areEqual {
let credential = URLCredential(trust: serverTrust!)
let certificate = credential.certificates // debug.
// ok
completionHandler(.useCredential, credential)
} else {
// fail:
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
How can I embed a custom CA SSL certificate in an application that connects to a local server?
This certificate should work like any CA certificate installed in the system, but embedded in this application instead of requiring the user to install it system-wide, so it won't show a security warning and only works with our local servers through the application
The need for this is to comply with ATS without needing the user to do any further configuration like downloading and installing the CA Certificate manually
I assume you're working with a self-signed certificate, in which case, what you're looking for is SSL pinning.
Depending on whether you're using a network lib or not, this might work differently, but what you want to do is store a public cert, and then handle the authentication challenge manually:
https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge
For example:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let trust = challenge.protectionSpace.serverTrust, SecTrustGetCertificateCount(trust) > 0 else {
// This case will probably get handled by ATS, but still...
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Or, compare the public keys
if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0), let serverCertificateKey = publicKey(for: serverCertificate) {
if pinnedKeys().contains(serverCertificateKey) {
completionHandler(.useCredential, URLCredential(trust: trust))
return
}
}
completionHandler(.cancelAuthenticationChallenge, nil)
}
fileprivate func pinnedKeys() -> [SecKey] {
var publicKeys: [SecKey] = []
if let pinnedCertificateURL = Bundle.main.url(forResource: "infinumco", withExtension: "crt") {
do {
let pinnedCertificateData = try Data(contentsOf: pinnedCertificateURL) as CFData
if let pinnedCertificate = SecCertificateCreateWithData(nil, pinnedCertificateData), let key = publicKey(for: pinnedCertificate) {
publicKeys.append(key)
}
} catch (_) {
// Handle error
}
}
return publicKeys
}
I have a couple of other examples how to handle this scenario here:
https://github.com/Adis/swift-ssl-pin-examples
I'm trying to implement HTTP Basic Auth with NSURLSession, but I run into several issues. Please read the entire question before responding, I doubt this is a duplicate of an other question.
According to the tests I've run, the behavior of NSURLSession is the following :
The first request is always made without the Authorization header.
If the first request fails with a 401 Unauthorized response and a WWW-Authenticate Basic realm=... header, it is automatically retried.
Before retrying the request, the session will attempt to obtain credentials by looking into the NSURLCredentialStorage of the session configuration or by calling the URLSession:task:didReceiveChallenge:completionHandler: delegate method (or both).
If credentials could be obtained the request is retried with the proper Authorization header. If not it is retried without the header (which is weird because in this case, this is exactly the same request).
If the second request succeeds, the task is transparently reported as successful and you're not even notified that the request was attempted twice. If not, the failure of the second request is reported (but not the first).
The problem I have with this behavior is that I am uploading large files to my server through multipart requests, so when the request is attempted twice, the entire POST body is sent twice which is a terrible overhead.
I have tried to manually add the Authorization header to the httpAdditionalHeaders of the session configuration, but it works only if the property is set before the session is created. Attempting to modify session.configuration.httpAdditionalHeaders afterwards doesn't work. Also the documentation clearly says that the Authorization header should not be set manually.
So my question is: If I need to start the session before I obtain the credentials and If I want to be sure that requests are always made with the proper Authorization header the first time, how do I do ?
Here is a code sample that I've used for my tests. You can reproduce all the behaviors I've described above with it.
Note that in order to be able to see the double requests you wil need to either use your own http server and log the requests or connect through a proxy that logs all requests (I've used Charles Proxy for this)
class URLSessionTest: NSObject, URLSessionDelegate
{
static let shared = URLSessionTest()
func start()
{
let requestURL = URL(string: "https://httpbin.org/basic-auth/username/password")!
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
let protectionSpace = URLProtectionSpace(host: "httpbin.org", port: 443, protocol: NSURLProtectionSpaceHTTPS, realm: "Fake Realm", authenticationMethod: NSURLAuthenticationMethodHTTPBasic)
let useHTTPHeader = false
let useCredentials = true
let useCustomCredentialsStorage = false
let useDelegateMethods = true
let sessionConfiguration = URLSessionConfiguration.default
if (useHTTPHeader) {
let authData = "\(credential.user!):\(credential.password!)".data(using: .utf8)!
let authValue = "Basic " + authData.base64EncodedString()
sessionConfiguration.httpAdditionalHeaders = ["Authorization": authValue]
}
if (useCredentials) {
if (useCustomCredentialsStorage) {
let urlCredentialStorage = URLCredentialStorage()
urlCredentialStorage.set(credential, for: protectionSpace)
sessionConfiguration.urlCredentialStorage = urlCredentialStorage
} else {
sessionConfiguration.urlCredentialStorage?.set(credential, for: protectionSpace)
}
}
let delegate = useDelegateMethods ? self : nil
let session = URLSession(configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
self.makeBasicAuthTest(url: requestURL, session: session) {
self.makeBasicAuthTest(url: requestURL, session: session) {
DispatchQueue.main.asyncAfter(deadline: .now() + 61.0) {
self.makeBasicAuthTest(url: requestURL, session: session) {}
}
}
}
}
func makeBasicAuthTest(url: URL, session: URLSession, completion: #escaping () -> Void)
{
let task = session.dataTask(with: url) { (data, response, error) in
if let response = response {
print("response : \(response)")
}
if let data = data {
if let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) {
print("json : \(json)")
} else if data.count > 0, let string = String(data: data, encoding: .utf8) {
print("string : \(string)")
} else {
print("data : \(data)")
}
}
if let error = error {
print("error : \(error)")
}
print()
DispatchQueue.main.async(execute: completion)
}
task.resume()
}
#objc(URLSession:didReceiveChallenge:completionHandler:)
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
print("Session authenticationMethod: \(challenge.protectionSpace.authenticationMethod)")
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) {
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
#objc(URLSession:task:didReceiveChallenge:completionHandler:)
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
print("Task authenticationMethod: \(challenge.protectionSpace.authenticationMethod)")
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) {
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
Note 1: When making multiple requests in a row to the same endpoint, the behavior I've described above concerns only the first request. Subsequent requests are tried with the proper Authorization header the first time. However, if you wait some time (about 1 minute), the session will return to the default behavior (first request tried twice).
Note 2: This is not directly related, but using a custom NSURLCredentialStorage for the urlCredentialStorage of the session configuration doesn't seem to work. Only using the default value (which is the shared NSURLCredentialStorage according to the documentation) works.
Note 3: I've tried using Alamofire, but since it's based on NSURLSession, it behaves in the exact same way.
If possible, the server should respond with an error long before the client finishes sending the body. However, in many high-level server-side languages, this is difficult, and there's no guarantee that the upload will stop even if you do so.
The real problem is that you're performing a large upload using a single POST request. That make authentication problematic, and also prevents any sort of useful continuation of uploads if the connection drops midway through the upload. Chunking the upload basically solves all of your issues:
For your first request, send only the amount that will fit without adding additional Ethernet packets, i.e. compute your typical header size, mod by 1500 bytes, add a few tens of bytes for good measure, subtract from 1500, and hard-code that size for your first chunk. At most, you've wasted a few packets.
For subsequent chunks, crank the size up.
When a request fails, ask the server how much it got, and retry from where the upload left off.
Issue a request to tell the server when you've finished uploading.
Periodically purge partial uploads on the server side with a cron job or whatever.
That said, if you don't have control over the server side, the usual workaround is to sent an authenticated GET request right before your POST request. This minimizes wasted packets while still mostly working as long as the network is reliable.
I am using swift 3 and hitting a web service for the first time. My web service runs over HTTPS and I want to test with encryption in place.
Here's my code so far:
let config = URLSessionConfiguration.default // Session Configuration
let session = URLSession(configuration: config) // Load configuration into Session
let url = URL(string: webService.getLoginUrl())!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error == nil {
do {
if let json =
try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]{
//Implement your logic
print(json)
}
} catch {
print("error in JSONSerialization")
}
} else {
print(error!.localizedDescription)
}
})
task.resume()
When I run this against my test server, which is self-signed, I get:
The certificate for this server is invalid. You might be connecting to a server that is pretending to be “10.0.0.51” which could put your confidential information at risk.
So what I'd like to do is accept all certificates when testing, but not in production.
I've found a couple of sites like:
http://www.byteblocks.com/Post/Use-self-signed-SSL-certificate-in-iOS-application
https://github.com/socketio/socket.io-client-swift/issues/326
But these appear to predate swift 3.
How do I solve this problem?
After much research, I learned about how delegates work with URLSession objects in swift 3. Too many pieces to post a link, but in the end, this was the most helpful: https://gist.github.com/stinger/420107a71a02995c312036eb7919e9f9
So, to fix the problem, I inherited my class from URLSessionDelegate and then added the following function:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//accept all certs when testing, perform default handling otherwise
if webService.isTesting() {
print("Accepting cert as always")
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
else {
print("Using default handling")
completionHandler(.performDefaultHandling, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
The isTesting() call determines if I'm using the test server, and then we accept all certificates if we're in testing mode.