koalazak / dorita980

Unofficial iRobot Roomba and Braava (i7/i7+, 980, 960, 900, e5, 690, 675, m6, etc) node.js library (SDK) to control your robot
MIT License
959 stars 149 forks source link

Cloud API firmware 2.x.x #25

Closed koalazak closed 3 years ago

koalazak commented 7 years ago

Hi guys, I have almost everything ready to make the Cloud API possible:

But i dont found the correct topic and message content to send basic commands like start or stop. Anybody sniff that data? or has that data?

My sniff data is weired and malformed, i dont know if my sslsplit is showingme the info in the correct encoding. When I send a command with my phone over the cloud I see some bytes in the comunication but no one string like topic o json message.

can anybody help?

here is the working snippet:

const AWSIoTData = require('aws-iot-device-sdk');
const AWS = require('aws-sdk');
const request = require('request-promise');
// install with: npm install aws-iot-device-sdk aws-sdk request-promise request

const ROBOT_BLID = ''; // same as local api
const ROBOT_PASSWORD = ''; // same as local api
const APP_ID = ''; // like IOS-12345678-1234-1234-1234-123456789098

function cloudDiscovery () {
  var requestOptions = {
    'method': 'GET',
    'uri': `https://disc-prod.iot.irobotapi.com/v1/robot/discover/${ROBOT_BLID}`,
    'json': true
  };
  return request(requestOptions);
}

function cloudLogin () {
  return cloudDiscovery().then(function (discoveryData) {
    var postData = {
      'associations': {
        '0': {
          'robot_id': ROBOT_BLID,
          'deleted': false,
          'password': ROBOT_PASSWORD
        }
      },
      'app_id': APP_ID
    };

    var requestOptions = {
      'method': 'POST',
      'headers': {
        'Content-Type': 'application/json',
        'User-Agent': 'aspen/1.9.1.184.1 CFNetwork/808.2.16 Darwin/16.3.0'
      },
      'uri': `${discoveryData.httpBase}/v1/login`,
      'body': postData,
      'json': true
    };

    return request(requestOptions).then((rawLoginResponse) => {
      return {login: rawLoginResponse.associations['0'], credentials: rawLoginResponse.credentials, discovery: discoveryData};
    });
  });
}

function initMQTT (amazonData) {
  var awsConfiguration = {
    poolId: amazonData.credentials.CognitoId,
    region: amazonData.discovery.awsRegion
  };

  var AWSConfiguration = awsConfiguration;

  var clientId = ROBOT_BLID + '-' + (Math.floor((Math.random() * 100000) + 1));

  AWS.config.region = AWSConfiguration.region;

  AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: AWSConfiguration.poolId
  });

  const mqttClient = AWSIoTData.device({
    region: AWS.config.region,
    clientId: clientId,
    protocol: 'wss',
    maximumReconnectTimeMs: 8000,
    debug: true,
    accessKeyId: amazonData.credentials.AccessKeyId,
    secretKey: amazonData.credentials.SecretKey,
    sessionToken: amazonData.credentials.SessionToken
  });

  mqttClient.on('connect', function (e) {
    console.log('connect!', e);

    mqttClient.subscribe('$aws/things/' + ROBOT_BLID + '/shadow/#'); // all subtopics

    // const cmd = {'state': {'desired': {'cleanSchedule': {'cycle': ['none', 'none', 'none', 'none', 'none', 'none', 'none'], 'h': [17, 10, 10, 12, 10, 13, 17], 'm': [0, 30, 30, 0, 30, 30, 0]}}}};
    // mqttClient.publish('$aws/things/' + ROBOT_BLID + '/shadow/update', JSON.stringify(cmd));
  });

  mqttClient.on('reconnect', function () {
    console.log('reconnect!');
  });

  mqttClient.on('message', function (t, m) {
    console.log('message:');
    console.log('topic:', t);
    console.log(m.toString());
  });

  mqttClient.on('delta', function (m) {
    console.log('delta:');
    console.log(m);
  });
  mqttClient.on('status', function (m) {
    console.log('status:');
    console.log(m);
  });

  mqttClient.on('data', function (m) {
    console.log('data:');
    console.log(m);
  });

  mqttClient.on('error', function (e) {
    console.log('error:');
    console.log(e);
  });

  mqttClient.on('packetreceive', function (m) {
    console.log('packetreceive:');
    console.log(m);
  });
}

