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 }