Initial support for ssh

This commit is contained in:
oupson 2023-02-26 13:34:27 +01:00
parent 0b8689ef6c
commit dba7b10698
Signed by: oupson
GPG Key ID: 3BD88615552EFCB7
17 changed files with 533 additions and 19 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "bindings/vapi"]
path = bindings/vapi
url = https://gitlab.gnome.org/GNOME/vala-extra-vapis.git

1
bindings/vapi Submodule

@ -0,0 +1 @@
Subproject commit 9ce7531a398c3ba629c89aad6ba43fb73c6060b6

View File

@ -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,

View File

@ -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')

View File

@ -2,6 +2,9 @@
<gresources>
<gresource prefix="/fr/oupson/FooTerm">
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">newpane.ui</file>
<file preprocess="xml-stripblanks">terminalpane.ui</file>
<file preprocess="xml-stripblanks">newserver.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource>
</gresources>

View File

@ -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;
}

View File

@ -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'

35
src/model/Server.vala Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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;
}
}
}

3
src/model/meson.build Normal file
View File

@ -0,0 +1,3 @@
footerm_sources += files(
'Server.vala'
)

45
src/newpane.ui Normal file
View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="Adw" version="1.0"/>
<requires lib="gtk" version="4.0"/>
<template class="FooTermNewPane" parent="GtkBox">
<child>
<object class="GtkStack" id="newpane_stack">
<child>
<object class="AdwClamp">
<child>
<object class="AdwPreferencesGroup" id="server_list">
<property name="margin-bottom">32</property>
<property name="margin-top">32</property>
<property name="margin-start">32</property>
<property name="margin-end">32</property>
<property name="title" translatable="yes">Servers</property>
<child type="header-suffix">
<object class="GtkButton" id="newpane_add_button">
<property name="valign">center</property>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">list-add-symbolic</property>
<property name="label" translatable="yes">Add new</property>
</object>
</property>
<style>
<class name="flat"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwClamp">
<child>
<object class="FooTermNewServer" id="new_server"></object>
</child>
</object>
</child>"
</object>
</child>
</template>
</interface>

71
src/newpane.vala Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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);
}
}
}
}

50
src/newserver.ui Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="FooTermNewServer" parent="GtkBox">
<property name="margin-bottom">32</property>
<property name="margin-top">32</property>
<property name="margin-start">32</property>
<property name="margin-end">32</property>
<property name="spacing">12</property>
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwEntryRow" id="hostname_entry">
<property name="title" translatable="yes">Hostname</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="port_entry">
<property name="title" translatable="yes">Port</property>
<property name="input-purpose">number</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwEntryRow" id="username_entry">
<property name="title" translatable="yes">Username</property>
</object>
</child>
<child>
<object class="AdwPasswordEntryRow" id="password_entry">
<property name="title" translatable="yes">Password</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="add_server_button">
<property name="label" translatable="yes">Add</property>
<property name="margin-top">12</property>
<property name="halign">end</property>
<style>
<class name="suggested-action" />
</style>
</object>
</child>
</template>
</interface>

58
src/newserver.vala Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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));
}
}
}

11
src/terminalpane.ui Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="vte-2.91-gtk4" version="0.70.0"/>
<template class="FooTermTerminalPane" parent="GtkBox">
<child>
<object class="VteTerminal" id="terminal">
<property name="hexpand">true</property>
</object>
</child>
</template>
</interface>

190
src/terminalpane.vala Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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<bool>? 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<bool>();
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;
}
});
}
}
}
}

View File

@ -2,7 +2,6 @@
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="Adw" version="1.0"/>
<requires lib="vte-2.91-gtk4" version="0.70.0"/>
<template class="FootermWindow" parent="AdwApplicationWindow">
<property name="default-width">600</property>
<property name="default-height">300</property>
@ -20,8 +19,8 @@
</object>
</child>
<child>
<object class="VteTerminal" id="terminal">
<property name="hexpand">true</property>
<object class="GtkBox" id="pane_content">
</object>
</child>
</object>

View File

@ -22,22 +22,21 @@ namespace Footerm {
[GtkTemplate (ui = "/fr/oupson/FooTerm/window.ui")]
public class Window : Adw.ApplicationWindow {
[GtkChild]
private unowned Vte.Terminal terminal;
private unowned Gtk.Box pane_content;
public Window (Gtk.Application app) {
Object (application: app);
}
construct {
this.terminal.set_enable_sixel (true);
this.terminal.spawn_async (Vte.PtyFlags.NO_CTTY, null, {"flatpak-spawn", "--host", "bash"}, null, 0, null, -1, null, this.on_spawn_finished);
}
private void on_spawn_finished (Vte.Terminal t, Pid _pid, GLib.Error? error) {
if (error != null) {
warning ("%s", error.message);
}
var new_pane = new FooTerm.NewPane();
ulong handler_id;
handler_id = new_pane.on_server_selected.connect((s) => {
new_pane.disconnect (handler_id);
pane_content.remove(new_pane);
pane_content.append(new FooTerm.TerminalPane(s));
});
pane_content.append(new_pane);
}
}
}