File indexing completed on 2025-02-02 04:47:49

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 package org.kde.kdeconnect.Backends.LanBackend;
0008 
0009 import android.content.Context;
0010 import android.content.SharedPreferences;
0011 import android.net.Network;
0012 import android.os.Build;
0013 import android.preference.PreferenceManager;
0014 import android.util.Log;
0015 
0016 import androidx.annotation.Nullable;
0017 import androidx.annotation.WorkerThread;
0018 
0019 import org.json.JSONException;
0020 import org.kde.kdeconnect.Backends.BaseLink;
0021 import org.kde.kdeconnect.Backends.BaseLinkProvider;
0022 import org.kde.kdeconnect.Device;
0023 import org.kde.kdeconnect.DeviceInfo;
0024 import org.kde.kdeconnect.Helpers.DeviceHelper;
0025 import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
0026 import org.kde.kdeconnect.Helpers.ThreadHelper;
0027 import org.kde.kdeconnect.Helpers.TrustedNetworkHelper;
0028 import org.kde.kdeconnect.KdeConnect;
0029 import org.kde.kdeconnect.NetworkPacket;
0030 import org.kde.kdeconnect.UserInterface.CustomDevicesActivity;
0031 import org.kde.kdeconnect.UserInterface.SettingsFragment;
0032 
0033 import java.io.BufferedReader;
0034 import java.io.IOException;
0035 import java.io.InputStreamReader;
0036 import java.io.OutputStream;
0037 import java.net.DatagramPacket;
0038 import java.net.DatagramSocket;
0039 import java.net.InetAddress;
0040 import java.net.InetSocketAddress;
0041 import java.net.ServerSocket;
0042 import java.net.Socket;
0043 import java.net.SocketException;
0044 import java.net.UnknownHostException;
0045 import java.security.cert.Certificate;
0046 import java.util.ArrayList;
0047 import java.util.HashMap;
0048 import java.util.List;
0049 import java.util.concurrent.ConcurrentHashMap;
0050 
0051 import javax.net.SocketFactory;
0052 import javax.net.ssl.SSLSocket;
0053 
0054 import kotlin.text.Charsets;
0055 
0056 /**
0057  * This LanLinkProvider creates {@link LanLink}s to other devices on the same
0058  * WiFi network. The first packet sent over a socket must be an
0059  * {@link DeviceInfo#toIdentityPacket()}.
0060  *
0061  * @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted)
0062  */
0063 public class LanLinkProvider extends BaseLinkProvider {
0064 
0065     final static int UDP_PORT = 1716;
0066     final static int MIN_PORT = 1716;
0067     final static int MAX_PORT = 1764;
0068     final static int PAYLOAD_TRANSFER_MIN_PORT = 1739;
0069 
0070     final static int MAX_UDP_PACKET_SIZE = 1024 * 512;
0071 
0072     final static long MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE = 500L;
0073 
0074     private final Context context;
0075 
0076     final HashMap<String, LanLink> visibleDevices = new HashMap<>(); // Links by device id
0077 
0078     final ConcurrentHashMap<String, Long> lastConnectionTime = new ConcurrentHashMap<>();
0079 
0080     private ServerSocket tcpServer;
0081     private DatagramSocket udpServer;
0082 
0083     private MdnsDiscovery mdnsDiscovery;
0084 
0085     private long lastBroadcast = 0;
0086     private final static long delayBetweenBroadcasts = 200;
0087 
0088     private boolean listening = false;
0089 
0090     public void onConnectionLost(BaseLink link) {
0091         String deviceId = link.getDeviceId();
0092         visibleDevices.remove(deviceId);
0093         super.onConnectionLost(link);
0094     }
0095 
0096     //They received my UDP broadcast and are connecting to me. The first thing they send should be their identity packet.
0097     @WorkerThread
0098     private void tcpPacketReceived(Socket socket) throws IOException {
0099 
0100         NetworkPacket networkPacket;
0101         try {
0102             BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
0103             String message = reader.readLine();
0104             networkPacket = NetworkPacket.unserialize(message);
0105             //Log.e("TcpListener", "Received TCP packet: " + networkPacket.serialize());
0106         } catch (Exception e) {
0107             Log.e("KDE/LanLinkProvider", "Exception while receiving TCP packet", e);
0108             return;
0109         }
0110 
0111         if (!networkPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
0112             Log.e("KDE/LanLinkProvider", "Expecting an identity packet instead of " + networkPacket.getType());
0113             return;
0114         }
0115 
0116         Log.i("KDE/LanLinkProvider", "identity packet received from a TCP connection from " + networkPacket.getString("deviceName"));
0117         identityPacketReceived(networkPacket, socket, LanLink.ConnectionStarted.Locally);
0118     }
0119 
0120     //I've received their broadcast and should connect to their TCP socket and send my identity.
0121     @WorkerThread
0122     private void udpPacketReceived(DatagramPacket packet) throws JSONException, IOException {
0123 
0124         final InetAddress address = packet.getAddress();
0125 
0126         String message = new String(packet.getData(), Charsets.UTF_8);
0127         final NetworkPacket identityPacket = NetworkPacket.unserialize(message);
0128         final String deviceId = identityPacket.getString("deviceId");
0129         if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
0130             Log.e("KDE/LanLinkProvider", "Expecting an UDP identity packet");
0131             return;
0132         }
0133 
0134         String myId = DeviceHelper.getDeviceId(context);
0135         if (deviceId.equals(myId)) {
0136             //Ignore my own broadcast
0137             return;
0138         }
0139 
0140         long now = System.currentTimeMillis();
0141         Long last =  lastConnectionTime.get(deviceId);
0142         if (last != null && (last + MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE > now)) {
0143             Log.i("LanLinkProvider", "Discarding second UDP packet from the same device " + deviceId + " received too quickly");
0144             return;
0145         }
0146         lastConnectionTime.put(deviceId, now);
0147 
0148         int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT);
0149         if (tcpPort < MIN_PORT || tcpPort > MAX_PORT) {
0150             Log.e("LanLinkProvider", "TCP port outside of kdeconnect's range");
0151             return;
0152         }
0153 
0154         Log.i("KDE/LanLinkProvider", "Broadcast identity packet received from " + identityPacket.getString("deviceName"));
0155 
0156         SocketFactory socketFactory = SocketFactory.getDefault();
0157         Socket socket = socketFactory.createSocket(address, tcpPort);
0158         configureSocket(socket);
0159 
0160         DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
0161         NetworkPacket myIdentity = myDeviceInfo.toIdentityPacket();
0162 
0163         OutputStream out = socket.getOutputStream();
0164         out.write(myIdentity.serialize().getBytes());
0165         out.flush();
0166 
0167         identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Remotely);
0168     }
0169 
0170     private void configureSocket(Socket socket) {
0171         try {
0172             socket.setKeepAlive(true);
0173         } catch (SocketException e) {
0174             Log.e("LanLink", "Exception", e);
0175         }
0176     }
0177 
0178     /**
0179      * Called when a new 'identity' packet is received. Those are passed here by
0180      * {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}.
0181      * <p>
0182      * Should be called on a new thread since it blocks until the handshake is completed.
0183      * </p><p>
0184      * If the remote device should be connected, this calls {@link #addLink}.
0185      * Otherwise, if there was an Exception, we unpair from that device.
0186      * </p>
0187      *
0188      * @param identityPacket    identity of a remote device
0189      * @param socket            a new Socket, which should be used to receive packets from the remote device
0190      * @param connectionStarted which side started this connection
0191      */
0192     @WorkerThread
0193     private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted) throws IOException {
0194 
0195         String myId = DeviceHelper.getDeviceId(context);
0196         final String deviceId = identityPacket.getString("deviceId");
0197         if (deviceId.equals(myId)) {
0198             Log.e("KDE/LanLinkProvider", "Somehow I'm connected to myself, ignoring. This should not happen.");
0199             return;
0200         }
0201 
0202         // If I'm the TCP server I will be the SSL client and viceversa.
0203         final boolean clientMode = (connectionStarted == LanLink.ConnectionStarted.Locally);
0204 
0205         SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
0206         boolean isDeviceTrusted = preferences.getBoolean(deviceId, false);
0207 
0208         if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) {
0209             //Device paired with and old version, we can't use it as we lack the certificate
0210             Device device = KdeConnect.getInstance().getDevice(deviceId);
0211             if (device == null) {
0212                 return;
0213             }
0214             device.unpair();
0215             //Retry as unpaired
0216             identityPacketReceived(identityPacket, socket, connectionStarted);
0217         }
0218 
0219         String deviceName = identityPacket.getString("deviceName", "unknown");
0220         Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + deviceName + " trusted:" + isDeviceTrusted);
0221 
0222         final SSLSocket sslSocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode);
0223         sslSocket.addHandshakeCompletedListener(event -> {
0224             String mode = clientMode ? "client" : "server";
0225             try {
0226                 Certificate certificate = event.getPeerCertificates()[0];
0227                 DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
0228                 Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + deviceName + " secured with " + event.getCipherSuite());
0229                 addLink(sslSocket, deviceInfo);
0230             } catch (IOException e) {
0231                 Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + deviceName, e);
0232                 Device device = KdeConnect.getInstance().getDevice(deviceId);
0233                 if (device == null) {
0234                     return;
0235                 }
0236                 device.unpair();
0237             }
0238         });
0239 
0240         //Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection
0241         Log.d("LanLinkProvider", "Starting handshake");
0242         sslSocket.startHandshake();
0243         Log.d("LanLinkProvider", "Handshake done");
0244     }
0245 
0246     /**
0247      * Add or update a link in the {@link #visibleDevices} map.
0248      *
0249      * @param socket           a new Socket, which should be used to send and receive packets from the remote device
0250      * @param deviceInfo       remote device info
0251      * @throws IOException if an exception is thrown by {@link LanLink#reset(SSLSocket)}
0252      */
0253     private void addLink(SSLSocket socket, DeviceInfo deviceInfo) throws IOException {
0254         LanLink link = visibleDevices.get(deviceInfo.id);
0255         if (link != null) {
0256             if (!link.getDeviceInfo().certificate.equals(deviceInfo.certificate)) {
0257                 Log.e("LanLinkProvider", "LanLink was asked to replace a socket but the certificate doesn't match, aborting");
0258                 return;
0259             }
0260             // Update existing link
0261             Log.d("KDE/LanLinkProvider", "Reusing same link for device " + deviceInfo.id);
0262             final Socket oldSocket = link.reset(socket);
0263         } else {
0264             // Create a new link
0265             Log.d("KDE/LanLinkProvider", "Creating a new link for device " + deviceInfo.id);
0266             link = new LanLink(context, deviceInfo, this, socket);
0267             visibleDevices.put(deviceInfo.id, link);
0268             onConnectionReceived(link);
0269         }
0270     }
0271 
0272     public LanLinkProvider(Context context) {
0273         this.context = context;
0274         this.mdnsDiscovery = new MdnsDiscovery(context, this);
0275     }
0276 
0277     private void setupUdpListener() {
0278         try {
0279             udpServer = new DatagramSocket(null);
0280             udpServer.setReuseAddress(true);
0281             udpServer.setBroadcast(true);
0282         } catch (SocketException e) {
0283             Log.e("LanLinkProvider", "Error creating udp server", e);
0284             throw new RuntimeException(e);
0285         }
0286         try {
0287             udpServer.bind(new InetSocketAddress(UDP_PORT));
0288         } catch (SocketException e) {
0289             // We ignore this exception and continue without being able to receive broadcasts instead of crashing the app.
0290             Log.e("LanLinkProvider", "Error binding udp server. We can send udp broadcasts but not receive them", e);
0291         }
0292         ThreadHelper.execute(() -> {
0293             Log.i("UdpListener", "Starting UDP listener");
0294             while (listening) {
0295                 try {
0296                     DatagramPacket packet = new DatagramPacket(new byte[MAX_UDP_PACKET_SIZE], MAX_UDP_PACKET_SIZE);
0297                     udpServer.receive(packet);
0298                     ThreadHelper.execute(() -> {
0299                         try {
0300                             udpPacketReceived(packet);
0301                         } catch (JSONException | IOException e) {
0302                             Log.e("LanLinkProvider", "Exception receiving incoming UDP connection", e);
0303                         }
0304                     });
0305                 } catch (IOException e) {
0306                     Log.e("LanLinkProvider", "UdpReceive exception", e);
0307                     onNetworkChange(null); // Trigger a UDP broadcast to try to get them to connect to us instead
0308                 }
0309             }
0310             Log.w("UdpListener", "Stopping UDP listener");
0311         });
0312     }
0313 
0314     private void setupTcpListener() {
0315         try {
0316             tcpServer = openServerSocketOnFreePort(MIN_PORT);
0317         } catch (IOException e) {
0318             Log.e("LanLinkProvider", "Error creating tcp server", e);
0319             throw new RuntimeException(e);
0320         }
0321         ThreadHelper.execute(() -> {
0322             while (listening) {
0323                 try {
0324                     Socket socket = tcpServer.accept();
0325                     configureSocket(socket);
0326                     ThreadHelper.execute(() -> {
0327                         try {
0328                             tcpPacketReceived(socket);
0329                         } catch (IOException e) {
0330                             Log.e("LanLinkProvider", "Exception receiving incoming TCP connection", e);
0331                         }
0332                     });
0333                 } catch (Exception e) {
0334                     Log.e("LanLinkProvider", "TcpReceive exception", e);
0335                 }
0336             }
0337             Log.w("TcpListener", "Stopping TCP listener");
0338         });
0339 
0340     }
0341 
0342     static ServerSocket openServerSocketOnFreePort(int minPort) throws IOException {
0343         int tcpPort = minPort;
0344         while (tcpPort <= MAX_PORT) {
0345             try {
0346                 ServerSocket candidateServer = new ServerSocket();
0347                 candidateServer.bind(new InetSocketAddress(tcpPort));
0348                 Log.i("KDE/LanLink", "Using port " + tcpPort);
0349                 return candidateServer;
0350             } catch (IOException e) {
0351                 tcpPort++;
0352                 if (tcpPort == MAX_PORT) {
0353                     Log.e("KDE/LanLink", "No ports available");
0354                     throw e; //Propagate exception
0355                 }
0356             }
0357         }
0358         throw new RuntimeException("This should not be reachable");
0359     }
0360 
0361     private void broadcastUdpIdentityPacket(@Nullable Network network) {
0362         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
0363         if (!preferences.getBoolean(SettingsFragment.KEY_UDP_BROADCAST_ENABLED, true)) {
0364             Log.i("LanLinkProvider", "UDP broadcast is disabled in settings. Skipping.");
0365             return;
0366         }
0367 
0368         ThreadHelper.execute(() -> {
0369             List<String> ipStringList = CustomDevicesActivity
0370                     .getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context));
0371 
0372             if (TrustedNetworkHelper.isTrustedNetwork(context)) {
0373                 ipStringList.add("255.255.255.255"); //Default: broadcast.
0374             } else {
0375                 Log.i("LanLinkProvider", "Current network isn't trusted, not broadcasting");
0376             }
0377 
0378             ArrayList<InetAddress> ipList = new ArrayList<>();
0379             for (String ip : ipStringList) {
0380                 try {
0381                     ipList.add(InetAddress.getByName(ip));
0382                 } catch (UnknownHostException e) {
0383                     e.printStackTrace();
0384                 }
0385             }
0386 
0387             if (ipList.isEmpty()) {
0388                 return;
0389             }
0390 
0391             sendUdpIdentityPacket(ipList, network);
0392         });
0393     }
0394 
0395     @WorkerThread
0396     public void sendUdpIdentityPacket(List<InetAddress> ipList, @Nullable Network network) {
0397         if (tcpServer == null || !tcpServer.isBound()) {
0398             Log.i("LanLinkProvider", "Won't broadcast UDP packet if TCP socket is not ready yet");
0399             return;
0400         }
0401 
0402         DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
0403         NetworkPacket identity = myDeviceInfo.toIdentityPacket();
0404         identity.set("tcpPort", tcpServer.getLocalPort());
0405 
0406         byte[] bytes;
0407         try {
0408             bytes = identity.serialize().getBytes(Charsets.UTF_8);
0409         } catch (JSONException e) {
0410             Log.e("KDE/LanLinkProvider", "Failed to serialize identity packet", e);
0411             return;
0412         }
0413 
0414         DatagramSocket socket;
0415         try {
0416             socket = new DatagramSocket();
0417             if (network != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
0418                 try {
0419                     network.bindSocket(socket);
0420                 } catch (IOException e) {
0421                     Log.w("LanLinkProvider", "Couldn't bind socket to the network");
0422                     e.printStackTrace();
0423                 }
0424             }
0425             socket.setReuseAddress(true);
0426             socket.setBroadcast(true);
0427         } catch (SocketException e) {
0428             Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket", e);
0429             return;
0430         }
0431 
0432         for (InetAddress ip : ipList) {
0433             try {
0434                 socket.send(new DatagramPacket(bytes, bytes.length, ip, MIN_PORT));
0435                 //Log.i("KDE/LanLinkProvider","Udp identity packet sent to address "+client);
0436             } catch (IOException e) {
0437                 Log.e("KDE/LanLinkProvider", "Sending udp identity packet failed. Invalid address? (" + ip.toString() + ")", e);
0438             }
0439         }
0440 
0441         socket.close();
0442     }
0443 
0444     @Override
0445     public void onStart() {
0446         //Log.i("KDE/LanLinkProvider", "onStart");
0447         if (!listening) {
0448 
0449             listening = true;
0450 
0451             setupUdpListener();
0452             setupTcpListener();
0453 
0454             mdnsDiscovery.startDiscovering();
0455             mdnsDiscovery.startAnnouncing();
0456 
0457             broadcastUdpIdentityPacket(null);
0458         }
0459     }
0460 
0461     @Override
0462     public void onNetworkChange(@Nullable Network network) {
0463         if (System.currentTimeMillis() < lastBroadcast + delayBetweenBroadcasts) {
0464             Log.i("LanLinkProvider", "onNetworkChange: relax cowboy");
0465             return;
0466         }
0467         lastBroadcast = System.currentTimeMillis();
0468 
0469         broadcastUdpIdentityPacket(network);
0470         mdnsDiscovery.stopDiscovering();
0471         mdnsDiscovery.startDiscovering();
0472     }
0473 
0474     @Override
0475     public void onStop() {
0476         //Log.i("KDE/LanLinkProvider", "onStop");
0477         listening = false;
0478         mdnsDiscovery.stopAnnouncing();
0479         mdnsDiscovery.stopDiscovering();
0480         try {
0481             tcpServer.close();
0482         } catch (Exception e) {
0483             Log.e("LanLink", "Exception", e);
0484         }
0485         try {
0486             udpServer.close();
0487         } catch (Exception e) {
0488             Log.e("LanLink", "Exception", e);
0489         }
0490     }
0491 
0492     @Override
0493     public String getName() {
0494         return "LanLinkProvider";
0495     }
0496 
0497     @Override
0498     public int getPriority() { return 20; }
0499 
0500 }