first checkin of code
This commit is contained in:
parent
cbf6b1c463
commit
f21fbe13c2
3 changed files with 319 additions and 0 deletions
86
README.md
Normal file
86
README.md
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
Snaphax: a PHP library to use the Snapchat API
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
This library allows you to communicate with Snapchat's servers using their
|
||||||
|
undocumented HTTP API. It was reverse engineered from the official Android
|
||||||
|
client (version 1.6)
|
||||||
|
|
||||||
|
Warning
|
||||||
|
-------
|
||||||
|
|
||||||
|
I made this by reverse engineering the app. It may be extremely buggy or piss
|
||||||
|
off the Snapchat people.
|
||||||
|
|
||||||
|
Limitations
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Only login (with list of new media) and fetching of images is implemented.
|
||||||
|
This is obviously a huge failing which I am to correct when I have more time.
|
||||||
|
|
||||||
|
Motivation
|
||||||
|
----------
|
||||||
|
|
||||||
|
I'm a huge fan of Snapchat. I'm stunned and delighted by the fact that a simple
|
||||||
|
feature like auto-expiration of images can create such a compelling and
|
||||||
|
challenging service. And it's not just me: everyone I've told about Snapchat
|
||||||
|
who has used it has loved it.
|
||||||
|
|
||||||
|
But I hate closed APIs, so I set about figuring out how it worked. [Adam
|
||||||
|
Caudill](http://adamcaudill.com/2012/06/16/snapchat-api-and-security/) wrote an
|
||||||
|
excellent analysis of their HTTP-based API by using an HTTPS traffic sniffer.
|
||||||
|
Unfortunately this information now seems out of date.
|
||||||
|
|
||||||
|
I ended up having to fetch the official Android client's app binary (APK),
|
||||||
|
decompiling the whole thing with a mix of tools (all of them seemed to produce
|
||||||
|
subtly incorrect output), and then puzzling through the process of creating
|
||||||
|
their dreaded access tokens (called req\_token in the HTTP calls).
|
||||||
|
|
||||||
|
Their system is a bit unusual: it AES-256 hashes the two input values
|
||||||
|
separately, using a secret key contained in the binary, and then uses a fixed
|
||||||
|
pattern string to pull bytes from one or the other. The final composition of
|
||||||
|
the two is used in HTTP requests.
|
||||||
|
|
||||||
|
Other things about the API that I've discovered so far:
|
||||||
|
|
||||||
|
- Speaks JSON over HTTPS, using POST as the verb
|
||||||
|
- Not made for human consumption; difficult error messaging
|
||||||
|
- Doesn't seem to support JSONP (i.e., callback parameter in post data is
|
||||||
|
ignored)
|
||||||
|
|
||||||
|
How to use
|
||||||
|
----------
|
||||||
|
|
||||||
|
Pretty simple:
|
||||||
|
|
||||||
|
```
|
||||||
|
require_once('snaphax/snaphax.php');
|
||||||
|
|
||||||
|
$opts = array();
|
||||||
|
$opts['username'] = 'username';
|
||||||
|
$opts['password'] = 'password';
|
||||||
|
$opts['debug'] = 1; // CHANGE THIS; major spewage
|
||||||
|
|
||||||
|
$s = new Snaphax($opts);
|
||||||
|
$result = $s->login();
|
||||||
|
var_dump($result);
|
||||||
|
```
|
||||||
|
|
||||||
|
The apocalyptic future
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The TODO list is almost endless at this point:
|
||||||
|
|
||||||
|
- Syncing (to mark snaps as seen)
|
||||||
|
- Video fetching
|
||||||
|
- Image/video posting
|
||||||
|
- Friend list maintenance
|
||||||
|
- Port to Javascript (probably via Node + NPM since their API doesn't seem to
|
||||||
|
support JSONP)
|
||||||
|
- Add support for PHP composer
|
||||||
|
|
||||||
|
Author
|
||||||
|
------
|
||||||
|
|
||||||
|
Made by [@tlack](http://twitter.com/tlack) with a lot of help from
|
||||||
|
[@adamcaudill](http://twitter.com/adamcaudill)
|
||||||
|
|
39
examples/fetch_all_new_photos.php
Normal file
39
examples/fetch_all_new_photos.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?
|
||||||
|
/*
|
||||||
|
Snaphax example: fetch all your new photo snaps
|
||||||
|
|
||||||
|
Run interactively like:
|
||||||
|
|
||||||
|
$ fetch_all_new_photos.php [username] [password]
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once('../snaphax.php');
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
global $argv;
|
||||||
|
|
||||||
|
if (count($argv) != 3) {
|
||||||
|
die("$argv[0]: usage $argv[0] [user] [pass]\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$opts = array();
|
||||||
|
$opts['username'] = $argv[1];
|
||||||
|
$opts['password'] = $argv[2];
|
||||||
|
$opts['debug'] = 1; // CHANGE THIS; major spewage
|
||||||
|
|
||||||
|
$s = new Snaphax($opts);
|
||||||
|
$result = $s->login();
|
||||||
|
var_dump($result);
|
||||||
|
foreach ($result['snaps'] as $snap) {
|
||||||
|
var_dump($snap['st']);
|
||||||
|
if ($snap['st'] == SnapHax::STATUS_NEW) {
|
||||||
|
echo "fetching $snap[id]";
|
||||||
|
$blob_data = $s->fetch($snap['id']);
|
||||||
|
if ($blob_data)
|
||||||
|
file_put_contents($snap['id'].'.jpg', $blob_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
194
snaphax.php
Normal file
194
snaphax.php
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
<?
|
||||||
|
/*
|
||||||
|
SnapHax: a library for communicating with Snaphax
|
||||||
|
Implements a subset of the Snaphax API
|
||||||
|
|
||||||
|
(c) Copyright 2012 Thomas Lackner <lackner@gmail.com>
|
||||||
|
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnaphaxApi {
|
||||||
|
function SnaphaxApi($options) {
|
||||||
|
$this->options = $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function debug($text) {
|
||||||
|
if ($this->options['debug'])
|
||||||
|
echo "SNAPHAX DEBUG: $text\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function blob($snap_id, $username, $auth_token) {
|
||||||
|
$un = urlencode($username);
|
||||||
|
$ts = time();
|
||||||
|
// $token = $this->hash($ts, $auth_token);
|
||||||
|
// $url = "https://feelinsonice.appspot.com/ph/blob?id=$snap_id&username=$un×tamp=$ts&req_token=$token";
|
||||||
|
$url = "/ph/blob";
|
||||||
|
$result = $this->postToEndpoint($url, array(
|
||||||
|
'id' => $snap_id,
|
||||||
|
'timestamp' => $ts,
|
||||||
|
'username' => $username,
|
||||||
|
), $auth_token, $ts, 0);
|
||||||
|
$this->debug('blob result: ' . $result);
|
||||||
|
$result_decoded = mcrypt_decrypt('rijndael-128', $this->options['blob_enc_key'], $result, 'ecb');
|
||||||
|
$this->debug('decoded: ' . $result_decoded);
|
||||||
|
if ($result_decoded[0] == chr(0xFF) &&
|
||||||
|
$result_decoded[1] == chr(0xD8)) {
|
||||||
|
return $result_decoded;
|
||||||
|
} else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function httpGet($url) {
|
||||||
|
$ch = curl_init();
|
||||||
|
|
||||||
|
curl_setopt($ch,CURLOPT_URL, $this->options['url'].$url);
|
||||||
|
curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($ch,CURLOPT_USERAGENT,'Snaphax 4.0.1 (iPad; iPhone OS 6.0; en_US)');
|
||||||
|
|
||||||
|
$this->debug($url);
|
||||||
|
|
||||||
|
// execute post
|
||||||
|
$result = curl_exec($ch);
|
||||||
|
$this->debug($result);
|
||||||
|
|
||||||
|
// close connection
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return json_decode($result, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postToEndpoint($endpoint, $post_data, $param1, $param2, $json=1) {
|
||||||
|
$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,'Snaphax 4.0.1 (iPad; iPhone OS 6.0; en_US)');
|
||||||
|
|
||||||
|
$post_data['req_token'] = $this->hash($param1, $param2);
|
||||||
|
curl_setopt($ch,CURLOPT_POST, count($post_data));
|
||||||
|
curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($post_data));
|
||||||
|
|
||||||
|
$this->debug(json_encode($post_data));
|
||||||
|
|
||||||
|
// execute post
|
||||||
|
$result = curl_exec($ch);
|
||||||
|
$this->debug($result);
|
||||||
|
|
||||||
|
// close connection
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($json)
|
||||||
|
return json_decode($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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Snaphax {
|
||||||
|
const STATUS_NEW = 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;
|
||||||
|
}
|
||||||
|
private function error($text) {
|
||||||
|
error_report($text);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function login() {
|
||||||
|
$ts = time();
|
||||||
|
$out = $this->api->postToEndpoint(
|
||||||
|
'/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) {
|
||||||
|
return $this->error('no auth token');
|
||||||
|
}
|
||||||
|
$blob = $this->api->blob($id,
|
||||||
|
$this->options['username'],
|
||||||
|
$this->auth_token);
|
||||||
|
return $blob;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue