mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-31 22:25:08 +00:00
Compare commits
12 Commits
v1.33.4
...
sredman/kd
Author | SHA1 | Date | |
---|---|---|---|
|
ad49e500fa | ||
|
826c0a854e | ||
|
cf32416243 | ||
|
d8749d8f85 | ||
|
75e08345f7 | ||
|
8168ff53e6 | ||
|
d939c18dd5 | ||
|
ec638fdbec | ||
|
e77978d1d5 | ||
|
a63100e5d4 | ||
|
c817fe1012 | ||
|
58de1aa868 |
@@ -12,7 +12,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ buildscript {
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
|
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
|
||||
* Copyright 2019 Simon Redman <simon@ergotech.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,495 @@
|
||||
/*
|
||||
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
|
||||
* Copyright 2018 Teemu Rytilahti
|
||||
* Copyright 2019 Simon Redman <simon@ergotech.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Backends.MulticastBackend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Network;
|
||||
import android.net.nsd.NsdManager;
|
||||
import android.net.nsd.NsdManager.RegistrationListener;
|
||||
import android.net.nsd.NsdManager.ResolveListener;
|
||||
import android.net.nsd.NsdServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.json.JSONException;
|
||||
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.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
/**
|
||||
* This MulticastLinkProvider 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";
|
||||
|
||||
static final String SERVICE_TYPE = "_kdeconnect._tcp";
|
||||
|
||||
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 NsdManager mNsdManager;
|
||||
private RegistrationListener mRegistrationListener;
|
||||
private NsdManager.DiscoveryListener discoveryListener;
|
||||
private ServerSocket tcpServer;
|
||||
private boolean listening = false;
|
||||
|
||||
private final HashMap<String, MulticastLink> visibleComputers = new HashMap<>(); //Links by device id
|
||||
|
||||
@Override // SocketClosedCallback
|
||||
public void linkDisconnected(MulticastLink brokenLink) {
|
||||
String deviceId = brokenLink.getDeviceId();
|
||||
visibleComputers.remove(deviceId);
|
||||
connectionLost(brokenLink);
|
||||
}
|
||||
|
||||
// They received my mDNS broadcast and are connecting to me. The first thing they send should be their identity.
|
||||
private void tcpPacketReceived(Socket socket) {
|
||||
|
||||
writeIdentity(socket);
|
||||
NetworkPacket otherIdentity = readIdentity(socket);
|
||||
|
||||
if (!otherIdentity.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
|
||||
Log.e(LOG_TAG, "Expecting an identity package instead of " + otherIdentity.getType());
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(LOG_TAG, "Identity package received from a TCP connection from " + otherIdentity.getString("deviceName"));
|
||||
identityPacketReceived(otherIdentity, socket, MulticastLink.ConnectionStarted.Locally);
|
||||
}
|
||||
|
||||
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)}
|
||||
* <p>
|
||||
* If the remote device should be connected, this calls {@link #addLink}.
|
||||
* Otherwise, if there was an Exception, we unpair from that device.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
* <p>
|
||||
* 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).
|
||||
* </p>
|
||||
*
|
||||
* @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 writeIdentity(Socket socket) {
|
||||
try {
|
||||
OutputStream out = socket.getOutputStream();
|
||||
NetworkPacket myIdentity = NetworkPacket.createIdentityPacket(context);
|
||||
out.write(myIdentity.serialize().getBytes());
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Unable to get stream from socket", e);
|
||||
return;
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Unable to deserialize JSON", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private NetworkPacket readIdentity(Socket socket) {
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String message = reader.readLine();
|
||||
Log.e("TcpListener","Received TCP package: "+message);
|
||||
return NetworkPacket.unserialize(message);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Exception while receiving TCP packet", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public void initializeRegistrationListener() {
|
||||
mRegistrationListener = new NsdManager.RegistrationListener() {
|
||||
|
||||
@Override
|
||||
public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
|
||||
// Save the service name. Android may have changed it in order to
|
||||
// resolve a conflict, so update the name you initially requested
|
||||
// with the name Android actually used.
|
||||
Log.e(LOG_TAG, "Registered " + NsdServiceInfo.getServiceName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
||||
// Registration failed! Put debugging code here to determine why.
|
||||
Log.e(LOG_TAG, "Registration failed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceUnregistered(NsdServiceInfo arg0) {
|
||||
// Service has been unregistered. This only happens when you call
|
||||
// NsdManager.unregisterService() and pass in this listener.
|
||||
Log.e(LOG_TAG, "Service unregistered: " + arg0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
||||
// Unregistration failed. Put debugging code here to determine why.
|
||||
Log.e(LOG_TAG, "Unregister of " + serviceInfo + " failed with: " + errorCode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public void initializeNsdManager() {
|
||||
|
||||
mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
|
||||
|
||||
NsdServiceInfo serviceInfo = new NsdServiceInfo();
|
||||
|
||||
// It wasn't possible for the device which receives the incoming connection
|
||||
// to use these attributes (there's no way to correlate the incoming connection
|
||||
// with a discovered service), so we have to rely on sending this stuff in
|
||||
// an identity packet like in the LanLink. That's the reason this is commented out.
|
||||
// These cause the requirement for api level 21.
|
||||
serviceInfo.setAttribute("id", DeviceHelper.getDeviceId(context));
|
||||
// The name is subject to change based on conflicts
|
||||
// with other services advertised on the same network.
|
||||
//serviceInfo.setAttribute("name", DeviceHelper.getDeviceName(context));
|
||||
//serviceInfo.setAttribute("type", DeviceHelper.getDeviceType(context).toString());
|
||||
//serviceInfo.setAttribute("version", Integer.toString(DeviceHelper.ProtocolVersion));
|
||||
// It might be nice to add the capabilities in the mDNS advertisement too, but without
|
||||
// some kind of encoding that is too large for the TXT record
|
||||
|
||||
serviceInfo.setServiceName("KDE Connect on " + DeviceHelper.getDeviceName(context));
|
||||
serviceInfo.setServiceType(SERVICE_TYPE);
|
||||
// This gets autodetected if not set
|
||||
//serviceInfo.setHost(this.tcpServer.getInetAddress());
|
||||
serviceInfo.setPort(this.tcpServer.getLocalPort());
|
||||
|
||||
//Log.d("KDE/Lan", "service: " + serviceInfo.toString());
|
||||
|
||||
mNsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
public ResolveListener createResolveListener() {
|
||||
return new ResolveListener() {
|
||||
|
||||
@Override
|
||||
public void onResolveFailed(NsdServiceInfo serviceInfo, int i) {
|
||||
Log.e(LOG_TAG, "Could not resolve service: " + serviceInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceResolved(NsdServiceInfo serviceInfo) {
|
||||
Log.e(LOG_TAG, "Successfully resolved " + serviceInfo);
|
||||
|
||||
byte[] id = serviceInfo.getAttributes().get("id");
|
||||
if (id != null && DeviceHelper.getDeviceId(context).equals(new String(id, StandardCharsets.UTF_8))) {
|
||||
Log.e(LOG_TAG, "Ignoring connection to myself");
|
||||
return;
|
||||
}
|
||||
|
||||
InetAddress hostname = serviceInfo.getHost();
|
||||
int remotePort = serviceInfo.getPort();
|
||||
|
||||
SocketFactory socketFactory = SocketFactory.getDefault();
|
||||
Socket socket;
|
||||
try {
|
||||
socket = socketFactory.createSocket(hostname, remotePort);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Unable to open socket to mDNS remote: " + serviceInfo, e);
|
||||
return;
|
||||
}
|
||||
|
||||
writeIdentity(socket);
|
||||
NetworkPacket otherIdentity = readIdentity(socket);
|
||||
|
||||
try {
|
||||
Log.i(LOG_TAG, "Got identity: " + otherIdentity.serialize());
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Got identity, but unable to serialize from " + serviceInfo, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
public void initializeDiscoveryListener() {
|
||||
// Instantiate a new DiscoveryListener
|
||||
discoveryListener = new NsdManager.DiscoveryListener() {
|
||||
|
||||
// Called as soon as service discovery begins.
|
||||
@Override
|
||||
public void onDiscoveryStarted(String regType) {
|
||||
Log.d(LOG_TAG, "Service discovery started");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceFound(NsdServiceInfo service) {
|
||||
// A service was found! Do something with it.
|
||||
Log.e(LOG_TAG, "Service discovery success" + service);
|
||||
mNsdManager.resolveService(service, createResolveListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceLost(NsdServiceInfo service) {
|
||||
// When the network service is no longer available.
|
||||
// Internal bookkeeping code goes here.
|
||||
Log.e(LOG_TAG, "service lost: " + service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDiscoveryStopped(String serviceType) {
|
||||
Log.e(LOG_TAG, "Discovery stopped: " + serviceType);
|
||||
listening = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
|
||||
Log.e(LOG_TAG, "Discovery failed: Error code:" + errorCode);
|
||||
mNsdManager.stopServiceDiscovery(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
|
||||
Log.e(LOG_TAG, "Discovery failed: Error code:" + errorCode);
|
||||
mNsdManager.stopServiceDiscovery(this);
|
||||
}
|
||||
};
|
||||
|
||||
mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkChange() {
|
||||
onStop();
|
||||
onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStart() {
|
||||
if (!listening) {
|
||||
listening = true;
|
||||
|
||||
// Need to set up TCP before setting up mDNS because we need to know the TCP listening
|
||||
// address and port
|
||||
setupTcpListener();
|
||||
initializeRegistrationListener();
|
||||
initializeNsdManager();
|
||||
initializeDiscoveryListener();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStop() {
|
||||
//if (listening) {
|
||||
// mNsdManager.stopServiceDiscovery(discoveryListener);
|
||||
// mNsdManager.unregisterService(mRegistrationListener);
|
||||
// try { tcpServer.close(); } catch (IOException ignored) { }
|
||||
// listening = false;
|
||||
//}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MulticastLinkProvider";
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2015 Vineet Garg <grg.vineet@gmail.com>
|
||||
* Copyright 2019 Simon Redman <simon@ergotech.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -42,6 +42,7 @@ import androidx.core.content.ContextCompat;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
|
||||
import org.kde.kdeconnect.Backends.MulticastBackend.MulticastLinkProvider;
|
||||
import org.kde.kdeconnect.Helpers.NotificationHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
@@ -162,7 +163,8 @@ public class BackgroundService extends Service {
|
||||
}
|
||||
|
||||
private void registerLinkProviders() {
|
||||
linkProviders.add(new LanLinkProvider(this));
|
||||
//linkProviders.add(new LanLinkProvider(this));
|
||||
linkProviders.add(new MulticastLinkProvider(this));
|
||||
// linkProviders.add(new LoopbackLinkProvider(this));
|
||||
// linkProviders.add(new BluetoothLinkProvider(this));
|
||||
}
|
||||
|
Reference in New Issue
Block a user