iOS 10 won't send session ID when trying to deliver mp4 via X-Sendfile
Asked Answered
C

2

11

I have a PHP file who's sole job is to check if a user is logged in + if a session variable is set, then delivers a file via nginx X-Sendfile. It works perfectly on any desktop browser, and previously on any mobile browser - but fails on any ios 10 browser with mp4s only. What makes little sense to me is that if I comment the die() line, it works as expected, but the php code should never even get into that line.

<?php
require_once('authConfig.php');
$userInfo = $auth0->getUser();
session_start();
include('cururl.php');
$murl = curPageURL();
parse_str($murl, $result);
$filename=$result['file'];
$folder=$result['folder'];
if (!$userInfo || !isset($_SESSION[$folder])) {
        header('Location:login.php');
        die();
}
$file="/var/www/uploads/" . $folder . "/" . $filename;
$aliasedFile = '/protected/' . $folder .'/' . $filename; 
$realFile = $file; 
header('Cache-Control: public, must-revalidate');
header('Pragma: no-cache');
header('Content-Type: ' . mime_content_type($file));
header('Content-Length: ' .(string)(filesize($realFile)) );
header('X-Accel-Redirect: ' . $aliasedFile);
exit;
?>

I can confirm that $_SESSION[$folder] is actually set by changing the code to:

if(isset($_SESSION[$folder])){
   echo "OK";
   die();
} 

UPDATE: Here's the access log from the iPhone. It should have access to the file.

10.0.0.1 forwarded for ..., 10.0.0.1 - - [16/Sep/2016:10:07:20 -0400]  "GET /xfiles.php?&file=001.mp4&folder=NNx6659rvB HTTP/1.0" 200 1944706 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1"
10.0.0.1 forwarded for ..., 10.0.0.1 - - [16/Sep/2016:10:07:21 -0400]  "GET /xfiles.php?&file=001.mp4&folder=NNx6659rvB HTTP/1.0" 302 0 "https://www.sonoclipshare.com/xfiles.php?&file=001.mp4&folder=NNx6659rvB" "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1"
10.0.0.1 forwarded for ..., 10.0.0.1 - - [16/Sep/2016:10:07:21 -0400]  "GET /login.php HTTP/1.0" 200 4166 "https://www.sonoclipshare.com/xfiles.php?&file=001.mp4&folder=NNx6659rvB" "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1"

UPDATE2: If I change the php file to examine the variables more closely:

<?php
require_once('authConfig.php');
$userInfo = $auth0->getUser();
session_start();
include('cururl.php');
$murl = curPageURL();
parse_str($murl, $result);
$filename=$result['file'];
$folder=$result['folder'];
print("info: $userInfo <br>SESSION: $_SESSION[$folder] <br>");
if(!$userInfo || !isset($_SESSION[$folder])) {
   print("exiting");
}else {
   print("sending file");
}

I'm getting a result of:

info: Array
SESSION: NNc6659rvB
sending file

when logged in, and:

info:
SESSION:
exiting

when not logged in. It should be working as expected.

UPDATE3:
Adding var_dump:

if(!$userInfo || !isset($_SESSION[$folder])) {
        var_dump($userInfo);
        var_dump($_SESSION[$folder]);
        //header('Location:login.php');
        exit();
}
echo "sending file";

I get NULL NULL when not logged in and sending file when logged in. No echo from the loop. iOS acts like it's loading the mp4, shows the play button but no video unless I remove the exit() line.

UPDATE4: If I replace die() with exit(), same effect. Replacing it with a nonsense function to kill the script also have the same effect. For some reason it gets into the if statement when I request an mp4 only.

SOLUTION: I had to pass the session ID to the page as a query string and then set it manually. This works well for my use case, as this php page shouldn't ever link out to other pages, exposing the session ID as a referral link. Not 100% ideal, but it'll bridge me until Apple pushes an iOS fix. Oh, and I removed the authConfig code check as this was redundant. The session variable can't be set if the user isn't already logged in.

