Skip to content

Instantly share code, notes, and snippets.

@andyholmes
Created September 18, 2017 18:07
Show Gist options
  • Save andyholmes/159a6fb628dcc926518d1b129e345f47 to your computer and use it in GitHub Desktop.
Save andyholmes/159a6fb628dcc926518d1b129e345f47 to your computer and use it in GitHub Desktop.
KDEConnect Device Channel in GJS
"use strict";
// Imports
const Lang = imports.lang;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
/** DeviceChannel */
var DeviceChannel = new Lang.Class({
Name: "JSConnectDeviceChannel",
Extends: GObject.Object,
Signals: {
"connected": {
flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED
},
"disconnected": {
flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED
},
"received": {
flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED,
param_types: [ GObject.TYPE_OBJECT ]
}
},
_init: function (device) {
this.parent();
this.device = device;
this._connection = null;
this._in = null;
this._out = null;
this._socket = null;
this._monitor = 0;
this._peer_cert = null;
},
open: function () {
log("DeviceChannel.open(" + this.device.id + ")");
this._socket = new Gio.Socket({
family: Gio.SocketFamily.IPV4,
type: Gio.SocketType.STREAM,
protocol: Gio.SocketProtocol.TCP
});
// Setup socket
this._socket.init(null);
this._socket.set_keepalive(true);
this._socket.set_option(6, 4, 10); // TCP_KEEPIDLE
this._socket.set_option(6, 5, 5); // TCP_KEEPINTVL
this._socket.set_option(6, 6, 3); // TCP_KEEPCNT
let tcpConnection = new Gio.TcpConnection({ socket: this._socket });
tcpConnection.connect_async(
new Gio.InetSocketAddress({
address: Gio.InetAddress.new_from_string(
this.device.identity.body.tcpHost
),
port: this.device.identity.body.tcpPort
}),
null,
Lang.bind(this, this.opened)
);
},
opened: function (tcpConnection, res) {
log("DeviceChannel.opened(" + this.device.id + ")");
try {
if (!tcpConnection.connect_finish(res)) {
throw Error("failed to connect");
}
} catch (e) {
log("Error finishing connection: " + e);
this.emit("disconnected"); // device will call this.close()
return;
}
// Send identity packet, indicating we're about ready for a handshake
let ident = new Packet.IdentityPacket(this.device.daemon);
let _out = new Gio.DataOutputStream({
base_stream: new Gio.UnixOutputStream({
fd: this._socket.fd,
close_fd: false
})
});
_out.put_string(ident.toData(), null);
_out.close(null);
// Wrap the TcpConnection in TlsServerConnection and handshake
this._connection = Gio.TlsServerConnection.new(
tcpConnection,
this.device.daemon.certificate
);
this._connection.validation_flags = 0;
this._connection.authentication_mode = 1;
// Handle certificate
this._connection.connect("accept-certificate", (conn, peer_cert, flags) => {
if (this.device.paired) {
log("verifying certificate...");
let paired_cert = Gio.TlsCertificate.new_from_file(
this.device.config_cert // Locally certificate generated with openssl, as in KDE Connect code
);
return (paired_cert.verify(null, peer_cert) === 0);
} else {
log("delaying certificate verification...");
this._peer_cert = peer_cert;
return true;
}
});
try {
this._connection.handshake(null);
} catch (e) {
log("Error during TLS handshake: " + e);
this.emit("disconnected"); // device will call this.close()
return false;
}
// Open In/Out Streams
this._in = new Gio.DataInputStream({
base_stream: this._connection.input_stream,
newline_type: Gio.DataStreamNewlineType.LF
});
this._out = new Gio.DataOutputStream({
base_stream: this._connection.output_stream
});
// Listen for packets
this._monitor = this._in.base_stream.create_source(null);
this._monitor.set_callback((condition) => {
let result = this.receive();
if (!result) { this.emit("disconnected"); } // device will call this.close()
return result;
});
this._monitor.attach(null);
this.emit("connected");
return true;
},
close: function () {
log("DeviceChannel.close(" + this.device.id + ")");
if (this._monitor > 0) {
Gio.Source.remove(this._monitor);
this._monitor = 0;
}
try {
if (this._in !== null) {
this._in.close(null);
}
} catch (e) {
log("error closing data input: " + e);
}
try {
if (this._out !== null) {
this._out.close(null);
}
} catch (e) {
log("error closing data output: " + e);
}
try {
if (this._connection !== null) {
this._connection.close(null);
}
} catch (e) {
log("error closing connection: " + e);
}
try {
if (this._socket !== null) {
this._socket.close(null);
}
} catch (e) {
log("error closing socket: " + e);
}
this._in = null;
this._out = null;
this._connection = null;
this._socket = null;
},
send: function (packet) {
log("DeviceChannel.send(" + packet.toString() + ")");
try {
this._out.put_string(packet.toData(), null);
} catch (e) {
log("error sending packet: " + e);
// TODO: disconnect? check kdeconnect code
}
},
receive: function () {
log("DeviceChannel.receive(" + this.device.id + ")");
let data, len, packet;
try {
[data, len] = this._in.read_line(null);
} catch (e) {
log("Failed to receive packet: " + e);
this.emit("disconnected");
}
if (data === null || data === undefined || !data.length) {
return false;
}
packet = new Packet.BasePacket(data);
this.emit("received", packet); // device will handle packet
return true;
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment