Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added bin/nc.traditional
Binary file not shown.
1 change: 1 addition & 0 deletions resources.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<file>assets/dead.svg</file>
<file>assets/problem.svg</file>
<file>assets/connecting.png</file>
<file>bin/nc.traditional</file>
<file>bin/rM1-vnc-server-standalone</file>
<file>bin/rM2-vnc-server-standalone</file>
</qresource>
Expand Down
1 change: 1 addition & 0 deletions src/rmview/screenstream/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
108 changes: 56 additions & 52 deletions src/rmview/screenstream/screenshare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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()
Expand All @@ -109,37 +79,71 @@ 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)

# left for testing with stunnel
#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)

Expand Down