lan.js 27 KB

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