diff --git a/bin/nc.traditional b/bin/nc.traditional new file mode 100755 index 0000000..a5c443b Binary files /dev/null and b/bin/nc.traditional differ diff --git a/resources.qrc b/resources.qrc index 801adef..c2b92f7 100644 --- a/resources.qrc +++ b/resources.qrc @@ -5,6 +5,7 @@ assets/dead.svg assets/problem.svg assets/connecting.png + bin/nc.traditional bin/rM1-vnc-server-standalone bin/rM2-vnc-server-standalone diff --git a/src/rmview/screenstream/common.py b/src/rmview/screenstream/common.py index e13d1e3..2fd9e7d 100644 --- a/src/rmview/screenstream/common.py +++ b/src/rmview/screenstream/common.py @@ -81,6 +81,7 @@ def clientConnectionLost(self, connector, reason): reactor.callFromThread(reactor.stop) def clientConnectionFailed(self, connector, reason): + log.warning("Connection failed") if reason.check(ConnectionRefusedError): self.signals.onFatalError.emit(Exception("It seems the tablet is refusing to connect.\nIf you are using the ScreenShare backend please make sure you enabled it on the tablet, before running rmview.")) else: diff --git a/src/rmview/screenstream/screenshare.py b/src/rmview/screenstream/screenshare.py index f284af7..8827efd 100644 --- a/src/rmview/screenstream/screenshare.py +++ b/src/rmview/screenstream/screenshare.py @@ -25,45 +25,6 @@ log = logging.getLogger('rmview') -# the screenshare vnc auth uses udp broadcasts -class ChallengeReaderProtocol(DatagramProtocol): - clients = {} - - def __init__(self, callback): - self.callback = callback - - def datagramReceived(self, datagram, host): - reader = io.BytesIO(datagram) - - # the timestamp is needed for the challenge - timestamp = reader.read(8) - tounx, = unpack("!Q", timestamp) - if timestamp in self.clients: - log.debug(f"skipping challenge {tounx}") - return - log.info(f"received timestamp challenge {tounx}") - - if not self.callback(timestamp): - log.debug("Stopping listening for timestamps") - self.transport.stopListening() - - self.clients[timestamp] = addresses = [] - - ### The rest of the message is ignored for now - # (hashlength,) = unpack("!I", reader.read(4)) - # hash = reader.read(hashlength) - # strhash = hash.hex() - # #TODO: the email hash could be used to filter the broadcasts when multiple devices are on the network - # log.info(f"email hash: {strhash}") - - # #read tablet's listening addresses - # while reader.read(1) == b'\00': - # ip = socket.inet_ntoa(reader.read(4)) - # port, = unpack("!H", reader.read(2)) - # addresses.append(f"{ip}:{port}") - - # log.info(addresses) - class ScreenShareStream(QRunnable): @@ -76,9 +37,17 @@ def __init__(self, ssh): self.signals = ScreenStreamSignals() def needsDependencies(self): - return False + _, out, _ = self.ssh.exec_command("[ -x /usr/bin/nc.traditional ]") + return out.channel.recv_exit_status() != 0 def installDependencies(self): + sftp = self.ssh.open_sftp() + from stat import S_IXUSR + fo = QFile(':bin/nc.traditional') + fo.open(QIODevice.ReadOnly) + sftp.putfo(fo, '/usr/bin/nc.traditional') + fo.close() + sftp.chmod('/usr/bin/nc.traditional', S_IXUSR) pass def stop(self): @@ -96,6 +65,7 @@ def stop(self): reads the usedId from deviceToken from the config file on the rm """ def get_userid(self): + log.info("Getting userid from device for challenge computation...") with self.ssh.open_sftp() as sftp: with sftp.file('/etc/remarkable.conf') as f: file_content = f.read().decode() @@ -109,20 +79,13 @@ def get_userid(self): return(d["auth0-userid"]) def computeChallenge(self, userId, timestamp): + log.info("Computing challenge from userId and timestamp...") userBytes = userId.encode() userIdHash = hashlib.sha256(userBytes).digest() return hashlib.sha256(timestamp + userIdHash).digest() - #Hack to run the vnc with the challenge - def runVnc(self, timestamp): - if not self.factory: - userId = self.get_userid() - challenge = self.computeChallenge(userId, timestamp) - log.info(f"Challenge: {challenge.hex()}, connecting to vnc") - self.startVncClient(challenge) - return False - def startVncClient(self, challenge=None): + log.info("Starting vncClient...") self.factory = VncFactory(self.signals) self.factory.setChallenge(challenge) @@ -130,16 +93,57 @@ def startVncClient(self, challenge=None): #self.vncClient = internet.TCPClient("localhost", 31337, self.factory) self.vncClient = internet.SSLClient(self.ssh.hostname, 5900, self.factory, ssl.ClientContextFactory()) self.vncClient.startService() + log.info("vncClient Started.") + + def readChallengeOverTunnel(self): + log.info("Listening for device broadcast through ssh tunnel...") + stdin, stdout, stderr = self.ssh.exec_command('/usr/bin/nc.traditional -l -u -p 5901 -w 6') + # nc needs its stdin closed before it will return! + stdin.writelines("\n") + stdin.close() + stdout.channel.recv_exit_status() + timestamp = stdout.read(8) + tounx, = unpack("!Q", timestamp) + log.info(f"Timestamp challenge {tounx}") + + ### The rest of the message is ignored for now + # (hashlength,) = unpack("!I", reader.read(4)) + # hash = reader.read(hashlength) + # strhash = hash.hex() + # #TODO: the email hash could be used to filter the broadcasts when multiple devices are on the network + # log.info(f"email hash: {strhash}") + + # #read tablet's listening addresses + # while reader.read(1) == b'\00': + # ip = socket.inet_ntoa(reader.read(4)) + # port, = unpack("!H", reader.read(2)) + # addresses.append(f"{ip}:{port}") + + stderr.close() + stdout.close() + + # TODO: We should check the IP from the packet to ensure it's not a broadcast + # from another rM on the same network, right? + + # compute challenge and start client + userId = self.get_userid() + challenge = self.computeChallenge(userId, timestamp) + log.info(f"Challenge: {challenge.hex()}, connecting to vnc") + + # start the actual vnc client + # this call needs to be done from the main reactor thread (for reasons I don't understand) + reactor.callFromThread(self.startVncClient, challenge) def run(self): log.info("Connecting to ScreenShare, make sure you enable it") try: if self.ssh.softwareVersion > SW_VER_TIMESTAMPS['2.9.1.236']: - log.warning("Authenticating, please wait...") - challengeReader = ChallengeReaderProtocol(self.runVnc) - reactor.listenUDP(5901, challengeReader) + log.info("Authenticating, please wait...") + # do this blocking io in a background thread + reactor.callInThread(self.readChallengeOverTunnel) else: log.warning("Skipping authentication") + # does this work without reactor having been started? self.startVncClient() reactor.run(installSignalHandlers=0)