I'm initializing jplayer with the following parameters:
$jplayer.jPlayer('setMedia',{
mp3: data.audioMP3,
oga: data.audioOGA
});
Assume that data.autdioMP3 (and it's OGA counterpart) are paths to a php script, for example: 'http://myserver.local/playaudio.php?songID=99&format=mp3'
Where I am struggling is with playaudio.php. I would like to read the MP3 file and stream it to jplayer without revealing the path to the audio (this is why I am not initializing jplayer with a path to the audio file).
Something like (taken partially from the example for readfile at php docs):
<?php
$if ($validUser && file_exists($file){
header('Content-Transfer-Encoding: binary');
header('Content-Type: audio/mpeg');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;
}
?>
I think this just forces the download of the file though... will this still reveal the path to the file to the user? Is there a better option to stream raw mp3 data to the user without revealing the path to the file that you know of?
Thanks!
Since you are using javascript to get the file, others can do the same by reading the headers/file requests from the browser (firebug, Chrome's default tools, etc).
The best way to protect your files from being downloaded is to make a series of checks to get at the data. It's not perfect, but it will deter all but the most determined. For some ideas, check out this page on Grooveshark's API (This is a bit old and some info is missing, so it's the perfect reference), or just visit Grooveshark/Youtube and see how they protect their content.
It is possible to recreate this kind of security with some clever usage of sessions and a good database.
i think you'll struggle to prevent users discovering your media content URL, i'd say it was pretty much impossible (a quick look at the 'Network' tab on Chrome would reveal your loaded track, regardless how you coded playaudio.php).
I'd say you were better off sticking to publishing media you don't mind sharing with the world.. Or just publish
A n second preview
A lesser quality version, e.g. 90bps / Mono
If you really wanted to protect your resources, you could use oAuth or Access Control at the server side.
Thanks for everyone's answers.
I actually ended up solving the problem without revealing the path to the audio file to the user.
Because this is for an online voice messaging application, it was important to protect the path to the messages in order to protect the user's privacy.
The situation ended up being further complicated by the fact that the messages are stored on a remote server... found this out right after I posted this question.
So all in all the process was:
1) Curl the MP3 file from the remote server
2) Set some headers to force the download of the data, which you then echo to stdout
Because the data is being put on standard out and the headers are set to force a download, the effect for jPlayer is the same as if it had requested a file at /some/dir/message.mp3, except that the request goes to /some/dir/playmessage.php - thus the path is never revealed to the user.
EDIT** - Assume that I have access control, validation running before the snippit below, otherwise exposing the path to the script below is no different than just exposing the path to the mp3. (see comments between me and Lloyd)
Here's the code that ended up getting the job done:
$ch = curl_init($remoteFile);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$data = curl_exec($ch);
curl_close($ch);
if ($data === false) {
echo 'CURL Failed';
exit;
}
//Get file size
if (preg_match('/Content-Length: (\d+)/', $data, $matches)) {
$contentLength = (int)$matches[1];
}
//force user to download file contents
header('Content-Transfer-Encoding: binary');
header('Content-Type: audio/mpeg');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Content-Length: ' . $contentLength);
ob_clean();
flush();
echo $data;
exit;
?>
Assume that $remoteFile is a path to the mp3 file that I want to play on the server where we are storing messages.
I found this resource on reading a remote file using PHP especially helpful. Here's some more information on modifying http headers.
NOTE I am essentially downloading the file twice, which slows the process down for the user. There is probably a better way to do this. :) If you kept the file locally, what I had initially in my question actually works (using readfile() to output the raw mp3 data).
Related
I would like to trigger an action when Apache detects that a certain file URL has been started for download (or: successfully downloaded).
Example: when https://example.com/download/token_A6FZ523/myfile.zip is downloaded by a client, execute the following query to a SQLite database:
INSERT INTO downloads(date, tokenID) VALUES(CURRENT_TIMESTAMP, "A6FZ523");
Usage: then, in a PHP Dashboard, I can check who has downloaded the delivered files.
I could do this by:
running a script every minute on the server,
parsing the Apache logs /var/log/apache2/other_vhosts_access.log in search for a pattern download/token_.*/myfile.zip
execute the INSERT INTO query in this case
This seems rather complex and the fact of having to run the script every minute is not a nice solution.
What is a good solution to ask Apache to save to a SQLite database the information "The file associated to download token A6FZ523 has been downloaded by the client."?
Or maybe should PHP be used instead?
I think your problem lies in that you are directly fetching a file that is stored on the server, as opposed to using PHP to "serve" this file programatically. This isn't the first problem you will encounter with this method, you also can't check for security or get the file from external file storage (generally speaking, you don't store files directly on the web server these days!).
But, simple to do once you know how :)
Firstly, lets change the URL you download your file from to something like https://example.com/download.php?token=A6FZ523
So, we are sending a GET variable to a php script named "download.php". In that script you will have something like the following:
<?php
$token = $_GET['token'];
// Get the information about the file from the DB, something like:
// SELECT filename, size, path FROM files WHERE token = $token;
// Giving you $filename, $size and $path
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $filename);
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . $size);
echo file_get_contents($path);
// This will be on a completed download
// INSERT INTO downloads(date, tokenID) VALUES(CURRENT_TIMESTAMP, $token);
?>
When the download.php file is called, the token is taken and matched to a file's info in the DB. You then set headers which basically tells the browser "this is a file", your browser responds accordingly by implementing a file download as normal. You then read the contents of the file to the user. Once this has been completed, you can log the download via another DB call.
A big thing to say is that this script (obviously with the DB calls written in) should do the very basics for you, but there is a lot more to add depending on your usage scenario. Think security, input validation, where you store your files and sending a MIME type header.
Hopefully that should point you in the right direction though :)
If you have access to server and authority to install thins you could add mod_log_sql and have the apache save the log directly into a database table (it even parse the info for you) them in your dashboard you can just do simple queries. The "thing" here it seams that you are in need to get the name of the downloader, therefore you should add that "tokenID" to your URL and set the Apache to deny the url if tokenID is not present. You would need to parse the tokenID from url in the log thought.
I don't actually now hoy to ask this question, so it may be probably repeated. Let's see: I would like to disable downloading a file from my web without a download script (just using the URL: http://something/file.zip) unless you're registered, with PHP preferably. Yes, it's a very common topic but I haven't found any information! A lot of pages do this, such as uploaded.net. I hope you understand what I'm talking about. Thanks!
First and foremost, don't allow direct access to the file. Store it outside of your web application's root folder, elsewhere on the file system, so that there is no link which can be used to download it. This is because direct access skips any PHP application and interacts only with the web server, which has no knowledge of your application's session values.
Then create a "download" script to serve the file to users. Generally such a script would be given some identifier for the file, something like:
http://yourserver.com/download.php?file=file.zip
(Important: Be very careful how you identify that file. Do not just blindly let users download whatever they want, or they can enter longer paths onto the URL and download any file from your server. Always validate access to things first.)
This would be just like any other PHP script, except that instead of displaying HTML it would return a file. The actual part of outputting the file can be as simple as:
readfile('/path/to/file.zip');
You'd also likely want to set content headers appropriately, etc. A more complete example can be found in the documentation:
<?php
$file = 'monkey.gif';
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
}
?>
I have a script that merges 2 documents and then it shows a final document in a browser.
It works in Firefox, Opera and IE. But it does not work in Chrome.
Chrome only shows loading and it stops in 1/4 of loading.
The code:
exec("pdftk A=$pdfin B=$tmpfname cat B1 A output $tmpfoutput");
$data = file_get_contents($tmpfoutput);
header("Content-type: application/pdf");
header("Content-disposition: inline;filename=GeneratedPdf.pdf");
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . filesize($tmpfoutput));
header('Accept-Ranges: bytes');
echo $data;
I have been told that it also worked in Chrome before so I think there will be only a problem with the last version.
Thanks for any help.
By appending Accept-Ranges: bytes, your script tells the browser that it will accept range requests, i.e. multiple requests that request a part of the response. Your script obviously does not support range requests because it generates and provides the data at once.
To fix the error, remove header('Accept-Ranges: bytes');
If your PDF files are usually large, then a more user-friendly solution is to actually implement range requests in your script. Odds are that your server does already have an efficient routine that handles range requests, so a smart choice is to save PDF file to a publicly accessible directory, then 302-redirect the request to this URL after the PDF has been generated. Make sure that the URLs are unguessable, e.g. by using UUIDs. And remove the PDF files at some point, e.g. using a cronjob.
I'm working to develop a website that allows clients to log in and see various PDFs saved on the server. These PDFs will be unique to the client and should not be accessible by someone who is not logged in. Getting the files onto the server shouldn't be an issue, I'm just not sure on how to serve them to end users.
I've implemented this kind of thing with data from SQL servers being served instead of files, so I'm not entirely sure what the most effective way to go about this.
The website is on a LAMP and my minimal experience is in PHP (but if a framework or other language would make this easier, I can learn it).
I'm probably in over my head but I usually am, so any input would be great.
Put the files outside of the webroot. Then using PHP pass the file though a script. That way no one can link to the file directly and bypass your controls. (Naturally make sure the script that does this only after verifying the user has permission to retrieve that file).
Sample PHP:
<?php
session_start();
if (!isset($_SESSION['authenticated'])) {
exit;
}
$file = '/path/to/file/outside/www/secret.pdf';
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;
?>
The easy way is to give these files long random filenames (say, 20 random characters). Technically they will be accessible by anyone, but it will not be possible to guess the URL, so only authorized people will have access.
Alternatively John Conde already outlined a way to serve a file from a PHP script. It will incur a small performance penalty, but will be as secure as your code is. The only thing I can add is that if you cannot place them outside webroot, then you might be able to use .htaccess to prevent people from accessing the files directly.
John posted the primary correct way of doing it, so I'm adding the (probably inferior) alternative: Serve it from the database. Just have a BLOB column for the PDF, and read/store the file data from the database. You'll end up with a quite large table, but it will work. Serving it requires setting the same header()s as John posted, you just send off the data from the DB instead of from the file.
This has the advantage of not having to ensure you don't have filename collisions, etc.
I've got a php page which handles requets for file downloads. I need to be able to detect when a file has been downloaded successfully. How can this be done? Perhaps there's some means of detecting this client-side then sending a confirmation down to the server.
Thanks.
Edit:
By handle, I mean that the page is doing something like this:
$file = '/var/www/html/file-to-download.xyz';
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($file));
header('Content-Disposition: attachment; filename=' . basename($file));
readfile($file);
Handle the download in a seperate php script (better do a little more than just readfile($file);, you can also provide the ability to resume downloads like in this question).
Then in this script, when you read the last block and send it, you know that all the file was sent. This is not the same as knowing that all was received, but it should be enough for most scenarios.
What Treb said, but I should add that you can check if the client is still listening during download with connection_status().
Don't forget to flush() after you've written data to the client, it helps in detecting if the connection is still up.