lan.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const Gio = imports.gi.Gio;
  6. const GLib = imports.gi.GLib;
  7. const GObject = imports.gi.GObject;
  8. const Config = imports.config;
  9. const Core = imports.service.core;
  10. /**
  11. * TCP Port Constants
  12. */
  13. const PROTOCOL_PORT_DEFAULT = 1716;
  14. const PROTOCOL_PORT_MIN = 1716;
  15. const PROTOCOL_PORT_MAX = 1764;
  16. const TRANSFER_MIN = 1739;
  17. const TRANSFER_MAX = 1764;
  18. /*
  19. * One-time check for Linux/FreeBSD socket options
  20. */
  21. var _LINUX_SOCKETS = true;
  22. try {
  23. // This should throw on FreeBSD
  24. Gio.Socket.new(
  25. Gio.SocketFamily.IPV4,
  26. Gio.SocketType.STREAM,
  27. Gio.SocketProtocol.TCP
  28. ).get_option(6, 5);
  29. } catch (e) {
  30. _LINUX_SOCKETS = false;
  31. }
  32. /**
  33. * Configure a socket connection for the KDE Connect protocol.
  34. *
  35. * @param {Gio.SocketConnection} connection - The connection to configure
  36. */
  37. function _configureSocket(connection) {
  38. try {
  39. if (_LINUX_SOCKETS) {
  40. connection.socket.set_option(6, 4, 10); // TCP_KEEPIDLE
  41. connection.socket.set_option(6, 5, 5); // TCP_KEEPINTVL
  42. connection.socket.set_option(6, 6, 3); // TCP_KEEPCNT
  43. // FreeBSD constants
  44. // https://github.com/freebsd/freebsd/blob/master/sys/netinet/tcp.h#L159
  45. } else {
  46. connection.socket.set_option(6, 256, 10); // TCP_KEEPIDLE
  47. connection.socket.set_option(6, 512, 5); // TCP_KEEPINTVL
  48. connection.socket.set_option(6, 1024, 3); // TCP_KEEPCNT
  49. }
  50. // Do this last because an error setting the keepalive options would
  51. // result in a socket that never times out
  52. connection.socket.set_keepalive(true);
  53. } catch (e) {
  54. debug(e, 'Configuring Socket');
  55. }
  56. }
  57. /**
  58. * Lan.ChannelService consists of two parts:
  59. *
  60. * The TCP Listener listens on a port and constructs a Channel object from the
  61. * incoming Gio.TcpConnection.
  62. *
  63. * The UDP Listener listens on a port for incoming JSON identity packets which
  64. * include the TCP port, while the IP address is taken from the UDP packet
  65. * itself. We respond by opening a TCP connection to that address.
  66. */
  67. var ChannelService = GObject.registerClass({
  68. GTypeName: 'GSConnectLanChannelService',
  69. Properties: {
  70. 'certificate': GObject.ParamSpec.object(
  71. 'certificate',
  72. 'Certificate',
  73. 'The TLS certificate',
  74. GObject.ParamFlags.READWRITE,
  75. Gio.TlsCertificate.$gtype
  76. ),
  77. 'port': GObject.ParamSpec.uint(
  78. 'port',
  79. 'Port',
  80. 'The port used by the service',
  81. GObject.ParamFlags.READWRITE,
  82. 0, GLib.MAXUINT16,
  83. PROTOCOL_PORT_DEFAULT
  84. ),
  85. },
  86. }, class LanChannelService extends Core.ChannelService {
  87. _init(params = {}) {
  88. super._init(params);
  89. // Track hosts we identify to directly, allowing them to ignore the
  90. // discoverable state of the service.
  91. this._allowed = new Set();
  92. //
  93. this._tcp = null;
  94. this._tcpPort = PROTOCOL_PORT_DEFAULT;
  95. this._udp4 = null;
  96. this._udp6 = null;
  97. // Monitor network status
  98. this._networkMonitor = Gio.NetworkMonitor.get_default();
  99. this._networkAvailable = false;
  100. this._networkChangedId = 0;
  101. }
  102. get certificate() {
  103. if (this._certificate === undefined)
  104. this._certificate = null;
  105. return this._certificate;
  106. }
  107. set certificate(certificate) {
  108. if (this.certificate === certificate)
  109. return;
  110. this._certificate = certificate;
  111. this.notify('certificate');
  112. }
  113. get channels() {
  114. if (this._channels === undefined)
  115. this._channels = new Map();
  116. return this._channels;
  117. }
  118. get port() {
  119. if (this._port === undefined)
  120. this._port = PROTOCOL_PORT_DEFAULT;
  121. return this._port;
  122. }
  123. set port(port) {
  124. if (this.port === port)
  125. return;
  126. this._port = port;
  127. this.notify('port');
  128. }
  129. _onNetworkChanged(monitor, network_available) {
  130. if (this._networkAvailable === network_available)
  131. return;
  132. this._networkAvailable = network_available;
  133. this.broadcast();
  134. }
  135. _initCertificate() {
  136. if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) {
  137. const error = new Error();
  138. error.name = _('OpenSSL not found');
  139. error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`;
  140. throw error;
  141. }
  142. const certPath = GLib.build_filenamev([
  143. Config.CONFIGDIR,
  144. 'certificate.pem',
  145. ]);
  146. const keyPath = GLib.build_filenamev([
  147. Config.CONFIGDIR,
  148. 'private.pem',
  149. ]);
  150. // Ensure a certificate exists with our id as the common name
  151. this._certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath,
  152. this.id);
  153. // If the service ID doesn't match the common name, this is probably a
  154. // certificate from an older version and we should amend ours to match
  155. if (this.id !== this._certificate.common_name)
  156. this._id = this._certificate.common_name;
  157. }
  158. _initTcpListener() {
  159. try {
  160. this._tcp = new Gio.SocketService();
  161. let tcpPort = this.port;
  162. const tcpPortMax = tcpPort +
  163. (PROTOCOL_PORT_MAX - PROTOCOL_PORT_MIN);
  164. while (tcpPort <= tcpPortMax) {
  165. try {
  166. this._tcp.add_inet_port(tcpPort, null);
  167. break;
  168. } catch (e) {
  169. if (tcpPort < tcpPortMax) {
  170. tcpPort++;
  171. continue;
  172. }
  173. throw e;
  174. }
  175. }
  176. this._tcpPort = tcpPort;
  177. this._tcp.connect('incoming', this._onIncomingChannel.bind(this));
  178. } catch (e) {
  179. this._tcp.stop();
  180. this._tcp.close();
  181. this._tcp = null;
  182. throw e;
  183. }
  184. }
  185. async _onIncomingChannel(listener, connection) {
  186. try {
  187. const host = connection.get_remote_address().address.to_string();
  188. // Create a channel
  189. const channel = new Channel({
  190. backend: this,
  191. certificate: this.certificate,
  192. host: host,
  193. port: this.port,
  194. });
  195. // Accept the connection
  196. await channel.accept(connection);
  197. channel.identity.body.tcpHost = channel.host;
  198. channel.identity.body.tcpPort = this._tcpPort;
  199. channel.allowed = this._allowed.has(host);
  200. this.channel(channel);
  201. } catch (e) {
  202. debug(e);
  203. }
  204. }
  205. _initUdpListener() {
  206. // Default broadcast address
  207. this._udp_address = Gio.InetSocketAddress.new_from_string(
  208. '255.255.255.255', this.port);
  209. try {
  210. this._udp6 = Gio.Socket.new(Gio.SocketFamily.IPV6,
  211. Gio.SocketType.DATAGRAM, Gio.SocketProtocol.UDP);
  212. this._udp6.set_broadcast(true);
  213. // Bind the socket
  214. const inetAddr = Gio.InetAddress.new_any(Gio.SocketFamily.IPV6);
  215. const sockAddr = Gio.InetSocketAddress.new(inetAddr, this.port);
  216. this._udp6.bind(sockAddr, true);
  217. // Input stream
  218. this._udp6_stream = new Gio.DataInputStream({
  219. base_stream: new Gio.UnixInputStream({
  220. fd: this._udp6.fd,
  221. close_fd: false,
  222. }),
  223. });
  224. // Watch socket for incoming packets
  225. this._udp6_source = this._udp6.create_source(GLib.IOCondition.IN, null);
  226. this._udp6_source.set_callback(this._onIncomingIdentity.bind(this, this._udp6));
  227. this._udp6_source.attach(null);
  228. } catch (e) {
  229. this._udp6 = null;
  230. }
  231. // Our IPv6 socket also supports IPv4; we're all done
  232. if (this._udp6 && this._udp6.speaks_ipv4()) {
  233. this._udp4 = null;
  234. return;
  235. }
  236. try {
  237. this._udp4 = Gio.Socket.new(Gio.SocketFamily.IPV4,
  238. Gio.SocketType.DATAGRAM, Gio.SocketProtocol.UDP);
  239. this._udp4.set_broadcast(true);
  240. // Bind the socket
  241. const inetAddr = Gio.InetAddress.new_any(Gio.SocketFamily.IPV4);
  242. const sockAddr = Gio.InetSocketAddress.new(inetAddr, this.port);
  243. this._udp4.bind(sockAddr, true);
  244. // Input stream
  245. this._udp4_stream = new Gio.DataInputStream({
  246. base_stream: new Gio.UnixInputStream({
  247. fd: this._udp4.fd,
  248. close_fd: false,
  249. }),
  250. });
  251. // Watch input socket for incoming packets
  252. this._udp4_source = this._udp4.create_source(GLib.IOCondition.IN, null);
  253. this._udp4_source.set_callback(this._onIncomingIdentity.bind(this, this._udp4));
  254. this._udp4_source.attach(null);
  255. } catch (e) {
  256. this._udp4 = null;
  257. // We failed to get either an IPv4 or IPv6 socket to bind
  258. if (this._udp6 === null)
  259. throw e;
  260. }
  261. }
  262. _onIncomingIdentity(socket) {
  263. let host;
  264. // Try to peek the remote address
  265. try {
  266. host = socket.receive_message([], Gio.SocketMsgFlags.PEEK, null)[1]
  267. .address.to_string();
  268. } catch (e) {
  269. logError(e);
  270. }
  271. // Whether or not we peeked the address, we need to read the packet
  272. try {
  273. let data;
  274. if (socket === this._udp6)
  275. data = this._udp6_stream.read_line_utf8(null)[0];
  276. else
  277. data = this._udp4_stream.read_line_utf8(null)[0];
  278. // Discard the packet if we failed to peek the address
  279. if (host === undefined)
  280. return GLib.SOURCE_CONTINUE;
  281. const packet = new Core.Packet(data);
  282. packet.body.tcpHost = host;
  283. this._onIdentity(packet);
  284. } catch (e) {
  285. logError(e);
  286. }
  287. return GLib.SOURCE_CONTINUE;
  288. }
  289. async _onIdentity(packet) {
  290. try {
  291. // Bail if the deviceId is missing
  292. if (!packet.body.hasOwnProperty('deviceId'))
  293. return;
  294. // Silently ignore our own broadcasts
  295. if (packet.body.deviceId === this.identity.body.deviceId)
  296. return;
  297. debug(packet);
  298. // Create a new channel
  299. const channel = new Channel({
  300. backend: this,
  301. certificate: this.certificate,
  302. host: packet.body.tcpHost,
  303. port: packet.body.tcpPort,
  304. identity: packet,
  305. });
  306. // Check if channel is already open with this address
  307. if (this.channels.has(channel.address))
  308. return;
  309. this._channels.set(channel.address, channel);
  310. // Open a TCP connection
  311. const address = Gio.InetSocketAddress.new_from_string(
  312. packet.body.tcpHost, packet.body.tcpPort);
  313. const client = new Gio.SocketClient({enable_proxy: false});
  314. const connection = await client.connect_async(address,
  315. this.cancellable);
  316. // Connect the channel and attach it to the device on success
  317. await channel.open(connection);
  318. this.channel(channel);
  319. } catch (e) {
  320. logError(e);
  321. }
  322. }
  323. /**
  324. * Broadcast an identity packet
  325. *
  326. * If @address is not %null it may specify an IPv4 or IPv6 address to send
  327. * the identity packet directly to, otherwise it will be broadcast to the
  328. * default address, 255.255.255.255.
  329. *
  330. * @param {string} [address] - An optional target IPv4 or IPv6 address
  331. */
  332. broadcast(address = null) {
  333. try {
  334. if (!this._networkAvailable)
  335. return;
  336. // Try to parse strings as <host>:<port>
  337. if (typeof address === 'string') {
  338. const [host, portstr] = address.split(':');
  339. const port = parseInt(portstr) || this.port;
  340. address = Gio.InetSocketAddress.new_from_string(host, port);
  341. }
  342. // If we succeed, remember this host
  343. if (address instanceof Gio.InetSocketAddress) {
  344. this._allowed.add(address.address.to_string());
  345. // Broadcast to the network if no address is specified
  346. } else {
  347. debug('Broadcasting to LAN');
  348. address = this._udp_address;
  349. }
  350. // Broadcast on each open socket
  351. if (this._udp6 !== null)
  352. this._udp6.send_to(address, this.identity.serialize(), null);
  353. if (this._udp4 !== null)
  354. this._udp4.send_to(address, this.identity.serialize(), null);
  355. } catch (e) {
  356. debug(e, address);
  357. }
  358. }
  359. buildIdentity() {
  360. // Chain-up, then add the TCP port
  361. super.buildIdentity();
  362. this.identity.body.tcpPort = this._tcpPort;
  363. }
  364. start() {
  365. if (this.active)
  366. return;
  367. // Ensure a certificate exists
  368. if (this.certificate === null)
  369. this._initCertificate();
  370. // Start TCP/UDP listeners
  371. try {
  372. if (this._tcp === null)
  373. this._initTcpListener();
  374. if (this._udp4 === null && this._udp6 === null)
  375. this._initUdpListener();
  376. } catch (e) {
  377. // Known case of another application using the protocol defined port
  378. if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ADDRESS_IN_USE)) {
  379. e.name = _('Port already in use');
  380. e.url = `${Config.PACKAGE_URL}/wiki/Error#port-already-in-use`;
  381. }
  382. throw e;
  383. }
  384. // Monitor network changes
  385. if (this._networkChangedId === 0) {
  386. this._networkAvailable = this._networkMonitor.network_available;
  387. this._networkChangedId = this._networkMonitor.connect(
  388. 'network-changed', this._onNetworkChanged.bind(this));
  389. }
  390. this._active = true;
  391. this.notify('active');
  392. }
  393. stop() {
  394. if (this._networkChangedId) {
  395. this._networkMonitor.disconnect(this._networkChangedId);
  396. this._networkChangedId = 0;
  397. this._networkAvailable = false;
  398. }
  399. if (this._tcp !== null) {
  400. this._tcp.stop();
  401. this._tcp.close();
  402. this._tcp = null;
  403. }
  404. if (this._udp6 !== null) {
  405. this._udp6_source.destroy();
  406. this._udp6_stream.close(null);
  407. this._udp6.close();
  408. this._udp6 = null;
  409. }
  410. if (this._udp4 !== null) {
  411. this._udp4_source.destroy();
  412. this._udp4_stream.close(null);
  413. this._udp4.close();
  414. this._udp4 = null;
  415. }
  416. for (const channel of this.channels.values())
  417. channel.close();
  418. this._active = false;
  419. this.notify('active');
  420. }
  421. destroy() {
  422. try {
  423. this.stop();
  424. } catch (e) {
  425. debug(e);
  426. }
  427. }
  428. });
  429. /**
  430. * Lan Channel
  431. *
  432. * This class essentially just extends Core.Channel to set TCP socket options
  433. * and negotiate TLS encrypted connections.
  434. */
  435. var Channel = GObject.registerClass({
  436. GTypeName: 'GSConnectLanChannel',
  437. }, class LanChannel extends Core.Channel {
  438. _init(params) {
  439. super._init();
  440. Object.assign(this, params);
  441. }
  442. get address() {
  443. return `lan://${this.host}:${this.port}`;
  444. }
  445. get certificate() {
  446. if (this._certificate === undefined)
  447. this._certificate = null;
  448. return this._certificate;
  449. }
  450. set certificate(certificate) {
  451. this._certificate = certificate;
  452. }
  453. get peer_certificate() {
  454. if (this._connection instanceof Gio.TlsConnection)
  455. return this._connection.get_peer_certificate();
  456. return null;
  457. }
  458. get host() {
  459. if (this._host === undefined)
  460. this._host = null;
  461. return this._host;
  462. }
  463. set host(host) {
  464. this._host = host;
  465. }
  466. get port() {
  467. if (this._port === undefined) {
  468. if (this.identity && this.identity.body.tcpPort)
  469. this._port = this.identity.body.tcpPort;
  470. else
  471. return PROTOCOL_PORT_DEFAULT;
  472. }
  473. return this._port;
  474. }
  475. set port(port) {
  476. this._port = port;
  477. }
  478. /**
  479. * Authenticate a TLS connection.
  480. *
  481. * @param {Gio.TlsConnection} connection - A TLS connection
  482. * @return {Promise} A promise for the operation
  483. */
  484. async _authenticate(connection) {
  485. // Standard TLS Handshake
  486. connection.validation_flags = Gio.TlsCertificateFlags.EXPIRED;
  487. connection.authentication_mode = Gio.TlsAuthenticationMode.REQUIRED;
  488. await connection.handshake_async(GLib.PRIORITY_DEFAULT,
  489. this.cancellable);
  490. // Get a settings object for the device
  491. let settings;
  492. if (this.device) {
  493. settings = this.device.settings;
  494. } else {
  495. const id = this.identity.body.deviceId;
  496. settings = new Gio.Settings({
  497. settings_schema: Config.GSCHEMA.lookup(
  498. 'org.gnome.Shell.Extensions.GSConnect.Device',
  499. true
  500. ),
  501. path: `/org/gnome/shell/extensions/gsconnect/device/${id}/`,
  502. });
  503. }
  504. // If we have a certificate for this deviceId, we can verify it
  505. const cert_pem = settings.get_string('certificate-pem');
  506. if (cert_pem !== '') {
  507. let certificate = null;
  508. let verified = false;
  509. try {
  510. certificate = Gio.TlsCertificate.new_from_pem(cert_pem, -1);
  511. verified = certificate.is_same(connection.peer_certificate);
  512. } catch (e) {
  513. logError(e);
  514. }
  515. /* The certificate is incorrect for one of two reasons, but both
  516. * result in us resetting the certificate and unpairing the device.
  517. *
  518. * If the certificate failed to load, it is probably corrupted or
  519. * otherwise invalid. In this case, if we try to continue we will
  520. * certainly crash the Android app.
  521. *
  522. * If the certificate did not match what we expected the obvious
  523. * thing to do is to notify the user, however experience tells us
  524. * this is a result of the user doing something masochistic like
  525. * nuking the Android app data or copying settings between machines.
  526. */
  527. if (verified === false) {
  528. if (this.device) {
  529. this.device.unpair();
  530. } else {
  531. settings.reset('paired');
  532. settings.reset('certificate-pem');
  533. }
  534. const name = this.identity.body.deviceName;
  535. throw new Error(`${name}: Authentication Failure`);
  536. }
  537. }
  538. return connection;
  539. }
  540. /**
  541. * Wrap the connection in Gio.TlsClientConnection and initiate handshake
  542. *
  543. * @param {Gio.TcpConnection} connection - The unauthenticated connection
  544. * @return {Gio.TlsClientConnection} The authenticated connection
  545. */
  546. _encryptClient(connection) {
  547. _configureSocket(connection);
  548. connection = Gio.TlsClientConnection.new(connection,
  549. connection.socket.remote_address);
  550. connection.set_certificate(this.certificate);
  551. return this._authenticate(connection);
  552. }
  553. /**
  554. * Wrap the connection in Gio.TlsServerConnection and initiate handshake
  555. *
  556. * @param {Gio.TcpConnection} connection - The unauthenticated connection
  557. * @return {Gio.TlsServerConnection} The authenticated connection
  558. */
  559. _encryptServer(connection) {
  560. _configureSocket(connection);
  561. connection = Gio.TlsServerConnection.new(connection, this.certificate);
  562. // We're the server so we trust-on-first-use and verify after
  563. const _id = connection.connect('accept-certificate', (connection) => {
  564. connection.disconnect(_id);
  565. return true;
  566. });
  567. return this._authenticate(connection);
  568. }
  569. /**
  570. * Negotiate an incoming connection
  571. *
  572. * @param {Gio.TcpConnection} connection - The incoming connection
  573. */
  574. async accept(connection) {
  575. debug(`${this.address} (${this.uuid})`);
  576. try {
  577. this._connection = connection;
  578. this.backend.channels.set(this.address, this);
  579. // In principle this disposable wrapper could buffer more than the
  580. // identity packet, but in practice the remote device shouldn't send
  581. // any more data until the TLS connection is negotiated.
  582. const stream = new Gio.DataInputStream({
  583. base_stream: connection.input_stream,
  584. close_base_stream: false,
  585. });
  586. const data = await stream.read_line_async(GLib.PRIORITY_DEFAULT,
  587. this.cancellable);
  588. stream.close_async(GLib.PRIORITY_DEFAULT, null, null);
  589. this.identity = new Core.Packet(data[0]);
  590. if (!this.identity.body.deviceId)
  591. throw new Error('missing deviceId');
  592. this._connection = await this._encryptClient(connection);
  593. } catch (e) {
  594. this.close();
  595. throw e;
  596. }
  597. }
  598. /**
  599. * Negotiate an outgoing connection
  600. *
  601. * @param {Gio.SocketConnection} connection - The remote connection
  602. */
  603. async open(connection) {
  604. debug(`${this.address} (${this.uuid})`);
  605. try {
  606. this._connection = connection;
  607. this.backend.channels.set(this.address, this);
  608. await connection.get_output_stream().write_all_async(
  609. this.backend.identity.serialize(),
  610. GLib.PRIORITY_DEFAULT,
  611. this.cancellable);
  612. this._connection = await this._encryptServer(connection);
  613. } catch (e) {
  614. this.close();
  615. throw e;
  616. }
  617. }
  618. /**
  619. * Close all streams associated with this channel, silencing any errors
  620. */
  621. close() {
  622. if (this.closed)
  623. return;
  624. debug(`${this.address} (${this.uuid})`);
  625. this._closed = true;
  626. this.notify('closed');
  627. this.backend.channels.delete(this.address);
  628. this.cancellable.cancel();
  629. if (this._connection)
  630. this._connection.close_async(GLib.PRIORITY_DEFAULT, null, null);
  631. if (this.input_stream)
  632. this.input_stream.close_async(GLib.PRIORITY_DEFAULT, null, null);
  633. if (this.output_stream)
  634. this.output_stream.close_async(GLib.PRIORITY_DEFAULT, null, null);
  635. }
  636. async download(packet, target, cancellable = null) {
  637. const address = Gio.InetSocketAddress.new_from_string(this.host,
  638. packet.payloadTransferInfo.port);
  639. const client = new Gio.SocketClient({enable_proxy: false});
  640. const connection = await client.connect_async(address, cancellable)
  641. .then(this._encryptClient.bind(this));
  642. // Start the transfer
  643. const transferredSize = await target.splice_async(
  644. connection.input_stream,
  645. (Gio.OutputStreamSpliceFlags.CLOSE_SOURCE |
  646. Gio.OutputStreamSpliceFlags.CLOSE_TARGET),
  647. GLib.PRIORITY_DEFAULT, cancellable);
  648. // If we get less than expected, we've certainly got corruption
  649. if (transferredSize < packet.payloadSize) {
  650. throw new Gio.IOErrorEnum({
  651. code: Gio.IOErrorEnum.FAILED,
  652. message: `Incomplete: ${transferredSize}/${packet.payloadSize}`,
  653. });
  654. // TODO: sometimes kdeconnect-android under-reports a file's size
  655. // https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1157
  656. } else if (transferredSize > packet.payloadSize) {
  657. logError(new Gio.IOErrorEnum({
  658. code: Gio.IOErrorEnum.FAILED,
  659. message: `Extra Data: ${transferredSize - packet.payloadSize}`,
  660. }));
  661. }
  662. }
  663. async upload(packet, source, size, cancellable = null) {
  664. // Start listening on the first available port between 1739-1764
  665. const listener = new Gio.SocketListener();
  666. let port = TRANSFER_MIN;
  667. while (port <= TRANSFER_MAX) {
  668. try {
  669. listener.add_inet_port(port, null);
  670. break;
  671. } catch (e) {
  672. if (port < TRANSFER_MAX) {
  673. port++;
  674. continue;
  675. } else {
  676. throw e;
  677. }
  678. }
  679. }
  680. // Listen for the incoming connection
  681. const acceptConnection = listener.accept_async(cancellable)
  682. .then(result => this._encryptServer(result[0]));
  683. // Create an upload request
  684. packet.body.payloadHash = this.checksum;
  685. packet.payloadSize = size;
  686. packet.payloadTransferInfo = {port: port};
  687. const requestUpload = this.sendPacket(new Core.Packet(packet),
  688. cancellable);
  689. // Request an upload stream, accept the connection and get the output
  690. const [, connection] = await Promise.all([requestUpload,
  691. acceptConnection]);
  692. // Start the transfer
  693. const transferredSize = await connection.output_stream.splice_async(
  694. source,
  695. (Gio.OutputStreamSpliceFlags.CLOSE_SOURCE |
  696. Gio.OutputStreamSpliceFlags.CLOSE_TARGET),
  697. GLib.PRIORITY_DEFAULT, cancellable);
  698. if (transferredSize !== size) {
  699. throw new Gio.IOErrorEnum({
  700. code: Gio.IOErrorEnum.PARTIAL_INPUT,
  701. message: 'Transfer incomplete',
  702. });
  703. }
  704. }
  705. async rejectTransfer(packet) {
  706. try {
  707. if (!packet || !packet.hasPayload())
  708. return;
  709. if (packet.payloadTransferInfo.port === undefined)
  710. return;
  711. const address = Gio.InetSocketAddress.new_from_string(this.host,
  712. packet.payloadTransferInfo.port);
  713. const client = new Gio.SocketClient({enable_proxy: false});
  714. const connection = await client.connect_async(address, null)
  715. .then(this._encryptClient.bind(this));
  716. connection.close_async(GLib.PRIORITY_DEFAULT, null, null);
  717. } catch (e) {
  718. debug(e, this.device.name);
  719. }
  720. }
  721. });