Intercepting EVERY response with Alamofire - ios

I'm just exploring using Alamofire and it is excellent but I'd like to do something that I feel is possible just not sure how.
Our authentication with the server uses one-time-use bearer tokens. So for every request made I have to store the new token sent down with that request.
What I'd like to do is intercept every response that comes back and check the Authorisation header. Save it to disk and then forward to the place waiting for the actual data.
Is this possible with Alamofire?
If so, please could you point me in the right direction.
Thanks

OK, after a bit of searching the github and head scratching I decided to create a new response serialiser by extending the Request type.
I created a new saveAuth() block like so...
extension Request {
public static func AuthSaver() -> ResponseSerializer<Bool, NSError> {
return ResponseSerializer { request, response, data, error in
guard error == nil else { return .Failure(error!) }
if let auth = response?.allHeaderFields["Authorization"] as? String {
Router.OAuthToken = auth // this uses a didset on the Router to save to keychain
}
return .Success(true)
}
}
public func saveAuth() -> Self {
return response(responseSerializer: Request.AuthSaver()) {_ in}
}
}
I can call it like...
Alamofire.request(Router.Search(query: query))
.validate()
.responseSaveAuth() // this line
.responseJSON {
response in
// ...
}
It still requires adding in each place that I want to strip out the newly sent auth token but it means I can choose when not to do it also and it's a single line of code.
It's maybe not the most elegant code in the extension (I'm still getting to grips with it all) but it makes it much easier to save the authentication each time.

