package com.odinvt.lanscan; import com.facebook.react.bridge.GuardedAsyncTask; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.odinvt.lanscan.impl.ManagedThreadPoolExecutor; import com.odinvt.lanscan.utils.IPv4; import android.content.Context; import android.net.*; import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.SystemClock; import android.util.Log; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; import javax.annotation.Nullable; public class LANScanModule extends ReactContextBaseJavaModule { private static final int MIN_PORT_NUMBER = 0; private static final int MAX_PORT_NUMBER = 0xFFFF; private static final String KEY_WIFISTATE_DNS1 = "dns1"; private static final String KEY_WIFISTATE_DNS2 = "dns2"; private static final String KEY_WIFISTATE_GATEWAY = "gateway"; private static final String KEY_WIFISTATE_IPADDRESS = "ipAddress"; private static final String KEY_WIFISTATE_LEASEDURATION = "leaseDuration"; private static final String KEY_WIFISTATE_NETMASK = "netmask"; private static final String KEY_WIFISTATE_SERVERADDRESS = "serverAddress"; private static final String KEY_IPv4_HOSTSNUMBER = "hostsNumber"; private static final String EVENT_START = "RNLANScanStart"; private static final String EVENT_STOP = "RNLANScanStop"; private static final String EVENT_STARTFETCH = "RNLANScanStartFetch"; private static final String EVENT_INFOFETCHED = "RNLANScanInfoFetched"; private static final String EVENT_FETCHERROR = "RNLANScanFetchError"; private static final String EVENT_STARTPINGS = "RNLANScanStartPings"; private static final String EVENT_HOSTFOUNDPING = "RNLANScanHostFoundPing"; private static final String EVENT_ENDPINGS = "RNLANScanEndPings"; private static final String EVENT_PORTOUTOFRANGEERROR = "RNLANScanPortOutOfRangeError"; private static final String EVENT_STARTBROADCAST = "RNLANScanStartBroadcast"; private static final String EVENT_HOSTFOUND = "RNLANScanHostFound"; private static final String EVENT_ENDBROADCAST = "RNLANScanEndBroadcast"; private static final String EVENT_END = "RNLANScanEnd"; private static final String EVENT_ERROR = "RNLANScanError"; private DhcpInfo dhcp_info; private IPv4 ipv4_wifi; private ArrayList<String> hosts_list; private HashMap<String, ArrayList<Integer>> available_hosts; public LANScanModule(ReactApplicationContext reactContext) { super(reactContext); } @Override public String getName() { return "RNLANScan"; } @ReactMethod public void fetchInfo(boolean force) { if(this.dhcp_info == null) this.getInfo(); else if(force) this.getInfo(); } @ReactMethod public void stop() { new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { ((ThreadPoolExecutor)ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS).shutdownNow(); ((ThreadPoolExecutor)ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).shutdownNow(); long startTime = System.currentTimeMillis(); long endTime = 0L; long timeout = 1000; boolean isTerminated_broadcast = false; boolean isTerminated_pings = false; // wait until all the threads are terminated // or grace timeout finishes while(!isTerminated_broadcast || !isTerminated_pings || endTime < timeout) { isTerminated_broadcast = ((ThreadPoolExecutor)ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).isTerminated(); isTerminated_pings = ((ThreadPoolExecutor)ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).isTerminated(); endTime = (new Date()).getTime() - startTime; } // successfully stopped the tasks... send top event sendEvent(getReactApplicationContext(), EVENT_STOP, null); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @ReactMethod public void scan(final int min_port, final int max_port, final int broadcast_timeout, final boolean fallback, final int ping_ms, final int port_ms) { // start event should be sent before probable errors sendEvent(getReactApplicationContext(), EVENT_START, null); if(min_port < MIN_PORT_NUMBER || min_port > MAX_PORT_NUMBER || max_port < MIN_PORT_NUMBER || max_port > MAX_PORT_NUMBER) { String err = "Port out of range"; sendEvent(getReactApplicationContext(), EVENT_PORTOUTOFRANGEERROR, err); sendEvent(getReactApplicationContext(), EVENT_ERROR, err); return ; } if(this.getInfo()) { final String broadcastAddr = ipv4_wifi.getBroadcastAddress(); this.available_hosts = new HashMap<>(); // trigger the start broadcast event if it is a broadcast address sendEvent(getReactApplicationContext(), EVENT_STARTBROADCAST, null); for(int i = min_port; i <= max_port; i++) { sendDatagram(broadcastAddr, true, i,broadcast_timeout, ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST); } ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).shutdown(); final long timeout = broadcast_timeout + 500; //awaitTermination of the sendDatagram tasks after locking them with shutdown inside an AsyncTask (no ui framedrops) new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { // TODO: better to use ThreadPoolExecutor.awaitTerminated (for some reason doesn't interrupt) long completed_tasks; long task_count = ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).getTaskCount(); long startTime = System.currentTimeMillis(); long endTime = 0L; // infinite loop that stops on 1 of the 2 conditions: // tasks are completed or timeout ran out while (true) { completed_tasks = ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).getCompletedTaskCount(); //Log.wtf("WAITING FOR TASKS BROADCAST : ", "WAITING FOR TASKS TO COMPLETE " + completed_tasks + "/" + task_count); if (completed_tasks < task_count || endTime < timeout) { endTime = (new Date()).getTime() - startTime; continue; } // at this point all the tasks should be closed. send end broadcast event sendEvent(getReactApplicationContext(), EVENT_ENDBROADCAST, null); if(fallback) { //Log.wtf("FOUND DEVICES : ", "THE BROADCAST FOUND : " + available_hosts.size() + " hosts"); // if no device are found and user wants to fallback to host to host port scan if (available_hosts.size() == 0) { sendEvent(getReactApplicationContext(), EVENT_STARTPINGS, null); ArrayList<String> connected = new ArrayList<>(); //Log.wtf("NETWORK", "PINGING " + hosts_list.size() + " Hosts...."); String device_ip = ""; try { device_ip = intToIp(dhcp_info.ipAddress); } catch (UnknownHostException e) { e.printStackTrace(); } for (String host : hosts_list) { try { if (host.equals(device_ip)) continue; if (InetAddress.getByName(host).isReachable(ping_ms)) { connected.add(host); //Log.wtf("HOST FOUND !!!", host + " RESPONDED"); sendEvent(getReactApplicationContext(), EVENT_HOSTFOUNDPING, host); for (int i = min_port; i <= max_port; i++) { sendDatagram(host, false, i, port_ms, ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS); } } else { //Log.wtf("HOST NOT RESPONSIVE", host + " is not responding"); } } catch (IOException ioe) { /* do nothing just continue to the next host */ ioe.printStackTrace(); } } sendEvent(getReactApplicationContext(), EVENT_ENDPINGS, connected.size()); } } // start waiting for ping UDP tasks to finish to send the end event ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS).shutdown(); long timeout_pings = port_ms + 500; // TODO: better to use ThreadPoolExecutor.awaitTerminated (for some reason doesn't interrupt) long completed_tasks_pings; long task_count_pings = ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS).getTaskCount(); long startTime_pings = System.currentTimeMillis(); long endTime_pings = 0L; // wait for ping tasks // infinite loop that stops on 1 of the 2 conditions: // ping udp tasks are completed or timeout ran out while(true) { completed_tasks_pings = ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS).getCompletedTaskCount(); if (completed_tasks_pings < task_count_pings || endTime_pings < timeout_pings) { endTime_pings = (new Date()).getTime() - startTime_pings; continue; } // at this point all the tasks (pings or broadcast) should be closed. send end event sendEvent(getReactApplicationContext(), EVENT_END, null); break; } break; } } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } public void sendDatagram(final String broadcastAddr, final boolean broadcast, final int port, final long timeout_ms, Executor thread_pool) { try { final DatagramSocket serverSocket = new DatagramSocket(); serverSocket.setBroadcast(broadcast); serverSocket.setReuseAddress(true); InetAddress IPAddress = InetAddress.getByName(broadcastAddr); //Log.wtf("Info", "Sending Discovery message to " + IPAddress.getHostAddress() + " Via UDP port " + port); // we're sending "RNLS" message so if you need to check on the other devices on local network // you need to open an udp listener on port 'port' and wait for the message "RNLS" which is a byte[4] byte[] sendData = new byte[4]; sendData[0] = 'R'; sendData[1] = 'N'; sendData[2] = 'L'; sendData[3] = 'S'; final DatagramPacket sendPacket = new DatagramPacket(sendData,sendData.length,IPAddress,port); //Log.wtf("STARTING TASK : ", "STARTING RECEIVER TASK FOR " + broadcastAddr); // Execute a receiver task in the background to start waiting for LAN replies before sending packets final AsyncTask guarded_receive_task = new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { byte[] receiveData = new byte[6]; // waiting for the message "RNLSOK" DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); // skip if it is the current device String device_ip = ""; try { device_ip = intToIp(dhcp_info.ipAddress); // try to set a timeout for the receive operation of the socket in this thread as // without this if no host responds it will hang forever serverSocket.setSoTimeout((int) timeout_ms); } catch (UnknownHostException e) { e.printStackTrace(); } catch (SocketException e) { e.printStackTrace(); /* if the socket fails to set a timeout for the receive operation we need to force close this thread as other waiting process depend on it completing */ return ; } //noinspection InfiniteLoopStatement while(!isCancelled()) { try { //Log.wtf("NETWORK : ", "WAITING FOR DATAGRAM RESPONSE..."); serverSocket.receive(receivePacket); String sentence = new String( receivePacket.getData(), 0, receivePacket.getLength() ); //Log.wtf("RECEIVED PACKET : " , sentence + " FROM " + receivePacket.getAddress() + ":" + receivePacket.getPort()); InetAddress address = receivePacket.getAddress(); String addr = address.getHostAddress(); int port = receivePacket.getPort(); // skip if packet received from current device if(addr.equals(device_ip)) { //Log.wtf("SKIPING : ", "SKIPPING CURRENT DEVICE RESPONDED ...."); continue; } if(available_hosts.containsKey(addr)) { if(!available_hosts.get(addr).contains(port)) { available_hosts.get(addr).add(port); } } else { ArrayList<Integer> port_arr = new ArrayList<>(); port_arr.add(port); available_hosts.put(addr, port_arr); } WritableMap available_host = new WritableNativeMap(); available_host.putString("host", addr); available_host.putInt("port", port); sendEvent(getReactApplicationContext(), EVENT_HOSTFOUND, available_host); //Log.wtf("NETWORK : ", "LOOPING BACK FOR THE NEXT DATAGRAM RECEIVE..."); } catch (IOException e) { Log.e("IOE", e.getMessage()); /* cancel current task if can't listen for packets */ this.cancel(true); } } } @Override protected void onCancelled() { //Log.wtf("CLOSING TASK : ", "CLOSING RECEIVER TASK FOR " + broadcastAddr); // at this point the socket is not used anymore so we can go ahead and close it serverSocket.disconnect(); serverSocket.close(); } }.executeOnExecutor(thread_pool); //Log.wtf("STARTING TASK : ", "STARTING SENDER TASK FOR " + broadcastAddr); // start sending packets on a background task until it is cancelled then trigger end broadcast event // if it is a broadcast address final AsyncTask guarded_send_task = new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { while(!isCancelled()) { try { ////Log.wtf("SENDING PACKET : " , sendPacket.getData().toString() + " TO " + broadcastAddr + ":" + port); serverSocket.send(sendPacket); } catch (IOException e) { /* do nothing just continue the loop to send the next packet */ Log.e("ERROR SENDING PACKETS " , e.getMessage()); } } // when we exit the loop it means that the task has been cancelled and we're not sending packets anymore //Log.wtf("CLOSING TASK : ", "TRYING TO CLOSE RECEIVER TASK FROM SENDER FOR " + broadcastAddr); // shutdown the receiver task since we're not gonna be needing it anymore if(guarded_receive_task != null) guarded_receive_task.cancel(true); } @Override protected void onCancelled() { //Log.wtf("CLOSING TASK : ", "CLOSING SENDER TASK FOR " + broadcastAddr); } }.executeOnExecutor(thread_pool); // run a sleep task on the background to wait for the timeout new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { SystemClock.sleep(timeout_ms); //Log.wtf("WAITED TIMEOUT : ", "WAIT FOR TIMEOUT FINISHED TRYING TO CLOSE SENDER TASK FOR " + broadcastAddr + "..."); if(guarded_send_task != null) guarded_send_task.cancel(true); this.cancel(true); } }.executeOnExecutor(thread_pool); } catch (SocketException | UnknownHostException e) { e.printStackTrace(); } finally { /* should not be closing datagram socket because it's still used asynchronously by threads */ } } private boolean getInfo() { sendEvent(getReactApplicationContext(), EVENT_STARTFETCH, null); WifiManager wifi_manager= (WifiManager) getReactApplicationContext().getSystemService(Context.WIFI_SERVICE); dhcp_info=wifi_manager.getDhcpInfo(); try { String s_dns1 = intToIp(dhcp_info.dns1); String s_dns2 = intToIp(dhcp_info.dns2); String s_gateway = intToIp(dhcp_info.gateway); String s_ipAddress = intToIp(dhcp_info.ipAddress); int s_leaseDuration = dhcp_info.leaseDuration; String s_netmask = intToIp(dhcp_info.netmask); String s_serverAddress = intToIp(dhcp_info.serverAddress); WritableMap device_info = new WritableNativeMap(); device_info.putString(KEY_WIFISTATE_DNS1, s_dns1); device_info.putString(KEY_WIFISTATE_DNS2, s_dns2); device_info.putString(KEY_WIFISTATE_GATEWAY, s_gateway); device_info.putString(KEY_WIFISTATE_IPADDRESS, s_ipAddress); device_info.putInt(KEY_WIFISTATE_LEASEDURATION, s_leaseDuration); device_info.putString(KEY_WIFISTATE_NETMASK, s_netmask); device_info.putString(KEY_WIFISTATE_SERVERADDRESS, s_serverAddress); ipv4_wifi = new IPv4(s_ipAddress, s_netmask); hosts_list = ipv4_wifi.getHostAddressList(); device_info.putInt(KEY_IPv4_HOSTSNUMBER, ipv4_wifi.getNumberOfHosts().intValue()); sendEvent(getReactApplicationContext(), EVENT_INFOFETCHED, device_info); return true; } catch (UnknownHostException e) { sendEvent(getReactApplicationContext(), EVENT_FETCHERROR, e.getMessage()); sendEvent(getReactApplicationContext(), EVENT_ERROR, e.getMessage()); } return false; } public String intToIp(int i) throws UnknownHostException { byte[] addressBytes = { (byte)(0xff & i), (byte)(0xff & (i >> 8)), (byte)(0xff & (i >> 16)), (byte)(0xff & (i >> 24)) }; InetAddress addr = InetAddress.getByAddress(addressBytes); return addr.getHostAddress(); } protected void sendEvent(ReactContext reactContext, String eventName, @Nullable Object params) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); //Log.wtf("EVENT : ", "TRIGGERED EVENT " + eventName); } }