├── .gitignore ├── .travis.yml ├── config.js.sample ├── tests └── index.js ├── cli.js ├── index.js ├── package.json ├── readme.md └── lib ├── icloud.js └── find-my-friends.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | ref 4 | config.js 5 | npm-debug.log 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "6.1" 5 | - "5.11" 6 | - "iojs" 7 | -------------------------------------------------------------------------------- /config.js.sample: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = { 5 | icloud: { 6 | email: '', 7 | password: '' 8 | } 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | 4 | 5 | describe('iCloud', function() { 6 | it('should have tests'); 7 | }); 8 | 9 | 10 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('./config.js'); 4 | var iCloud = require('./lib/icloud.js'); 5 | 6 | 7 | var client = new iCloud(config.icloud.email, config.icloud.password); 8 | 9 | client.getLocations(function () { 10 | console.log('done', arguments) 11 | }); 12 | 13 | 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('./config'); 4 | 5 | var express = require('express'); 6 | var app = express(); 7 | 8 | 9 | var iCloud = require('./lib/icloud.js'); 10 | 11 | 12 | app.get('/locations', function(req, res){ 13 | console.log(iCloud) 14 | var cloud = new iCloud(config.icloud.email, config.icloud.password); 15 | 16 | var locations = cloud.getLocations(function (err, locations) { 17 | console.log(err, locations) 18 | res.json(locations); 19 | res.send(); 20 | }); 21 | 22 | console.log('result', locations) 23 | }); 24 | 25 | 26 | app.listen(3001); 27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stalk-my-friends", 3 | "version": "0.1.3", 4 | "description": "Unofficial Apple Find My Friends API client.", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "repository": "madmod/stalk-my-friends", 8 | "author": { 9 | "name": "madmod", 10 | "email": "madmodmail@gmail.com", 11 | "url": "johnathanwells.com" 12 | }, 13 | "bin": "cli.js", 14 | "engines": { 15 | "node": ">=0.10.0" 16 | }, 17 | "scripts": { 18 | "test": "mocha --reporter nyan tests", 19 | "start": "node cli.js" 20 | }, 21 | "files": [ 22 | "index.js", 23 | "cli.js" 24 | ], 25 | "keywords": [ 26 | "apple", 27 | "find-my-friends" 28 | ], 29 | "dependencies": { 30 | "express": "^4.13.3", 31 | "https": "^1.0.0", 32 | "meow": "^3.3.0", 33 | "node-uuid": "^1.4.3", 34 | "request": "^2.74.0" 35 | }, 36 | "devDependencies": { 37 | "mocha": "*" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # stalk-my-friends [![Build Status](https://travis-ci.org/madmod/stalk-my-friends.svg?branch=master)](https://travis-ci.org/madmod/stalk-my-friends) 2 | 3 | Unofficial Apple Find My Friends API client. 4 | 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm install --save stalk-my-friends 10 | ``` 11 | 12 | 13 | ## CLI 14 | 15 | Copy `config.js.example` to `config.js` and add iCloud credentials. 16 | 17 | ``` 18 | node ./cli.js 19 | ``` 20 | 21 | ## TODO 22 | 23 | - Prevent Apple from thinking this is a "new device" every time it is used so we don't get a notification email. 24 | - Make a stable API and document it. 25 | - The API is a total mess with nonsense and redundancy everywhere. Clean it up. 26 | - Abstract away the apple specific response format into something much simpler and maybe find some kind of standard format for location data. 27 | - Cache logins and handle session renewal with the unfinished code in `lib/find-my-friends.js`. 28 | - Properly separate `lib/icloud.js` from `lib/find-my-friends.js`. 29 | 30 | 31 | ## License 32 | 33 | MIT © [madmod](http://johnathanwells.com) 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/icloud.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var uuid = require('node-uuid'); 5 | var https = require('https'); 6 | 7 | 8 | var iCloud = function iCloud(appleId, password, callback) { 9 | 10 | this.urls = { 11 | "version" : "https://www.icloud.com/system/version.json", 12 | "validate": "/setup/ws/1/validate?clientBuildNumber={0}&clientId={1}", 13 | "login": "/setup/ws/1/login?clientBuildNumber={0}&clientId={1}" 14 | } 15 | 16 | this.appleId = appleId; 17 | this.password = password; 18 | 19 | this.clientBuildNumber = '1P24'; 20 | this.clientId = uuid.v1().toString().toUpperCase(); 21 | 22 | // console.log('Generated UUID: ' + this.clientId); 23 | 24 | this.cookie = null; 25 | this.instance = null; 26 | 27 | return this; 28 | } 29 | 30 | 31 | iCloud.prototype = { 32 | getLocations: function (callback) { 33 | var me = this; 34 | 35 | var endpoint = this.urls.login 36 | .replace('{0}', this.clientBuildNumber) 37 | .replace('{1}', this.clientId); 38 | 39 | console.log(endpoint); 40 | 41 | var options = { 42 | host: "setup.icloud.com", 43 | path: endpoint, 44 | method: 'POST', 45 | headers: { 46 | 'Origin': 'https://www.icloud.com', 47 | 'Referer': 'https://www.icloud.com', 48 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36' 49 | } 50 | }; 51 | 52 | var data = JSON.stringify({ 53 | apple_id: this.appleId, 54 | password: this.password, 55 | extended_login: false 56 | }); 57 | 58 | var request = https.request(options, function(res) { 59 | console.log('request') 60 | 61 | if (res.headers['set-cookie']) me.cookie = res.headers['set-cookie']; 62 | 63 | var buffer = ''; 64 | 65 | res.on('data', function(data) { 66 | buffer += data; 67 | }); 68 | 69 | res.on('end', function() { 70 | 71 | me.instance = JSON.parse(buffer); 72 | 73 | console.log('instance', me.instance); 74 | 75 | var dsid = me.instance.dsInfo.dsid; 76 | var getFmfUrl = '/fmipservice/client/fmfWeb/initClient?clientBuildNumber={1}&clientId={2}&dsid={3}' 77 | .replace('{1}', me.clientBuildNumber) 78 | .replace('{2}', me.clientId) 79 | .replace('{3}', dsid); // &id={4} 80 | 81 | console.log(getFmfUrl); 82 | 83 | console.log(me.instance.webservices) 84 | var options2 = { 85 | host: me.instance.webservices.fmf.url.replace('https://', '').replace(':443', ''), 86 | path: getFmfUrl, 87 | method: 'POST', 88 | headers: { 89 | 'Origin': 'https://www.icloud.com', 90 | 'Referer': 'https://www.icloud.com', 91 | 'Cookie': me.cookie.join('; '), 92 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36' 93 | } 94 | }; 95 | 96 | var req2 = https.request(options2, function(res) { 97 | 98 | var buf2 = ''; 99 | res.on('data', function(data) { 100 | buf2 += data; 101 | }); 102 | 103 | res.on('end', function() { 104 | console.log(buf2) 105 | var data = JSON.parse(buf2); 106 | console.log(JSON.stringify(data, null, 2)); 107 | callback(null, data); 108 | }); 109 | }); 110 | 111 | req2.write('{"dataContext":null,"serverContext":null,"clientContext":{"productType":"fmfWeb","appVersion":"1.0","contextApp":"com.icloud.web.fmf","userInactivityTimeInMS":537,"windowInFocus":false,"windowVisible":true,"mapkitAvailable":true,"tileServer":"Apple"}}'); 112 | req2.end(); 113 | 114 | }); 115 | }); 116 | 117 | request.write(data); 118 | 119 | request.end(); 120 | } 121 | }; 122 | 123 | 124 | module.exports = iCloud; 125 | 126 | -------------------------------------------------------------------------------- /lib/find-my-friends.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'); 4 | var util = require('util'); 5 | 6 | 7 | var findmyfriends = { 8 | 9 | init: function (credentials, callback) { 10 | if (!credentials) 11 | return callback(TypeError('Missing required argument "credentials".')); 12 | 13 | var newLogin = !findmyfriends.jar; 14 | if (newLogin) findmyfriends.jar = request.jar(); 15 | 16 | findmyfriends.request = request.defaults({ 17 | jar: findmyfriends.jar, 18 | headers: { 19 | 'Origin': 'https://www.icloud.com' 20 | } 21 | }); 22 | 23 | if (newLogin) { 24 | findmyfriends.login(credentials, function (err, res, body) { 25 | return callback(err, res, body); 26 | }); 27 | } 28 | else { 29 | findmyfriends.checkSession(function (err, res, body) { 30 | if (err) { 31 | // session is dead, start new 32 | findmyfriends.jar = null; 33 | findmyfriends.jar = request.jar(); 34 | 35 | findmyfriends.login(credentials, function (err, res, body) { 36 | return callback(err, res, body); 37 | }); 38 | } else { 39 | console.log('reusing session'); 40 | return callback(err, res, body); 41 | } 42 | }); 43 | } 44 | }, 45 | 46 | login: function (credentials, callback) { 47 | if (!credentials.email || !credentials.password) 48 | return callback(TypeError('Argument "credentials" missing required keys "email" and "password".')); 49 | 50 | var options = { 51 | url: 'https://setup.icloud.com/setup/ws/1/login', 52 | json: { 53 | 'apple_id': credentials.email, 54 | 'password': credentials.password, 55 | 'extended_login': true 56 | } 57 | }; 58 | 59 | findmyfriends.request.post(options, function (error, response, body) { 60 | if (!response || response.statusCode != 200) { 61 | console.error('Login failed', response); 62 | return callback(Error('Login Error')); 63 | } 64 | 65 | findmyfriends.onLogin(body, function (err, resp, body) { 66 | return callback(err, resp, body); 67 | }); 68 | }); 69 | }, 70 | 71 | checkSession: function (callback) { 72 | var options = { 73 | url: 'https://setup.icloud.com/setup/ws/1/validate', 74 | }; 75 | 76 | findmyfriends.request.post(options, function (error, response, body) { 77 | if (!response || response.statusCode != 200) { 78 | return callback('Could not refresh session'); 79 | } 80 | 81 | findmyfriends.onLogin(body, function (err, resp, body) { 82 | return callback(err, resp, body); 83 | }); 84 | }); 85 | }, 86 | 87 | onLogin: function (body, callback) { 88 | console.log('on login body', body); 89 | 90 | if (body.hasOwnProperty('webservices') && body.webservices.hasOwnProperty('findme')) { 91 | findmyfriends.base_path = body.webservices.findme.url; 92 | 93 | console.log('findmyfriends.base_path', findmyfriends.base_path); 94 | 95 | /* 96 | options = { 97 | url: findmyfriends.base_path + '/fmipservice/client/web/initClient', 98 | json: { 99 | 'clientContext': { 100 | 'appName': 'iCloud Find (Web)', 101 | 'appVersion': '2.0', 102 | 'timezone': 'US/Eastern', 103 | 'inactiveTime': 3571, 104 | 'apiVersion': '3.0', 105 | 'fmly': true 106 | } 107 | } 108 | }; 109 | 110 | findmyfriends.request.post(options, callback); 111 | */ 112 | callback(); 113 | } 114 | else { 115 | return callback(Error('Login response missing findmyfriends base path.')); 116 | } 117 | } 118 | 119 | /* 120 | getDevices: function (callback) { 121 | findmyfriends.init(function (error, response, body) { 122 | if (!response || response.statusCode != 200) { 123 | return callback(error); 124 | } 125 | 126 | var devices = []; 127 | 128 | // Retrieve each device on the account 129 | body.content.forEach(function (device) { 130 | devices.push({ 131 | id: device.id, 132 | name: device.name, 133 | deviceModel: device.deviceModel, 134 | modelDisplayName: device.modelDisplayName, 135 | deviceDisplayName: device.deviceDisplayName, 136 | batteryLevel: device.batteryLevel, 137 | isLocating: device.isLocating, 138 | lostModeCapable: device.lostModeCapable, 139 | location: device.location 140 | }); 141 | }); 142 | 143 | callback(error, devices); 144 | }); 145 | } 146 | */ 147 | 148 | }; 149 | 150 | 151 | module.exports = findmyfriends.init; 152 | 153 | 154 | --------------------------------------------------------------------------------