kiding / beotsmusic

Standalone Mac OS X browser for Beats Music
http://beotsmusic.kiding.net
MIT License
26 stars 5 forks source link

"Add to My Library" is (definitely) broken. #10

Closed kiding closed 10 years ago

kiding commented 10 years ago

Beats Music web application started not to create the .transport__context .add_to_my_music DOM element before a hover event happens on ... (more) button.

We can resolve this issue by emulating a click event on + (plus) button, and use MutationObserver to wait for Add to My Library button to be added to the DOM.

kiding commented 10 years ago

2014-10-29 5 36 49

Beats Music makes use of PUT/DELETE to do so, and apparently the web app doesn't know a track is already added to the library until it requests and evaluates every single added track information.

It may be just a bug, it may be a quick hack to their end, but it means BeotsMusic has to wait for the long process to be finished, and then finally click the button. The app uses the same function to handle both add and remove.

Or... just an API call would solve that.

kiding commented 10 years ago

I've been trying to make the PUT API call on my own. The request itself is pretty simple – it's standard REST API with access token from #7, also somewhat documented. The power of Chrome Inspector FTW, I could manage to hand-craft the same request from the console, and it worked, as expected.

But a problem occurred right after I had decided to commit the changes. During the test – which I considered to be the last, a CORS error started to pop up. To be exact, the CORS headers, which had been present in response of the OPTIONS pre-flight request, were missing from that of the actual PUT request.

Well, it happens. The developer could've simply forgot to include the headers for PUT requests. In fact, it looks like that might be just the case. But if so, not only my crafted request but also Beats Music web app should not work. Not just that, my request should not have worked in the first place. And yet, the web app seems to experience no problem. So I examined the requests that came out of the app.

Now they appeared. What's going on? Re-checked the requests - they look identical to me. Could it be because it's synchronous? Nope. It sure looks like a bug, but who's responsible? Is this a WebKit bug, maybe some kind of cache corruption that, some headers are cached even though they're not supposed to? Well, Chromium disagrees with me pretty easily – KURL or std::make_pair can't be doing something wrong. Besides, it happens with Firefox too, so this theory flies out of window.

After some more attempts, it seemed a random GET/POST XHR request prior to the PUT request somehow solves the problem. It doesn't even have to be a valid request, or not even the same path – as long as they are going to the same host, and the first response contains CORS headers. But even if I try the sequence of requests using Copy as cURL, still no header.

How could the server know this client is the client that sent a random request before this PUT request? There's no Set-Cookie header, so it's not cookie. It's not IP address filtering – if so, cURL should've worked. How does the server know the client is a web browser, even after User-Agent is faked? It's impossible. Then what's the difference between a browser and a sequence of cURL commands? There is one thing left that explains everything – Connection: Keep-Alive.

Connection: Keep-Alive is a HTTP/1.1 functionality that asks the client to keep the socket opened for more requests even after one HTTP request/response is finished, in order to minimize the cost of establishing sockets. Now things make sense – it's the same socket, so the server would know what happened until it gets closed. I've made a simple node.js script to confirm this theory, so you can try out by yourself.

So, this is the very reason why Beats Music web app worked but not mine. As I mentioned in the previous comment, the app sends a batch of requests just to figure out if this song is already in the library. The socket for that batch would be re-used for the upcoming add (or remove) request. Also, they log every "event" using POST requests – that's definitely the random request we need.

But would it really definitely work? Choosing either, reusing the existing socket or creating a new one is basically up to the browser. HTTP is supposed to be stateless by the spec. Developers are not supposed to predict this kind of behavior. It could be a cool trick to achieve certain state without any extra setting, but it can never be reliable. In fact, you can easily experience this problem. Try turning the internet connection off and on right before clicking on Add To My Library button. Instant CORS error.

Of course, CORS error doesn't necessarily mean everything fails. In this case, since OPTIONS pre-flight is intact, the request itself would be sent, responded with the code 200, after that the browser checks the header, throws an error. The server received and processed it anyway, so it wouldn't be that much of a problem. However, browsers tend to cache the headers – at least for a moment. They would simply not fire the requests because they were marked invalid. Also the client not being able to check the result is never a good thing.

Lastly, could there be a security concern? A possible scenario I can think of is that, if there are some API endpoints that don't allow CORS so don't return the headers, the attacker might bypass the logic and acquire them by simply trying a GET request in advance. But I couldn't find that kind of endpoints from api.beatsmusic.com, moreover, the API is allowing all the origins, so I would say – not really.

I guess the headers happen at BigIP load balancer? I'm not sure... Well, that was fun.