lan.js 28 KB

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