diff --git a/app/src/main/java/org/briarproject/publicmesh/util/IoUtils.java b/app/src/main/java/org/briarproject/publicmesh/util/IoUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..37c0619b5f8dbe821ddc22ada28a16e0362aedf5 --- /dev/null +++ b/app/src/main/java/org/briarproject/publicmesh/util/IoUtils.java @@ -0,0 +1,66 @@ +package org.briarproject.publicmesh.util; + +import org.briarproject.publicmesh.nullsafety.NotNullByDefault; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import static org.briarproject.publicmesh.util.LogUtils.logException; + +@NotNullByDefault +public class IoUtils { + + public static void tryToClose(@Nullable Socket s, Logger logger, + Level level) { + try { + if (s != null) s.close(); + } catch (IOException e) { + logException(logger, level, e); + } + } + + public static void tryToClose(@Nullable ServerSocket ss, Logger logger, + Level level) { + try { + if (ss != null) ss.close(); + } catch (IOException e) { + logException(logger, level, e); + } + } + + // Workaround for a bug in Android 7, see + // https://android-review.googlesource.com/#/c/271775/ + public static InputStream getInputStream(Socket s) throws IOException { + try { + return s.getInputStream(); + } catch (NullPointerException e) { + throw new IOException(e); + } + } + + // Workaround for a bug in Android 7, see + // https://android-review.googlesource.com/#/c/271775/ + public static OutputStream getOutputStream(Socket s) throws IOException { + try { + return s.getOutputStream(); + } catch (NullPointerException e) { + throw new IOException(e); + } + } + + public static byte[] ipAddressIntToBytes(int ip) { + byte[] ipBytes = new byte[4]; + ipBytes[0] = (byte) (ip & 0xFF); + ipBytes[1] = (byte) ((ip >> 8) & 0xFF); + ipBytes[2] = (byte) ((ip >> 16) & 0xFF); + ipBytes[3] = (byte) ((ip >> 24) & 0xFF); + return ipBytes; + } +} diff --git a/app/src/main/java/org/briarproject/publicmesh/wifidirect/WifiDirectServiceImpl.java b/app/src/main/java/org/briarproject/publicmesh/wifidirect/WifiDirectServiceImpl.java index 59b510128d139b019b11a811395850dd5bdb18bc..bd3ea5b1bee35f172f9e01eba32e04a8d7a23248 100644 --- a/app/src/main/java/org/briarproject/publicmesh/wifidirect/WifiDirectServiceImpl.java +++ b/app/src/main/java/org/briarproject/publicmesh/wifidirect/WifiDirectServiceImpl.java @@ -6,7 +6,12 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.DhcpInfo; import android.net.MacAddress; +import android.net.Network; +import android.net.NetworkRequest; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; @@ -16,6 +21,7 @@ import android.net.wifi.p2p.WifiP2pDevice; import android.net.wifi.p2p.WifiP2pManager; import android.net.wifi.p2p.WifiP2pManager.ActionListener; import android.net.wifi.p2p.WifiP2pManager.Channel; +import android.net.wifi.p2p.WifiP2pManager.ConnectionInfoListener; import android.net.wifi.p2p.WifiP2pManager.DnsSdServiceResponseListener; import android.net.wifi.p2p.WifiP2pManager.DnsSdTxtRecordListener; import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener; @@ -27,18 +33,30 @@ import org.briarproject.publicmesh.R; import org.briarproject.publicmesh.lifecycle.Service; import org.briarproject.publicmesh.nullsafety.NotNullByDefault; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; import java.util.Map; +import java.util.Random; import java.util.TreeMap; import java.util.logging.Logger; import javax.inject.Inject; +import javax.net.SocketFactory; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import static android.content.Context.CONNECTIVITY_SERVICE; import static android.content.Context.WIFI_P2P_SERVICE; import static android.content.Context.WIFI_SERVICE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.wifi.WifiManager.WIFI_MODE_FULL; import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF; import static android.net.wifi.p2p.WifiP2pManager.BUSY; @@ -48,7 +66,12 @@ import static android.net.wifi.p2p.WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION; import static android.net.wifi.p2p.WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION; import static android.os.Build.VERSION.SDK_INT; import static java.util.Objects.requireNonNull; +import static java.util.logging.Level.INFO; import static java.util.logging.Logger.getLogger; +import static org.briarproject.publicmesh.util.IoUtils.getInputStream; +import static org.briarproject.publicmesh.util.IoUtils.getOutputStream; +import static org.briarproject.publicmesh.util.IoUtils.ipAddressIntToBytes; +import static org.briarproject.publicmesh.util.IoUtils.tryToClose; import static org.briarproject.publicmesh.util.StringUtils.getRandomString; import static org.briarproject.publicmesh.wifidirect.WifiDirectService.Status.STARTED; import static org.briarproject.publicmesh.wifidirect.WifiDirectService.Status.STARTING; @@ -61,9 +84,12 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { private static final String SERVICE_TYPE = "_mesh._tcp"; private static final String KEY_NETWORK_NAME = "ssid"; private static final String KEY_PASSPHRASE = "pass"; + private static final int PORT = 55555; private static final int MAX_GROUP_INFO_ATTEMPTS = 5; + private static final int MAX_CONNECTION_INFO_ATTEMPTS = 5; private static final int MAX_WIFI_CONNECTION_ATTEMPTS = 5; + private static final int MAX_SOCKET_CONNECTION_ATTEMPTS = 5; private static final Logger LOG = getLogger(WifiDirectServiceImpl.class.getName()); @@ -74,6 +100,7 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { private final WifiManager wifiManager; @Nullable private final WifiP2pManager wifiP2pManager; + private final ConnectivityManager connectivityManager; private final MutableLiveData<Status> hotspotStatus = new MutableLiveData<>(STOPPED); private final MutableLiveData<Status> discoveryStatus = new MutableLiveData<>(STOPPED); private final WifiDirectBroadcastReceiver receiver = new WifiDirectBroadcastReceiver(); @@ -83,12 +110,19 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { private WifiLock wifiLock; @Nullable private Channel channel; - private boolean connectionInProgress = false; + private boolean connectionInProgress = false, connectedToPeer = false; @Nullable private WifiP2pDnsSdServiceInfo serviceInfo; @Nullable private WifiP2pDnsSdServiceRequest serviceRequest; private int networkId = -1; + @Nullable + private ServerSocket serverSocket; + @Nullable + private NetworkCallback networkCallback; + private SocketFactory socketFactory = SocketFactory.getDefault(); + @Nullable + private SenderThread senderThread; @Inject WifiDirectServiceImpl(Application app) { @@ -97,6 +131,8 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { handler = new Handler(app.getMainLooper()); wifiManager = (WifiManager) app.getSystemService(WIFI_SERVICE); wifiP2pManager = (WifiP2pManager) app.getSystemService(WIFI_P2P_SERVICE); + connectivityManager = (ConnectivityManager) + requireNonNull(app.getSystemService(CONNECTIVITY_SERVICE)); } @Override @@ -216,6 +252,15 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { private void addService(String networkName, String passphrase) { Channel channel = this.channel; if (channel == null) return; + try { + serverSocket = new ServerSocket(PORT); + LOG.info("Opened server socket " + serverSocket.getLocalSocketAddress()); + new ServerThread(serverSocket).start(); + } catch (IOException e) { + LOG.info("Failed to open server socket: " + e); + releaseHotspot(); + return; + } Map<String, String> record = new TreeMap<>(); record.put(KEY_NETWORK_NAME, networkName); record.put(KEY_PASSPHRASE, passphrase); @@ -242,6 +287,7 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { } private void releaseHotspot() { + closeServerSocket(); removeService(); if (SDK_INT >= 27) requireNonNull(channel).close(); channel = null; @@ -250,6 +296,12 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { LOG.info("Released hotspot"); } + private void closeServerSocket() { + if (serverSocket == null) return; + tryToClose(serverSocket, LOG, INFO); + serverSocket = null; + } + private void removeService() { if (serviceInfo == null) return; ActionListener listener = new ActionListener() { @@ -422,7 +474,23 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { } private void connectToPeer(WifiP2pDevice device, String networkName, String passphrase) { - if (channel == null || connectionInProgress) return; + Channel channel = this.channel; + if (channel == null || connectionInProgress || connectedToPeer) return; + // If we're using wifi (not WFD) on API 21+ we need the network's socket factory + if (SDK_INT >= 21 && SDK_INT < 29) { + NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .removeCapability(NET_CAPABILITY_INTERNET) + .build(); + networkCallback = new NetworkCallback() { + @Override + public void onAvailable(Network network) { + LOG.info("Network is available, setting socket factory"); + socketFactory = network.getSocketFactory(); + } + }; + connectivityManager.registerNetworkCallback(networkRequest, networkCallback); + } if (SDK_INT >= 29) { LOG.info("Connecting to peer " + device.deviceName); connectionInProgress = true; @@ -435,11 +503,15 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { @Override public void onSuccess() { LOG.info("Connected to peer " + device.deviceName); + connectedToPeer = true; + connectionInProgress = false; + requestConnectionInfo(1); } @Override public void onFailure(int reason) { LOG.info("Failed to connect to peer " + device.deviceName + ": " + reason); + connectionInProgress = false; } }; try { @@ -465,6 +537,33 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { } } + private void requestConnectionInfo(int attempt) { + Channel channel = this.channel; + if (channel == null) return; + ConnectionInfoListener listener = info -> { + if (info.groupOwnerAddress == null) { + // On some devices we need to wait for the connection info to become available + if (attempt < MAX_CONNECTION_INFO_ATTEMPTS) { + handler.postDelayed(() -> requestConnectionInfo(attempt + 1), 1000); + } else { + LOG.info("Failed to get group owner's address"); + releaseDiscovery(); + } + } else { + LOG.info("Server address " + info.groupOwnerAddress); + senderThread = new SenderThread(info.groupOwnerAddress); + senderThread.start(); + } + }; + try { + requireNonNull(wifiP2pManager).requestConnectionInfo(channel, listener); + LOG.info("Requested connection info"); + } catch (SecurityException e) { + LOG.info("Failed to request connection info: " + e); + releaseDiscovery(); + } + } + private void connectToNetwork(String networkName, int networkId, int attempt) { String ssid = "\"" + networkName + "\""; try { @@ -476,8 +575,23 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { // If we want to allow the device to remain connected to another network, // we should return here and there's no need to call disconnect() below } else { - LOG.info("Successfully connected to wifi network " + networkName); - return; + DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); + if (dhcpInfo == null) { + LOG.info("No DHCP info"); + // Fall through + } else { + LOG.info("Successfully connected to wifi network " + networkName); + byte[] ipBytes = ipAddressIntToBytes(dhcpInfo.gateway); + try { + InetAddress serverAddress = InetAddress.getByAddress(ipBytes); + LOG.info("Server address " + serverAddress); + senderThread = new SenderThread(serverAddress); + senderThread.start(); + return; + } catch (UnknownHostException e) { + throw new AssertionError(e); + } + } } if (attempt > MAX_WIFI_CONNECTION_ATTEMPTS) return; LOG.info("Connecting to " + networkName + ", attempt " + attempt); @@ -496,21 +610,21 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { } private void releaseDiscovery() { - if (connectionInProgress) { - cancelConnection(); - removeGroup(); - connectionInProgress = false; - } + cancelConnection(); + removeGroup(); removeServiceRequest(); if (SDK_INT >= 27) requireNonNull(channel).close(); channel = null; removeNetwork(); + unregisterNetworkCallback(); + interruptSenderThread(); releaseLock(); discoveryStatus.postValue(STOPPED); LOG.info("Released service discovery"); } private void cancelConnection() { + if (!connectionInProgress) return; ActionListener listener = new ActionListener() { @Override public void onSuccess() { @@ -527,9 +641,11 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { } catch (SecurityException e) { LOG.info("Failed to cancel connection: " + e); } + connectionInProgress = false; } private void removeGroup() { + if (!connectedToPeer) return; ActionListener listener = new ActionListener() { @Override public void onSuccess() { @@ -546,6 +662,7 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { } catch (SecurityException e) { LOG.info("Failed to remove group: " + e); } + connectedToPeer = false; } private void removeServiceRequest() { @@ -581,6 +698,20 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { networkId = -1; } + private void unregisterNetworkCallback() { + if (SDK_INT < 21 || networkCallback == null) return; + requireNonNull(connectivityManager).unregisterNetworkCallback(networkCallback); + LOG.info("Unregistered network callback"); + networkCallback = null; + } + + private void interruptSenderThread() { + if (senderThread == null) return; + senderThread.interrupt(); + LOG.info("Interrupted sender thread"); + senderThread = null; + } + @Override public void start() { IntentFilter filter = new IntentFilter(); @@ -607,4 +738,114 @@ class WifiDirectServiceImpl implements WifiDirectService, Service { if (WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) requestPeers(); } } + + private static class ServerThread extends Thread { + + private final ServerSocket serverSocket; + + private ServerThread(ServerSocket serverSocket) { + this.serverSocket = serverSocket; + } + + @Override + public void run() { + try { + // We'll break out of the loop when the server socket is closed + //noinspection InfiniteLoopStatement + while (true) { + Socket s = serverSocket.accept(); + LOG.info("Incoming connection from " + s.getRemoteSocketAddress()); + new ReceiverThread(s).start(); + } + } catch (IOException e) { + LOG.info("Server thread exiting: " + e); + } + } + } + + private static class ReceiverThread extends Thread { + + private final Socket socket; + + private ReceiverThread(Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + try { + InputStream in = getInputStream(socket); + OutputStream out = getOutputStream(socket); + byte[] buf = new byte[4096]; + while (true) { + int read = in.read(buf); + if (read == -1) { + LOG.info("Receiver thread exiting: EOF"); + return; + } + LOG.info("Received " + read + " bytes"); + out.write(buf, 0, read); + LOG.info("Sent " + read + " bytes"); + } + } catch (IOException e) { + LOG.info("Receiver thread exiting: " + e); + tryToClose(socket, LOG, INFO); + } + } + } + + private class SenderThread extends Thread { + + private final InetAddress serverAddress; + + private SenderThread(InetAddress serverAddress) { + this.serverAddress = serverAddress; + } + + @Override + public void run() { + Socket socket = null; + for (int i = 0; i < MAX_SOCKET_CONNECTION_ATTEMPTS; i++) { + try { + LOG.info("Connecting to " + serverAddress); + socket = socketFactory.createSocket(serverAddress, PORT); + LOG.info("Connected to " + serverAddress); + break; + } catch (IOException e) { + LOG.info("Failed to connect to " + serverAddress + ": " + e); + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + LOG.info("Sender thread interrupted"); + return; + } + } + if (socket == null) return; + byte[] buf = new byte[4096]; + Random random = new Random(); + try { + InputStream in = getInputStream(socket); + OutputStream out = getOutputStream(socket); + // We'll break out of the loop when the sender thread is interrupted + while (true) { + int bytesToSend = 1 + random.nextInt(99); + out.write(buf, 0, bytesToSend); + LOG.info("Sent " + bytesToSend + " bytes"); + int read = in.read(buf); + if (read == -1) { + LOG.info("Sender thread exiting: EOF"); + tryToClose(socket, LOG, INFO); + return; + } + LOG.info("Received " + read + " bytes"); + //noinspection BusyWait + Thread.sleep(1000); + } + } catch (IOException | InterruptedException e) { + LOG.info("Sender thread exiting: " + e); + tryToClose(socket, LOG, INFO); + } + } + } }