While testing images being loaded from an email in my iOS Outlook app, I see inconsistencies with how the app caches them. These images are loading from external sources (i.e. HTML img tag with src pointing to a server, not images that are embedded/attached). The app makes several requests to the same image source as you navigate around. Some of these requests contain the HTTP_IF_NONE_MATCH header but there is a lot of inconsistency. I don't think it's caching them correctly. I've included more details below. it looks like it's causing my image to be loaded several times when the person is browsing the email/their inbox.
The reason this is a problem for me is that I'm building an email tracking tool that relies on a pixel tracker inside the email. I'm trying to determine how many times my email is opened by its various recipients. So this inconsistency is causing misfires for the tool unfortunately. While I realize that could be considered consequential limitation of relying on the app, I'm wondering if the lack of image caching is a larger issue that could be addressed. It is effectively firing requests to load the image repeatedly so that is eating up mobile data unnecessarily.
Is there a way to improve its caching somehow? I'm also looking for any other ways to determine requests coming from the same phone. Here's what I see in my server log when dealing with an email that contains my pixel tracker.
Push notification for email received on iPhone
Click the app to load the inbox -> Tracker request fired for 1st time (i.e. image loaded for first time)
Open email -> Tracker request fired for 2nd time (without any HTTP_IF_NONE_MATCH header)
Go back to inbox -> Tracker request fired for 3rd time but this time it includes HTTP_IF_NONE_MATCH header. Note previous requests did not include this header even though it's the same URL so some inconsistency indicating it did not cache it
Open email for the second time -> Tracker request fired for 4th time but it did not include the HTTP_IF_NONE_MATCH header (inconsistency with 3rd time, which had header)
Go back to inbox -> No request fired
Open email for third time -> Tracker request fired for 5th time and it included HTTP_IF_NONE_MATCH header (so inconsistency between this and 4th request, which did not include it)
From here on, repeating any of the above steps would trigger requests but would always include the HTTP_IF_NONE_MATCH header successfully. I also tested changing folders, refreshing the inbox, replying to emails etc. While this triggered tracker requests as well, they also included the appropriate header.
However, I noticed that closing the app (by swiping it off) and then opening it again effectively caused the behaviour to reset. The 1st, 2nd and 4th requests would still not include the header while the others would. Note the same happens on data vs wifi.
Generally speaking, it looks like the iOS app is repeatedly loading the images that it should have already cached. Outlook desktop clients work great because it only fires 1 request to load the pixel. For the Outlook Android app, it seems to do a better job caching the image. It makes 2 or 3 requests the first couple of times you open the email but the 2nd/3rd requests always include the header. After that, it stops loading the image altogether so it must be caching it on the phone somehow. Since the Android app functions fairly differently, this is what lead me to believe it may be an issue that could be resolved in the app itself. However, as I said above, I'm looking for alternative ways to determine how to track the user.
Another way I normally rely on is to set a cookie to indicate an open just happened by that device. I then simply skip these false positives to avoid the repeated requests. However, because of the nature of iOS apps needing to whitelist third-party cookies of web requests within the code, each subsequent request is not sending my cookie along either. Here's a related question as well. I confirmed the Safari settings do not block 3rd party cookies in case it conflicts.
Alternatively, if there's another way to tell a unique device, I could figure out how to skip repeated opens for that phone on the server. I checked the rest of the request headers. The only thing that's somewhat identifiable is a combination of user-agent and IP address but that seems very unreliable to me. Two people in the same office with the same corporate phone could be on the same wifi so I think that would fire false negatives instead.
Is there another way to determine the device is opening it multiple times? Ideally if it sent a unique identifier or something, that would be very useful.
Is there a way to force the app to cache the image (this would heavily reduce the misfires) via say some response header?
Related
I am trying to be able to send SMS messages with links that contain OpenGraph preview images which will load in the iOS "Messages" application and will display the thumbnail without the user having to press [Tap To Load Preview] first... How can this be achieved?
For this, I am sending a text SMS message to an iPhone X which is running iOS 10 and opened with the Messages app.
The text message body contains a URL that points to a resource (a HTML web page) whose body contains OpenGraph metadata with an og:image tag. Eg:
<meta property="og:image" content="https://www.apple.com/v/iphone/home/t/images/home/og.png?201610171354" />
For presentation purposes, we are trying to make it so that the image will load first and immediately, without the user having to tap the button in order to to see it...
Expected Behavior:
Actual Behavior:
As a side note, on the Android clients we have tested, where OpenGraph is supported the image will display instantly without the user being prompted to do anything. The same is true for any other OpenGraph supported application tested, including Facebook.
For reference, here are some of the methods I've tested to try to get this working for us (as well as combinations therein):
Tried serving the image directly with no intermediate redirects, also tried with redirects.
Tried serving PNG and JPG images.
Tried serving the images from URL's containing no more than 20 characters where the URL has the ".jpg" and ".png" parameters and no additional GET parameters. Also tried when the extensions aren't part of the link.
Tried serving the image from the server by referencing its IP directly instead of using a public domain name.
Tried with GET parameters as well, with random numbers to clarify a totally unique URL each time.
Tried serving the image from HTTPS and HTTP links.
Tried serving with dynamically generated images, which should entail a brief delay of some milliseconds while the image is rendered and served.
Tried an enforced sleep in the script that responds to the URL page as well as for the image request to induce an intentional delay of some milliseconds and experimented with various settings for that.
Tried serving the image with a variety of different dimensions, portrait and landscape as well as extremely large and extremely small and other variants between (50x50, 60x50, etc and up).
Always ensured that the image is <1 MB in size, but also tested larger images anyway to see if they would work.
Tried serving images from the same canonical source that the phone or Message service might already 'recognise' as 'trusted' as we have already loaded the preview from those those in the past (testing if such a feature exists, which it probably doesn't).
Tried specifically, all suggestions as noted Apple Technical Note "Best Practices for Link Previews in Messages" see https://developer.apple.com/library/content/technotes/tn2444/_index.html
Tried moving the OG tags outside of the <head> of the page.
Tried stripping of the page of all tags except for pertinent og:image tags.
Tried removing HTTP headers for the response to the GET to the image resource so that just the image itself is returned. Tried then adding back the Content-Type header alone.
Tried priming the request to the image to respond with various HTTP specification cache-invalidating related headers..
Tried sending from various phone numbers US and Australian, also tried changing the "From" field for the SMS message itself to strings like "VERIZON", "Verizon", "Telstra", "APPLE", "Apple", "Facebook", "Uber", "China".
Tried sending the messages from handheld phone as well as from the Twilio Messaging API service.
None of the above work for an iPhone X.
There is no way to do this with SMS. If you send it as an iMessage it will automatically display the preview.
On iPhones the preview is generated on the device rather than server-side. When a user taps preview their device sends 4 GET requests to the server. If the preview was generated automatically there would be a security vulnerability. You could send a text to any iPhone user and get their IP Address.
With iMessages the preview is generated automatically but there is no security vulnerability. The device sending the link sends 4 GET requests to the server, generates the preview and then transmits the preview to the recipient via iMessages. As a result, the receiving device has no need to send a request to the server to generate a preview. And their IP Address is not known to the owner of a webserver.
On Google's Android Messaging app the preview is generated server side. There is one GET request sent to the web server. But it originates from the Android Messages server. So again the owner of the webserver does not receive the recipient's IP Address from their receipt of the text.
I tested Universal Links in iOS by turning on Airplane mode and saw that the correct application was opened (instead of a website)
This indicates some level of "caching" the apple-app-site-association.
I want to determine the extent to which this is cached, so I can determine
What UX edge cases are there (e.g. Offline for x days)
What security considerations are there (e.g. MITM / SSLStrip + .well-known/URL)
etc.
Ideally I would like to have details if additional logic is employed (conditional caching if HTTPS employed, DNSSec, etc)
The exact behavior here is (intentionally?) unclear from Apple. Here is my personal experience, gleaned partly from official documentation and partly from helping thousands of apps implement Universal Links at Branch.io.
The apple-app-site-association file is cached once when the app is first installed.
If this initial scrape fails, in almost all situations it will not be reattempted. The only exception to this is if the initial return is a 5xx error, in which case a limited number of retries may occur. This is not well-documented, and is not covered in Universal Links documentation at all. You can find a mention in the Shared Web Credentials docs.
The file is not checked at all when a Universal Link is opened. This is why you are able to get Universal Links behavior in airplane mode.
The file does not expire. Once it is cached, it sticks permanently for as long as the app is installed.
The file will be re-checked when installing an app update.
The file must be accessible via a valid SSL connection at either https://example.com/apple-app-site-association or https://example.com/.well-known/apple-app-site-association. If there are redirects of any kind, this will fail.
It is theoretically possible to MITM the request if you are able to install a new SSL certificate directly on the device in question. Charles Proxy for example uses this approach for debugging. I have never seen or heard of this being exploited, and the damage would be quite limited because the domain still has to be specified inside the app itself.
I found a way to get around the caching issue. The cache is bound to the domain name, so for every time you want iOS to request apple-app-site-association you can create a new subdomain, and configure iOS to use that subdomain as the universal link for your app.
Extremely hacky, but it is the only workaround that worked for me.
I have a HTML page with a Download app button. When clicked by an iOS user, it redirect the users to the App Store via itms-services. The link I used here is:
window.location.replace("itms-apps://itunes.apple.com/app/id578505616");
It looks like so on a mobile browser:
Facebook/Pinterest has an in-app browser which asks for the user's consent before redirect the user out of their app in the form of an Alert box with Cancel or Open app, which Open app opens the external link. It looks like this:
It looks like this with the Alert box:
Assuming the user is using an iOS device. The expected behavior here is when the Open app button is clicked, the user gets redirected to the App Store app. However, we are seeing about a 10 seconds delay before the actual redirection happens.
See videos below:
https://www.youtube.com/watch?v=_dgh7v-IsK8
https://www.youtube.com/watch?v=_LFHeRnBFgg
https://www.youtube.com/watch?v=82Y-kj-pMyw
Notes:
I can't seem to reproduce this at all times. It is only reproducible sometimes. Sorry!
Once a device is delayed for the first time, all subsequent links to AppStores will be fast, until roughly 1 day late. Rebooting the phone and/or clearing the browser cache and/or reinstall Facebook/Pinterest does not help reproducing this.
I tried to replace the itms-apps:// link with https://itunes.apple.com/us/app/trusper-tips-tutorials-how/id578505616?mt=8, but we were still experiencing the delay from time to time.
The delay seems to be exactly 10 seconds.
Why does this delay occur? This seems like a bad user experience.
My current theory is that the problem is on the Apple side. The reason being, once the Open app button is clicked, it is out of the hand of our website, as well as Facebook/Pinterest app. I presume, once the button is clicked, iOS is handling the actual context switch from one app to another. Therefore, it suggests that either the system call actually opening the URL is hanging, or the system call opening the App Store app is?? However, I haven't been able to prove either one to be true or false. Does anyone have a better explanation?
[Edit 04/02/2015]
I can confirm this problem still exists as of today on my phone (iPhone 5S w/ iOS 8.2).
I did some further research and found this article which I think is suggesting that this is a iOS system-related bug: OpenUrl freezes app for over 10 seconds
As there is no proper documentation on how it works, it is hard to pinpoint the cause of it.
Let me advance a theory.
Opening an app from browser causes a context switch; indeed from browser to App Store. So for the very first time it'd cost some time, while in subsequent times it will open faster as it will get opened from cache.
If you consider AppStore uses Keep-alive connection between iOS AppStore in device and iOS AppStore Server for the first time socket opening in server would seem higher than the subsequent times as subsequent request would reuse the same connection.
If the app is removed from the cache then it'd again take some time for the app to open and there would be a little network delay as the app would have lost the cookie for keep-alive connection then the app would have to endure the socketing opening cost.
There are also other factors like Safari hanging, resulting in some delay. You can read how Safari can be messed up here.
iOS doesn't have garbage collection, only things like ARC (Automatic Reference Counting). Each has its own advantages and drawbacks. Sometimes these drawbacks can surface and cause some problem; nothing is to be ruled out. For example a memory leak or app-crash might leave dangling pointers.
Technically, using itms-apps should give a little advantage over plain iTunes url because itms-apps should narrow down the search; itms-apps mean iTunes Music Store-apps.
A plain iTunes URL may be a Book (iBook), Music (iTunes), App (Appstore), etc ... so iOS might have to decide which app to launch.
I hope this helps.
I have been serving some mp3s to a small community for years just by linking straight to the MP3 and letting the device's browser handle the playback. All worked fine until users updated their iPhones to iOS 8. Now playing MP3's natively doesn't work. After lots of testing I seem to have narrowed it down to the use of .htaccess.
Here are two identical directories with the same mp3 and directory listing software in them. But one is password protected (U:music P:access).
Normal: http://danielmee.com/dl/
Secured: http://danielmee.com/dl-secure/
I've also tried using a JS player which also broke once behind the secure directory.
What changed in iOS 8 to disable MP3 playback and is there a way to fix it?
The hoops that iOS goes through to play a music file are interesting, to say the least. Looking at a packet trace, the credentials are sent properly for the initial request of the file, but the subsequent partial requests do not have credentials. This is likely a bug in Safari; if you get a free developer account with Apple, you can report the problem to them.
HTTP authentication works this way: the browser makes a request for a resource; the server responds with "401 Authorization Needed" and includes a WWW-Authenticate header indicating the type of authentication — in your case, basic — and the "realm" which is just a way of grouping protected resources. The client re-requests the same resource, adding an Authorization header that contains the username and password — in the case of basic auth, it's just base 64 user:pass. Typically, the browser will cache these credentials and send them without being asked, for every subsequent request within the same realm.
I've got (actually my employer has) a mobile website that enables Safari integration (for iPhones and iPads) - meaning that customers can bookmark it to their home screen and then it would behave as a standalone web app (no address bar, custom icon, start-up image etc).
It works all right except that one week ago (coincidentally soon after apple has released iOS 6.1.2) some of our customers (6 of them initially) complained that they no longer get the normal content but a '404 page' of a public wifi provider (The Cloud owned by Sky here in the UK). After a bit of investigation we've figured that at some point those customers connected to the Cloud wifi without actually logging in (it's one of those providers that would redirect you to a login page to enter your credentials, after which you can carry on browsing). The thing is that even after switching back to their private wifi or mobile data connection the application would display the Cloud's page.
This only happens (as far as I can tell) when the application is launched via the bookmark (I couldn't see this behavior when using it from safari).
What happens is that the customers would connect to the cloud wifi (without logging in), they would open the application at which point the router will issue a redirect response to their login page; the application would cache the login page and it will always display it whenever using the bookmark again. (I've performed a capture when this happens and there are no requests being made at start-up whatsoever).
Even weirder, in this situation, if removing the existing bookmark and adding a new one will show you the same cached page (with the whole operation being performed away from the Cloud). We've fixed this by adding a unique identifier to the URL each time we hit the bookmark screen (this indicates that the web apps' sandboxes are linked to the url, which is to be expected).
What we're trying to achieve is to have the application properly recovering after the customer has moved away from the Cloud. But there doesn't seem to be a straight forward way to do this.
Furthermore there's a level of inconsistency in all of this - most of the times when the flow is performed I will see a 404 page (a custom 404 page https://service.thecloud.net/service-platform), but sometimes I would be properly redirected to the login page, in which case the application would not break.
My assumption is that there is a weird race condition in the standalone web app application model causing the browser not to properly handle redirects (and actually caching 404 pages). I've raised a support incident with Apple (which eventually turned into a bug report) but it might take a while and I'm trying my best to figure out a workaround.
Any ideas, maybe someone has seen this before?
The issue is aggravated by the fact that I need to have a 5 minutes walk ever time I'm testing any fixes; I've tried creating simple test forms, but I wasn't able to reproduce the issue, where as with the full app I can do it pretty much every time.
Here's a summary of the steps to reproduce:
Via private wifi (or mobile data connection) add a bookmark to a website (I've managed to reproduce it with quite a couple of apps that support safari integration as described above)
Open the application to review the normal content
Connect to a Cloud hotspot and open the application from the bookmark (open-close it for a couple of times if you don't get the 404 right away)
Connect to the private wifi (or mobile data connection) and open the application via the bookmark -> you'll see the same 404 page again
In the end the fix was to add a unique query string parameter with the initial page request (pretty easy with the setup we already had, via the launcher page). I've filed a bug report with Apple which they've acknowledged by linking it to a previous item. Here's a post on the topic:
http://blog.onos.ro/ios-6.1.2-caching-issue