cloudLogin().then((credentialData) => {
  console.log(credentialData);
  initMQTT(credentialData);
}).catch(console.log);
koalazak commented 7 years ago

@akpotter @Thoro :)

barbagianni commented 7 years ago

Hey @koalazak!

I'd like to help. Can you provide some instructions?

How do I find the app id to run your snippet? I guess it's from the mobile app?

Cheers

koalazak commented 7 years ago

I think you can use any string with that format. I get my app ID sniffing the trafic.

barbagianni commented 7 years ago

OK, it seems to do something. Which is the relevant output? :)

barbagianni commented 7 years ago

So i did run a start stop cycle through the cloud and got readable output from your script.

Is this what you're looking for?

koalazak commented 7 years ago

the output is all the messages sent to the topic $aws/things/' + ROBOT_BLID + '/shadow (and subtopics) by the robot or by the official mobile app. When you say through the cloud and got readable output you mean using the official mobile app? or using this script (publishing to a topic with this script)?

What im looking for is what message and what topic we suppose to use in mqttClient.publish() method in this script to start/stop the robot via this script.

barbagianni commented 7 years ago

Running this script, then triggering the bot via the official mobile app.

When I start the script, the output ends with "undefined".

barbagianni commented 7 years ago

One sec, I'm sanitizing the output.

koalazak commented 7 years ago

ok, you are looking the messages published by the mobile app or robot to that topics. that is what i get too. and using the mqttClient.publish() method I can set new schedule for example. But What im looking for is what message and what topic we suppose to use in mqttClient.publish() method in this script to start/stop the robot via this script.

barbagianni commented 7 years ago

So do you have some instructions to obtain them?

I have a ton of JSON output. But only after triggering something via the official mobile app.

koalazak commented 7 years ago

you can start making a MITM attack to sniff the data between the official mobile app and the cloud, getting the mqtt packets and decode them. Google about MITM

barbagianni commented 7 years ago

No TLS on that?

No chance to reconstruct the command from something like this? message: topic: $aws/things/ROBOT_BLID_HERE/shadow/update/accepted {"state":{"reported":{"lastCommand":{"command":"start","time":1488399056,"initiator":"rmtApp"}}},"metadata":{"reported":{"lastCommand":{"command":{"timestamp":1488399056},"time":{"timestamp":1488399056},"initiator":{"timestamp":1488399056}}}},"version":1617,"timestamp":1488399057}

koalazak commented 7 years ago

yes, TLS on that. bypassing with sslsplit. I try some variants of that message but no way.

barbagianni commented 7 years ago

Is there a way to trigger the cloud api when you're on wifi?

I have a proxy running to monitor the traffic.

barbagianni commented 7 years ago

Never mind, i isolated my networks, now it tries to go through the cloud.

I see a message with the app id, i also get the cloud icon in the app. But now my roomba doesn't react when i press start.

barbagianni commented 7 years ago

Reset everything, now the roomba reacts, but I don't see the commands in the proxy. Only the requests for mission history, login, etc. Will debug a bit more...

koalazak commented 7 years ago

Its like the start/stop commands are in other format not json, like raw mqtt packets, a few bytes...

koalazak commented 7 years ago

Make sure you are creating certs with a valid CommonName. The mobile app is validating the CN field in the roomba cert with the format 'Roomba-{number16}' if it is not valid, then the mobile app disconnect. (the validation is in the mobile app, so you need a selfisgned cert with this CN if you want to perform a sslsplit to sniff the trafic

koalazak commented 7 years ago

but if you are seeing the mission commands and commands when you set preferences, there is no problem with the cert.

barbagianni commented 7 years ago

Hmm, I used mitmproxy instead of sslsplit. I'm a bit too tired to read through how to use sslsplit right now.

There should be a option to see raw tcp traffic in mitmproxy. I'm seeing the http traffic, but I'm guessing mqtt gets lost.

barbagianni commented 7 years ago

I think i have a working setup now redirecting to sslsplit. A test port 80 request got logged. I tried redirecting 8883 but get no traffic. Any tips to debug the problem?

barbagianni commented 7 years ago

OK, finally got a start message :)

Can I send it to you by email? It's quite long. Seems like it came over an upgraded ssl websocket carrying mqtt. Theres the upgrade, some garbage (probably mqtt), then start message, garbage, pause message.

koalazak commented 7 years ago

yes sure. I got the message too. But I cant figure out how to reproduce it in my test snippet. Can you?

barbagianni commented 7 years ago

