~fabrixxm/remotami

a474b264c244c30b5d54d3e3d5c359d11d109567 — fabrixxm 1 year, 8 months ago 69a5815 master
Experimental GUI client in python+gtk/libhandy
2 files changed, 588 insertions(+), 0 deletions(-)

A client/remotami.py
A client/win.glade
A client/remotami.py => client/remotami.py +297 -0
@@ 0,0 1,297 @@
#!/usr/bin/env python3
import time
from pathlib import Path
from random import randrange
import requests
from sshpubkeys import SSHKey

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Handy', '1')

from gi.repository import GObject
from gi.repository import GLib, Gdk, Gtk, Gio
from gi.repository import Handy

##

HERE = Path(__file__).parent

SERVER = "kirgroup.net"
VNCVIEWER = "/usr/bin/vinagre vnc://{host}:{port}"

PRIV_KEY_FILE = Path.home() / ".ssh" / "remotami"
KEY_FILE = Path.home() / ".ssh" / "remotami.pub"

http = f"http://{SERVER}:5000"

##

def get_userid(key):
    keyhash = key.hash_md5().split(":")[-5:]
    code = [int(n, 16) for n in keyhash]
    return " ".join([str(x) for x in code])


def get_password():
    return hex(randrange(10000, 1000000)).replace("0x", "").upper()


def register():
    if not PRIV_KEY_FILE.exists():
        cmd = ["ssh-keygen", "-N", "", "-f", str(PRIV_KEY_FILE)]
        subprocess.run(args=cmd)

    with open(KEY_FILE, "r") as f:
        pubkey = SSHKey(f.read())

    r = requests.post(f"{http}/register", data={'pubkey': pubkey.keydata})
    if not r.ok:
        raise Exception(f"Unable to register. Status code: {r.status_code}")
    return pubkey


def get_port(userid):
    r = requests.get(f"{http}/port", params={'u': userid.replace(" ", "")})
    if not r.ok:
        raise Exception(f"Unable to request port. Status code: {r.status_code}")

    remoteport = int(r.text)
    if remoteport < 5900 or remoteport >= 6000:
        raise Exception("Got invalid port from server")

    return remoteport


def get_remote_port(userid):
    r = requests.get(f"{http}/remoteport", params={'u': userid.replace(" ", "")})
    if not r.ok:
        raise Exception(f"Unable to request port. Status code: {r.status_code}")

    remoteport = int(r.text)
    if remoteport < 5900 or remoteport >= 6000:
        print("Got invalid port from server")
        sys.exit(-1)

    return remoteport



class Subprocess(GObject.GObject):
    @GObject.Signal(arg_types=(int,))
    def done(self, status_code):
        ...
    
    @GObject.Signal(arg_types=(str,))
    def new_text(self, text):
        ...

    def __init__(self, cmd):
        super().__init__()
        print(f"Starting '{cmd}'")
        args = cmd.split(" ")
        self.bin_name = args[0]
        self._subp = Gio.Subprocess.new(
            args,
            Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE
        )
        self._cancellable = Gio.Cancellable.new()
        stdout = self._subp.get_stdout_pipe()
        self._read(stdout)
        self._subp.wait_check_async(self._cancellable, self._on_done, None)

    def _read(self, stdout):
        stdout.read_bytes_async(
            1024, GLib.PRIORITY_DEFAULT, self._cancellable, self._on_read, None)

    def _on_done(self, source, res, data=None):
        print(f"[{self.bin_name}] done")
        if not self._cancellable.is_cancelled():
            self.emit("done", source.get_exit_status())

    def _on_read(self, source, res, data=None):
        if self._cancellable.is_cancelled():
            return
        readed_bytes = source.read_bytes_finish(res).unref_to_data()
        text = readed_bytes.decode("utf8")
        if text != "":
            self.emit("new_text", text)
        if self._subp.get_if_exited() or source.is_closed():
            return
        self._read(source)

    def stop(self):
        print(f"[{self.bin_name}] stop")
        self._cancellable.cancel()
        # maybe we should send "SIGTERM" instead..
        self._subp.force_exit()


## UI


