Add some configs from generic Ubuntu

This commit is contained in:
2020-05-11 05:16:27 -04:00
parent f32c1048d1
commit 754f64f135
16037 changed files with 205635 additions and 137 deletions

View File

@@ -0,0 +1,125 @@
'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
/**
* Creates a GTlsCertificate from the PEM-encoded data in @cert_path and
* @key_path. If either are missing a new pair will be generated.
*
* Additionally, the private key will be added using ssh-add to allow sftp
* connections using Gio.
*
* @param {string} certPath - Absolute path to a x509 certificate in PEM format
* @param {string} keyPath - Absolute path to a private key in PEM format
* @param {string} commonName - A unique common name for the certificate
*
* See: https://github.com/KDE/kdeconnect-kde/blob/master/core/kdeconnectconfig.cpp#L119
*/
Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = null) {
// Check if the certificate/key pair already exists
let certExists = GLib.file_test(certPath, GLib.FileTest.EXISTS);
let keyExists = GLib.file_test(keyPath, GLib.FileTest.EXISTS);
// Create a new certificate and private key if necessary
if (!certExists || !keyExists) {
// If we weren't passed a common name, generate a random one
if (!commonName) {
commonName = GLib.uuid_string_random();
}
let proc = new Gio.Subprocess({
argv: [
gsconnect.metadata.bin.openssl, 'req',
'-new', '-x509', '-sha256',
'-out', certPath,
'-newkey', 'rsa:4096', '-nodes',
'-keyout', keyPath,
'-days', '3650',
'-subj', `/O=andyholmes.github.io/OU=GSConnect/CN=${commonName}`
],
flags: (Gio.SubprocessFlags.STDOUT_SILENCE |
Gio.SubprocessFlags.STDERR_SILENCE)
});
proc.init(null);
proc.wait_check(null);
}
return Gio.TlsCertificate.new_from_files(certPath, keyPath);
};
Object.defineProperties(Gio.TlsCertificate.prototype, {
/**
* Compute a SHA1 fingerprint of the certificate.
* See: https://gitlab.gnome.org/GNOME/glib/issues/1290
*
* @return {string} - A SHA1 fingerprint of the certificate.
*/
'fingerprint': {
value: function() {
if (!this.__fingerprint) {
let proc = new Gio.Subprocess({
argv: [gsconnect.metadata.bin.openssl, 'x509', '-noout', '-fingerprint', '-sha1', '-inform', 'pem'],
flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE
});
proc.init(null);
let stdout = proc.communicate_utf8(this.certificate_pem, null)[1];
this.__fingerprint = /[a-zA-Z0-9:]{59}/.exec(stdout)[0];
proc.wait_check(null);
}
return this.__fingerprint;
},
enumerable: false
},
/**
* The common name of the certificate.
*/
'common_name': {
get: function() {
if (!this.__common_name) {
let proc = new Gio.Subprocess({
argv: [gsconnect.metadata.bin.openssl, 'x509', '-noout', '-subject', '-inform', 'pem'],
flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE
});
proc.init(null);
let stdout = proc.communicate_utf8(this.certificate_pem, null)[1];
this.__common_name = /(?:cn|CN) ?= ?([^,\n]*)/.exec(stdout)[1];
proc.wait_check(null);
}
return this.__common_name;
},
enumerable: true
},
/**
* The common name of the certificate.
*/
'certificate_der': {
get: function() {
if (!this.__certificate_der) {
let proc = new Gio.Subprocess({
argv: [gsconnect.metadata.bin.openssl, 'x509', '-outform', 'der', '-inform', 'pem'],
flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE
});
proc.init(null);
let stdout = proc.communicate(new GLib.Bytes(this.certificate_pem), null)[1];
this.__certificate_der = stdout.toArray();
proc.wait_check(null);
}
return this.__certificate_der;
},
enumerable: true
}
});

View File

