Skip to content

Commit

Permalink
restore session implement (#325)
Browse files Browse the repository at this point in the history
* fix keep Alive Request
* restore session implement
* add client restore button

Co-authored-by: hossinasaadi <Hossin277>
  • Loading branch information
hossinasaadi authored Aug 13, 2021
1 parent 34d7a39 commit 4853ada
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 7 deletions.
Binary file added .DS_Store
Binary file not shown.
48 changes: 41 additions & 7 deletions backend/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
import hashlib;
import hmac;
import traceback;

import binascii
from Crypto import Random
from whatsapp_defines import WATags, WASingleByteTokens, WADoubleByteTokens, WAWebMessageInfo;
from whatsapp_binary_writer import whatsappWriteBinary, WASingleByteTokens, WADoubleByteTokens, WAWebMessageInfo;
from whatsapp_defines import WAMetrics;
import websocket;
import curve25519;
import pyqrcode;
Expand Down Expand Up @@ -132,6 +136,10 @@ def onClose(self, ws):
if self.onCloseCallback is not None and "func" in self.onCloseCallback:
self.onCloseCallback["func"](self.onCloseCallback);
eprint("WhatsApp backend Websocket closed.");
def keepAlive(self):
if self.activeWs is not None:
self.activeWs.send("?,,")
Timer(20.0, self.keepAlive).start()

def onMessage(self, ws, message):
try:
Expand All @@ -145,7 +153,8 @@ def onMessage(self, ws, message):
if messageContent[0] == 'Pong' and messageContent[1] == True:
pend["callback"]({"Connected": True,"user":self.connInfo["me"],"pushname":self.connInfo["pushname"]})
elif pend["desc"] == "_restoresession":
eprint("") # TODO implement Challenge Solving
pend["callback"]["func"]({ "type": "restore_session" }, pend["callback"]);

elif pend["desc"] == "_login":
eprint("Message after login: ", message);
self.loginInfo["serverRef"] = json.loads(messageContent)["ref"];
Expand Down Expand Up @@ -182,7 +191,7 @@ def onMessage(self, ws, message):
if isinstance(jsonObj, list) and len(jsonObj) > 0: # check if the result is an array
eprint(json.dumps(jsonObj));
if jsonObj[0] == "Conn":
Timer(25, lambda: self.activeWs.send('?,,')).start() # Keepalive Request
Timer(20.0, self.keepAlive).start() # Keepalive Request
self.connInfo["clientToken"] = jsonObj[1]["clientToken"];
self.connInfo["serverToken"] = jsonObj[1]["serverToken"];
self.connInfo["browserToken"] = jsonObj[1]["browserToken"];
Expand All @@ -200,6 +209,7 @@ def onMessage(self, ws, message):
self.loginInfo["key"]["encKey"] = keysDecrypted[:32];
self.loginInfo["key"]["macKey"] = keysDecrypted[32:64];

self.save_session();
# eprint("private key : ", base64.b64encode(self.loginInfo["privateKey"].serialize()));
# eprint("secret : ", base64.b64encode(self.connInfo["secret"]));
# eprint("shared secret : ", base64.b64encode(self.connInfo["sharedSecret"]));
Expand All @@ -210,6 +220,14 @@ def onMessage(self, ws, message):

eprint("set connection info: client, server and browser token; secret, shared secret, enc key, mac key");
eprint("logged in as " + jsonObj[1]["pushname"] + " (" + jsonObj[1]["wid"] + ")");
elif jsonObj[0] == "Cmd":
if jsonObj[1]["type"] == "challenge": # Do challenge
challenge = WhatsAppEncrypt(self.loginInfo["key"]["encKey"], self.loginInfo["key"]["macKey"], base64.b64decode(jsonObj[1]["challenge"]))

challenge = base64.b64encode(challenge)
messageTag = str(getTimestamp());
eprint(json.dumps( [messageTag,["admin","challenge",challenge,self.connInfo["serverToken"],self.loginInfo["clientId"]]]))
self.activeWs.send(json.dumps( [messageTag,["admin","challenge",challenge,self.connInfo["serverToken"],self.loginInfo["clientId"]]]));
elif jsonObj[0] == "Stream":
pass;
elif jsonObj[0] == "Props":
Expand Down Expand Up @@ -239,17 +257,33 @@ def generateQRCode(self, callback=None):
self.activeWs.send(message);

def restoreSession(self, callback=None):
with open("session.json","r") as f:
session_file = f.read()
session = json.loads(session_file)
self.connInfo["clientToken"] = session['clientToken']
self.connInfo["serverToken"] = session['serverToken']
self.loginInfo["clientId"] = session['clientId']
self.loginInfo["key"]["macKey"] = session['macKey'].encode("latin_1")
self.loginInfo["key"]["encKey"] = session['encKey'].encode("latin_1")

messageTag = str(getTimestamp())
message = messageTag + ',["admin","init",['+ WHATSAPP_WEB_VERSION + '],["Chromium at ' + datetime.now().isoformat() + '","Chromium"],"' + self.loginInfo["clientId"] + '",true]'
message = messageTag + ',["admin","init",['+ WHATSAPP_WEB_VERSION + '],["StatusDownloader","Chromium"],"' + self.loginInfo["clientId"] + '",true]'
self.activeWs.send(message)

messageTag = str(getTimestamp())
self.messageQueue[messageTag] = {"desc": "_restoresession"}
self.messageQueue[messageTag] = {"desc": "_restoresession","callback": callback}
message = messageTag + ',["admin","login","' + self.connInfo["clientToken"] + '", "' + self.connInfo[
"serverToken"] + '", "' + self.loginInfo["clientId"] + '", "takeover"]'

self.activeWs.send(message)

def save_session(self):
session = {"clientToken":self.connInfo["clientToken"],"serverToken":self.connInfo["serverToken"],
"clientId":self.loginInfo["clientId"],"macKey": self.loginInfo["key"]["macKey"].decode("latin_1")
,"encKey": self.loginInfo["key"]["encKey"].decode("latin_1")};
f = open("./session.json","w")
f.write(json.dumps(session))
f.close()

def getLoginInfo(self, callback):
callback["func"]({ "type": "login_info", "data": self.loginInfo }, callback);

Expand All @@ -266,7 +300,7 @@ def sendTextMessage(self, number, text):
self.messageSentCount = self.messageSentCount + 1
self.messageQueue[messageId] = {"desc": "__sending"}
self.activeWs.send(payload, websocket.ABNF.OPCODE_BINARY)

def status(self, callback=None):
if self.activeWs is not None:
messageTag = str(getTimestamp())
Expand Down
2 changes: 2 additions & 0 deletions backend/whatsapp_web_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ def handleMessage(self):
cmd = obj["command"];
if cmd == "backend-generateQRCode":
currWhatsAppInstance.generateQRCode(callback);
elif cmd == "backend-restoreSession":
currWhatsAppInstance.restoreSession(callback);
elif cmd == "backend-getLoginInfo":
currWhatsAppInstance.getLoginInfo(callback);
elif cmd == "backend-getConnectionInfo":
Expand Down
1 change: 1 addition & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<h1>WhatsApp Web</h1>
<div id="bootstrap-container-content">
<button id="bootstrap-button" class="btn">Click to connect to API.</button>
<button id="restore-session" class="btn">Restore Session</button>
</div>
</div>

Expand Down
95 changes: 95 additions & 0 deletions client/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ $(document).ready(function() {

allWhatsAppMessages = [];
$("#messages-list-table-body").empty();
$("#restore-session").addClass("hidden");

},
activateQRCode: image => {
let container = $("#bootstrap-container").removeClass("hidden").children("#bootstrap-container-content");
Expand All @@ -60,6 +62,11 @@ $(document).ready(function() {
$("#main-container").removeClass("hidden");
$("#button-disconnect").html("Disconnect").attr("disabled", false);
},
restoreSession: () => {
$("#restore-session").removeClass("hidden");
$("#restore-session").html("Restore Session");

},
steps: [
new BootstrapStep({
websocket: apiWebsocket,
Expand Down Expand Up @@ -116,6 +123,8 @@ $(document).ready(function() {
connLost: "Connection of backend to WhatsApp closed. Click to reconnect."
},
actor: websocket => {
bootstrapInfo.restoreSession();

websocket.waitForMessage({
condition: obj => obj.type == "resource_gone" && obj.resource == "whatsapp",
keepWhenHit: false
Expand Down Expand Up @@ -206,10 +215,96 @@ $(document).ready(function() {
},
timeoutCondition: websocket => websocket.backendConnectedToWhatsApp
}
}),
new BootstrapStep({
websocket: apiWebsocket,
texts: {
handling: "Restoring...",
success: "Restored in %1 ms.",
failure: "Restore failed: %1. Click to try again."
},
request: {
type: "call",
callArgs: { command: "backend-restoreSession" },
successCondition: obj => obj.type == "restore_session" ,
successActor: (websocket) => {
websocket.waitForMessage({
condition: obj => obj.type == "whatsapp_message_received" && obj.message,
keepWhenHit: true
}).then(whatsAppMessage => {

bootstrapInfo.deactivate();
/*<tr>
<th scope="row">1</th>
<td>Do., 21.12.2017, 22:59:09.123</td>
<td>Binary</td>
<td class="fill no-monospace"><button class="btn">View</button></td>
</tr>*/

let d = whatsAppMessage.data;
let viewJSONButton = $("<button></button>").addClass("btn").html("View").click(function() {
let messageIndex = parseInt($(this).parent().parent().attr("data-message-index"));
let jsonData = allWhatsAppMessages[messageIndex];
let tree, collapse = false;
let dialog = bootbox.dialog({
title: `WhatsApp message #${messageIndex+1}`,
message: "<p>Loading JSON...</p>",
buttons: {
noclose: {
label: "Collapse/Expand All",
className: "btn-info",
callback: function () {
if (!tree)
return true;

if (collapse === false)
tree.expand();
else
tree.collapse();

collapse = !collapse;

return false;
}
}
}
});
dialog.init(() => {
tree = jsonTree.create(jsonData, dialog.find(".bootbox-body").empty()[0]);
});
});

let tableRow = $("<tr></tr>").attr("data-message-index", allWhatsAppMessages.length);
tableRow.append($("<th></th>").attr("scope", "row").html(allWhatsAppMessages.length+1));
tableRow.append($("<td></td>").html(moment.unix(d.timestamp/1000.0).format("ddd, DD.MM.YYYY, HH:mm:ss.SSS")));
tableRow.append($("<td></td>").html(d.message_type));
tableRow.append($("<td></td>").addClass("fill no-monospace").append(viewJSONButton));
$("#messages-list-table-body").append(tableRow);
allWhatsAppMessages.push(d.message);

//$("#main-container-content").empty();
//jsonTree.create(whatsAppMessage.data.message, $("#main-container-content")[0]);
}).run();
},
timeoutCondition: websocket => websocket.backendConnectedToWhatsApp
}
})

]
};
$("#restore-session").addClass("hidden");

$("#restore-session").click(function() {
bootstrapInfo.steps[4].run(apiInfo.timeout).then(() => {
let text = currStep.texts.success.replace("%1", Math.round(performance.now() - stepStartTime));
$(this).html(text).attr("disabled", false);
bootstrapState++;
})
.catch(reason => {
let text = currStep.texts.failure.replace("%1", reason);
$(this).html(text).attr("disabled", false);
});
});
$("#bootstrap-button").click(function() {
let currStep = bootstrapInfo.steps[bootstrapState];
let stepStartTime = performance.now();
Expand Down
35 changes: 35 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,42 @@ wss.on("connection", function(clientWebsocketRaw, req) {
clientCallRequest.respond({ type: "error", reason: reason });
})
}).run();
clientWebsocket.waitForMessage({
condition: obj =>
{
last_session_data = obj.last_session
return obj.from == "client" && obj.type == "call" && obj.command == "backend-restoreSession"
},
keepWhenHit: true
}).then(clientCallRequest => {
if(!backendWebsocket.isOpen) {
clientCallRequest.respond({ type: "error", reason: "No backend connected." });
clientWebsocket.send({ data: backendResponse });

return;
}
new BootstrapStep({
websocket: backendWebsocket,
request: {
type: "call",
callArgs: { command: "backend-restoreSession", last_session: last_session_data, whatsapp_instance_id: backendWebsocket.activeWhatsAppInstanceId },
successCondition: obj => obj.from == "backend" && obj.type == "restore_session"
}
}).run(backendInfo.timeout).then(backendResponse => {
clientWebsocket.send({ data: backendResponse });
clientCallRequest.respond({ type: "restore_session", res: backendResponse })

backendWebsocket.waitForMessage({
condition: obj => obj.type == "whatsapp_message_received" && obj.message && obj.message_type && obj.timestamp && obj.resource_instance_id == backendWebsocket.activeWhatsAppInstanceId,
keepWhenHit: true
}).then(whatsAppMessage => {
let d = whatsAppMessage.data;
clientWebsocket.send({ type: "whatsapp_message_received", message: d.message, message_type: d.message_type, timestamp: d.timestamp });
}).run();
}).catch(reason => {
clientCallRequest.respond({ type: "error", reason: reason });
})
}).run();

//TODO:
// - designated backend call function to make everything shorter
Expand Down
1 change: 1 addition & 0 deletions session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"macKey": "\u00dc]\u00c4\u00aaG\f\u0003G\u00fa\u00a3.\u00e6xK;\u00e6\u00de}y\u001e\u0017\u00ae\u0017\u00d3\rH\u00c1R\u0004\u00a4\u0019\u0011", "serverToken": "1@B6whfXk6pYOR7Yt6GjLoQST3RfXAZpp19dsQz3aUvKo87z7Xpw5ceI2ZbeApDaeu+DjohY+ZqMOVJQ==", "encKey": "\u00e4\u0083In\u007f1\u001f\u00f1\n\u00965\u001c\u00cbn\u0085hM<\u0082\n\u001dT\u00f4\u00b5\u00b9y\u00ee\u00f3{\u0085\bN", "clientId": "5svFWIYYQdBGTxqFyhcsUg==", "clientToken": "InUzqzrXdq5ZwqLHwMMjNSmsV3ZzTHLmHwLPvh1YiME="}

0 comments on commit 4853ada

Please sign in to comment.