diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..97b0c84 Binary files /dev/null and b/.DS_Store differ diff --git a/backend/whatsapp.py b/backend/whatsapp.py index 07a125a..a7a7dfa 100755 --- a/backend/whatsapp.py +++ b/backend/whatsapp.py @@ -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; @@ -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: @@ -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"]; @@ -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"]; @@ -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"])); @@ -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": @@ -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); @@ -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()) diff --git a/backend/whatsapp_web_backend.py b/backend/whatsapp_web_backend.py index b75711d..0e33a6e 100755 --- a/backend/whatsapp_web_backend.py +++ b/backend/whatsapp_web_backend.py @@ -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": diff --git a/client/index.html b/client/index.html index 81bc7bf..24cd268 100644 --- a/client/index.html +++ b/client/index.html @@ -35,6 +35,7 @@

WhatsApp Web

+
diff --git a/client/js/main.js b/client/js/main.js index cf2eb45..05ef9ec 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -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"); @@ -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, @@ -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 @@ -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(); + /* + 1 + Do., 21.12.2017, 22:59:09.123 + Binary + + */ + + let d = whatsAppMessage.data; + let viewJSONButton = $("").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: "

Loading JSON...

", + 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 = $("").attr("data-message-index", allWhatsAppMessages.length); + tableRow.append($("").attr("scope", "row").html(allWhatsAppMessages.length+1)); + tableRow.append($("").html(moment.unix(d.timestamp/1000.0).format("ddd, DD.MM.YYYY, HH:mm:ss.SSS"))); + tableRow.append($("").html(d.message_type)); + tableRow.append($("").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(); diff --git a/index.js b/index.js index f6bbcba..fb1ba6e 100644 --- a/index.js +++ b/index.js @@ -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 diff --git a/session.json b/session.json new file mode 100644 index 0000000..c04a7c9 --- /dev/null +++ b/session.json @@ -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="} \ No newline at end of file