@@ -0,0 +1,516 @@
'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
/**
* Packet
*
* The packet class is a simple Object-derived class. It only exists to offer
* conveniences for coercing to a string writable to a channel and constructing
* from Strings and Objects. In future, it could probably be optimized to avoid
* excessive shape-trees since it's the most common object in the protocol.
*/
var Packet = class Packet {
constructor(data = null) {
this.id = 0;
this.type = undefined;
this.body = {};
if (typeof data === 'string') {
this.fromString(data);
} else {
this.fromObject(data);
}
}
[Symbol.toPrimitive](hint) {
this.id = Date.now();
switch (hint) {
case 'string':
return `${JSON.stringify(this)}\n`;
case 'number':
return `${JSON.stringify(this)}\n`.length;
default:
return true;
}
}
/**
* Update the packet from an Object, using and intermediate call to
* JSON.stringify() to deep-copy the object, avoiding reference entanglement
*
* @param {string} data - An object
*/
fromObject(data) {
try {
let json = JSON.parse(JSON.stringify(data));
Object.assign(this, json);
} catch (e) {
throw Error(`Malformed packet: ${e.message}`);
}
}
/**
* Update the packet from a string of JSON
*
* @param {string} data - A string of text
*/
fromString(data) {
try {
let json = JSON.parse(data);
Object.assign(this, json);
} catch (e) {
throw Error(`Malformed packet: ${e.message}`);
}
}
/**
* Make a deep copy of the packet, using and intermediate call to
* JSON.stringify() to avoid reference entanglement.
*
* @return {Core.Packet} - A new packet
*/
toObject() {
try {
let data = JSON.stringify(this);
return new Packet(data);
} catch (e) {
throw Error(`Malformed packet: ${e.message}`);
}
}
/**
* Serialize the packet as a single line with a terminating new-line (\n)
* character, ready to be written to a channel.
*
* @return {string} - A serialized packet
*/
toString() {
return `${this}`;
}
/**
* Check if the packet has a payload.
*
* @returns (boolean} - %true if @packet has a payload
*/
hasPayload() {
if (!this.hasOwnProperty('payloadSize'))
return false;
if (!this.hasOwnProperty('payloadTransferInfo'))
return false;
return (Object.keys(this.payloadTransferInfo).length > 0);
}
};
/**
* Channel
*
* Channels are essentially wrappers around an I/O stream pair that handle KDE
* Connect identity exchange and either packet or data exchange.
*
* There are effectively two types of channels: packet exchange channels and
* data transfer channels. Both channel types begin by exchanging identity
* packets and then performing whatever encryption or authentication is
* appropriate for the transport protocol.
*
*
* Packet Channels
*
* Packet exchange channels are used to send or receive packets, which are JSON
* objects serialized as single line with a terminating new-line character
* marking the end of the packet. The only packet type allowed to be exchanged
* before authentication is `kdeconnect.identity`. The only packets allowed
* before pairing are `kdeconnect.identity` and `kdeconnect.pair`.
*
*
* Transfer Channels
*
* Data transfer channels are used to send or receive streams of binary data and
* are only possible for paired and authenticated devices. Once the
* identification and authentication has completed, the binary payload is read
* or written and then the channel is closed (unless cancelled first).
*
* These channels are opened when the uploading party sends a packet with two
* extra fields in the top-level of the packet: `payloadSize` (size in bytes)
* and `payloadTransferInfo` which contains protocol specific information such
* as a TCP port. The uploading party then waits for an incoming connection that
* corresponds with the `payloadTransferInfo` field.
*/
var Channel = GObject.registerClass({
GTypeName: 'GSConnectChannel',
Requires: [GObject.Object]
}, class Channel extends GObject.Interface {
get address() {
throw new GObject.NotImplementedError();
}
get backend() {
return this._backend || null;
}
set backend(backend) {
this._backend = backend;
}
get cancellable() {
if (this._cancellable === undefined) {
this._cancellable = new Gio.Cancellable();
}
return this._cancellable;
}
get input_stream() {
if (this._input_stream === undefined) {
if (this._connection instanceof Gio.IOStream) {
return this._connection.get_input_stream();
}
return null;
}
return this._input_stream;
}
set input_stream(stream) {
this._input_stream = stream;
}
get output_queue() {
if (this._output_queue === undefined) {
this._output_queue = [];
}
return this._output_queue;
}
get output_stream() {
if (this._output_stream === undefined) {
if (this._connection instanceof Gio.IOStream) {
return this._connection.get_output_stream();
}
return null;
}
return this._output_stream;
}
set output_stream(stream) {
this._output_stream = stream;
}
get service() {
if (this._service === undefined) {
this._service = Gio.Application.get_default();
}
return this._service;
}
get uuid() {
if (this._uuid === undefined) {
this._uuid = GLib.uuid_string_random();
}
return this._uuid;
}
set uuid(uuid) {
this._uuid = uuid;
}
/**
* Override these to send and receive the identity packet during initial
* connection negotiation.
*/
_receiveIdent(connection) {
throw new GObject.NotImplementedError();
}
_sendIdent(connection) {
throw new GObject.NotImplementedError();
}
accept(connection) {
throw new GObject.NotImplementedError();
}
open(connection) {
throw new GObject.NotImplementedError();
}
/**
* Attach to @device as the default channel used for packet exchange. This
* should connect the channel's Gio.Cancellable to mark the device as
* disconnected, setup the IO streams, start the receive() loop and set the
* device as connected.
*
* @param {Device.Device} device - The device to attach to
*/
attach(device) {
throw new GObject.NotImplementedError();
}
/**
* Close all streams associated with this channel, silencing any errors
*/
close() {
throw new GObject.NotImplementedError();
}
/**
* Receive a packet from the channel and call receivePacket() on the device
*
* @param {Device.Device} device - The device which will handle the packet
*/
receive(device) {
this.input_stream.read_line_async(
GLib.PRIORITY_DEFAULT,
this.cancellable,
(stream, res) => {
try {
let data = stream.read_line_finish_utf8(res)[0];
if (data === null) {
throw new Gio.IOErrorEnum({
message: 'End of stream',
code: Gio.IOErrorEnum.CONNECTION_CLOSED
});
}
// Queue another receive() before handling the packet
this.receive(device);
// Malformed packets aren't fatal
try {
let packet = new Packet(data);
debug(packet, device.name);
device.receivePacket(packet);
} catch (e) {
debug(e, device.name);
}
} catch (e) {
if (!e.code || e.code !== Gio.IOErrorEnum.CANCELLED) {
debug(e, device.name);
}
this.close();
}
}
);
}
/**
* Send a packet to a device
*
* @param {object} packet - An dictionary of packet data
*/
async send(packet) {
let next;
try {
this.output_queue.push(new Packet(packet));
if (!this.__lock) {
this.__lock = true;
while ((next = this.output_queue.shift())) {
await new Promise((resolve, reject) => {
this.output_stream.write_all_async(
next.toString(),
GLib.PRIORITY_DEFAULT,
this.cancellable,
(stream, res) => {
try {
resolve(stream.write_all_finish(res));
} catch (e) {
reject(e);
}
}
);
});
debug(next, this.identity.body.deviceName);
}
this.__lock = false;
}
} catch (e) {
debug(e, this.identity.body.deviceName);
this.close();
}
}
/**
* Override these in subclasses to negotiate payload transfers. `download()`
* and `upload()` should cleanup after themselves and return a success
* boolean.
*
* The default implementation will always report failure, for protocols that
* won't or don't yet support payload transfers.
*/
createTransfer(params) {
throw new GObject.NotImplementedError();
}
async download() {
let result = false;
try {
throw new GObject.NotImplementedError();
} catch (e) {
debug(e, this.identity.body.deviceName);
} finally {
this.close();
}
return result;
}
async upload() {
let result = false;
try {
throw new GObject.NotImplementedError();
} catch (e) {
debug(e, this.identity.body.deviceName);
} finally {
this.close();
}
return result;
}
/**
* Transfer using g_output_stream_splice()
*
* @return {Boolean} - %true on success, %false on failure.
*/
async transfer() {
let result = false;
try {
result = await new Promise((resolve, reject) => {
this.output_stream.splice_async(
this.input_stream,
Gio.OutputStreamSpliceFlags.NONE,
GLib.PRIORITY_DEFAULT,
this.cancellable,
(source, res) => {
try {
if (source.splice_finish(res) < this.size) {
throw new Error('incomplete data');
}
resolve(true);
} catch (e) {
reject(e);
}
}
);
});
} catch (e) {
debug(e, this.device.name);
} finally {
this.close();
}
return result;
}
});
/**
* ChannelService
*/
var ChannelService = GObject.registerClass({
GTypeName: 'GSConnectChannelService',
Requires: [GObject.Object],
Properties: {
'name': GObject.ParamSpec.string(
'name',
'Name',
'The name of the backend',
GObject.ParamFlags.READABLE,
null
)
},
Signals: {
'channel': {
flags: GObject.SignalFlags.RUN_LAST,
param_types: [Channel.$gtype],
return_type: GObject.TYPE_BOOLEAN
},
}
}, class ChannelService extends GObject.Interface {
get name() {
throw new GObject.NotImplementedError();
}
get service() {
if (this._service === undefined) {
this._service = Gio.Application.get_default();
}
return this._service;
}
/**
* Broadcast directly to @address or the whole network if %null
*
* @param {string} [address] - A string address
*/
broadcast(address = null) {
throw new GObject.NotImplementedError();
}
/**
* Emit Core.ChannelService::channel
*
* @param {Core.Channel} channel - The new channel
*/
channel(channel) {
try {
if (!this.emit('channel', channel)) {
channel.close();
}
} catch (e) {
logError(e);
}
}
/**
* Start the channel service. Implementations should throw an error if the
* service fails to meet any of its requirements for opening or accepting
* connections.
*/
start() {
throw new GObject.NotImplementedError();
}
/**
* Stop the channel service.
*/
stop() {
throw new GObject.NotImplementedError();
}
/**
* Destroy the channel service.
*/
destroy() {
}
});

