lan.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  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. // Sanitize invalid device names
  306. if (!Device.validateName(packet.body.deviceName)) {
  307. const sanitized = Device.sanitizeName(packet.body.deviceName);
  308. debug(`Sanitized invalid device name "${packet.body.deviceName}" to "${sanitized}"`);
  309. packet.body.deviceName = sanitized;
  310. }
  311. debug(packet);
  312. // Create a new channel
  313. const channel = new Channel({
  314. backend: this,
  315. certificate: this.certificate,
  316. host: packet.body.tcpHost,
  317. port: packet.body.tcpPort,
  318. identity: packet,
  319. });
  320. // Check if channel is already open with this address
  321. if (this.channels.has(channel.address))
  322. return;
  323. this._channels.set(channel.address, channel);
  324. // Open a TCP connection
  325. const address = Gio.InetSocketAddress.new_from_string(
  326. packet.body.tcpHost, packet.body.tcpPort);
  327. const client = new Gio.SocketClient({enable_proxy: false});
  328. const connection = await client.connect_async(address,
  329. this.cancellable);
  330. // Connect the channel and attach it to the device on success
  331. await channel.open(connection);
  332. this.channel(channel);
  333. } catch (e) {
  334. logError(e);
  335. }
  336. }
  337. /**
  338. * Broadcast an identity packet
  339. *
  340. * If @address is not %null it may specify an IPv4 or IPv6 address to send
  341. * the identity packet directly to, otherwise it will be broadcast to the
  342. * default address, 255.255.255.255.
  343. *
  344. * @param {string} [address] - An optional target IPv4 or IPv6 address
  345. */
  346. broadcast(address = null) {
  347. try {
  348. if (!this._networkAvailable)
  349. return;
  350. // Try to parse strings as <host>:<port>
  351. if (typeof address === 'string') {
  352. const [host, portstr] = address.split(':');
  353. const port = parseInt(portstr) || this.port;
  354. address = Gio.InetSocketAddress.new_from_string(host, port);
  355. }
  356. // If we succeed, remember this host
  357. if (address instanceof Gio.InetSocketAddress) {
  358. this._allowed.add(address.address.to_string());
  359. // Broadcast to the network if no address is specified
  360. } else {
  361. debug('Broadcasting to LAN');
  362. address = this._udp_address;
  363. }
  364. // Broadcast on each open socket
  365. if (this._udp6 !== null)
  366. this._udp6.send_to(address, this.identity.serialize(), null);
  367. if (this._udp4 !== null)
  368. this._udp4.send_to(address, this.identity.serialize(), null);
  369. } catch (e) {
  370. debug(e, address);
  371. }
  372. }
  373. buildIdentity() {
  374. // Chain-up, then add the TCP port
  375. super.buildIdentity();
  376. this.identity.body.tcpPort = this._tcpPort;
  377. }
  378. start() {
  379. if (this.active)
  380. return;
  381. // Ensure a certificate exists
  382. if (this.certificate === null)
  383. this._initCertificate();
  384. // Start TCP/UDP listeners
  385. try {
  386. if (this._tcp === null)
  387. this._initTcpListener();
  388. if (this._udp4 === null && this._udp6 === null)
  389. this._initUdpListener();
  390. } catch (e) {
  391. // Known case of another application using the protocol defined port
  392. if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ADDRESS_IN_USE)) {
  393. e.name = _('Port already in use');
  394. e.url = `${Config.PACKAGE_URL}/wiki/Error#port-already-in-use`;
  395. }
  396. throw e;
  397. }
  398. // Monitor network changes
  399. if (this._networkChangedId === 0) {
  400. this._networkAvailable = this._networkMonitor.network_available;
  401. this._networkChangedId = this._networkMonitor.connect(
  402. 'network-changed', this._onNetworkChanged.bind(this));
  403. }
  404. this._active = true;
  405. this.notify('active');
  406. }
  407. stop() {
  408. if (this._networkChangedId) {
  409. this._networkMonitor.disconnect(this._networkChangedId);
  410. this._networkChangedId = 0;
  411. this._networkAvailable = false;
  412. }
  413. if (this._tcp !== null) {
  414. this._tcp.stop();
  415. this._tcp.close();
  416. this._tcp = null;
  417. }
  418. if (this._udp6 !== null) {
  419. this._udp6_source.destroy();
  420. this._udp6_stream.close(null);
  421. this._udp6.close();
  422. this._udp6 = null;
  423. }
  424. if (this._udp4 !== null) {
  425. this._udp4_source.destroy();
  426. this._udp4_stream.close(null);
  427. this._udp4.close();
  428. this._udp4 = null;
  429. }
  430. for (const channel of this.channels.values())
  431. channel.close();
  432. this._active = false;
  433. this.notify('active');
  434. }
  435. destroy() {
  436. try {
  437. this.stop();
  438. } catch (e) {
  439. debug(e);
  440. }
  441. }
  442. });
  443. /**
  444. * Lan Channel
  445. *
  446. * This class essentially just extends Core.Channel to set TCP socket options
  447. * and negotiate TLS encrypted connections.
  448. */
  449. export const Channel = GObject.registerClass({
  450. GTypeName: 'GSConnectLanChannel',
  451. }, class LanChannel extends Core.Channel {
  452. _init(params) {
  453. super._init();
  454. Object.assign(this, params);
  455. }
  456. get address() {
  457. return `lan://${this.host}:${this.port}`;
  458. }
  459. get certificate() {
  460. if (this._certificate === undefined)
  461. this._certificate = null;
  462. return this._certificate;
  463. }
  464. set certificate(certificate) {
  465. this._certificate = certificate;
  466. }
  467. get peer_certificate() {
  468. if (this._connection instanceof Gio.TlsConnection)
  469. return this._connection.get_peer_certificate();
  470. return null;
  471. }
  472. get host() {
  473. if (this._host === undefined)
  474. this._host = null;
  475. return this._host;
  476. }
  477. set host(host) {
  478. this._host = host;
  479. }
  480. get port() {
  481. if (this._port === undefined) {
  482. if (this.identity && this.identity.body.tcpPort)
  483. this._port = this.identity.body.tcpPort;
  484. else
  485. return PROTOCOL_PORT_DEFAULT;
  486. }
  487. return this._port;
  488. }
  489. set port(port) {
  490. this._port = port;
  491. }
  492. /**
  493. * Authenticate a TLS connection.
  494. *
  495. * @param {Gio.TlsConnection} connection - A TLS connection
  496. * @returns {Promise} A promise for the operation
  497. */
  498. async _authenticate(connection) {
  499. // Standard TLS Handshake
  500. connection.validation_flags = Gio.TlsCertificateFlags.EXPIRED;
  501. connection.authentication_mode = Gio.TlsAuthenticationMode.REQUIRED;
  502. await connection.handshake_async(GLib.PRIORITY_DEFAULT,
  503. this.cancellable);
  504. // Get a settings object for the device
  505. let settings;
  506. if (this.device) {
  507. settings = this.device.settings;
  508. } else {
  509. const id = this.identity.body.deviceId;
  510. settings = new Gio.Settings({
  511. settings_schema: Config.GSCHEMA.lookup(
  512. 'org.gnome.Shell.Extensions.GSConnect.Device',
  513. true
  514. ),
  515. path: `/org/gnome/shell/extensions/gsconnect/device/${id}/`,
  516. });
  517. }
  518. // If we have a certificate for this deviceId, we can verify it
  519. const cert_pem = settings.get_string('certificate-pem');
  520. if (cert_pem !== '') {
  521. let certificate = null;
  522. let verified = false;
  523. try {
  524. certificate = Gio.TlsCertificate.new_from_pem(cert_pem, -1);
  525. verified = certificate.is_same(connection.peer_certificate);
  526. } catch (e) {
  527. logError(e);
  528. }
  529. /* The certificate is incorrect for one of two reasons, but both
  530. * result in us resetting the certificate and unpairing the device.
  531. *
  532. * If the certificate failed to load, it is probably corrupted or
  533. * otherwise invalid. In this case, if we try to continue we will
  534. * certainly crash the Android app.
  535. *
  536. * If the certificate did not match what we expected the obvious
  537. * thing to do is to notify the user, however experience tells us
  538. * this is a result of the user doing something masochistic like
  539. * nuking the Android app data or copying settings between machines.
  540. */
  541. if (verified === false) {
  542. if (this.device) {
  543. this.device.unpair();
  544. } else {
  545. settings.reset('paired');
  546. settings.reset('certificate-pem');
  547. }
  548. const name = this.identity.body.deviceName;
  549. throw new Error(`${name}: Authentication Failure`);
  550. }
  551. }
  552. return connection;
  553. }
  554. /**
  555. * Wrap the connection in Gio.TlsClientConnection and initiate handshake
  556. *
  557. * @param {Gio.TcpConnection} connection - The unauthenticated connection
  558. * @returns {Gio.TlsClientConnection} The authenticated connection
  559. */
  560. _encryptClient(connection) {
  561. _configureSocket(connection);
  562. connection = Gio.TlsClientConnection.new(connection,
  563. connection.socket.remote_address);
  564. connection.set_certificate(this.certificate);
  565. return this._authenticate(connection);
  566. }
  567. /**
  568. * Wrap the connection in Gio.TlsServerConnection and initiate handshake
  569. *
  570. * @param {Gio.TcpConnection} connection - The unauthenticated connection
  571. * @returns {Gio.TlsServerConnection} The authenticated connection
  572. */
  573. _encryptServer(connection) {
  574. _configureSocket(connection);
  575. connection = Gio.TlsServerConnection.new(connection, this.certificate);
  576. // We're the server so we trust-on-first-use and verify after
  577. const _id = connection.connect('accept-certificate', (connection) => {
  578. connection.disconnect(_id);
  579. return true;
  580. });
  581. return this._authenticate(connection);
  582. }
  583. async _exchangeIdentities() {
  584. await this.sendPacket(this.backend.identity);
  585. const identity = await this.readPacket();
  586. if (this.identity.body.protocolVersion !== identity.body.protocolVersion) {
  587. this.identity = null;
  588. throw new Error(`Unexpected protocol version ${identity.protocolVersion}; ` +
  589. `handshake started with protocol version ${this.identity.protocolVersion}`);
  590. }
  591. if (this.identity.body.deviceId !== identity.body.deviceId) {
  592. this.identity = null;
  593. throw new Error(`Unexpected device ID "${identity.body.deviceId}"; ` +
  594. `handshake started with device ID "${this.identity.body.deviceId}"`);
  595. }
  596. this.identity = identity;
  597. }
  598. /**
  599. * Negotiate an incoming connection
  600. *
  601. * @param {Gio.TcpConnection} connection - The incoming connection
  602. */
  603. async accept(connection) {
  604. debug(`${this.address} (${this.uuid})`);
  605. try {
  606. this._connection = connection;
  607. this.backend.channels.set(this.address, this);
  608. // In principle this disposable wrapper could buffer more than the
  609. // identity packet, but in practice the remote device shouldn't send
  610. // any more data until the TLS connection is negotiated.
  611. const stream = new Gio.DataInputStream({
  612. base_stream: connection.input_stream,
  613. close_base_stream: false,
  614. });
  615. const data = await stream.read_line_async(GLib.PRIORITY_DEFAULT,
  616. this.cancellable);
  617. stream.close_async(GLib.PRIORITY_DEFAULT, null, null);
  618. this.identity = new Core.Packet(data[0]);
  619. if (!this.identity.body.deviceId)
  620. throw new Error('missing deviceId');
  621. // Reject invalid device IDs
  622. if (!Device.validateId(this.identity.body.deviceId))
  623. throw new Error(`invalid deviceId "${this.identity.body.deviceId}"`);
  624. if (!this.identity.body.deviceName)
  625. throw new Error('missing deviceName');
  626. // Sanitize invalid device names
  627. if (!Device.validateName(this.identity.body.deviceName)) {
  628. const sanitized = Device.sanitizeName(this.identity.body.deviceName);
  629. debug(`Sanitized invalid device name "${this.identity.body.deviceName}" to "${sanitized}"`);
  630. this.identity.body.deviceName = sanitized;
  631. }
  632. this._connection = await this._encryptClient(connection);
  633. // Starting with protocol version 8, the devices are expected to
  634. // exchange identity packets again after TLS negotiation
  635. if (this.identity.body.protocolVersion >= 8) {
  636. await this._exchangeIdentities();
  637. }
  638. } catch (e) {
  639. this.close();
  640. throw e;
  641. }
  642. }
  643. /**
  644. * Negotiate an outgoing connection
  645. *
  646. * @param {Gio.SocketConnection} connection - The remote connection
  647. */
  648. async open(connection) {
  649. debug(`${this.address} (${this.uuid})`);
  650. try {
  651. this._connection = connection;
  652. this.backend.channels.set(this.address, this);
  653. await connection.get_output_stream().write_all_async(
  654. this.backend.identity.serialize(),
  655. GLib.PRIORITY_DEFAULT,
  656. this.cancellable);
  657. this._connection = await this._encryptServer(connection);
  658. // Starting with protocol version 8, the devices are expected to
  659. // exchange identity packets again after TLS negotiation
  660. if (this.identity.body.protocolVersion >= 8) {
  661. await this._exchangeIdentities();
  662. }
  663. } catch (e) {
  664. this.close();
  665. throw e;
  666. }
  667. }
  668. /**
  669. * Close all streams associated with this channel, silencing any errors
  670. */
  671. close() {
  672. if (this.closed)
  673. return;
  674. debug(`${this.address} (${this.uuid})`);
  675. this._closed = true;
  676. this.notify('closed');
  677. this.backend.channels.delete(this.address);
  678. this.cancellable.cancel();
  679. if (this._connection)
  680. this._connection.close_async(GLib.PRIORITY_DEFAULT, null, null);
  681. if (this.input_stream)
  682. this.input_stream.close_async(GLib.PRIORITY_DEFAULT, null, null);
  683. if (this.output_stream)
  684. this.output_stream.close_async(GLib.PRIORITY_DEFAULT, null, null);
  685. }
  686. async download(packet, target, cancellable = null) {
  687. const address = Gio.InetSocketAddress.new_from_string(this.host,
  688. packet.payloadTransferInfo.port);
  689. const client = new Gio.SocketClient({enable_proxy: false});
  690. const connection = await client.connect_async(address, cancellable)
  691. .then(this._encryptClient.bind(this));
  692. // Start the transfer
  693. const transferredSize = await target.splice_async(
  694. connection.input_stream,
  695. (Gio.OutputStreamSpliceFlags.CLOSE_SOURCE |
  696. Gio.OutputStreamSpliceFlags.CLOSE_TARGET),
  697. GLib.PRIORITY_DEFAULT, cancellable);
  698. // If we get less than expected, we've certainly got corruption
  699. if (transferredSize < packet.payloadSize) {
  700. throw new Gio.IOErrorEnum({
  701. code: Gio.IOErrorEnum.FAILED,
  702. message: `Incomplete: ${transferredSize}/${packet.payloadSize}`,
  703. });
  704. // TODO: sometimes kdeconnect-android under-reports a file's size
  705. // https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1157
  706. } else if (transferredSize > packet.payloadSize) {
  707. logError(new Gio.IOErrorEnum({
  708. code: Gio.IOErrorEnum.FAILED,
  709. message: `Extra Data: ${transferredSize - packet.payloadSize}`,
  710. }));
  711. }
  712. }
  713. async upload(packet, source, size, cancellable = null) {
  714. // Start listening on the first available port between 1739-1764
  715. const listener = new Gio.SocketListener();
  716. let port = TRANSFER_MIN;
  717. while (port <= TRANSFER_MAX) {
  718. try {
  719. listener.add_inet_port(port, null);
  720. break;
  721. } catch (e) {
  722. if (port < TRANSFER_MAX) {
  723. port++;
  724. continue;
  725. } else {
  726. throw e;
  727. }
  728. }
  729. }
  730. // Listen for the incoming connection
  731. const acceptConnection = listener.accept_async(cancellable)
  732. .then(result => this._encryptServer(result[0]));
  733. // Create an upload request
  734. packet.body.payloadHash = this.checksum;
  735. packet.payloadSize = size;
  736. packet.payloadTransferInfo = {port: port};
  737. const requestUpload = this.sendPacket(new Core.Packet(packet),
  738. cancellable);
  739. // Request an upload stream, accept the connection and get the output
  740. const [, connection] = await Promise.all([requestUpload,
  741. acceptConnection]);
  742. // Start the transfer
  743. const transferredSize = await connection.output_stream.splice_async(
  744. source,
  745. (Gio.OutputStreamSpliceFlags.CLOSE_SOURCE |
  746. Gio.OutputStreamSpliceFlags.CLOSE_TARGET),
  747. GLib.PRIORITY_DEFAULT, cancellable);
  748. if (transferredSize !== size) {
  749. throw new Gio.IOErrorEnum({
  750. code: Gio.IOErrorEnum.PARTIAL_INPUT,
  751. message: 'Transfer incomplete',
  752. });
  753. }
  754. }
  755. async rejectTransfer(packet) {
  756. try {
  757. if (!packet || !packet.hasPayload())
  758. return;
  759. if (packet.payloadTransferInfo.port === undefined)
  760. return;
  761. const address = Gio.InetSocketAddress.new_from_string(this.host,
  762. packet.payloadTransferInfo.port);
  763. const client = new Gio.SocketClient({enable_proxy: false});
  764. const connection = await client.connect_async(address, null)
  765. .then(this._encryptClient.bind(this));
  766. connection.close_async(GLib.PRIORITY_DEFAULT, null, null);
  767. } catch (e) {
  768. debug(e, this.device.name);
  769. }
  770. }
  771. });