class NotificationOverlay(Gtk.Revealer):
    """Show a closeable message in overlay"""
    def __init__(self):
        super().__init__(halign=Gtk.Align.CENTER, valign=Gtk.Align.START)
        box = Gtk.HBox(spacing=20)
        box.get_style_context().add_class("app-notification")
        label = Gtk.Label(label="")
        self.button = button = Gtk.Button.new_from_icon_name("window-close-symbolic", Gtk.IconSize.BUTTON)
        box.pack_start(label,False, True, 0)
        box.pack_start(button, False, False, 0)
        self.add(box)

        button.connect("clicked", self.close)
        self.label = label

        self.show_all()

    def show(self, title, body=None, timeout=None):
        text = f"<b>{title}</b>"
        if body:
            text = f"{text}\n{body}"
        self.label.set_markup(text)
        self.set_reveal_child(True)
        if timeout:
            self.button.set_visible(False)
            GLib.timeout_add(timeout*1000, self.close)
        else:
            self.button.set_visible(True)

    def close(self, *args):
        self.set_reveal_child(False)


class RemotamiApp:
    def __init__(self):
        self.subprocess = []
        
        self.build = Gtk.Builder.new_from_file(str(HERE / "win.glade"))
        self.go = self.build.get_object
        self.window = self.go("winMain")
        self.sharing_window = self.go("winSharing")
        
        self.textbuffer = self.go("textbuffer1")
        self.input_remote_code = self.go("inputRemoteCode")
        self.btn_connect = self.go("btnConnect")
        
        self.notification = NotificationOverlay()
        self.go("overlay").add_overlay(self.notification)
        
        self.build.connect_signals(self)
        self.window.show()
        
        try:
            self.pubkey = register()
        except Exception as e:
            print(e)
            self.notification.show("Impossibile registrarsi")
            return

        self.code = get_userid(self.pubkey)
        self.pwd = get_password()
        
        self.go("btnCopy").set_sensitive(True)
        self.go("btnStartShare").set_sensitive(True)
        self.go("viewSwitcher").set_sensitive(True)
        
        self.go("lblCode").set_label(self.code)
        self.go("lblPassword").set_label(self.pwd)

    def on_copy(self, *args):
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        text = f"Codice: {self.code}\nPassword: {self.pwd}"
        clipboard.set_text(text, -1)
        self.notification.show("Codice e password copiati negli apputi", timeout=5)

    def _run(self, cmd):
        subp = Subprocess(cmd)
        subp.connect("new_text", lambda _,text: self.textbuffer.insert_at_cursor(text, -1))
        subp.connect("done", self.on_sub_stopped)
        self.subprocess.append(subp)

    def on_start_share(self, *args):
        print("on start share")
        self.textbuffer.set_text("setting up...\n", -1)
        
        try:
            remoteport = get_port(self.code)
        except Exception as e:
            print(e)
            self.notification.show("Errore richiedendo la porta remota")
            return
        
        self._run(f"ssh -N -R {remoteport}:localhost:5900 -i '{PRIV_KEY_FILE}' vnc@{SERVER}")
        self._run(f"x11vnc -localhost -display :0 -passwd {self.pwd}")

        self.window.hide()
        self.go("btnStopShare").set_visible(True)
        self.sharing_window.show()

    def on_connect(self, *args):
        print("on connect")
        code = self.input_remote_code.get_text()
        
        textbuffer = self.go("textbuffer1")
        textbuffer.set_text("setting up...\n", -1)
        
        try:
            remoteport = get_remote_port(code)
        except Exception as e:
            print(e)
            self.notification.show("Errore richiedendo la porta remota")
            return
        
        self._run(f"ssh -N -L 5899:localhost:{remoteport} -i '{PRIV_KEY_FILE}' vnc@{SERVER}")
        
        # TODO: this will block event loop
        # we need to wait for ssh to connect and open local port...
        time.sleep(3)
        
        self._run(VNCVIEWER.format(host="localhost", port="5899"))
        self.window.hide()

    def on_inputRemoteCode_changed(self, *args):
        text = self.input_remote_code.get_text()
        self.btn_connect.set_sensitive(len(text) > 0)

    def on_sub_stopped(self, source, status_code):
        print("on sub stop", status_code)
        if status_code == 0 and len(self.subprocess) > 0:
            self._close_sharing_window()
        else:
            self.go("btnStopShare").set_visible(False)
            self.sharing_window.show()
        self._stop_all_subprocess()

    def _stop_all_subprocess(self):
        if len(self.subprocess) > 0:
            for k in range(len(self.subprocess)):
                self.subprocess[k].stop()
            self.subprocess = []

    def _close_sharing_window(self):
        self.window.show()
        self.sharing_window.hide()

    def on_stop_share(self, *args):
        print("on stop share")
        self._stop_all_subprocess()
        self._close_sharing_window()

    def on_quit(self, *args):
        print("on quit")
        Gtk.main_quit()


