diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java index bcef76b3..0876a338 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java @@ -21,7 +21,6 @@ package org.kde.kdeconnect.Backends.LanBackend; import android.content.Context; -import android.content.SharedPreferences; import android.util.Log; import org.json.JSONObject; @@ -45,8 +44,6 @@ import java.net.SocketTimeoutException; import java.nio.channels.NotYetConnectedException; import java.security.PublicKey; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; public class LanLink extends BaseLink { @@ -62,6 +59,10 @@ public class LanLink extends BaseLink { private Socket socket = null; + interface SocketClosedCallback { + void socketClosed(LanLink brokenLink, boolean linkHasAnotherSocket); + }; + @Override public void disconnect() { @@ -81,7 +82,7 @@ public class LanLink extends BaseLink { } //Returns the old socket - public Socket reset(final Socket newSocket, ConnectionStarted connectionSource, final LanLinkProvider linkProvider) throws IOException { + public Socket reset(final Socket newSocket, ConnectionStarted connectionSource, final SocketClosedCallback callback) throws IOException { Socket oldSocket = socket; socket = newSocket; @@ -92,8 +93,8 @@ public class LanLink extends BaseLink { oldSocket.close(); //This should cancel the readThread } - Log.e("LanLink", "Start listening"); - //Start listening + //Log.e("LanLink", "Start listening"); + //Create a thread to take care of incoming data for the new socket new Thread(new Runnable() { @Override public void run() { @@ -107,16 +108,18 @@ public class LanLink extends BaseLink { continue; } if (packet == null) { - throw new IOException("Read null"); + throw new IOException("End of stream"); + } + if (packet.isEmpty()) { + continue; } - if (packet.isEmpty()) continue; NetworkPackage np = NetworkPackage.unserialize(packet); - injectNetworkPackage(np); + receivedNetworkPackage(np); } } catch (Exception e) { - Log.e("LanLink", "Socket closed " + newSocket.hashCode() + " reason: " + e.getMessage()); + Log.i("LanLink", "Socket closed: " + newSocket.hashCode() + ". Reason: " + e.getMessage()); boolean thereIsaANewSocket = (newSocket != socket); - linkProvider.socketClosed(newSocket, thereIsaANewSocket); + callback.socketClosed(LanLink.this, thereIsaANewSocket); } } }).start(); @@ -153,7 +156,7 @@ public class LanLink extends BaseLink { //Prepare socket for the payload final ServerSocket server; if (np.hasPayload()) { - server = openTcpSocketOnFreePort(context, getDeviceId()); + server = LanLinkProvider.openServerSocketOnFreePort(LanLinkProvider.PAYLOAD_TRANSFER_MIN_PORT); JSONObject payloadTransferInfo = new JSONObject(); payloadTransferInfo.put("port", server.getLocalPort()); np.setPayloadTransferInfo(payloadTransferInfo); @@ -182,38 +185,48 @@ public class LanLink extends BaseLink { //Send payload if (server != null) { - OutputStream socket = null; + Socket payloadSocket = null; + OutputStream outputStream = null; + InputStream inputStream = null; 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); - socket = server.accept().getOutputStream(); + + payloadSocket = server.accept(); + + //Convert to SSL if needed + if (socket instanceof SSLSocket) { + payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false); + } + + outputStream = payloadSocket.getOutputStream(); + inputStream = np.getPayload(); Log.i("KDE/LanLink", "Beginning to send payload"); - byte[] buffer = new byte[4096]; int bytesRead; long progress = 0; - InputStream stream = np.getPayload(); - while ((bytesRead = stream.read(buffer)) != -1) { + while ((bytesRead = inputStream.read(buffer)) != -1) { //Log.e("ok",""+bytesRead); progress += bytesRead; - socket.write(buffer, 0, bytesRead); + outputStream.write(buffer, 0, bytesRead); if (np.getPayloadSize() > 0) { callback.sendProgress((int)(progress / np.getPayloadSize())); } } - socket.flush(); - stream.close(); + outputStream.flush(); + outputStream.close(); Log.i("KDE/LanLink", "Finished sending payload ("+progress+" bytes written)"); } catch (Exception e) { + e.printStackTrace(); Log.e("KDE/sendPackage", "Exception: "+e); callback.sendFailure(e); return; } finally { - if (socket != null) { - try { socket.close(); } catch (Exception e) { } - } try { server.close(); } catch (Exception e) { } + try { payloadSocket.close(); } catch (Exception e) { } + try { inputStream.close(); } catch (Exception e) { } + try { outputStream.close(); } catch (Exception e) { } } } @@ -244,7 +257,7 @@ public class LanLink extends BaseLink { sendPackageInternal(np, callback, key); } - public void injectNetworkPackage(NetworkPackage np) { + private void receivedNetworkPackage(NetworkPackage np) { if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_ENCRYPTED)) { try { @@ -257,16 +270,12 @@ public class LanLink extends BaseLink { if (np.hasPayloadTransferInfo()) { - Socket payloadSocket = null; + Socket payloadSocket = new Socket(); try { // Use ssl if existing link is on ssl if (socket instanceof SSLSocket) { - SSLContext sslContext = SslHelper.getSslContext(context, getDeviceId(), true); - payloadSocket = sslContext.getSocketFactory().createSocket(); - } else { - payloadSocket = new Socket(); + payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, true); } - int tcpPort = np.getPayloadTransferInfo().getInt("port"); InetSocketAddress address = (InetSocketAddress) socket.getRemoteSocketAddress(); payloadSocket.connect(new InetSocketAddress(address.getAddress(), tcpPort)); @@ -282,49 +291,6 @@ public class LanLink extends BaseLink { packageReceived(np); } - ServerSocket openTcpSocketOnFreePort(Context context, String deviceId) throws IOException { - if (socket instanceof SSLSocket) { - return openSecureServerSocket(context, deviceId); - } else { - return openUnsecureSocketOnFreePort(1739); - } - } - - - static ServerSocket openUnsecureSocketOnFreePort(int minPort) throws IOException { - int tcpPort = minPort; - while(tcpPort < 1764) { - try { - ServerSocket candidateServer = new ServerSocket(); - candidateServer.bind(new InetSocketAddress(tcpPort)); - Log.i("KDE/LanLink", "Using port "+tcpPort); - return candidateServer; - } catch(IOException e) { - tcpPort++; - } - } - Log.e("KDE/LanLink", "No more ports available"); - throw new IOException("No more ports available"); - } - - static ServerSocket openSecureServerSocket(Context context, String deviceId) throws IOException{ - SSLContext tlsContext = SslHelper.getSslContext(context, deviceId, true); - SSLServerSocketFactory sslServerSocketFactory = tlsContext.getServerSocketFactory(); - int tcpPort = 1739; - while(tcpPort < 1764) { - try { - ServerSocket candidateServer = sslServerSocketFactory.createServerSocket(); - candidateServer.bind(new InetSocketAddress(tcpPort)); - Log.i("KDE/LanLink", "Using port "+tcpPort); - return candidateServer; - } catch(IOException e) { - tcpPort++; - } - } - Log.e("KDE/LanLink", "No more ports available"); - throw new IOException("No more ports available"); - } - @Override public boolean linkShouldBeKeptAlive() { diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java index 1e606a10..e092cf07 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java @@ -43,6 +43,7 @@ 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; @@ -56,22 +57,20 @@ import javax.net.SocketFactory; import javax.net.ssl.HandshakeCompletedEvent; import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -public class LanLinkProvider extends BaseLinkProvider { +public class LanLinkProvider extends BaseLinkProvider implements LanLink.SocketClosedCallback { public static final int MIN_VERSION_WITH_SSL_SUPPORT = 6; public static final int MIN_VERSION_WITH_NEW_PORT_SUPPORT = 7; - public static final String KEY_CUSTOM_DEVLIST_PREFERENCE = "device_list_preference"; - - private final static int oldPort = 1714; - private final static int port = 1716; + final static int MIN_PORT_LEGACY = 1714; + final static int MIN_PORT = 1716; + 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 final HashMap nioLinks = new HashMap<>(); //Links by socket private ServerSocket tcpServer; private DatagramSocket udpServer; @@ -82,26 +81,12 @@ public class LanLinkProvider extends BaseLinkProvider { // To prevent infinte loop between Android < IceCream because both device can only broadcast identity package but cannot connect via TCP private ArrayList reverseConnectionBlackList = new ArrayList<>(); - public void socketClosed(Socket socket, boolean linkHasAnotherSocket) { - final LanLink brokenLink = nioLinks.remove(socket); - if (brokenLink != null) { - - if (!linkHasAnotherSocket) { - String deviceId = brokenLink.getDeviceId(); - visibleComputers.remove(deviceId); - new Thread(new Runnable() { - @Override - public void run() { - //Wait a bit before emitting connectionLost, in case the same device re-appears - try { - Thread.sleep(200); - } catch (InterruptedException e) { - } - connectionLost(brokenLink); - - } - }).start(); - } + @Override // SocketClosedCallback + public void socketClosed(final LanLink brokenLink, boolean linkHasAnotherSocket) { + if (!linkHasAnotherSocket) { + String deviceId = brokenLink.getDeviceId(); + visibleComputers.remove(deviceId); + connectionLost(brokenLink); } } @@ -113,26 +98,22 @@ public class LanLinkProvider extends BaseLinkProvider { BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String message = reader.readLine(); networkPackage = NetworkPackage.unserialize(message); - //Log.i("TcpListener","Received TCP package: "+networkPackage.serialize()); + //Log.e("TcpListener","Received TCP package: "+networkPackage.serialize()); } catch (Exception e) { e.printStackTrace(); return; } - if (networkPackage.getType().equals(NetworkPackage.PACKAGE_TYPE_IDENTITY)) { - Log.i("KDE/LanLinkProvider", "Identity package received from a TCP connection from " + networkPackage.getString("deviceName")); - identityPackageReceived(networkPackage, socket, LanLink.ConnectionStarted.Locally); - } else { - LanLink link = nioLinks.get(socket); - if (link== null) { - Log.e("KDE/LanLinkProvider","Expecting an identity package instead of " + networkPackage.getType()); - } else { - link.injectNetworkPackage(networkPackage); - } + if (!networkPackage.getType().equals(NetworkPackage.PACKAGE_TYPE_IDENTITY)) { + Log.e("KDE/LanLinkProvider", "Expecting an identity package instead of " + networkPackage.getType()); + return; } + Log.i("KDE/LanLinkProvider", "Identity package received from a TCP connection from " + networkPackage.getString("deviceName")); + identityPackageReceived(networkPackage, socket, LanLink.ConnectionStarted.Locally); } + //I've received their broadcast and should connect to their TCP socket and send my identity. protected void udpPacketReceived(DatagramPacket packet) throws Exception { final InetAddress address = packet.getAddress(); @@ -153,21 +134,22 @@ public class LanLinkProvider extends BaseLinkProvider { } } - if (identityPackage.getInt("protocolVersion") >= MIN_VERSION_WITH_NEW_PORT_SUPPORT && identityPackage.getInt("tcpPort") < port) { - Log.w("KDE/LanLinkProvider", "Ignoring a udp broadcast from an old port because it comes from a device which knows about the new port."); + if (identityPackage.getInt("protocolVersion") >= MIN_VERSION_WITH_NEW_PORT_SUPPORT && identityPackage.getInt("tcpPort") < MIN_PORT) { + Log.w("KDE/LanLinkProvider", "Ignoring a udp broadcast from legacy port because it comes from a device which knows about the new port."); return; } Log.i("KDE/LanLinkProvider", "Broadcast identity package received from " + identityPackage.getString("deviceName")); - int tcpPort = identityPackage.getInt("tcpPort", port); + int tcpPort = identityPackage.getInt("tcpPort", MIN_PORT); SocketFactory socketFactory = SocketFactory.getDefault(); Socket socket = socketFactory.createSocket(address, tcpPort); configureSocket(socket); OutputStream out = socket.getOutputStream(); - out.write(NetworkPackage.createIdentityPackage(context).serialize().getBytes()); + NetworkPackage myIdentity = NetworkPackage.createIdentityPackage(context); + out.write(myIdentity.serialize().getBytes()); out.flush(); identityPackageReceived(identityPackage, socket, LanLink.ConnectionStarted.Remotely); @@ -188,7 +170,6 @@ public class LanLinkProvider extends BaseLinkProvider { // Try to cause a reverse connection onNetworkChange(); } - } } @@ -217,14 +198,11 @@ public class LanLinkProvider extends BaseLinkProvider { if (identityPackage.getInt("protocolVersion") >= MIN_VERSION_WITH_SSL_SUPPORT) { SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); - final boolean isDeviceTrusted = preferences.getBoolean(deviceId, false); + boolean isDeviceTrusted = preferences.getBoolean(deviceId, false); Log.i("KDE/LanLinkProvider","Starting SSL handshake with " + identityPackage.getString("deviceName") + " trusted:"+isDeviceTrusted); - SSLSocketFactory sslsocketFactory = SslHelper.getSslContext(context, deviceId, isDeviceTrusted).getSocketFactory(); - final SSLSocket sslsocket = (SSLSocket)sslsocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); - SslHelper.configureSslSocket(sslsocket, isDeviceTrusted, clientMode); - configureSocket(sslsocket); + final SSLSocket sslsocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode); sslsocket.addHandshakeCompletedListener(new HandshakeCompletedListener() { @Override public void handshakeCompleted(HandshakeCompletedEvent event) { @@ -276,19 +254,10 @@ public class LanLinkProvider extends BaseLinkProvider { //Update old link Log.i("KDE/LanLinkProvider", "Reusing same link for device " + deviceId); final Socket oldSocket = currentLink.reset(socket, connectionOrigin, this); - new Timer().schedule(new TimerTask() { - @Override - public void run() { - nioLinks.remove(oldSocket); - //Log.e("KDE/LanLinkProvider", "Forgetting about socket " + socket.hashCode()); - } - }, 500); //Stop accepting messages from the old socket after 500ms - nioLinks.put(socket, currentLink); //Log.e("KDE/LanLinkProvider", "Replacing socket. old: "+ oldSocket.hashCode() + " - new: "+ socket.hashCode()); } else { //Let's create the link LanLink link = new LanLink(context, deviceId, this, socket, connectionOrigin); - nioLinks.put(socket, link); visibleComputers.put(deviceId, link); connectionAccepted(identityPackage, link); } @@ -333,7 +302,7 @@ public class LanLinkProvider extends BaseLinkProvider { private void setupTcpListener() { try { - tcpServer = LanLink.openUnsecureSocketOnFreePort(port); + tcpServer = openServerSocketOnFreePort(MIN_PORT); new Thread(new Runnable() { @Override public void run() { @@ -356,13 +325,29 @@ public class LanLinkProvider extends BaseLinkProvider { } } + 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("KDE/LanLink", "Using port "+tcpPort); + return candidateServer; + } catch(IOException e) { + tcpPort++; + } + } + Log.e("KDE/LanLink", "No ports available"); + throw new IOException("No ports available"); + } + void broadcastUdpPackage() { new Thread(new Runnable() { @Override public void run() { - String deviceListPrefs = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_CUSTOM_DEVLIST_PREFERENCE, ""); + String deviceListPrefs = PreferenceManager.getDefaultSharedPreferences(context).getString(CustomDevicesActivity.KEY_CUSTOM_DEVLIST_PREFERENCE, ""); ArrayList iplist = new ArrayList<>(); if (!deviceListPrefs.isEmpty()) { iplist = CustomDevicesActivity.deserializeIpList(deviceListPrefs); @@ -370,7 +355,7 @@ public class LanLinkProvider extends BaseLinkProvider { iplist.add("255.255.255.255"); //Default: broadcast. NetworkPackage identity = NetworkPackage.createIdentityPackage(context); - identity.set("tcpPort", port); + identity.set("tcpPort", MIN_PORT); DatagramSocket socket = null; byte[] bytes = null; try { @@ -388,8 +373,8 @@ public class LanLinkProvider extends BaseLinkProvider { for (String ipstr : iplist) { try { InetAddress client = InetAddress.getByName(ipstr); - socket.send(new DatagramPacket(bytes, bytes.length, client, port)); - socket.send(new DatagramPacket(bytes, bytes.length, client, oldPort)); + socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT)); + socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT_LEGACY)); //Log.i("KDE/LanLinkProvider","Udp identity package sent to address "+client); } catch (Exception e) { e.printStackTrace(); @@ -412,8 +397,8 @@ public class LanLinkProvider extends BaseLinkProvider { listening = true; - udpServer = setupUdpListener(port); - udpServerOldPort = setupUdpListener(oldPort); + udpServer = setupUdpListener(MIN_PORT); + udpServerOldPort = setupUdpListener(MIN_PORT_LEGACY); // Due to certificate request from SSL server to client, the certificate request message from device with latest android version to device with // old android version causes a FATAL ALERT message stating that incorrect certificate request diff --git a/src/org/kde/kdeconnect/Helpers/SecurityHelpers/SslHelper.java b/src/org/kde/kdeconnect/Helpers/SecurityHelpers/SslHelper.java index 096c9b7b..36c036d8 100644 --- a/src/org/kde/kdeconnect/Helpers/SecurityHelpers/SslHelper.java +++ b/src/org/kde/kdeconnect/Helpers/SecurityHelpers/SslHelper.java @@ -41,6 +41,7 @@ import org.spongycastle.operator.jcajce.JcaContentSignerBuilder; import java.io.IOException; import java.math.BigInteger; +import java.net.Socket; import java.nio.channels.ServerSocketChannel; import java.security.KeyStore; import java.security.MessageDigest; @@ -58,6 +59,7 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; @@ -132,6 +134,7 @@ public class SslHelper { } public static SSLContext getSslContext(Context context, String deviceId, boolean isDeviceTrusted) { + //TODO: Cache try { // Get device private key PrivateKey privateKey = RsaHelper.getPrivateKey(context); @@ -225,6 +228,13 @@ public class SslHelper { } + public static SSLSocket convertToSslSocket(Context context, Socket socket, String deviceId, boolean isDeviceTrusted, boolean clientMode) throws IOException { + SSLSocketFactory sslsocketFactory = SslHelper.getSslContext(context, deviceId, isDeviceTrusted).getSocketFactory(); + SSLSocket sslsocket = (SSLSocket)sslsocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); + SslHelper.configureSslSocket(sslsocket, isDeviceTrusted, clientMode); + return sslsocket; + } + public static String getCertificateHash(Certificate certificate) { try { byte[] hash = MessageDigest.getInstance("SHA-1").digest(certificate.getEncoded()); @@ -238,7 +248,6 @@ public class SslHelper { } catch (Exception e) { return null; } - } public static Certificate parseCertificate(byte[] certificateBytes) throws IOException, CertificateException {