999 lines
30 KiB
JavaScript
999 lines
30 KiB
JavaScript
|
'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;
|
||
|
}
|
||
|
});
|
||
|
|