I have solved this by only having one place in my app that sends network requests. Basically, I have a "network manager" that builds up NSURLRequests and pipes them to one function that actually sends the request (in my case it's an NSOperation sub class). That way I have only one location that I'm reading responses from.

Related

Alamofire Network error exceptions handling

I am currently developing an application for iOS. Most of the features that I wanted implemented I have already finished, but there is one feature in particular that I really need to have - Network Errors handling.
So for example: A user is trying to refresh his data inside my application. Instead of my app crashing or simply not doing anything, I would love for that exception to be caught, identified and then display a corresponding message on screen using AlertDialogs. for example:
Network Error - title;
Unreachable host, please check your network connectivity and try again - Message;
OK - button;
I was able to have this working in my Android application and it's quite useful, however, I am quite new to Swift and iOS development, so, please help me out here and point me in the right direction.
I am currently using latest Alamofire for sending HTTP Requests, here is my example of HTTP Request that I have implemented inside my application.
func loadProfile() {
let url = Constants.profileURL
let headers: HTTPHeaders = ["Cookie": "username; password"]
AF.request(url, method: .post, headers: headers).response {response in
if let data = response.data, let dataString = String(data: data, encoding: .utf8) {
if dataString.contains(Constants.loginSuccess) {
//Do something
}
}
}
}
Alamofire's DataResponse (and all response types) contain the Result of serialization. You can use that value to check whether serialization, and the request in general, succeeded or failed. You can then pass the result value to completion handlers to be dealt with as you wish.

Swift Siesta Framework: do something before sending requests

I am trying the Siesta framework and I want to call a function before sending every API calls.
I saw that decorateRequests(with:) is best suited for what I am looking to do, but as the return value must be a Request, there's an error on the following code:
service.decorateRequests(with: { (res, req) -> Request in
if (res.url == self.tests.url) {
// do things..., then call req.repeated()
} else {
req.repeated()
}
})
However, I have this error:
Missing return in a closure expected to return 'Request'
Any idea how I can make it work? Thanks
The basic Swift syntax error here is that you need to use the return keyword if a value-returning closure contains more than one statement.
If what you need to do is something that either:
can synchronously block the main thread because it is brief (e.g. log the request, flip a test expectation), or
needs to be started when the request is sent, but the request can go ahead and start immediately without waiting for it to finish
…then it needn’t be complicated. Do your brief task while everyone waits, then return the request:
service.decorateRequests { req, res in
if res.url == self.tests.url {
doThatOtherThing() // blocks the main thread, which is fine if it’s brief
}
return req
}
If on the other hand you need to do something that will take an indefinite amount of time while the main thread continues, and then at a later time initiate that request, then Siesta currently doesn’t support that very well. You can do it by writing a custom implementation of the Request protocol, but that's laborious and error prone. A better approach is coming in a future version of Siesta.

How to reduce boilerplate in responseJSON just like I use URLRequestConvertible to group related web calls

I use URLRequestConvertible to groups my web calls, and to reduce the boilerplate code. But in each responseJSON I still have boilerplate to process my JSON response. They all look like these,
Check response.result.isSuccess
Check response.result.value as? the type data I expect (mostly a dictionary)
Check for the success indicator in dictionary
If succeed then retrieve the data I need.
And because I group the related calls in one URLRequestConvertible, their responses have the similar format that I actually have the 5th step to further retrieve the "real" data I am looking for.
So is there any way to reduce these boilerplate codes in responseJSON?
BTW, I actually come up with a kludge solution for it. But I was wondering is there any common practice for that?
I raised the same question at alamofire forum #2099 and got the answer to use ResponseSerializer
But after check the ResponseSerializer document I realize my home-made solution was not as crappy as I thought (using ResponseSerializer seems rather complicated)
So my solution is to add a static verify method to my Router and let it do the basic verification work (from Step #1 to Step #5)
static func verify(json:DataResponse<Any>, request:Router) -> result //needs the 2nd
parameter b/c is a static method
Now my calling method changed to these,
var result = CallResult.fail
Alamofire.request(Router.Callback(input))
.responseJSON { response in
result = Router.verify(json:response,request:Router.Callback(input))
}
.responseJSON { _ in //AS I already parsed response into my result
//process the result now
}

What is the better way to encrypt NSURLCache?

I want to encrypt/decrypt all cached data from a NSURLSession using AES256. I'm new using Alamofire but I think it is possible to do it without involving the library itself.
I don't know exactly what is the most seamless way to encrypt the data before caching and decrypt it after being retrieved from cache.
I see I can use Alamofire's SessionDelegate and the methods dataTaskWillCacheResponse and dataTaskWillCacheResponseWithCompletion to encrypt but I don't see anything related with the data being extracted from the cache to do the decrypting.
On the other hand I was thinking about a custom NSURLProtocol to override cachedResponse but I don't see anything related with the caching of that response, only with the extracted data.
In summary, I don't know if it is possible to accomplish this, or I have to use a mix between the NSURLSessionDelegate/SessionDelegate and NSURLProtocol, or maybe subclass NSURLCache to do the job and pass it to the Alamofire session, or there is something simpler out there, or I'm terribly wrong :P
Any help will be really appreciated.
EDIT
I'm trying to achieve it with the next implementation. First of all a very simple subclass of the cache:
class EncryptedURLCache: URLCache {
let encryptionKey: String
init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String? = nil, encryptionKey: String) {
guard !encryptionKey.isEmpty else {
fatalError("No encryption key provided")
}
self.encryptionKey = encryptionKey
super.init(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: path)
}
override func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
return super.cachedResponse(for: request)?.cloneDecryptingData(withKey: encryptionKey)
}
override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
super.storeCachedResponse(cachedResponse.cloneEncryptingData(withKey: encryptionKey), for: request)
}
}
And an extension of the cached response to return the encrypted/decrypted data
extension CachedURLResponse {
func cloneEncryptingData(withKey key: String) -> CachedURLResponse {
return clone(withData: data.aes256Encrypted(withKey: key))
}
func cloneDecryptingData(withKey key: String) -> CachedURLResponse {
return clone(withData: data.aes256Decrypted(withKey: key) ?? data)
}
private func clone(withData data: Data) -> CachedURLResponse {
return CachedURLResponse(
response: response,
data: data,
userInfo: userInfo,
storagePolicy: storagePolicy
)
}
}
This is working but only for a mockable.io that I mounted with the header Cache-Control: max-age=60. I'm also testing against the SWAPI http://swapi.co/api/people/1/ and against Google Books https://www.googleapis.com/books/v1/volumes?q=swift+programming.
In all three cases the responses are correctly encrypted and cached. I'm doing my testing cutting off the Internet connection and setting the session configuration's requestCachePolicy = .returnCacheDataDontLoad.
In this scenario, the request made to mockable.io is correctly decrypted and returned from cache but the others say NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline.". This is VERY strange because, with that policy, it has to say NSURLErrorDomain Code=-1008 "resource unavailable" if there is no possibility to return the cached data. If there is an error decrypting then it says it was an error serializing to a JSON object.
I've also tested with the common shared cache and it works as expected, with that policy the data is returned. I thought it could be something related with the absence of cache headers in the SWAPI and GBooks responses but this test works, it returns the cached data.
Then I made another test: using my cache but without encrypting/decrypting data, simply cloning the returned cached response with the data as is, with no results. Then I tried a final and very stupid test: to avoid cloning the response, just return the cachedResponse and then IT WORKED. How the h*** is that possible? If I clone the cachedResponse to inject my encrypted/decrypted data it does not work! Even in examples from Apple they are creating new cached responses with no fear.
I don't know where is the error but I'm going to jump over the window in a minute or two.
Please, any help? Thank you so much.
EDIT 2
I was changing emails with a DTS engineer from Apple and the conclusion is that this is not possible to achieve this because the backing CF type is doing more logic than the Foundation object, in this case it is doing a validation against the URLRequest that is passed to it when the system caches the response, but I cannot pass it when make the clone with the regular NSCachedURLResponse.
When the system validates against the request, there is none to match with.
There is no way to intercept cache retrieval calls from the delegate side that I'm aware of, and I don't think that a custom protocol will even be asked to handle the request if it comes out of the cache, but I could be wrong. So probably your options are:
Explicitly ask the cache for the data before you make the URL request.
Add code in the code that actually handles the response so that it recognizes that the data is encrypted and decrypt it.
For example, you could insert an additional header into the headers as you store it into the cache to indicate that the cached data is encrypted. Then, when you see that magic header value on the way back out, decrypt it.
Write a subclass of NSURLCache and handle the decryption there (and ideally, store the on-disk data in a different file to avoid breaking any requests in your app that use the normal cache).

