diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..604652f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "bindings/vapi"] + path = bindings/vapi + url = https://gitlab.gnome.org/GNOME/vala-extra-vapis.git diff --git a/bindings/vapi b/bindings/vapi new file mode 160000 index 0000000..9ce7531 --- /dev/null +++ b/bindings/vapi @@ -0,0 +1 @@ +Subproject commit 9ce7531a398c3ba629c89aad6ba43fb73c6060b6 diff --git a/fr.oupson.FooTerm.json b/fr.oupson.FooTerm.json index 64ee555..d88f4d4 100644 --- a/fr.oupson.FooTerm.json +++ b/fr.oupson.FooTerm.json @@ -37,13 +37,33 @@ "buildsystem": "meson", "config-opts": ["-Dgtk4=true", "-Dgtk3=false", "-Dsixel=true"], "sources": [ - { - "type": "archive", - "url": "https://gitlab.gnome.org/GNOME/vte/-/archive/015ca4d2fdc57b625add7b23b0afa7193adc45a9/vte-015ca4d2fdc57b625add7b23b0afa7193adc45a9.tar.gz", - "sha256": "ba918109936692fe555f1b28f428680ed4940a350709bcf908bdbf9a93498c08" - } + { + "type": "archive", + "url": "https://gitlab.gnome.org/GNOME/vte/-/archive/015ca4d2fdc57b625add7b23b0afa7193adc45a9/vte-015ca4d2fdc57b625add7b23b0afa7193adc45a9.tar.gz", + "sha256": "ba918109936692fe555f1b28f428680ed4940a350709bcf908bdbf9a93498c08" + } ] }, + { + "name" : "libssh2", + "buildsystem" : "cmake-ninja", + "config-opts" : [ + "-DCMAKE_BUILD_TYPE=RelWithDebInfo", + "-DCMAKE_INSTALL_LIBDIR:PATH=/app/lib", + "-DBUILD_SHARED_LIBS:BOOL=ON" + ], + "cleanup" : [ + "/share/doc" + ], + "sources" : [ + { + "type" : "git", + "url" : "https://github.com/libssh2/libssh2.git", + "tag" : "libssh2-1.10.0" + } + ] + }, + { "name" : "footerm", "builddir" : true, diff --git a/meson.build b/meson.build index fa5e1ca..25c8db1 100644 --- a/meson.build +++ b/meson.build @@ -7,7 +7,17 @@ project('footerm', ['c', 'vala'], i18n = import('i18n') gnome = import('gnome') +metadata_dir = meson.project_source_root() / 'bindings'/ 'metadata' +vapi_dir = meson.project_source_root() / 'bindings' / 'vapi' +add_project_arguments([ + # Make sure Meson can find custom VAPIs + '--vapidir', vapi_dir, + '--metadatadir', metadata_dir, + ], + language: 'vala' +) +add_project_arguments('-D_GNU_SOURCE', language: 'c') subdir('data') subdir('src') diff --git a/src/footerm.gresource.xml b/src/footerm.gresource.xml index f1dfa8a..c923920 100644 --- a/src/footerm.gresource.xml +++ b/src/footerm.gresource.xml @@ -2,6 +2,9 @@ window.ui + newpane.ui + terminalpane.ui + newserver.ui gtk/help-overlay.ui diff --git a/src/main.vala b/src/main.vala index 0cc9227..fc21864 100644 --- a/src/main.vala +++ b/src/main.vala @@ -19,6 +19,15 @@ */ int main (string[] args) { + var rc = SSH2.init (0); + if (rc != SSH2.Error.NONE) { + stdout.printf ("libssh2 initialization failed (%d)\n", rc); + return -1; + } + var app = new Footerm.Application (); - return app.run (args); + var res = app.run(args); + + SSH2.exit (); + return res; } diff --git a/src/meson.build b/src/meson.build index 91d60de..f8f946b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,14 +2,21 @@ footerm_sources = [ 'main.vala', 'application.vala', 'window.vala', + 'newpane.vala', + 'newserver.vala', + 'terminalpane.vala', ] footerm_deps = [ dependency('gtk4'), dependency('libadwaita-1'), dependency('vte-2.91-gtk4', version: '>= 0.70.0'), + dependency('json-glib-1.0', version: '>= 1.2.0'), + dependency('libssh2'), ] +subdir('model') + footerm_sources += gnome.compile_resources('footerm-resources', 'footerm.gresource.xml', c_name: 'footerm' diff --git a/src/model/Server.vala b/src/model/Server.vala new file mode 100644 index 0000000..1e4bceb --- /dev/null +++ b/src/model/Server.vala @@ -0,0 +1,35 @@ +/* Server.vala + * + * Copyright 2023 oupson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace FooTerm.Model { + public class Server { + public string hostname; + public ushort port; + public string username; + public string password; + + public Server(string hostname, ushort port, string username, string password) { + this.hostname = hostname; + this.port = port; + this.username = username; + this.password = password; + } + } +} diff --git a/src/model/meson.build b/src/model/meson.build new file mode 100644 index 0000000..933b095 --- /dev/null +++ b/src/model/meson.build @@ -0,0 +1,3 @@ +footerm_sources += files( + 'Server.vala' +) \ No newline at end of file diff --git a/src/newpane.ui b/src/newpane.ui new file mode 100644 index 0000000..219c104 --- /dev/null +++ b/src/newpane.ui @@ -0,0 +1,45 @@ + + + + + + diff --git a/src/newpane.vala b/src/newpane.vala new file mode 100644 index 0000000..c5515d2 --- /dev/null +++ b/src/newpane.vala @@ -0,0 +1,71 @@ +/* newpane.vala + * + * Copyright 2023 oupson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace FooTerm { + [GtkTemplate (ui = "/fr/oupson/FooTerm/newpane.ui")] + public class NewPane : Gtk.Box { + [GtkChild] + private unowned Adw.PreferencesGroup server_list; + + [GtkChild] + private unowned FooTerm.NewServer new_server; + + [GtkChild] + private unowned Gtk.Stack newpane_stack; + + [GtkChild] + private unowned Gtk.Button newpane_add_button; + + public signal void on_server_selected(FooTerm.Model.Server server); + + construct { + this.new_server.on_new_server.connect((s) => { + this.newpane_stack.set_visible_child(server_list.get_parent()); + var action_row = new Adw.ActionRow(); + action_row.set_title(s.hostname); + action_row.set_activatable(true); + action_row.activated.connect(() => { + this.on_server_selected(s); + }); + server_list.add(action_row); + }); + this.newpane_add_button.clicked.connect (() => { + this.newpane_stack.set_visible_child(new_server.get_parent()); + }); + + try { + var config_dir = GLib.File.new_for_path(GLib.Environment.get_user_config_dir()); + var server_files = config_dir.get_child("servers.json"); + + if (server_files.query_exists()) { + var parser = new Json.Parser(); + + parser.load_from_file(server_files.get_path()); + + // TODO + } else { + GLib.debug("Server file does not exist"); + } + } catch (Error e) { + GLib.warning("Failed to read server list : %s", e.message); + } + } + } +} diff --git a/src/newserver.ui b/src/newserver.ui new file mode 100644 index 0000000..a6d8ed7 --- /dev/null +++ b/src/newserver.ui @@ -0,0 +1,50 @@ + + + + diff --git a/src/newserver.vala b/src/newserver.vala new file mode 100644 index 0000000..dd193aa --- /dev/null +++ b/src/newserver.vala @@ -0,0 +1,58 @@ +/* newserver.vala + * + * Copyright 2023 oupson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace FooTerm { + [GtkTemplate (ui = "/fr/oupson/FooTerm/newserver.ui")] + public class NewServer : Gtk.Box { + public signal void on_new_server(FooTerm.Model.Server server); + + [GtkChild] + private unowned Adw.EntryRow hostname_entry; + + [GtkChild] + private unowned Adw.EntryRow port_entry; + + [GtkChild] + private unowned Adw.EntryRow username_entry; + + [GtkChild] + private unowned Adw.PasswordEntryRow password_entry; + + [GtkChild] + private unowned Gtk.Button add_server_button; + + construct { + add_server_button.clicked.connect (this.on_add_button_clicked); + } + + private void on_add_button_clicked() { + var hostname = this.hostname_entry.get_text(); + var port = int.parse(this.port_entry.get_text()); + var username = this.username_entry.get_text(); + var password = this.password_entry.get_text(); + + if (port > 65545 || port < 0) { + // Port is invalid + } + + this.on_new_server(new FooTerm.Model.Server(hostname, (ushort)port, username, password)); + } + } +} diff --git a/src/terminalpane.ui b/src/terminalpane.ui new file mode 100644 index 0000000..008c8cb --- /dev/null +++ b/src/terminalpane.ui @@ -0,0 +1,11 @@ + + + + + diff --git a/src/terminalpane.vala b/src/terminalpane.vala new file mode 100644 index 0000000..b1231d4 --- /dev/null +++ b/src/terminalpane.vala @@ -0,0 +1,190 @@ +/* terminalpane.vala + * + * Copyright 2023 oupson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace FooTerm { + [GtkTemplate (ui = "/fr/oupson/FooTerm/terminalpane.ui")] + public class TerminalPane : Gtk.Box { + [GtkChild] + private unowned Vte.Terminal terminal; + + private SSH2.Session? session; + private SSH2.Channel? channel; + private Socket socket; + private int slave_pty; + + private FooTerm.Model.Server server; + + public TerminalPane(FooTerm.Model.Server server) { + this.server = server; + this.terminal.set_enable_sixel (true); + this.connect_to_server(); + this.terminal.char_size_changed.connect(() => { + int rows = 0; + int columns = 0; + this.terminal.get_pty().get_size(out rows, out columns); + this.channel.request_pty_size(columns, rows); + }); + } + + ~TerminalPane() { + // TODO DESTRUCT + // session.disconnect( "Normal Shutdown, Thank you for playing"); + // session = null; + // Posix.close(sock); + // stdout.printf("all done!\n");) + } + + private void connect_to_server() throws GLib.IOError, GLib.Error { + this.socket = new Socket (SocketFamily.IPV4, SocketType.STREAM, SocketProtocol.TCP); + var addrs = new NetworkAddress(this.server.hostname, this.server.port); + socket.connect(addrs.enumerate().next(), null); + + var sock = socket.get_fd(); // TODO + + this.session = SSH2.Session.create(); + if (session.handshake(sock) != SSH2.Error.NONE) { + stderr.printf("Failure establishing SSH session\n"); + return; + } + + var fingerprint = session.get_host_key_hash(SSH2.HashType.SHA1); + stdout.printf("Fingerprint: "); + for(var i = 0; i < 20; i++) { + stdout.printf("%02X ", fingerprint[i]); + } + stdout.printf("\n"); + + if (session.auth_password(this.server.username, this.server.password) != SSH2.Error.NONE) { + stdout.printf("\tAuthentication by password failed!\n"); + session.disconnect( "Normal Shutdown, Thank you for playing"); + session = null; + Posix.close(sock); + return; + } else { + stdout.printf("\tAuthentication by password succeeded.\n"); + } + + this.channel = null; + if (session.authenticated && (channel = session.open_channel()) == null) { + stderr.printf("Unable to open a session\n"); + } else { + if (channel.request_pty("xterm-256color".data) != SSH2.Error.NONE) { + stderr.printf("Failed requesting pty\n"); + session.disconnect( "Normal Shutdown, Thank you for playing"); + session = null; + Posix.close(sock); + } + + channel.set_env ("TERM", "xterm-256color"); + + if (channel.start_shell() != SSH2.Error.NONE) { + stderr.printf("Unable to request shell on allocated pty\n"); + session.disconnect( "Normal Shutdown, Thank you for playing"); + session = null; + Posix.close(sock); + } + + var master_pty = Posix.posix_openpt(Posix.O_RDWR); + if (master_pty == -1) { + throw GLib.IOError.from_errno (Posix.errno); + } + + var settings = Posix.termios(); + Posix.cfmakeraw (ref settings); + + if (Posix.tcsetattr (master_pty, Posix.TCSANOW, settings) == -1) { + throw GLib.IOError.from_errno (Posix.errno); + } + + if (Posix.grantpt(master_pty) == -1) { + throw GLib.IOError.from_errno (Posix.errno); + } + + if (Posix.unlockpt(master_pty) == -1) { + throw GLib.IOError.from_errno (Posix.errno); + } + + var pts_name = Posix.ptsname(master_pty); + if (pts_name == null) { + throw GLib.IOError.from_errno (Posix.errno); + } + + this.slave_pty = Posix.open(pts_name, Posix.O_RDWR); + if (this.slave_pty < 0) { + throw GLib.IOError.from_errno (Posix.errno); + } + + var vte_pty = new Vte.Pty.foreign_sync(master_pty, null); + this.terminal.set_pty(vte_pty); + + session.blocking = false; + + var sock_channel = new GLib.IOChannel.unix_new(sock); + sock_channel.set_encoding (null); + sock_channel.set_buffered (false); + sock_channel.set_close_on_unref (false); + + var slave_channel = new GLib.IOChannel.unix_new(slave_pty); + slave_channel.set_encoding (null); + slave_channel.set_buffered (false); + + sock_channel.add_watch (GLib.IOCondition.IN, (source, condition) => { + if (condition == IOCondition.HUP) { + print ("The connection has been broken.\n"); + return false; + } + + try { + var buffer = new uint8[1024]; + var size = this.channel.read(buffer); + + if (Posix.write(this.slave_pty, buffer, size) < 0) { + throw GLib.IOError.from_errno(Posix.errno); + } + + return true; + } catch(Error e) { + GLib.warning("Failed to read from ssh : %s", e.message); + return false; + } + }); + + slave_channel.add_watch (GLib.IOCondition.IN, (source, condition) => { + if (condition == IOCondition.HUP) { + print ("The connection has been broken.\n"); + return false; + } + + try { + var buffer = new char[1024]; + size_t size = 0; + source.read_chars(buffer, out size); + + this.channel.write ((uint8[])buffer[0:size]); + return true; + } catch (Error e) { + GLib.warning("Failed to read from terminal : %s", e.message); + return false; + } + }); + } + } + } +} diff --git a/src/window.ui b/src/window.ui index ba00f0d..4069947 100644 --- a/src/window.ui +++ b/src/window.ui @@ -2,7 +2,6 @@ -