def main():
    Handy.init()
    app = RemotamiApp()
    Gtk.main()


if __name__ == "__main__":
    main()


A client/win.glade => client/win.glade +291 -0
@@ 0,0 1,291 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
  <requires lib="gtk+" version="3.24"/>
  <requires lib="libhandy" version="3.24"/>
  <object class="GtkTextBuffer" id="textbuffer1"/>
  <object class="GtkWindow" id="winSharing">
    <property name="can-focus">False</property>
    <property name="title" translatable="yes">RemotAmi</property>
    <property name="type-hint">dialog</property>
    <signal name="destroy" handler="on_stop_share" swapped="no"/>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can-focus">False</property>
        <property name="margin-start">8</property>
        <property name="margin-end">8</property>
        <property name="margin-top">8</property>
        <property name="margin-bottom">8</property>
        <property name="orientation">vertical</property>
        <property name="spacing">8</property>
        <child>
          <object class="GtkScrolledWindow">
            <property name="width-request">200</property>
            <property name="height-request">100</property>
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="shadow-type">in</property>
            <child>
              <object class="GtkTextView">
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="editable">False</property>
                <property name="cursor-visible">False</property>
                <property name="buffer">textbuffer1</property>
              </object>
            </child>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="btnStopShare">
            <property name="label" translatable="yes">Ferma condivisione</property>
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="receives-default">True</property>
            <signal name="clicked" handler="on_stop_share" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
  <object class="GtkWindow" id="winMain">
    <property name="width-request">500</property>
    <property name="height-request">400</property>
    <property name="can-focus">False</property>
    <signal name="destroy" handler="on_quit" swapped="no"/>
    <child>
      <object class="GtkOverlay" id="overlay">
        <property name="visible">True</property>
        <property name="can-focus">False</property>
        <child>
          <object class="HdyClamp">
            <property name="visible">True</property>
            <property name="can-focus">False</property>
            <property name="margin-start">8</property>
            <property name="margin-end">8</property>
            <property name="maximum-size">400</property>
            <child>
              <object class="GtkStack" id="stack1">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="margin-start">8</property>
                <property name="margin-end">8</property>
                <property name="margin-top">8</property>
                <property name="margin-bottom">8</property>
                <child>
                  <object class="GtkBox">
                    <property name="visible">True</property>
                    <property name="can-focus">False</property>
                    <property name="vexpand">True</property>
                    <property name="orientation">vertical</property>
                    <property name="spacing">8</property>
                    <child>
                      <object class="GtkLabel">
                        <property name="visible">True</property>
                        <property name="can-focus">False</property>
                        <property name="halign">start</property>
                        <property name="label" translatable="yes">Il tuo codice postazione</property>
                      </object>
                      <packing>
                        <property name="expand">False</property>
                        <property name="fill">True</property>
                        <property name="position">0</property>
                      </packing>
                    </child>
                    <child>
                      <object class="GtkLabel" id="lblCode">
                        <property name="visible">True</property>
                        <property name="can-focus">False</property>
                        <property name="halign">end</property>
                        <property name="justify">right</property>
                        <property name="selectable">True</property>
                        <attributes>
                          <attribute name="weight" value="bold"/>
                          <attribute name="scale" value="2"/>
                        </attributes>
                      </object>
                      <packing>
                        <property name="expand">False</property>
                        <property name="fill">True</property>
                        <property name="position">1</property>
                      </packing>
                    </child>
                    <child>
                      <object class="GtkLabel">
                        <property name="visible">True</property>
                        <property name="can-focus">False</property>
                        <property name="halign">start</property>
                        <property name="label" translatable="yes">La tua password</property>
                      </object>
                      <packing>
                        <property name="expand">False</property>
                        <property name="fill">True</property>
                        <property name="position">2</property>
                      </packing>
                    </child>
                    <child>
                      <object class="GtkLabel" id="lblPassword">
                        <property name="visible">True</property>
                        <property name="can-focus">False</property>
                        <property name="halign">end</property>
                        <property name="justify">right</property>
                        <property name="selectable">True</property>
                        <attributes>
                          <attribute name="weight" value="bold"/>
                          <attribute name="scale" value="2"/>
                        </attributes>
                      </object>
                      <packing>
                        <property name="expand">False</property>
                        <property name="fill">True</property>
                        <property name="position">3</property>
                      </packing>
                    </child>
                    <child>
                      <object class="GtkButton" id="btnCopy">
                        <property name="label">gtk-copy</property>
                        <property name="visible">True</property>
                        <property name="sensitive">False</property>
                        <property name="can-focus">True</property>
                        <property name="receives-default">True</property>
                        <property name="halign">end</property>
                        <property name="valign">start</property>
                        <property name="use-stock">True</property>
                        <property name="always-show-image">True</property>
                        <signal name="clicked" handler="on_copy" swapped="no"/>
                      </object>
                      <packing>
                        <property name="expand">True</property>
                        <property name="fill">True</property>
                        <property name="position">4</property>
                      </packing>
                    </child>
                    <child>
                      <object class="GtkButton" id="btnStartShare">
                        <property name="label" translatable="yes">Avvia condivisione</property>
                        <property name="visible">True</property>
                        <property name="sensitive">False</property>
                        <property name="can-focus">True</property>
                        <property name="receives-default">True</property>
                        <signal name="clicked" handler="on_start_share" swapped="no"/>
                        <style>
                          <class name="suggested-action"/>
                        </style>
                      </object>
                      <packing>
                        <property name="expand">False</property>
                        <property name="fill">True</property>
                        <property name="position">5</property>
                      </packing>
                    </child>
                  </object>
                  <packing>
                    <property name="name">page0</property>
                    <property name="title" translatable="yes">Condividi</property>
                  </packing>
                </child>
                <child>
                  <object class="GtkBox">
                    <property name="visible">True</property>
                    <property name="can-focus">False</property>
                    <property name="orientation">vertical</property>
                    <property name="spacing">8</property>
                    <child>
                      <object class="GtkLabel">
                        <property name="visible">True</property>
                        <property name="can-focus">False</property>
                        <property name="halign">start</property>
                        <property name="label" translatable="yes">Codice postazione</property>
                      </object>
                      <packing>
                        <property name="expand">False</property>
                        <property name="fill">True</property>
                        <property name="position">0</property>
                      </packing>
                    </child>
                    <child>
                      <object class="GtkEntry" id="inputRemoteCode">
                        <property name="visible">True</property>
                        <property name="can-focus">True</property>
                        <property name="input-purpose">digits</property>
                        <signal name="changed" handler="on_inputRemoteCode_changed" swapped="no"/>
                      </object>
                      <packing>
                        <property name="expand">False</property>
                        <property name="fill">True</property>
                        <property name="position">1</property>
                      </packing>
                    </child>
                    <child>
                      <object class="GtkButton" id="btnConnect">
                        <property name="label" translatable="yes">Connetti</property>
                        <property name="visible">True</property>
                        <property name="sensitive">False</property>
                        <property name="can-focus">True</property>
                        <property name="receives-default">True</property>
                        <property name="valign">end</property>
                        <signal name="clicked" handler="on_connect" swapped="no"/>
                        <style>
                          <class name="suggested-action"/>
                        </style>
                      </object>
                      <packing>
                        <property name="expand">True</property>
                        <property name="fill">True</property>
                        <property name="position">3</property>
                      </packing>
                    </child>
                  </object>
                  <packing>
                    <property name="name">page1</property>
                    <property name="title" translatable="yes">Controlla</property>
                    <property name="position">1</property>
                  </packing>
                </child>
              </object>
            </child>
          </object>
          <packing>
            <property name="index">-1</property>
          </packing>
        </child>
      </object>
    </child>
    <child type="titlebar">
      <object class="HdyHeaderBar">
        <property name="visible">True</property>
        <property name="can-focus">False</property>
        <property name="show-close-button">True</property>
        <child type="title">
          <object class="HdySqueezer">
            <property name="visible">True</property>
            <property name="can-focus">False</property>
            <property name="homogeneous">True</property>
            <child>
              <object class="HdyViewSwitcher" id="viewSwitcher">
                <property name="visible">True</property>
                <property name="sensitive">False</property>
                <property name="can-focus">False</property>
                <property name="stack">stack1</property>
              </object>
            </child>
            <child>
              <placeholder/>
            </child>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>