Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations sizbut on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Protecting media files 1

Status
Not open for further replies.

OsakaWebbie

Programmer
Feb 11, 2003
628
JP
I have a private (small number of users) PHP-MySQL site that uses session authentication, and I want to offer MP3 files to be accessed by users only (I'll put them on pages via embed tags with mini-controls that the users can play). But how do I keep the media files themselves from being accessed directly by non-authenticated people? I found a few others who have asked this question (or something similar), but I didn't see an answer that completely satisfied me. These are three suggestions I saw:

1) Use HTTP authentication (.htaccess) on the directory where the media files would be - I haven't seen anyone suggest how to avoid the users having to log in twice, and me having to maintain two lists of users/passwords (DB and htpassword).

2) Place the files outside the web tree and use passthru or readfile to serve them - as far as I can tell, that would not allow me to embed the audio on a page with HTML, but only to serve the binary file directly with the appropriate Content-Type - not what I want.

3) Keep the permanent files outside the web tree and make a randomly named copy in the web tree when needed - pretty cumbersome, especially regarding deciding when and how to delete the temporary files later.

Are there other, simple but effective ideas? Or ways to overcome the problems in one of the above methods?
 
if you are asking whether the server can be certain that a page is not called statically but only ever by an embed tag...
No, not necessarily that in particular. I was just wondering if the fact that the file is calling itself, rather than somewhere else, opened up any other methods besides "if ($_SERVER('HTTP_REFERER'] == $PHP_SELF)...". Perhaps a way to refer to oneself besides by explicit filename, so that REQUEST_URI or some other environment variable is something special? Or something I can add to the URL ($_GET stuff) that can't easily be faked? I'm just about ready to give up and assume there will not in the near future be hackers among my users (there aren't at the moment), since this final detail is only security against people who can log into my site - the main concern I had originally has been solved by the <embed src=file-with-readfile-call.php> technique.
 
After sitting on it for a couple days and then looking at it again, I came up with a way to protect myself without using the HTTP_REFERER variable that was mysteriously causing IE to fail. I always like to close out threads with the solution, so this is what I did:

Since I am already using sessions, I got the idea to set a temporary session variable to tell myself I'm being called from the right place. Also, as I said in my previous post I might do, I changed to a self-calling structure. In the pages that call it I use "window.open" to make the window nice and small so that the user can still see the song information on the previous page why he is listening. Here is my code (with the names changed as usual to match my previous examples):
Code:
<?php
session_start();
if (!isset($_SESSION['userid'])) exit;
if (isset($_GET['playsong'])) {
  if (!isset($_SESSION['audio_ok'])) exit("You can't call this directly in order to download this audio!");
  unset($_SESSION['audio_ok']);
  $file = "audio/".$_GET['playsong'].".mp3";
  if (!file_exists($file)) exit;
  $size = filesize($file);
  header("Content-Type: audio/mpeg");
  header("Content-Transfer-Encoding: binary");
  header("Content-Length: ".$size);
  @readfile($file);
} else {
  $_SESSION['audio_ok'] = 1;
  echo "<html><head>\n<title>Audio: $title</title></head>\n<body>\n";
  echo "<div align=center>\n";
  echo "<embed src=\"".$_SERVER['PHP_SELF']."?playsong=".$songnum."\" controls=\"console\" width=\"144\" height=\"60\""; 
  echo " vspace=\"0\" hspace=\"0\" border=\"2\" align=\"top\" autoplay=true";
  echo " pluginspage=\"[URL unfurl="true"]http://www.apple.com/quicktime/download/?video/quicktime\"></embed>\n</div>\n";[/URL]
  echo "</body></html>";
}
?>
This works fine in both Firefox and IE, and unless I missed something, I don't think anyone can download the audio files without either hacking the session or somehow getting past a "deny from all" in .htaccess - that's perfectly acceptable to me. (Well, I guess they could use their record function in their soundcard, but that would give them a generation of loss on the compression, and if they're that intentional, the copyright issue is their problem, not mine.)
 
Argh! Yesterday my code was working. Today IE is being uncooperative again. I did make minor mods to the code, but they should have been unrelated to the issue, and even after I took them back out, it's still not working. Now, the check of second session variable (audio_ok) is the check that causes the audio data to be considered "broken" by the plugin. As before, there is nothing wrong with the check itself - I have put in enough diagnostics to ascertain that the check definitely passes, and the code goes on to set the headers for MPEG data and read the audio file. But just the presence of that check somehow causes the later execution to be corrupted - the headers are sent, the data is passed through, but the plugin gives me the "torn filmstrip" icon. The check of the session variable "userid" causes no problems. For a moment I thought maybe simply the presence of two variable checks like that was somehow too much for IE (snicker), but commenting out the userid one and leaving in the audio_ok one still causes the error.

And just as before, there is no problem in Firefox - it's just in IE.

Please, is there someone out there that knows why this is happening?
 
couple of things:

the IE error is quite possibly being caused by cacheing. empty the IE cache, turn the computer off, dunk it in the freezer for an hour or so ( ;-) ) and you might just have got rid of IE's temp files.

using a self-serve page won't avoid users from being able to access the file from the browser address bar within a session. but at least (if you log all the accesses) you will know who has done this.

 
As far as I understand, I did everything you suggested except for the freezer part (;-)) - I am not aware of a difference between "cache" and "temporary internet files", as those terms seem to always be treated as synonyms, but I went in the options dialog, clicked the "Delete Files" button, and chose the "Delete all files" checkbox before executing. Then I shut down the PC, waited 30 seconds, and rebooted. Unfortunately, the error still occurs.
using a self-serve page won't avoid users from being able to access the file from the browser address bar within a session.
If you look at my code carefully, you'll see that when the page is first called (with a passed variable of "songnum"), I set a session variable called "audio_ok". Then in the code that runs when the embed calls itself (with the passed variable of "playsong") I check the variable and then immediately unset it. If a logged-in, session-owning user tries to call it directly (e.g. " $_SESSION['audio_ok'] is not set, so it gives them only a message saying that they can't do that. I tested it - it works. Of course, if a non-logged-in user tries it, the code doesn't even get that far, but exits at the test of $_SESSION['userid'].
 