View File

@@ -0,0 +1,998 @@
'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Core = imports.service.protocol.core;
/**
* TCP Port Constants
*/
const DEFAULT_PORT = 1716;
const TRANSFER_MIN = 1739;
const TRANSFER_MAX = 1764;
/**
* One-time check for Linux/FreeBSD socket options
*/
var _LINUX_SOCKETS = false;
try {
// This should throw on FreeBSD
// https://github.com/freebsd/freebsd/blob/master/sys/netinet/tcp.h#L159
new Gio.Socket({
family: Gio.SocketFamily.IPV4,
protocol: Gio.SocketProtocol.TCP,
type: Gio.SocketType.STREAM
}).get_option(6, 5);
// Otherwise we can use Linux socket options
_LINUX_SOCKETS = true;
} catch (e) {
_LINUX_SOCKETS = false;
}
/**
* Lan.ChannelService consists of two parts.
*
* The TCP Listener listens on a port and constructs a Channel object from the
* incoming Gio.TcpConnection.
*
* The UDP Listener listens on a port for incoming JSON identity packets which
* include the TCP port for connections, while the IP address is taken from the
* UDP packet itself. We respond to incoming packets by opening a TCP connection
* and broadcast outgoing packets to 255.255.255.255.
*/
var ChannelService = GObject.registerClass({
GTypeName: 'GSConnectLanChannelService',
Implements: [Core.ChannelService],
Properties: {
'name': GObject.ParamSpec.override('name', Core.ChannelService),
'port': GObject.ParamSpec.uint(
'port',
'Port',
'The port used by the service',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
0, GLib.MAXUINT16,
DEFAULT_PORT
)
}
}, class LanChannelService extends GObject.Object {
_init(params) {
super._init(params);
// Track hosts we identify to directly, allowing them to ignore the
// discoverable state of the service.
this._allowed = new Set();
//
this._tcp = null;
this._udp4 = null;
this._udp6 = null;
// Monitor network status
this._networkMonitor = Gio.NetworkMonitor.get_default();
this._networkAvailable = false;
this._networkChangedId = 0;
}
get certificate() {
return this._certificate;
}
get channels() {
if (this._channels === undefined) {
this._channels = new Map();
}
return this._channels;
}
get name() {
return 'lan';
}
get port() {
if (this._port === undefined) {
this._port = DEFAULT_PORT;
}
return this._port;
}
set port(port) {
if (this.port !== port) {
this._port = port;
this.notify('port');
}
}
_onNetworkChanged(monitor, network_available) {
if (this._networkAvailable !== network_available) {
this._networkAvailable = network_available;
this.broadcast();
}
}
_initCertificate() {
let certPath = GLib.build_filenamev([
gsconnect.configdir,
'certificate.pem'
]);
let keyPath = GLib.build_filenamev([
gsconnect.configdir,
'private.pem'
]);
// Ensure a certificate exists with our id as the common name
try {
this._certificate = Gio.TlsCertificate.new_for_paths(
certPath,
keyPath,
this.service.id
);
} catch (e) {
e.name = 'CertificateError';
throw e;
}
// If the service id doesn't match the common name, this is probably a
// certificate from an earlier version and we need to set it now
if (this.service.settings.get_string('id') !== this._certificate.common_name) {
this.service.settings.set_string('id', this._certificate.common_name);
}
}
_initTcpListener() {
this._tcp = new Gio.SocketService();
this._tcp.add_inet_port(this.port, null);
this._tcp.connect('incoming', this._onIncomingChannel.bind(this));
}
async _onIncomingChannel(listener, connection) {
try {
let host = connection.get_remote_address().address.to_string();
// Decide whether we should try to accept this connection
if (!this._allowed.has(host) && !this.service.discoverable) {
connection.close_async(0, null, null);
return;
}
// Create a channel
let channel = new Channel({
backend: this,
certificate: this.certificate,
host: host,
port: DEFAULT_PORT
});
// Accept the connection
await channel.accept(connection);
channel.identity.body.tcpHost = channel.host;
channel.identity.body.tcpPort = DEFAULT_PORT;
this.channel(channel);
} catch (e) {
debug(e);
}
}
_initUdpListener() {
// Default broadcast address
this._udp_address = Gio.InetSocketAddress.new_from_string(
'255.255.255.255',
this.port
);
try {
this._udp6 = new Gio.Socket({
family: Gio.SocketFamily.IPV6,
type: Gio.SocketType.DATAGRAM,
protocol: Gio.SocketProtocol.UDP,
broadcast: true
});
this._udp6.init(null);
// Bind the socket
let inetAddr = Gio.InetAddress.new_any(Gio.SocketFamily.IPV6);
let sockAddr = Gio.InetSocketAddress.new(inetAddr, this.port);
this._udp6.bind(sockAddr, false);
// Input stream
this._udp6_stream = new Gio.DataInputStream({
base_stream: new Gio.UnixInputStream({
fd: this._udp6.fd,
close_fd: false
})
});
// Watch socket for incoming packets
this._udp6_source = this._udp6.create_source(GLib.IOCondition.IN, null);
this._udp6_source.set_callback(this._onIncomingIdentity.bind(this, this._udp6));
this._udp6_source.attach(null);
} catch (e) {
this._udp6 = null;
}
// Our IPv6 socket also supports IPv4; we're all done
if (this._udp6 && this._udp6.speaks_ipv4()) {
this._udp4 = null;
return;
}
try {
this._udp4 = new Gio.Socket({
family: Gio.SocketFamily.IPV4,
type: Gio.SocketType.DATAGRAM,
protocol: Gio.SocketProtocol.UDP,
broadcast: true
});
this._udp4.init(null);
// Bind the socket
let inetAddr = Gio.InetAddress.new_any(Gio.SocketFamily.IPV4);
let sockAddr = Gio.InetSocketAddress.new(inetAddr, this.port);
this._udp4.bind(sockAddr, false);
// Input stream
this._udp4_stream = new Gio.DataInputStream({
base_stream: new Gio.UnixInputStream({
fd: this._udp4.fd,
close_fd: false
})
});
// Watch input socket for incoming packets
this._udp4_source = this._udp4.create_source(GLib.IOCondition.IN, null);
this._udp4_source.set_callback(this._onIncomingIdentity.bind(this, this._udp4));
this._udp4_source.attach(null);
} catch (e) {
this._udp4 = null;
// We failed to get either an IPv4 or IPv6 socket to bind
if (this._udp6 === null) {
e.name = 'LanError';
throw e;
}
}
}
_onIncomingIdentity(socket) {
let host, data, packet;
// Try to peek the remote address
try {
host = socket.receive_message(
[],
Gio.SocketMsgFlags.PEEK,
null
)[1].address.to_string();
} catch (e) {
logError(e);
}
// Whether or not we peeked the address, we need to read the packet
try {
if (socket === this._udp6) {
data = this._udp6_stream.read_line_utf8(null)[0];
} else {
data = this._udp4_stream.read_line_utf8(null)[0];
}
// Only process the packet if we succeeded in peeking the address
if (host !== undefined) {
packet = new Core.Packet(data);
packet.body.tcpHost = host;
this._onIdentity(packet);
}
} catch (e) {
logError(e);
}
return GLib.SOURCE_CONTINUE;
}
async _onIdentity(packet) {
try {
// Bail if the deviceId is missing
if (!packet.body.hasOwnProperty('deviceId')) {
debug(`${packet.body.deviceName}: missing deviceId`);
return;
}
// Silently ignore our own broadcasts
if (packet.body.deviceId === this.service.identity.body.deviceId) {
return;
}
debug(packet);
// Create a new channel
let channel = new Channel({
backend: this,
certificate: this.certificate,
host: packet.body.tcpHost,
port: packet.body.tcpPort,
identity: packet
});
// Check if channel is already open with this address
if (this.channels.has(channel.address)) {
return;
} else {
this.channels.set(channel.address, channel);
}
// Open a TCP connection
let connection = await new Promise((resolve, reject) => {
let address = Gio.InetSocketAddress.new_from_string(
packet.body.tcpHost,
packet.body.tcpPort
);
let client = new Gio.SocketClient({enable_proxy: false});
client.connect_async(address, null, (client, res) => {
try {
resolve(client.connect_finish(res));
} catch (e) {
reject(e);
}
});
});
// Connect the channel and attach it to the device on success
await channel.open(connection);
this.channel(channel);
} catch (e) {
logError(e);
}
}
/**
* Broadcast an identity packet
*
* If @address is not %null it may specify an IPv4 or IPv6 address to send
* the identity packet directly to, otherwise it will be broadcast to the
* default address, 255.255.255.255.
*
* @param {string} [address] - An optional target IPv4 or IPv6 address
*/
broadcast(address = null) {
try {
if (!this._networkAvailable) {
return;
}
// Try to parse strings as <host>:<port>
if (typeof address === 'string') {
let [host, port] = address.split(':');
port = parseInt(port) || DEFAULT_PORT;
address = Gio.InetSocketAddress.new_from_string(host, port);
}
// If we succeed, remember this host
if (address instanceof Gio.InetSocketAddress) {
this._allowed.add(address.address.to_string());
// Broadcast to the network if no address is specified
} else {
debug('Broadcasting to LAN');
address = this._udp_address;
}
// Set the tcpPort before broadcasting
this.service.identity.body.tcpPort = this.port;
if (this._udp6 !== null) {
this._udp6.send_to(address, `${this.service.identity}`, null);
}
if (this._udp4 !== null) {
this._udp4.send_to(address, `${this.service.identity}`, null);
}
} catch (e) {
debug(e, address);
} finally {
this.service.identity.body.tcpPort = undefined;
}
}
start() {
// Ensure a certificate exists
this._initCertificate();
// Start TCP/UDP listeners
if (this._udp4 === null && this._udp6 === null) {
this._initUdpListener();
}
if (this._tcp === null) {
this._initTcpListener();
}
// Monitor network changes
if (this._networkChangedId === 0) {
this._networkAvailable = this._networkMonitor.network_available;
this._networkChangedId = this._networkMonitor.connect(
'network-changed',
this._onNetworkChanged.bind(this)
);
}
}
stop() {
if (this._networkChangedId) {
this._networkMonitor.disconnect(this._networkChangedId);
this._networkChangedId = 0;
this._networkAvailable = false;
}
if (this._tcp !== null) {
this._tcp.stop();
this._tcp.close();
this._tcp = null;
}
if (this._udp6 !== null) {
this._udp6_source.destroy();
this._udp6_stream.close(null);
this._udp6.close();
this._udp6 = null;
}
if (this._udp4 !== null) {
this._udp4_source.destroy();
this._udp4_stream.close(null);
this._udp4.close();
this._udp4 = null;
}
}
destroy() {
try {
this.stop();
} catch (e) {
debug(e);
}
}
});
/**
* Lan Channel
*
* This class essentially just extends Core.Channel to set TCP socket options
* and negotiate TLS encrypted connections.
*/
var Channel = GObject.registerClass({
GTypeName: 'GSConnectLanChannel',
Implements: [Core.Channel]
}, class LanChannel extends GObject.Object {
_init(params) {
super._init();
Object.assign(this, params);
}
get address() {
return `lan://${this.host}:${this.port}`;
}
get certificate() {
return this._certificate || null;
}
set certificate(certificate) {
this._certificate = certificate;
}
get peer_certificate() {
if (this._connection instanceof Gio.TlsConnection) {
return this._connection.get_peer_certificate();
}
return null;
}
get host() {
if (this._host === undefined) {
this._host = null;
}
return this._host;
}
set host(host) {
this._host = host;
}
get port() {
if (this._port === undefined) {
if (this.identity && this.identity.body.tcpPort) {
this._port = this.identity.body.tcpPort;
} else {
return DEFAULT_PORT;
}
}
return this._port;
}
set port(port) {
this._port = port;
}
_initSocket(connection) {
connection.socket.set_keepalive(true);
if (_LINUX_SOCKETS) {
connection.socket.set_option(6, 4, 10); // TCP_KEEPIDLE
connection.socket.set_option(6, 5, 5); // TCP_KEEPINTVL
connection.socket.set_option(6, 6, 3); // TCP_KEEPCNT
} else {
connection.socket.set_option(6, 256, 10); // TCP_KEEPIDLE
connection.socket.set_option(6, 512, 5); // TCP_KEEPINTVL
connection.socket.set_option(6, 1024, 3); // TCP_KEEPCNT
}
return connection;
}
/**
* Handshake Gio.TlsConnection
*/
_handshake(connection) {
return new Promise((resolve, reject) => {
connection.validation_flags = Gio.TlsCertificateFlags.EXPIRED;
connection.authentication_mode = Gio.TlsAuthenticationMode.REQUIRED;
connection.handshake_async(
GLib.PRIORITY_DEFAULT,
this.cancellable,
(connection, res) => {
try {
resolve(connection.handshake_finish(res));
} catch (e) {
reject(e);
}
}
);
});
}
async _authenticate(connection) {
// Standard TLS Handshake
await this._handshake(connection);
// Get a settings object for the device
let settings;
if (this.device) {
settings = this.device.settings;
} else {
let id = this.identity.body.deviceId;
settings = new Gio.Settings({
settings_schema: gsconnect.gschema.lookup(
'org.gnome.Shell.Extensions.GSConnect.Device',
true
),
path: `/org/gnome/shell/extensions/gsconnect/device/${id}/`
});
}
// If we have a certificate for this deviceId, we can verify it
let cert_pem = settings.get_string('certificate-pem');
if (cert_pem !== '') {
let certificate = null;
let verified = false;
try {
certificate = Gio.TlsCertificate.new_from_pem(cert_pem, -1);
verified = certificate.is_same(connection.peer_certificate);
} catch (e) {
logError(e);
}
/* The certificate is incorrect for one of two reasons, but both
* result in us resetting the certificate and unpairing the device.
*
* If the certificate failed to load, it is probably corrupted or
* otherwise invalid. In this case, if we try to continue we will
* certainly crash the Android app.
*
* If the certificate did not match what we expected the obvious
* thing to do is to notify the user, however experience tells us
* this is a result of the user doing something masochistic like
* nuking the Android app data or copying settings between machines.
*/
if (verified === false) {
if (this.device) {
this.device.unpair();
} else {
settings.reset('paired');
settings.reset('certificate-pem');
}
let name = this.identity.body.deviceName;
throw new Error(`${name}: Authentication Failure`);
}
}
return connection;
}
/**
* Wrap the connection in Gio.TlsClientConnection and initiate handshake
*
* @param {Gio.TcpConnection} connection - The unauthenticated connection
* @return {Gio.TlsServerConnection} - The authenticated connection
*/
_clientEncryption(connection) {
connection = Gio.TlsClientConnection.new(
connection,
connection.socket.remote_address
);
connection.set_certificate(this.certificate);
return this._authenticate(connection);
}
/**
* Wrap the connection in Gio.TlsServerConnection and initiate handshake
*
* @param {Gio.TcpConnection} connection - The unauthenticated connection
* @return {Gio.TlsServerConnection} - The authenticated connection
*/
_serverEncryption(connection) {
connection = Gio.TlsServerConnection.new(connection, this.certificate);
// We're the server so we trust-on-first-use and verify after
let _id = connection.connect('accept-certificate', (connection) => {
connection.disconnect(_id);
return true;
});
return this._authenticate(connection);
}
/**
* Read the identity packet from the new connection
*
* @param {Gio.SocketConnection} connection - An unencrypted socket
* @return {Gio.SocketConnection} - The connection after success
*/
_receiveIdent(connection) {
return new Promise((resolve, reject) => {
let stream = new Gio.DataInputStream({
base_stream: connection.input_stream,
close_base_stream: false
});
stream.read_line_async(
GLib.PRIORITY_DEFAULT,
this.cancellable,
(stream, res) => {
try {
let data = stream.read_line_finish_utf8(res)[0];
stream.close(null);
// Store the identity as an object property
this.identity = new Core.Packet(data);
// Reject connections without a deviceId
if (!this.identity.body.deviceId) {
throw new Error('missing deviceId');
}
resolve(connection);
} catch (e) {
reject(e);
}
}
);
});
}
/**
* Write our identity packet to the new connection
*
* @param {Gio.SocketConnection} connection - An unencrypted socket
* @return {Gio.SocketConnection} - The connection after success
*/
_sendIdent(connection) {
return new Promise((resolve, reject) => {
this.service.identity.body.tcpPort = this.backend.port;
connection.output_stream.write_all_async(
`${this.service.identity}`,
GLib.PRIORITY_DEFAULT,
this.cancellable,
(stream, res) => {
try {
this.service.identity.body.tcpPort = undefined;
stream.write_all_finish(res);
resolve(connection);
} catch (e) {
reject(e);
}
}
);
});
}
/**
* Negotiate an incoming connection
*
* @param {Gio.TcpConnection} connection - The incoming connection
*/
async accept(connection) {
try {
debug(`${this.address} (${this.uuid})`);
this.backend.channels.set(this.address, this);
this._connection = this._initSocket(connection);
this._connection = await this._receiveIdent(this._connection);
this._connection = await this._clientEncryption(this._connection);
} catch (e) {
this.close();
return Promise.reject(e);
}
}
/**
* Negotiate an outgoing connection
*
* @param {Gio.SocketConnection} connection - The remote connection
*/
async open(connection) {
try {
debug(`${this.address} (${this.uuid})`);
this.backend.channels.set(this.address, this);
this._connection = this._initSocket(connection);
this._connection = await this._sendIdent(this._connection);
this._connection = await this._serverEncryption(this._connection);
} catch (e) {
this.close();
return Promise.reject(e);
}
}
/**
* Close all streams associated with this channel, silencing any errors
*/
close() {
if (this._closed === undefined) {
debug(`${this.address} (${this.uuid})`);
this._closed = true;
this.backend.channels.delete(this.address);
// Cancel any queued operations
this.cancellable.cancel();
// Close any streams
let streams = [
this.input_stream,
this.output_stream,
this._connection
];
for (let stream of streams) {
try {
stream.close_async(0, null, null);
} catch (e) {
// Silence errors
}
}
}
}
/**
* Attach to @device as the default channel used for packet exchange.
*
* @param {Device.Device} device - The device to attach to
*/
attach(device) {
try {
// Detach any existing channel and avoid an unnecessary disconnect
if (device._channel && device._channel !== this) {
debug(`${device._channel.address} (${device._channel.uuid}) => ${this.address} (${this.uuid})`);
let channel = device._channel;
channel.cancellable.disconnect(channel._id);
channel.close();
this._output_queue = channel._output_queue;
}
// Attach the new channel and parse it's identity
device._channel = this;
this._id = this.cancellable.connect(device._setDisconnected.bind(device));
device._handleIdentity(this.identity);
// Setup streams for packet exchange
this.input_stream = new Gio.DataInputStream({
base_stream: this._connection.input_stream
});
this.output_stream = this._connection.output_stream;
// Start listening for packets
this.receive(device);
device._setConnected();
} catch (e) {
logError(e);
this.close();
}
}
createTransfer(params) {
params = Object.assign(params, {
backend: this.backend,
certificate: this.certificate,
host: this.host
});
return new Transfer(params);
}
});
/**
* Lan Transfer
*/
var Transfer = GObject.registerClass({
GTypeName: 'GSConnectLanTransfer'
}, class Transfer extends Channel {
/**
* @param {object} params - Transfer parameters
* @param {Device.Device} params.device - The device that owns this transfer
* @param {Gio.InputStream} params.input_stream - The input stream (read)
* @param {Gio.OutputStream} params.output_stream - The output stream (write)
* @param {number} params.size - The size of the transfer in bytes
*/
_init(params) {
super._init(params);
// The device tracks transfers it owns so they can be closed from the
// notification action.
this.device._transfers.set(this.uuid, this);
}
/**
* Override to untrack the transfer UUID
*/
close() {
this.device._transfers.delete(this.uuid);
super.close();
}
/**
* Connect to @port and read from the remote output stream into the local
* input stream.
*
* When finished the channel and local input stream will be closed whether
* or not the transfer succeeds.
*
* @return {boolean} - %true on success or %false on fail
*/
async download() {
let result = false;
try {
this._connection = await new Promise((resolve, reject) => {
// Connect
let client = new Gio.SocketClient({enable_proxy: false});
// Use the address from GSettings with @port
let address = Gio.InetSocketAddress.new_from_string(
this.host,
this.port
);
client.connect_async(address, null, (client, res) => {
try {
resolve(client.connect_finish(res));
} catch (e) {
reject(e);
}
});
});
this._connection = await this._initSocket(this._connection);
this._connection = await this._clientEncryption(this._connection);
this.input_stream = this._connection.get_input_stream();
// Start the transfer
result = await this.transfer();
} catch (e) {
logError(e, this.device.name);
} finally {
this.close();
}
return result;
}
/**
* Start listening on the first available port for an incoming connection,
* then send @packet with the payload transfer info. When the connection is
* accepted write to the remote input stream from the local output stream.
*
* When finished the channel and local output stream will be closed whether
* or not the transfer succeeds.
*
* @param {Core.Packet} packet - The packet describing the transfer
* @return {boolean} - %true on success or %false on fail
*/
async upload(packet) {
let port = TRANSFER_MIN;
let result = false;
try {
// Start listening on the first available port between 1739-1764
let listener = new Gio.SocketListener();
while (port <= TRANSFER_MAX) {
try {
listener.add_inet_port(port, null);
this._port = port;
break;
} catch (e) {
if (port < TRANSFER_MAX) {
port++;
continue;
} else {
throw e;
}
}
}
// Await the incoming connection
let connection = new Promise((resolve, reject) => {
listener.accept_async(
this.cancellable,
(listener, res, source_object) => {
try {
resolve(listener.accept_finish(res)[0]);
} catch (e) {
reject(e);
}
}
);
});
// Notify the device we're ready
packet.body.payloadHash = this.checksum;
packet.payloadSize = this.size;
packet.payloadTransferInfo = {port: port};
this.device.sendPacket(packet);
// Accept the connection and configure the channel
this._connection = await connection;
this._connection = await this._initSocket(this._connection);
this._connection = await this._serverEncryption(this._connection);
this.output_stream = this._connection.get_output_stream();
// Start the transfer
result = await this.transfer();
} catch (e) {
logError(e, this.device.name);
} finally {
this.close();
}
return result;
}
});