├── .gitignore ├── .DS_Store ├── demo ├── mail │ ├── getFolders.js │ ├── listMessages.js │ ├── createFolder.js │ ├── getMessage.js │ ├── deleteMessages.js │ ├── flagMessages.js │ ├── unflagMessages.js │ ├── moveMessages.js │ ├── deleteFolder.js │ ├── renameFolder.js │ ├── moveFolder.js │ └── sendMail.js ├── photos │ └── getPhotos.js ├── friends │ └── getLocations.js ├── calendar │ ├── getCollections.js │ ├── getEvents.js │ ├── createCollection.js │ ├── deleteEvent.js │ ├── changeEvent.js │ └── createEvent.js ├── drive │ ├── getItem.js │ ├── createFolders.js │ ├── deleteItems.js │ └── renameItems.js ├── contacts │ ├── listContacts.js │ ├── newGroups.js │ ├── newContact.js │ ├── changeContact.js │ ├── deleteContact.js │ ├── changeGroups.js │ └── deleteGroups.js ├── reminders │ ├── getOpenTasks.js │ ├── getCompletedTasks.js │ ├── createCollection.js │ ├── createTask.js │ ├── deleteTask.js │ ├── completeTask.js │ ├── deleteCollection.js │ ├── changeTask.js │ └── changeCollection.js ├── findme │ ├── findMe.js │ └── playSound.js ├── push │ └── notes.js ├── notes │ ├── fetch.js │ ├── resolveNotes.js │ └── getFolders.js └── prompt-credentials.js ├── resources ├── protobuf │ ├── versioned-document.proto │ ├── crframework.proto │ └── topotext.proto ├── helper.js ├── apps │ ├── Friends.js │ ├── Contacts.js │ ├── Photos.js │ ├── FindMe.js │ ├── Notes.js │ ├── Reminders.js │ ├── Mail.js │ ├── Drive.js │ └── Calendar.js └── apps.json ├── package.json ├── main.js └── setup.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElyaConrad/iCloud-API/HEAD/.DS_Store -------------------------------------------------------------------------------- /demo/mail/getFolders.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | console.log(folders); 11 | })(); 12 | -------------------------------------------------------------------------------- /demo/photos/getPhotos.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const photos = await myCloud.Photos.get(); 9 | 10 | console.log(photos); 11 | 12 | })(); 13 | -------------------------------------------------------------------------------- /demo/friends/getLocations.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const locations = await myCloud.Friends.getLocations(); 9 | 10 | console.log(locations); 11 | 12 | })(); 13 | -------------------------------------------------------------------------------- /demo/calendar/getCollections.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const collections = await myCloud.Calendar.getCollections(); 9 | 10 | console.log(collections); 11 | })(); 12 | -------------------------------------------------------------------------------- /demo/calendar/getEvents.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | var events = await myCloud.Calendar.getEvents("2018-11-01", "2018-11-30"); 9 | 10 | console.log(events); 11 | })(); 12 | -------------------------------------------------------------------------------- /demo/drive/getItem.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | var itemInfo = await myCloud.Drive.getItem("FOLDER::com.apple.CloudDocs::root"); 9 | 10 | console.log(itemInfo); 11 | })(); 12 | -------------------------------------------------------------------------------- /demo/contacts/listContacts.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | console.log("Getting contacts..."); 9 | 10 | var contactsData = await myCloud.Contacts.list(); 11 | console.log(contactsData); 12 | })(); 13 | -------------------------------------------------------------------------------- /demo/reminders/getOpenTasks.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const collectionsWithOpenTasks = await myCloud.Reminders.getOpenTasks(); 9 | 10 | console.log(collectionsWithOpenTasks); 11 | 12 | })(); 13 | -------------------------------------------------------------------------------- /demo/findme/findMe.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | try { 9 | var devices = await myCloud.FindMe.get(); 10 | } 11 | catch (e) { 12 | console.error(e); 13 | } 14 | console.log(devices); 15 | })(); 16 | -------------------------------------------------------------------------------- /demo/reminders/getCompletedTasks.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const collectionsWithCompletedTasks = await myCloud.Reminders.getCompletedTasks(); 9 | 10 | console.log(collectionsWithCompletedTasks); 11 | 12 | })(); 13 | -------------------------------------------------------------------------------- /demo/mail/listMessages.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const messages = await myCloud.Mail.listMessages(folders[0], 1, 50); 11 | 12 | console.log(messages); 13 | })(); 14 | -------------------------------------------------------------------------------- /demo/drive/createFolders.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | console.log("Creating new folder 'this folder is new' at '/'..."); 9 | 10 | var createChangeset = await myCloud.Drive.createFolders("/", ["this folder is new"]); 11 | 12 | console.log(createChangeset); 13 | 14 | })(); 15 | -------------------------------------------------------------------------------- /demo/push/notes.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | myCloud.initPush(); 9 | 10 | myCloud.on("push", function(notification) { 11 | console.log("New push notification!"); 12 | console.log(notification); 13 | 14 | // demonstrate deactivating push after one notification 15 | myCloud.deactivatePush(); 16 | }); 17 | 18 | })(); 19 | -------------------------------------------------------------------------------- /resources/protobuf/versioned-document.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto2"; 3 | 4 | package versioned_document; 5 | 6 | option optimize_for = LITE_RUNTIME; 7 | 8 | message Document { 9 | // Just in case. 10 | optional uint32 serializationVersion = 1; 11 | 12 | repeated Version version = 2; 13 | } 14 | 15 | message Version { 16 | optional uint32 serializationVersion = 1; 17 | optional uint32 minimumSupportedVersion = 2; 18 | 19 | // Interpreted as a topotext.String 20 | // Archived as bytes to ensure bit-perfect backward compatibility. 21 | optional bytes data = 3; 22 | } 23 | -------------------------------------------------------------------------------- /demo/mail/createFolder.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | console.log("Creating folder 'My new folder' at top level..."); 9 | 10 | const createChangeset = await myCloud.Mail.createFolder({ 11 | name: "My new folder", // Default null 12 | parent: null // Can be a folder object or a guid string (Default null) 13 | }); 14 | 15 | console.log(createChangeset); 16 | })(); 17 | -------------------------------------------------------------------------------- /demo/reminders/createCollection.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | console.log("Creating new collection 'My new collection' with symblic color 'green'..."); 9 | 10 | const createChangeset = await myCloud.Reminders.createCollection({ 11 | title: "My new collection", 12 | symbolicColor: "green" // Default 'auto' 13 | }); 14 | 15 | console.log(createChangeset); 16 | 17 | })(); 18 | -------------------------------------------------------------------------------- /demo/mail/getMessage.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const messagesData = await myCloud.Mail.listMessages(folders[0], 1, 50); 11 | 12 | console.log("Getting first message's details..."); 13 | 14 | const myMessageDetailed = await myCloud.Mail.getMessage(messagesData.messages[0]); 15 | 16 | console.log(myMessageDetailed); 17 | })(); 18 | -------------------------------------------------------------------------------- /demo/notes/fetch.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | // Get all notes 9 | const notes = myCloud.Notes.fetch(); 10 | 11 | var done = false; 12 | 13 | notes.on("data", zone => { 14 | 15 | console.log("Fetched: " + notes.__folders.length + " folders; " + notes.__notes.length + " notes"); 16 | 17 | // Log folders structured 18 | console.log(notes.folders); 19 | 20 | }); 21 | 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /demo/mail/deleteMessages.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const messages = (await myCloud.Mail.listMessages(folders[0], 1, 50)).messages; 11 | 12 | console.log("Deleting first message..."); 13 | 14 | const deleteChangeset = await myCloud.Mail.delete([ 15 | messages[0], 16 | //myMessages[1] 17 | ]); 18 | 19 | console.log(deleteChangeset); 20 | })(); 21 | -------------------------------------------------------------------------------- /demo/mail/flagMessages.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const messages = (await myCloud.Mail.listMessages(folders[0], 1, 50)).messages; 11 | 12 | console.log("Flagging first message..."); 13 | 14 | const flagChangset = await myCloud.Mail.flag([ 15 | messages[0], 16 | //myMessages[1] 17 | ], "flagged"); 18 | 19 | console.log(flagChangset); 20 | })(); 21 | -------------------------------------------------------------------------------- /demo/mail/unflagMessages.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const messages = (await myCloud.Mail.listMessages(folders[0], 1, 50)).messages; 11 | 12 | console.log("Unflagging first message..."); 13 | 14 | const unflagChangset = await myCloud.Mail.unflag([ 15 | messages[0], 16 | //myMessages[1] 17 | ], "flagged"); 18 | 19 | console.log(unflagChangset); 20 | })(); 21 | -------------------------------------------------------------------------------- /demo/calendar/createCollection.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | var createChangeset = await myCloud.Calendar.createCollection({ 9 | // Properties for your collection (Everything like id's will be calculated automatically) 10 | title: "My Collection!", // The name of the collection 11 | color: "#c4ff70" // The color that will be displayed in iCloud clients and represent the collection (Optional default is #ff2d55) 12 | }); 13 | 14 | console.log(createChangeset); 15 | })(); 16 | -------------------------------------------------------------------------------- /demo/drive/deleteItems.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const rootInfo = await myCloud.Drive.getItem("FOLDER::com.apple.CloudDocs::root"); 9 | 10 | const deletionFile = rootInfo.items.find(item => item.type === "FILE"); 11 | 12 | console.log("Deleting " + deletionFile.name + "." + deletionFile.extension + "..."); 13 | 14 | 15 | var deleteChangeset = await myCloud.Drive.deleteItems([ 16 | deletionFile.drivewsid 17 | ]); 18 | 19 | console.log(deleteChangeset); 20 | 21 | })(); 22 | -------------------------------------------------------------------------------- /demo/findme/playSound.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | try { 9 | var devices = await myCloud.FindMe.get(); 10 | } 11 | catch (e) { 12 | console.error(e); 13 | } 14 | const myPhone = devices.content[0]; 15 | 16 | console.log("Playing sound on " + myPhone.name + "..."); 17 | 18 | try { 19 | const res = await myCloud.FindMe.playSound(myPhone.id); 20 | 21 | console.log("Successfully played sound!"); 22 | } catch (e) { 23 | console.error(e); 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /demo/contacts/newGroups.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require('../prompt-credentials'); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | // specify groups to create 9 | const groupNames = ['my-new-group-1', 'my-new-group-2']; 10 | console.log(`Creating new contact group (${groupNames})...`); 11 | 12 | // list contacts to get sync token 13 | await myCloud.Contacts.list(); 14 | 15 | // Promise 16 | const groups = groupNames.map(x => ({ name: x })); 17 | const newChangeset = await myCloud.Contacts.newGroups(groups); 18 | 19 | console.log('Changeset:', newChangeset); 20 | })(); 21 | -------------------------------------------------------------------------------- /demo/contacts/newContact.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | console.log("Creating new contact (Maurice Conrad)..."); 9 | 10 | await myCloud.Contacts.list(); 11 | 12 | // Promise 13 | var newChangeset = await myCloud.Contacts.new({ 14 | firstName: 'Maurice', 15 | lastName: 'Conrad', 16 | emailAddresses: [ 17 | { 18 | label: "Privat", 19 | field: "conr.maur@googlemail.com" 20 | } 21 | ], 22 | isCompany: false, 23 | }); 24 | 25 | console.log("Changeset:", newChangeset); 26 | })(); 27 | -------------------------------------------------------------------------------- /demo/mail/moveMessages.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const messages = (await myCloud.Mail.listMessages(folders[0], 1, 50)).messages; 11 | 12 | console.log("Moving first message into '" + folders[1].name + "'..."); 13 | 14 | var movementChangset = await myCloud.Mail.move([ 15 | messages[0], // The messages have to be in the same folder! 16 | //myMessages[1] // Otherwise an error will occur! 17 | ], folders[1]); 18 | 19 | console.log(movementChangset); 20 | })(); 21 | -------------------------------------------------------------------------------- /demo/reminders/createTask.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | console.log("Creating task 'I have to do this!' with priority 1..."); 9 | 10 | const createChangeset = await myCloud.Reminders.createTask({ 11 | title: "I have to do this!", 12 | pGuid: "tasks", // The 'guid' of a collection you got from ('getOpenTasks()' || 'getCompletedTasks') 13 | priority: 1, // 1 is "High", 5 is "Medium" & 9 is "Low", 14 | description: "This describes my task perfectly!" 15 | }); 16 | 17 | console.log(createChangeset); 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /demo/contacts/changeContact.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | await myCloud.Contacts.list(); 9 | 10 | var contactsData = await myCloud.Contacts.list(); 11 | 12 | var firstContact = contactsData.contacts[0]; 13 | 14 | console.log("Changing first contact's(" + firstContact.firstName + " " + firstContact.lastName + ") last name to " + "'NewLastName'..."); 15 | 16 | firstContact.lastName = "NewLastName2"; 17 | 18 | var changeset = await myCloud.Contacts.change(firstContact); 19 | 20 | console.log("Changeset: ", changeset); 21 | })(); 22 | -------------------------------------------------------------------------------- /demo/mail/deleteFolder.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const userCustomFolder = folders.find(folder => folder.role == "FOLDER"); 11 | 12 | if (userCustomFolder) { 13 | console.log("Deleting '" + userCustomFolder.name + "'..."); 14 | 15 | const deleteChangeset = await myCloud.Mail.deleteFolder(userCustomFolder); 16 | 17 | console.log(deleteChangeset); 18 | } 19 | else { 20 | console.error("No custom user folder found that could be deleted."); 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /demo/reminders/deleteTask.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const collectionsWithOpenTasks = await myCloud.Reminders.getOpenTasks(); 9 | 10 | const myTask = collectionsWithOpenTasks[0].tasks[0]; 11 | 12 | if (myTask) { 13 | console.log("Deleting task '" + myTask.title + "'..."); 14 | 15 | const deletionChangeset = await myCloud.Reminders.deleteTask(myTask); 16 | 17 | console.log(deletionChangeset); 18 | } 19 | else { 20 | console.error("No task found within in the first collection that could be deleted."); 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /demo/calendar/deleteEvent.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | var events = await myCloud.Calendar.getEvents("2018-11-01", "2018-11-30"); 9 | 10 | const deletingEvent = events[0]; 11 | 12 | console.log("Delete " + deletingEvent.title + " event..."); 13 | 14 | if (deletingEvent) { 15 | // 2nd argument defines wether all recurring events should be deleted 16 | var deleteChangeset = await myCloud.Calendar.deleteEvent(events[0], false); 17 | 18 | console.log(deleteChangeset); 19 | } 20 | console.error("No date found in the given time area"); 21 | })(); 22 | -------------------------------------------------------------------------------- /demo/mail/renameFolder.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const userCustomFolder = folders.find(folder => folder.role == "FOLDER"); 11 | 12 | if (userCustomFolder) { 13 | console.log("Renaming '" + userCustomFolder.name + "' to 'New name!'..."); 14 | 15 | var renameChangeset = await myCloud.Mail.renameFolder(userCustomFolder, "New name!"); 16 | 17 | console.log(renameChangeset); 18 | } 19 | else { 20 | console.error("No custom user folder found that could be renamed."); 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /demo/reminders/completeTask.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const collectionsWithOpenTasks = await myCloud.Reminders.getOpenTasks(); 9 | 10 | const myTask = collectionsWithOpenTasks[0].tasks[0]; 11 | 12 | if (myTask) { 13 | console.log("Complete task '" + myTask.title + "'..."); 14 | 15 | const completedChangeset = await myCloud.Reminders.completeTask(myTask); 16 | 17 | console.log(completedChangeset); 18 | } 19 | else { 20 | console.error("No open task found within in the first collection that could be completed."); 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /demo/contacts/deleteContact.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | await myCloud.Contacts.list(); 9 | 10 | var contactsData = await myCloud.Contacts.list(); 11 | 12 | // Use the first contact 13 | var myContactIDontLike = contactsData.contacts[0]; 14 | 15 | console.log("Deleting first contact(" + myContactIDontLike.firstName + " " + myContactIDontLike.lastName + ")..."); 16 | 17 | // Attention! This will delete the first contact! 18 | var delChangeset = await myCloud.Contacts.delete(myContactIDontLike); 19 | 20 | console.log("Deleteing Changeset:", delChangeset); 21 | })(); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apple-icloud", 3 | "version": "1.0.2", 4 | "description": "iCloud API", 5 | "keywords": [ 6 | "icloud", 7 | "apple", 8 | "cloud", 9 | "cloudkit", 10 | "api", 11 | "reminders", 12 | "mail", 13 | "notes", 14 | "contacts", 15 | "drive", 16 | "find", 17 | "my", 18 | "iphone", 19 | "me", 20 | "friends", 21 | "photos" 22 | ], 23 | "license": "ISC", 24 | "author": "Maurice Conrad", 25 | "main": "main.js", 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/MauriceConrad/iCloud-API.git" 29 | }, 30 | "scripts": { 31 | "test": "echo \"Error: no test specified\" && exit 1" 32 | }, 33 | "dependencies": { 34 | "cookie": "^0.3.1", 35 | "prompt": "^1.0.0", 36 | "protobufjs": "^6.8.8", 37 | "request": "^2.88.0", 38 | "zlib": "^1.0.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demo/reminders/deleteCollection.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const collectionsWithOpenTasks = await myCloud.Reminders.getOpenTasks(); 9 | 10 | const collectionToDelete = collectionsWithOpenTasks[1]; 11 | 12 | if (collectionToDelete) { 13 | console.log("Deleting collection '" + collectionToDelete.title + "'..."); 14 | 15 | const deletionChangeset = await myCloud.Reminders.deleteCollection(collectionToDelete); 16 | 17 | console.log(deletionChangeset); 18 | } 19 | else { 20 | console.error("No custom collection found that could be deleted..."); 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /demo/reminders/changeTask.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const collectionsWithOpenTasks = await myCloud.Reminders.getOpenTasks(); 9 | 10 | const myTask = collectionsWithOpenTasks[0].tasks[0]; 11 | 12 | if (myTask) { 13 | console.log("Changing task '" + myTask.title + "'s title to 'new title!'..."); 14 | 15 | myTask.title = "new title!"; 16 | const changeset = await myCloud.Reminders.changeTask(myTask); 17 | 18 | console.log(changeset); 19 | } 20 | else { 21 | console.error("No task found within in the first collection that could be changed."); 22 | } 23 | 24 | })(); 25 | -------------------------------------------------------------------------------- /demo/reminders/changeCollection.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const collectionsWithOpenTasks = await myCloud.Reminders.getOpenTasks(); 9 | 10 | if (collectionsWithOpenTasks[0]) { 11 | console.log("Setting collection '" + collectionsWithOpenTasks[0].title + "'s symbolic color to 'red'"); 12 | 13 | collectionsWithOpenTasks[0].symbolicColor = "red"; 14 | 15 | const changeset = await myCloud.Reminders.changeCollection(collectionsWithOpenTasks[0]); 16 | 17 | console.log(changeset); 18 | } 19 | else { 20 | console.error("No collection found with at least one open task..."); 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /demo/mail/moveFolder.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const folders = await myCloud.Mail.getFolders(); 9 | 10 | const customFolders = folders.filter(folder => folder.role == "FOLDER"); 11 | 12 | if (customFolders.length >= 2) { 13 | console.log("Move '" + customFolders[0].name + "' into '" + customFolders[1].name + "'..."); 14 | 15 | const movementChangeset = await myCloud.Mail.moveFolder(customFolders[0], customFolders[1]); 16 | 17 | console.log(movementChangeset); 18 | } 19 | else { 20 | console.error("You need at least two custom folders to move the first into the second one."); 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /demo/drive/renameItems.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const rootInfo = await myCloud.Drive.getItem("FOLDER::com.apple.CloudDocs::root"); 9 | 10 | const firstFile = rootInfo.items.find(item => item.type === "FILE"); 11 | 12 | console.log("Renaming " + firstFile.name + "." + firstFile.extension + " to 'new_name." + firstFile.extension + "'..."); 13 | 14 | var renameChangeset = await myCloud.Drive.renameItems({ 15 | [firstFile.drivewsid]: "new name." + firstFile.extension, 16 | //"Oh/another/item.txt": "renamed.txt" // Of course you can use a drivewsid 17 | }); 18 | 19 | console.log(renameChangeset); 20 | 21 | })(); 22 | -------------------------------------------------------------------------------- /demo/mail/sendMail.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const sendResult = await myCloud.Mail.send({ 9 | //from: "Your Name", If not given, your address will be found automatically 10 | to: "conr.maur@googlemail.com", // Required 11 | subject: "Your API", 12 | body: "Hey Maurice,

I totally love your API. Of course it's not perfect but I love it", // HTML string 13 | attachments: [ // Optional 14 | // Your attachments 15 | // Not implemented yet 16 | // Coming soon 17 | ] 18 | }); 19 | 20 | console.log(sendResult); 21 | })(); 22 | -------------------------------------------------------------------------------- /demo/calendar/changeEvent.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | var events = await myCloud.Calendar.getEvents("2018-11-01", "2018-11-30"); 9 | 10 | 11 | const changingEvent = events[0]; 12 | 13 | console.log("Changing " + changingEvent.title + " event's location to 'Mainz am Rhein'..."); 14 | 15 | if (changingEvent) { 16 | changingEvent.location = "Mainz am Rhein"; 17 | 18 | // 2nd argument: true - If you want to change all recurred events ('recurrence') (Default is false) 19 | var changeset = await myCloud.Calendar.changeEvent(changingEvent, true); 20 | 21 | console.log(changeset); 22 | } 23 | else { 24 | console.error("No date found in the given time area"); 25 | } 26 | 27 | })(); 28 | -------------------------------------------------------------------------------- /demo/contacts/changeGroups.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | const groupName = 'my-new-group-1'; 9 | const contactFirstName = 'Allice'; 10 | 11 | const { contacts, groups } = await myCloud.Contacts.list(); 12 | 13 | const group = groups.find(x => x.name === groupName); 14 | const contact = contacts.find(x => x.firstName === contactFirstName); 15 | 16 | if (!group) throw new Error('could not find group ' + groupName); 17 | if (!contact) throw new Error('could not find contact ' + contactName); 18 | 19 | console.log(`Adding ${contact.firstName} to ${group.name}`) 20 | 21 | // magic 22 | group.contactIds.push(contact.contactId); 23 | const changeset = await myCloud.Contacts.changeGroups(group); 24 | 25 | console.log("Changeset: ", changeset); 26 | })(); 27 | -------------------------------------------------------------------------------- /demo/notes/resolveNotes.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | // Get all notes 9 | const notes = myCloud.Notes.fetch(); 10 | 11 | var done = false; 12 | 13 | notes.on("data", async zone => { 14 | 15 | console.log("Fetched: " + notes.__folders.length + " folders; " + notes.__notes.length + " notes"); 16 | 17 | // If at least 1 folder is known 18 | if (notes.folders.length > 0) { 19 | // Select the folder 20 | const myFolder = notes.folders[0]; 21 | // Select the note we want to resolve() 22 | const myNote = myFolder.notes[0]; 23 | 24 | // Get note detailed 25 | const myNoteDetailed = (await myCloud.Notes.resolve(myNote))[0]; 26 | 27 | console.log(myNoteDetailed); 28 | 29 | } 30 | 31 | }); 32 | 33 | 34 | })(); 35 | -------------------------------------------------------------------------------- /demo/contacts/deleteGroups.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require('../prompt-credentials'); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | // specify groups to delete 9 | const groupNames = ['my-new-group-1', 'my-new-group-2']; 10 | 11 | // must call list at least once to get sync token 12 | const { groups: existingGroups } = await myCloud.Contacts.list(); 13 | const groups = []; 14 | groupNames.forEach(groupName => { 15 | const group = existingGroups.find(x => x.name === groupName); 16 | if (!group) throw new Error(`Could not find group ${groupName}`); 17 | 18 | groups.push(group); 19 | }) 20 | 21 | console.log(`Deleting contact groups (${groups.map(x => x.name)})...`); 22 | 23 | // Promise 24 | const newChangeset = await myCloud.Contacts.deleteGroups(groups); 25 | 26 | console.log('Changeset:', newChangeset); 27 | })(); 28 | -------------------------------------------------------------------------------- /demo/calendar/createEvent.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | var createChangeset = await myCloud.Calendar.createEvent({ 9 | title: "Best Event ever", // Required 10 | location: "Mainz am Rhein", // Optional 11 | description: "This is the best description ever", // Optional 12 | url: "https://maurice-conrad.eu", // Optional 13 | pGuid: "home", // 'guid' of collection 14 | alarms: [ // Lists all alarms (Optional) 15 | { 16 | before: true, 17 | weeks: 0, 18 | days: 0, 19 | hours: 0, 20 | minutes: 35, 21 | seconds: 0 22 | } 23 | ], 24 | // Describes the rule to repeat the event 25 | recurrence: { 26 | count: 3, // How many times the event will be repeated (Optional, default is Infinity) 27 | freq: "daily", // Type of frequence (e.g. 'daily', 'weekly') 28 | interval: 10 // Interval for frequence 29 | }, 30 | startDate: new Date("2018-11-05 12:00"), // UTC Time is required from local, therefore the event start time means your local 12:00) 31 | endDate: new Date("2018-11-05 13:00") // Same here 32 | }); 33 | 34 | console.log(createChangeset); 35 | })(); 36 | -------------------------------------------------------------------------------- /demo/notes/getFolders.js: -------------------------------------------------------------------------------- 1 | // Require function that already logs in and asks for the credentials if needed 2 | const promptiCloud = require("../prompt-credentials"); 3 | 4 | (async () => { 5 | // Login to icloud and ask for new credentials if needed 6 | const myCloud = await promptiCloud(); 7 | 8 | // Get all notes without callback because we handle it only with "progress" event 9 | myCloud.Notes.getAll(); 10 | 11 | // Handler 12 | myCloud.on("progress", function(event) { 13 | // Relate to the 'getAll()' method 14 | if (event.parentAction === "getAll") { 15 | // The 'zone' object is not important. Important are the 'records' within it 16 | // These records are new and may contain Notes or Folders 17 | // What they are actually containing is described in their object structure 18 | 19 | // Here you can have a look at every record's 'parent' property and when the parent folder isn't loaded as record, you can load it manually with 'getFolders()' because it accepts also 'recordName' as arguments 20 | 21 | // What we are doing here is JUST an example. As you can see, firstly we get notes from 'getAll()' method and secondly we call 'getFolders()' to all of these notes. 22 | // This is just to demonstrate the method 'getFolders()' and the difference to 'getAll()'. 'getFolders()' gets folders directly if you know their object literal or just their 'recordName' ! 23 | 24 | // 'getFolders()' can be useful when you are using 'getAll()' and you know a Note record already and its 'parentFolder' property describes a folder you actually don't know. 25 | // But because of the 'parentFolder' property of the note, you know the 'recordName' of the folder. Now, you can request the folder directly and you don't have to wait, that the folder comes as record from 'getAll()' 26 | 27 | 28 | for (let noteRecord of event.zone.records.filter(record => record.recordType == "Note")) { 29 | console.log("Getting current note's parent folder..."); 30 | myCloud.Notes.getFolders([ // Can also be a single folder object or a single 'recordName' string if it's only one 31 | noteRecord.parent 32 | ], function(err, folders) { 33 | // If an error occurs 34 | if (err) return console.error(err); 35 | // Array with your folder's data 36 | console.log(require('util').inspect(folders, { depth: null })); 37 | }); 38 | } 39 | 40 | } 41 | }); 42 | 43 | 44 | })(); 45 | -------------------------------------------------------------------------------- /demo/prompt-credentials.js: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE 3 | THis file is just a ready-to-use application that logs in to icloud and asks for the required credentials if needed 4 | */ 5 | 6 | const iCloud = require('../'); 7 | 8 | var prompt = require('prompt'); 9 | 10 | // Handles the requirement of two-factor-authentication 11 | async function readyHandlerTwoFactor(myCloud) { 12 | 13 | if (myCloud.twoFactorAuthenticationIsRequired) { 14 | prompt.get(["Security Code"], function(err, input) { 15 | if (err) return console.error(err); 16 | const code = input["Security Code"]; 17 | myCloud.securityCode = code; 18 | }); 19 | return false; 20 | } 21 | else { 22 | console.log("You are logged in successfully!"); 23 | 24 | return true; 25 | } 26 | 27 | } 28 | 29 | const sessionPath = "/tmp/icloud-session.json"; 30 | 31 | module.exports = function login() { 32 | return new Promise(function(resolve, reject) { 33 | prompt.start(); 34 | 35 | var myCloud = new iCloud(sessionPath); 36 | myCloud.on("ready", async function() { 37 | // Returns 38 | const isAutheticated = await readyHandlerTwoFactor(myCloud); 39 | if (isAutheticated) { 40 | resolve(myCloud); 41 | } 42 | }); 43 | myCloud.on("err", function(err) { 44 | if (err.errorCode == 6) { 45 | //console.error("Given session does not eixst or is expired. Try to use contained credentials from session to re-login..."); 46 | } 47 | // Error code 7: Invalid credentials or borken account 48 | if (err.errorCode == 7) { 49 | console.error("The contained credentials are not correct or the given session does not exist/is broken."); 50 | // Try to get new credentials 51 | console.log("\nPlease log in with your credentials! (Username = Mail)"); 52 | 53 | prompt.get({ 54 | properties: { 55 | username: { 56 | pattern: /^.*$/, 57 | message: 'Mail', 58 | required: false 59 | }, 60 | password: { 61 | required: false, 62 | hidden: true 63 | } 64 | } 65 | }, function(err, input) { 66 | if (err) return console.error(err); 67 | 68 | // This creates your iCloud instance 69 | var myCloud = new iCloud(sessionPath, input.username, input.password); 70 | 71 | myCloud.on("ready", async function() { 72 | 73 | const isAutheticated = await readyHandlerTwoFactor(myCloud); 74 | if (isAutheticated) { 75 | resolve(myCloud); 76 | } 77 | 78 | }); 79 | 80 | myCloud.on("sessionUpdate", function() { 81 | myCloud.saveSession(); 82 | }); 83 | }); 84 | } 85 | }); 86 | 87 | 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /resources/helper.js: -------------------------------------------------------------------------------- 1 | const Cookie = require('cookie'); 2 | 3 | function fillDefaults(a, b) { 4 | Object.keys(b).forEach(key => { 5 | if (!(key in a)) { 6 | a[key] = b[key]; 7 | } else if (typeof b[key] == "object" && b[key] != null) { 8 | a[key] = fillDefaults(a[key], b[key]); 9 | } 10 | }); 11 | return a; 12 | } 13 | 14 | module.exports = { 15 | getHostFromWebservice(webservice) { 16 | return webservice.url.replace(":443", "").replace("https://", ""); 17 | }, 18 | cookiesToStr(cookies) { 19 | return cookies.map(function(cookie) { 20 | return Object.keys(cookie)[0] + '="' + cookie[Object.keys(cookie)[0]] + '"'; 21 | }).join("; "); 22 | }, 23 | parseCookieStr(str) { 24 | return str.map(cookie => Cookie.parse(cookie)) 25 | }, 26 | fillCookies(object, fill) { 27 | Object.keys(fill).forEach(function(key) { 28 | object[key] = fill[key]; 29 | }); 30 | 31 | 32 | return object; 33 | }, 34 | newId() { 35 | var structure = [8, 4, 4, 4, 12]; 36 | var chars = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]; 37 | var id = structure.map(function(part) { 38 | var partStr = ""; 39 | for (var i = 0; i < part; i++) { 40 | partStr += chars[Math.trunc(Math.random() * chars.length)]; 41 | } 42 | return partStr; 43 | }); 44 | return id.join("-"); 45 | }, 46 | indexOfKey(value, key, start = 0, exclude = []) { 47 | for (var i = start; i < this.length; i++) { 48 | if (this[i][key] === value && exclude.indexOf(i) < 0) { 49 | return i; 50 | } 51 | } 52 | return -1; 53 | }, 54 | paramStr(object) { 55 | return Object.keys(object).map(function(parameter) { 56 | return parameter + "=" + object[parameter]; 57 | }).join("&"); 58 | 59 | 60 | }, 61 | paramString: object => Object.keys(object).map(parameter => parameter + "=" + object[parameter]).join("&"), 62 | timeArray(date) { 63 | //[20170607, 2017, 6, 7, 12, 0, 720]; 64 | if (!date || date instanceof Array) return date; 65 | return [parseInt(date.toISOString().slice(0,10).replace(/-/g,"")), date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), 720]; 66 | 67 | }, 68 | arrayTime(array) { 69 | if (!array || array instanceof Date) return array; 70 | array[2]--; 71 | return Date.applyConstructor(array.slice(1, 6)); 72 | }, 73 | fillMethods(main, obj, self) { 74 | // Loop trough all keys of the given object 75 | Object.keys(obj).forEach(key => { 76 | if (typeof obj[key] === "object") { 77 | // If the current propterty contains an object, loop trough this in the same way. 'main' is here an empty object 78 | main[key] = module.exports.fillMethods({}, obj[key], self); 79 | } 80 | else { 81 | // Replace the function with a custom one that applies the function with a custom 'this' reference (self client instance) 82 | main[key] = function() { 83 | // The arguments of the parent function are passed normally as an array 84 | return obj[key].apply(self, arguments); 85 | } 86 | } 87 | }); 88 | return main; 89 | }, 90 | fillDefaults: fillDefaults 91 | }; 92 | Function.prototype.applyConstructor = function(args) { 93 | return new (Function.prototype.bind.apply(this, [null].concat(args))); 94 | } 95 | -------------------------------------------------------------------------------- /resources/protobuf/crframework.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto2"; 3 | 4 | package CRDT; 5 | 6 | option optimize_for = LITE_RUNTIME; 7 | 8 | message ObjectID { 9 | oneof contents { 10 | sint64 integerValue = 1; 11 | uint64 unsignedIntegerValue = 2; 12 | double doubleValue = 3; 13 | string stringValue = 4; 14 | bytes bytesValue = 5; 15 | // Store objects as an index into ‘objects' 16 | uint32 objectIndex = 6; 17 | } 18 | } 19 | 20 | message Timestamp { 21 | optional uint64 replicaIndex = 1; 22 | optional int64 counter = 2; 23 | } 24 | 25 | message RegisterLatest { 26 | optional Timestamp timestamp = 1; 27 | optional ObjectID contents = 2; 28 | } 29 | 30 | message VectorTimestamp { 31 | message Element { 32 | optional uint64 replicaIndex = 1; 33 | optional uint64 clock = 2; 34 | optional uint64 subclock = 3; 35 | } 36 | repeated Element element = 1; 37 | } 38 | 39 | message Dictionary { 40 | message Element { 41 | optional ObjectID key = 1; 42 | optional ObjectID value = 2; 43 | optional VectorTimestamp timestamp = 3; 44 | optional RegisterLatest index = 4; 45 | } 46 | repeated Element element = 1; 47 | } 48 | 49 | message Index { 50 | message Element { 51 | optional uint64 replicaIndex = 1; 52 | optional int64 integer = 2; 53 | } 54 | repeated Element element = 1; 55 | } 56 | 57 | message OneOf { 58 | message Element { 59 | optional ObjectID value = 1; 60 | optional Timestamp timestamp = 2; 61 | } 62 | repeated Element element = 1; 63 | } 64 | 65 | message StringArray { 66 | optional topotext.String contents = 1; 67 | repeated ArrayAttachment attachments = 2; 68 | message ArrayAttachment { 69 | optional uint64 attachmentIndex = 1; 70 | optional bytes contents = 2; 71 | } 72 | } 73 | 74 | message Array { 75 | optional StringArray array = 1; 76 | optional Dictionary dictionary = 2; 77 | } 78 | 79 | message OrderedSet { 80 | optional Array array = 1; 81 | optional Dictionary set = 2; 82 | } 83 | 84 | message Document { 85 | // A list of all objects in the graph. 86 | message CustomObject { 87 | // object type as an identifier index 88 | optional int32 type = 1; 89 | // key as identifier index -> ObjectID 90 | message MapEntry { 91 | required int32 key = 1; 92 | required ObjectID value = 2; 93 | } 94 | repeated MapEntry mapEntry = 3; 95 | } 96 | message DocObject { 97 | oneof contents { 98 | RegisterLatest registerLatest = 1; 99 | RegisterLatest registerGreatest = 2; 100 | RegisterLatest registerLeast = 3; 101 | Dictionary set = 4; 102 | Dictionary orderedSet = 5; 103 | Dictionary dictionary = 6; 104 | Timestamp timestamp = 7; 105 | VectorTimestamp vectorTimestamp = 8; 106 | Index index = 9; 107 | topotext.String string = 10; 108 | uint64 weakReference = 11; 109 | OneOf oneof = 12; 110 | CustomObject custom = 13; 111 | StringArray stringArray = 14; 112 | Array array = 15; 113 | OrderedSet tsOrderedSet = 16; 114 | // Note: tags 1-15 take less space 115 | } 116 | } 117 | 118 | optional VectorTimestamp version = 1; 119 | optional VectorTimestamp startVersion = 2; 120 | repeated DocObject object = 3; 121 | repeated string keyItem = 4; 122 | repeated string typeItem = 5; 123 | repeated bytes uuidItem = 6; 124 | optional topotext.VectorTimestamp ttTimestamp = 7; 125 | } 126 | -------------------------------------------------------------------------------- /resources/apps/Friends.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, indexOfKey, paramStr, fillDefaults} = require("./../helper"); 3 | 4 | 5 | 6 | module.exports = { 7 | getLocations(callback = function() {}) { 8 | var self = this; 9 | var time = new Date().getTime(); 10 | var randomTime = time => Math.round(time + (Math.random() * 20000 - 10000)); 11 | var content = { 12 | "dataContext": { 13 | "0": randomTime(time), 14 | "1": randomTime(time), 15 | "2": randomTime(time), 16 | "5": randomTime(time), 17 | "6": randomTime(time), 18 | "8": randomTime(time), 19 | "9": "", 20 | "10": randomTime(time), 21 | "11": randomTime(time), 22 | "12": randomTime(time), 23 | "13": randomTime(time), 24 | "18": randomTime(time), 25 | "19": randomTime(time), 26 | "20": randomTime(time), 27 | "21": randomTime(time), 28 | "22": randomTime(time) 29 | }, 30 | "serverContext": { 31 | "minCallbackIntervalInMS": 5000, 32 | "res": null, 33 | "clientId": "", 34 | "showAirDropImportViewOniCloudAlert": true, 35 | "authToken": null, 36 | "maxCallbackIntervalInMS": 10000, 37 | "prsId": 181764936, 38 | "callbackTimeoutIntervalInMS": 0, 39 | "heartbeatIntervalInSec": 543600, 40 | "transientDataContext": { 41 | "0": 0, 42 | "1": 0, 43 | "2": 1, 44 | "3": 1, 45 | "4": time 46 | }, 47 | "sendMyLocation": true, 48 | "notificationToken": null, 49 | "iterationNumber": 0 50 | }, 51 | "clientContext": { 52 | "productType": "fmfWeb", 53 | "appVersion": "1.0", 54 | "contextApp": "com.icloud.web.fmf", 55 | "userInactivityTimeInMS": 0, 56 | "windowInFocus": false, 57 | "windowVisible": true, 58 | "mapkitAvailable": true, 59 | "tileServer":"Apple" 60 | } 61 | }; 62 | content = JSON.stringify(content); 63 | 64 | var host = getHostFromWebservice(self.account.webservices.fmf); 65 | 66 | var requestPromise = new Promise(function(resolve, reject) { 67 | request.post("https://" + host + "/fmipservice/client/fmfWeb/refreshClient?" + paramStr({ 68 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 69 | "clientId": self.clientId, 70 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 71 | "dsid": self.account.dsInfo.dsid, 72 | }), { 73 | headers: fillDefaults({ 74 | 'Host': host, 75 | 'Cookie': cookiesToStr(self.auth.cookies), 76 | 'Content-Length': content.length 77 | }, self.clientSettings.defaultHeaders), 78 | body: content, 79 | //gzip: true 80 | }, function(err, response, body) { 81 | if (err) { 82 | reject(err); 83 | return callback(err); 84 | } 85 | var result = JSON.parse(body); 86 | 87 | var cookies = parseCookieStr(response.headers["set-cookie"]); 88 | self.auth.cookies = fillCookies(self.auth.cookies, cookies); 89 | 90 | var locations = []; 91 | 92 | if ("locations" in result) { 93 | locations = result.locations.map(function(pos) { 94 | if (pos.location == null) return {}; 95 | return { 96 | person: { 97 | info: result.following[result.following.indexOfKey(pos.id, "id")] || null, 98 | contact: result.contactDetails[result.contactDetails.indexOfKey(pos.id, "id")] || null 99 | }, 100 | address: pos.location.address, 101 | accuracy: pos.location.horizontalAccuracy, 102 | timestamp: pos.location.timestamp, 103 | longitude: pos.location.longitude, 104 | latitude: pos.location.latitude 105 | }; 106 | }); 107 | } 108 | 109 | resolve(locations); 110 | callback(null, locations); 111 | }); 112 | }); 113 | 114 | return requestPromise; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /resources/protobuf/topotext.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto2"; 3 | 4 | package topotext; 5 | 6 | option optimize_for = LITE_RUNTIME; 7 | 8 | message String { 9 | optional string string = 2; 10 | 11 | // Only needed for mergeable strings. 12 | repeated Substring substring = 3; 13 | optional VectorTimestamp timestamp = 4; 14 | 15 | repeated AttributeRun attributeRun = 5; 16 | 17 | // Optional attachment data. 18 | repeated Attachment attachment = 6; 19 | } 20 | 21 | message VectorTimestamp { 22 | message Clock { 23 | optional bytes replicaUUID = 1; 24 | message ReplicaClock { 25 | optional uint32 clock = 1; 26 | optional uint32 subclock = 2; 27 | } 28 | repeated ReplicaClock replicaClock = 2; 29 | } 30 | repeated Clock clock = 1; 31 | } 32 | 33 | message CharID { 34 | optional uint32 replicaID = 1; 35 | optional uint32 clock = 2; 36 | } 37 | 38 | message Substring { 39 | optional CharID charID = 1; 40 | // Length of substring in UTF-16 characters (surrogate pairs are separate characters). 41 | optional uint32 length = 2; 42 | 43 | // Style timestamp. 44 | optional CharID timestamp = 3; 45 | 46 | optional bool tombstone = 4; 47 | 48 | // Index into String.substring. 49 | repeated uint32 child = 5; 50 | } 51 | 52 | message Selection { 53 | message Range { 54 | optional CharID fromChar = 1; 55 | optional CharID toChar = 2; 56 | }; 57 | repeated bytes replicaUUID = 1; 58 | repeated Range range = 2; 59 | 60 | enum Affinity { 61 | Backward = 0; 62 | Forward = 1; 63 | } 64 | optional Affinity affinity = 3; 65 | } 66 | 67 | message AttributeRun { 68 | // Length of run in UTF-16 characters (surrogate pairs are separate characters). 69 | optional uint32 length = 1; 70 | 71 | optional ParagraphStyle paragraphStyle = 2; 72 | 73 | // Overrides bold, italic and paragraph styles. 74 | optional Font font = 3; 75 | 76 | // Modifiers applied on-top of paragraph style. 77 | // Bold, italic... 78 | optional uint32 fontHints = 5; 79 | 80 | // Integer values for future compatibility. 81 | // Only set as 1 or 0 currently. 82 | optional uint32 underline = 6; 83 | optional uint32 strikethrough = 7; 84 | 85 | optional int32 superscript = 8; 86 | optional string link = 9; 87 | optional Color color = 10; 88 | 89 | enum WritingDirection { 90 | NaturalDirection = 0; 91 | LeftToRight = 1; 92 | RightToLeft = 2; 93 | LeftToRightOverride = 3; 94 | RightToLeftOverride = 4; 95 | } 96 | 97 | optional WritingDirection writingDirection = 11; 98 | 99 | optional AttachmentInfo attachmentInfo = 12; 100 | 101 | // Not used, for future compatibility. 102 | optional uint64 timestamp = 13; 103 | } 104 | 105 | message Font { 106 | optional string name = 1; 107 | optional float pointSize = 2; 108 | 109 | // Hints for when named font does not exist on device. 110 | optional uint32 fontHints = 3; 111 | } 112 | 113 | message ParagraphStyle { 114 | enum Alignment { 115 | Left = 0; 116 | Center = 1; 117 | Right = 2; 118 | Justified = 3; 119 | Natural = 4; 120 | } 121 | 122 | optional uint32 style = 1; 123 | optional Alignment alignment = 2; 124 | optional AttributeRun.WritingDirection writingDirection = 3; 125 | optional int32 indent = 4; 126 | optional Todo todo = 5; 127 | 128 | // Hints for when paragraph style does not exist in this version. 129 | optional uint32 paragraphHints = 6; 130 | 131 | // Only used for the initial item of a numbered list. 132 | optional uint32 startingListItemNumber = 7; 133 | } 134 | 135 | message AttachmentInfo { 136 | optional string attachmentIdentifier = 1; 137 | optional string typeUTI = 2; 138 | } 139 | 140 | message Attachment { 141 | optional string identifier = 2; 142 | optional bytes mergeableData = 3; 143 | optional float sizeHeight = 4; 144 | optional float sizeWidth = 5; 145 | optional string summary = 6; 146 | optional string title = 7; 147 | optional string typeUTI = 8; 148 | optional string urlString = 9; 149 | 150 | optional Location location = 10; 151 | optional Media media = 11; 152 | repeated PreviewImage preview = 12; 153 | 154 | optional float originX = 13; 155 | optional float originY = 14; 156 | optional int32 orientation = 15; 157 | 158 | optional double previewUpdateDate = 16; 159 | optional double modificationDate = 17; 160 | optional string remoteFileURL = 18; 161 | optional bool checkedForLocation = 19; 162 | optional int64 fileSize = 20; 163 | optional double duration = 21; 164 | 165 | optional int32 imageFilterType = 22; 166 | optional bytes markupModelData = 23; 167 | repeated Attachment subAttachment = 24; 168 | 169 | optional int64 minimumSupportedNotesVersion = 25; 170 | 171 | optional double croppingQuadBottomLeftX = 26; 172 | optional double croppingQuadBottomLeftY = 27; 173 | optional double croppingQuadBottomRightX = 28; 174 | optional double croppingQuadBottomRightY = 29; 175 | optional double croppingQuadTopLeftX = 30; 176 | optional double croppingQuadTopLeftY = 31; 177 | optional double croppingQuadTopRightX = 32; 178 | optional double croppingQuadTopRightY = 33; 179 | 180 | optional bytes metadataData = 34; 181 | 182 | optional string userTitle = 35; 183 | 184 | optional bytes fallbackImageData = 36; 185 | } 186 | 187 | message Location { 188 | optional double latitude = 1; 189 | optional double longitude = 2; 190 | 191 | optional bytes placemark = 3; 192 | 193 | optional bool placeUpdated = 4; 194 | } 195 | 196 | message Media { 197 | optional string identifier = 1; 198 | optional string filenameExtension = 2; 199 | optional bytes data = 3; 200 | optional string filename = 4; 201 | 202 | optional int64 minimumSupportedNotesVersion = 5; 203 | } 204 | 205 | message PreviewImage { 206 | optional float scale = 1; 207 | optional bool scaleWhenDrawing = 2; 208 | optional bytes data = 3; 209 | optional bytes metadata = 4; 210 | optional int32 version = 5; 211 | optional bool versionOutOfDate = 6; 212 | 213 | optional int64 minimumSupportedNotesVersion = 7; 214 | } 215 | 216 | message Todo { 217 | optional bytes todoUUID = 1; 218 | optional bool done = 2; 219 | } 220 | 221 | message Color { 222 | optional float red = 1; 223 | optional float green = 2; 224 | optional float blue = 3; 225 | optional float alpha = 4; 226 | } 227 | -------------------------------------------------------------------------------- /resources/apps.json: -------------------------------------------------------------------------------- 1 | { 2 | "contacts": { 3 | "path": "contacts/", 4 | "requiredServices": ["contacts", "keyvalue"], 5 | "pushTopic": "73f7bfc9253abaaa423eba9a48e9f187994b7bd9", 6 | "isBeta": false, 7 | "isWWW": true, 8 | "supportsLite": true, 9 | "instanceName": "Contacts", 10 | "modulePath": "resources/apps/Contacts" 11 | }, 12 | "calendar": { 13 | "path": "calendar/", 14 | "requiredServices": ["calendar", "keyvalue"], 15 | "pushTopic": "dce593a0ac013016a778712b850dc2cf21af8266", 16 | "showsPushNotifications": true, 17 | "awakesInBackgroundFromPush": true, 18 | "isBeta": false, 19 | "isWWW": true, 20 | "isFuture": true, 21 | "instanceName": "Calendar", 22 | "modulePath": "resources/apps/Calendar" 23 | }, 24 | "find": { 25 | "requiredServices": ["findme"], 26 | "pushTopic": "f68850316c5241d8fd120f3bc6da2ff4a6cca9a8", 27 | "showsPushNotifications": true, 28 | "awakesInBackgroundFromPush": true, 29 | "isBeta": false, 30 | "isWWW": true, 31 | "isFuture": false, 32 | "instanceName": "FindMe", 33 | "modulePath": "resources/apps/FindMe" 34 | }, 35 | "fmf": { 36 | "requiredServices": ["fmf"], 37 | "isBeta": false, 38 | "isWWW": true, 39 | "instanceName": "Friends", 40 | "modulePath": "resources/apps/Friends" 41 | }, 42 | "mail": { 43 | "path": "mail/", 44 | "requiredServices": ["mail"], 45 | "pushTopic": "e850b097b840ef10ce5a7ed95b171058c42cc435", 46 | "showsPushNotifications": true, 47 | "awakesInBackgroundFromPush": true, 48 | "isBeta": true, 49 | "isWWW": true, 50 | "isFuture": true, 51 | "supportsPhoneNumberBasedAppleId": false, 52 | "instanceName": "Mail", 53 | "modulePath": "resources/apps/Mail" 54 | }, 55 | "notes": { 56 | "path": "notes/", 57 | "requiredServices": ["mail"], 58 | "pushTopic": null, 59 | "pushTopicOld": "c5292888edbd54273cf7c82fe861b81f0d2b87ef", 60 | "alternateAppName": "notes2", 61 | "supportsCKSharing": false, 62 | "isBeta": false, 63 | "isWWW": true, 64 | "isSupportedOnMobile": false 65 | }, 66 | "notes2": { 67 | "path": "notes2/", 68 | "alternateAppName": "notes", 69 | "requiredServices": ["ckdatabasews"], 70 | "isBeta": false, 71 | "isWWW": true, 72 | "isSupportedOnMobile": false, 73 | "supportsLite": true, 74 | "isPCSRequired": true, 75 | "preloadCoreTypes": true, 76 | "isHidden": "YES", 77 | "containerIdentifier": "com.apple.notes", 78 | "shareInfo": { 79 | "titleFields": [{ 80 | "name": "TitleEncrypted", 81 | "type": "EncryptedBytes" 82 | }] 83 | }, 84 | "instanceName": "Notes", 85 | "modulePath": "resources/apps/Notes" 86 | }, 87 | "reminders": { 88 | "path": "reminders/", 89 | "requiredServices": ["reminders", "keyvalue"], 90 | "pushTopic": "8a40cb6b1d3fcd0f5c204504eb8fb9aa64b78faf", 91 | "showsPushNotifications": true, 92 | "awakesInBackgroundFromPush": true, 93 | "isBeta": false, 94 | "isWWW": true, 95 | "isFuture": true, 96 | "instanceName": "Reminders", 97 | "modulePath": "resources/apps/Reminders" 98 | }, 99 | "photos": { 100 | "path": "photos/", 101 | "requiredServices": ["ckdatabasews"], 102 | "isFuture": true, 103 | "isBeta": false, 104 | "isWWW": true, 105 | "isPCSRequired": true, 106 | "containerIdentifier": "com.apple.photos.cloud", 107 | "instanceName": "Photos", 108 | "modulePath": "resources/apps/Photos" 109 | }, 110 | "iclouddrive": { 111 | "path": "iclouddrive/", 112 | "isFuture": true, 113 | "isBeta": true, 114 | "isWWW": true, 115 | "isPCSRequired": true, 116 | "preloadCoreTypes": true, 117 | "hasUIForAcceptedSharesOnMobile": true, 118 | "shareInfo": { 119 | "titleFields": [{ 120 | "name": "encryptedBasename", 121 | "type": "EncryptedBytes" 122 | }, { 123 | "name": "bounceNo", 124 | "type": "Number" 125 | }] 126 | }, 127 | "containerIdentifier": "com.apple.clouddocs", 128 | "instanceName": "Drive", 129 | "modulePath": "resources/apps/Drive" 130 | }, 131 | "settings": { 132 | "path": "settings/", 133 | "supportsLite": true, 134 | "isFuture": true, 135 | "isBeta": false, 136 | "isWWW": true 137 | }, 138 | "pages": { 139 | "requiredServices": ["ubiquity", "iwmb", "keyvalue"], 140 | "supportedFileExtensions": ["pages"], 141 | "pushTopic": "5a5fc3a1fea1dfe3770aab71bc46d0aa8a4dad41", 142 | "supportsLite": true, 143 | "isPCSRequired": true, 144 | "isCarry": false, 145 | "additionalSupportedLocales": ["ar-sa", "iw-il"], 146 | "containerIdentifier": "com.apple.clouddocs" 147 | , 148 | "shareInfo": { 149 | "titleFields": [{ 150 | "name": "encryptedBasename", 151 | "type": "EncryptedBytes" 152 | }, { 153 | "name": "bounceNo", 154 | "type": "Number" 155 | }], 156 | "thumbnailField": { 157 | "name": "thumb1024", 158 | "type": "Asset" 159 | } 160 | } 161 | }, 162 | "numbers": { 163 | "requiredServices": ["ubiquity", "iwmb", "keyvalue"], 164 | "supportedFileExtensions": ["numbers"], 165 | "pushTopic": "5a5fc3a1fea1dfe3770aab71bc46d0aa8a4dad41", 166 | "supportsLite": true, 167 | "isPCSRequired": true, 168 | "isCarry": false, 169 | "containerIdentifier": "com.apple.clouddocs", 170 | "shareInfo": { 171 | "titleFields": [{ 172 | "name": "encryptedBasename", 173 | "type": "EncryptedBytes" 174 | }, { 175 | "name": "bounceNo", 176 | "type": "Number" 177 | }], 178 | "thumbnailField": { 179 | "name": "thumb1024", 180 | "type": "Asset" 181 | } 182 | } 183 | }, 184 | "keynote": { 185 | "requiredServices": ["ubiquity", "iwmb", "keyvalue"], 186 | "supportedFileExtensions": ["key"], 187 | "pushTopic": "5a5fc3a1fea1dfe3770aab71bc46d0aa8a4dad41", 188 | "supportsLite": true, 189 | "isPCSRequired": true, 190 | "isCarry": false, 191 | "additionalSupportedLocales": ["ar-sa", "iw-il"], 192 | "containerIdentifier": "com.apple.clouddocs", 193 | "shareInfo": { 194 | "titleFields": [{ 195 | "name": "encryptedBasename", 196 | "type": "EncryptedBytes" 197 | }, { 198 | "name": "bounceNo", 199 | "type": "Number" 200 | }], 201 | "thumbnailField": { 202 | "name": "thumb1024", 203 | "type": "Asset" 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /resources/apps/Contacts.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, newId, fillDefaults} = require("./../helper"); 3 | 4 | 5 | module.exports = { 6 | list(callback = function() {}) { 7 | var self = this; 8 | var host = getHostFromWebservice(self.account.webservices.contacts); 9 | var requestPromise = new Promise(function(resolve, reject) { 10 | request.get("https://" + host + "/co/startup?clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientId=" + self.clientId + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&clientVersion=2.1&dsid=" + self.account.dsInfo.dsid + "&locale=de_DE&order=first%2Clast", { 11 | headers: fillDefaults({ 12 | 'Host': host, 13 | 'Cookie': cookiesToStr(self.auth.cookies) 14 | }, self.clientSettings.defaultHeaders) 15 | }, function(err, response, body) { 16 | if (err) { 17 | reject(err); 18 | return callback(err); 19 | } 20 | var result = JSON.parse(body); 21 | 22 | self.Contacts.syncToken = result.syncToken; 23 | self.Contacts.prefToken = result.prefToken; 24 | 25 | resolve(result); 26 | callback(null, result); 27 | }); 28 | }); 29 | 30 | return requestPromise; 31 | }, 32 | __card(contacts, callback = function() { }, method) { 33 | var self = this; 34 | 35 | var requestPromise = new Promise(function(resolve, reject) { 36 | var content = { 37 | "contacts": contacts 38 | }; 39 | content = JSON.stringify(content); 40 | if (!("syncToken" in self.Contacts)) { 41 | var errorObj = { 42 | error: 'No "syncToken" found! Please call "Contacts.list()" to initialize the Contacts services!', 43 | errorCode: 4 44 | }; 45 | reject(errorObj); 46 | return callback(errorObj); 47 | } 48 | 49 | var host = getHostFromWebservice(self.account.webservices.contacts); 50 | request.post("https://" + host + "/co/contacts/card/" + 51 | "?clientBuildNumber=" + self.clientSettings.clientBuildNumber + 52 | "&clientId=" + self.clientId + 53 | "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + 54 | "&clientVersion=2.1" + 55 | "&dsid=" + self.account.dsInfo.dsid + 56 | "&locale=en_US" + 57 | "&method=" + method + 58 | "&order=first%2Clast" + 59 | "&prefToken=" + encodeURIComponent(self.Contacts.prefToken) + 60 | "&syncToken=" + encodeURIComponent(self.Contacts.syncToken), { 61 | headers: fillDefaults({ 62 | 'Host': host, 63 | 'Cookie': cookiesToStr(self.auth.cookies), 64 | 'Content-Length': content.length 65 | }, self.clientSettings.defaultHeaders), 66 | body: content 67 | }, function(err, response, body) { 68 | if (err) { 69 | reject(err); 70 | return callback(err); 71 | } 72 | var result = JSON.parse(body); 73 | 74 | if ("errorCode" in result) { 75 | reject(body); 76 | return callback(body); 77 | } 78 | 79 | self.Contacts.syncToken = result.syncToken; 80 | self.Contacts.prefToken = result.prefToken; 81 | 82 | resolve(result); 83 | callback(null, result); 84 | }); 85 | }); 86 | 87 | return requestPromise; 88 | }, 89 | change(contacts, callback = function() {}) { 90 | var self = this; 91 | if (!(contacts instanceof Array)) { 92 | contacts = [contacts]; 93 | } 94 | return self.Contacts.__card(contacts, callback, "PUT"); 95 | }, 96 | new(contacts, callback = function() {}) { 97 | var self = this; 98 | if (!(contacts instanceof Array)) { 99 | contacts = [contacts]; 100 | } 101 | contacts = contacts.map(function(contact) { 102 | if (!("contactId" in contact)) contact["contactId"] = newId(); 103 | return contact; 104 | }); 105 | return self.Contacts.__card(contacts, callback, ""); 106 | }, 107 | delete(contacts, callback = function() {}) { 108 | var self = this; 109 | if (!(contacts instanceof Array)) { 110 | contacts = [contacts]; 111 | } 112 | contacts = contacts.map(function(contact) { 113 | return { 114 | contactId: contact.contactId, 115 | etag: contact.etag 116 | } 117 | }); 118 | return self.Contacts.__card(contacts, callback, "DELETE"); 119 | }, 120 | 121 | __group(groups, callback = function () { }, method) { 122 | var self = this; 123 | 124 | var requestPromise = new Promise(function(resolve, reject) { 125 | var content = { 126 | "groups": groups 127 | }; 128 | content = JSON.stringify(content); 129 | if (!("syncToken" in self.Contacts)) { 130 | var errorObj = { 131 | error: 'No "syncToken" found! Please call "Contacts.list()" to initialize the Contacts services!', 132 | errorCode: 4 133 | }; 134 | reject(errorObj); 135 | return callback(errorObj); 136 | } 137 | 138 | var host = getHostFromWebservice(self.account.webservices.contacts); 139 | request.post("https://" + host + "/co/groups/card/" + 140 | "?clientBuildNumber=" + self.clientSettings.clientBuildNumber + 141 | "&clientId=" + self.clientId + 142 | "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + 143 | "&clientVersion=2.1" + 144 | "&dsid=" + self.account.dsInfo.dsid + 145 | "&locale=en_US" + 146 | "&method=" + method + 147 | "&order=first%2Clast" + 148 | "&prefToken=" + encodeURIComponent(self.Contacts.prefToken) + 149 | "&syncToken=" + encodeURIComponent(self.Contacts.syncToken), { 150 | headers: fillDefaults({ 151 | 'Host': host, 152 | 'Cookie': cookiesToStr(self.auth.cookies), 153 | 'Content-Length': content.length 154 | }, self.clientSettings.defaultHeaders), 155 | body: content 156 | }, function(err, response, body) { 157 | if (err) { 158 | reject(err); 159 | return callback(err); 160 | } 161 | var result = JSON.parse(body); 162 | 163 | if ("errorCode" in result) { 164 | reject(body); 165 | return callback(body); 166 | } 167 | 168 | self.Contacts.syncToken = result.syncToken; 169 | self.Contacts.prefToken = result.prefToken; 170 | 171 | resolve(result); 172 | callback(null, result); 173 | }); 174 | }); 175 | 176 | return requestPromise; 177 | }, 178 | 179 | newGroups(groups, callback = function () { }) { 180 | var self = this; 181 | if (!(groups instanceof Array)) { 182 | groups = [groups]; 183 | } 184 | groups = groups.map(function(group) { 185 | if (!("groupId" in group)) group["groupId"] = newId(); 186 | if (!("contactIds" in group)) group["contactIds"] = []; 187 | return group; 188 | }); 189 | return self.Contacts.__group(groups, callback, ""); 190 | }, 191 | 192 | deleteGroups(groups, callback = function () { }) { 193 | var self = this; 194 | if (!(groups instanceof Array)) { 195 | groups = [groups]; 196 | } 197 | return self.Contacts.__group(groups, callback, "DELETE"); 198 | }, 199 | 200 | changeGroups(groups, callback = function () { }) { 201 | var self = this; 202 | if (!(groups instanceof Array)) { 203 | groups = [groups]; 204 | } 205 | return self.Contacts.__group(groups, callback, "PUT"); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /resources/apps/Photos.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, paramString, fillDefaults} = require("./../helper"); 3 | 4 | module.exports = { 5 | get(callback = function() {}) { 6 | var self = this; 7 | var host = getHostFromWebservice(self.account.webservices.ckdatabasews); 8 | 9 | var photosPromise = new Promise(function(resolve, reject) { 10 | request.get("https://" + host + "/database/1/com.apple.photos.cloud/production/private/zones/list?remapEnums=true&ckjsBuildVersion=17DProjectDev77&ckjsVersion=2.0.5&getCurrentSyncToken=true&clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&clientId=" + self.clientId + "&dsid=" + self.account.dsInfo.dsid, { 11 | headers: fillDefaults({ 12 | 'Host': host, 13 | 'Cookie': cookiesToStr(self.auth.cookies) 14 | }, self.clientSettings.defaultHeaders) 15 | }, function(err, response, body) { 16 | if (err) { 17 | reject(err); 18 | return console.error(err); 19 | } 20 | var result = JSON.parse(body); 21 | 22 | 23 | if ("set-cookie" in response.headers) { 24 | var cookies = parseCookieStr(response.headers["set-cookie"]); 25 | self.auth.cookies = fillCookies(self.auth.cookies, cookies); 26 | } 27 | //console.log(result.zones); 28 | var content = { 29 | "records": [ 30 | { 31 | "recordName": "PrimarySync-0000-ZS" 32 | } 33 | ], 34 | "zoneID": { 35 | "zoneName": "PrimarySync" 36 | } 37 | }; 38 | content = JSON.stringify(content); 39 | 40 | if ("error" in result) { 41 | return reject({ 42 | error: result.reason, 43 | code: 12 44 | }); 45 | } 46 | 47 | var content = { 48 | "query": { 49 | "recordType": "CPLAssetAndMasterByAssetDateWithoutHiddenOrDeleted", 50 | "filterBy": [ 51 | { 52 | "fieldName": "startRank", 53 | "comparator": "EQUALS", 54 | "fieldValue": { 55 | "value": 0, 56 | "type": "INT64" 57 | } 58 | }, 59 | { 60 | "fieldName": "direction", 61 | "comparator": "EQUALS", 62 | "fieldValue": { 63 | "value": "ASCENDING", 64 | "type": "STRING" 65 | } 66 | } 67 | ] 68 | }, 69 | "zoneID": { 70 | "zoneName": result.zones[0].zoneID.zoneName 71 | }, 72 | "desiredKeys": [ 73 | "resJPEGFullWidth", 74 | "resJPEGFullHeight", 75 | "resJPEGFullFileType", 76 | "resJPEGFullFingerprint", 77 | "resJPEGFullRes", 78 | "resJPEGLargeWidth", 79 | "resJPEGLargeHeight", 80 | "resJPEGLargeFileType", 81 | "resJPEGLargeFingerprint", 82 | "resJPEGLargeRes", 83 | "resJPEGMedWidth", 84 | "resJPEGMedHeight", 85 | "resJPEGMedFileType", 86 | "resJPEGMedFingerprint", 87 | "resJPEGMedRes", 88 | "resJPEGThumbWidth", 89 | "resJPEGThumbHeight", 90 | "resJPEGThumbFileType", 91 | "resJPEGThumbFingerprint", 92 | "resJPEGThumbRes", 93 | "resVidFullWidth", 94 | "resVidFullHeight", 95 | "resVidFullFileType", 96 | "resVidFullFingerprint", 97 | "resVidFullRes", 98 | "resVidMedWidth", 99 | "resVidMedHeight", 100 | "resVidMedFileType", 101 | "resVidMedFingerprint", 102 | "resVidMedRes", 103 | "resVidSmallWidth", 104 | "resVidSmallHeight", 105 | "resVidSmallFileType", 106 | "resVidSmallFingerprint", 107 | "resVidSmallRes", 108 | "resSidecarWidth", 109 | "resSidecarHeight", 110 | "resSidecarFileType", 111 | "resSidecarFingerprint", 112 | "resSidecarRes", 113 | "itemType", 114 | "dataClassType", 115 | "mediaMetaDataType", 116 | "mediaMetaDataEnc", 117 | "filenameEnc", 118 | "originalOrientation", 119 | "resOriginalWidth", 120 | "resOriginalHeight", 121 | "resOriginalFileType", 122 | "resOriginalFingerprint", 123 | "resOriginalRes", 124 | "resOriginalAltWidth", 125 | "resOriginalAltHeight", 126 | "resOriginalAltFileType", 127 | "resOriginalAltFingerprint", 128 | "resOriginalAltRes", 129 | "resOriginalVidComplWidth", 130 | "resOriginalVidComplHeight", 131 | "resOriginalVidComplFileType", 132 | "resOriginalVidComplFingerprint", 133 | "resOriginalVidComplRes", 134 | "isDeleted", 135 | "isExpunged", 136 | "dateExpunged", 137 | "remappedRef", 138 | "recordName", 139 | "recordType", 140 | "recordChangeTag", 141 | "masterRef", 142 | "adjustmentRenderType", 143 | "assetDate", 144 | "addedDate", 145 | "isFavorite", 146 | "isHidden", 147 | "orientation", 148 | "duration", 149 | "assetSubtype", 150 | "assetSubtypeV2", 151 | "assetHDRType", 152 | "burstFlags", 153 | "burstFlagsExt", 154 | "burstId", 155 | "captionEnc", 156 | "locationEnc", 157 | "locationV2Enc", 158 | "locationLatitude", 159 | "locationLongitude", 160 | "adjustmentType", 161 | "timeZoneOffset", 162 | "vidComplDurValue", 163 | "vidComplDurScale", 164 | "vidComplDispValue", 165 | "vidComplDispScale", 166 | "vidComplVisibilityState", 167 | "customRenderedValue", 168 | "containerId", 169 | "itemId", 170 | "position", 171 | "isKeyAsset" 172 | ], 173 | "resultsLimit": 100000 174 | }; 175 | 176 | content = JSON.stringify(content); 177 | request.post("https://" + host + "/database/1/com.apple.photos.cloud/production/private/records/query?remapEnums=true&ckjsBuildVersion=17DProjectDev77&ckjsVersion=2.0.5&getCurrentSyncToken=true&clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&clientId=" + self.clientId + "&dsid=" + self.account.dsInfo.dsid, { 178 | headers: fillDefaults({ 179 | 'Host': host, 180 | 'Cookie': cookiesToStr(self.auth.cookies), 181 | 'Content-Length': content.length 182 | }, self.clientSettings.defaultHeaders), 183 | body: content 184 | }, function(err, response, body) { 185 | if (err) { 186 | reject(err); 187 | return console.error(err); 188 | } 189 | var result = JSON.parse(body); 190 | var images = result.records.filter(function(record) { 191 | return record.recordType == "CPLMaster"; 192 | }).map(function(record) { 193 | //console.log(record); 194 | var image = { 195 | __recordName: record.recordName, 196 | width: record.fields.resOriginalWidth.value, 197 | height: record.fields.resOriginalHeight.value, 198 | orientation: record.fields.originalOrientation.value, 199 | filetype: record.fields.resOriginalFileType.value.replace("public.", ""), 200 | fingerprint: record.fields.resOriginalFingerprint.value, 201 | size: record.fields.resOriginalRes.value.size, 202 | original: record.fields.resOriginalRes.value.downloadURL, 203 | thumbnail: record.fields.resJPEGThumbRes.value.downloadURL 204 | } 205 | image.original = image.original.replace("${f}", image.fingerprint + "." + image.filetype); 206 | image.thumbnail = image.thumbnail.replace("${f}", image.fingerprint + "." + image.filetype); 207 | 208 | var refMetaRecord = findMetaRecord(image.__recordName, result.records); 209 | 210 | console.log(refMetaRecord); 211 | 212 | var meta = { 213 | __recordName: refMetaRecord.recordName, 214 | date: refMetaRecord.fields.assetDate.value, 215 | subtypeV2: "assetSubtypeV2" in refMetaRecord.fields ? refMetaRecord.fields.assetSubtypeV2.value : null, 216 | HDRType: "assetHDRType" in refMetaRecord.fields ? refMetaRecord.fields.assetHDRType.value : null, 217 | timeZoneOffset: refMetaRecord.fields.timeZoneOffset ? refMetaRecord.fields.timeZoneOffset.value : null, 218 | duration: "duration" in refMetaRecord.fields ? refMetaRecord.fields.duration.value : 0, 219 | favorite: ("isFavorite" in refMetaRecord.fields ? refMetaRecord.fields.isFavorite.value : null) ? true : false, 220 | created: refMetaRecord.created.timestamp, 221 | modified: refMetaRecord.modified.timestamp 222 | } 223 | image.meta = meta; 224 | return image; 225 | }); 226 | resolve(images); 227 | callback(null, images); 228 | }); 229 | 230 | 231 | }); 232 | }); 233 | 234 | return photosPromise; 235 | 236 | }, 237 | upload(file, callback) { 238 | //https://p44-uploadimagews.icloud.com/upload?filename=big-drogen1.jpg&dsid=11298614181&lastModDate=1497953695000&timezoneOffset=-120 239 | var self = this; 240 | var host = getHostFromWebservice(self.account.webservices.ckdatabasews); 241 | var content = fs.readFileSync(file); 242 | request("https://" + host + "/upload?filename=" + file.jpg + "&dsid=" + self.account.dsInfo.dsid + "&lastModDate=" + (new Date().getTime()) + "&timezoneOffset=-120", { 243 | method: "POST", 244 | headers: fillDefaults({ 245 | 'Host': host, 246 | 'Cookie': cookiesToStr(self.auth.cookies), 247 | 'Content-Length': content.length 248 | }, self.clientSettings.defaultHeaders) 249 | }, function(err, response, body) { 250 | if (err) return callback(err); 251 | console.log(body); 252 | var result = JSON.parse(body); 253 | callback(null, result); 254 | 255 | }); 256 | } 257 | } 258 | 259 | function findMetaRecord(recordName, records) { 260 | for (var i = 0; i < records.length; i++) { 261 | if ("masterRef" in records[i].fields && records[i].fields.masterRef.value.recordName === recordName) { 262 | return records[i]; 263 | } 264 | } 265 | return null; 266 | } 267 | -------------------------------------------------------------------------------- /resources/apps/FindMe.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, newId, indexOfKey, paramString, paramStr, fillDefaults} = require("./../helper"); 3 | 4 | module.exports = { 5 | initialized: false, 6 | get(username, password, callback = function() {}) { 7 | var self = this; 8 | 9 | username = username || self.username; 10 | password = password || self.password; 11 | 12 | 13 | var devicesPromise = new Promise(function(resolve, reject) { 14 | self.FindMe.__saveGet(username, password, function(err, result) { 15 | if (err) { 16 | reject({ 17 | error: "Something went wrong. Maybe your credentials are incorrect.", 18 | code: 11 19 | }); 20 | return callback(err); 21 | } 22 | resolve(result); 23 | callback(null, result); 24 | }); 25 | }); 26 | 27 | return devicesPromise; 28 | }, 29 | __start(callback) { 30 | var self = this; 31 | 32 | var host = getHostFromWebservice(self.account.webservices.findme); 33 | 34 | // Define post body 35 | 36 | var content = JSON.stringify({ 37 | "clientContext": { 38 | "appName": "iCloud Find (Web)", 39 | "appVersion": "2.0", 40 | "timezone": "Europe/Rome", 41 | "inactiveTime": 1905, 42 | "apiVersion": "3.0", 43 | "deviceListVersion": 1, 44 | "fmly": true 45 | } 46 | }); 47 | 48 | // Post the start up request 49 | 50 | request.post("https://" + host + "/fmipservice/client/web/initClient?" + paramStr({ 51 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 52 | "clientId": self.clientId, 53 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 54 | "dsid": self.account.dsInfo.dsid, 55 | }), { 56 | headers: fillDefaults({ 57 | 'Host': host, 58 | 'Cookie': cookiesToStr(self.auth.cookies), 59 | 'Content-Length': content.length 60 | }, self.clientSettings.defaultHeaders), 61 | body: content 62 | }, function(err, response, body) { 63 | if (err) return callback(err); 64 | try { 65 | // Try to parse the result as JSON (Sometimes it fails because the cookies invalid) 66 | var result = JSON.parse(body); 67 | } catch (e) { 68 | return callback(e); 69 | } 70 | // Parse new cookies from response headers 71 | var cookies = parseCookieStr(response.headers["set-cookie"]); 72 | // Update cookies of current session 73 | self.auth.cookies = fillCookies(self.auth.cookies, cookies); 74 | // Fire session update event 75 | self.emit("sessionUpdate"); 76 | // Return the result 77 | callback(null, result); 78 | }); 79 | }, 80 | __saveGet(username, password, callback = function() {}) { 81 | var self = this; 82 | 83 | if (module.exports.initialized) { 84 | self.FindMe.__data(callback); 85 | } 86 | else { 87 | // Try to load start up data. Start up data is not different to the normal data but it's an "initialization" iCloud.com does and so, we also do it 88 | self.FindMe.__start(function(err, result) { 89 | // Doesn't worked. Mostly we need new cookies 90 | if (err) { 91 | // Login 92 | self.login(username, password, function(err) { 93 | if (err) return callback(err); 94 | // No error logging in. Let's call get() again. 95 | self.FindMe.get(username, password, callback); 96 | }); 97 | } 98 | else { 99 | // Start up data was successfully requested. Now, initialized = true and the next data requests will use a differnet endpoint 100 | module.exports.initialized = true; 101 | result = handleFindMeData(result); 102 | callback(null, result); 103 | } 104 | }); 105 | } 106 | }, 107 | __generateContent() { 108 | return { 109 | "serverContext": { 110 | "minCallbackIntervalInMS": 1000, 111 | "enable2FAFamilyActions": false, 112 | "preferredLanguage": this.clientSettings.language, 113 | "lastSessionExtensionTime": null, 114 | "enableMapStats": true, 115 | "callbackIntervalInMS": 10000, 116 | "validRegion": true, 117 | "authToken": null, 118 | "maxCallbackIntervalInMS": 60000, 119 | "classicUser": false, 120 | "isHSA": false, 121 | "trackInfoCacheDurationInSecs": 86400, 122 | "imageBaseUrl": "https://statici.icloud.com", 123 | "minTrackLocThresholdInMts": 100, 124 | "maxLocatingTime": 90000, 125 | "sessionLifespan": 900000, 126 | "useAuthWidget": true, 127 | "clientId": this.clientId, 128 | "enable2FAFamilyRemove": false, 129 | "macCount": 0, 130 | "deviceLoadStatus": "200", 131 | "maxDeviceLoadTime": 60000, 132 | "showSllNow": false, 133 | "cloudUser": true, 134 | "enable2FAErase": false, 135 | "id": "server_ctx" 136 | }, 137 | "clientContext": { 138 | "appName": "iCloud Find (Web)", 139 | "appVersion": "2.0", 140 | "timezone": this.clientSettings.timezone, 141 | "inactiveTime": 0, 142 | "apiVersion": "3.0", 143 | "deviceListVersion": 1, 144 | "fmly": true 145 | } 146 | }; 147 | }, 148 | __data(callback) { 149 | const self = this; 150 | 151 | var host = getHostFromWebservice(self.account.webservices.findme); 152 | // Define request body for post with own properties 153 | var content = JSON.stringify(self.FindMe.__generateContent()); 154 | 155 | // Post the request 156 | request.post("https://" + host + "/fmipservice/client/web/initClient?" + paramStr({ 157 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 158 | "clientId": self.clientId, 159 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 160 | "dsid": self.account.dsInfo.dsid, 161 | }), { 162 | headers: fillDefaults({ 163 | 'Host': host, 164 | 'Cookie': cookiesToStr(self.auth.cookies), 165 | 'Content-Length': content.length 166 | }, self.clientSettings.defaultHeaders), 167 | body: content 168 | }, function(err, response, body) { 169 | if (err) return callback(err); 170 | try { 171 | // Try to parse the result as JSON (Sometimes it fails because the cookies are invalid) 172 | var result = JSON.parse(body); 173 | } catch (e) { 174 | 175 | return callback({ 176 | error: "Request body is no valid JSON", 177 | errorCode: 13, 178 | requestBody: body 179 | }); 180 | } 181 | // Handle result 182 | result = handleFindMeData(result); 183 | // Return result 184 | callback(null, result); 185 | }); 186 | }, 187 | playSound(device, callback = function() {}) { 188 | const self = this; 189 | 190 | const deviceId = typeof device == "object" ? device.id : device; 191 | 192 | const content = JSON.stringify(Object.assign(self.FindMe.__generateContent(), { 193 | "device": deviceId, 194 | "subject": "Reminder \u201eFInd my iPhone\u201c" 195 | })); 196 | 197 | var host = getHostFromWebservice(this.account.webservices.findme); 198 | 199 | return new Promise(function(resolve, reject) { 200 | request.post("https://" + host + "/fmipservice/client/web/playSound?" + paramStr({ 201 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 202 | "clientId": self.clientId, 203 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 204 | "dsid": self.account.dsInfo.dsid, 205 | }), { 206 | headers: fillDefaults({ 207 | 'Host': host, 208 | 'Cookie': cookiesToStr(self.auth.cookies), 209 | //'Content-Length': content.length 210 | }, self.clientSettings.defaultHeaders), 211 | body: content 212 | }, function(err, response, body) { 213 | if (err) return callback(err); 214 | 215 | try { 216 | // Try to parse the result as JSON (Sometimes it fails because the cookies are invalid) 217 | var result = JSON.parse(body); 218 | } catch (e) { 219 | reject({ 220 | error: "Request body is no valid JSON", 221 | errorCode: 13, 222 | requestBody: body 223 | }); 224 | return callback({ 225 | error: "Request body is no valid JSON", 226 | errorCode: 13, 227 | requestBody: body 228 | }); 229 | } 230 | if (result) { 231 | callback(null, result); 232 | resolve(result); 233 | } 234 | 235 | }); 236 | }); 237 | 238 | }, 239 | lostDevice(device, options, callback = function() {}) { 240 | const self = this; 241 | 242 | const deviceId = typeof device == "object" ? device.id : device; 243 | 244 | const data = { 245 | "device": deviceId, 246 | "lostModeEnabled": true, 247 | "trackingEnabled": true, 248 | "userText": true, 249 | "text": "", 250 | "emailUpdates": true 251 | }; 252 | 253 | if (typeof options === 'object' && options.constructor === Object) { 254 | for (let [key, value] of Object.entries(data)) { 255 | if (options.hasOwnProperty(key) && typeof options[key] === typeof value) { 256 | data[key] = options[key] 257 | } 258 | } 259 | } 260 | 261 | const content = JSON.stringify(Object.assign(self.FindMe.__generateContent(), data)); 262 | 263 | var host = getHostFromWebservice(this.account.webservices.findme); 264 | 265 | return new Promise(function(resolve, reject) { 266 | request.post("https://" + host + "/fmipservice/client/web/lostDevice?" + paramStr({ 267 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 268 | "clientId": self.clientId, 269 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 270 | "dsid": self.account.dsInfo.dsid, 271 | }), { 272 | headers: fillDefaults({ 273 | 'Host': host, 274 | 'Cookie': cookiesToStr(self.auth.cookies), 275 | //'Content-Length': content.length 276 | }, self.clientSettings.defaultHeaders), 277 | body: content 278 | }, function(err, response, body) { 279 | if (err) return callback(err); 280 | 281 | try { 282 | // Try to parse the result as JSON (Sometimes it fails because the cookies are invalid) 283 | var result = JSON.parse(body); 284 | } catch (e) { 285 | reject({ 286 | error: "Request body is no valid JSON", 287 | errorCode: 13, 288 | requestBody: body 289 | }); 290 | return callback({ 291 | error: "Request body is no valid JSON", 292 | errorCode: 13, 293 | requestBody: body 294 | }); 295 | } 296 | if (result) { 297 | callback(null, result); 298 | resolve(result); 299 | } 300 | 301 | }); 302 | }); 303 | 304 | } 305 | } 306 | 307 | function handleFindMeData(data) { 308 | return data; 309 | } 310 | -------------------------------------------------------------------------------- /resources/apps/Notes.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const EventEmitter = require('events'); 3 | const protobuf = require('protobufjs'); 4 | const zlib = require('zlib'); 5 | const path = require('path'); 6 | 7 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, newId, indexOfKey, paramString, paramStr, timeArray, arrayTime, fillDefaults} = require("./../helper"); 8 | 9 | 10 | 11 | module.exports = { 12 | fetch() { 13 | var self = this; 14 | 15 | const emitter = Object.assign(new EventEmitter(), { 16 | __records: [] 17 | }); 18 | Object.defineProperty(emitter, "__folders", { 19 | get() { 20 | return this.__records.filter(record => record.recordType === "Folder"); 21 | } 22 | }); 23 | Object.defineProperty(emitter, "__notes", { 24 | get() { 25 | return this.__records.filter(record => { 26 | return ( 27 | record.recordType === "Note" || 28 | record.recordType === "Note_UserSpecific" 29 | ); 30 | }); 31 | } 32 | }); 33 | Object.defineProperty(emitter, "folders", { 34 | get() { 35 | return this.__folders.map(folder => { 36 | folder.notes = this.__notes.filter(note => { 37 | const noteParentFolder = (() => { 38 | if (note.parent) { 39 | return note.parent; 40 | } 41 | else { 42 | if (note.fields.Folder) { 43 | return note.fields.Folder.value; 44 | } 45 | // Mostly if note is deleted 46 | else {} 47 | } 48 | })(); 49 | return noteParentFolder ? (noteParentFolder.recordName === folder.recordName) : undefined; 50 | }); 51 | return folder; 52 | }); 53 | } 54 | }); 55 | 56 | 57 | 58 | 59 | var requestId = 0; 60 | 61 | self.Notes.__zone(undefined, handleZone); 62 | function handleZone(err, zone) { 63 | if (err) { 64 | reject(err); 65 | return callback(err); 66 | } 67 | 68 | requestId++; 69 | 70 | if (zone.records.length > 0) { 71 | emitter.__records = emitter.__records.concat(zone.records); 72 | emitter.emit("data", zone); 73 | } 74 | if (zone.moreComing) { 75 | self.Notes.__zone(zone.syncToken, handleZone); 76 | } 77 | else { 78 | emitter.emit("end"); 79 | } 80 | } 81 | 82 | return emitter; 83 | }, 84 | async resolve(...notes) { 85 | const self = this; 86 | 87 | const result = await self.Notes.__lookup(notes); 88 | 89 | return result.records.map(record => { 90 | // Decrypt title & snippet 91 | record.fields.title = decryptBase64(record.fields.TitleEncrypted.value).toString(); 92 | record.fields.snippet = decryptBase64(record.fields.SnippetEncrypted.value).toString(); 93 | 94 | const data = decryptBase64(record.fields.TextDataEncrypted.value); 95 | record.fields.documentData = (data[0] === 0x1f && data[1] === 0x8b) ? zlib.gunzipSync(data) : zlib.inflateSync(data); 96 | 97 | // load protobuf definitions 98 | const root = getProtobufRoot(); 99 | 100 | const Document = root.lookupType("versioned_document.Document"); 101 | const StringData = root.lookupType("topotext.String"); 102 | 103 | record.fields.document = Document.decode(record.fields.documentData); 104 | record.fields.text = StringData.decode(record.fields.document.version[record.fields.document.version.length - 1].data); 105 | 106 | return record; 107 | }); 108 | 109 | }, 110 | resolve(...notes) { 111 | const self = this; 112 | 113 | return new Promise(function(resolve, reject) { 114 | var host = getHostFromWebservice(self.account.webservices.ckdatabasews); 115 | 116 | const content = JSON.stringify({ 117 | "shortGUIDs": notes.map(note => ({ 118 | "value": typeof note == "string" ? note : note.shortGUID 119 | })) 120 | }); 121 | 122 | 123 | request.post("https://" + host + "/database/1/com.apple.cloudkit/production/public/records/resolve?" + paramStr({ 124 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 125 | "clientId": self.clientId, 126 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 127 | "dsid": self.account.dsInfo.dsid 128 | }), { 129 | headers: fillDefaults({ 130 | 'Host': host, 131 | 'Cookie': cookiesToStr(self.auth.cookies), 132 | 'Content-Length': content.length 133 | }, self.clientSettings.defaultHeaders), 134 | body: content 135 | }, function(err, response, body) { 136 | if (err) return callback(err); 137 | var result = JSON.parse(body); 138 | 139 | const records = result.results.map(res => res.rootRecord).map(record => { 140 | // Decrypt title & snippet 141 | record.fields.title = decryptBase64(record.fields.TitleEncrypted.value).toString(); 142 | record.fields.snippet = decryptBase64(record.fields.SnippetEncrypted.value).toString(); 143 | 144 | const data = decryptBase64(record.fields.TextDataEncrypted.value); 145 | record.fields.documentData = (data[0] === 0x1f && data[1] === 0x8b) ? zlib.gunzipSync(data) : zlib.inflateSync(data); 146 | 147 | // load protobuf definitions 148 | const root = getProtobufRoot(); 149 | 150 | const Document = root.lookupType("versioned_document.Document"); 151 | const StringData = root.lookupType("topotext.String"); 152 | 153 | record.fields.document = Document.decode(record.fields.documentData); 154 | record.fields.text = StringData.decode(record.fields.document.version[record.fields.document.version.length - 1].data); 155 | 156 | return record; 157 | }); 158 | 159 | resolve(records); 160 | }); 161 | }); 162 | 163 | }, 164 | __zone(syncToken = undefined, callback = function() {}) { 165 | var self = this; 166 | var host = getHostFromWebservice(self.account.webservices.ckdatabasews); 167 | 168 | var content = JSON.stringify({ 169 | "zones": [ 170 | { 171 | "zoneID": { 172 | "zoneName": "Notes", 173 | "zoneType": "REGULAR_CUSTOM_ZONE" 174 | }, 175 | "desiredKeys": [ 176 | "TitleEncrypted", 177 | "SnippetEncrypted", 178 | "FirstAttachmentUTIEncrypted", 179 | "FirstAttachmentThumbnail", 180 | "FirstAttachmentThumbnailOrientation", 181 | "ModificationDate", 182 | "Deleted", 183 | "Folders", 184 | "Folder", 185 | "Attachments", 186 | "ParentFolder", 187 | "Folder", 188 | "Note", 189 | "LastViewedModificationDate", 190 | "MinimumSupportedNotesVersion" 191 | ], 192 | "desiredRecordTypes": [ 193 | "Note", 194 | "SearchIndexes", 195 | "Folder", 196 | "PasswordProtectedNote", 197 | "User", 198 | "Users", 199 | "Note_UserSpecific", 200 | "cloudkit.share" 201 | ], 202 | "syncToken": syncToken, 203 | "reverse": true 204 | } 205 | ] 206 | }); 207 | 208 | request.post("https://" + host + "/database/1/com.apple.notes/production/private/changes/zone?" + paramStr({ 209 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 210 | "clientId": self.clientId, 211 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 212 | "dsid": self.account.dsInfo.dsid 213 | }), { 214 | headers: fillDefaults({ 215 | 'Host': host, 216 | 'Cookie': cookiesToStr(self.auth.cookies), 217 | 'Content-Length': content.length 218 | }, self.clientSettings.defaultHeaders), 219 | body: content 220 | }, function(err, response, body) { 221 | if (err) return callback(err); 222 | var result = JSON.parse(body); 223 | 224 | callback(null, result.zones[0]); 225 | }); 226 | }, 227 | createFolders(folders, callback) { 228 | var self = this; 229 | folders = folders instanceof Array ? folders : [folders]; 230 | 231 | var operations = folders.map(function(folder) { 232 | return { 233 | "operationType": "create", 234 | "record": { 235 | "recordName": newId().toLowerCase(), 236 | "fields": { 237 | "TitleEncrypted": { 238 | "value": encryptBase64(folder).toString("base64") 239 | } 240 | }, 241 | "recordType": "Folder" 242 | } 243 | }; 244 | }); 245 | //operations = [{"operationType":"create","record":{"recordName":"r03e0ee1-7f72-41cd-b3c6-88e8d8452f51","fields":{"TitleEncrypted":{"value":"TmV1"}},"recordType":"Folder"}}] 246 | content = JSON.stringify({"operations":[{"operationType":"create","record":{"recordName":"d23e0ee1-7f72-41cd-b3c6-88e8d8452f51","fields":{"TitleEncrypted":{"value":"TmV1","type":"ENCRYPTED_BYTES"}},"recordType":"Folder"}}],"zoneID":{"zoneName":"Notes"}}); 247 | self.Notes.__modify(content, function(err, result) { 248 | if (err) return callback(err); 249 | callback(null, result.records); 250 | }); 251 | }, 252 | __lookup(records) { 253 | var self = this; 254 | 255 | return new Promise(function(resolve, reject) { 256 | var host = getHostFromWebservice(self.account.webservices.ckdatabasews); 257 | 258 | var content = JSON.stringify({ 259 | "records": records.map(record => ({ 260 | recordName: typeof record === "object" ? record.recordName : record 261 | })), 262 | "zoneID": { 263 | "zoneName": "Notes" 264 | } 265 | }); 266 | request.post("https://" + host + "/database/1/com.apple.notes/production/private/records/lookup?" + paramStr({ 267 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 268 | "clientId": self.clientId, 269 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 270 | "dsid": self.account.dsInfo.dsid, 271 | "remapEnums": true, 272 | "ckjsVersion": "2.0" 273 | }), { 274 | headers: fillDefaults({ 275 | 'Host': host, 276 | 'Cookie': cookiesToStr(self.auth.cookies), 277 | 'Content-Length': content.length 278 | }, self.clientSettings.defaultHeaders), 279 | body: content 280 | }, function(err, response, body) { 281 | if (err) return reject(err); 282 | const result = JSON.parse(body); 283 | resolve(result); 284 | }); 285 | }); 286 | }, 287 | __modify(content, callback) { 288 | var self = this; 289 | 290 | var host = getHostFromWebservice(self.account.webservices.ckdatabasews); 291 | 292 | request.post("https://" + host + "/database/1/com.apple.notes/production/private/records/modify?" + paramStr({ 293 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 294 | "clientId": self.clientId, 295 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 296 | "dsid": self.account.dsInfo.dsid, 297 | "remapEnums": true, 298 | "ckjsVersion": "2.0" 299 | }), { 300 | headers: fillDefaults({ 301 | 'Host': host, 302 | 'Cookie': cookiesToStr(self.auth.cookies), 303 | 'Content-Length': content.length 304 | }, self.clientSettings.defaultHeaders), 305 | body: content 306 | }, function(err, response, body) { 307 | if (err) return console.error(err); 308 | var result = JSON.parse(body); 309 | callback(null, result); 310 | }); 311 | } 312 | } 313 | 314 | function decryptBase64(value) { 315 | return Buffer.from(value, "base64"); 316 | } 317 | function encryptBase64(value) { 318 | return Buffer.from(value, "utf8"); 319 | } 320 | 321 | function getProtobufRoot() { 322 | if (!this.protobufRoot) { 323 | const paths = ["versioned-document.proto", "topotext.proto"/*, "crframework.proto"*/].map(p => path.join(__dirname, '../protobuf/', p)); 324 | this.protobufRoot = protobuf.loadSync(paths); 325 | } 326 | return this.protobufRoot; 327 | } 328 | -------------------------------------------------------------------------------- /resources/apps/Reminders.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, newId, indexOfKey, paramString, paramStr, timeArray, arrayTime, fillDefaults} = require("./../helper"); 3 | 4 | module.exports = { 5 | getOpenTasks(callback = function() {}) { 6 | var self = this; 7 | var host = getHostFromWebservice(self.account.webservices.reminders); 8 | 9 | var taskPromise = new Promise(function(resolve, reject) { 10 | //https://p44-remindersws.icloud.com/rd/startup?clientBuildNumber=17EProject65&clientId=A100C28C-110F-41C2-9E33-A8561E44033A&clientMasteringNumber=17E57&clientVersion=4.0&dsid=11298614181&lang=de-de&usertz=US%2FPacific 11 | request.get("https://" + host + "/rd/startup?" + paramStr({ 12 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 13 | "clientId": self.clientId, 14 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 15 | "dsid": self.account.dsInfo.dsid, 16 | "lang": self.clientSettings.language, 17 | "usertz": self.clientSettings.timezone 18 | }), { 19 | headers: fillDefaults({ 20 | 'Host': host, 21 | 'Cookie': cookiesToStr(self.auth.cookies) 22 | }, self.clientSettings.defaultHeaders) 23 | }, function(err, response, body) { 24 | if (err) { 25 | reject(err); 26 | return callback(err); 27 | } 28 | var result = JSON.parse(body); 29 | 30 | self.Reminders.collections = result.Collections; 31 | 32 | result.Collections.forEach(function(collection) { 33 | collection.createdDate = arrayTime(collection.createdDate); 34 | collection.tasks = []; 35 | }); 36 | 37 | result.Reminders.forEach(function(reminder) { 38 | reminder.alarms.forEach(function(alarm) { 39 | alarm.onDate = arrayTime(alarm.onDate); 40 | }); 41 | reminder.createdDate = arrayTime(reminder.createdDate); 42 | reminder.lastModifiedDate = arrayTime(reminder.lastModifiedDate); 43 | reminder.dueDate = arrayTime(reminder.dueDate); 44 | 45 | result.Collections[result.Collections.indexOfKey(reminder.pGuid, "guid")].tasks.push(reminder); 46 | 47 | //console.log(reminder); 48 | }); 49 | resolve(result.Collections); 50 | callback(null, result.Collections); 51 | }); 52 | }); 53 | 54 | return taskPromise; 55 | }, 56 | changeTask(task, callback = function() {}) { 57 | var self = this; 58 | 59 | task.lastModifiedDate = new Date(); 60 | 61 | return self.Reminders.__task(task, callback, "PUT"); 62 | }, 63 | __task(task, callback = function() {}, methodOverride = false) { 64 | var self = this; 65 | var host = getHostFromWebservice(self.account.webservices.reminders); 66 | 67 | var taskPromise = new Promise(function(resolve, reject) { 68 | task.createdDate = timeArray(task.createdDate); 69 | task.lastModifiedDate = timeArray(task.lastModifiedDate); 70 | task.dueDate = timeArray(task.dueDate); 71 | task.startDate = timeArray(task.startDate); 72 | 73 | if ("alarms" in task) { 74 | task.alarms.forEach(function(alarm) { 75 | alarm.onDate = timeArray(alarm.onDate); 76 | }); 77 | } 78 | 79 | var content = JSON.stringify({ 80 | "Reminders": task, 81 | "ClientState": { 82 | "Collections": [ 83 | { 84 | "guid": task.pGuid 85 | } 86 | ] 87 | } 88 | }); 89 | 90 | request.post("https://p67-remindersws.icloud.com/rd/reminders/tasks?clientBuildNumber=2003Project34&clientId=b0d019e2-3eca-4e1c-b5a8-907bf9b01721&clientMasteringNumber=2003B27&clientVersion=4.0&dsid=181764936&lang=de-de&usertz=Europe%2FRome", { 91 | headers: fillDefaults({ 92 | 'Host': host, 93 | 'Cookie': cookiesToStr(self.auth.cookies), 94 | 'Content-Length': content.length 95 | } ,self.clientSettings.defaultHeaders), 96 | body: content 97 | }, function(err, response, body) { 98 | if (err) { 99 | reject(err); 100 | return callback(err); 101 | } 102 | var result = JSON.parse(body); 103 | resolve(result); 104 | callback(null, result); 105 | }); 106 | }); 107 | 108 | 109 | return taskPromise; 110 | }, 111 | completeTask(task, callback = function() {}) { 112 | var self = this; 113 | var host = getHostFromWebservice(self.account.webservices.reminders); 114 | 115 | var completePromise = new Promise(function(resolve, reject) { 116 | task.completedDate = timeArray(new Date()); 117 | task.createdDate = timeArray(task.createdDate); 118 | task.lastModifiedDate = timeArray(task.lastModifiedDate); 119 | 120 | var content = JSON.stringify({ 121 | "Reminders": task, 122 | "ClientState": { 123 | "Collections": [ 124 | 125 | ] 126 | } 127 | }); 128 | 129 | request.post("https://" + host + "/rd/reminders/" + task.pGuid + "?" + paramStr({ 130 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 131 | "clientId": self.clientId, 132 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 133 | "dsid": self.account.dsInfo.dsid, 134 | "lang": self.clientSettings.language, 135 | "usertz": self.clientSettings.timezone, 136 | "methodOverride": "PUT" 137 | }), { 138 | headers: fillDefaults({ 139 | 'Host': host, 140 | 'Cookie': cookiesToStr(self.auth.cookies), 141 | 'Content-Length': content.length 142 | }, self.clientSettings.defaultHeaders), 143 | body: content 144 | }, function(err, response, body) { 145 | if (err) { 146 | reject(err); 147 | return callback(err); 148 | } 149 | var result = JSON.parse(body); 150 | resolve(result); 151 | callback(null, result); 152 | }); 153 | }); 154 | 155 | return completePromise; 156 | }, 157 | getCompletedTasks(callback = function() {}) { 158 | var self = this; 159 | var host = getHostFromWebservice(self.account.webservices.reminders); 160 | var taskPromise = new Promise(function(resolve, reject) { 161 | //https://p44-remindersws.icloud.com/rd/completed?clientBuildNumber=17EProject65&clientId=2E1D48B0-04F1-453B-864C-E7A30080AA2F&clientMasteringNumber=17E57&clientVersion=4.0&dsid=11298614181&lang=de-de&usertz=US%2FPacific 162 | request.get("https://" + host + "/rd/completed?" + paramStr({ 163 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 164 | "clientId": self.clientId, 165 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 166 | "dsid": self.account.dsInfo.dsid, 167 | "lang": self.clientSettings.language, 168 | "usertz": self.clientSettings.timezone 169 | }), { 170 | headers: fillDefaults({ 171 | 'Host': host, 172 | 'Cookie': cookiesToStr(self.auth.cookies) 173 | }, self.clientSettings.defaultHeaders) 174 | }, function(err, response, body) { 175 | if (err) { 176 | reject(err); 177 | return callback(err); 178 | } 179 | var result = JSON.parse(body); 180 | 181 | result.Reminders.forEach(function(task) { 182 | task.createdDate = arrayTime(task.createdDate); 183 | task.lastModifiedDate = arrayTime(task.lastModifiedDate); 184 | task.completedDate = arrayTime(task.completedDate); 185 | task.dueDate = arrayTime(task.dueDate); 186 | task.alarms.forEach(function(alarm) { 187 | alarm.onDate = arrayTime(alarm.onDate); 188 | }); 189 | }); 190 | resolve(result.Reminders); 191 | callback(null, result.Reminders); 192 | }); 193 | }); 194 | 195 | return taskPromise; 196 | }, 197 | createTask(task, callback = function() {}) { 198 | var self = this; 199 | 200 | task = fillDefaults(task, { 201 | "title": null, 202 | "description": null, 203 | "pGuid": "tasks", 204 | "etag": null, 205 | "order": null, 206 | "priority": 0, 207 | "recurrence": null, 208 | "alarms": [], 209 | "createdDateExtended": new Date().getTime(), 210 | "guid": newId(), 211 | "startDate": null, 212 | "startDateTz": null, 213 | "startDateIsAllDay": false, 214 | "completedDate": null, 215 | "dueDate": null, 216 | "dueDateIsAllDay": false, 217 | "lastModifiedDate": null, 218 | "createdDate": new Date(), 219 | "isFamily": null 220 | }); 221 | return self.Reminders.__task(task, callback, "POST"); 222 | }, 223 | openTask(task, callback) { 224 | 225 | /* 226 | Not working yet because of Apple's API 227 | */ 228 | 229 | var self = this; 230 | delete task.completedDate; 231 | 232 | self.Reminders.changeTask(task, callback); 233 | }, 234 | deleteTask(task, callback = function() {}) { 235 | var self = this; 236 | 237 | task = { 238 | "guid": task.guid, 239 | "etag": task.etag, 240 | "pGuid": task.pGuid 241 | } 242 | 243 | return self.Reminders.__task([task], callback, "DELETE"); 244 | }, 245 | __collection(collection, callback = function() {}, methodOverride = false) { 246 | //https://p44-remindersws.icloud.com/rd/collections/9E489E0F-B5FF-4D3E-AC45-FBE1B3C04231?clientBuildNumber=17EProject65&clientId=082F853D-784D-408B-8BF3-2F4DEE703E49&clientMasteringNumber=17E57&clientVersion=4.0&dsid=11298614181&ifMatch=FT%3D-%40RU%3Dc16342e5-f8fd-4909-9157-7b2b23e0b38d%40S%3D353&lang=de-de&methodOverride=DELETE&usertz=US%2FPacific 247 | var self = this; 248 | var host = getHostFromWebservice(self.account.webservices.reminders); 249 | 250 | 251 | var content = JSON.stringify({ 252 | "Collections": collection, 253 | "ClientState": { 254 | "Collections": [ 255 | 256 | ] 257 | } 258 | }); 259 | 260 | var collectionPromise = new Promise(function(resolve, reject) { 261 | request.post("https://" + host + "/rd/collections/" + collection.guid + "?" + paramStr((function() { 262 | var args = { 263 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 264 | "clientId": self.clientId, 265 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 266 | "dsid": self.account.dsInfo.dsid, 267 | "lang": self.clientSettings.language, 268 | "usertz": self.clientSettings.timezone, 269 | "methodOverride": methodOverride, 270 | "ifMatch": encodeURIComponent(collection.ctag) 271 | } 272 | if (!args["methodOverride"]) { 273 | delete args["methodOverride"]; 274 | } 275 | return args; 276 | })()), { 277 | headers: fillDefaults({ 278 | 'Host': host, 279 | 'Cookie': cookiesToStr(self.auth.cookies), 280 | 'Content-Length': content.length 281 | }, self.clientSettings.defaultHeaders), 282 | body: content 283 | }, function(err, response, body) { 284 | if (err) { 285 | reject(err); 286 | return callback(err); 287 | } 288 | var result = JSON.parse(body); 289 | resolve(result); 290 | callback(null, result); 291 | }); 292 | }); 293 | 294 | 295 | return collectionPromise; 296 | }, 297 | deleteCollection(collection, callback = function() {}) { 298 | var self = this; 299 | 300 | collection = { 301 | guid: collection.guid 302 | } 303 | 304 | return self.Reminders.__collection(collection, callback, "DELETE"); 305 | }, 306 | createCollection(collection, callback = function() {}) { 307 | var self = this; 308 | 309 | collection = fillDefaults(collection, { 310 | "order": 2, 311 | "title": "New Collection", 312 | "color": "#b14bc9", 313 | "participants": null, 314 | "ctag": null, 315 | "enabled": true, 316 | "symbolicColor": null, 317 | "guid": newId(), 318 | "lastModifiedDate": null, 319 | "createdDate": null, 320 | "createdDateExtended": null, 321 | "completedCount": 0, 322 | "emailNotification": false, 323 | "collectionShareType": null 324 | }); 325 | 326 | return self.Reminders.__collection(collection, callback); 327 | }, 328 | changeCollection(collection, callback = function() {}) { 329 | var self = this; 330 | 331 | collection.lastModifiedDate = timeArray(new Date()); 332 | 333 | return self.Reminders.__collection(collection, callback, "PUT"); 334 | } 335 | 336 | } 337 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const fs = require('fs'); 3 | 4 | const { getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, newId, indexOfKey, fillMethods, fillDefaults} = require("./resources/helper"); 5 | 6 | Array.prototype.indexOfKey = indexOfKey; 7 | 8 | class iCloud extends EventEmitter { 9 | constructor(session = {}, username, password) { 10 | super(); 11 | var self = this; 12 | // LoggedIn is false because we can't be sure that the session is valid 13 | self.loggedIn = false; 14 | // enable push initially 15 | self.enablePush = true; 16 | // If the session argument is a string, it will be interpreted as a file path and the file will be read 17 | if (typeof session === "string") { 18 | // Set instances's sessionFile key to use it later as path 19 | self.sessionFile = (" " + session).substring(1); 20 | // Read the session file 21 | fs.readFile(session, "utf8", function(err, contents) { 22 | // If there was no error reading the file, set the session argument to the file's contents 23 | if (!err) { 24 | // Set session argument to the contents of the file 25 | session = JSON.parse(contents); 26 | // Continue with this session object as base 27 | sessionInit(session); 28 | } 29 | else { 30 | // If it was not possible to read the file, set the session to an empty object to work with it 31 | session = {}; 32 | // Continue with the empty session 33 | sessionInit(session); 34 | } 35 | }); 36 | } 37 | else { 38 | // The given session argument is actually an object literal, therefore there is no file to be read. Continue :) 39 | sessionInit(session); 40 | } 41 | 42 | var currTopics = self.Setup.getPushTopics(self.apps); 43 | 44 | function sessionInit(session) { 45 | // Session Validation. This adds default properties to the session that doesn't exists 46 | session = fillDefaults(session, { 47 | username: username, 48 | password: password, 49 | auth: { 50 | token: null, 51 | xAppleTwosvTrustToken: null, 52 | cookies: [] 53 | }, 54 | twoFactorAuthentication: false, 55 | securityCode: null, 56 | clientSettings: { 57 | language: "en-us", 58 | locale: "en_US", 59 | xAppleWidgetKey: '83545bf919730e51dbfba24e7e8a78d2', 60 | get ["xAppleIFDClientInfo"]() { 61 | return { 62 | "U": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.1 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.1", 63 | "L": session.clientSettings.locale, 64 | "Z": "GMT+02:00", 65 | "V": "1.1", 66 | "F": "" 67 | }; 68 | }, 69 | timezone: "US/Pacific", 70 | clientBuildNumber: "2018Project35", 71 | clientMasteringNumber: "2018B29", 72 | defaultHeaders: { 73 | 'Referer': 'https://www.icloud.com/', 74 | 'Content-Type': 'text/plain', 75 | 'Origin': 'https://www.icloud.com', 76 | 'Host': '', 77 | 'Accept': '*/*', 78 | 'Connection': 'keep-alive', 79 | get ["Accept-Language"]() { 80 | return session.clientSettings.language; 81 | }, 82 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.25 (KHTML, like Gecko) Version/11.0 Safari/604.1.25', 83 | 'Cookie': '', 84 | 'X-Requested-With': "XMLHttpRequest" 85 | } 86 | }, 87 | clientId: self.Setup.getClientId(), 88 | apps: self.apps, 89 | push: { 90 | topics: currTopics ? currTopics.filter((topic, index) => currTopics.indexOf(topic) == index) : [], 91 | token: null, 92 | ttl: 43200, 93 | courierUrl: "", 94 | registered: [] 95 | }, 96 | account: {}, 97 | logins: [] 98 | }); 99 | 100 | // Session object is validated. Now, the (self) instance will be extended with session's properties using the fill defaults function. 101 | self = fillDefaults(self, session); 102 | 103 | Object.keys(self.apps).forEach(function(appPropName) { 104 | if ("instanceName" in self.apps[appPropName]) { 105 | var service = require("./" + self.apps[appPropName].modulePath); 106 | self[self.apps[appPropName].instanceName] = fillMethods({}, service, self); 107 | } 108 | }); 109 | 110 | 111 | 112 | // Now, validate the session with checking for important aspects that show that the session can be used to get data (e.g. there need to be a token, some cookies and account info) 113 | self.loggedIn = (function() { 114 | //console.log(self.auth.cookies.length > 0, !!self.auth.token, Object.keys(self.account).length > 0, self.username === username); 115 | return ((self.auth.cookies.length > 0) && (self.auth.token && Object.keys(self.account).length > 0) && (username ? (self.username === username) : false)); 116 | })(); 117 | 118 | 119 | self.cookiesValid = (function() { 120 | const timestamp = new Date().getTime(); 121 | // Get list of cookies, represented to a boolean value wether the cookie is expired or no 122 | // ignore cookie wich is expiring in 1970 --> so no extra code auth, when starting app 123 | const cookiesExpired = self.auth.cookies.map(function (cookie) { 124 | if ('X-APPLE-WEBAUTH-HSA-LOGIN' in cookie && 'Expires' in cookie) { 125 | return false; 126 | } else { 127 | return new Date(cookie.Expires).getTime() - timestamp < 0; 128 | } 129 | }); 130 | // If no cookie is expired, the array contains just 'false' keys 131 | // Return wether there is no expired cookie (true) 132 | return cookiesExpired.indexOf(true) === -1; 133 | })(); 134 | 135 | 136 | 137 | // If the session is valid, the client is ready! Emit the 'ready' event of the (self) instance 138 | if (self.loggedIn && self.cookiesValid) { 139 | self.logins.push(new Date().getTime()); 140 | self.emit("ready"); 141 | } 142 | // If not, the session is invalid: An error event occurs and username and password arguments will be used for logging in and creating a new session 143 | else { 144 | self.emit("err", { 145 | error: "Session is expired or invalid", 146 | errorCode: 6 147 | }); 148 | // 'progress' event of login is fired because it's an automatic aspect of the algorithm that it tries to login if the session was invalid 149 | self.emit("progress", { 150 | action: "start", 151 | parentAction: "login", 152 | progress: 0, 153 | message: "Trying to reset session and login" 154 | }); 155 | 156 | // Login with username and password 157 | self.login(self.username, self.password, function(err) { 158 | if (err) { 159 | // If an error ocurs, fire an 'error' event 160 | 161 | // handle non-standard errors 162 | if (err.error) { 163 | return self.emit("err", err) 164 | } 165 | 166 | // otherwise, generic handling 167 | return self.emit("err", { 168 | error: "Account is broken or password and username are invalid", 169 | errorCode: 7 170 | }); 171 | } 172 | self.username = username; 173 | self.password = password; 174 | 175 | self.auth.created = new Date().getTime(); 176 | self.logins.push(self.auth.created); 177 | // Client is ready 178 | self.emit("ready"); 179 | }); 180 | } 181 | } 182 | } 183 | set securityCode(code) { 184 | var self = this; 185 | 186 | self.Setup.enterSecurityCode(self, code, function(result) { 187 | if (!result) { 188 | return self.emit("err", { 189 | message: "Failed to enter security code", 190 | code: 16 191 | }); 192 | } 193 | self.emit("ready"); 194 | }); 195 | 196 | self.__securityCode = code; 197 | } 198 | get securityCode() { 199 | return this.__securityCode; 200 | } 201 | async sendSecurityCode(mode) { 202 | if (mode === "sms") { 203 | const { response, body } = await this.Setup.__securityPhone(this, "sms"); 204 | } 205 | else if (mode === "voice") { 206 | const { response, body } = await this.Setup.__securityPhone(this, "voice"); 207 | } 208 | else { 209 | const { response, body } = await this.Setup.__securityCode(this, null, "PUT"); 210 | } 211 | } 212 | get twoFactorAuthenticationIsRequired() { 213 | return this.twoFactorAuthentication && !this.auth.xAppleTwosvTrustToken && !this.securityCode; 214 | } 215 | // Login method 216 | login(account, password, callback) { 217 | var self = this; 218 | self.Setup.getAuthToken(account, password, self, function(err, authentification) { 219 | if (err) return callback(err); 220 | // Got token 221 | self.auth.token = authentification.token; 222 | 223 | self.clientSettings.xAppleIDSessionId = authentification.sessionID; 224 | self.clientSettings.scnt = authentification.scnt; 225 | // If two factor authentification is required 226 | if (authentification.response.authType === "hsa2") { 227 | // Set the 'twoFactorAuthentication' property to true to communicate this 228 | self.twoFactorAuthentication = true; 229 | self.emit("err", { 230 | error: "Two-factor-authentication is required.", 231 | code: 15 232 | }); 233 | } 234 | self.emit("progress", { 235 | parentAction: "login", 236 | action: "authToken", 237 | progress: 1 / 3 238 | }); 239 | self.Setup.accountLogin(self, function(err, result, response) { 240 | if (err) return callback(err); 241 | // Logged in 242 | // Add result to instance 243 | self.account = result; 244 | 245 | // Parse cookies to objects 246 | var cookies = parseCookieStr(response.headers["set-cookie"]); 247 | self.auth.cookies = fillCookies(self.auth.cookies, cookies); 248 | // Parse cookie objects to a cookie string array and join it to a single string 249 | var cookiesStr = cookiesToStr(self.auth.cookies); 250 | 251 | // Fire progress event 252 | self.emit("progress", { 253 | parentAction: "login", 254 | action: "accountLogin", 255 | progress: 2 / 3 256 | }); 257 | self.loggedIn = true; 258 | 259 | self.Setup.getPushToken(self, cookiesStr, function(err, token, url, cookies) { 260 | if (err) return callback(err); 261 | // Got push token. 262 | self.push.token = token; 263 | self.push.courierUrl = url; 264 | 265 | self.emit("progress", { 266 | parentAction: "login", 267 | action: "pushToken", 268 | progress: 3 / 3 269 | }); 270 | 271 | callback(null, self); 272 | self.emit("sessionUpdate"); 273 | 274 | }); 275 | 276 | }); 277 | }); 278 | //callback(null, "Hey you!"); 279 | } 280 | registerPushService(service, callback = function() {}) { 281 | var self = this; 282 | self.Setup.registerPush(self, service, function(err, result) { 283 | if (err) { 284 | return callback(err); 285 | } 286 | self.push.registered.push(service); 287 | self.emit("sessionUpdate"); 288 | callback(null, result); 289 | }); 290 | } 291 | initPush(callback = function () { }) { 292 | // breaks the callback loop if we decide to disable push 293 | if (!this.enablePush) { 294 | this.enablePush = true; 295 | return; 296 | } 297 | 298 | var self = this; 299 | self.Setup.registerTopics(self, function(err, result) { 300 | if (err) return console.error(err); 301 | 302 | self.push.registered = self.push.registered.concat(result.registeredTopics); 303 | 304 | 305 | }); 306 | 307 | for (let appName in self.apps) { 308 | if (self.apps.hasOwnProperty(appName) && self.apps[appName].containerIdentifier) { 309 | const customPushTopic = self.apps[appName]; 310 | this.registerPushService(customPushTopic.containerIdentifier); 311 | } 312 | } 313 | 314 | self.Setup.initPush(self, function(err, result) { 315 | if (err) { 316 | self.emit("err", { 317 | error: "Push token is expired. Getting new one...", 318 | errorCode: 22 319 | }); 320 | return self.Setup.getPushToken(self, cookiesToStr(self.auth.cookies), function(err, token, url, cookies) { 321 | if (err) return callback(err); 322 | // Got push token. 323 | self.push.token = token; 324 | self.push.courierUrl = url; 325 | 326 | //callback(null, self); 327 | self.initPush(callback); 328 | 329 | self.emit("sessionUpdate"); 330 | 331 | }); 332 | } 333 | // Push request is answered 334 | self.emit("push", result); 335 | self.initPush(callback); 336 | }); 337 | } 338 | 339 | deactivatePush() { 340 | this.enablePush = false; 341 | } 342 | 343 | exportSession() { 344 | // Export session as object 345 | return { 346 | username: this.username || null, 347 | password: this.password || null, 348 | twoFactorAuthentication: this.twoFactorAuthentication, 349 | securityCode: this.__securityCode || null, 350 | auth: this.auth, 351 | clientId: this.clientId, 352 | push: this.push, 353 | account: this.account, 354 | logins: this.logins, 355 | clientSettings: this.clientSettings 356 | } 357 | } 358 | saveSession(file = this.sessionFile) { 359 | // If file argument is not given, try to use the source the session was read from (Only possible if given) 360 | if (file) { 361 | fs.writeFile(file, JSON.stringify(this.exportSession(), null, 2), (err) => { 362 | if (err) return this.emit("error", err); 363 | }); 364 | } 365 | else { 366 | return this.emit("err", { 367 | error: "File path is invalid", 368 | errorCode: 12 369 | }) 370 | } 371 | } 372 | get Setup() { 373 | return require("./setup"); 374 | } 375 | get apps() { 376 | return this.Setup.getApps(); 377 | } 378 | 379 | } 380 | 381 | module.exports = iCloud; 382 | -------------------------------------------------------------------------------- /resources/apps/Mail.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, newId, indexOfKey, paramString, paramStr, timeArray, arrayTime, fillDefaults} = require("./../helper"); 3 | 4 | 5 | module.exports = { 6 | getFolders(callback = function() {}) { 7 | var self = this; 8 | //var host = getHostFromWebservice(self.account.webservices.mailws); 9 | 10 | return self.Mail.__folder("list", null, callback); 11 | }, 12 | listMessages(folder, selected = 1, count = 50, callback = function() {}) { 13 | var self = this; 14 | var host = "p44-mailws.icloud.com"; 15 | 16 | var content = JSON.stringify({ 17 | "jsonrpc": "2.0", 18 | "id": (new Date().getTime()) + "/1", 19 | "method": "list", 20 | "params": { 21 | "guid": folder.guid, 22 | "sorttype": "Date", 23 | "sortorder": "descending", 24 | "searchtype": null, 25 | "searchtext": null, 26 | "requesttype": "index", 27 | "selected": selected, 28 | "count": count, 29 | "rollbackslot": "0.0" 30 | }, 31 | "userStats": {}, 32 | "systemStats": [0, 0, 0, 0] 33 | }); 34 | 35 | var listPromise = new Promise(function(resolve, reject) { 36 | self.Mail.__message(content, function(err, data) { 37 | if (err) { 38 | reject(err); 39 | return callback(err); 40 | } 41 | data = { 42 | meta: data.result.filter(message => message.type === "CoreMail.MessageListMetaData"), 43 | messages: data.result.filter(message => message.type === "CoreMail.Message") 44 | }; 45 | if (data.meta.length >= 1) { 46 | data.meta = data.meta[0]; 47 | } 48 | resolve(data); 49 | callback(null, data); 50 | }); 51 | }); 52 | 53 | 54 | return listPromise; 55 | }, 56 | getMessage(mail, callback = function() {}) { 57 | var self = this; 58 | var host = "p44-mailws.icloud.com"; 59 | 60 | //return console.log(mail); 61 | 62 | var content = JSON.stringify({ 63 | "jsonrpc": "2.0", 64 | "id": (new Date().getTime()) + "/1", 65 | "method": "get", 66 | "params": { 67 | "guid": mail.guid, 68 | "parts": mail.parts.map(part => part.guid) 69 | }, 70 | "userStats": {}, 71 | "systemStats": [0, 0, 0, 0] 72 | }); 73 | var detailsPromise = new Promise(function(resolve, reject) { 74 | self.Mail.__message(content, function(err, data) { 75 | if (err) { 76 | reject(err); 77 | return callback(err); 78 | } 79 | var mailDetailed = data.result[data.result.indexOfKey("CoreMail.MessageDetail", "recordType")]; 80 | mailDetailed.data = mailDetailed.parts.map(part => part.content).join(""); 81 | delete mailDetailed.parts; 82 | resolve(mailDetailed); 83 | callback(null, mailDetailed); 84 | }); 85 | }); 86 | 87 | return detailsPromise; 88 | 89 | }, 90 | move(messages, destination, callback = function() {}) { 91 | messages = messages instanceof Array ? messages : [messages]; 92 | var self = this; 93 | 94 | var folder = getFolder(messages); 95 | if (!folder) return callback({ 96 | error: "Messages are not in the same folder", 97 | errorCode: 21 98 | }); 99 | 100 | var content = JSON.stringify({ 101 | "jsonrpc": "2.0", 102 | "id": (new Date()) + "/1", 103 | "method": "move", 104 | "params": { 105 | "folder": folder, 106 | "dest": destination.guid, 107 | "uids": messages.map(message => message.uid), 108 | "rollbackslot": "0.0" 109 | }, 110 | "userStats": { 111 | "tm": 1, 112 | "ae": 1 113 | }, 114 | "systemStats": [0, 0, 0, 0] 115 | }); 116 | 117 | var movePromise = new Promise(function(resolve, reject) { 118 | self.Mail.__message(content, function(err, data) { 119 | if (err) { 120 | reject(err); 121 | return callback(err); 122 | } 123 | resolve(data); 124 | callback(null, data); 125 | }); 126 | }); 127 | 128 | return movePromise; 129 | }, 130 | flag(messages, flag = "flagged", callback = function() {}) { 131 | var self = this; 132 | return self.Mail.__flag("setflag", flag, messages, callback); 133 | }, 134 | unflag(messages, flag = "flagged", callback = function() {}) { 135 | var self = this; 136 | return self.Mail.__flag("clrflag", flag, messages, callback); 137 | }, 138 | delete(messages, callback = function() {}) { 139 | var self = this; 140 | messages = messages instanceof Array ? messages : [messages]; 141 | 142 | var folder = getFolder(messages); 143 | if (!folder) return callback({ 144 | error: "Messages are not in the same folder", 145 | errorCode: 21 146 | }); 147 | 148 | var content = JSON.stringify({ 149 | "jsonrpc": "2.0", 150 | "id": (new Date()) + "/1", 151 | "method": "delete", 152 | "params": { 153 | "folder": folder, 154 | "uids": messages.map(message => message.uid), 155 | "rollbackslot": "0.0" 156 | }, 157 | "userStats": {}, 158 | "systemStats": [0, 0, 0, 0] 159 | }, null, 2); 160 | 161 | //return console.log(content); 162 | 163 | var deletePromise = new Promise(function(resolve, reject) { 164 | self.Mail.__message(content, function(err, result) { 165 | if (err) { 166 | reject(err); 167 | return callback(err); 168 | } 169 | resolve(result); 170 | callback(null, result); 171 | }); 172 | }); 173 | 174 | return deletePromise; 175 | }, 176 | send(message, callback = function() {}) { 177 | var self = this; 178 | 179 | 180 | var sendPromise = new Promise(function(resolve, reject) { 181 | 182 | if (self.Mail.preference) { 183 | sendMail(self.Mail.preference[1].emails[self.Mail.preference[1].emails.indexOfKey(true, "canSendFrom")].address, self.Mail.preference[1].fullName); 184 | } 185 | else if (message.from) { 186 | sendMail(); 187 | } 188 | else { 189 | self.Mail.__preference(function(err, data) { 190 | self.Mail.preference = data; 191 | sendMail(self.Mail.preference[1].emails[self.Mail.preference[1].emails.indexOfKey(true, "canSendFrom")].address, self.Mail.preference[1].fullName); 192 | }); 193 | } 194 | function sendMail(address, fullName) { 195 | var from = fullName + "<" + address + ">"; 196 | message = fillDefaults(message, { 197 | "date": new Date().toString(), 198 | "to": null, 199 | "subject": "", 200 | "body": "", 201 | "webmailClientBuild": self.clientSettings.clientBuildNumber, 202 | "attachments": [], 203 | "draftGuid": null 204 | }); 205 | message.from = message.from ? message.from : from; 206 | message.textBody = message.body.replace(/<[^<>]*>/g, ""); 207 | 208 | var content = JSON.stringify({ 209 | "jsonrpc": "2.0", 210 | "id": (new Date().getTime()) + "/1", 211 | "method": "send", 212 | "params": message, 213 | "userStats": { 214 | "biuc": 1 215 | }, 216 | "systemStats": [0, 0, 0, 0] 217 | }); 218 | self.Mail.__message(content, function(err, result) { 219 | if (err) { 220 | reject(err); 221 | return callback(err); 222 | } 223 | resolve(result); 224 | callback(null, result); 225 | }); 226 | } 227 | }); 228 | 229 | return sendPromise; 230 | 231 | 232 | }, 233 | createFolder(folder, callback = function() {}) { 234 | var self = this; 235 | 236 | var params = fillDefaults(folder, { 237 | name: null, 238 | parent: null 239 | }); 240 | params.parent = (typeof params.parent === "object" && params.parent != null) ? params.parent.guid : params.parent; 241 | 242 | return self.Mail.__folder("put", params, callback); 243 | }, 244 | renameFolder(folder, name, callback = function() {}) { 245 | var self = this; 246 | folder.name = name; 247 | 248 | return self.Mail.__folder("rename", folder, callback); 249 | 250 | }, 251 | moveFolder(folder, target, callback = function() {}) { 252 | var self = this; 253 | if (target) folder.parent = target.guid; 254 | 255 | return self.Mail.__folder("move", folder, callback); 256 | }, 257 | deleteFolder(folder, callback = function() {}) { 258 | var self = this; 259 | return self.Mail.__folder("delete", { 260 | guid: typeof folder === "string" ? folder : folder.guid 261 | }, callback); 262 | }, 263 | __flag(method, flag, messages, callback = function() {}) { 264 | var self = this; 265 | 266 | messages = messages instanceof Array ? messages : [messages]; 267 | 268 | var flagPromise = new Promise(function(resolve, reject) { 269 | var folder = getFolder(messages); 270 | if (!folder) { 271 | var errObj = { 272 | error: "Messages are not in the same folder", 273 | errorCode: 21 274 | }; 275 | reject(errObj); 276 | return callback(errObj); 277 | } 278 | var content = JSON.stringify({ 279 | "jsonrpc": "2.0", 280 | "id": (new Date().getTime()) + "/1", 281 | "method": method, 282 | "params": { 283 | "folder": folder, 284 | "uids": messages.map(message => message.uid), 285 | "flag": flag, 286 | "rollbackslot": "0.0" 287 | }, 288 | "userStats": {}, 289 | "systemStats": [0, 0, 0, 0] 290 | }); 291 | 292 | self.Mail.__message(content, function(err, data) { 293 | if (err) { 294 | reject(err); 295 | return callback(err); 296 | } 297 | resolve(data); 298 | callback(null, data); 299 | }); 300 | }); 301 | 302 | return flagPromise; 303 | }, 304 | __message(content, callback = function() {}) { 305 | var self = this; 306 | var host = "p44-mailws.icloud.com"; 307 | 308 | request.post("https://" + host + "/wm/message?" + paramStr({ 309 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 310 | "clientId": self.clientId, 311 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 312 | "dsid": self.account.dsInfo.dsid 313 | }), { 314 | headers: fillDefaults({ 315 | 'Host': host, 316 | 'Cookie': cookiesToStr(self.auth.cookies), 317 | 'Content-Length': content.length 318 | }, self.clientSettings.defaultHeaders), 319 | body: content 320 | }, function(err, response, body) { 321 | if (err) return callback(err); 322 | var data = handleAuthFail(body, response, self); 323 | if (data) { 324 | callback(null, data); 325 | } 326 | else { 327 | self.Mail.__message(content, callback) 328 | } 329 | }); 330 | }, 331 | __folder(method, params, callback = function() {}) { 332 | var self = this; 333 | var host = "p44-mailws.icloud.com"; 334 | 335 | 336 | 337 | var content = { 338 | "jsonrpc": "2.0", 339 | "id": (new Date().getTime()) + "/1", 340 | "method": method, 341 | "params": params, 342 | "userStats": {}, 343 | "systemStats": [0, 0, 0, 0] 344 | }; 345 | if (!content.params) delete content.params; 346 | 347 | content = JSON.stringify(content); 348 | 349 | var folderPromise = new Promise(function(resolve, reject) { 350 | request.post("https://" + host + "/wm/folder?" + paramStr({ 351 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 352 | "clientId": self.clientId, 353 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 354 | "dsid": self.account.dsInfo.dsid 355 | }), { 356 | headers: fillDefaults({ 357 | 'Host': host, 358 | 'Cookie': cookiesToStr(self.auth.cookies), 359 | 'Content-Length': content.length 360 | }, self.clientSettings.defaultHeaders), 361 | body: content 362 | }, function(err, response, body) { 363 | if (err) { 364 | reject(err); 365 | return callback(err); 366 | } 367 | var data = handleAuthFail(body, response, self); 368 | if (data && !data.error) { 369 | resolve(data.result); 370 | callback(null, data.result); 371 | } 372 | else if (data && data.error) { 373 | reject(data.error); 374 | callback(data.error); 375 | } 376 | else { 377 | self.Mail.__folder(method, params, callback).then(resolve); 378 | } 379 | }); 380 | }); 381 | 382 | return folderPromise; 383 | }, 384 | __preference(callback) { 385 | var self = this; 386 | var host = "p44-mailws.icloud.com"; 387 | var content = JSON.stringify({ 388 | "jsonrpc": "2.0", 389 | "id": (new Date().getTime()) + "/1", 390 | "method": "list", 391 | "params": { 392 | "locale": self.clientSettings.language, 393 | "timeZone": self.clientSettings.timezone 394 | }, 395 | "userStats": { 396 | "dm": "Widescreen", 397 | "ost": "Date", 398 | "osv": "Descending", 399 | "al": 0, 400 | "vro": 2, 401 | "so": 2 402 | }, 403 | "systemStats": [0, 0, 0, 0] 404 | }); 405 | request.post("https://" + host + "/wm/preference?" + paramStr({ 406 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 407 | "clientId": self.clientId, 408 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 409 | "dsid": self.account.dsInfo.dsid 410 | }), { 411 | headers: fillDefaults({ 412 | 'Host': host, 413 | 'Cookie': cookiesToStr(self.auth.cookies), 414 | 'Content-Length': content.length 415 | }, self.clientSettings.defaultHeaders), 416 | body: content 417 | }, function(err, response, body) { 418 | if (err) return callback(err); 419 | var data = handleAuthFail(body, response, self); 420 | if (data) { 421 | callback(null, data.result); 422 | } 423 | else { 424 | self.Mail.__preference(callback); 425 | } 426 | }); 427 | } 428 | } 429 | 430 | function handleAuthFail(body, response, self) { 431 | try { 432 | var result = JSON.parse(body); 433 | } catch (e) { 434 | // Parse new cookies from response headers & update cookies of current session 435 | self.auth.cookies = fillCookies(self.auth.cookies, parseCookieStr(response.headers["set-cookie"])); 436 | self.emit("sessionUpdate"); 437 | // Try it again 438 | return null; 439 | } finally { 440 | 441 | } 442 | return result; 443 | } 444 | function getFolder(messages) { 445 | var folder = messages[0].folder; 446 | for (var i = 0; i < messages.length; i++) { 447 | if (messages[i].folder != folder) return null; 448 | } 449 | return folder; 450 | } 451 | -------------------------------------------------------------------------------- /resources/apps/Drive.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const https = require('https'); 3 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, fillDefaults} = require("./../helper"); 4 | 5 | function isFolder(item) { 6 | var folderTypes = ["FOLDER", "APP_LIBRARY"]; 7 | return folderTypes.indexOf(item.type) > -1; 8 | } 9 | 10 | module.exports = { 11 | folderCache: { 12 | 13 | }, 14 | getItem(itemPath, callback = function() {}, useCache = true) { 15 | var self = this; 16 | 17 | var itemArgType = itemPath.search("::com.apple.CloudDocs::") > -1 ? "drivewsid" : "unixPath"; 18 | 19 | var filePromise = new Promise(function(resolve, reject) { 20 | if (itemArgType == "drivewsid") { 21 | // An item is requested directly by it's drivewsid 22 | // We can return it directtly if when is a FOLDER or, when it is a FILE, loading it and return it afterwards 23 | listItem(itemPath, function(err, info) { 24 | if (err) { 25 | reject(err); 26 | return callback(err); 27 | } 28 | if (info.type == "FILE") { 29 | loadFile(info); 30 | } 31 | else { 32 | // Return the folder directly 33 | callback(null, info, null); 34 | resolve(info); 35 | } 36 | }, false); 37 | } 38 | else { 39 | /* 40 | 41 | Please remember that this loops trough every part of the path because it's not possible to go directly to a unix path using icloud apis. Therefore we need to look trough every folder on the way of an item. To avoid double requests, this method uses a cache 42 | 43 | */ 44 | 45 | // The requested path 46 | var pathList = itemPath.split("/").filter(item => item != ""); 47 | // The first item of the requested path that will be used has the index 0 48 | var pathLength = 0; 49 | // This is the root path, every document has to be added to 50 | var root = "FOLDER::com.apple.CloudDocs"; 51 | // This is the first request path 52 | var path = root + "::root"; 53 | // The first targetItem is the first item of the requested path 54 | var targetItem = pathList[pathLength]; 55 | 56 | // Start the loop 57 | listItem(path, itemHandle, useCache); 58 | } 59 | // Handle every listed item 60 | function itemHandle(err, item, eventsEnabled = true) { 61 | if (err) { 62 | reject(err); 63 | return callback(err); 64 | } 65 | // itemList is the list of childs 66 | var itemList = item.items; 67 | // If we're actually handling the last item of the pasth list, this has to be the searched item (It also has to be folder because if we search for a file, the itemFound() is just fired before by handlimng its parent folder) 68 | if (pathLength >= pathList.length) { 69 | return itemFound(item); 70 | } 71 | else if (eventsEnabled) { 72 | // If the current folder is not the searched one, fire an 'progress' event 73 | self.emit("progress", { 74 | parentAction: "getItems", 75 | action: "listItem", 76 | searchedItem: itemPath, 77 | currItem: "/" + pathList.slice(0, pathLength).join("/"), 78 | progress: (pathLength + 1) / (pathList.length + 1) 79 | }); 80 | } 81 | // Loop trough the childs of the current item (always a folder) 82 | for (var i = 0; i < itemList.length; i++) { 83 | // Set the name of the current children with extension 84 | var currName = itemList[i].name + (itemList[i].extension ? ("." + itemList[i].extension) : ""); 85 | // Wether the current name is the same as the targetName (target name is tbe current searched item name) 86 | if (currName == targetItem) { 87 | // Check wether the currnet item is a folder, then we have to request it's informations again to get it's children 88 | if (isFolder(itemList[i])) { 89 | // Set new path to current item's complete drivewsid (Valid iCloud-Drive API path) 90 | path = itemList[i].drivewsid; 91 | // Add 1 to pathLength to prepare for the next pass 92 | pathLength++; 93 | // Set new targetItem to the new item with the index of the new pathLength 94 | targetItem = pathList[pathLength]; 95 | // List the current folder again 96 | listItem(path, itemHandle, useCache); 97 | } 98 | else if (itemList[i].type === "FILE") { 99 | // Wether the current item is a file, it has to be the searched one 100 | loadFile(itemList[i]); 101 | } 102 | return; 103 | } 104 | } 105 | // No item in current folder of loop founded that equals to the required one by path. 106 | // Searched item seems to be non-existing 107 | var errObj = { 108 | success: false, 109 | error: "No such file or directory", 110 | searchedItem: itemPath, 111 | errorCode: 2 112 | }; 113 | reject(errObj); 114 | callback(errObj); 115 | 116 | } 117 | function itemFound(result, stream = null) { 118 | result = deleteSpamInfo(result); 119 | if ("items" in result) { 120 | result.items = result.items.map(function(item) { 121 | item = deleteSpamInfo(item); 122 | return item; 123 | }); 124 | } 125 | function deleteSpamInfo(obj) { 126 | delete obj.zone; 127 | delete obj.parentId; 128 | //delete obj.etag; 129 | //delete obj.drivewsid; 130 | delete obj.numberOfItems; 131 | return obj; 132 | } 133 | resolve(result); 134 | callback(null, result, stream); 135 | } 136 | 137 | 138 | function listItem(drivewsid, callback, useCache = true) { 139 | 140 | if (drivewsid in self.Drive.folderCache && useCache) { 141 | callback(null, self.Drive.folderCache[drivewsid], false); 142 | return; 143 | } 144 | 145 | //console.log(self.auth.cookies); 146 | 147 | var content = JSON.stringify([ 148 | { 149 | "drivewsid": drivewsid, 150 | "partialData": false 151 | } 152 | ]); 153 | var host = getHostFromWebservice(self.account.webservices.drivews); 154 | request.post("https://" + host + "/retrieveItemDetailsInFolders?clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientId=" + self.clientId + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&dsid=" + self.account.dsInfo.dsid, { 155 | headers: fillDefaults({ 156 | 'Host': host, 157 | 'Cookie': cookiesToStr(self.auth.cookies), 158 | 'Content-Length': content.length 159 | }, self.clientSettings.defaultHeaders), 160 | body: content 161 | }, function(err, response, body) { 162 | if (err) return callback(err); 163 | var result = JSON.parse(body); 164 | 165 | console.log(result); 166 | 167 | if (0 in result) { 168 | self.Drive.folderCache[result[0].drivewsid] = result[0]; 169 | callback(null, result[0]); 170 | } 171 | else { 172 | callback(result); 173 | } 174 | }); 175 | } 176 | function loadFile(file) { 177 | var host = getHostFromWebservice(self.account.webservices.docws); 178 | var url = "https://" + host + "/ws/com.apple.CloudDocs/download/by_id?document_id=" + file.docwsid + "&token=" + self.auth.token + "&clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&clientId=" + self.clientId + "&dsid=" + self.account.dsInfo.dsid; 179 | request.get(url, { 180 | headers: fillDefaults({ 181 | 'Host': host, 182 | 'Cookie': cookiesToStr(self.auth.cookies) 183 | }, self.clientSettings.defaultHeaders) 184 | }, function(err, response, body) { 185 | if (err) return callback(err); 186 | file.contents = JSON.parse(body); 187 | 188 | itemFound(file); 189 | }); 190 | } 191 | }); 192 | 193 | 194 | 195 | 196 | return filePromise; 197 | }, 198 | createFolders(destination, folders, callback = function() {}, useCache = true) { 199 | var self = this; 200 | // If folders arg is a string, put as a single item into an array 201 | if (typeof folders === "string") { 202 | folders = [folders]; 203 | } 204 | 205 | var createPromise = new Promise(function(resolve, reject) { 206 | // Get destination information 207 | self.Drive.getItem(destination, function(err, item) { 208 | if (err) { 209 | reject(err); 210 | return callback(err); 211 | } 212 | var host = getHostFromWebservice(self.account.webservices.drivews); 213 | if (isFolder(item)) { 214 | var content = JSON.stringify({ 215 | "destinationDrivewsId": item.drivewsid, 216 | "folders": folders.map(function(folderName) { 217 | return { 218 | "clientId": self.clientId, 219 | "name": folderName 220 | } 221 | }) 222 | }); 223 | 224 | request.post("https://" + host + "/createFolders?clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&clientId=" + self.clientId + "&dsid=" + self.account.dsInfo.dsid, { 225 | headers: fillDefaults({ 226 | 'Host': host, 227 | 'Cookie': cookiesToStr(self.auth.cookies), 228 | 'Content-Length': content.length 229 | }, self.clientSettings.defaultHeaders), 230 | body: content 231 | }, function(err, response, body) { 232 | if (err) { 233 | reject(err); 234 | return callback(err); 235 | } 236 | var result = JSON.parse(body); 237 | resolve(result); 238 | callback(null, result); 239 | }); 240 | } 241 | }, useCache); 242 | }); 243 | 244 | return createPromise; 245 | }, 246 | deleteItems(items, callback = function() {}, useCache = true) { 247 | var self = this; 248 | 249 | // If folders arg is a string, put as a single item into an array 250 | if (typeof items === "string") { 251 | items = [items]; 252 | } 253 | var host = getHostFromWebservice(self.account.webservices.drivews); 254 | var content = { 255 | "items": [] 256 | }; 257 | 258 | var deletePromise = new Promise(function(resolve, reject) { 259 | items.forEach(function(currItem, index) { 260 | self.Drive.getItem(currItem, function(err, itemInfo) { 261 | if (err) { 262 | reject(err); 263 | return callback(err); 264 | } 265 | content.items.push({ 266 | "drivewsid": itemInfo.drivewsid, 267 | "etag": itemInfo.etag 268 | }); 269 | // If last item of item's list is reached 270 | if (index >= items.length - 1) { 271 | // Last item 272 | content = JSON.stringify(content); 273 | request.post("https://" + host + "/deleteItems?clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&clientId=" + self.clientId + "&dsid=" + self.account.dsInfo.dsid, { 274 | headers: fillDefaults({ 275 | 'Host': host, 276 | 'Cookie': cookiesToStr(self.auth.cookies), 277 | 'Content-Length': content.length 278 | }, self.clientSettings.defaultHeaders), 279 | body: content 280 | }, function(err, response, body) { 281 | if (err) { 282 | reject(err); 283 | return callback(err); 284 | } 285 | var result = JSON.parse(body); 286 | resolve(result); 287 | callback(err, result); 288 | }); 289 | } 290 | }, useCache); 291 | }); 292 | }); 293 | 294 | 295 | return deletePromise; 296 | }, 297 | renameItems(items, callback = function() {}, useCache = true) { 298 | var self = this; 299 | 300 | var host = getHostFromWebservice(self.account.webservices.drivews); 301 | 302 | var content = { 303 | "items": [] 304 | }; 305 | var renamePromise = new Promise(function(resolve, reject) { 306 | Object.keys(items).forEach(function(itemPath, index) { 307 | var name = items[itemPath].split("."); 308 | self.Drive.getItem(itemPath, function(err, itemInfo) { 309 | if (err) { 310 | reject(err); 311 | return callback(err); 312 | } 313 | content.items.push({ 314 | "drivewsid": itemInfo.drivewsid, 315 | "etag": itemInfo.etag, 316 | "name": name[0], 317 | "extension": name[1] 318 | }); 319 | if (index >= Object.keys(items).length - 1) { 320 | content = JSON.stringify(content); 321 | request.post("https://" + host + "/renameItems?clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&clientId=" + self.clientId + "&dsid=" + self.account.dsInfo.dsid, { 322 | headers: fillDefaults({ 323 | 'Host': host, 324 | 'Cookie': cookiesToStr(self.auth.cookies), 325 | 'Content-Length': content.length 326 | }, self.clientSettings.defaultHeaders), 327 | body: content 328 | }, function(err, response, body) { 329 | if (err) { 330 | reject(err); 331 | return console.error(err); 332 | } 333 | var result = JSON.parse(body); 334 | resolve(result); 335 | callback(null, result); 336 | }); 337 | } 338 | }, useCache); 339 | }); 340 | }); 341 | 342 | return renamePromise; 343 | }, 344 | upload(file, callback) { 345 | var self = this; 346 | 347 | var host = getHostFromWebservice(self.account.webservices.docws); 348 | var content = JSON.stringify({ 349 | "filename": "file.txt", 350 | "type": "FILE", 351 | "content_type": "text/plain", 352 | "size": 15 353 | }); 354 | 355 | request.post("https://" + host + "/ws/com.apple.CloudDocs/upload/web?token=NONE&clientBuildNumber=" + self.clientSettings.clientBuildNumber + "&clientMasteringNumber=" + self.clientSettings.clientMasteringNumber + "&clientId=1356E574-A2A4-415F-A028-1BC2FB56FFB3&dsid=11298614181", { 356 | headers: fillDefaults({ 357 | 'Host': host, 358 | 'Cookie': cookiesToStr(self.auth.cookies), 359 | 'Content-Length': content.length 360 | }, self.clientSettings.defaultHeaders), 361 | body: content 362 | }, function(err, response, body) { 363 | if (err) return callback(err); 364 | var result = JSON.parse(body); 365 | console.log(result); 366 | var content = '------WebKitFormBoundaryeKzg6g4kckug2g31\r\nContent-Disposition: form-data; name="files"; filename="file.txt"\r\nContent-Type: text/plain\r\n\r\n\r\n------WebKitFormBoundaryeKzg6g4kckug2g31--\r\n'; 367 | var req = request.post(result[0].url, { 368 | headers: fillDefaults({ 369 | 'Host': host, 370 | 'Cookie': cookiesToStr(self.auth.cookies), 371 | 'Content-Length': content.length 372 | }, self.clientSettings.defaultHeaders), 373 | body: content 374 | }, function(err, response, body) { 375 | if (err) return callback(err); 376 | var result = JSON.parse(body); 377 | console.log(result); 378 | }); 379 | var form = req.form(); 380 | form.append('file', fs.readFileSync(file), { 381 | filename: 'file.txt', 382 | contentType: 'text/plain' 383 | }); 384 | //form.append('files', fs.createReadStream(file)); 385 | //console.log(fs.readFileSync(file)); 386 | }); 387 | 388 | //var form = req.form(); 389 | //form.append('file', fs.createReadStream(file)); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /resources/apps/Calendar.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, newId, indexOfKey, paramString, paramStr, timeArray, arrayTime, fillDefaults} = require("./../helper"); 3 | 4 | 5 | 6 | module.exports = { 7 | getEvents(startDate, endDate, callback = function() {}) { 8 | var self = this; 9 | 10 | 11 | 12 | var host = getHostFromWebservice(self.account.webservices.calendar); 13 | var events = []; 14 | var eventsPromise = new Promise(function(resolve, reject) { 15 | request.get("https://" + host + "/ca/events?" + paramStr({ 16 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 17 | "clientId": self.clientId, 18 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 19 | "clientVersion": 5.1, 20 | "dsid": self.account.dsInfo.dsid, 21 | "lang": self.clientSettings.language, 22 | "requestID": 4, 23 | "startDate": startDate, 24 | "endDate": endDate, 25 | "usertz": self.clientSettings.timezone 26 | }), { 27 | headers: fillDefaults({ 28 | 'Host': host, 29 | 'Cookie': cookiesToStr(self.auth.cookies) 30 | }, self.clientSettings.defaultHeaders) 31 | }, function(err, response, body) { 32 | if (err) { 33 | reject(err); 34 | return callback(err); 35 | } 36 | var result = JSON.parse(body); 37 | if (result.status === 421) { 38 | reject(result.status); 39 | return callback(result.status); 40 | } 41 | 42 | var requestEventsCount = result.Event.length; 43 | var count = 0; 44 | result.Event.forEach(function(event, index) { 45 | request.get("https://" + host + "/ca/eventdetail/" + event.pGuid + "/" + event.guid + "?" + paramStr({ 46 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 47 | "clientId": self.clientId, 48 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 49 | "clientVersion": 5.1, 50 | "dsid": self.account.dsInfo.dsid, 51 | "lang": self.clientSettings.language, 52 | "requestID": 4, 53 | "startDate": startDate, 54 | "endDate": endDate, 55 | "usertz": self.clientSettings.timezone 56 | }), { 57 | headers: fillDefaults({ 58 | 'Host': host, 59 | 'Cookie': cookiesToStr(self.auth.cookies) 60 | }, self.clientSettings.defaultHeaders) 61 | }, function(err, response, body) { 62 | if (err) { 63 | reject(err); 64 | return callback(err); 65 | } 66 | var result = JSON.parse(body); 67 | events = events.concat(result.Event.map(function(event) { 68 | event.startDate = arrayTime(event.startDate); 69 | event.endDate = arrayTime(event.endDate); 70 | event.createdDate = arrayTime(event.createdDate); 71 | event.lastModifiedDate = arrayTime(event.lastModifiedDate); 72 | event.localStartDate = arrayTime(event.localStartDate); 73 | event.localEndDate = arrayTime(event.localEndDate); 74 | 75 | event.alarms = event.alarms.map(function(alarmGuidId) { 76 | return result.Alarm[result.Alarm.indexOfKey(alarmGuidId, "guid")]; 77 | }); 78 | if ("Recurrence" in result) { 79 | event.recurrence = result.Recurrence[result.Recurrence.indexOfKey(event.recurrence, "guid")]; 80 | if (event.recurrence && "until" in event.recurrence) event.recurrence.until = arrayTime(event.recurrence.until); 81 | if (event.recurrence && "recurrenceMasterStartDate" in event.recurrence) event.recurrence.recurrenceMasterStartDate = arrayTime(event.recurrence.recurrenceMasterStartDate); 82 | } 83 | return event; 84 | })); 85 | count++; 86 | var progress = count / (requestEventsCount + 1); 87 | self.emit("progress", { 88 | action: "event-detail", 89 | parentAction: "getEvents", 90 | progress: progress 91 | }); 92 | 93 | if (count >= requestEventsCount) { 94 | resolve(events); 95 | callback(null, events); 96 | } 97 | }); 98 | }); 99 | self.emit("progress", { 100 | action: "list-events", 101 | parentAction: "getEvents", 102 | progress: 1 / (requestEventsCount + 1) 103 | }); 104 | if (result.Event.length <= 0) { 105 | resolve(events); 106 | callback(null, events); 107 | } 108 | }); 109 | }); 110 | 111 | return eventsPromise; 112 | }, 113 | getCollections(callback = function() {}) { 114 | var self = this; 115 | 116 | var startDate = new Date(); 117 | startDate = startDate.getFullYear() + "-" + (startDate.getMonth() + 1) + "-" + startDate.getDate(); 118 | var endDate = startDate; 119 | 120 | var host = getHostFromWebservice(self.account.webservices.calendar); 121 | var collectionsPromise = new Promise(function(resolve, reject) { 122 | request.get("https://" + host + "/ca/startup?" + paramStr({ 123 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 124 | "clientId": self.clientId, 125 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 126 | "clientVersion": 5.1, 127 | "dsid": self.account.dsInfo.dsid, 128 | "lang": self.clientSettings.language, 129 | "requestID": 4, 130 | "startDate": startDate, 131 | "endDate": endDate, 132 | "usertz": self.clientSettings.timezone 133 | }), { 134 | headers: fillDefaults({ 135 | 'Host': host, 136 | 'Cookie': cookiesToStr(self.auth.cookies) 137 | }, self.clientSettings.defaultHeaders) 138 | }, function(err, response, body) { 139 | if (err) { 140 | reject(err); 141 | return callback(err); 142 | } 143 | var result = JSON.parse(body); 144 | if (result.status === 400) { 145 | reject(result); 146 | return callback(result); 147 | } 148 | self.Calendar.collections = result.Collection; 149 | resolve(result.Collection); 150 | callback(null, result.Collection); 151 | }); 152 | }); 153 | 154 | return collectionsPromise; 155 | //https://p44-calendarws.icloud.com/ca/startup?clientBuildNumber=17DProject78&clientId=808A0397-2A8E-4BA0-8AB4-76DD91E95B60&clientMasteringNumber=17D68&clientVersion=5.1&dsid=11298614181&endDate=2017-07-24&lang=de-de&requestID=3&startDate=2017-07-17&usertz=US%2FPacific 156 | }, 157 | createEvent(event, callback = function() {}) { 158 | var self = this; 159 | 160 | var eventGuid = newId(); 161 | 162 | event.alarms = event.alarms.map(function(alarm) { 163 | return { 164 | messageType: 'message', 165 | measurement: alarm, 166 | description: 'Event reminder', 167 | pGuid: eventGuid, 168 | guid: eventGuid + ":" + newId(), 169 | isLocationBased: false 170 | } 171 | }); 172 | //console.log(event.alarms); 173 | event.recurrence = fillDefaults(event.recurrence, { 174 | pGuid: eventGuid, 175 | guid: eventGuid + '*MME-RID', 176 | recurrenceMasterStartDate: null, // [ 20170726, 2017, 7, 26, 19, 0, 1140 ] 177 | weekStart: null, 178 | freq: 'daily', 179 | count: null, 180 | interval: 1, 181 | byDay: null, 182 | byMonth: null, 183 | until: null, 184 | frequencyDays: null, 185 | weekDays: null 186 | }); 187 | 188 | event = fillDefaults(event, { 189 | guid: eventGuid, 190 | pGuid: 'home', 191 | etag: null, 192 | extendedDetailsAreIncluded: true, 193 | lastModifiedDate: null, 194 | createdDate: new Date(), 195 | updatedByName: null, 196 | updatedByNameFirst: null, 197 | updatedByNameLast: null, 198 | updatedByDate: null, 199 | createdByName: null, 200 | createdByNameFirst: null, 201 | createdByNameLast: null, 202 | createdByDate: null, 203 | eventStatus: null, 204 | birthdayFirstName: null, 205 | birthdayLastName: null, 206 | birthdayNickname: null, 207 | birthdayCompanyName: null, 208 | birthdayShowAsCompany: null, 209 | birthdayIsYearlessBday: null, 210 | birthdayBirthDate: null, 211 | title: 'New Event', 212 | location: null, 213 | url: null, 214 | description: null, 215 | startDate: new Date(), 216 | endDate: new Date(), 217 | allDay: false, 218 | tz: self.clientSettings.timezone, 219 | duration: 60, 220 | localStartDate: event.startDate, 221 | localEndDate: event.endDate, 222 | icon: 0, 223 | hasAttachments: false, 224 | shouldShowJunkUIWhenAppropriate: true, 225 | alarms: [], 226 | recurrence: null, 227 | recurrenceMaster: true, 228 | recurrenceException: true, 229 | readOnly: false, 230 | invitees: null, 231 | organizer: null 232 | }); 233 | 234 | return self.Calendar.__event(event, false, callback, false); 235 | }, 236 | createCollection(collection, callback = function() {}) { 237 | var self = this; 238 | 239 | //https://p44-calendarws.icloud.com/ca/collections/E9C67535-2CAF-46B9-852D-6D2E348D49AC?clientBuildNumber=17EProject67&clientId=0381983D-7B5E-4F37-8DC2-452B932AD099&clientMasteringNumber=17E57&clientVersion=5.1&dsid=11298614181&endDate=2017-09-02&lang=de-de&requestID=22&startDate=2017-06-26&usertz=US%2FPacific 240 | 241 | var guid = newId(); 242 | 243 | var content = JSON.stringify({ 244 | "Collection": { 245 | "supportedType": "Event", 246 | "extendedDetailsAreIncluded": true, 247 | "order": 3, 248 | "symbolicColor": collection.color || "#ff2d55", 249 | "color": collection.color || "#ff2d55", 250 | "guid": guid, 251 | "title": collection.title, 252 | "participants": null, 253 | "meAsParticipant": null, 254 | "deferLoading": null, 255 | "shareType": null, 256 | "shareTitle": "", 257 | "etag": null, 258 | "ctag": null, 259 | "objectType": "personal", 260 | "readOnly": null, 261 | "lastModifiedDate": null, 262 | "description": null, 263 | "sharedUrl": "", 264 | "ignoreAlarms": null, 265 | "enabled": true, 266 | "ignoreEventUpdates": null, 267 | "emailNotification": null, 268 | "removeTodos": null, 269 | "removeAlarms": null, 270 | "isDefault": null, 271 | "prePublishedUrl": null, 272 | "publishedUrl": null, 273 | "isFamily": null 274 | }, 275 | "ClientState": { 276 | "Collection": [], 277 | "fullState": false, 278 | "userTime": 1234567890, 279 | "alarmRange": 60 280 | } 281 | }); 282 | 283 | var startDate = new Date(); 284 | startDate = startDate.getFullYear() + "-" + (startDate.getMonth() + 1) + "-" + startDate.getDate(); 285 | var endDate = startDate; 286 | 287 | var host = getHostFromWebservice(self.account.webservices.calendar); 288 | var createPromise = new Promise(function(resolve, reject) { 289 | request.post("https://" + host + "/ca/collections/" + guid + "?" + paramStr({ 290 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 291 | "clientId": self.clientId, 292 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 293 | "clientVersion": 5.1, 294 | "dsid": self.account.dsInfo.dsid, 295 | "lang": self.clientSettings.language, 296 | "requestID": 4, 297 | "startDate": startDate, 298 | "endDate": endDate, 299 | "usertz": self.clientSettings.timezone 300 | }), { 301 | headers: fillDefaults({ 302 | 'Host': host, 303 | 'Cookie': cookiesToStr(self.auth.cookies), 304 | 'Content-Length': content.length 305 | }, self.clientSettings.defaultHeaders), 306 | body: content 307 | }, function(err, response, body) { 308 | if (err) { 309 | reject(err); 310 | return callback(err); 311 | } 312 | var result = JSON.parse(body); 313 | resolve(result); 314 | callback(null, result); 315 | }); 316 | }); 317 | 318 | return createPromise; 319 | }, 320 | deleteEvent(event, all = false, callback = function() {}) { 321 | var self = this; 322 | // Just call the event at its enpoint with DELETE as method 323 | return self.Calendar.__event(event, all, callback, "DELETE"); 324 | 325 | }, 326 | changeEvent(event, all = false, callback = function() {}) { 327 | var self = this; 328 | event.lastModifiedDate = new Date(); 329 | return self.Calendar.__event(event, all, callback, "PUT"); 330 | }, 331 | __event(event, all, callback = function() {}, methodOverride) { 332 | // General accesing the endpoint of an event and run different methods ("PUT" = changing, "DELETE", = deleting, false = creating) 333 | 334 | var self = this; 335 | var host = getHostFromWebservice(self.account.webservices.calendar); 336 | var eventPromise = new Promise(function(resolve, reject) { 337 | 338 | if (!event) { 339 | reject({ 340 | error: "Event invalid" 341 | }); 342 | } 343 | 344 | var alarms = event.alarms; 345 | event.alarms = event.alarms.map(function(alarm) { 346 | return alarm.guid; 347 | }); 348 | 349 | var recurrence = event.recurrence; 350 | if (recurrence && "until" in recurrence) recurrence.until = timeArray(recurrence.until); 351 | event.recurrence = event.recurrence ? event.recurrence.guid : null; 352 | 353 | 354 | event.startDate = timeArray(event.startDate); 355 | event.endDate = timeArray(event.endDate); 356 | event.createdDate = timeArray(event.createdDate); 357 | event.lastModifiedDate = timeArray(event.lastModifiedDate); 358 | event.localStartDate = timeArray(event.localStartDate); 359 | event.localEndDate = timeArray(event.localEndDate); 360 | 361 | //return console.log(event); 362 | 363 | 364 | //console.log(event); 365 | 366 | var content = { 367 | "Event": methodOverride == "DELETE" ? {} : event, 368 | "Recurrence": recurrence ? [ 369 | recurrence 370 | ] : null, 371 | "Alarm": alarms, 372 | "ClientState": { 373 | "Collection": [ 374 | { 375 | "guid": event.pGuid 376 | } 377 | ], 378 | "fullState": false, 379 | "userTime": 1234567890, 380 | "alarmRange": 60 381 | } 382 | }; 383 | if (!content.Recurrence) delete content.Recurrence; 384 | 385 | 386 | content = JSON.stringify(content); 387 | 388 | var startDate = new Date(); 389 | startDate = startDate.getFullYear() + "-" + (startDate.getMonth() + 1) + "-" + startDate.getDate(); 390 | var endDate = startDate; 391 | 392 | var url = "https://" + host + "/ca/events/" + event.pGuid + "/" + event.guid + (event.recurrence ? (all? "/future" : "/this") : "") + "?" + paramStr((function() { 393 | var args = { 394 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 395 | "clientId": self.clientId, 396 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 397 | "clientVersion": 5.1, 398 | "dsid": self.account.dsInfo.dsid, 399 | "endDate": "2100-12-31", 400 | "ifMatch": event.etag ? encodeURIComponent(event.etag) : null, 401 | "lang": self.clientSettings.language, 402 | "methodOverride": methodOverride, 403 | "startDate": "1900-01-01", 404 | "usertz": self.clientSettings.timezone 405 | }; 406 | 407 | if (!args["methodOverride"]) { 408 | delete args["methodOverride"]; 409 | } 410 | if (!args["ifMatch"]) { 411 | delete args["ifMatch"]; 412 | } 413 | return args; 414 | })()); 415 | 416 | request.post(url, { 417 | headers: fillDefaults({ 418 | 'Host': host, 419 | 'Cookie': cookiesToStr(self.auth.cookies), 420 | 'Content-Length': content.length 421 | }, self.clientSettings.defaultHeaders), 422 | body: content 423 | }, function(err, response, body) { 424 | 425 | if (err) { 426 | reject(err); 427 | return callback(err); 428 | } 429 | try { 430 | var result = JSON.parse(body); 431 | resolve(result); 432 | callback(null, result); 433 | } 434 | catch (err) { 435 | reject({ 436 | err: err, 437 | body: body 438 | }); 439 | } 440 | }); 441 | }); 442 | 443 | return eventPromise; 444 | } 445 | } 446 | 447 | function getCTag(etag) { 448 | return "FT=-@RU=" + etag.substring(etag.search(/[^-]{8}-[^-]{4}-[^-]{4}-[^-]{4}-[^-]{12}/)) + "@S=" + etag.substring(etag.search(/[0-9]{1,}/), etag.search("@")) 449 | } 450 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const https = require('https'); 3 | const http = require('http'); 4 | const request = require('request'); 5 | const fs = require('fs'); 6 | 7 | var {getHostFromWebservice, cookiesToStr, parseCookieStr, fillCookies, newId, indexOfKey, paramStr, fillDefaults} = require("./resources/helper"); 8 | 9 | module.exports = { 10 | 11 | getClientId: newId, 12 | 13 | getAuthToken(account, password, self, callback) { 14 | // Define login client info object 15 | var xAppleIFDClientInfo = { 16 | "U": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.1 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.1", 17 | "L": self.clientSettings.locale, 18 | "Z": "GMT+02:00", 19 | "V": "1.1", 20 | "F": "" 21 | }; 22 | // Define data object with login info 23 | var loginData = { 24 | "accountName": account, 25 | "password": password, 26 | "rememberMe": true, 27 | "trustTokens": [] 28 | }; 29 | request.post("https://idmsa.apple.com/appleauth/auth/signin", { 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | 'Referer': 'https://idmsa.apple.com/appleauth/auth/signin', 33 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 34 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.1 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.1', 35 | 'Origin': 'https://idmsa.apple.com', 36 | 'X-Apple-Widget-Key': self.clientSettings.xAppleWidgetKey, 37 | 'X-Requested-With': 'XMLHttpRequest', 38 | 'X-Apple-I-FD-Client-Info': JSON.stringify(xAppleIFDClientInfo) 39 | }, 40 | body: JSON.stringify(loginData) 41 | }, function(err, response, body) { 42 | // If there are any request errors 43 | if (err) return callback(err); 44 | var result; 45 | try { 46 | result = JSON.parse(body); 47 | } catch (err) { 48 | return callback({ 49 | error: "Failed to parse token.", 50 | code: 0, 51 | requestBody: body 52 | }) 53 | } 54 | 55 | // If the session token exists 56 | var sessionToken; 57 | if ("x-apple-session-token" in response.headers) { 58 | sessionToken = response.headers["x-apple-session-token"]; 59 | } 60 | var sessionID; 61 | if ("x-apple-id-session-id" in response.headers) { 62 | sessionID = response.headers["x-apple-id-session-id"]; 63 | } 64 | var scnt; 65 | if ("scnt" in response.headers) { 66 | scnt = response.headers["scnt"]; 67 | } 68 | callback(sessionToken ? null : ({ 69 | error: "No session token", 70 | code: 0 71 | }), { 72 | token: sessionToken, 73 | sessionID: sessionID, 74 | scnt: scnt, 75 | response: result 76 | }); 77 | }); 78 | }, 79 | accountLogin(self, callback = function() {}, trustToken = null) { 80 | var authData = { 81 | "dsWebAuthToken": self.auth.token, 82 | "extended_login": true, 83 | "trustToken": trustToken 84 | }; 85 | 86 | return new Promise(function(resolve, reject) { 87 | request.post("https://setup.icloud.com/setup/ws/1/accountLogin?" + paramStr({ 88 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 89 | "clientId": self.clientId, 90 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber 91 | }), { 92 | headers: { 93 | 'Content-Type': 'text/plain', 94 | 'Referer': 'https://www.icloud.com/', 95 | 'Accept': '*/*', 96 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.1 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.1', 97 | 'Origin': 'https://www.icloud.com' 98 | }, 99 | body: JSON.stringify(authData) 100 | }, function(err, response, body) { 101 | // If there are any request errors 102 | if (err) { 103 | reject(err); 104 | return callback(err); 105 | } 106 | var result = JSON.parse(body); 107 | // If the login itself was successfully 108 | if ("success" in result && !result.success) { 109 | return callback(result.error); 110 | } 111 | // Everything is okay. Logged in 112 | callback(null, result, response); 113 | resolve({ result, response }); 114 | }); 115 | }); 116 | }, 117 | getApps() { 118 | return JSON.parse(fs.readFileSync(__dirname + "/resources/apps.json", "utf8")); 119 | }, 120 | getPushTopics(apps) { 121 | //return ["73f7bfc9253abaaa423eba9a48e9f187994b7bd9", "5a5fc3a1fea1dfe3770aab71bc46d0aa8a4dad41"]; 122 | return Object.keys(apps).map(appName => apps[appName].pushTopic).filter(topic => topic); 123 | }, 124 | getPushToken(self, cookiesStr, callback) { 125 | var tokenData = { 126 | "pushTopics": self.push.topics, 127 | "pushTokenTTL": self.push.ttl 128 | }; 129 | 130 | var content = JSON.stringify(tokenData); 131 | 132 | var host = getHostFromWebservice(self.account.webservices.push); 133 | 134 | request.post("https://" + host + "/getToken?" + paramStr({ 135 | "attempt": 1, 136 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 137 | "clientId": self.clientId, 138 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 139 | "dsid": self.account.dsInfo.dsid 140 | }), { 141 | headers: fillDefaults({ 142 | 'Host': host, 143 | 'Cookie': cookiesToStr(self.auth.cookies), 144 | 'Content-Length': content.length 145 | }, self.clientSettings.defaultHeaders), 146 | body: content, 147 | //gzip: true 148 | }, function(err, response, body) { 149 | if (err) return callback(err); 150 | var result = JSON.parse(body); 151 | //console.log(result); 152 | callback(null, result.pushToken, result.webCourierURL, response.headers["set-cookie"]); 153 | }); 154 | }, 155 | getState(self, callback) { 156 | var content = JSON.stringify({ 157 | "pushTopics": self.push.topics 158 | }); 159 | 160 | var host = getHostFromWebservice(self.account.webservices.push); 161 | 162 | request.post("https://" + host + "/getState?" + paramStr({ 163 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 164 | "clientId": self.clientId, 165 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 166 | "dsid": self.account.dsInfo.dsid, 167 | "pcsEnabled": true 168 | }), { 169 | headers: fillDefaults({ 170 | 'Host': host, 171 | 'Cookie': cookiesToStr(self.auth.cookies), 172 | 'Content-Length': content.length 173 | }, self.clientSettings.defaultHeaders), 174 | body: content 175 | }, function(err, response, body) { 176 | if (err) return callback(err); 177 | var result = JSON.parse(body); 178 | callback(null, result.states); 179 | }); 180 | }, 181 | initPush(self, callback) { 182 | var uri = "https://webcourier.push.apple.com/aps" + "?" + paramStr({ 183 | "tok": self.push.token, 184 | "ttl": self.push.ttl 185 | }); 186 | //uri = "https://webcourier.push.apple.com/aps?tok=a819303f3199aa62b6be55a9aa635e29b69defc4a261c01ecf4ab4c9c3fcc9b6&ttl=43200"; 187 | //console.log("\n\n", uri, "\n\n"); 188 | var req = request.get(uri, { 189 | headers: fillDefaults({}, self.clientSettings.defaultHeaders), 190 | rejectUnauthorized: false 191 | }, function(err, response, body) { 192 | if (err) { 193 | return callback(err); 194 | } 195 | try { 196 | var result = JSON.parse(body); 197 | } 198 | catch (e) { 199 | callback({ 200 | error: "Failed to parse request body as JSON", 201 | requestBody: body, 202 | errorCode: 21 203 | }); 204 | } 205 | callback(null, result); 206 | }); 207 | }, 208 | registerTopics(self, callback = function() {}) { 209 | 210 | const host = getHostFromWebservice(self.account.webservices.push); 211 | 212 | const content = JSON.stringify({ 213 | "pushToken": self.push.token, 214 | "pushTopics": self.push.topics, 215 | "pushTokenTTL": self.push.ttl 216 | }); 217 | 218 | const url = "https://" + host + "/registerTopics?" + paramStr({ 219 | "attempt": 1, 220 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 221 | "clientId": self.clientId, 222 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 223 | "dsid": self.account.dsInfo.dsid 224 | }); 225 | 226 | request.post(url, { 227 | headers: fillDefaults({ 228 | 'Host': host, 229 | 'Cookie': cookiesToStr(self.auth.cookies) 230 | }, self.clientSettings.defaultHeaders), 231 | body: content 232 | }, function(err, response, body) { 233 | 234 | if (err) return callback(err); 235 | 236 | var result = JSON.parse(body); 237 | 238 | callback(null, result); 239 | }); 240 | }, 241 | registerPush(self, service, callback) { 242 | var content = JSON.stringify({ 243 | "apnsToken": self.push.token, 244 | "clientID": self.clientId, 245 | "apnsEnvironment": "production" 246 | }); 247 | 248 | var host = getHostFromWebservice(self.account.webservices.ckdeviceservice); 249 | 250 | var url = "https://" + host + "/device/1/" + service + "/production/tokens/register?" + paramStr({ 251 | "clientBuildNumber": self.clientSettings.clientBuildNumber, 252 | "clientId": self.clientId, 253 | "clientMasteringNumber": self.clientSettings.clientMasteringNumber, 254 | "dsid": self.account.dsInfo.dsid 255 | }); 256 | 257 | request.post(url, { 258 | headers: fillDefaults({ 259 | 'Host': host, 260 | 'Cookie': cookiesToStr(self.auth.cookies) 261 | }, self.clientSettings.defaultHeaders), 262 | body: content 263 | }, function(err, response, body) { 264 | 265 | if (err) return callback(err); 266 | 267 | var result = JSON.parse(body); 268 | callback(null, result); 269 | }); 270 | }, 271 | __auth(self) { 272 | const host = "idmsa.apple.com"; 273 | const signInReferer = "https://" + host + "/appleauth/auth/signin?widgetKey=" + self.clientSettings.xAppleWidgetKey + "&locale=" + self.clientSettings.locale + "&font=sf"; 274 | return new Promise(function(resolve, reject) { 275 | const url = "https://" + host + "/appleauth/auth"; 276 | 277 | request.get(url, { 278 | headers: fillDefaults({ 279 | 'Referer': signInReferer, 280 | 'Host': host, 281 | 'Cookie': cookiesToStr(self.auth.cookies), 282 | 'X-Apple-Widget-Key': self.clientSettings.xAppleWidgetKey, 283 | 'X-Apple-I-FD-Client-Info': JSON.stringify(self.clientSettings.xAppleIFDClientInfo), 284 | 'X-Apple-ID-Session-Id': self.clientSettings.xAppleIDSessionId, 285 | 'scnt': self.clientSettings.scnt 286 | }, self.clientSettings.defaultHeaders), 287 | //body: content 288 | }, (err, response, body) => { 289 | if (err) reject(err); 290 | resolve({ response, body }); 291 | }); 292 | }); 293 | }, 294 | __securityCode(self, bodyObj = null, method = "POST") { 295 | const host = "idmsa.apple.com"; 296 | const signInReferer = "https://" + host + "/appleauth/auth/signin?widgetKey=" + self.clientSettings.xAppleWidgetKey + "&locale=" + self.clientSettings.locale + "&font=sf"; 297 | return new Promise(function(resolve, reject) { 298 | request({ 299 | url: "https://" + host + "/appleauth/auth/verify/trusteddevice/securitycode", 300 | method: method, 301 | headers: fillDefaults({ 302 | 'Content-Type': 'application/json', 303 | 'Referer': signInReferer, 304 | 'Host': host, 305 | 'Cookie': cookiesToStr(self.auth.cookies), 306 | 'X-Apple-Widget-Key': self.clientSettings.xAppleWidgetKey, 307 | 'X-Apple-I-FD-Client-Info': JSON.stringify(self.clientSettings.xAppleIFDClientInfo), 308 | 'X-Apple-ID-Session-Id': self.clientSettings.xAppleIDSessionId, 309 | 'scnt': self.clientSettings.scnt 310 | }, self.clientSettings.defaultHeaders), 311 | body: JSON.stringify(bodyObj) 312 | }, function(err, response, body) { 313 | if (err) return reject(err); 314 | 315 | resolve({ response, body }); 316 | }); 317 | }); 318 | }, 319 | __securityPhone(self, mode = "sms") { 320 | const host = "idmsa.apple.com"; 321 | const signInReferer = "https://" + host + "/appleauth/auth/signin?widgetKey=" + self.clientSettings.xAppleWidgetKey + "&locale=" + self.clientSettings.locale + "&font=sf"; 322 | return new Promise(function(resolve, reject) { 323 | request({ 324 | url: "https://" + host + "/appleauth/auth/verify/phone", 325 | method: "PUT", 326 | headers: fillDefaults({ 327 | 'Content-Type': 'application/json', 328 | 'Referer': signInReferer, 329 | 'Host': host, 330 | 'Cookie': cookiesToStr(self.auth.cookies), 331 | 'X-Apple-Widget-Key': self.clientSettings.xAppleWidgetKey, 332 | 'X-Apple-I-FD-Client-Info': JSON.stringify(self.clientSettings.xAppleIFDClientInfo), 333 | 'X-Apple-ID-Session-Id': self.clientSettings.xAppleIDSessionId, 334 | 'scnt': self.clientSettings.scnt 335 | }, self.clientSettings.defaultHeaders), 336 | body: JSON.stringify({ 337 | phoneNumber: { 338 | id: 1 339 | }, 340 | mode: mode 341 | }) 342 | }, function(err, response, body) { 343 | if (err) return reject(err); 344 | 345 | resolve({ response, body }); 346 | }); 347 | }); 348 | }, 349 | trust(self) { 350 | const host = "idmsa.apple.com"; 351 | const signInReferer = "https://" + host + "/appleauth/auth/signin?widgetKey=" + self.clientSettings.xAppleWidgetKey + "&locale=" + self.clientSettings.locale + "&font=sf"; 352 | return new Promise(function(resolve, reject) { 353 | request.post("https://" + host + "/appleauth/auth/2sv/trust", { 354 | headers: fillDefaults({ 355 | 'Content-Type': 'application/json', 356 | 'Referer': signInReferer, 357 | 'Host': host, 358 | 'Cookie': cookiesToStr(self.auth.cookies), 359 | 'X-Apple-Widget-Key': self.clientSettings.xAppleWidgetKey, 360 | 'X-Apple-I-FD-Client-Info': JSON.stringify(self.clientSettings.xAppleIFDClientInfo), 361 | 'X-Apple-ID-Session-Id': self.clientSettings.xAppleIDSessionId, 362 | 'scnt': self.clientSettings.scnt 363 | }, self.clientSettings.defaultHeaders) 364 | }, function(err, response, body) { 365 | if (err) return reject(err); 366 | 367 | resolve({ response, body }); 368 | }); 369 | }); 370 | }, 371 | 372 | enterSecurityCode(self, code, callback = function() {}) { 373 | 374 | (async () => { 375 | // General /auth request for current session 376 | //const auth = await this.__auth(self); 377 | 378 | // Firstly, do a normal /accountLogin 379 | //const accountLoginResult = await this.accountLogin(self); 380 | // Parse cookies to objects 381 | //var cookies = parseCookieStr(accountLoginResult.response.headers["set-cookie"]); 382 | //self.auth.cookies = fillCookies(self.auth.cookies, cookies); 383 | 384 | // Enter the security code for current session 385 | const securityCodeResult = await this.__securityCode(self, { 386 | securityCode: { 387 | code: code 388 | } 389 | }); 390 | 391 | // Trust the current device 392 | const trusting = await this.trust(self); 393 | 394 | // Use /trust's headers (X-Apple-Twosv-Trust-Token and new authentication token) 395 | if ("x-apple-session-token" in trusting.response.headers) { 396 | self.auth.token = trusting.response.headers["x-apple-session-token"]; 397 | } 398 | if ("x-apple-twosv-trust-token" in trusting.response.headers) { 399 | self.auth.xAppleTwosvTrustToken = trusting.response.headers["x-apple-twosv-trust-token"]; 400 | } 401 | 402 | // Do a complete new /accountLogin 403 | const lastAccountLoginResult = await this.accountLogin(self, function() {}, self.auth.xAppleTwosvTrustToken); 404 | 405 | // Cookies from this can be used 406 | var cookies = parseCookieStr(lastAccountLoginResult.response.headers["set-cookie"]); 407 | self.auth.cookies = fillCookies(self.auth.cookies, cookies); 408 | 409 | self.emit("sessionUpdate"); 410 | 411 | callback(true); 412 | 413 | })(); 414 | } 415 | } 416 | 417 | })(); 418 | --------------------------------------------------------------------------------