using the session check is a good thing but it doesn't stop people downloading the content. once they are on the first page they could just enter type the address of the song-fetch script into the address bar and it would come zooming down the bitpipe.

i'm intrigued by why it is not working with IE.

also can you confirm the version of php you are using?
 
using the session check is a good thing but it doesn't stop people downloading the content. once they are on the first page they could just enter type the address of the song-fetch script into the address bar and it would come zooming down the bitpipe.
They'd have to be REALLY quick, because what you call "the first page" (the part of the code that sets the session variable) immediately calls the "song-fetch script" (i.e. the part of the same file that unsets the session variable as one of the first things it does). If there is a way that a clever user can stop the second part from executing (which would still have to be one of my registered users, which is a small number of musicians, not hackers), I would say that at that point the copyright violation is on their own head, not mine.

The server I have been using this on so far has PHP version 4.3.10, although I haven't a clue why that is relevent, since it works in Firefox.
 
my idea is to use fopen and then a sequential fread + echo instead of readfile. i suspect that IE is getting bored waiting for the download to start. readfile is materially slower than fread as, for one reason, it needs to read the entire file into memory. clearly the difference gets worse the larger the file size.
Code:
\\instead of @readfile($file);
$fh = fopen ($file, "rb");
while (!feof($fh)):
  echo fread($fh, 2046);
endwhile;
fclose($fh);
 
That's a good idea (I had also read someplace that it was faster, so I was considering it anyway), and the window responds faster than it did (in Firefox it actually was a bit too fast - the playback overran the buffered data right at first). But unfortunately the same problem persists in IE.