Able to POST direct messages using Twitter's REST API, but trying to GET returns a 401 error

I am trying to get direct messages working in my app. I'm able to POST DMs just fine, but when I try to GET them from the endpoint https://api.twitter.com/1.1/direct_messages.json it returns a 401 - Unauthorized. I don't really understand how I can be authorized to send DMs but not get ones sent to me.
Here's how I'm authenticating initially:
if Twitter.sharedInstance().sessionStore.session() == nil {
Twitter.sharedInstance().logInWithCompletion { session, error in
if (session != nil) {
// successfully logged in, call loading functions
} else {
print("error: \(error!.localizedDescription)")
}
}
} else {
// already logged in, call loading functions
}
Every time I make a request using the REST API, it begins with
if let userID = Twitter.sharedInstance().sessionStore.session()?.userID {
let client = TWTRAPIClient(userID: userID)
The client is initialised the same way in both the POST and GET requests for DMs, yet the GET request fails.
As far as permissions go, I've checked that it has read/write/DM access according to Twitter, and successful requests return "x-access-level" = "read-write-directmessages";, so I think it's set properly.
I was concerned at one point that I might not be authenticating properly, since Twitter's documentation goes through the 3 step process for O-Auth and all I'm doing is telling the Twitter singleton to just log in... but I rationalised that away by assuming that those steps are all carried out in the logInWithCompletion function. And besides, if I wasn't authenticated properly I surely wouldn't be able to send DMs, right?
Any ideas on how I can fix this? I'm quite new so it may be something nice and simple! I've looked through some other questions, but they all seem to code the requests in full rather than using built-in methods like these - or have I got it all wrong?
Yeah, it was a stupid problem - I left the parameters blank since they are all marked as 'optional' - as in, a dictionary of ["" : ""]. I just set the paramaters in the request to nil, and now it works.

Resources