diff options
-rwxr-xr-x | AdobeHDS.php | 2041 | ||||
-rwxr-xr-x | fb-img-resize | 12 | ||||
-rw-r--r-- | woof | 620 | ||||
-rwxr-xr-x | ximkeys | 75 |
4 files changed, 2748 insertions, 0 deletions
diff --git a/AdobeHDS.php b/AdobeHDS.php new file mode 100755 index 0000000..508935d --- /dev/null +++ b/AdobeHDS.php @@ -0,0 +1,2041 @@ +#!/usr/bin/php +<?php + define('AUDIO', 0x08); + define('VIDEO', 0x09); + define('SCRIPT_DATA', 0x12); + define('FRAME_TYPE_INFO', 0x05); + define('CODEC_ID_AVC', 0x07); + define('CODEC_ID_AAC', 0x0A); + define('AVC_SEQUENCE_HEADER', 0x00); + define('AAC_SEQUENCE_HEADER', 0x00); + define('AVC_NALU', 0x01); + define('AVC_SEQUENCE_END', 0x02); + define('FRAMEFIX_STEP', 40); + define('INVALID_TIMESTAMP', -1); + define('STOP_PROCESSING', 2); + + class CLI + { + protected static $ACCEPTED = array(); + var $params = array(); + + function __construct($options = array(), $handleUnknown = false) + { + global $argc, $argv; + + if (count($options)) + self::$ACCEPTED = $options; + + // Parse params + if ($argc > 1) + { + $paramSwitch = false; + for ($i = 1; $i < $argc; $i++) + { + $arg = $argv[$i]; + $isSwitch = preg_match('/^-+/', $arg); + + if ($isSwitch) + $arg = preg_replace('/^-+/', '', $arg); + + if ($paramSwitch and $isSwitch) + $this->error("[param] expected after '$paramSwitch' switch (" . self::$ACCEPTED[1][$paramSwitch] . ')'); + else if (!$paramSwitch and !$isSwitch) + { + if ($handleUnknown) + $this->params['unknown'][] = $arg; + else + $this->error("'$arg' is an invalid option, use --help to display valid switches."); + } + else if (!$paramSwitch and $isSwitch) + { + if (isset($this->params[$arg])) + $this->error("'$arg' switch can't occur more than once"); + + $this->params[$arg] = true; + if (isset(self::$ACCEPTED[1][$arg])) + $paramSwitch = $arg; + else if (!isset(self::$ACCEPTED[0][$arg])) + $this->error("there's no '$arg' switch, use --help to display all switches."); + } + else if ($paramSwitch and !$isSwitch) + { + $this->params[$paramSwitch] = $arg; + $paramSwitch = false; + } + } + } + + // Final check + foreach ($this->params as $k => $v) + if (isset(self::$ACCEPTED[1][$k]) and $v === true) + $this->error("[param] expected after '$k' switch (" . self::$ACCEPTED[1][$k] . ')'); + } + + function displayHelp() + { + LogInfo("You can use script with following switches:\n"); + foreach (self::$ACCEPTED[0] as $key => $value) + LogInfo(sprintf(" --%-17s %s", $key, $value)); + foreach (self::$ACCEPTED[1] as $key => $value) + LogInfo(sprintf(" --%-9s%-8s %s", $key, " [param]", $value)); + } + + function error($msg) + { + LogError($msg); + } + + function getParam($name) + { + if (isset($this->params[$name])) + return $this->params[$name]; + else + return false; + } + } + + class cURL + { + var $headers, $user_agent, $compression, $cookie_file; + var $active, $cert_check, $fragProxy, $maxSpeed, $proxy, $response; + var $mh, $ch, $mrc; + static $ref = 0; + + function __construct($cookies = true, $cookie = 'Cookies.txt', $compression = 'gzip', $proxy = '') + { + $this->headers = $this->headers(); + $this->user_agent = 'Mozilla/5.0 (Windows NT 5.1; rv:26.0) Gecko/20100101 Firefox/26.0'; + $this->compression = $compression; + $this->cookies = $cookies; + if ($this->cookies == true) + $this->cookie($cookie); + $this->cert_check = false; + $this->fragProxy = false; + $this->maxSpeed = 0; + $this->proxy = $proxy; + self::$ref++; + } + + function __destruct() + { + $this->stopDownloads(); + if ((self::$ref <= 1) and file_exists($this->cookie_file)) + unlink($this->cookie_file); + self::$ref--; + } + + function headers() + { + $headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; + $headers[] = 'Connection: Keep-Alive'; + return $headers; + } + + function cookie($cookie_file) + { + if (file_exists($cookie_file)) + $this->cookie_file = $cookie_file; + else + { + $file = fopen($cookie_file, 'w') or $this->error('The cookie file could not be opened. Make sure this directory has the correct permissions.'); + $this->cookie_file = $cookie_file; + fclose($file); + } + } + + function get($url) + { + $process = curl_init($url); + $options = array( + CURLOPT_HTTPHEADER => $this->headers, + CURLOPT_HEADER => 0, + CURLOPT_USERAGENT => $this->user_agent, + CURLOPT_ENCODING => $this->compression, + CURLOPT_TIMEOUT => 30, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FOLLOWLOCATION => 1 + ); + curl_setopt_array($process, $options); + if (!$this->cert_check) + curl_setopt($process, CURLOPT_SSL_VERIFYPEER, false); + if ($this->cookies == true) + { + curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file); + curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file); + } + if ($this->proxy) + $this->setProxy($process, $this->proxy); + $this->response = curl_exec($process); + if ($this->response !== false) + $status = curl_getinfo($process, CURLINFO_HTTP_CODE); + curl_close($process); + if (isset($status)) + return $status; + else + return false; + } + + function post($url, $data) + { + $process = curl_init($url); + $headers = $this->headers; + $headers[] = 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8'; + $options = array( + CURLOPT_HTTPHEADER => $headers, + CURLOPT_HEADER => 1, + CURLOPT_USERAGENT => $this->user_agent, + CURLOPT_ENCODING => $this->compression, + CURLOPT_TIMEOUT => 30, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FOLLOWLOCATION => 1, + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => $data + ); + curl_setopt_array($process, $options); + if (!$this->cert_check) + curl_setopt($process, CURLOPT_SSL_VERIFYPEER, false); + if ($this->cookies == true) + { + curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file); + curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file); + } + if ($this->proxy) + $this->setProxy($process, $this->proxy); + $return = curl_exec($process); + curl_close($process); + return $return; + } + + function setProxy(&$process, $proxy) + { + $type = ""; + $separator = strpos($proxy, "://"); + if ($separator !== false) + { + $type = strtolower(substr($proxy, 0, $separator)); + $proxy = substr($proxy, $separator + 3); + } + switch ($type) + { + case "socks4": + $type = CURLPROXY_SOCKS4; + break; + case "socks5": + $type = CURLPROXY_SOCKS5; + break; + default: + $type = CURLPROXY_HTTP; + } + curl_setopt($process, CURLOPT_PROXY, $proxy); + curl_setopt($process, CURLOPT_PROXYTYPE, $type); + } + + function addDownload($url, $id) + { + if (!isset($this->mh)) + $this->mh = curl_multi_init(); + if (isset($this->ch[$id])) + return false; + $download =& $this->ch[$id]; + $download['id'] = $id; + $download['url'] = $url; + $download['ch'] = curl_init($url); + $options = array( + CURLOPT_HTTPHEADER => $this->headers, + CURLOPT_HEADER => 0, + CURLOPT_USERAGENT => $this->user_agent, + CURLOPT_ENCODING => $this->compression, + CURLOPT_LOW_SPEED_LIMIT => 1024, + CURLOPT_LOW_SPEED_TIME => 10, + CURLOPT_BINARYTRANSFER => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FOLLOWLOCATION => 1 + ); + curl_setopt_array($download['ch'], $options); + if (!$this->cert_check) + curl_setopt($download['ch'], CURLOPT_SSL_VERIFYPEER, false); + if ($this->cookies == true) + { + curl_setopt($download['ch'], CURLOPT_COOKIEFILE, $this->cookie_file); + curl_setopt($download['ch'], CURLOPT_COOKIEJAR, $this->cookie_file); + } + if ($this->fragProxy and $this->proxy) + $this->setProxy($download['ch'], $this->proxy); + if ($this->maxSpeed > 0) + curl_setopt($download['ch'], CURLOPT_MAX_RECV_SPEED_LARGE, $this->maxSpeed); + curl_multi_add_handle($this->mh, $download['ch']); + do + { + $this->mrc = curl_multi_exec($this->mh, $this->active); + } while ($this->mrc == CURLM_CALL_MULTI_PERFORM); + return true; + } + + function checkDownloads() + { + if (isset($this->mh)) + { + curl_multi_select($this->mh); + $this->mrc = curl_multi_exec($this->mh, $this->active); + if ($this->mrc != CURLM_OK) + return false; + while ($info = curl_multi_info_read($this->mh)) + { + foreach ($this->ch as $download) + if ($download['ch'] == $info['handle']) + break; + $array['id'] = $download['id']; + $array['url'] = $download['url']; + $info = curl_getinfo($download['ch']); + if ($info['http_code'] == 0) + { + /* if curl fails due to network connectivity issues or some other reason it's * + * better to add some delay before next try to avoid busy loop. */ + LogDebug("Fragment " . $download['id'] . ": " . curl_error($download['ch'])); + usleep(1000000); + $array['status'] = false; + $array['response'] = ""; + } + else if ($info['http_code'] == 200) + { + if ($info['size_download'] >= $info['download_content_length']) + { + $array['status'] = $info['http_code']; + $array['response'] = curl_multi_getcontent($download['ch']); + } + else + { + $array['status'] = false; + $array['response'] = ""; + } + } + else + { + $array['status'] = $info['http_code']; + $array['response'] = curl_multi_getcontent($download['ch']); + } + $downloads[] = $array; + curl_multi_remove_handle($this->mh, $download['ch']); + curl_close($download['ch']); + unset($this->ch[$download['id']]); + } + if (isset($downloads) and (count($downloads) > 0)) + return $downloads; + } + return false; + } + + function stopDownloads() + { + if (isset($this->mh)) + { + if (isset($this->ch)) + { + foreach ($this->ch as $download) + { + curl_multi_remove_handle($this->mh, $download['ch']); + curl_close($download['ch']); + } + unset($this->ch); + } + curl_multi_close($this->mh); + unset($this->mh); + } + } + + function error($error) + { + LogError("cURL Error : $error"); + } + } + + class F4F + { + var $audio, $auth, $baseFilename, $baseTS, $bootstrapUrl, $baseUrl, $debug, $duration, $fileCount, $filesize, $fixWindow; + var $format, $live, $media, $metadata, $outDir, $outFile, $parallel, $play, $processed, $quality, $rename, $video; + var $prevTagSize, $tagHeaderLen; + var $segTable, $fragTable, $segNum, $fragNum, $frags, $fragCount, $lastFrag, $fragUrl, $discontinuity; + var $negTS, $prevAudioTS, $prevVideoTS, $pAudioTagLen, $pVideoTagLen, $pAudioTagPos, $pVideoTagPos; + var $prevAVC_Header, $prevAAC_Header, $AVC_HeaderWritten, $AAC_HeaderWritten; + + function __construct() + { + $this->auth = ""; + $this->baseFilename = ""; + $this->bootstrapUrl = ""; + $this->debug = false; + $this->duration = 0; + $this->fileCount = 1; + $this->fixWindow = 1000; + $this->format = ""; + $this->live = false; + $this->metadata = true; + $this->outDir = ""; + $this->outFile = ""; + $this->parallel = 8; + $this->play = false; + $this->processed = false; + $this->quality = "high"; + $this->rename = false; + $this->segTable = array(); + $this->fragTable = array(); + $this->segStart = false; + $this->fragStart = false; + $this->frags = array(); + $this->fragCount = 0; + $this->lastFrag = 0; + $this->discontinuity = ""; + $this->InitDecoder(); + } + + function InitDecoder() + { + $this->audio = false; + $this->filesize = 0; + $this->video = false; + $this->prevTagSize = 4; + $this->tagHeaderLen = 11; + $this->baseTS = INVALID_TIMESTAMP; + $this->negTS = INVALID_TIMESTAMP; + $this->prevAudioTS = INVALID_TIMESTAMP; + $this->prevVideoTS = INVALID_TIMESTAMP; + $this->pAudioTagLen = 0; + $this->pVideoTagLen = 0; + $this->pAudioTagPos = 0; + $this->pVideoTagPos = 0; + $this->prevAVC_Header = false; + $this->prevAAC_Header = false; + $this->AVC_HeaderWritten = false; + $this->AAC_HeaderWritten = false; + } + + function GetManifest(cURL $cc, $manifest) + { + $status = $cc->get($manifest); + if ($status == 403) + LogError("Access Denied! Unable to download the manifest."); + else if ($status != 200) + LogError("Unable to download the manifest"); + $xml = simplexml_load_string(trim($cc->response)); + if (!$xml) + LogError("Failed to load xml"); + $namespace = $xml->getDocNamespaces(); + $namespace = $namespace['']; + $xml->registerXPathNamespace("ns", $namespace); + return $xml; + } + + function ParseManifest($cc, $parentManifest) + { + LogInfo("Processing manifest info...."); + $xml = $this->GetManifest($cc, $parentManifest); + + // Extract baseUrl from manifest url + $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL"); + if (isset($baseUrl[0])) + $baseUrl = GetString($baseUrl[0]); + else + { + $baseUrl = $parentManifest; + if (strpos($baseUrl, '?') !== false) + $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?')); + $baseUrl = substr($baseUrl, 0, strrpos($baseUrl, '/')); + } + + $childManifests = array(); + $url = $xml->xpath("/ns:manifest/ns:media[@*]"); + if (isset($url[0]['href'])) + { + $count = 1; + foreach ($url as $childManifest) + { + if (isset($childManifest['bitrate'])) + $bitrate = floor(GetString($childManifest['bitrate'])); + else + $bitrate = $count++; + $entry =& $childManifests[$bitrate]; + $entry['bitrate'] = $bitrate; + $entry['url'] = AbsoluteUrl($baseUrl, GetString($childManifest['href'])); + $entry['xml'] = $this->GetManifest($cc, $entry['url']); + } + unset($entry, $childManifest); + } + else + { + $childManifests[0]['bitrate'] = 0; + $childManifests[0]['url'] = $parentManifest; + $childManifests[0]['xml'] = $xml; + } + + $count = 1; + foreach ($childManifests as $childManifest) + { + $xml = $childManifest['xml']; + + // Extract baseUrl from manifest url + $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL"); + if (isset($baseUrl[0])) + $baseUrl = GetString($baseUrl[0]); + else + { + $baseUrl = $childManifest['url']; + if (strpos($baseUrl, '?') !== false) + $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?')); + $baseUrl = substr($baseUrl, 0, strrpos($baseUrl, '/')); + } + + $streams = $xml->xpath("/ns:manifest/ns:media"); + foreach ($streams as $stream) + { + $array = array(); + foreach ($stream->attributes() as $k => $v) + $array[strtolower($k)] = GetString($v); + $array['metadata'] = GetString($stream->{'metadata'}); + $stream = $array; + + if (isset($stream['bitrate'])) + $bitrate = floor($stream['bitrate']); + else if ($childManifest['bitrate'] > 0) + $bitrate = $childManifest['bitrate']; + else + $bitrate = $count++; + while (isset($this->media[$bitrate])) + $bitrate++; + $streamId = isset($stream[strtolower('streamId')]) ? $stream[strtolower('streamId')] : ""; + $mediaEntry =& $this->media[$bitrate]; + + $mediaEntry['baseUrl'] = $baseUrl; + $mediaEntry['url'] = $stream['url']; + if (isRtmpUrl($mediaEntry['baseUrl']) or isRtmpUrl($mediaEntry['url'])) + LogError("Provided manifest is not a valid HDS manifest"); + + // Use embedded auth information when available + $idx = strpos($mediaEntry['url'], '?'); + if ($idx !== false) + { + $mediaEntry['queryString'] = substr($mediaEntry['url'], $idx); + $mediaEntry['url'] = substr($mediaEntry['url'], 0, $idx); + if (strlen($this->auth) != 0 and strcmp($this->auth, $mediaEntry['queryString']) != 0) + LogDebug("Manifest overrides 'auth': " . $mediaEntry['queryString']); + } + else + $mediaEntry['queryString'] = $this->auth; + + if (isset($stream[strtolower('bootstrapInfoId')])) + $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo[@id='" . $stream[strtolower('bootstrapInfoId')] . "']"); + else + $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo"); + if (isset($bootstrap[0]['url'])) + { + $mediaEntry['bootstrapUrl'] = AbsoluteUrl($mediaEntry['baseUrl'], GetString($bootstrap[0]['url'])); + if (strpos($mediaEntry['bootstrapUrl'], '?') === false) + $mediaEntry['bootstrapUrl'] .= $this->auth; + } + else + $mediaEntry['bootstrap'] = base64_decode(GetString($bootstrap[0])); + if (isset($stream['metadata'])) + $mediaEntry['metadata'] = base64_decode($stream['metadata']); + else + $mediaEntry['metadata'] = ""; + } + unset($mediaEntry, $childManifest); + } + + // Available qualities + $bitrates = array(); + if (!count($this->media)) + LogError("No media entry found"); + krsort($this->media, SORT_NUMERIC); + LogDebug("Manifest Entries:\n"); + LogDebug(sprintf(" %-8s%s", "Bitrate", "URL")); + for ($i = 0; $i < count($this->media); $i++) + { + $key = KeyName($this->media, $i); + $bitrates[] = $key; + LogDebug(sprintf(" %-8d%s", $key, $this->media[$key]['url'])); + } + LogDebug(""); + LogInfo("Quality Selection:\n Available: " . implode(' ', $bitrates)); + + // Quality selection + $key = $this->quality; + if (is_numeric($key) and isset($this->media[$key])) + $this->media = $this->media[$key]; + else + { + $this->quality = strtolower($this->quality); + switch ($this->quality) + { + case "low": + $this->quality = 2; + break; + case "medium": + $this->quality = 1; + break; + default: + $this->quality = 0; + } + while ($this->quality >= 0) + { + $key = KeyName($this->media, $this->quality); + if ($key !== NULL) + { + $this->media = $this->media[$key]; + break; + } + else + $this->quality -= 1; + } + } + LogInfo(" Selected : " . $key); + + // Parse initial bootstrap info + $this->baseUrl = $this->media['baseUrl']; + if (isset($this->media['bootstrapUrl'])) + { + $this->bootstrapUrl = $this->media['bootstrapUrl']; + $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl); + } + else + { + $bootstrapInfo = $this->media['bootstrap']; + ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); + if ($boxType == "abst") + $this->ParseBootstrapBox($bootstrapInfo, $pos); + else + LogError("Failed to parse bootstrap info"); + } + } + + function UpdateBootstrapInfo(cURL $cc, $bootstrapUrl) + { + $fragNum = $this->fragCount; + $retries = 0; + + // Backup original headers and add no-cache directive for fresh bootstrap info + $headers = $cc->headers; + $cc->headers[] = "Cache-Control: no-cache"; + $cc->headers[] = "Pragma: no-cache"; + + while (($fragNum == $this->fragCount) and ($retries < 30)) + { + $bootstrapPos = 0; + LogDebug("Updating bootstrap info, Available fragments: " . $this->fragCount); + $status = $cc->get($bootstrapUrl); + if ($status != 200) + LogError("Failed to refresh bootstrap info, Status: " . $status); + $bootstrapInfo = $cc->response; + ReadBoxHeader($bootstrapInfo, $bootstrapPos, $boxType, $boxSize); + if ($boxType == "abst") + $this->ParseBootstrapBox($bootstrapInfo, $bootstrapPos); + else + LogError("Failed to parse bootstrap info"); + LogDebug("Update complete, Available fragments: " . $this->fragCount); + if ($fragNum == $this->fragCount) + { + LogInfo("Updating bootstrap info, Retries: " . ++$retries, true); + usleep(4000000); + } + } + + // Restore original headers + $cc->headers = $headers; + } + + function ParseBootstrapBox($bootstrapInfo, $pos) + { + $version = ReadByte($bootstrapInfo, $pos); + $flags = ReadInt24($bootstrapInfo, $pos + 1); + $bootstrapVersion = ReadInt32($bootstrapInfo, $pos + 4); + $byte = ReadByte($bootstrapInfo, $pos + 8); + $profile = ($byte & 0xC0) >> 6; + if (($byte & 0x20) >> 5) + { + $this->live = true; + $this->metadata = false; + } + $update = ($byte & 0x10) >> 4; + if (!$update) + { + $this->segTable = array(); + $this->fragTable = array(); + } + $timescale = ReadInt32($bootstrapInfo, $pos + 9); + $currentMediaTime = ReadInt64($bootstrapInfo, $pos + 13); + $smpteTimeCodeOffset = ReadInt64($bootstrapInfo, $pos + 21); + $pos += 29; + $movieIdentifier = ReadString($bootstrapInfo, $pos); + $serverEntryCount = ReadByte($bootstrapInfo, $pos++); + for ($i = 0; $i < $serverEntryCount; $i++) + $serverEntryTable[$i] = ReadString($bootstrapInfo, $pos); + $qualityEntryCount = ReadByte($bootstrapInfo, $pos++); + for ($i = 0; $i < $qualityEntryCount; $i++) + $qualityEntryTable[$i] = ReadString($bootstrapInfo, $pos); + $drmData = ReadString($bootstrapInfo, $pos); + $metadata = ReadString($bootstrapInfo, $pos); + $segRunTableCount = ReadByte($bootstrapInfo, $pos++); + $segTable = array(); + LogDebug(sprintf("%s:", "Segment Tables")); + for ($i = 0; $i < $segRunTableCount; $i++) + { + LogDebug(sprintf("\nTable %d:", $i + 1)); + ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); + if ($boxType == "asrt") + $segTable[$i] = $this->ParseAsrtBox($bootstrapInfo, $pos); + $pos += $boxSize; + } + $fragRunTableCount = ReadByte($bootstrapInfo, $pos++); + $fragTable = array(); + LogDebug(sprintf("%s:", "Fragment Tables")); + for ($i = 0; $i < $fragRunTableCount; $i++) + { + LogDebug(sprintf("\nTable %d:", $i + 1)); + ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); + if ($boxType == "afrt") + $fragTable[$i] = $this->ParseAfrtBox($bootstrapInfo, $pos); + $pos += $boxSize; + } + $this->segTable = array_replace($this->segTable, $segTable[0]); + $this->fragTable = array_replace($this->fragTable, $fragTable[0]); + $this->ParseSegAndFragTable(); + } + + function ParseAsrtBox($asrt, $pos) + { + $segTable = array(); + $version = ReadByte($asrt, $pos); + $flags = ReadInt24($asrt, $pos + 1); + $qualityEntryCount = ReadByte($asrt, $pos + 4); + $pos += 5; + for ($i = 0; $i < $qualityEntryCount; $i++) + $qualitySegmentUrlModifiers[$i] = ReadString($asrt, $pos); + $segCount = ReadInt32($asrt, $pos); + $pos += 4; + LogDebug(sprintf(" %-8s%-10s", "Number", "Fragments")); + for ($i = 0; $i < $segCount; $i++) + { + $firstSegment = ReadInt32($asrt, $pos); + $segEntry =& $segTable[$firstSegment]; + $segEntry['firstSegment'] = $firstSegment; + $segEntry['fragmentsPerSegment'] = ReadInt32($asrt, $pos + 4); + if ($segEntry['fragmentsPerSegment'] & 0x80000000) + $segEntry['fragmentsPerSegment'] = 0; + $pos += 8; + } + unset($segEntry); + foreach ($segTable as $segEntry) + LogDebug(sprintf(" %-8s%-10s", $segEntry['firstSegment'], $segEntry['fragmentsPerSegment'])); + LogDebug(""); + return $segTable; + } + + function ParseAfrtBox($afrt, $pos) + { + $fragTable = array(); + $version = ReadByte($afrt, $pos); + $flags = ReadInt24($afrt, $pos + 1); + $timescale = ReadInt32($afrt, $pos + 4); + $qualityEntryCount = ReadByte($afrt, $pos + 8); + $pos += 9; + for ($i = 0; $i < $qualityEntryCount; $i++) + $qualitySegmentUrlModifiers[$i] = ReadString($afrt, $pos); + $fragEntries = ReadInt32($afrt, $pos); + $pos += 4; + LogDebug(sprintf(" %-12s%-16s%-16s%-16s", "Number", "Timestamp", "Duration", "Discontinuity")); + for ($i = 0; $i < $fragEntries; $i++) + { + $firstFragment = ReadInt32($afrt, $pos); + $fragEntry =& $fragTable[$firstFragment]; + $fragEntry['firstFragment'] = $firstFragment; + $fragEntry['firstFragmentTimestamp'] = ReadInt64($afrt, $pos + 4); + $fragEntry['fragmentDuration'] = ReadInt32($afrt, $pos + 12); + $fragEntry['discontinuityIndicator'] = ""; + $pos += 16; + if ($fragEntry['fragmentDuration'] == 0) + $fragEntry['discontinuityIndicator'] = ReadByte($afrt, $pos++); + } + unset($fragEntry); + foreach ($fragTable as $fragEntry) + LogDebug(sprintf(" %-12s%-16s%-16s%-16s", $fragEntry['firstFragment'], $fragEntry['firstFragmentTimestamp'], $fragEntry['fragmentDuration'], $fragEntry['discontinuityIndicator'])); + LogDebug(""); + return $fragTable; + } + + function ParseSegAndFragTable() + { + $firstSegment = reset($this->segTable); + $lastSegment = end($this->segTable); + $firstFragment = reset($this->fragTable); + $lastFragment = end($this->fragTable); + + // Check if live stream is still live + if (($lastFragment['fragmentDuration'] == 0) and ($lastFragment['discontinuityIndicator'] == 0)) + { + $this->live = false; + array_pop($this->fragTable); + $lastFragment = end($this->fragTable); + } + + // Count total fragments by adding all entries in compactly coded segment table + $invalidFragCount = false; + $prev = reset($this->segTable); + $this->fragCount = $prev['fragmentsPerSegment']; + while ($current = next($this->segTable)) + { + $this->fragCount += ($current['firstSegment'] - $prev['firstSegment'] - 1) * $prev['fragmentsPerSegment']; + $this->fragCount += $current['fragmentsPerSegment']; + $prev = $current; + } + if (!($this->fragCount & 0x80000000)) + $this->fragCount += $firstFragment['firstFragment'] - 1; + if ($this->fragCount & 0x80000000) + { + $this->fragCount = 0; + $invalidFragCount = true; + } + if ($this->fragCount < $lastFragment['firstFragment']) + $this->fragCount = $lastFragment['firstFragment']; + + // Determine starting segment and fragment + if ($this->segStart === false) + { + if ($this->live) + $this->segStart = $lastSegment['firstSegment']; + else + $this->segStart = $firstSegment['firstSegment']; + if ($this->segStart < 1) + $this->segStart = 1; + } + if ($this->fragStart === false) + { + if ($this->live and !$invalidFragCount) + $this->fragStart = $this->fragCount - 2; + else + $this->fragStart = $firstFragment['firstFragment'] - 1; + if ($this->fragStart < 0) + $this->fragStart = 0; + } + } + + function GetSegmentFromFragment($fragNum) + { + $firstSegment = reset($this->segTable); + $lastSegment = end($this->segTable); + $firstFragment = reset($this->fragTable); + $lastFragment = end($this->fragTable); + + if (count($this->segTable) == 1) + return $firstSegment['firstSegment']; + else + { + $prev = $firstSegment['firstSegment']; + $start = $firstFragment['firstFragment']; + for ($i = $firstSegment['firstSegment']; $i <= $lastSegment['firstSegment']; $i++) + { + if (isset($this->segTable[$i])) + $seg = $this->segTable[$i]; + else + $seg = $prev; + $end = $start + $seg['fragmentsPerSegment']; + if (($fragNum >= $start) and ($fragNum < $end)) + return $i; + $prev = $seg; + $start = $end; + } + } + return $lastSegment['firstSegment']; + } + + function DownloadFragments(cURL $cc, $manifest, $opt = array()) + { + $start = 0; + extract($opt, EXTR_IF_EXISTS); + + $this->ParseManifest($cc, $manifest); + $segNum = $this->segStart; + $fragNum = $this->fragStart; + if ($start) + { + $segNum = $this->GetSegmentFromFragment($start); + $fragNum = $start - 1; + $this->segStart = $segNum; + $this->fragStart = $fragNum; + } + $this->lastFrag = $fragNum; + $opt['cc'] = $cc; + $opt['duration'] = 0; + $firstFragment = reset($this->fragTable); + LogInfo(sprintf("Fragments Total: %s, First: %s, Start: %s, Parallel: %s", $this->fragCount, $firstFragment['firstFragment'], $fragNum + 1, $this->parallel)); + + // Extract baseFilename + $this->baseFilename = $this->media['url']; + if (substr($this->baseFilename, -1) == '/') + $this->baseFilename = substr($this->baseFilename, 0, -1); + $this->baseFilename = RemoveExtension($this->baseFilename); + $lastSlash = strrpos($this->baseFilename, '/'); + if ($lastSlash !== false) + $this->baseFilename = substr($this->baseFilename, $lastSlash + 1); + if (strpos($manifest, '?')) + $manifestHash = md5(substr($manifest, 0, strpos($manifest, '?'))); + else + $manifestHash = md5($manifest); + if (strlen($this->baseFilename) > 32) + $this->baseFilename = md5($this->baseFilename); + $this->baseFilename = $manifestHash . "_" . $this->baseFilename . "_Seg" . $segNum . "-Frag"; + + if ($fragNum >= $this->fragCount) + LogError("No fragment available for downloading"); + + $this->fragUrl = AbsoluteUrl($this->baseUrl, $this->media['url']); + LogDebug("Base Fragment Url:\n" . $this->fragUrl . "\n"); + LogDebug("Downloading Fragments:\n"); + + while (($fragNum < $this->fragCount) or $cc->active) + { + while ((count($cc->ch) < $this->parallel) and ($fragNum < $this->fragCount)) + { + $frag = array(); + $fragNum = $fragNum + 1; + $frag['id'] = $fragNum; + LogInfo("Downloading $fragNum/$this->fragCount fragments", true); + if (in_array_field($fragNum, "firstFragment", $this->fragTable, true)) + $this->discontinuity = value_in_array_field($fragNum, "firstFragment", "discontinuityIndicator", $this->fragTable, true); + else + { + $closest = reset($this->fragTable); + $closest = $closest['firstFragment']; + while ($current = next($this->fragTable)) + { + if ($current['firstFragment'] < $fragNum) + $closest = $current['firstFragment']; + else + break; + } + $this->discontinuity = value_in_array_field($closest, "firstFragment", "discontinuityIndicator", $this->fragTable, true); + } + if ($this->discontinuity !== "") + { + LogDebug("Skipping fragment $fragNum due to discontinuity, Type: " . $this->discontinuity); + $frag['response'] = false; + $this->rename = true; + } + else if (file_exists($this->baseFilename . $fragNum)) + { + LogDebug("Fragment $fragNum is already downloaded"); + $frag['response'] = file_get_contents($this->baseFilename . $fragNum); + } + if (isset($frag['response'])) + { + if ($this->WriteFragment($frag, $opt) === STOP_PROCESSING) + break 2; + else + continue; + } + + LogDebug("Adding fragment $fragNum to download queue"); + $segNum = $this->GetSegmentFromFragment($fragNum); + $cc->addDownload($this->fragUrl . "Seg" . $segNum . "-Frag" . $fragNum . $this->media['queryString'], $fragNum); + } + + $downloads = $cc->checkDownloads(); + if ($downloads !== false) + { + for ($i = 0; $i < count($downloads); $i++) + { + $frag = array(); + $download = $downloads[$i]; + $frag['id'] = $download['id']; + if ($download['status'] == 200) + { + if ($this->VerifyFragment($download['response'])) + { + LogDebug("Fragment " . $this->baseFilename . $download['id'] . " successfully downloaded"); + if (!($this->live or $this->play)) + file_put_contents($this->baseFilename . $download['id'], $download['response']); + $frag['response'] = $download['response']; + } + else + { + LogDebug("Fragment " . $download['id'] . " failed to verify"); + LogDebug("Adding fragment " . $download['id'] . " to download queue"); + $cc->addDownload($download['url'], $download['id']); + } + } + else if ($download['status'] === false) + { + LogDebug("Fragment " . $download['id'] . " failed to download"); + LogDebug("Adding fragment " . $download['id'] . " to download queue"); + $cc->addDownload($download['url'], $download['id']); + } + else if ($download['status'] == 403) + LogError("Access Denied! Unable to download fragments."); + else if ($download['status'] == 503) + { + LogDebug("Fragment " . $download['id'] . " seems temporary unavailable"); + LogDebug("Adding fragment " . $download['id'] . " to download queue"); + $cc->addDownload($download['url'], $download['id']); + } + else + { + LogDebug("Fragment " . $download['id'] . " doesn't exist, Status: " . $download['status']); + $frag['response'] = false; + $this->rename = true; + + /* Resync with latest available fragment when we are left behind due to slow * + * connection and short live window on streaming server. make sure to reset * + * the last written fragment. */ + if ($this->live and ($fragNum >= $this->fragCount) and ($i + 1 == count($downloads)) and !$cc->active) + { + LogDebug("Trying to resync with latest available fragment"); + if ($this->WriteFragment($frag, $opt) === STOP_PROCESSING) + break 2; + unset($frag['response']); + $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl); + $fragNum = $this->fragCount - 1; + $this->lastFrag = $fragNum; + } + } + if (isset($frag['response'])) + if ($this->WriteFragment($frag, $opt) === STOP_PROCESSING) + break 2; + } + unset($downloads, $download); + } + if ($this->live and ($fragNum >= $this->fragCount) and !$cc->active) + $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl); + } + + LogInfo(""); + LogDebug("\nAll fragments downloaded successfully\n"); + $cc->stopDownloads(); + $this->processed = true; + } + + function VerifyFragment(&$frag) + { + $fragPos = 0; + $fragLen = strlen($frag); + + /* Some moronic servers add wrong boxSize in header causing fragment verification * + * to fail so we have to fix the boxSize before processing the fragment. */ + while ($fragPos < $fragLen) + { + ReadBoxHeader($frag, $fragPos, $boxType, $boxSize); + if ($boxType == "mdat") + { + $len = strlen(substr($frag, $fragPos, $boxSize)); + if ($boxSize and ($len == $boxSize)) + return true; + else + { + $boxSize = $fragLen - $fragPos; + WriteBoxSize($frag, $fragPos, $boxType, $boxSize); + return true; + } + } + $fragPos += $boxSize; + } + return false; + } + + function DecodeFragment($frag, $fragNum, $opt = array()) + { + $debug = $this->debug; + $flv = false; + $test = false; + extract($opt, EXTR_IF_EXISTS); + if ($test) + $debug = false; + + $flvData = ""; + $fragPos = 0; + $packetTS = 0; + $fragLen = strlen($frag); + + if (!$this->VerifyFragment($frag)) + { + LogInfo("Skipping fragment number $fragNum"); + return false; + } + + while ($fragPos < $fragLen) + { + ReadBoxHeader($frag, $fragPos, $boxType, $boxSize); + if ($boxType == "mdat") + { + $fragLen = $fragPos + $boxSize; + break; + } + $fragPos += $boxSize; + } + + LogDebug(sprintf("\nFragment %d:\n" . $this->format . "%-16s", $fragNum, "Type", "CurrentTS", "PreviousTS", "Size", "Position"), $debug); + while ($fragPos < $fragLen) + { + $packetType = ReadByte($frag, $fragPos); + $packetSize = ReadInt24($frag, $fragPos + 1); + $packetTS = ReadInt24($frag, $fragPos + 4); + $packetTS = $packetTS | (ReadByte($frag, $fragPos + 7) << 24); + if ($packetTS & 0x80000000) + $packetTS &= 0x7FFFFFFF; + $totalTagLen = $this->tagHeaderLen + $packetSize + $this->prevTagSize; + + // Try to fix the odd timestamps and make them zero based + $currentTS = $packetTS; + $lastTS = $this->prevVideoTS >= $this->prevAudioTS ? $this->prevVideoTS : $this->prevAudioTS; + $fixedTS = $lastTS + FRAMEFIX_STEP; + if (($this->baseTS == INVALID_TIMESTAMP) and (($packetType == AUDIO) or ($packetType == VIDEO))) + $this->baseTS = $packetTS; + if (($this->baseTS > 1000) and ($packetTS >= $this->baseTS)) + $packetTS -= $this->baseTS; + if ($lastTS != INVALID_TIMESTAMP) + { + $timeShift = $packetTS - $lastTS; + if ($timeShift > $this->fixWindow) + { + LogDebug("Timestamp gap detected: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " Timeshift=" . $timeShift, $debug); + if ($this->baseTS < $packetTS) + $this->baseTS += $timeShift - FRAMEFIX_STEP; + else + $this->baseTS = $timeShift - FRAMEFIX_STEP; + $packetTS = $fixedTS; + } + else + { + $lastTS = $packetType == VIDEO ? $this->prevVideoTS : $this->prevAudioTS; + if ($packetTS < ($lastTS - $this->fixWindow)) + { + if (($this->negTS != INVALID_TIMESTAMP) and (($packetTS + $this->negTS) < ($lastTS - $this->fixWindow))) + $this->negTS = INVALID_TIMESTAMP; + if ($this->negTS == INVALID_TIMESTAMP) + { + $this->negTS = $fixedTS - $packetTS; + LogDebug("Negative timestamp detected: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " NegativeTS=" . $this->negTS, $debug); + $packetTS = $fixedTS; + } + else + { + if (($packetTS + $this->negTS) <= ($lastTS + $this->fixWindow)) + $packetTS += $this->negTS; + else + { + $this->negTS = $fixedTS - $packetTS; + LogDebug("Negative timestamp override: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " NegativeTS=" . $this->negTS, $debug); + $packetTS = $fixedTS; + } + } + } + } + } + if ($packetTS != $currentTS) + WriteFlvTimestamp($frag, $fragPos, $packetTS); + + switch ($packetType) + { + case AUDIO: + if ($packetTS > $this->prevAudioTS - $this->fixWindow) + { + $FrameInfo = ReadByte($frag, $fragPos + $this->tagHeaderLen); + $CodecID = ($FrameInfo & 0xF0) >> 4; + if ($CodecID == CODEC_ID_AAC) + { + $AAC_PacketType = ReadByte($frag, $fragPos + $this->tagHeaderLen + 1); + if ($AAC_PacketType == AAC_SEQUENCE_HEADER) + { + if ($this->AAC_HeaderWritten) + { + LogDebug(sprintf("%s\n" . $this->format, "Skipping AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); + break; + } + else + { + LogDebug("Writing AAC sequence header", $debug); + $this->AAC_HeaderWritten = true; + } + } + else if (!$this->AAC_HeaderWritten) + { + LogDebug(sprintf("%s\n" . $this->format, "Discarding audio packet received before AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); + break; + } + } + if ($packetSize > 0) + { + // Check for packets with non-monotonic audio timestamps and fix them + if (!(($CodecID == CODEC_ID_AAC) and (($AAC_PacketType == AAC_SEQUENCE_HEADER) or $this->prevAAC_Header))) + if (($this->prevAudioTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevAudioTS)) + { + LogDebug(sprintf("%s\n" . $this->format, "Fixing audio timestamp", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); + $packetTS += (FRAMEFIX_STEP / 5) + ($this->prevAudioTS - $packetTS); + WriteFlvTimestamp($frag, $fragPos, $packetTS); + } + if (is_resource($flv)) + { + $this->pAudioTagPos = ftell($flv); + $status = fwrite($flv, substr($frag, $fragPos, $totalTagLen), $totalTagLen); + if (!$status) + LogError("Failed to write flv data to file"); + if ($debug) + LogDebug(sprintf($this->format . "%-16s", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize, $this->pAudioTagPos)); + } + else + { + $flvData .= substr($frag, $fragPos, $totalTagLen); + if ($debug) + LogDebug(sprintf($this->format, "AUDIO", $packetTS, $this->prevAudioTS, $packetSize)); + } + if (($CodecID == CODEC_ID_AAC) and ($AAC_PacketType == AAC_SEQUENCE_HEADER)) + $this->prevAAC_Header = true; + else + $this->prevAAC_Header = false; + $this->prevAudioTS = $packetTS; + $this->pAudioTagLen = $totalTagLen; + } + else + LogDebug(sprintf("%s\n" . $this->format, "Skipping small sized audio packet", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); + } + else + LogDebug(sprintf("%s\n" . $this->format, "Skipping audio packet in fragment $fragNum", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); + if (!$this->audio) + $this->audio = true; + break; + case VIDEO: + if ($packetTS > $this->prevVideoTS - $this->fixWindow) + { + $FrameInfo = ReadByte($frag, $fragPos + $this->tagHeaderLen); + $FrameType = ($FrameInfo & 0xF0) >> 4; + $CodecID = $FrameInfo & 0x0F; + if ($FrameType == FRAME_TYPE_INFO) + { + LogDebug(sprintf("%s\n" . $this->format, "Skipping video info frame", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); + break; + } + if ($CodecID == CODEC_ID_AVC) + { + $AVC_PacketType = ReadByte($frag, $fragPos + $this->tagHeaderLen + 1); + if ($AVC_PacketType == AVC_SEQUENCE_HEADER) + { + if ($this->AVC_HeaderWritten) + { + LogDebug(sprintf("%s\n" . $this->format, "Skipping AVC sequence header", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); + break; + } + else + { + LogDebug("Writing AVC sequence header", $debug); + $this->AVC_HeaderWritten = true; + } + } + else if (!$this->AVC_HeaderWritten) + { + LogDebug(sprintf("%s\n" . $this->format, "Discarding video packet received before AVC sequence header", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); + break; + } + } + if ($packetSize > 0) + { + $pts = $packetTS; + if (($CodecID == CODEC_ID_AVC) and ($AVC_PacketType == AVC_NALU)) + { + $cts = ReadInt24($frag, $fragPos + $this->tagHeaderLen + 2); + $cts = ($cts + 0xff800000) ^ 0xff800000; + $pts = $packetTS + $cts; + if ($cts != 0) + LogDebug("DTS: $packetTS CTS: $cts PTS: $pts", $debug); + } + + // Check for packets with non-monotonic video timestamps and fix them + if (!(($CodecID == CODEC_ID_AVC) and (($AVC_PacketType == AVC_SEQUENCE_HEADER) or ($AVC_PacketType == AVC_SEQUENCE_END) or $this->prevAVC_Header))) + if (($this->prevVideoTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevVideoTS)) + { + LogDebug(sprintf("%s\n" . $this->format, "Fixing video timestamp", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); + $packetTS += (FRAMEFIX_STEP / 5) + ($this->prevVideoTS - $packetTS); + WriteFlvTimestamp($frag, $fragPos, $packetTS); + } + if (is_resource($flv)) + { + $this->pVideoTagPos = ftell($flv); + $status = fwrite($flv, substr($frag, $fragPos, $totalTagLen), $totalTagLen); + if (!$status) + LogError("Failed to write flv data to file"); + if ($debug) + LogDebug(sprintf($this->format . "%-16s", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize, $this->pVideoTagPos)); + } + else + { + $flvData .= substr($frag, $fragPos, $totalTagLen); + if ($debug) + LogDebug(sprintf($this->format, "VIDEO", $packetTS, $this->prevVideoTS, $packetSize)); + } + if (($CodecID == CODEC_ID_AVC) and ($AVC_PacketType == AVC_SEQUENCE_HEADER)) + $this->prevAVC_Header = true; + else + $this->prevAVC_Header = false; + $this->prevVideoTS = $packetTS; + $this->pVideoTagLen = $totalTagLen; + } + else + LogDebug(sprintf("%s\n" . $this->format, "Skipping small sized video packet", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); + } + else + LogDebug(sprintf("%s\n" . $this->format, "Skipping video packet in fragment $fragNum", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); + if (!$this->video) + $this->video = true; + break; + case SCRIPT_DATA: + break; + default: + if (($packetType == 10) or ($packetType == 11)) + LogError("This stream is encrypted with Akamai DRM. Decryption of such streams isn't currently possible with this script.", 2); + else if (($packetType == 40) or ($packetType == 41)) + LogError("This stream is encrypted with FlashAccess DRM. Decryption of such streams isn't currently possible with this script.", 2); + else + { + LogInfo("Unknown packet type " . $packetType . " encountered! Unable to process fragment $fragNum"); + break 2; + } + } + $fragPos += $totalTagLen; + } + $this->duration = round($packetTS / 1000, 0); + if (is_resource($flv)) + { + $this->filesize = ftell($flv) / (1024 * 1024); + return true; + } + else + return $flvData; + } + + function WriteFragment($download, &$opt) + { + $this->frags[$download['id']] = $download; + + $available = count($this->frags); + for ($i = 0; $i < $available; $i++) + { + if (isset($this->frags[$this->lastFrag + 1])) + { + $frag = $this->frags[$this->lastFrag + 1]; + if ($frag['response'] !== false) + { + LogDebug("Writing fragment " . $frag['id'] . " to flv file"); + if (!isset($opt['file'])) + { + $opt['test'] = true; + if ($this->play) + $outFile = STDOUT; + else if ($this->outFile) + { + if ($opt['filesize']) + $outFile = JoinUrl($this->outDir, $this->outFile . '-' . $this->fileCount++ . ".flv"); + else + $outFile = JoinUrl($this->outDir, $this->outFile . ".flv"); + } + else + { + if ($opt['filesize']) + $outFile = JoinUrl($this->outDir, $this->baseFilename . '-' . $this->fileCount++ . ".flv"); + else + $outFile = JoinUrl($this->outDir, $this->baseFilename . ".flv"); + } + $this->InitDecoder(); + $this->DecodeFragment($frag['response'], $frag['id'], $opt); + $opt['file'] = WriteFlvFile($outFile, $this->audio, $this->video); + if ($this->metadata) + WriteMetadata($this, $opt['file']); + + $opt['test'] = false; + $this->InitDecoder(); + } + $flvData = $this->DecodeFragment($frag['response'], $frag['id'], $opt); + if (strlen($flvData)) + { + $status = fwrite($opt['file'], $flvData, strlen($flvData)); + if (!$status) + LogError("Failed to write flv data"); + if (!$this->play) + $this->filesize = ftell($opt['file']) / (1024 * 1024); + } + $this->lastFrag = $frag['id']; + } + else + { + $this->lastFrag += 1; + LogDebug("Skipping failed fragment " . $this->lastFrag); + } + unset($this->frags[$this->lastFrag]); + } + else + break; + + if ($opt['tDuration'] and (($opt['duration'] + $this->duration) >= $opt['tDuration'])) + { + LogInfo(""); + LogInfo(($opt['duration'] + $this->duration) . " seconds of content has been recorded successfully.", true); + return STOP_PROCESSING; + } + if ($opt['filesize'] and ($this->filesize >= $opt['filesize'])) + { + $this->filesize = 0; + $opt['duration'] += $this->duration; + fclose($opt['file']); + unset($opt['file']); + } + } + + if (!count($this->frags)) + unset($this->frags); + return true; + } + } + + function ReadByte($str, $pos) + { + $int = unpack('C', $str[$pos]); + return $int[1]; + } + + function ReadInt16($str, $pos) + { + $int32 = unpack('N', "\x00\x00" . substr($str, $pos, 2)); + return $int32[1]; + } + + function ReadInt24($str, $pos) + { + $int32 = unpack('N', "\x00" . substr($str, $pos, 3)); + return $int32[1]; + } + + function ReadInt32($str, $pos) + { + $int32 = unpack('N', substr($str, $pos, 4)); + return $int32[1]; + } + + function ReadInt64($str, $pos) + { + $hi = sprintf("%u", ReadInt32($str, $pos)); + $lo = sprintf("%u", ReadInt32($str, $pos + 4)); + $int64 = bcadd(bcmul($hi, "4294967296"), $lo); + return $int64; + } + + function ReadDouble($str, $pos) + { + $double = unpack('d', strrev(substr($str, $pos, 8))); + return $double[1]; + } + + function ReadString($str, &$pos) + { + $len = 0; + while ($str[$pos + $len] != "\x00") + $len++; + $str = substr($str, $pos, $len); + $pos += $len + 1; + return $str; + } + + function ReadBoxHeader($str, &$pos, &$boxType, &$boxSize) + { + if (!isset($pos)) + $pos = 0; + $boxSize = ReadInt32($str, $pos); + $boxType = substr($str, $pos + 4, 4); + if ($boxSize == 1) + { + $boxSize = ReadInt64($str, $pos + 8) - 16; + $pos += 16; + } + else + { + $boxSize -= 8; + $pos += 8; + } + if ($boxSize <= 0) + $boxSize = 0; + } + + function WriteByte(&$str, $pos, $int) + { + $str[$pos] = pack('C', $int); + } + + function WriteInt24(&$str, $pos, $int) + { + $str[$pos] = pack('C', ($int & 0xFF0000) >> 16); + $str[$pos + 1] = pack('C', ($int & 0xFF00) >> 8); + $str[$pos + 2] = pack('C', $int & 0xFF); + } + + function WriteInt32(&$str, $pos, $int) + { + $str[$pos] = pack('C', ($int & 0xFF000000) >> 24); + $str[$pos + 1] = pack('C', ($int & 0xFF0000) >> 16); + $str[$pos + 2] = pack('C', ($int & 0xFF00) >> 8); + $str[$pos + 3] = pack('C', $int & 0xFF); + } + + function WriteBoxSize(&$str, $pos, $type, $size) + { + if (substr($str, $pos - 4, 4) == $type) + WriteInt32($str, $pos - 8, $size); + else + { + WriteInt32($str, $pos - 8, 0); + WriteInt32($str, $pos - 4, $size); + } + } + + function WriteFlvTimestamp(&$frag, $fragPos, $packetTS) + { + WriteInt24($frag, $fragPos + 4, ($packetTS & 0x00FFFFFF)); + WriteByte($frag, $fragPos + 7, ($packetTS & 0xFF000000) >> 24); + } + + function AbsoluteUrl($baseUrl, $url) + { + if (!isHttpUrl($url)) + $url = JoinUrl($baseUrl, $url); + return NormalizePath($url); + } + + function GetString($object) + { + return trim(strval($object)); + } + + function isHttpUrl($url) + { + return (strncasecmp($url, "http", 4) == 0) ? true : false; + } + + function isRtmpUrl($url) + { + return (preg_match('/^rtm(p|pe|pt|pte|ps|pts|fp):\/\//i', $url)) ? true : false; + } + + function JoinUrl($firstUrl, $secondUrl) + { + if ($firstUrl and $secondUrl) + { + if (substr($firstUrl, -1) == '/') + $firstUrl = substr($firstUrl, 0, -1); + if (substr($secondUrl, 0, 1) == '/') + $secondUrl = substr($secondUrl, 1); + return $firstUrl . '/' . $secondUrl; + } + else if ($firstUrl) + return $firstUrl; + else + return $secondUrl; + } + + function KeyName(array $a, $pos) + { + $temp = array_slice($a, $pos, 1, true); + return key($temp); + } + + function LogDebug($msg, $display = true) + { + global $debug, $showHeader; + if ($showHeader) + { + ShowHeader(); + $showHeader = false; + } + if ($display and $debug) + fwrite(STDERR, $msg . "\n"); + } + + function LogError($msg, $code = 1) + { + LogInfo($msg); + exit($code); + } + + function LogInfo($msg, $progress = false) + { + global $quiet, $showHeader; + if ($showHeader) + { + ShowHeader(); + $showHeader = false; + } + if (!$quiet) + PrintLine($msg, $progress); + } + + function NormalizePath($path) + { + $inSegs = preg_split('/(?<!\/)\/(?!\/)/u', $path); + $outSegs = array(); + + foreach ($inSegs as $seg) + { + if ($seg == '' or $seg == '.') + continue; + if ($seg == '..') + array_pop($outSegs); + else + array_push($outSegs, $seg); + } + $outPath = implode('/', $outSegs); + + if (substr($path, 0, 1) == '/') + $outPath = '/' . $outPath; + if (substr($path, -1) == '/') + $outPath .= '/'; + return $outPath; + } + + function PrintLine($msg, $progress = false) + { + if ($msg) + { + printf("\r%-79s\r", ""); + if ($progress) + printf("%s\r", $msg); + else + printf("%s\n", $msg); + } + else + printf("\n"); + } + + function RemoveExtension($outFile) + { + preg_match("/\.\w{1,4}$/i", $outFile, $extension); + if (isset($extension[0])) + { + $extension = $extension[0]; + $outFile = substr($outFile, 0, -strlen($extension)); + return $outFile; + } + return $outFile; + } + + function RenameFragments($baseFilename, $fragNum, $fileExt) + { + $files = array(); + $retries = 0; + + while (true) + { + if ($retries >= 50) + break; + $file = $baseFilename . ++$fragNum; + if (file_exists($file)) + { + $files[] = $file; + $retries = 0; + } + else if (file_exists($file . $fileExt)) + { + $files[] = $file . $fileExt; + $retries = 0; + } + else + $retries++; + } + + $fragCount = count($files); + natsort($files); + for ($i = 0; $i < $fragCount; $i++) + rename($files[$i], $baseFilename . ($i + 1)); + } + + function ShowHeader() + { + $header = "KSV Adobe HDS Downloader"; + $len = strlen($header); + $width = floor((80 - $len) / 2) + $len; + $format = "\n%" . $width . "s\n\n"; + printf($format, $header); + } + + function WriteFlvFile($outFile, $audio = true, $video = true) + { + $flvHeader = pack("H*", "464c5601050000000900000000"); + $flvHeaderLen = strlen($flvHeader); + + // Set proper Audio/Video marker + WriteByte($flvHeader, 4, $audio << 2 | $video); + + if (is_resource($outFile)) + $flv = $outFile; + else + $flv = fopen($outFile, "w+b"); + if (!$flv) + LogError("Failed to open " . $outFile); + fwrite($flv, $flvHeader, $flvHeaderLen); + return $flv; + } + + function WriteMetadata($f4f, $flv) + { + if (isset($f4f->media) and $f4f->media['metadata']) + { + $metadataSize = strlen($f4f->media['metadata']); + WriteByte($metadata, 0, SCRIPT_DATA); + WriteInt24($metadata, 1, $metadataSize); + WriteInt24($metadata, 4, 0); + WriteInt32($metadata, 7, 0); + $metadata = implode("", $metadata) . $f4f->media['metadata']; + WriteByte($metadata, $f4f->tagHeaderLen + $metadataSize - 1, 0x09); + WriteInt32($metadata, $f4f->tagHeaderLen + $metadataSize, $f4f->tagHeaderLen + $metadataSize); + if (is_resource($flv)) + { + fwrite($flv, $metadata, $f4f->tagHeaderLen + $metadataSize + $f4f->prevTagSize); + return true; + } + else + return $metadata; + } + return false; + } + + function in_array_field($needle, $needle_field, $haystack, $strict = false) + { + if ($strict) + { + foreach ($haystack as $item) + if (isset($item[$needle_field]) and $item[$needle_field] === $needle) + return true; + } + else + { + foreach ($haystack as $item) + if (isset($item[$needle_field]) and $item[$needle_field] == $needle) + return true; + } + return false; + } + + function value_in_array_field($needle, $needle_field, $value_field, $haystack, $strict = false) + { + if ($strict) + { + foreach ($haystack as $item) + if (isset($item[$needle_field]) and $item[$needle_field] === $needle) + return $item[$value_field]; + } + else + { + foreach ($haystack as $item) + if (isset($item[$needle_field]) and $item[$needle_field] == $needle) + return $item[$value_field]; + } + return false; + } + + // Global code starts here + $format = " %-8s%-16s%-16s%-8s"; + $baseFilename = ""; + $debug = false; + $duration = 0; + $delete = false; + $fileExt = ".f4f"; + $fileCount = 1; + $filesize = 0; + $fixWindow = 1000; + $fragCount = 0; + $fragNum = 0; + $manifest = ""; + $maxSpeed = 0; + $metadata = true; + $outDir = ""; + $outFile = ""; + $play = false; + $quiet = false; + $referrer = ""; + $rename = false; + $showHeader = true; + $start = 0; + $update = false; + + $options = array( + 0 => array( + 'help' => 'displays this help', + 'debug' => 'show debug output', + 'delete' => 'delete fragments after processing', + 'fproxy' => 'force proxy for downloading of fragments', + 'play' => 'dump stream to stdout for piping to media player', + 'rename' => 'rename fragments sequentially before processing', + 'update' => 'update the script to current git version' + ), + 1 => array( + 'auth' => 'authentication string for fragment requests', + 'duration' => 'stop recording after specified number of seconds', + 'filesize' => 'split output file in chunks of specified size (MB)', + 'fragments' => 'base filename for fragments', + 'fixwindow' => 'timestamp gap between frames to consider as timeshift', + 'manifest' => 'manifest file for downloading of fragments', + 'maxspeed' => 'maximum bandwidth consumption (KB) for fragment downloading', + 'outdir' => 'destination folder for output file', + 'outfile' => 'filename to use for output file', + 'parallel' => 'number of fragments to download simultaneously', + 'proxy' => 'proxy for downloading of manifest', + 'quality' => 'selected quality level (low|medium|high) or exact bitrate', + 'referrer' => 'Referer to use for emulation of browser requests', + 'start' => 'start from specified fragment', + 'useragent' => 'User-Agent to use for emulation of browser requests' + ) + ); + $cli = new CLI($options, true); + + // Set large enough memory limit + ini_set("memory_limit", "512M"); + + // Check if STDOUT is available + if ($cli->getParam('play')) + { + $play = true; + $quiet = true; + $showHeader = false; + } + if ($cli->getParam('help')) + { + $cli->displayHelp(); + exit(0); + } + + // Check for required extensions + $required_extensions = array( + "bcmath", + "curl", + "SimpleXML" + ); + $missing_extensions = array_diff($required_extensions, get_loaded_extensions()); + if ($missing_extensions) + { + $msg = "You have to install the following extension(s) to continue: '" . implode("', '", $missing_extensions) . "'"; + LogError($msg); + } + + // Initialize classes + $cc = new cURL(); + $f4f = new F4F(); + + $f4f->baseFilename =& $baseFilename; + $f4f->debug =& $debug; + $f4f->fixWindow =& $fixWindow; + $f4f->format =& $format; + $f4f->metadata =& $metadata; + $f4f->outDir =& $outDir; + $f4f->outFile =& $outFile; + $f4f->play =& $play; + $f4f->rename =& $rename; + + // Process command line options + if (isset($cli->params['unknown'])) + $baseFilename = $cli->params['unknown'][0]; + if ($cli->getParam('debug')) + $debug = true; + if ($cli->getParam('delete')) + $delete = true; + if ($cli->getParam('fproxy')) + $cc->fragProxy = true; + if ($cli->getParam('rename')) + $rename = $cli->getParam('rename'); + if ($cli->getParam('update')) + $update = true; + if ($cli->getParam('auth')) + $f4f->auth = '?' . $cli->getParam('auth'); + if ($cli->getParam('duration')) + $duration = $cli->getParam('duration'); + if ($cli->getParam('filesize')) + $filesize = $cli->getParam('filesize'); + if ($cli->getParam('fixwindow')) + $fixWindow = $cli->getParam('fixwindow'); + if ($cli->getParam('fragments')) + $baseFilename = $cli->getParam('fragments'); + if ($cli->getParam('manifest')) + $manifest = $cli->getParam('manifest'); + if ($cli->getParam('maxspeed')) + $maxSpeed = $cli->getParam('maxspeed'); + if ($cli->getParam('outdir')) + $outDir = $cli->getParam('outdir'); + if ($cli->getParam('outfile')) + $outFile = $cli->getParam('outfile'); + if ($cli->getParam('parallel')) + $f4f->parallel = $cli->getParam('parallel'); + if ($cli->getParam('proxy')) + $cc->proxy = $cli->getParam('proxy'); + if ($cli->getParam('quality')) + $f4f->quality = $cli->getParam('quality'); + if ($cli->getParam('referrer')) + $referrer = $cli->getParam('referrer'); + if ($cli->getParam('start')) + $start = $cli->getParam('start'); + if ($cli->getParam('useragent')) + $cc->user_agent = $cli->getParam('useragent'); + + // Use custom referrer + if ($referrer) + $cc->headers[] = "Referer: " . $referrer; + + // Update the script + if ($update) + { + LogInfo("Updating script...."); + $status = $cc->get("https://raw.github.com/K-S-V/Scripts/master/AdobeHDS.php"); + if ($status == 200) + { + if (md5($cc->response) == md5(file_get_contents($argv[0]))) + LogError("You are already using the latest version of this script.", 0); + $status = file_put_contents($argv[0], $cc->response); + if (!$status) + LogError("Failed to write script file"); + LogError("Script has been updated successfully.", 0); + } + else + LogError("Failed to update script"); + } + + // Set overall maximum bandwidth for fragment downloading + if ($maxSpeed > 0) + { + $cc->maxSpeed = ($maxSpeed * 1024) / $f4f->parallel; + LogDebug(sprintf("Setting maximum speed to %.2f KB per fragment (overall $maxSpeed KB)", $cc->maxSpeed / 1024)); + } + + // Create output directory + if ($outDir) + { + $outDir = rtrim(str_replace('\\', '/', $outDir)); + if (!file_exists($outDir)) + { + LogDebug("Creating destination directory " . $outDir); + if (!mkdir($outDir, 0777, true)) + LogError("Failed to create destination directory " . $outDir); + } + } + + // Remove existing file extension + if ($outFile) + $outFile = RemoveExtension($outFile); + + // Disable filesize when piping + if ($play) + $filesize = 0; + + // Disable metadata if it invalidates the stream duration + if ($start or $duration or $filesize) + $metadata = false; + + // Download fragments when manifest is available + if ($manifest) + { + $manifest = AbsoluteUrl("http://", $manifest); + $opt = array( + 'start' => $start, + 'tDuration' => $duration, + 'filesize' => $filesize + ); + $f4f->DownloadFragments($cc, $manifest, $opt); + } + + // Determine output filename + if (!$outFile) + { + $baseFilename = str_replace('\\', '/', $baseFilename); + $lastChar = substr($baseFilename, -1); + if ($baseFilename and !(($lastChar == '/') or ($lastChar == ':'))) + { + $lastSlash = strrpos($baseFilename, '/'); + if ($lastSlash) + $outFile = substr($baseFilename, $lastSlash + 1); + else + $outFile = $baseFilename; + } + else + $outFile = "Joined"; + $outFile = RemoveExtension($outFile); + } + + // Check for available fragments and rename if required + if ($f4f->fragNum) + $fragNum = $f4f->fragNum; + else if ($start) + $fragNum = $start - 1; + if ($rename) + { + RenameFragments($baseFilename, $fragNum, $fileExt); + $fragNum = 0; + } + $count = $fragNum + 1; + while (true) + { + if (file_exists($baseFilename . $count) or file_exists($baseFilename . $count . $fileExt)) + $fragCount++; + else + break; + $count++; + } + LogInfo("Found $fragCount fragments"); + + if (!$f4f->processed) + { + // Process available fragments + if ($fragCount < 1) + exit(1); + $timeStart = microtime(true); + LogDebug("Joining Fragments:"); + for ($i = $fragNum + 1; $i <= $fragNum + $fragCount; $i++) + { + $file = $baseFilename . $i; + if (file_exists($file)) + $frag = file_get_contents($file); + else if (file_exists($file . $fileExt)) + $frag = file_get_contents($file . $fileExt); + if (!isset($opt['flv'])) + { + $opt['test'] = true; + $f4f->InitDecoder(); + $f4f->DecodeFragment($frag, $i, $opt); + if ($filesize) + $opt['flv'] = WriteFlvFile(JoinUrl($outDir, $outFile . '-' . $fileCount++ . ".flv"), $f4f->audio, $f4f->video); + else + $opt['flv'] = WriteFlvFile(JoinUrl($outDir, $outFile . ".flv"), $f4f->audio, $f4f->video); + if ($metadata) + WriteMetadata($f4f, $opt['flv']); + + $opt['test'] = false; + $f4f->InitDecoder(); + } + $f4f->DecodeFragment($frag, $i, $opt); + if ($filesize and ($f4f->filesize >= $filesize)) + { + $f4f->filesize = 0; + fclose($opt['flv']); + unset($opt['flv']); + } + LogInfo("Processed " . ($i - $fragNum) . " fragments", true); + } + if (isset($opt['flv'])) + fclose($opt['flv']); + $timeEnd = microtime(true); + $timeTaken = sprintf("%.2f", $timeEnd - $timeStart); + LogInfo("Joined $fragCount fragments in $timeTaken seconds"); + } + + // Delete fragments after processing + if ($delete) + { + for ($i = $fragNum + 1; $i <= $fragNum + $fragCount; $i++) + { + $file = $baseFilename . $i; + if (file_exists($file)) + unlink($file); + else if (file_exists($file . $fileExt)) + unlink($file . $fileExt); + } + } + + LogInfo("Finished"); +?> diff --git a/fb-img-resize b/fb-img-resize new file mode 100755 index 0000000..8825b5b --- /dev/null +++ b/fb-img-resize @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +TMPDIR="$(mktemp -d "/tmp/${0##*/}.XXXXXX")" +trap "rm -rf '${TMPDIR}'" EXIT TERM + +for i in "$@"; do + convert "$i" -resize 1500x1500 "$TMPDIR/${i##*/}" +done + +fb -m "$TMPDIR"/* @@ -0,0 +1,620 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# +# woof -- an ad-hoc single file webserver +# Copyright (C) 2004-2009 Simon Budig <simon@budig.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of the GNU General Public License is available at +# http://www.fsf.org/licenses/gpl.txt, you can also write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +# Darwin support with the help from Mat Caughron, <mat@phpconsulting.com> +# Solaris support by Colin Marquardt, <colin.marquardt@zmd.de> +# FreeBSD support with the help from Andy Gimblett, <A.M.Gimblett@swansea.ac.uk> +# Cygwin support by Stefan Reichör <stefan@xsteve.at> +# tarfile usage suggested by Morgan Lefieux <comete@geekandfree.org> +# File upload support loosely based on code from Stephen English <steve@secomputing.co.uk> + +import sys, os, errno, socket, getopt, commands, tempfile +import cgi, urllib, urlparse, BaseHTTPServer +import readline +import ConfigParser +import shutil, tarfile, zipfile +import struct + +maxdownloads = 1 +TM = object +cpid = -1 +compressed = 'gz' +upload = False + + +class EvilZipStreamWrapper(TM): + def __init__ (self, victim): + self.victim_fd = victim + self.position = 0 + self.tells = [] + self.in_file_data = 0 + + def tell (self): + self.tells.append (self.position) + return self.position + + def seek (self, offset, whence = 0): + if offset != 0: + if offset == self.tells[0] + 14: + # the zipfile module tries to fix up the file header. + # write Data descriptor header instead, + # the next write from zipfile + # is CRC, compressed_size and file_size (as required) + self.write ("PK\007\010") + elif offset == self.tells[1]: + # the zipfile module goes to the end of the file. The next + # data written definitely is infrastructure (in_file_data = 0) + self.tells = [] + self.in_file_data = 0 + else: + raise "unexpected seek for EvilZipStreamWrapper" + + def write (self, data): + # only test for headers if we know that we're not writing + # (potentially compressed) data. + if self.in_file_data == 0: + if data[:4] == zipfile.stringFileHeader: + # fix the file header for extra Data descriptor + hdr = list (struct.unpack (zipfile.structFileHeader, data[:30])) + hdr[3] |= (1 << 3) + data = struct.pack (zipfile.structFileHeader, *hdr) + data[30:] + self.in_file_data = 1 + elif data[:4] == zipfile.stringCentralDir: + # fix the directory entry to match file header. + hdr = list (struct.unpack (zipfile.structCentralDir, data[:46])) + hdr[5] |= (1 << 3) + data = struct.pack (zipfile.structCentralDir, *hdr) + data[46:] + + self.position += len (data) + self.victim_fd.write (data) + + def __getattr__ (self, name): + return getattr (self.victim_fd, name) + + +# Utility function to guess the IP (as a string) where the server can be +# reached from the outside. Quite nasty problem actually. + +def find_ip (): + # we get a UDP-socket for the TEST-networks reserved by IANA. + # It is highly unlikely, that there is special routing used + # for these networks, hence the socket later should give us + # the ip address of the default route. + # We're doing multiple tests, to guard against the computer being + # part of a test installation. + + candidates = [] + for test_ip in ["192.0.2.0", "198.51.100.0", "203.0.113.0"]: + s = socket.socket (socket.AF_INET, socket.SOCK_DGRAM) + s.connect ((test_ip, 80)) + ip_addr = s.getsockname ()[0] + s.close () + if ip_addr in candidates: + return ip_addr + candidates.append (ip_addr) + + return candidates[0] + + +# our own HTTP server class, fixing up a change in python 2.7 +# since we do our fork() in the request handler +# the server must not shutdown() the socket. + +class ForkingHTTPServer (BaseHTTPServer.HTTPServer): + def process_request(self, request, client_address): + self.finish_request (request, client_address) + self.close_request (request) + + +# Main class implementing an HTTP-Requesthandler, that serves just a single +# file and redirects all other requests to this file (this passes the actual +# filename to the client). +# Currently it is impossible to serve different files with different +# instances of this class. + +class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler): + server_version = "Simons FileServer" + protocol_version = "HTTP/1.0" + + filename = "." + + def log_request (self, code='-', size='-'): + if code == 200: + BaseHTTPServer.BaseHTTPRequestHandler.log_request (self, code, size) + + + def do_POST (self): + global maxdownloads, upload + + if not upload: + self.send_error (501, "Unsupported method (POST)") + return + + # taken from + # http://mail.python.org/pipermail/python-list/2006-September/402441.html + + ctype, pdict = cgi.parse_header (self.headers.getheader ('Content-Type')) + form = cgi.FieldStorage (fp = self.rfile, + headers = self.headers, + environ = {'REQUEST_METHOD' : 'POST'}, + keep_blank_values = 1, + strict_parsing = 1) + if not form.has_key ("upfile"): + self.send_error (403, "No upload provided") + return + + upfile = form["upfile"] + + if not upfile.file or not upfile.filename: + self.send_error (403, "No upload provided") + return + + upfilename = upfile.filename + + if "\\" in upfilename: + upfilename = upfilename.split ("\\")[-1] + + upfilename = os.path.basename (upfile.filename) + + destfile = None + for suffix in ["", ".1", ".2", ".3", ".4", ".5", ".6", ".7", ".8", ".9"]: + destfilename = os.path.join (".", upfilename + suffix) + try: + destfile = os.open (destfilename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0644) + break + except OSError, e: + if e.errno == errno.EEXIST: + continue + raise + + if not destfile: + upfilename += "." + destfile, destfilename = tempfile.mkstemp (prefix = upfilename, dir = ".") + + print >>sys.stderr, "accepting uploaded file: %s -> %s" % (upfilename, destfilename) + + shutil.copyfileobj (upfile.file, os.fdopen (destfile, "w")) + + if upfile.done == -1: + self.send_error (408, "upload interrupted") + + txt = """\ + <html> + <head><title>Woof Upload</title></head> + <body> + <h1>Woof Upload complete</title></h1> + <p>Thanks a lot!</p> + </body> + </html> + """ + self.send_response (200) + self.send_header ("Content-Type", "text/html") + self.send_header ("Content-Length", str (len (txt))) + self.end_headers () + self.wfile.write (txt) + + maxdownloads -= 1 + + return + + + def do_GET (self): + global maxdownloads, cpid, compressed, upload + + # Form for uploading a file + if upload: + txt = """\ + <html> + <head><title>Woof Upload</title></head> + <body> + <h1>Woof Upload</title></h1> + <form name="upload" method="POST" enctype="multipart/form-data"> + <p><input type="file" name="upfile" /></p> + <p><input type="submit" value="Upload!" /></p> + </form> + </body> + </html> + """ + self.send_response (200) + self.send_header ("Content-Type", "text/html") + self.send_header ("Content-Length", str (len (txt))) + self.end_headers () + self.wfile.write (txt) + return + + # Redirect any request to the filename of the file to serve. + # This hands over the filename to the client. + + self.path = urllib.quote (urllib.unquote (self.path)) + location = "/" + urllib.quote (os.path.basename (self.filename)) + if os.path.isdir (self.filename): + if compressed == 'gz': + location += ".tar.gz" + elif compressed == 'bz2': + location += ".tar.bz2" + elif compressed == 'zip': + location += ".zip" + else: + location += ".tar" + + if self.path != location: + txt = """\ + <html> + <head><title>302 Found</title></head> + <body>302 Found <a href="%s">here</a>.</body> + </html>\n""" % location + self.send_response (302) + self.send_header ("Location", location) + self.send_header ("Content-Type", "text/html") + self.send_header ("Content-Length", str (len (txt))) + self.end_headers () + self.wfile.write (txt) + return + + maxdownloads -= 1 + + # let a separate process handle the actual download, so that + # multiple downloads can happen simultaneously. + + cpid = os.fork () + + if cpid == 0: + # Child process + child = None + type = None + + if os.path.isfile (self.filename): + type = "file" + elif os.path.isdir (self.filename): + type = "dir" + + if not type: + print >> sys.stderr, "can only serve files or directories. Aborting." + sys.exit (1) + + self.send_response (200) + self.send_header ("Content-Type", "application/octet-stream") + self.send_header ("Content-Disposition", "attachment;filename=%s" % urllib.quote (os.path.basename (self.filename))) + if os.path.isfile (self.filename): + self.send_header ("Content-Length", + os.path.getsize (self.filename)) + self.end_headers () + + try: + if type == "file": + datafile = file (self.filename) + shutil.copyfileobj (datafile, self.wfile) + datafile.close () + elif type == "dir": + if compressed == 'zip': + ezfile = EvilZipStreamWrapper (self.wfile) + zfile = zipfile.ZipFile (ezfile, 'w', zipfile.ZIP_DEFLATED) + stripoff = os.path.dirname (self.filename) + os.sep + + for root, dirs, files in os.walk (self.filename): + for f in files: + filename = os.path.join (root, f) + if filename[:len (stripoff)] != stripoff: + raise RuntimeException, "invalid filename assumptions, please report!" + zfile.write (filename, filename[len (stripoff):]) + zfile.close () + else: + tfile = tarfile.open (mode=('w|' + compressed), + fileobj=self.wfile) + tfile.add (self.filename, + arcname=os.path.basename (self.filename)) + tfile.close () + except Exception, e: + print e + print >>sys.stderr, "Connection broke. Aborting" + + +def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080): + global maxdownloads + + maxdownloads = maxdown + + # We have to somehow push the filename of the file to serve to the + # class handling the requests. This is an evil way to do this... + + FileServHTTPRequestHandler.filename = filename + + try: + httpd = ForkingHTTPServer ((ip_addr, port), FileServHTTPRequestHandler) + except socket.error: + print >>sys.stderr, "cannot bind to IP address '%s' port %d" % (ip_addr, port) + sys.exit (1) + + if not ip_addr: + ip_addr = find_ip () + if ip_addr: + if filename: + location = "http://%s:%s/%s" % (ip_addr, httpd.server_port, + urllib.quote (os.path.basename (filename))) + if os.path.isdir (filename): + if compressed == 'gz': + location += ".tar.gz" + elif compressed == 'bz2': + location += ".tar.bz2" + elif compressed == 'zip': + location += ".zip" + else: + location += ".tar" + else: + location = "http://%s:%s/" % (ip_addr, httpd.server_port) + + print "Now serving on %s" % location + + while cpid != 0 and maxdownloads > 0: + httpd.handle_request () + + + +def usage (defport, defmaxdown, errmsg = None): + name = os.path.basename (sys.argv[0]) + print >>sys.stderr, """ + Usage: %s [-i <ip_addr>] [-p <port>] [-c <count>] <file> + %s [-i <ip_addr>] [-p <port>] [-c <count>] [-z|-j|-Z|-u] <dir> + %s [-i <ip_addr>] [-p <port>] [-c <count>] -s + %s [-i <ip_addr>] [-p <port>] [-c <count>] -U + + %s <url> + + Serves a single file <count> times via http on port <port> on IP + address <ip_addr>. + When a directory is specified, an tar archive gets served. By default + it is gzip compressed. You can specify -z for gzip compression, + -j for bzip2 compression, -Z for ZIP compression or -u for no compression. + You can configure your default compression method in the configuration + file described below. + + When -s is specified instead of a filename, %s distributes itself. + + When -U is specified, woof provides an upload form, allowing file uploads. + + defaults: count = %d, port = %d + + If started with an url as an argument, woof acts as a client, + downloading the file and saving it in the current directory. + + You can specify different defaults in two locations: /etc/woofrc + and ~/.woofrc can be INI-style config files containing the default + port and the default count. The file in the home directory takes + precedence. The compression methods are "off", "gz", "bz2" or "zip". + + Sample file: + + [main] + port = 8008 + count = 2 + ip = 127.0.0.1 + compressed = gz + """ % (name, name, name, name, name, name, defmaxdown, defport) + + if errmsg: + print >>sys.stderr, errmsg + print >>sys.stderr + sys.exit (1) + + + +def woof_client (url): + urlparts = urlparse.urlparse (url, "http") + if urlparts[0] not in [ "http", "https" ] or urlparts[1] == '': + return None + + fname = None + + f = urllib.urlopen (url) + + f_meta = f.info () + disp = f_meta.getheader ("Content-Disposition") + + if disp: + disp = disp.split (";") + + if disp and disp[0].lower () == 'attachment': + fname = [x[9:] for x in disp[1:] if x[:9].lower () == "filename="] + if len (fname): + fname = fname[0] + else: + fname = None + + if fname == None: + url = f.geturl () + urlparts = urlparse.urlparse (url) + fname = urlparts[2] + + if not fname: + fname = "woof-out.bin" + + if fname: + fname = urllib.unquote (fname) + fname = os.path.basename (fname) + + readline.set_startup_hook (lambda: readline.insert_text (fname)) + fname = raw_input ("Enter target filename: ") + readline.set_startup_hook (None) + + override = False + + destfile = None + destfilename = os.path.join (".", fname) + try: + destfile = os.open (destfilename, + os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0644) + except OSError, e: + if e.errno == errno.EEXIST: + override = raw_input ("File exists. Overwrite (y/n)? ") + override = override.lower () in [ "y", "yes" ] + else: + raise + + if destfile == None: + if override == True: + destfile = os.open (destfilename, os.O_WRONLY | os.O_CREAT, 0644) + else: + for suffix in [".1", ".2", ".3", ".4", ".5", ".6", ".7", ".8", ".9"]: + destfilename = os.path.join (".", fname + suffix) + try: + destfile = os.open (destfilename, + os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0644) + break + except OSError, e: + if e.errno == errno.EEXIST: + continue + raise + + if not destfile: + destfile, destfilename = tempfile.mkstemp (prefix = fname + ".", + dir = ".") + print "alternate filename is:", destfilename + + print "downloading file: %s -> %s" % (fname, destfilename) + + shutil.copyfileobj (f, os.fdopen (destfile, "w")) + + return 1; + + + +def main (): + global cpid, upload, compressed + + maxdown = 1 + port = 8080 + ip_addr = '' + + config = ConfigParser.ConfigParser () + config.read (['/etc/woofrc', os.path.expanduser ('~/.woofrc')]) + + if config.has_option ('main', 'port'): + port = config.getint ('main', 'port') + + if config.has_option ('main', 'count'): + maxdown = config.getint ('main', 'count') + + if config.has_option ('main', 'ip'): + ip_addr = config.get ('main', 'ip') + + if config.has_option ('main', 'compressed'): + formats = { 'gz' : 'gz', + 'true' : 'gz', + 'bz' : 'bz2', + 'bz2' : 'bz2', + 'zip' : 'zip', + 'off' : '', + 'false' : '' } + compressed = config.get ('main', 'compressed') + compressed = formats.get (compressed, 'gz') + + defaultport = port + defaultmaxdown = maxdown + + try: + options, filenames = getopt.getopt (sys.argv[1:], "hUszjZui:c:p:") + except getopt.GetoptError, desc: + usage (defaultport, defaultmaxdown, desc) + + for option, val in options: + if option == '-c': + try: + maxdown = int (val) + if maxdown <= 0: + raise ValueError + except ValueError: + usage (defaultport, defaultmaxdown, + "invalid download count: %r. " + "Please specify an integer >= 0." % val) + + elif option == '-i': + ip_addr = val + + elif option == '-p': + try: + port = int (val) + except ValueError: + usage (defaultport, defaultmaxdown, + "invalid port number: %r. Please specify an integer" % val) + + elif option == '-s': + filenames.append (__file__) + + elif option == '-h': + usage (defaultport, defaultmaxdown) + + elif option == '-U': + upload = True + + elif option == '-z': + compressed = 'gz' + elif option == '-j': + compressed = 'bz2' + elif option == '-Z': + compressed = 'zip' + elif option == '-u': + compressed = '' + + else: + usage (defaultport, defaultmaxdown, "Unknown option: %r" % option) + + if upload: + if len (filenames) > 0: + usage (defaultport, defaultmaxdown, + "Conflicting usage: simultaneous up- and download not supported.") + filename = None + + else: + if len (filenames) == 1: + if woof_client (filenames[0]) != None: + sys.exit (0) + + filename = os.path.abspath (filenames[0]) + else: + usage (defaultport, defaultmaxdown, + "Can only serve single files/directories.") + + if not os.path.exists (filename): + usage (defaultport, defaultmaxdown, + "%s: No such file or directory" % filenames[0]) + + if not (os.path.isfile (filename) or os.path.isdir (filename)): + usage (defaultport, defaultmaxdown, + "%s: Neither file nor directory" % filenames[0]) + + serve_files (filename, maxdown, ip_addr, port) + + # wait for child processes to terminate + if cpid != 0: + try: + while 1: + os.wait () + except OSError: + pass + + + +if __name__=='__main__': + try: + main () + except KeyboardInterrupt: + print + @@ -0,0 +1,75 @@ +#!/bin/bash + +# List the X compose sequences available to generate the specified character. +# I.E. the keyboard key sequence to enter after the compose (multi) key or +# a dead key is pressed. +# +# This version has been heavily modified by me (David the H.). It is now +# bash-specific, reduces the need for external tools (only grep is needed), +# and can handle multiple inputs. +# +# Original script info follows. For the original version, go here: +# http://www.pixelbeat.org/docs/xkeyboard/ +# +# Author: +# P@draigBrady.com +# Notes: +# GTK+ apps use a different but broadly similar input method +# to X by default. Personally I tell GTK+ to use the X one by +# adding `export GTK_IM_MODULE=xim` to /etc/profile +# Changes: +# V0.1, 09 Sep 2005, Initial release +# V0.2, 04 May 2007, Added support for ubuntu +# + +if [[ -z $* ]]; then + echo "Usage: ${0##*/} 'character(s)'" >&2 + echo "Multiple characters are supported." >&2 + echo "They don't need to be space-separated." >&2 + exit 1 +fi + +if [[ $LANG =~ (.*)[.]UTF.*8 ]]; then + + lang="${BASH_REMATCH[1]}" + codeset=UTF-8 + +else + + echo "Sorry, only UTF-8 is supported at present" >&2 + exit 1 + #could try and normalise codeset, and get char with printf %q + #but would not be general enough I think. + +fi + +dir=/usr/share/X11/locale #ubuntu + +if [[ ! -d "$dir" ]]; then + + dir=/usr/X11R6/lib/X11/locale #redhat/debian + +fi + +if [[ ! -f "$dir/locale.dir" ]]; then + + echo "Sorry, couldn't find your X windows locale data" >&2 + exit 1 + +fi + +page="$( grep -m1 "${lang}.${codeset}$" <$dir/locale.dir )" +page=${page%%/*} + +file="$dir/$page/Compose" + +while read -n 1 character; do + + [[ -z $character ]] && continue + echo "combinations found for [$character]" + grep -F "\"$character\"" "$file" + echo + +done <<<"$@" + +exit 0
\ No newline at end of file |