I don't know if the following info will be helpful or not, but I just tried this on a different computer, whose IE is set up to use Real Player rather than Quicktime for playing MP3s, so I get a different plugin which functions differently. With or without the check for the variable, the console comes up but does not autoplay (even though my code tells it to - this is apparently something in the Real Player plugin settings). With the offending code (the check on $_SESSION['audio_ok']) in place, when I click the play icon, it immediately connects to Real Networks in a vain attempt to find an update that will help it play what it thinks I gave it - clicking on Details reveals that it thinks the content type is "text/html"! If the offending code is commented out, it still doesn't autostart, but it does play when I click the button.

The "text/html" clue would seem to indicate that IE is loading a default dummy page but not replacing it when I sent my MPEG header and the rest of the data. I know that this symptom also happens if the programmer inadvertently sends any characters (like a space) before the PHP code starts, but if I were doing that: (a) it would not work in Firefox; (b) it would not be cured by commenting out an if statement that evaluates false and has no else clause.

Does this information give you any new ideas as to what might be wrong?
 
rats. I was confident that the issue was speed of starting the data transfer.

I'm now thinking it might not be an IE issue so much as the binding between ie and the music client. Could you try it with a native MS binding to windows media player?

I agree that the text/html thing is interesting too. I'll give it some thought. I'll give it some thought and try to set up a sandbox that mimics your environment (I admit not to having realplayer or quicktime on my system. Somehow they invidiously start popping up boxes and icons everywhere).
 
Could you try it with a native MS binding to windows media player?
How do I do that? I looked in IE's options (program tab), but don't see anything for media files. In regular file associations (in the file explorer), MP3 is actually assigned to Real Player (on the machine that uses Quicktime for this code), so that's apparently not the spot. All other "MPEG" flavors except MPEG4 movies are already assigned to Windows Media Player. I don't know the term "binding" in this context, so I don't really know what to look for.
 
umm. i'd start off with trying to assign mp3 to WMP.
 
Tried it - that doesn't seem to affect what happens in IE. But I found a third computer to play with, which already is using Windows Media Player for MP3 embeds (at least I assume so - the mini-console looks different than either Quicktime or RealPlayer, and the owner of the computer said he probably hasn't even installed any players that didn't come with his computer). Its behavior is different, but clearly is having the same problem with the data - with the if statement commented out it auto-plays quite nicely, but with the if statement included, the console comes up, but it just sits there (probably looking much like it would if autostart was set to false), and clicking the play button does nothing.

I'm ready to give up. My main concern was about the world having access to these files - that was solved many posts ago. This final check is only to protect the files against official users who not only have enough HTML knowhow to look through the source code and figure out what they need to enter in the address bar to download the file, but also enough cleptomaniacal desire to decide to steal a highly compressed (i.e. not great quality) version of a church worship song. With the method I am using, a casual "right-click -> Save As..." won't do it - they have to be intentionally going against the design of the code. The kind of people who would be using this application - Christian musicians - typically have higher morals and lower technical savvy than average, so at this point, with no solution in sight for this mysterious problem, I think it's time to move on.
 
Hi,

You can do this kind of simple.

The problem with embedded files, is that a simple view source will give out the URL to the file.

However, why do you need to tell your users where the file is?

My suggestion:

Make a table: "temp_file"
Fields:
file_id (unique id)
file_downloaded (int(1))
and maybe more fields

Then make a table: "users"
user_id
user_name
user_passwd
etc. etc.

After that, make the table: "downloads"
Fields:
download_id (unique id)
download_id_file_id (int)
download_title (text)
download_file_path (text)

downloaded_id_file_id
How will it work?

When your user has logged in, he clicks "download section".
Thag page, runs a query:
SELECT ... FROM downloads;

He then gets a list of your files, as I guess he already does. Then there will be a link, "Download" or "View film" or something.

In that page that it's linked to, you run a new query:
SELECT .... FROM downloads, temp_file WHERE download_id_file_id = file_id;

if (mysql_numrows() < 1) {
// Run an insert query, that inserts a new row in temp_file
INSERT INTO temp_file ....;
}

Now you have a row in the temp_file for the current file.
You use the mysql last insert id, to retrieve the ID.

SELECT ... FROM temp_file WHERE file_id = '$lastinsert' AND file_downloaded='0';

