From 58de1aa868b9e13be172b0ac635e546dfcd3948f Mon Sep 17 00:00:00 2001 From: Simon Redman Date: Sat, 20 Jul 2019 12:29:01 +0200 Subject: [PATCH] Duplicate LanBackend as MulticastBackend --- .../MulticastBackend/MulticastLink.java | 268 +++++++++++ .../MulticastLinkProvider.java | 447 ++++++++++++++++++ .../MulticastPairingHandler.java | 217 +++++++++ 3 files changed, 932 insertions(+) create mode 100644 src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastLink.java create mode 100644 src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastLinkProvider.java create mode 100644 src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastPairingHandler.java diff --git a/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastLink.java b/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastLink.java new file mode 100644 index 00000000..0d3048de --- /dev/null +++ b/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastLink.java @@ -0,0 +1,268 @@ +/* + * Copyright 2014 Albert Vaca Cintora + * Copyright 2019 Simon Redman + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package org.kde.kdeconnect.Backends.MulticastBackend; + +import android.content.Context; +import android.util.Log; + +import org.json.JSONObject; +import org.kde.kdeconnect.Backends.BaseLink; +import org.kde.kdeconnect.Backends.BasePairingHandler; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; +import org.kde.kdeconnect.Helpers.StringsHelper; +import org.kde.kdeconnect.NetworkPacket; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.channels.NotYetConnectedException; + +import javax.net.ssl.SSLSocket; + +public class MulticastLink extends BaseLink { + + static final String LOG_TAG = "MulticastLink"; + + public interface LinkDisconnectedCallback { + void linkDisconnected(MulticastLink brokenLink); + } + + public enum ConnectionStarted { + Locally, Remotely + } + + private ConnectionStarted connectionSource; // If the other device sent me a broadcast, + // I should not close the connection with it + // because it's probably trying to find me and + // potentially ask for pairing. + + private volatile SSLSocket socket = null; + + private final LinkDisconnectedCallback callback; + + @Override + public void disconnect() { + Log.i(LOG_TAG + "Disconnect","socket:"+ socket.hashCode()); + try { + socket.close(); + } catch (IOException e) { + Log.e(LOG_TAG, "Error", e); + } + } + + //Returns the old socket + public SSLSocket reset(final SSLSocket newSocket, ConnectionStarted connectionSource) throws IOException { + + SSLSocket oldSocket = socket; + socket = newSocket; + + this.connectionSource = connectionSource; + + if (oldSocket != null) { + oldSocket.close(); //This should cancel the readThread + } + + //Log.e(LOG_TAG, "Start listening"); + //Create a thread to take care of incoming data for the new socket + new Thread(() -> { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(newSocket.getInputStream(), StringsHelper.UTF8)); + while (true) { + String packet; + try { + packet = reader.readLine(); + } catch (SocketTimeoutException e) { + continue; + } + if (packet == null) { + throw new IOException("End of stream"); + } + if (packet.isEmpty()) { + continue; + } + NetworkPacket np = NetworkPacket.unserialize(packet); + receivedNetworkPacket(np); + } + } catch (Exception e) { + Log.i(LOG_TAG, "Socket closed: " + newSocket.hashCode() + ". Reason: " + e.getMessage()); + try { Thread.sleep(300); } catch (InterruptedException ignored) {} // Wait a bit because we might receive a new socket meanwhile + boolean thereIsaANewSocket = (newSocket != socket); + if (!thereIsaANewSocket) { + callback.linkDisconnected(MulticastLink.this); + } + } + }).start(); + + return oldSocket; + } + + public MulticastLink(Context context, String deviceId, MulticastLinkProvider linkProvider, SSLSocket socket, ConnectionStarted connectionSource) throws IOException { + super(context, deviceId, linkProvider); + callback = linkProvider; + reset(socket, connectionSource); + } + + + @Override + public String getName() { + return "LanLink"; + } + + @Override + public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) { + return new MulticastPairingHandler(device, callback); + } + + //Blocking, do not call from main thread + @Override + public boolean sendPacket(NetworkPacket np, final Device.SendPacketStatusCallback callback) { + if (socket == null) { + Log.e(LOG_TAG + "sendPacket", "Not yet connected"); + callback.onFailure(new NotYetConnectedException()); + return false; + } + + try { + + //Prepare socket for the payload + final ServerSocket server; + if (np.hasPayload()) { + server = MulticastLinkProvider.openServerSocketOnFreePort(MulticastLinkProvider.PAYLOAD_TRANSFER_MIN_PORT); + JSONObject payloadTransferInfo = new JSONObject(); + payloadTransferInfo.put("port", server.getLocalPort()); + np.setPayloadTransferInfo(payloadTransferInfo); + } else { + server = null; + } + + //Log.e("LanLink/sendPacket", np.getType()); + + //Send body of the network package + try { + OutputStream writer = socket.getOutputStream(); + writer.write(np.serialize().getBytes(StringsHelper.UTF8)); + writer.flush(); + } catch (Exception e) { + disconnect(); //main socket is broken, disconnect + throw e; + } + + //Send payload + if (server != null) { + Socket payloadSocket = null; + OutputStream outputStream = null; + InputStream inputStream; + try { + //Wait a maximum of 10 seconds for the other end to establish a connection with our socket, close it afterwards + server.setSoTimeout(10*1000); + + payloadSocket = server.accept(); + + //Convert to SSL if needed + payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false); + + outputStream = payloadSocket.getOutputStream(); + inputStream = np.getPayload().getInputStream(); + + Log.i(LOG_TAG, "Beginning to send payload"); + byte[] buffer = new byte[4096]; + int bytesRead; + long size = np.getPayloadSize(); + long progress = 0; + long timeSinceLastUpdate = -1; + while (!np.isCanceled() && (bytesRead = inputStream.read(buffer)) != -1) { + //Log.e("ok",""+bytesRead); + progress += bytesRead; + outputStream.write(buffer, 0, bytesRead); + if (size > 0) { + if (timeSinceLastUpdate + 500 < System.currentTimeMillis()) { //Report progress every half a second + long percent = ((100 * progress) / size); + callback.onProgressChanged((int) percent); + timeSinceLastUpdate = System.currentTimeMillis(); + } + } + } + outputStream.flush(); + Log.i(LOG_TAG, "Finished sending payload ("+progress+" bytes written)"); + } finally { + try { server.close(); } catch (Exception ignored) { } + try { payloadSocket.close(); } catch (Exception ignored) { } + np.getPayload().close(); + try { outputStream.close(); } catch (Exception ignored) { } + } + } + + if (!np.isCanceled()) { + callback.onSuccess(); + } + return true; + } catch (Exception e) { + if (callback != null) { + callback.onFailure(e); + } + return false; + } finally { + //Make sure we close the payload stream, if any + if (np.hasPayload()) { + np.getPayload().close(); + } + } + } + + private void receivedNetworkPacket(NetworkPacket np) { + + if (np.hasPayloadTransferInfo()) { + Socket payloadSocket = new Socket(); + try { + int tcpPort = np.getPayloadTransferInfo().getInt("port"); + InetSocketAddress deviceAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); + payloadSocket.connect(new InetSocketAddress(deviceAddress.getAddress(), tcpPort)); + payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, true); + np.setPayload(new NetworkPacket.Payload(payloadSocket, np.getPayloadSize())); + } catch (Exception e) { + try { payloadSocket.close(); } catch(Exception ignored) { } + Log.e(LOG_TAG, "Exception connecting to payload remote socket", e); + } + + } + + packageReceived(np); + } + + @Override + public boolean linkShouldBeKeptAlive() { + + return true; //FIXME: Current implementation is broken, so for now we will keep links always established + + //We keep the remotely initiated connections, since the remotes require them if they want to request + //pairing to us, or connections that are already paired. + //return (connectionSource == ConnectionStarted.Remotely); + + } +} diff --git a/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastLinkProvider.java b/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastLinkProvider.java new file mode 100644 index 00000000..5f89d13a --- /dev/null +++ b/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastLinkProvider.java @@ -0,0 +1,447 @@ +/* + * Copyright 2014 Albert Vaca Cintora + * Copyright 2019 Simon Redman + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package org.kde.kdeconnect.Backends.MulticastBackend; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Base64; +import android.util.Log; + +import org.kde.kdeconnect.Backends.BaseLink; +import org.kde.kdeconnect.Backends.BaseLinkProvider; +import org.kde.kdeconnect.BackgroundService; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.Helpers.DeviceHelper; +import org.kde.kdeconnect.Helpers.NetworkHelper; +import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; +import org.kde.kdeconnect.Helpers.StringsHelper; +import org.kde.kdeconnect.NetworkPacket; +import org.kde.kdeconnect.UserInterface.CustomDevicesActivity; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocket; + +/** + * This BaseLinkProvider creates {@link MulticastLink}s to other devices on the same + * WiFi network. The first packet sent over a socket must be an + * {@link NetworkPacket#createIdentityPacket(Context)}. + * + * @see #identityPacketReceived(NetworkPacket, Socket, MulticastLink.ConnectionStarted) + */ +public class MulticastLinkProvider extends BaseLinkProvider implements MulticastLink.LinkDisconnectedCallback { + + static final String LOG_TAG = "MulticastLink"; + + private final static int MIN_PORT = 1716; + private final static int MAX_PORT = 1764; + final static int PAYLOAD_TRANSFER_MIN_PORT = 1739; + + private final Context context; + + private final HashMap visibleComputers = new HashMap<>(); //Links by device id + + private ServerSocket tcpServer; + private DatagramSocket udpServer; + + private boolean listening = false; + + // To prevent infinte loop between Android < IceCream because both device can only broadcast identity package but cannot connect via TCP + private final ArrayList reverseConnectionBlackList = new ArrayList<>(); + + @Override // SocketClosedCallback + public void linkDisconnected(MulticastLink brokenLink) { + String deviceId = brokenLink.getDeviceId(); + visibleComputers.remove(deviceId); + connectionLost(brokenLink); + } + + //They received my UDP broadcast and are connecting to me. The first thing they sned should be their identity. + private void tcpPacketReceived(Socket socket) { + + NetworkPacket networkPacket; + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String message = reader.readLine(); + networkPacket = NetworkPacket.unserialize(message); + //Log.e("TcpListener","Received TCP package: "+networkPacket.serialize()); + } catch (Exception e) { + Log.e(LOG_TAG, "Exception while receiving TCP packet", e); + return; + } + + if (!networkPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { + Log.e(LOG_TAG, "Expecting an identity package instead of " + networkPacket.getType()); + return; + } + + Log.i(LOG_TAG, "Identity package received from a TCP connection from " + networkPacket.getString("deviceName")); + identityPacketReceived(networkPacket, socket, MulticastLink.ConnectionStarted.Locally); + } + + //I've received their broadcast and should connect to their TCP socket and send my identity. + private void udpPacketReceived(DatagramPacket packet) { + + final InetAddress address = packet.getAddress(); + + try { + + String message = new String(packet.getData(), StringsHelper.UTF8); + final NetworkPacket identityPacket = NetworkPacket.unserialize(message); + final String deviceId = identityPacket.getString("deviceId"); + if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { + Log.e(LOG_TAG, "Expecting an UDP identity package"); + return; + } else { + String myId = DeviceHelper.getDeviceId(context); + if (deviceId.equals(myId)) { + //Ignore my own broadcast + return; + } + } + + Log.i(LOG_TAG, "Broadcast identity package received from " + identityPacket.getString("deviceName")); + + int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT); + + SocketFactory socketFactory = SocketFactory.getDefault(); + Socket socket = socketFactory.createSocket(address, tcpPort); + configureSocket(socket); + + OutputStream out = socket.getOutputStream(); + NetworkPacket myIdentity = NetworkPacket.createIdentityPacket(context); + out.write(myIdentity.serialize().getBytes()); + out.flush(); + + identityPacketReceived(identityPacket, socket, MulticastLink.ConnectionStarted.Remotely); + + } catch (Exception e) { + Log.e(LOG_TAG, "Cannot connect to " + address, e); + if (!reverseConnectionBlackList.contains(address)) { + Log.w(LOG_TAG, "Blacklisting " + address); + reverseConnectionBlackList.add(address); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + reverseConnectionBlackList.remove(address); + } + }, 5 * 1000); + + // Try to cause a reverse connection + onNetworkChange(); + } + } + } + + private void configureSocket(Socket socket) { + try { + socket.setKeepAlive(true); + } catch (SocketException e) { + Log.e(LOG_TAG, "Exception", e); + } + } + + /** + * Called when a new 'identity' packet is received. Those are passed here by + * {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}. + *

+ * If the remote device should be connected, this calls {@link #addLink}. + * Otherwise, if there was an Exception, we unpair from that device. + *

+ * + * @param identityPacket identity of a remote device + * @param socket a new Socket, which should be used to receive packets from the remote device + * @param connectionStarted which side started this connection + */ + private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final MulticastLink.ConnectionStarted connectionStarted) { + + String myId = DeviceHelper.getDeviceId(context); + final String deviceId = identityPacket.getString("deviceId"); + if (deviceId.equals(myId)) { + Log.e(LOG_TAG, "Somehow I'm connected to myself, ignoring. This should not happen."); + return; + } + + // If I'm the TCP server I will be the SSL client and viceversa. + final boolean clientMode = (connectionStarted == MulticastLink.ConnectionStarted.Locally); + + // Do the SSL handshake + try { + SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); + boolean isDeviceTrusted = preferences.getBoolean(deviceId, false); + + if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) { + //Device paired with and old version, we can't use it as we lack the certificate + BackgroundService.RunCommand(context, service -> { + Device device = service.getDevice(deviceId); + if (device == null) return; + device.unpair(); + //Retry as unpaired + identityPacketReceived(identityPacket, socket, connectionStarted); + }); + } + + Log.i(LOG_TAG, "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted); + + final SSLSocket sslsocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode); + sslsocket.addHandshakeCompletedListener(event -> { + String mode = clientMode ? "client" : "server"; + try { + Certificate certificate = event.getPeerCertificates()[0]; + identityPacket.set("certificate", Base64.encodeToString(certificate.getEncoded(), 0)); + Log.i(LOG_TAG, "Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite()); + addLink(identityPacket, sslsocket, connectionStarted); + } catch (Exception e) { + Log.e(LOG_TAG, "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e); + BackgroundService.RunCommand(context, service -> { + Device device = service.getDevice(deviceId); + if (device == null) return; + device.unpair(); + }); + } + }); + //Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection + new Thread(() -> { + try { + synchronized (this) { + sslsocket.startHandshake(); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Handshake failed with " + identityPacket.getString("deviceName"), e); + + //String[] ciphers = sslsocket.getSupportedCipherSuites(); + //for (String cipher : ciphers) { + // Log.i("SupportedCiphers","cipher: " + cipher); + //} + } + }).start(); + } catch (Exception e) { + Log.e(LOG_TAG, "Exception", e); + } + + } + + /** + * Add or update a link in the {@link #visibleComputers} map. This method is synchronized, which ensures that only one + * link is operated on at a time. + *

+ * Without synchronization, the call to {@link SslHelper#parseCertificate(byte[])} in + * {@link Device#addLink(NetworkPacket, BaseLink)} crashes on some devices running Oreo 8.1 (SDK level 27). + *

+ * + * @param identityPacket representation of remote device + * @param socket a new Socket, which should be used to receive packets from the remote device + * @param connectionOrigin which side started this connection + * @throws IOException if an exception is thrown by {@link MulticastLink#reset(SSLSocket, MulticastLink.ConnectionStarted)} + */ + private void addLink(final NetworkPacket identityPacket, SSLSocket socket, MulticastLink.ConnectionStarted connectionOrigin) throws IOException { + + String deviceId = identityPacket.getString("deviceId"); + MulticastLink currentLink = visibleComputers.get(deviceId); + if (currentLink != null) { + //Update old link + Log.i(LOG_TAG, "Reusing same link for device " + deviceId); + final Socket oldSocket = currentLink.reset(socket, connectionOrigin); + //Log.e(LOG_TAG, "Replacing socket. old: "+ oldSocket.hashCode() + " - new: "+ socket.hashCode()); + } else { + Log.i(LOG_TAG, "Creating a new link for device " + deviceId); + //Let's create the link + MulticastLink link = new MulticastLink(context, deviceId, this, socket, connectionOrigin); + visibleComputers.put(deviceId, link); + connectionAccepted(identityPacket, link); + } + } + + public MulticastLinkProvider(Context context) { + this.context = context; + } + + private void setupUdpListener() { + try { + udpServer = new DatagramSocket(MIN_PORT); + udpServer.setReuseAddress(true); + udpServer.setBroadcast(true); + } catch (SocketException e) { + Log.e(LOG_TAG, "Error creating udp server", e); + return; + } + new Thread(() -> { + while (listening) { + final int bufferSize = 1024 * 512; + byte[] data = new byte[bufferSize]; + DatagramPacket packet = new DatagramPacket(data, bufferSize); + try { + udpServer.receive(packet); + udpPacketReceived(packet); + } catch (Exception e) { + Log.e(LOG_TAG, "UdpReceive exception", e); + } + } + Log.w("UdpListener", "Stopping UDP listener"); + }).start(); + } + + private void setupTcpListener() { + try { + tcpServer = openServerSocketOnFreePort(MIN_PORT); + } catch (Exception e) { + Log.e(LOG_TAG, "Error creating tcp server", e); + return; + } + new Thread(() -> { + while (listening) { + try { + Socket socket = tcpServer.accept(); + configureSocket(socket); + tcpPacketReceived(socket); + } catch (Exception e) { + Log.e(LOG_TAG, "TcpReceive exception", e); + } + } + Log.w("TcpListener", "Stopping TCP listener"); + }).start(); + + } + + static ServerSocket openServerSocketOnFreePort(int minPort) throws IOException { + int tcpPort = minPort; + while (tcpPort <= MAX_PORT) { + try { + ServerSocket candidateServer = new ServerSocket(); + candidateServer.bind(new InetSocketAddress(tcpPort)); + Log.i(LOG_TAG, "Using port " + tcpPort); + return candidateServer; + } catch (IOException e) { + tcpPort++; + if (tcpPort == MAX_PORT) { + Log.e(LOG_TAG, "No ports available"); + throw e; //Propagate exception + } + } + } + throw new RuntimeException("This should not be reachable"); + } + + private void broadcastUdpPacket() { + + if (NetworkHelper.isOnMobileNetwork(context)) { + Log.w(LOG_TAG, "On 3G network, not sending broadcast."); + return; + } + + new Thread(() -> { + ArrayList iplist = CustomDevicesActivity + .getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context)); + iplist.add("255.255.255.255"); //Default: broadcast. + + NetworkPacket identity = NetworkPacket.createIdentityPacket(context); + int port = (tcpServer == null || !tcpServer.isBound()) ? MIN_PORT : tcpServer.getLocalPort(); + identity.set("tcpPort", port); + DatagramSocket socket = null; + byte[] bytes = null; + try { + socket = new DatagramSocket(); + socket.setReuseAddress(true); + socket.setBroadcast(true); + bytes = identity.serialize().getBytes(StringsHelper.UTF8); + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to create DatagramSocket", e); + } + + if (bytes != null) { + //Log.e(LOG_TAG,"Sending packet to "+iplist.size()+" ips"); + for (String ipstr : iplist) { + try { + InetAddress client = InetAddress.getByName(ipstr); + socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT)); + //Log.i(LOG_TAG,"Udp identity package sent to address "+client); + } catch (Exception e) { + Log.e(LOG_TAG, "Sending udp identity package failed. Invalid address? (" + ipstr + ")", e); + } + } + } + + if (socket != null) { + socket.close(); + } + + }).start(); + } + + @Override + public void onStart() { + //Log.i(LOG_TAG, "onStart"); + if (!listening) { + + listening = true; + + setupUdpListener(); + setupTcpListener(); + + broadcastUdpPacket(); + } + } + + @Override + public void onNetworkChange() { + broadcastUdpPacket(); + } + + @Override + public void onStop() { + //Log.i(LOG_TAG, "onStop"); + listening = false; + try { + tcpServer.close(); + } catch (Exception e) { + Log.e(LOG_TAG, "Exception", e); + } + try { + udpServer.close(); + } catch (Exception e) { + Log.e(LOG_TAG, "Exception", e); + } + } + + @Override + public String getName() { + return "MulticastLinkProvider"; + } + +} diff --git a/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastPairingHandler.java b/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastPairingHandler.java new file mode 100644 index 00000000..c3ab93bb --- /dev/null +++ b/src/org/kde/kdeconnect/Backends/MulticastBackend/MulticastPairingHandler.java @@ -0,0 +1,217 @@ +/* + * Copyright 2015 Vineet Garg + * Copyright 2019 Simon Redman + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package org.kde.kdeconnect.Backends.MulticastBackend; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; +import android.util.Log; + +import org.kde.kdeconnect.Backends.BasePairingHandler; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.NetworkPacket; +import org.kde.kdeconnect_tp.R; + +import java.security.cert.CertificateEncodingException; +import java.util.Timer; +import java.util.TimerTask; + +public class MulticastPairingHandler extends BasePairingHandler { + + private Timer mPairingTimer; + + public MulticastPairingHandler(Device device, final PairingHandlerCallback callback) { + super(device, callback); + + if (device.isPaired()) { + mPairStatus = PairStatus.Paired; + } else { + mPairStatus = PairStatus.NotPaired; + } + } + + private NetworkPacket createPairPacket() { + NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR); + np.set("pair", true); + return np; + } + + @Override + public void packageReceived(NetworkPacket np) { + + boolean wantsPair = np.getBoolean("pair"); + + if (wantsPair == isPaired()) { + if (mPairStatus == PairStatus.Requested) { + //Log.e("Device","Unpairing (pair rejected)"); + mPairStatus = PairStatus.NotPaired; + hidePairingNotification(); + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer)); + } + return; + } + + if (wantsPair) { + + if (mPairStatus == PairStatus.Requested) { //We started pairing + + hidePairingNotification(); + + pairingDone(); + + } else { + + // If device is already paired, accept pairing silently + if (mDevice.isPaired()) { + acceptPairing(); + return; + } + + // Pairing notifications are still managed by device as there is no other way to + // know about notificationId to cancel notification when PairActivity is started + // Even putting notificationId in intent does not work because PairActivity can be + // started from MainActivity too, so then notificationId cannot be set + hidePairingNotification(); + mDevice.displayPairingNotification(); + + mPairingTimer = new Timer(); + + mPairingTimer.schedule(new TimerTask() { + @Override + public void run() { + Log.w("KDE/Device","Unpairing (timeout B)"); + mPairStatus = PairStatus.NotPaired; + hidePairingNotification(); + } + }, 25*1000); //Time to show notification, waiting for user to accept (peer will timeout in 30 seconds) + mPairStatus = PairStatus.RequestedByPeer; + mCallback.incomingRequest(); + + } + } else { + Log.i("KDE/Pairing", "Unpair request"); + + if (mPairStatus == PairStatus.Requested) { + hidePairingNotification(); + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer)); + } else if (mPairStatus == PairStatus.Paired) { + mCallback.unpaired(); + } + + mPairStatus = PairStatus.NotPaired; + + } + + } + + @Override + public void requestPairing() { + + Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() { + @Override + public void onSuccess() { + hidePairingNotification(); //Will stop the pairingTimer if it was running + mPairingTimer = new Timer(); + mPairingTimer.schedule(new TimerTask() { + @Override + public void run() { + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_timed_out)); + Log.w("KDE/Device","Unpairing (timeout A)"); + mPairStatus = PairStatus.NotPaired; + } + }, 30*1000); //Time to wait for the other to accept + mPairStatus = PairStatus.Requested; + } + + @Override + public void onFailure(Throwable e) { + Log.e("LanPairing/onFailure", "Exception", e); + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_could_not_send_package)); + } + }; + mDevice.sendPacket(createPairPacket(), statusCallback); + } + + private void hidePairingNotification() { + mDevice.hidePairingNotification(); + if (mPairingTimer != null) { + mPairingTimer .cancel(); + } + } + + @Override + public void acceptPairing() { + hidePairingNotification(); + Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() { + @Override + public void onSuccess() { + pairingDone(); + } + + @Override + public void onFailure(Throwable e) { + Log.e("LanPairing/onFailure", "Exception", e); + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_not_reachable)); + } + }; + mDevice.sendPacket(createPairPacket(), statusCallback); + } + + @Override + public void rejectPairing() { + hidePairingNotification(); + mPairStatus = PairStatus.NotPaired; + NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR); + np.set("pair", false); + mDevice.sendPacket(np); + } + + private void pairingDone() { + // Store device information needed to create a Device object in a future + //Log.e("KDE/PairingDone", "Pairing Done"); + SharedPreferences.Editor editor = mDevice.getContext().getSharedPreferences(mDevice.getDeviceId(), Context.MODE_PRIVATE).edit(); + + try { + String encodedCertificate = Base64.encodeToString(mDevice.certificate.getEncoded(), 0); + editor.putString("certificate", encodedCertificate); + } catch (NullPointerException n) { + Log.w("KDE/PairingDone", "Certificate is null, remote device does not support ssl", n); + } catch (CertificateEncodingException c) { + Log.e("KDE/PairingDOne", "Error encoding certificate", c); + } catch (Exception e) { + Log.e("KDE/Pairng", "Exception", e); + } + editor.apply(); + + mPairStatus = PairStatus.Paired; + mCallback.pairingDone(); + + } + + @Override + public void unpair() { + mPairStatus = PairStatus.NotPaired; + NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR); + np.set("pair", false); + mDevice.sendPacket(np); + } +}