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

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.util.Log;
0011 
0012 import org.apache.commons.io.IOUtils;
0013 import androidx.annotation.NonNull;
0014 import androidx.annotation.WorkerThread;
0015 
0016 import org.json.JSONObject;
0017 import org.kde.kdeconnect.Backends.BaseLink;
0018 import org.kde.kdeconnect.Backends.BaseLinkProvider;
0019 import org.kde.kdeconnect.Device;
0020 import org.kde.kdeconnect.DeviceInfo;
0021 import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
0022 import org.kde.kdeconnect.Helpers.ThreadHelper;
0023 import org.kde.kdeconnect.NetworkPacket;
0024 
0025 import java.io.BufferedReader;
0026 import java.io.IOException;
0027 import java.io.InputStream;
0028 import java.io.InputStreamReader;
0029 import java.io.OutputStream;
0030 import java.net.InetSocketAddress;
0031 import java.net.ServerSocket;
0032 import java.net.Socket;
0033 import java.net.SocketTimeoutException;
0034 import java.nio.channels.NotYetConnectedException;
0035 
0036 import javax.net.ssl.SSLHandshakeException;
0037 import javax.net.ssl.SSLSocket;
0038 
0039 import kotlin.text.Charsets;
0040 
0041 public class LanLink extends BaseLink {
0042 
0043     public enum ConnectionStarted {
0044         Locally, Remotely
0045     }
0046 
0047     private final DeviceInfo deviceInfo;
0048 
0049     private volatile SSLSocket socket = null;
0050 
0051     @Override
0052     public void disconnect() {
0053         Log.i("LanLink/Disconnect","socket:"+ socket.hashCode());
0054         try {
0055             socket.close();
0056         } catch (IOException e) {
0057             Log.e("LanLink", "Error", e);
0058         }
0059     }
0060 
0061     //Returns the old socket
0062     public SSLSocket reset(final SSLSocket newSocket) throws IOException {
0063 
0064         SSLSocket oldSocket = socket;
0065         socket = newSocket;
0066 
0067         IOUtils.close(oldSocket); //This should cancel the readThread
0068 
0069         //Log.e("LanLink", "Start listening");
0070         //Create a thread to take care of incoming data for the new socket
0071         ThreadHelper.execute(() -> {
0072             try {
0073                 BufferedReader reader = new BufferedReader(new InputStreamReader(newSocket.getInputStream(), Charsets.UTF_8));
0074                 while (true) {
0075                     String packet;
0076                     try {
0077                         packet = reader.readLine();
0078                     } catch (SocketTimeoutException e) {
0079                         continue;
0080                     }
0081                     if (packet == null) {
0082                         throw new IOException("End of stream");
0083                     }
0084                     if (packet.isEmpty()) {
0085                         continue;
0086                     }
0087                     NetworkPacket np = NetworkPacket.unserialize(packet);
0088                     receivedNetworkPacket(np);
0089                 }
0090             } catch (Exception e) {
0091                 Log.i("LanLink", "Socket closed: " + newSocket.hashCode() + ". Reason: " + e.getMessage());
0092                 try { Thread.sleep(300); } catch (InterruptedException ignored) {} // Wait a bit because we might receive a new socket meanwhile
0093                 boolean thereIsaANewSocket = (newSocket != socket);
0094                 if (!thereIsaANewSocket) {
0095                     Log.i("LanLink", "Socket closed and there's no new socket, disconnecting device");
0096                     getLinkProvider().onConnectionLost(LanLink.this);
0097                 }
0098             }
0099         });
0100 
0101         return oldSocket;
0102     }
0103 
0104     public LanLink(@NonNull Context context, @NonNull DeviceInfo deviceInfo, @NonNull BaseLinkProvider linkProvider, @NonNull SSLSocket socket) throws IOException {
0105         super(context, linkProvider);
0106         this.deviceInfo = deviceInfo;
0107         reset(socket);
0108     }
0109 
0110     @Override
0111     public String getName() {
0112         return "LanLink";
0113     }
0114 
0115     @Override
0116     public DeviceInfo getDeviceInfo() {
0117         return deviceInfo;
0118     }
0119 
0120     @WorkerThread
0121     @Override
0122     public boolean sendPacket(@NonNull NetworkPacket np, @NonNull final Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {
0123         if (socket == null) {
0124             Log.e("KDE/sendPacket", "Not yet connected");
0125             callback.onFailure(new NotYetConnectedException());
0126             return false;
0127         }
0128 
0129         try {
0130 
0131             //Prepare socket for the payload
0132             final ServerSocket server;
0133             if (np.hasPayload()) {
0134                 server = LanLinkProvider.openServerSocketOnFreePort(LanLinkProvider.PAYLOAD_TRANSFER_MIN_PORT);
0135                 JSONObject payloadTransferInfo = new JSONObject();
0136                 payloadTransferInfo.put("port", server.getLocalPort());
0137                 np.setPayloadTransferInfo(payloadTransferInfo);
0138             } else {
0139                 server = null;
0140             }
0141 
0142             //Log.e("LanLink/sendPacket", np.getType());
0143 
0144             //Send body of the network packet
0145             try {
0146                 OutputStream writer = socket.getOutputStream();
0147                 writer.write(np.serialize().getBytes(Charsets.UTF_8));
0148                 writer.flush();
0149             } catch (Exception e) {
0150                 disconnect(); //main socket is broken, disconnect
0151                 throw e;
0152             }
0153 
0154             //Send payload
0155             if (server != null) {
0156                 if (sendPayloadFromSameThread) {
0157                     sendPayload(np, callback, server);
0158                 } else {
0159                     ThreadHelper.execute(() -> {
0160                         try {
0161                             sendPayload(np, callback, server);
0162                         } catch (IOException e) {
0163                             e.printStackTrace();
0164                             Log.e("LanLink/sendPacket", "Async sendPayload failed for packet of type " + np.getType() + ". The Plugin was NOT notified.");
0165                         }
0166                     });
0167                 }
0168             }
0169 
0170             if (!np.isCanceled()) {
0171                 callback.onSuccess();
0172             }
0173             return true;
0174         } catch (Exception e) {
0175             callback.onFailure(e);
0176             return false;
0177         } finally  {
0178             //Make sure we close the payload stream, if any
0179             if (np.hasPayload()) {
0180                 np.getPayload().close();
0181             }
0182         }
0183     }
0184 
0185     private void sendPayload(NetworkPacket np, Device.SendPacketStatusCallback callback, ServerSocket server) throws IOException {
0186         Socket payloadSocket = null;
0187         OutputStream outputStream = null;
0188         InputStream inputStream;
0189         try {
0190             if (!np.isCanceled()) {
0191                 //Wait a maximum of 10 seconds for the other end to establish a connection with our socket, close it afterwards
0192                 server.setSoTimeout(10 * 1000);
0193 
0194                 payloadSocket = server.accept();
0195 
0196                 //Convert to SSL if needed
0197                 payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false);
0198 
0199                 outputStream = payloadSocket.getOutputStream();
0200                 inputStream = np.getPayload().getInputStream();
0201 
0202                 Log.i("KDE/LanLink", "Beginning to send payload for " + np.getType());
0203                 byte[] buffer = new byte[4096];
0204                 int bytesRead;
0205                 long size = np.getPayloadSize();
0206                 long progress = 0;
0207                 long timeSinceLastUpdate = -1;
0208                 while (!np.isCanceled() && (bytesRead = inputStream.read(buffer)) != -1) {
0209                     //Log.e("ok",""+bytesRead);
0210                     progress += bytesRead;
0211                     outputStream.write(buffer, 0, bytesRead);
0212                     if (size > 0) {
0213                         if (timeSinceLastUpdate + 500 < System.currentTimeMillis()) { //Report progress every half a second
0214                             long percent = ((100 * progress) / size);
0215                             callback.onPayloadProgressChanged((int) percent);
0216                             timeSinceLastUpdate = System.currentTimeMillis();
0217                         }
0218                     }
0219                 }
0220                 outputStream.flush();
0221                 Log.i("KDE/LanLink", "Finished sending payload (" + progress + " bytes written)");
0222             }
0223         } catch(SocketTimeoutException e) {
0224             Log.e("LanLink", "Socket for payload in packet " + np.getType() + " timed out. The other end didn't fetch the payload.");
0225         } catch(SSLHandshakeException e) {
0226             // The exception can be due to several causes. "Connection closed by peer" seems to be a common one.
0227             // If we could distinguish different cases we could react differently for some of them, but I haven't found how.
0228             Log.e("sendPacket","Payload SSLSocket failed");
0229             e.printStackTrace();
0230         } finally {
0231             try { server.close(); } catch (Exception ignored) { }
0232             try { IOUtils.close(payloadSocket); } catch (Exception ignored) { }
0233             np.getPayload().close();
0234             try { IOUtils.close(outputStream); } catch (Exception ignored) { }
0235         }
0236     }
0237 
0238     private void receivedNetworkPacket(NetworkPacket np) {
0239 
0240         if (np.hasPayloadTransferInfo()) {
0241             Socket payloadSocket = new Socket();
0242             try {
0243                 int tcpPort = np.getPayloadTransferInfo().getInt("port");
0244                 InetSocketAddress deviceAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
0245                 payloadSocket.connect(new InetSocketAddress(deviceAddress.getAddress(), tcpPort));
0246                 payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, true);
0247                 np.setPayload(new NetworkPacket.Payload(payloadSocket, np.getPayloadSize()));
0248             } catch (Exception e) {
0249                 try { payloadSocket.close(); } catch(Exception ignored) { }
0250                 Log.e("KDE/LanLink", "Exception connecting to payload remote socket", e);
0251             }
0252 
0253         }
0254 
0255         packetReceived(np);
0256     }
0257 
0258 }