IF (more than 0 rows) {
Use HEADER with attatchment, so you can stream to the current ID.
What you do, when the stream is done, is to update the file_downloaded = 1;
}
else {
Ask them to try the download link once again.
}

Also, you can "fake" the url's with htaccess...
I have already made this system and it wont take a experienced programmer more than a couple of hours to make.

The most timeconsuming interface, is the GUI interface(s).
However it's not very hard, also you can as I said above "fake" url's..

Then you could run embedded on the URL... (would look static), and you could simply in the ELSE send them another video of the file is downloaded..

eg. you could send them a "dont leech from me, pal".

Olav Alexander Mjelde
Admin & Webmaster
 
oh,btw. you could also append to my idea a field for user_ip and maybe a "download lifetime", so you could run a query on those factors too.. (would narrow down remote linking to a VERY few potential "farmers")

Olav Alexander Mjelde
Admin & Webmaster
 
I read your message about three times, but you lost me. Authentication, an extensive database, and a complex GUI are already in place - I am not starting from scratch. At the point where your suggestions would become relevent, I already know what file the user wants - that's all already in my system (serving audio files is actually a very minor part of my application). The main part of your post that sounded like a new idea was unfortunately the part I didn't understand - it seemed like you went over it very quickly.
Use HEADER with attatchment, so you can stream to the current ID.
"Stream to the current ID"? Is that the same as the readfile or fread loop that I have been using?
Also, you can "fake" the url's with htaccess...

Then you could run embedded on the URL... (would look static), and you could simply in the ELSE send them another video of the file is downloaded..
Fake what URL? I don't know what you mean. Since I want the audio embedded in HTML, I need a real <embed> with a real URL that points to a real file. Naturally I structure it such that it points to something I can control, not to the MP3 itself. Currently it points to itself (the same PHP file that contains the embed, with the same authentication check), but with parameters that make it run a different section of code, code which serves (perhaps what you called streaming, but I don't have a streaming server) the MP3 file from a directory that is protected from direct access. The final problem I was having was not a lack of ideas for design of the system, but a mysterious glitch in IE caused by a seemingly innocuous check of either of two system variables (either HTTP_REFERER or a particular session variable, even though other session vars are not a problem). It's true that without checking one of them, a bonafide logged-in user could in theory study my code and circumvent it. But read on...
(would narrow down remote linking to a VERY few potential "farmers")
My potential farmers are already VERY few - I currently have 6 users, all of whom (other than me) are of the level of knowledge about computers that they wouldn't know what "View Source" does, let alone recognize an embed tag and know what to do with it. They are also Christians and personal friends of mine - if I tell them the files are copyrighted, they are not going to copy them.

Naturally I would like the final part of the security in place if possible, for completeness and also so that if someday I share this application with another church (and they share it with others, etc.), it would be designed the way it should be to protect copyrights. But most copyrighted material on the Internet doesn't have as much protection as I already have on this - at some point I should be able to say I have done my part, and any hackers that get past me bear their own responsibility for breaking copyright.

As mysterious as this "glitch" in IE is, I fear that even if I figure out what you are suggesting and redesign this section of my code to implement it, some other if statement will end up causing the same glitch. I'd rather know what the problem is and solve it. But if you can explain your method a little more, maybe it is accomplished without stumbling over any "glitch-ers" - who knows?
 
Sorry, a bit technical maybe :p

I remember that I once made a videostreaming test..
As far as I can remember, it worked..

This is how the code was:
Code:
<?php
if (isset($lid))  {
  /* 
   This is a function that corresponds the id to a filename.  You
   may get the filename from the db or whatever..
  */

  $filename = getFilenameFromID($lid);
		
  // let's strip the filename, to get a real name without path
  $fnam = substr($filename, (strrpos($filename, "/") + 1), strlen($filename)); 
  $furl = substr($filename, 0, (strrpos($filename, "/") + 1)); // returns real filename

  // now set the right headers...
  header("Content-type: application/octet-stream");
  header("Content-Disposition: attachment; filename=$fnam");
  header( "Content-Location:$furl" );
  header("Content-Transfer-Encoding: binary");

  // now print the file..
  print $content;
  }