I got to a version mismatch. It seems to be a sequence number. The question is where from.

koalazak commented 7 years ago

well, that is a step forward! There is not in state object?

koalazak commented 7 years ago

version is in every message received:

{
"state":{"reported":{"svcEndpoints":{"svcDeplId":"v007"}}},
"metadata":{"reported":{"svcEndpoints":{"svcDeplId":{"timestamp":1488553359}}}},
"version":3345,
"timestamp":1488553359
}

you can parse all the messages and store the version to use in the next call.

can you share your code? zaktu.x@gmail.com

barbagianni commented 7 years ago

Will send it later

barbagianni commented 7 years ago

Sent the log.

Btw, sending the previous command without the version actually gets a valid reply. But the robot doesn't start.

The message I sent was: {"state":{"reported":{"lastCommand":{"command":"start","time":1488399056,"initiator":"rmtApp"}}},"metadata":{"reported":{"lastCommand":{"command":{"timestamp":1488399056},"time":{"timestamp":1488399056},"initiator":{"timestamp":1488399056}}}},"timestamp":1488399057}

barbagianni commented 7 years ago

{ "state":{ "desired":{ "command":{ "command":"start","time":+new Date(),"initiator":"rmtApp" } } }, "timestamp":+new Date() }

Bam! Your working start command! :)

koalazak commented 7 years ago

I think that string you see in logs are a post-execution message sent by the robot after receibe the command. And the real command is all the ugly bytes before that string

koalazak commented 7 years ago

nice! I tried something like that before but nothing. Maybe I dont use the time filed in my tests... Testing now....

barbagianni commented 7 years ago

I actually got the robot to react with the last posted one. "start" for start and "stop" for stop.

There seems to be some kind of problems with the timestamps though, as it will start over and over again.

barbagianni commented 7 years ago

Yes, it seems to be important.

koalazak commented 7 years ago

oh crap, that works but the robot now is receiving the start command every second :p. let me kwno if you can stop it :p

barbagianni commented 7 years ago

Send the stop message. Same format. x)

koalazak commented 7 years ago

you promise that i can not get a loop of start AND stop commands now? :p

barbagianni commented 7 years ago

I can't, but my hope is that a hard reset will be able to fix it. :)

Let me try if the app is still able to start it.

barbagianni commented 7 years ago

Actually it is in a start stop loop. I'm resetting mine.

barbagianni commented 7 years ago

Yep, a reset fixed it. But that means we have to find out where to get the right timestamps.

barbagianni commented 7 years ago

Or maybe it's not about the timestamps but some of the other parts of the messages. The messages in the app seem to contain more information.

koalazak commented 7 years ago

reset as 10 second the start button?

barbagianni commented 7 years ago

Didn't try that. I used the "reset roomba" option in the app. But you have to pair it again.

koalazak commented 7 years ago

reset with 10 second start button doest work. but yes with the App.

iosdeveloper commented 7 years ago

New firmware is rolling out (again). Does anybody know what's changed? Hopefully nothing broke. img_4182

koalazak commented 7 years ago

@iosdeveloper opened a new issue for that: #31

koalazak commented 7 years ago

@letier do you have any progress in the cloud api reverse engeniering?

barbagianni commented 7 years ago

No luck. :(

It works with the iOS app at the moment, but doesn't do anything when I send the signals. The reset option in the app is greyed out, so I have no clue if I got it stuck in a strange state again.

barbagianni commented 7 years ago

Managed to reset and catch another conversation with the app. Saw the start message. But I'm suddenly not getting it to start anymore. And there seems to be all kind of state from my past trials saved. I'll need to find a way to clean it up. Posting via the snippet doesn't seem to work anymore.

barbagianni commented 7 years ago

So the situation seems to be that you somehow have to switch between certain states.

The following cycles work for me: { "state":{"desired":{"command":{"command":"start","time":Math.floor(new Date()/1000),"initiator":"rmtApp"}}}} { "state":{"desired":{"command":{"command":"clean","time":Math.floor(new Date()/1000),"initiator":"rmtApp"}}}} { "state":{"desired":{"command":null}}}

{ "state":{"desired":{"command":{"command":"stop","time":Math.floor(new Date()/1000),"initiator":"rmtApp"}}}} { "state":{"desired":{"command":null}}}

I'm not sure if clean is a real command. I just tried it and the robot started moving.

koalazak commented 7 years ago

I dont like that aprouch :p may there is another fancy way haha. If i dont foudn the way y just implement that in next version.