diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8442cc7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/IniParser"] + path = submodules/IniParser + url = git@github.com:ehime/IniParser.git diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..aea36d5 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spelling + + + + + SpellCheckingInspection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1371228341549 + 1371228341549 + + + 1371228585565 + 1371228585565 + + + 1371228824075 + 1371228824075 + + + 1371230372076 + 1371230372076 + + + 1371230731896 + 1371230731896 + + + 1371230847468 + 1371230847468 + + + 1371232705033 + 1371232705033 + + + 1371234884456 + 1371234884456 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.phpstorm/spelling/snaphax.dic b/.phpstorm/spelling/snaphax.dic new file mode 100644 index 0000000..502b3eb --- /dev/null +++ b/.phpstorm/spelling/snaphax.dic @@ -0,0 +1,35 @@ +adamcaudill +appspot +austinehyde +autoload +barbaz +caudill +dechex +doesnt +ehime +fdata +feelinsonice +fname +helloworld +incase +iniparser +jsonp +klampaeckel +lackner +mcrypt +multipart +noninfringement +php's +prepending +repr +requirements +rijndael +screenshot +settype +snapchat +snapchat'ssnaphax +snaphax +submodule +submodules +tlack +urlencode \ No newline at end of file diff --git a/configuration/config.ini.php b/configuration/config.ini.php new file mode 100644 index 0000000..c9f9af2 --- /dev/null +++ b/configuration/config.ini.php @@ -0,0 +1,17 @@ +; \ No newline at end of file diff --git a/snaphax.php b/snaphax.php index e3f8933..da7322a 100644 --- a/snaphax.php +++ b/snaphax.php @@ -1,277 +1,335 @@ - - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - - $SNAPHAX_DEFAULT_OPTIONS = array( - 'blob_enc_key' => 'M02cnQ51Ji97vwT4', - 'debug' => false, - 'pattern' => '0001110111101110001111010101111011010001001110011000110001000110', - 'secret' => 'iEk21fuwZApXlz93750dmW22pw389dPwOk', - 'static_token' => 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9', - 'url' => 'https://feelinsonice.appspot.com', - 'user_agent' => 'Snaphax 4.0.1 (iPad; iPhone OS 6.0; en_US)' - ); - - if (!function_exists('curl_init')) { - throw new Exception('Snaphax needs the CURL PHP extension.'); - } - if (!function_exists('json_decode')) { - throw new Exception('Snaphax needs the JSON PHP extension.'); - } - if (!function_exists('mcrypt_decrypt')) { - throw new Exception('Snaphax needs the mcrypt PHP extension.'); - } - - class Snaphax { - // High level class to perform actions on Snapchat - - const STATUS_NEW = 1; - const MEDIA_IMAGE = 0; - const MEDIA_VIDEO = 1; - - function Snaphax($options) { - global $SNAPHAX_DEFAULT_OPTIONS; - - $this->options = array_merge($SNAPHAX_DEFAULT_OPTIONS, $options); - $this->api = new SnaphaxApi($this->options); - $this->auth_token = false; - } - function login() { - $ts = $this->api->time(); - $out = $this->api->postCall( - '/ph/login', - array( - 'username' => $this->options['username'], - 'password' => $this->options['password'], - 'timestamp' => $ts - ), - $this->options['static_token'], - $ts - ); - if (is_array($out) && - !empty($out['auth_token'])) { - $this->auth_token = $out['auth_token']; - } - return $out; - } - function fetch($id) { - if (!$this->auth_token) { - throw new Exception('no auth token'); - } - $ts = $this->api->time(); - $url = "/ph/blob"; - $result = $this->api->postCall($url, array( - 'id' => $id, - 'timestamp' => $ts, - 'username' => urlencode($this->options['username']) - ), $this->auth_token, $ts, 0); - $this->api->debug('blob result', $result); - - // some blobs are not encrypted - if ($this->api->isValidBlobHeader(substr($result, 0, 256))) { - $this->api->debug('blob not encrypted'); - return $result; - } - - $result_decoded = $this->api->decrypt($result); - $this->api->debug('decoded snap', $result_decoded); - if ($this->api->isValidBlobHeader(substr($result_decoded, 0, 256))) { - return $result_decoded; - } else { - $this->api->debug('invalid image/video data header'); - return false; - } - } - function reqToken($param1, $param2) { - return $this->api->hash($param1, $param2); - } - function upload($file_data, $type, $recipients, $time=8) { - if ($type != self::MEDIA_IMAGE && $type != self::MEDIA_VIDEO) - throw new Exception('Snaphax: upload type must be MEDIA_IMAGE or MEDIA_VIDEO'); - if (!$this->auth_token) { - throw new Exception('no auth token'); - } - if (!is_array($recipients)) - $recipients = array($recipients); - $ts = $this->api->time(); - $media_id = strtoupper($this->options['username']).time(); - $this->api->debug('upload snap data', $file_data); - $file_data_encrypted = $this->api->encrypt($file_data); - $this->api->debug('upload snap data encrypted', $file_data_encrypted); - file_put_contents('/tmp/blah.jpg', $file_data_encrypted); - $result = $this->api->postCall( - '/ph/upload', - array( - 'username' => $this->options['username'], - 'timestamp' => $ts, - 'type' => $type, - // 'data' => urlencode($file_data_encrypted).'; filename="file"', - 'data' => '@/tmp/blah.jpg;filename=file', - 'media_id' => $media_id - ), - $this->auth_token, - $ts, - 0, - array("Content-type: multipart/form-data") - ); - $this->api->debug('upload result', $result); - - foreach ($recipients as $recipient) { - $ts = $this->api->time(); - $result = $this->api->postCall( - '/ph/send', - array( - 'username' => $this->options['username'], - 'timestamp' => $ts, - 'recipient' => $recipient, - 'media_id' => $media_id, - 'time' => $time - ), - $this->auth_token, - $ts, - 0 - ); - $this->api->debug("send to $recipient: " . $result); - } - - return $media_id; - } - } - - class SnaphaxApi { - // Low level code to communicate with Snapchat via HTTP - - function SnaphaxApi($options) { - $this->options = $options; - } - - function debug($text, $binary = false) { - if ($this->options['debug']) { - echo "SNAPHAX DEBUG: $text"; - if ($binary !== false) { - // shortened hex repr of binary - $len = strlen($binary); - $tmp = " hex ($len bytes): "; - $tmp.= join(' ', array_map('dechex', array_map('ord', str_split(substr($binary, 0, 16))))); - $tmp.= ' ... '; - $tmp.= join(' ', array_map('dechex', array_map('ord', str_split(substr($binary, -16))))); - echo $tmp; - } - echo "\n"; - } - } - - function isValidBlobHeader($header) { - if (($header[0] == chr(00) && // mp4 - $header[0] == chr(00)) || - ($header[0] == chr(0xFF) && // jpg - $header[1] == chr(0xD8))) - return true; - else - return false; - } - - function decrypt($data) { - return mcrypt_decrypt('rijndael-128', $this->options['blob_enc_key'], $data, 'ecb'); - } - - function encrypt($data) { - return mcrypt_encrypt('rijndael-128', $this->options['blob_enc_key'], $data, 'ecb'); - } - - public function postCall($endpoint, $post_data, $param1, $param2, $json=1, $headers=false) { - $ch = curl_init(); - - // set the url, number of POST vars, POST data - curl_setopt($ch,CURLOPT_URL, $this->options['url'].$endpoint); - curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch,CURLOPT_USERAGENT, $this->options['user_agent']); - - if ($headers && is_array($headers)) { - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - } - - $post_data['req_token'] = $this->hash($param1, $param2); - curl_setopt($ch, CURLOPT_POST, count($post_data)); - if (!$headers) - curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data)); - else - curl_setopt($ch,CURLOPT_POSTFIELDS, $post_data); - $this->debug('POST params: ' . json_encode($post_data)); - $result = curl_exec($ch); - if ($result === false) { - $this->debug('CURL error: '.curl_error($ch)); - return false; - } - $this->debug('HTTP response code' . curl_getinfo($ch, CURLINFO_HTTP_CODE)); - $this->debug('POST return ' . $result); - - // close connection - curl_close($ch); - - if ($json) - return json_decode(utf8_encode($result), true); - else - return $result; - } - - function hash($param1, $param2) { - $this->debug("p1: $param1"); - $this->debug("p2: $param2"); - - $s1 = $this->options['secret'] . $param1; - $this->debug("s1: $s1"); - $s2 = $param2 . $this->options['secret']; - $this->debug("s2: $s2"); - - $hash = hash_init('sha256'); - hash_update($hash, $s1); - $s3 = hash_final($hash, false); - $this->debug("s3: $s3"); - - $hash = hash_init('sha256'); - hash_update($hash, $s2); - $s4 = hash_final($hash, false); - $this->debug("s4: $s4"); - - $out = ''; - for ($i = 0; $i < strlen($this->options['pattern']); $i++) { - $c = $this->options['pattern'][$i]; - if ($c == '0') - $out .= $s3[$i]; - else - $out .= $s4[$i]; - } - $this->debug("out: $out"); - return $out; - } - - function time() { - return round(microtime(true) * 1000); - } - - } - - + + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + + require 'submodules/IniParser/src/IniParser.php'; + + Class Snaphax + { + // High level class to perform actions on Snapchat + + const STATUS_NEW = 1; + const MEDIA_IMAGE = 0; + const MEDIA_VIDEO = 1; + + /** + * @var string + */ + private $_configFile = 'configuration/config.ini.php'; + + + /** + * @param array $options + */ + public function __construct(Array $options = array()) + { + $this->_checkRequirements(); + + $this->options = array_merge($this->_getOptionsIni(), $options); + $this->api = New SnaphaxApi($this->options); + $$this->_auth_token = false; + } + + /** + * @return array|bool|mixed + */ + public function login() + { + $ts = $this->api->time(); + $out = $this->api->postCall( + '/ph/login', + array( + 'username' => $this->options['username'], + 'password' => $this->options['password'], + 'timestamp' => $ts + ), + $this->options['static_token'], + $ts + ); + + if (is_array($out) && !empty($out['auth_token'])) + { + $$this->_auth_token = $out['auth_token']; + } + return $out; + } + + /** + * @param $id + * @return bool|mixed|string + * @throws Exception + */ + public function fetch($id) + { + if (!$$this->_auth_token) + { + Throw New Exception('no auth token'); + } + + $ts = $this->api->time(); + $url = "/ph/blob"; + + $result = $this->api->postCall($url, array( + 'id' => $id, + 'timestamp' => $ts, + 'username' => urlencode($this->options['username']) + ), $$this->_auth_token, $ts, 0); + + $this->api->debug('blob result', $result); + + // some blobs are not encrypted + if ($this->api->isValidBlobHeader(substr($result, 0, 256))) + { + $this->api->debug('blob not encrypted'); + return $result; + } + + $result_decoded = $this->api->decrypt($result); + $this->api->debug('decoded snap', $result_decoded); + + if ($this->api->isValidBlobHeader(substr($result_decoded, 0, 256))) return $result_decoded; + + $this->api->debug('invalid image/video data header'); + return false; + } + + /** + * @param $param1 + * @param $param2 + * @return string + */ + public function reqToken($param1, $param2) + { + return $this->api->hash($param1, $param2); + } + + /** + * @param $file_data + * @param $type + * @param $recipients + * @param int $time + * @return string + * @throws Exception + */ + public function upload($file_data, $type, $recipients, $time=8) + { + if ($type != self::MEDIA_IMAGE && $type != self::MEDIA_VIDEO) Throw New Exception('Snaphax: upload type must be MEDIA_IMAGE or MEDIA_VIDEO'); + + if (!$$this->_auth_token) Throw New Exception('no auth token'); + + if (!is_array($recipients)) + { + $recipients = array($recipients); + } + + $ts = $this->api->time(); + $media_id = strtoupper($this->options['username']).time(); + $this->api->debug('upload snap data', $file_data); + + $file_data_encrypted = $this->api->encrypt($file_data); + $this->api->debug('upload snap data encrypted', $file_data_encrypted); + + file_put_contents('/tmp/blah.jpg', $file_data_encrypted); + + $result = $this->api->postCall( + '/ph/upload', + array( + 'username' => $this->options['username'], + 'timestamp' => $ts, + 'type' => $type, + // 'data' => urlencode($file_data_encrypted).'; filename="file"', + 'data' => '@/tmp/blah.jpg;filename=file', + 'media_id' => $media_id + ), + $$this->_auth_token, + $ts, + 0, + array('Content-type: multipart/form-data; boundary=AaB03x') // not compatible with declaration error noted. + ); + $this->api->debug('upload result', $result); + + foreach ($recipients as $recipient) { + $ts = $this->api->time(); + $result = $this->api->postCall( + '/ph/send', + array( + 'username' => $this->options['username'], + 'timestamp' => $ts, + 'recipient' => $recipient, + 'media_id' => $media_id, + 'time' => $time + ), + $$this->_auth_token, + $ts, + 0 + ); + $this->api->debug("send to $recipient: " . $result); + } + + return $media_id; + } + + /** + * @returns void + * @throws Exception + */ + private function _checkRequirements() + { + $requirements = New \IniParser($this->_configFile); + $requirements->parse('REQUIREMENTS'); + + foreach ($requirements->getArray() AS $module => $extension) + { + if (!function_exists($module)) Throw New Exception("Snaphax needs the {$extension} PHP extension."); + } + } + + /** + * @return array + * @throws Exception + */ + private function _getOptionsIni () + { + if (!file_exists($this->_configFile)) Throw New Exception ('missing instantiating INI file, please make sure this exists.'); + + $config = New \IniParser($this->_configFile); + $config->parse('SNAPHAX_DEFAULT_OPTIONS'); + return $config->getArray(); + } +} + +class SnaphaxApi { + // Low level code to communicate with Snapchat via HTTP + + function SnaphaxApi($options) { + $this->options = $options; + } + + function debug($text, $binary = false) { + if ($this->options['debug']) { + echo "SNAPHAX DEBUG: $text"; + if ($binary !== false) { + // shortened hex repr of binary + $len = strlen($binary); + $tmp = " hex ($len bytes): "; + $tmp.= join(' ', array_map('dechex', array_map('ord', str_split(substr($binary, 0, 16))))); + $tmp.= ' ... '; + $tmp.= join(' ', array_map('dechex', array_map('ord', str_split(substr($binary, -16))))); + echo $tmp; + } + echo "\n"; + } + } + + function isValidBlobHeader($header) { + if (($header[0] == chr(00) && // mp4 + $header[0] == chr(00)) || + ($header[0] == chr(0xFF) && // jpg + $header[1] == chr(0xD8))) + return true; + else + return false; + } + + function decrypt($data) { + return mcrypt_decrypt('rijndael-128', $this->options['blob_enc_key'], $data, 'ecb'); + } + + function encrypt($data) { + return mcrypt_encrypt('rijndael-128', $this->options['blob_enc_key'], $data, 'ecb'); + } + + public function postCall($endpoint, $post_data, $param1, $param2, $json=1, $headers=false) { + $ch = curl_init(); + + // set the url, number of POST vars, POST data + curl_setopt($ch,CURLOPT_URL, $this->options['url'].$endpoint); + curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch,CURLOPT_USERAGENT, $this->options['user_agent']); + + if ($headers && is_array($headers)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + $post_data['req_token'] = $this->hash($param1, $param2); + curl_setopt($ch, CURLOPT_POST, count($post_data)); + if (!$headers) + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data)); + else + curl_setopt($ch,CURLOPT_POSTFIELDS, $post_data); + $this->debug('POST params: ' . json_encode($post_data)); + $result = curl_exec($ch); + if ($result === false) { + $this->debug('CURL error: '.curl_error($ch)); + return false; + } + $this->debug('HTTP response code' . curl_getinfo($ch, CURLINFO_HTTP_CODE)); + $this->debug('POST return ' . $result); + + // close connection + curl_close($ch); + + if ($json) + return json_decode(utf8_encode($result), true); + else + return $result; + } + + function hash($param1, $param2) { + $this->debug("p1: $param1"); + $this->debug("p2: $param2"); + + $s1 = $this->options['secret'] . $param1; + $this->debug("s1: $s1"); + $s2 = $param2 . $this->options['secret']; + $this->debug("s2: $s2"); + + $hash = hash_init('sha256'); + hash_update($hash, $s1); + $s3 = hash_final($hash, false); + $this->debug("s3: $s3"); + + $hash = hash_init('sha256'); + hash_update($hash, $s2); + $s4 = hash_final($hash, false); + $this->debug("s4: $s4"); + + $out = ''; + for ($i = 0; $i < strlen($this->options['pattern']); $i++) { + $c = $this->options['pattern'][$i]; + if ($c == '0') + $out .= $s3[$i]; + else + $out .= $s4[$i]; + } + $this->debug("out: $out"); + return $out; + } + + function time() { + return round(microtime(true) * 1000); + } + + } diff --git a/submodules/IniParser b/submodules/IniParser new file mode 160000 index 0000000..cf602c2 --- /dev/null +++ b/submodules/IniParser @@ -0,0 +1 @@ +Subproject commit cf602c2e4eef7d05015f0f2f1a1fab8280e783ad