?>
<h3>Video</h3>
	<?

  $result = mysql_query("SELECT * from  cms_attatchment 
ORDER BY att_title ASC") or die (mysql_error());
  while($row = mysql_fetch_array($result)) {?>
<p>
  <strong>title:</strong><?=$row['att_title'] ?><br />
  <strong>Beskrivelse:</strong>  <?=$row['att_description'] ?><br />
  <a href="?lid=<?=$row['att_lid'] ?>">Last ned</a></td>
</p>
<? 
  } 
?>
</table>
<?php
function getFilenameFromID($file_id)
{
// kjør query mot db, WHERE id = '$file_id'
  $result = mysql_query("SELECT * from  cms_attatchment WHERE att_lid = '$file_id'");
  while($row = mysql_fetch_array($result)) {
    $f = $row['att_url'];
    }
  return $f;
}
?>

another file included this one above:

Code:
<?php
// connect to the database
mysql_connect ('localhost', 'very', 'secret');
// select database
mysql_select_db (indeed);

require("downloadtest.inc");
?>

Code:
CREATE TABLE `cms_attatchment` (
  `att_lid` int(6) NOT NULL auto_increment,
  `att_title` varchar(255) NOT NULL default '',
  `att_description` varchar(255) NOT NULL default '',
  `att_text` text NOT NULL,
  `att_url` varchar(255) NOT NULL default '',
  `att_type_id_id_type` int(2) NOT NULL default '0',
  UNIQUE KEY `att_lid` (`att_lid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COMMENT='this table is for attatchments (primarilly videos)' AUTO_INCREMENT=2 ;

I hope you can use this for something. I might provide better code tomorrow, as I have programmed a better system last year.. (this code is a bit old and sloppy (testcode))

Olav Alexander Mjelde
Admin & Webmaster
 
I don't mind "old and sloppy" - the examples of what to do are what are important. What interests me in your code is the section where you actually send the media file (the sections "now set the headers" and "now print the file") - you did it a little differently than the two ways I have tried; perhaps yours might not be susceptible to the error I was getting in IE, so I'd like to try it.

But where does the variable $content come from? Do you put the whole media file into one variable (in some part of the code you didn't show me)? I wouldn't have thought one could have a variable with a "value" that large.

At first, when I read what you do with the headers, it seemed like HTTP was going to somehow get the file on its own (why else would one need to specify the filename and path?). But then you proceed to "print $content", so it appears that one still has to serve the file manually. Sorry for my ignorance, but this is my first experience using PHP to output something other than HTML content, and based on my experience with the IE problems, the process is apparently rather touchy.
 
@osakawebbie

variable size is limited pretty much only by system capacity.

readfile pretty much copies the entire contents of a file and dumps it as one transaction into the browser. but you're right to spot that olav's posted doesn't include the call to populate the $content variable.

to be honest I don't think that olav's code is addressing your current issue in that it doesn't solve the IE glitch caused by the HTTP_referer check and i'm not sure it enhances security over that which you currently have. But perhaps olav has come across this IE glitch?

do you have a spare machine that you could try IE 7 on? i cannot recreate the problem you are experiencing. on my servers it just works. what i might do, if it's ok with you, is externalise what i have done and see whether it works for you from your side. do you still have the energy?
 
Energy is getting low for working on this, but maybe I can try just a little more...
to be honest I don't think that olav's code is addressing your current issue in that it doesn't solve the IE glitch caused by the HTTP_referer check...
Since the glitch was some mysterious interaction between checks of system variables and the downloading of the data, I figured it was possible that a different method of downloading the data might respond better. Grasping at straws, but that's what you do when you have a mysterious problem, right? [ponder]

do you have a spare machine that you could try IE 7 on?
I don't have a "spare" machine, and even if I did, the English version is apparently still in beta, so I suspect (I didn't check) there's not even a test version available in Japanese yet. (All my computers run Japanese Windows, and although in the case of simple, non-low-level third-party software I often run English versions just fine, I think English IE on Japanese Windows, especially a beta version, might be high risk.)
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top