include('cururl.php');
$murl = curPageURL();
parse_str($murl, $result);
$filename=$result['file'];
$folder=$result['folder'];
$session_id=$result['session_id'];
session_id($session_id);
session_start();
if(empty($_SESSION[$folder])) {
        echo "redirecting";
        exit(header("Location: login.php"));
}
$file="/var/www/uploads/" . $folder . "/" . $filename;
$aliasedFile = '/protected/' . $folder .'/' . $filename; //this is the nginx alias of the file path
$realFile = $file; //this is the physical file path
header('Cache-Control: public, must-revalidate');
header('Pragma: no-cache');
header('Content-Type: ' . mime_content_type($file));
header('Content-Length: ' .(string)(filesize($realFile)) );
header('X-Accel-Redirect: ' . $aliasedFile);
exit;
Chrysarobin answered 16/9, 2016 at 2:13 Comment(11)
I combined the conditions but the problem persists: if (!$userInfo || !isset($_SESSION[$folder])) { header('Location:login.php'); exit();}Chrysarobin
If I remove die()/exit(), the redirect doesn't work but the file is delivered just fine.Chrysarobin
Code that is not executed cannot have an effect. Clearly, if something inside an if(!isset($_SESSION[$folder])) block is having an effect, then you must be receiving a request with $_SESSION[$folder] unset. Perhaps iOS is sending two requests for the file for some reason? Have you actually looked into your request logs? (You do have request logging enabled, right?)Arianaariane
I do. Let me have a look.Chrysarobin
Added the log entries.Chrysarobin
You should run this in xdebug, or at a minimum add a var_dump of all of your variables right before that die(); statement. Without that, you'll have a hard time figuring out what's going onRarotonga
I'll have a look at xdebug, var_dump didn't give any answers.Chrysarobin
Added xdebug, but not sure how to use it for this situation =/Chrysarobin
as expected, if I define the variables as true right before the check - the file's delivered as expected: $userInfo=$_SESSION[$folder]=1;Chrysarobin
Annoying thing is that it works perfectly on desktop, and works perfectly for other file types - jpg in particular.Chrysarobin
FYI, the mp4 encoding is H.264, Level 3.0, Baseline profile for max compatibility and works 100% as a directly served file.Chrysarobin
D
4

Ilmari has told you the situation: $_SESSION[$folder] is not being set when the mp4 is requested. Which means the iOS is not sending sessionID back.

Removing the die will "fix" the problem as the code simply continues, but it's a red herring. It's possibly to issue a "redirect header" but if you don't exit there will be other headers that will essentially nullify the redirect request. Always have an exit (or die) after issuing a redirect header.

What you need to check for is if $_SESSION is set for those requests. You won't find that in the Apache/NGNIX access logs, but will need to log it yourself. Simplest case is to add file_put_contents('\tmp\debug.log\, print_r($_SESSION, true)); into the code. (Will overwrite each run, use fopen if you want to append). Then chase back to see what cookies are being sent (or not sent) using the same logging. But something is not sending the sessionID.

As for a solution: that's tricky. Until it's fixed properly, perhaps you can use IP address? Not as reliable (in fact, very unreliable), but if a user is logged on using a particular IP, then allow any request from that IP? Or experiment using your own cookies / session system to see if they behave the same way. That's all I can think of for now.


Something from the Apple Forums: https://forums.developer.apple.com/thread/60688

Safari seems to be only adding those cookies whose path is different from the request url path. But this seesm to work on other browsers on IOS9 as these may be creating the cookies on the root of the domain.

So try changing the path of the request.

Demerol answered 20/9, 2016 at 0:23 Comment(7)
You're correct. I'm getting an empty session variable when requesting an mp4. But why doesn't it happen with jpgs?Chrysarobin
Followup question: can you elaborate on changing the path of the request?Chrysarobin
Tried using setcookie to verify the user against the database and it looks like I can't read it back to the server. Running short on options.Chrysarobin
Added some code to check current ip address against last visited ip address. Interestingly loads the mp4 directly but not when called as part of a page as a src atrribute.Chrysarobin
"Why does it not happen with JPEGS"? No idea - perhaps you should direct that at Apple - they wrote it. "Change the path" ? That's from the forum post I linked, you could ask there? I'd suggest that instead of asking for "/video.php" try asking for "vids/video.php" and put the php in the sub-directory? "Added some code to check current ip address" Post your code; this is 100% controlled by your server, not the browser. No reason it "sometimes works" (except where the IP address changes, which will happen).Demerol
Thanks for the help. I was able to get it working by passing the session ID as a query string and setting it manually with session_id($session_id);. Still not ideal, but since I'm only serving static content with the query-string URL I shouldn't be subject to being leaked via HTTP referrers.Chrysarobin
The query string is a nice idea, but WILL be logged, and can be copied. So apply some time-out to the video (i.e. only allow it to work for 30 minutes after you deliver the HTML page) or lock by IP address as well (stops it being shared). But glad you got it working. Hopefully Apple will fix this soon - unless it's a "feature" of iOS 10 - as it's going to affect many developers, including myself, probably.Demerol
T
1

Try it (without die/exit):

<?php

require_once 'authConfig.php'; // ok this *_once
$userInfo = $auth0->getUser();
session_start(); // why this line after get userinfo? probably this line must be place before including authConfig.php
if (!$userInfo || !isset($_SESSION[$folder])) {
    header('Location:login.php');
} else {
    include 'cururl.php'; // why this not *_once?
    $murl = curPageURL();
    parse_str($murl, $result);
    $filename    = $result['file'];
    $folder      = $result['folder'];
    $file        = '/var/www/uploads/' . $folder . '/' . $filename;
    $aliasedFile = '/protected/' . $folder .'/' . $filename; 
    $realFile    = $file; 
    header('Cache-Control: public, must-revalidate');
    header('Pragma: no-cache');
    header('Content-Type: ' . mime_content_type($file));
    header('Content-Length: ' . filesize($realFile)); // dont require cast to string because concatenation
    header('X-Accel-Redirect: ' . $aliasedFile);
}
Tocharian answered 19/9, 2016 at 23:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.