├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── android ├── build.gradle ├── react-native-lanscan.iml └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── odinvt │ └── lanscan │ ├── LANScanModule.java │ ├── LANScanReactModule.java │ ├── impl │ └── ManagedThreadPoolExecutor.java │ └── utils │ └── IPv4.java ├── main.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["airbnb", "react-native"] } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | android/build/ 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | main-tf.js 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | 4 | build/ 5 | android/build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | *.xcworkspace 15 | !default.xcworkspace 16 | xcuserdata 17 | profile 18 | *.moved-aside 19 | DerivedData 20 | 21 | local.properties 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Oussama El Bacha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-lanscan 2 | > A React Native library for scanning LAN IPs and ports to find services via device Wi-Fi 3 | 4 | ###### Support 5 | This package is only supported on **android** devices, since I'm not much of an iOS Developer. Feel free to submit a PR of an iOS port I'll be happy to merge ! :) 6 | 7 | ###### Permissions 8 | On **android**, this package adds another request for the `android.permission.ACCESS_WIFI_STATE` permission for your app in the package manifest. So no need to add the permission to your app module AndroidManifest. 9 | 10 | ###### General Information 11 | This package scans the LAN network of the device via Wi-Fi and searches for hosts with services on a port range of your choosing. 12 | 13 | Please take your precautions and check if the device is connected to the network with Wi-Fi first as this package doesn't do that for you. Use the React Native official [NetInfo](https://facebook.github.io/react-native/docs/netinfo.html) API. 14 | 15 | Please keep in mind that this is a fallback method. There are more suitable ways of finding services on the local network like mDNS of the Zero-configuration standard (implementations like Avahi, Bonjour or NSD). Take a look at the awesome [react-native-zeroconf](https://github.com/Apercu/react-native-zeroconf) package by [Balthazar Gronon](https://github.com/Apercu/). But there are switches and routers that tend to block mDNS Registry and Discovery. And this is where **react-native-lanscan** comes in handy. 16 | 17 | Here is how it works : 18 | - Fetches the device dhcp wifi Info. 19 | - Calculates the list of the possible IP Addresses on the network using the fetched device IP address and netmask. 20 | - Calculates the subnet broadcast IP address (e.g For the CIDR `192.168.1.100/24` that would be `192.168.1.255`). 21 | - Keeps sending datagram UDP packets to the subnet broadcast IP address and the port range of your choosing, and keeps waiting for hosts on the subnet to respond for a specified timeout. 22 | - If it does not find any hosts, either there aren't any or the switch is blocking subnet broadcast packets. So if you choose to do so, the package falls back to a **DeepScan**. 23 | - The **DeepScan** consists of sending ICMP ECHO requests to the list of the possible network IPs that was calculated before. (This only works if the switch is not blocking ICMP requests. If it is, well... there isn't much we can do, is there ? :D) 24 | - Keeps sending datagram UDP packets to each connected host on the same port range and keeps waiting for replies. 25 | - Once the services are found, you can then initialize a socket connection to those services using the awesome [react-native-udp](https://github.com/tradle/react-native-udp) package or its fork the [react-native-tcp](https://github.com/PeelTechnologies/react-native-tcp) package by [Andy Prock](https://github.com/aprock) depending on your needs. 26 | 27 | ### Install 28 | npm i -S react-native-lanscan 29 | 30 | #### Android 31 | 32 | 33 | - Add the following line to the bottom of your project's `settings.gradle` file. 34 | 35 | `project(':react-native-lanscan').projectDir = new File(settingsDir, '../node_modules/react-native-lanscan/android')` 36 | 37 | - Change the `include` line of your project's `settings.gradle` to include the `:react-native-lanscan` project. 38 | 39 | `include ':react-native-lanscan', ':app'` 40 | 41 | - Open your app's `build.gradle` file and add the following line to the `dependencies` block. 42 | 43 | `compile project(":react-native-lanscan")` 44 | 45 | - In your app's `MainActivity.java` file, add `new LANScanReactModule()` to the return statement of the `getPackages()` function. 46 | 47 | ``` 48 | ... 49 | new MainReactPackage(), 50 | new LANScanReactModule() 51 | ... 52 | ``` 53 | 54 | - Then in the same file add the import statement : 55 | `import com.odinvt.lanscan.LANScanReactModule;` 56 | 57 | 58 | ### Usage 59 | 60 | import { LANScan } from 'react-native-lanscan'; 61 | let lanscan = new LANScan(); 62 | 63 | #### API 64 | 65 | All the found hosts are returned via event emitter. You need to bind your listeners to the proper events first depending on your logic, then call the API methods. 66 | 67 | ##### Methods 68 | 69 | ###### `scan(min_port, max_port, broadcast_timeout = 500, fallback = true, icmp_timeout = 50, packet_timeout = 500)` Starts the scan for the hosts that have services in the min_port-max_port port range 70 | 71 | 72 | - This method does exactly the steps detailed in the **General Information** section. 73 | - `min_port` and `max_port` are included in the port range. 74 | - `min_port` and `max_port` must be within the port range 0-65535. 75 | - The `broadcast_timeout` **(optional)** parameter `default : 500` specifies how much time in `milliseconds` the UDP broadcast messages should wait for replies. 76 | - The `fallback` **(optional)** parameter `default : true` tells the method to fall back to DEEPSCAN if no host replied to the UDP broadcast messages. 77 | - The `icmp_timeout` **(optional)** parameter `default : 50` specifies how much time in `milliseconds` each ICMP ECHO request should wait for a reply from the host. Keep in mind that the hosts are on the local network so this shouldn't take more than 10 ms. 78 | - If a host replies to an ICMP ECHO request, the method starts sending UDP packets to the hosts on the min_port-max_port port range. The `packet_timeout` **(optional)** paramter `default : 500` tells it how much time in `milliseconds` the UDP packets should wait for replies. 79 | 80 | > The Datagram UDP Sockets send a **byte[4]** "RNLS" message with the packets and expect a **byte[6]** response message with the packets like "RNLSOK". (Check out the java service example at the last section of this README). 81 | 82 | Example : 83 | 84 | ```javascript 85 | 86 | lanscan.scan(48500, 48503, 100, true, 20, 100); 87 | 88 | /* 89 | starts the scan for all the hosts in the network 90 | that have ready UDP services on the ports : 48500, 48501, 48502 or 48503 91 | */ 92 | 93 | ``` 94 | 95 | ###### `fetchInfo()` Fetches the device wifi dhcp info 96 | 97 | - Use this method if you need the system to only fetch the dhcp info without starting a scan because the `scan` method already calls it. 98 | - The info are returned in the `info_fetched` event too. 99 | 100 | ###### `getConnectedHosts()` Returns the hosts that successfully replied to an ICMP ECHO request 101 | 102 | - This will return an `array` of the connected hosts. 103 | - Be sure to **only use this method if** you've set the `fallback` parameter of the `scan` call to `true`. Otherwise, it will always return an empty array as it would've never fallen back to the deepscan. 104 | - This should be called inside `end_pings` or `end` event handlers. As it is only then when you're sure that all the connected hosts list will be available. 105 | 106 | Example : 107 | 108 | ```javascript 109 | 110 | lanscan.on('end_pings', () => { 111 | let connected_hosts = lanscan.getConnectedHosts(); 112 | // connected_hosts = ["192.168.1.10", "192.168.1.15"] 113 | // if 192.168.1.10 & 192.168.1.15 responded to the pings. 114 | }); 115 | lanscan.scan(48500, 48503, 100, true, 20, 100); 116 | 117 | ``` 118 | 119 | ###### `getAvailableHosts()` Returns the hosts that are available on the LAN and the ports that services are listening on on those hosts 120 | 121 | - This will return an `object` of the available hosts. The keys of the object are the `IP addresses` and the values are `array`s of the ports that services are listening on, on those hosts. 122 | - This should be called inside an `end` event handler. As it is only then when you're sure that all the available hosts list will be available. 123 | - You can call this inside an `end_broadcast` event handler too **but only if** you set the `fallback` parameter of the `scan` call to `false` 124 | 125 | Example : 126 | 127 | ```javascript 128 | 129 | lanscan.on('end', () => { 130 | let available_hosts = lanscan.getAvailableHosts(); 131 | }) 132 | lanscan.scan(48500, 48503, 100, true, 20, 100); 133 | 134 | ``` 135 | `available_hosts` is equal to : 136 | 137 | { 138 | "192.168.1.10" : [48500, 48502, 48503], 139 | "192.168.1.15" : [48501], 140 | "192.168.1.18" : [48502, 48503], 141 | } 142 | 143 | if those are the hosts that have services listening on respective UDP ports. Keep in mind that those would be UDP sockets that responded to either broadcast Datagram UDP packets or host-specific Datagram UDP packets with a byte[6] message (check out the **Java Service Example** section at the end of this README). 144 | 145 | 146 | ###### `stop()` Stops all the running scan threads and cleans up the used resources on the native side 147 | 148 | - This does not clear the connectedHosts and availableHosts cached lists. If you want such behavior, you can overwrite your object with a new instance `lanscan = new LANScan()` (don't forget to bind your event listeners to the new object as those would've been destroyed). 149 | 150 | ##### Events 151 | 152 | ```javascript 153 | lanscan.on('eventName', (params...) => { 154 | ... 155 | }) 156 | ``` 157 | 158 | ###### `start` Triggered on scan start 159 | ###### `stop` Triggered after stop() successfully shuts down device scan threads 160 | ###### `start_fetch` Triggered when it starts fetching device dhcp info 161 | ###### `info_fetched` Triggered when the device info are fetched 162 | 163 | - Broadcasts an `object` that contains the device Wi-Fi dhcp info 164 | 165 | ```javascript 166 | 167 | lanscan.on('info_fetched', (info) => { 168 | // do something with `info` 169 | }) 170 | 171 | ``` 172 | 173 | { 174 | ipAddress : "192.168.1.2", 175 | netmask : "255.255.255.0", 176 | gateway : "192.168.1.1", 177 | serverAddress : "192.168.1.1", 178 | dns1 : "8.8.8.8", 179 | dns2 : "8.8.4.4", 180 | hostsNumber : "256" 181 | } 182 | 183 | ###### `start_broadcast` Triggered when the broadcast discovery starts before any packets are sent 184 | ###### `host_found` Triggered when a host is found with a service running on it 185 | 186 | - Broadcasts an `object`. This object has a `string` property named `host` which is the IP address of the found host, and an `int` property named `port` which is the port of the service that responded. 187 | - Broadcasts an `object` of the available hosts up until the current host was discovered. The keys of the object are the `IP addresses` and the values are `array`s of the ports that services are listening on, on those hosts. 188 | - Can trigger either when a host responds to a broadcast UDP packet on a port, or when a host responds to a host-specific UDP packet that was sent to it after it was discovered by an ICMP ECHO request. 189 | - You can safely use this event to update your UI and show the current found services. 190 | 191 | 192 | ```javascript 193 | 194 | lanscan.on('host_found', (host, currentAvailableHosts) => { 195 | // update my UI 196 | }) 197 | lanscan.scan(48500, 48503, 100, true, 20, 100); 198 | 199 | ``` 200 | 201 | `host` is equal to : 202 | 203 | { 204 | host : "192.168.1.15", 205 | port : 48501 206 | } 207 | 208 | If the service on `192.168.1.15:48501` responded to a UDP packet. 209 | 210 | `currentAvailableHosts` is equal to : 211 | 212 | { 213 | "192.168.1.10" : [48500, 48502, 48503], 214 | "192.168.1.15" : [48501] 215 | } 216 | 217 | If those are the services that it found up until the event was triggered. If it finds another service on `192.168.1.15:48502`, then, in another `host_found` event trigger, it would just add that to the ports array : 218 | 219 | { 220 | "192.168.1.10" : [48500, 48502, 48503], 221 | "192.168.1.15" : [48501, 48502] 222 | } 223 | 224 | ###### `end_broadcast` Triggered when the broadcast discovery threads finish 225 | 226 | - You can use this to call `getAvailableHosts()` **but only if** you set the `fallback` parameter of the `scan` call to `false`. 227 | 228 | ###### `start_pings` Triggered when the package starts pinging the calculated list of possible subnet IPs 229 | 230 | - This only triggers when the `scan` call doesn't find any hosts and the `fallback` parameter is set to `true`. 231 | 232 | ###### `host_found_ping` Triggered when a host responds to an ICMP ECHO request within the specified timeout 233 | 234 | - Triggers only between `start_pings` event and `end_pings` event in time. 235 | - Broadcasts a `string` of the IP address of the host that was found. 236 | - Broadcasts an `array` of the hosts that responded to ping requests up until the moment the current host was found. 237 | 238 | ```javascript 239 | 240 | lanscan.on('host_found_ping', (host, currentConnectedHosts) => { 241 | // host = "192.168.1.15" 242 | /* currentConnectedHosts = ["192.168.1.11", "192.168.1.13", "192.168.1.15"] 243 | if "192.168.1.11" and "192.168.1.13" already responded */ 244 | }) 245 | 246 | ``` 247 | 248 | ###### `end_pings` Triggered when all the pings are finished 249 | 250 | - Do not use this to call `getAvailableHosts()` as the threads that send and receive datagram packets may still be running when this event is triggered. 251 | 252 | ###### `end` Triggered when scan stops (when all threads finish) 253 | 254 | - It is always safe to use this to call `getAvailableHosts()` and `getConnectedHosts()` 255 | 256 | ###### `port_out_of_range_error` Triggers when the specified min_port and max_port ports are out of range 257 | 258 | - Broadcasts a `string` message of the error. 259 | - This error does break the execution of the whole process. 260 | 261 | ###### `fetch_error` Triggers when fetchInfo() of the device Wi-Fi State fails 262 | 263 | - Broadcasts a `string` message of the error. 264 | - This error does break the execution of the whole process. 265 | 266 | ###### `error` Triggers whenever an error occurs during the process 267 | 268 | - This includes `port_out_of_range_error` and `fetch_error` errors. 269 | - Broadcasts a `string` message of the error. 270 | - This error does break the execution of the whole process. 271 | 272 | ### Java Service Example 273 | 274 | This is an example of a service implementation that will be discovered by the `react-native-lanscan` package. 275 | 276 | ```java 277 | 278 | import java.io.IOException; 279 | import java.net.*; 280 | 281 | public class Receiver { 282 | 283 | public static void main(String[] args) { 284 | int port = 48500; 285 | new Receiver().run(port); 286 | } 287 | 288 | public void run(int port) { 289 | DatagramSocket serverSocket = null; 290 | try { 291 | serverSocket = new DatagramSocket(port); 292 | byte[] receiveData = new byte[4]; 293 | 294 | System.out.printf("Listening on udp:%s:%d%n", 295 | InetAddress.getLocalHost().getHostAddress(), port); 296 | DatagramPacket receivePacket = new DatagramPacket(receiveData, 297 | receiveData.length); 298 | 299 | while(true) 300 | { 301 | serverSocket.receive(receivePacket); 302 | String sentence = new String( receivePacket.getData(), 0, 303 | receivePacket.getLength() ); 304 | System.out.println("RECEIVED: " + sentence); 305 | // now send acknowledgement packet back to sender 306 | InetAddress IPAddress = receivePacket.getAddress(); 307 | String sendString = "RNLSOK"; 308 | byte[] sendData = sendString.getBytes("UTF-8"); 309 | DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, 310 | IPAddress, receivePacket.getPort()); 311 | serverSocket.send(sendPacket); 312 | } 313 | } catch (IOException e) { 314 | System.out.println(e); 315 | } finally { 316 | serverSocket.close(); 317 | } 318 | } 319 | } 320 | 321 | ``` 322 | 323 | ### Babel 324 | 325 | This component uses ES6. So if you're using `Webpack` you should launch `babel` on `main.js` and output to `main-tf.js` if for some reason the `npm postinstall` script didn't execute. 326 | 327 | "postinstall": "babel main.js --out-file main-tf.js" 328 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | minSdkVersion 16 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | } 14 | 15 | dependencies { 16 | compile 'com.android.support:appcompat-v7:23.4.0' 17 | compile 'com.facebook.react:react-native:+' 18 | } 19 | -------------------------------------------------------------------------------- /android/react-native-lanscan.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/src/main/java/com/odinvt/lanscan/LANScanModule.java: -------------------------------------------------------------------------------- 1 | package com.odinvt.lanscan; 2 | 3 | import com.facebook.react.bridge.GuardedAsyncTask; 4 | import com.facebook.react.bridge.ReactApplicationContext; 5 | import com.facebook.react.bridge.ReactContext; 6 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 7 | import com.facebook.react.bridge.ReactMethod; 8 | import com.facebook.react.bridge.WritableMap; 9 | import com.facebook.react.bridge.WritableNativeMap; 10 | import com.facebook.react.modules.core.DeviceEventManagerModule; 11 | import com.odinvt.lanscan.impl.ManagedThreadPoolExecutor; 12 | import com.odinvt.lanscan.utils.IPv4; 13 | 14 | import android.content.Context; 15 | import android.net.*; 16 | import android.net.wifi.WifiManager; 17 | import android.os.AsyncTask; 18 | import android.os.SystemClock; 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.net.DatagramPacket; 23 | import java.net.DatagramSocket; 24 | import java.net.InetAddress; 25 | import java.net.SocketException; 26 | import java.net.UnknownHostException; 27 | import java.util.ArrayList; 28 | import java.util.Date; 29 | import java.util.HashMap; 30 | import java.util.concurrent.Executor; 31 | import java.util.concurrent.ThreadPoolExecutor; 32 | 33 | import javax.annotation.Nullable; 34 | 35 | public class LANScanModule extends ReactContextBaseJavaModule { 36 | private static final int MIN_PORT_NUMBER = 0; 37 | private static final int MAX_PORT_NUMBER = 0xFFFF; 38 | 39 | private static final String KEY_WIFISTATE_DNS1 = "dns1"; 40 | private static final String KEY_WIFISTATE_DNS2 = "dns2"; 41 | private static final String KEY_WIFISTATE_GATEWAY = "gateway"; 42 | private static final String KEY_WIFISTATE_IPADDRESS = "ipAddress"; 43 | private static final String KEY_WIFISTATE_LEASEDURATION = "leaseDuration"; 44 | private static final String KEY_WIFISTATE_NETMASK = "netmask"; 45 | private static final String KEY_WIFISTATE_SERVERADDRESS = "serverAddress"; 46 | private static final String KEY_IPv4_HOSTSNUMBER = "hostsNumber"; 47 | 48 | 49 | private static final String EVENT_START = "RNLANScanStart"; 50 | private static final String EVENT_STOP = "RNLANScanStop"; 51 | private static final String EVENT_STARTFETCH = "RNLANScanStartFetch"; 52 | private static final String EVENT_INFOFETCHED = "RNLANScanInfoFetched"; 53 | private static final String EVENT_FETCHERROR = "RNLANScanFetchError"; 54 | private static final String EVENT_STARTPINGS = "RNLANScanStartPings"; 55 | private static final String EVENT_HOSTFOUNDPING = "RNLANScanHostFoundPing"; 56 | private static final String EVENT_ENDPINGS = "RNLANScanEndPings"; 57 | private static final String EVENT_PORTOUTOFRANGEERROR = "RNLANScanPortOutOfRangeError"; 58 | private static final String EVENT_STARTBROADCAST = "RNLANScanStartBroadcast"; 59 | private static final String EVENT_HOSTFOUND = "RNLANScanHostFound"; 60 | private static final String EVENT_ENDBROADCAST = "RNLANScanEndBroadcast"; 61 | private static final String EVENT_END = "RNLANScanEnd"; 62 | private static final String EVENT_ERROR = "RNLANScanError"; 63 | 64 | private DhcpInfo dhcp_info; 65 | private IPv4 ipv4_wifi; 66 | private ArrayList hosts_list; 67 | private HashMap> available_hosts; 68 | 69 | public LANScanModule(ReactApplicationContext reactContext) { 70 | super(reactContext); 71 | } 72 | 73 | @Override 74 | public String getName() { 75 | return "RNLANScan"; 76 | } 77 | 78 | @ReactMethod 79 | public void fetchInfo(boolean force) { 80 | if(this.dhcp_info == null) 81 | this.getInfo(); 82 | else if(force) 83 | this.getInfo(); 84 | } 85 | 86 | @ReactMethod 87 | public void stop() { 88 | new GuardedAsyncTask(getReactApplicationContext()) { 89 | @Override 90 | protected void doInBackgroundGuarded(Void... params) { 91 | ((ThreadPoolExecutor)ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS).shutdownNow(); 92 | ((ThreadPoolExecutor)ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).shutdownNow(); 93 | 94 | long startTime = System.currentTimeMillis(); 95 | long endTime = 0L; 96 | long timeout = 1000; 97 | boolean isTerminated_broadcast = false; 98 | boolean isTerminated_pings = false; 99 | 100 | // wait until all the threads are terminated 101 | // or grace timeout finishes 102 | while(!isTerminated_broadcast || !isTerminated_pings || endTime < timeout) { 103 | isTerminated_broadcast = ((ThreadPoolExecutor)ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).isTerminated(); 104 | isTerminated_pings = ((ThreadPoolExecutor)ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).isTerminated(); 105 | endTime = (new Date()).getTime() - startTime; 106 | } 107 | 108 | // successfully stopped the tasks... send top event 109 | sendEvent(getReactApplicationContext(), EVENT_STOP, null); 110 | } 111 | }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 112 | } 113 | 114 | @ReactMethod 115 | 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) { 116 | 117 | // start event should be sent before probable errors 118 | sendEvent(getReactApplicationContext(), EVENT_START, null); 119 | 120 | if(min_port < MIN_PORT_NUMBER || min_port > MAX_PORT_NUMBER || max_port < MIN_PORT_NUMBER || max_port > MAX_PORT_NUMBER) { 121 | String err = "Port out of range"; 122 | sendEvent(getReactApplicationContext(), EVENT_PORTOUTOFRANGEERROR, err); 123 | sendEvent(getReactApplicationContext(), EVENT_ERROR, err); 124 | 125 | return ; 126 | } 127 | 128 | if(this.getInfo()) { 129 | final String broadcastAddr = ipv4_wifi.getBroadcastAddress(); 130 | 131 | this.available_hosts = new HashMap<>(); 132 | 133 | // trigger the start broadcast event if it is a broadcast address 134 | sendEvent(getReactApplicationContext(), EVENT_STARTBROADCAST, null); 135 | 136 | for(int i = min_port; i <= max_port; i++) { 137 | sendDatagram(broadcastAddr, true, i,broadcast_timeout, ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST); 138 | } 139 | 140 | ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).shutdown(); 141 | final long timeout = broadcast_timeout + 500; 142 | 143 | 144 | //awaitTermination of the sendDatagram tasks after locking them with shutdown inside an AsyncTask (no ui framedrops) 145 | new GuardedAsyncTask(getReactApplicationContext()) { 146 | @Override 147 | protected void doInBackgroundGuarded(Void... params) { 148 | 149 | // TODO: better to use ThreadPoolExecutor.awaitTerminated (for some reason doesn't interrupt) 150 | long completed_tasks; 151 | long task_count = ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).getTaskCount(); 152 | 153 | long startTime = System.currentTimeMillis(); 154 | long endTime = 0L; 155 | // infinite loop that stops on 1 of the 2 conditions: 156 | // tasks are completed or timeout ran out 157 | while (true) { 158 | completed_tasks = ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_BROADCAST).getCompletedTaskCount(); 159 | //Log.wtf("WAITING FOR TASKS BROADCAST : ", "WAITING FOR TASKS TO COMPLETE " + completed_tasks + "/" + task_count); 160 | if (completed_tasks < task_count || endTime < timeout) { 161 | endTime = (new Date()).getTime() - startTime; 162 | continue; 163 | } 164 | 165 | // at this point all the tasks should be closed. send end broadcast event 166 | sendEvent(getReactApplicationContext(), EVENT_ENDBROADCAST, null); 167 | 168 | 169 | if(fallback) { 170 | 171 | //Log.wtf("FOUND DEVICES : ", "THE BROADCAST FOUND : " + available_hosts.size() + " hosts"); 172 | 173 | // if no device are found and user wants to fallback to host to host port scan 174 | if (available_hosts.size() == 0) { 175 | 176 | sendEvent(getReactApplicationContext(), EVENT_STARTPINGS, null); 177 | 178 | ArrayList connected = new ArrayList<>(); 179 | //Log.wtf("NETWORK", "PINGING " + hosts_list.size() + " Hosts...."); 180 | String device_ip = ""; 181 | try { 182 | device_ip = intToIp(dhcp_info.ipAddress); 183 | } catch (UnknownHostException e) { 184 | e.printStackTrace(); 185 | } 186 | for (String host : hosts_list) { 187 | try { 188 | if (host.equals(device_ip)) 189 | continue; 190 | if (InetAddress.getByName(host).isReachable(ping_ms)) { 191 | connected.add(host); 192 | 193 | //Log.wtf("HOST FOUND !!!", host + " RESPONDED"); 194 | sendEvent(getReactApplicationContext(), EVENT_HOSTFOUNDPING, host); 195 | 196 | for (int i = min_port; i <= max_port; i++) { 197 | sendDatagram(host, false, i, port_ms, ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS); 198 | } 199 | } else { 200 | //Log.wtf("HOST NOT RESPONSIVE", host + " is not responding"); 201 | } 202 | } catch (IOException ioe) { 203 | /* do nothing just continue to the next host */ 204 | ioe.printStackTrace(); 205 | } 206 | } 207 | sendEvent(getReactApplicationContext(), EVENT_ENDPINGS, connected.size()); 208 | 209 | } 210 | 211 | } 212 | 213 | // start waiting for ping UDP tasks to finish to send the end event 214 | ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS).shutdown(); 215 | long timeout_pings = port_ms + 500; 216 | 217 | // TODO: better to use ThreadPoolExecutor.awaitTerminated (for some reason doesn't interrupt) 218 | long completed_tasks_pings; 219 | long task_count_pings = ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS).getTaskCount(); 220 | 221 | long startTime_pings = System.currentTimeMillis(); 222 | long endTime_pings = 0L; 223 | // wait for ping tasks 224 | // infinite loop that stops on 1 of the 2 conditions: 225 | // ping udp tasks are completed or timeout ran out 226 | while(true) { 227 | completed_tasks_pings = ((ThreadPoolExecutor) ManagedThreadPoolExecutor.THREAD_POOL_EXECUTOR_PINGS).getCompletedTaskCount(); 228 | if (completed_tasks_pings < task_count_pings || endTime_pings < timeout_pings) { 229 | endTime_pings = (new Date()).getTime() - startTime_pings; 230 | continue; 231 | } 232 | 233 | // at this point all the tasks (pings or broadcast) should be closed. send end event 234 | sendEvent(getReactApplicationContext(), EVENT_END, null); 235 | 236 | break; 237 | } 238 | 239 | 240 | 241 | break; 242 | } 243 | } 244 | }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 245 | 246 | 247 | } 248 | } 249 | 250 | public void sendDatagram(final String broadcastAddr, 251 | final boolean broadcast, 252 | final int port, 253 | final long timeout_ms, 254 | Executor thread_pool) { 255 | 256 | 257 | try { 258 | final DatagramSocket serverSocket = new DatagramSocket(); 259 | serverSocket.setBroadcast(broadcast); 260 | serverSocket.setReuseAddress(true); 261 | InetAddress IPAddress = InetAddress.getByName(broadcastAddr); 262 | //Log.wtf("Info", "Sending Discovery message to " + IPAddress.getHostAddress() + " Via UDP port " + port); 263 | 264 | // we're sending "RNLS" message so if you need to check on the other devices on local network 265 | // you need to open an udp listener on port 'port' and wait for the message "RNLS" which is a byte[4] 266 | byte[] sendData = new byte[4]; 267 | sendData[0] = 'R'; 268 | sendData[1] = 'N'; 269 | sendData[2] = 'L'; 270 | sendData[3] = 'S'; 271 | 272 | final DatagramPacket sendPacket = new DatagramPacket(sendData,sendData.length,IPAddress,port); 273 | 274 | //Log.wtf("STARTING TASK : ", "STARTING RECEIVER TASK FOR " + broadcastAddr); 275 | // Execute a receiver task in the background to start waiting for LAN replies before sending packets 276 | final AsyncTask guarded_receive_task = new GuardedAsyncTask(getReactApplicationContext()) { 277 | @Override 278 | protected void doInBackgroundGuarded(Void... params) { 279 | byte[] receiveData = new byte[6]; // waiting for the message "RNLSOK" 280 | DatagramPacket receivePacket = new DatagramPacket(receiveData, 281 | receiveData.length); 282 | 283 | // skip if it is the current device 284 | String device_ip = ""; 285 | try { 286 | device_ip = intToIp(dhcp_info.ipAddress); 287 | 288 | // try to set a timeout for the receive operation of the socket in this thread as 289 | // without this if no host responds it will hang forever 290 | serverSocket.setSoTimeout((int) timeout_ms); 291 | } catch (UnknownHostException e) { 292 | e.printStackTrace(); 293 | } catch (SocketException e) { 294 | e.printStackTrace(); 295 | /* if the socket fails to set a timeout for the receive operation we need to force close this thread as 296 | other waiting process depend on it completing 297 | */ 298 | 299 | return ; 300 | } 301 | 302 | 303 | //noinspection InfiniteLoopStatement 304 | while(!isCancelled()) { 305 | 306 | try { 307 | //Log.wtf("NETWORK : ", "WAITING FOR DATAGRAM RESPONSE..."); 308 | serverSocket.receive(receivePacket); 309 | 310 | String sentence = new String( receivePacket.getData(), 0, 311 | receivePacket.getLength() ); 312 | //Log.wtf("RECEIVED PACKET : " , sentence + " FROM " + receivePacket.getAddress() + ":" + receivePacket.getPort()); 313 | 314 | InetAddress address = receivePacket.getAddress(); 315 | String addr = address.getHostAddress(); 316 | int port = receivePacket.getPort(); 317 | 318 | 319 | // skip if packet received from current device 320 | if(addr.equals(device_ip)) { 321 | //Log.wtf("SKIPING : ", "SKIPPING CURRENT DEVICE RESPONDED ...."); 322 | continue; 323 | } 324 | 325 | if(available_hosts.containsKey(addr)) { 326 | if(!available_hosts.get(addr).contains(port)) { 327 | available_hosts.get(addr).add(port); 328 | } 329 | } else { 330 | ArrayList port_arr = new ArrayList<>(); 331 | port_arr.add(port); 332 | available_hosts.put(addr, port_arr); 333 | } 334 | 335 | WritableMap available_host = new WritableNativeMap(); 336 | available_host.putString("host", addr); 337 | available_host.putInt("port", port); 338 | sendEvent(getReactApplicationContext(), EVENT_HOSTFOUND, available_host); 339 | //Log.wtf("NETWORK : ", "LOOPING BACK FOR THE NEXT DATAGRAM RECEIVE..."); 340 | } catch (IOException e) { 341 | Log.e("IOE", e.getMessage()); 342 | /* cancel current task if can't listen for packets */ 343 | this.cancel(true); 344 | } 345 | } 346 | } 347 | 348 | @Override 349 | protected void onCancelled() { 350 | //Log.wtf("CLOSING TASK : ", "CLOSING RECEIVER TASK FOR " + broadcastAddr); 351 | 352 | // at this point the socket is not used anymore so we can go ahead and close it 353 | serverSocket.disconnect(); 354 | serverSocket.close(); 355 | } 356 | }.executeOnExecutor(thread_pool); 357 | 358 | //Log.wtf("STARTING TASK : ", "STARTING SENDER TASK FOR " + broadcastAddr); 359 | // start sending packets on a background task until it is cancelled then trigger end broadcast event 360 | // if it is a broadcast address 361 | final AsyncTask guarded_send_task = new GuardedAsyncTask(getReactApplicationContext()) { 362 | @Override 363 | protected void doInBackgroundGuarded(Void... params) { 364 | 365 | 366 | while(!isCancelled()) { 367 | try { 368 | ////Log.wtf("SENDING PACKET : " , sendPacket.getData().toString() + " TO " + broadcastAddr + ":" + port); 369 | serverSocket.send(sendPacket); 370 | } catch (IOException e) { 371 | /* do nothing just continue the loop to send the next packet */ 372 | Log.e("ERROR SENDING PACKETS " , e.getMessage()); 373 | } 374 | } 375 | // when we exit the loop it means that the task has been cancelled and we're not sending packets anymore 376 | 377 | //Log.wtf("CLOSING TASK : ", "TRYING TO CLOSE RECEIVER TASK FROM SENDER FOR " + broadcastAddr); 378 | // shutdown the receiver task since we're not gonna be needing it anymore 379 | if(guarded_receive_task != null) 380 | guarded_receive_task.cancel(true); 381 | } 382 | 383 | @Override 384 | protected void onCancelled() { 385 | //Log.wtf("CLOSING TASK : ", "CLOSING SENDER TASK FOR " + broadcastAddr); 386 | 387 | } 388 | }.executeOnExecutor(thread_pool); 389 | 390 | // run a sleep task on the background to wait for the timeout 391 | new GuardedAsyncTask(getReactApplicationContext()) { 392 | @Override 393 | protected void doInBackgroundGuarded(Void... params) { 394 | SystemClock.sleep(timeout_ms); 395 | //Log.wtf("WAITED TIMEOUT : ", "WAIT FOR TIMEOUT FINISHED TRYING TO CLOSE SENDER TASK FOR " + broadcastAddr + "..."); 396 | if(guarded_send_task != null) 397 | guarded_send_task.cancel(true); 398 | this.cancel(true); 399 | } 400 | }.executeOnExecutor(thread_pool); 401 | 402 | 403 | 404 | } catch (SocketException | UnknownHostException e) { 405 | e.printStackTrace(); 406 | } 407 | finally { 408 | /* should not be closing datagram socket because it's still used asynchronously by threads */ 409 | } 410 | 411 | 412 | } 413 | 414 | private boolean getInfo() { 415 | sendEvent(getReactApplicationContext(), EVENT_STARTFETCH, null); 416 | 417 | WifiManager wifi_manager= (WifiManager) getReactApplicationContext().getSystemService(Context.WIFI_SERVICE); 418 | dhcp_info=wifi_manager.getDhcpInfo(); 419 | 420 | try { 421 | String s_dns1 = intToIp(dhcp_info.dns1); 422 | String s_dns2 = intToIp(dhcp_info.dns2); 423 | String s_gateway = intToIp(dhcp_info.gateway); 424 | String s_ipAddress = intToIp(dhcp_info.ipAddress); 425 | int s_leaseDuration = dhcp_info.leaseDuration; 426 | String s_netmask = intToIp(dhcp_info.netmask); 427 | String s_serverAddress = intToIp(dhcp_info.serverAddress); 428 | 429 | WritableMap device_info = new WritableNativeMap(); 430 | device_info.putString(KEY_WIFISTATE_DNS1, s_dns1); 431 | device_info.putString(KEY_WIFISTATE_DNS2, s_dns2); 432 | device_info.putString(KEY_WIFISTATE_GATEWAY, s_gateway); 433 | device_info.putString(KEY_WIFISTATE_IPADDRESS, s_ipAddress); 434 | device_info.putInt(KEY_WIFISTATE_LEASEDURATION, s_leaseDuration); 435 | device_info.putString(KEY_WIFISTATE_NETMASK, s_netmask); 436 | device_info.putString(KEY_WIFISTATE_SERVERADDRESS, s_serverAddress); 437 | 438 | ipv4_wifi = new IPv4(s_ipAddress, s_netmask); 439 | hosts_list = ipv4_wifi.getHostAddressList(); 440 | 441 | device_info.putInt(KEY_IPv4_HOSTSNUMBER, ipv4_wifi.getNumberOfHosts().intValue()); 442 | 443 | sendEvent(getReactApplicationContext(), EVENT_INFOFETCHED, device_info); 444 | 445 | return true; 446 | } catch (UnknownHostException e) { 447 | 448 | sendEvent(getReactApplicationContext(), EVENT_FETCHERROR, e.getMessage()); 449 | sendEvent(getReactApplicationContext(), EVENT_ERROR, e.getMessage()); 450 | } 451 | 452 | return false; 453 | 454 | } 455 | 456 | public String intToIp(int i) throws UnknownHostException { 457 | 458 | byte[] addressBytes = { (byte)(0xff & i), 459 | (byte)(0xff & (i >> 8)), 460 | (byte)(0xff & (i >> 16)), 461 | (byte)(0xff & (i >> 24)) }; 462 | 463 | InetAddress addr = InetAddress.getByAddress(addressBytes); 464 | 465 | return addr.getHostAddress(); 466 | } 467 | 468 | protected void sendEvent(ReactContext reactContext, 469 | String eventName, 470 | @Nullable Object params) { 471 | reactContext 472 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) 473 | .emit(eventName, params); 474 | //Log.wtf("EVENT : ", "TRIGGERED EVENT " + eventName); 475 | } 476 | 477 | } 478 | -------------------------------------------------------------------------------- /android/src/main/java/com/odinvt/lanscan/LANScanReactModule.java: -------------------------------------------------------------------------------- 1 | package com.odinvt.lanscan; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class LANScanReactModule implements ReactPackage { 14 | 15 | @Override 16 | public List> createJSModules() { 17 | return Collections.emptyList(); 18 | } 19 | 20 | @Override 21 | public List createViewManagers(ReactApplicationContext reactContext) { 22 | return Collections.emptyList(); 23 | } 24 | 25 | @Override 26 | public List createNativeModules( 27 | ReactApplicationContext reactContext) { 28 | List modules = new ArrayList<>(); 29 | 30 | modules.add(new LANScanModule(reactContext)); 31 | 32 | return modules; 33 | } 34 | } -------------------------------------------------------------------------------- /android/src/main/java/com/odinvt/lanscan/impl/ManagedThreadPoolExecutor.java: -------------------------------------------------------------------------------- 1 | package com.odinvt.lanscan.impl; 2 | 3 | 4 | import android.support.annotation.NonNull; 5 | 6 | import java.util.concurrent.BlockingQueue; 7 | import java.util.concurrent.Executor; 8 | import java.util.concurrent.LinkedBlockingQueue; 9 | import java.util.concurrent.ThreadFactory; 10 | import java.util.concurrent.ThreadPoolExecutor; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | public class ManagedThreadPoolExecutor { 15 | private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); 16 | private static final int CORE_POOL_SIZE = CPU_COUNT + 1; 17 | private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; 18 | private static final int KEEP_ALIVE = 1; 19 | private static final BlockingQueue sPoolWorkQueue = 20 | new LinkedBlockingQueue<>(128); 21 | private static final ThreadFactory sThreadFactory = new ThreadFactory() { 22 | private final AtomicInteger mCount = new AtomicInteger(1); 23 | 24 | public Thread newThread(@NonNull Runnable r) { 25 | return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); 26 | } 27 | }; 28 | 29 | public static final Executor THREAD_POOL_EXECUTOR_BROADCAST = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, 30 | TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); 31 | public static final Executor THREAD_POOL_EXECUTOR_PINGS = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, 32 | TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); 33 | } 34 | -------------------------------------------------------------------------------- /android/src/main/java/com/odinvt/lanscan/utils/IPv4.java: -------------------------------------------------------------------------------- 1 | package com.odinvt.lanscan.utils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Credits to Nidhish Krishnan 8 | */ 9 | public class IPv4 { 10 | int baseIPnumeric; 11 | int netmaskNumeric; 12 | public IPv4(String symbolicIP, String netmask) throws NumberFormatException { 13 | 14 | /* IP */ 15 | String[] st = symbolicIP.split("\\."); 16 | 17 | if (st.length != 4) 18 | throw new NumberFormatException("Invalid IP address: " + symbolicIP); 19 | 20 | int i = 24; 21 | baseIPnumeric = 0; 22 | 23 | for (int n = 0; n < st.length; n++) { 24 | 25 | int value = Integer.parseInt(st[n]); 26 | 27 | if (value != (value & 0xff)) { 28 | 29 | throw new NumberFormatException("Invalid IP address: "+ symbolicIP); 30 | } 31 | 32 | baseIPnumeric += value << i; 33 | i -= 8; 34 | } 35 | 36 | /* Netmask */ 37 | st = netmask.split("\\."); 38 | 39 | if (st.length != 4) 40 | throw new NumberFormatException("Invalid netmask address: " 41 | 42 | + netmask); 43 | 44 | i = 24; 45 | netmaskNumeric = 0; 46 | 47 | if (Integer.parseInt(st[0]) < 255) { 48 | 49 | throw new NumberFormatException( 50 | "The first byte of netmask can not be less than 255"); 51 | } 52 | for (int n = 0; n < st.length; n++) { 53 | 54 | int value = Integer.parseInt(st[n]); 55 | 56 | if (value != (value & 0xff)) { 57 | 58 | throw new NumberFormatException("Invalid netmask address: " + netmask); 59 | } 60 | 61 | netmaskNumeric += value << i; 62 | i -= 8; 63 | 64 | } 65 | /* 66 | * see if there are zeroes inside netmask, like: 1111111101111 This is 67 | * illegal, throw exception if encountered. Netmask should always have 68 | * only ones, then only zeroes, like: 11111111110000 69 | */ 70 | boolean encounteredOne = false; 71 | int ourMaskBitPattern = 1; 72 | 73 | for (i = 0; i < 32; i++) { 74 | 75 | if ((netmaskNumeric & ourMaskBitPattern) != 0) { 76 | 77 | encounteredOne = true; // the bit is 1 78 | } else { // the bit is 0 79 | if (encounteredOne == true) 80 | 81 | throw new NumberFormatException("Invalid netmask: " + netmask + " (bit " + (i + 1) + ")"); 82 | } 83 | 84 | ourMaskBitPattern = ourMaskBitPattern << 1; 85 | } 86 | } 87 | 88 | /** 89 | * Specify IP in CIDR format like: new IPv4("10.1.0.25/16"); 90 | * 91 | *@param IPinCIDRFormat 92 | */ 93 | public IPv4(String IPinCIDRFormat) throws NumberFormatException { 94 | 95 | String[] st = IPinCIDRFormat.split("\\/"); 96 | if (st.length != 2) 97 | 98 | throw new NumberFormatException("Invalid CIDR format '" 99 | + IPinCIDRFormat + "', should be: xx.xx.xx.xx/xx"); 100 | 101 | String symbolicIP = st[0]; 102 | String symbolicCIDR = st[1]; 103 | 104 | Integer numericCIDR = new Integer(symbolicCIDR); 105 | if (numericCIDR > 32) 106 | 107 | throw new NumberFormatException("CIDR can not be greater than 32"); 108 | 109 | /* IP */ 110 | st = symbolicIP.split("\\."); 111 | 112 | if (st.length != 4) 113 | throw new NumberFormatException("Invalid IP address: " + symbolicIP); 114 | 115 | int i = 24; 116 | baseIPnumeric = 0; 117 | 118 | for (int n = 0; n < st.length; n++) { 119 | 120 | int value = Integer.parseInt(st[n]); 121 | 122 | if (value != (value & 0xff)) { 123 | 124 | throw new NumberFormatException("Invalid IP address: " + symbolicIP); 125 | } 126 | 127 | baseIPnumeric += value << i; 128 | i -= 8; 129 | 130 | } 131 | 132 | /* netmask from CIDR */ 133 | if (numericCIDR < 8) 134 | throw new NumberFormatException("Netmask CIDR can not be less than 8"); 135 | netmaskNumeric = 0xffffffff; 136 | netmaskNumeric = netmaskNumeric << (32 - numericCIDR); 137 | 138 | } 139 | 140 | /** 141 | * Get the IP in symbolic form, i.e. xxx.xxx.xxx.xxx 142 | * 143 | *@return 144 | */ 145 | public String getIP() { 146 | return convertNumericIpToSymbolic(baseIPnumeric); 147 | 148 | } 149 | 150 | private String convertNumericIpToSymbolic(Integer ip) { 151 | StringBuffer sb = new StringBuffer(15); 152 | 153 | for (int shift = 24; shift > 0; shift -= 8) { 154 | 155 | // process 3 bytes, from high order byte down. 156 | sb.append(Integer.toString((ip >>> shift) & 0xff)); 157 | 158 | sb.append('.'); 159 | } 160 | sb.append(Integer.toString(ip & 0xff)); 161 | 162 | return sb.toString(); 163 | } 164 | 165 | /** 166 | * Get the net mask in symbolic form, i.e. xxx.xxx.xxx.xxx 167 | * 168 | *@return 169 | */ 170 | 171 | public String getNetmask() { 172 | StringBuffer sb = new StringBuffer(15); 173 | 174 | for (int shift = 24; shift > 0; shift -= 8) { 175 | 176 | // process 3 bytes, from high order byte down. 177 | sb.append(Integer.toString((netmaskNumeric >>> shift) & 0xff)); 178 | 179 | sb.append('.'); 180 | } 181 | sb.append(Integer.toString(netmaskNumeric & 0xff)); 182 | 183 | return sb.toString(); 184 | } 185 | 186 | /** 187 | * Get the IP and netmask in CIDR form, i.e. xxx.xxx.xxx.xxx/xx 188 | * 189 | *@return 190 | */ 191 | 192 | public String getCIDR() { 193 | int i; 194 | for (i = 0; i < 32; i++) { 195 | 196 | if ((netmaskNumeric << i) == 0) 197 | break; 198 | 199 | } 200 | return convertNumericIpToSymbolic(baseIPnumeric & netmaskNumeric) + "/" + i; 201 | } 202 | 203 | /** 204 | * Get an arry of all the IP addresses available for the IP and netmask/CIDR 205 | * given at initialization 206 | * 207 | *@return 208 | */ 209 | public List getAvailableIPs(Integer numberofIPs) { 210 | 211 | ArrayList result = new ArrayList(); 212 | int numberOfBits; 213 | 214 | for (numberOfBits = 0; numberOfBits < 32; numberOfBits++) { 215 | 216 | if ((netmaskNumeric << numberOfBits) == 0) 217 | break; 218 | 219 | } 220 | Integer numberOfIPs = 0; 221 | for (int n = 0; n < (32 - numberOfBits); n++) { 222 | 223 | numberOfIPs = numberOfIPs << 1; 224 | numberOfIPs = numberOfIPs | 0x01; 225 | 226 | } 227 | 228 | Integer baseIP = baseIPnumeric & netmaskNumeric; 229 | 230 | for (int i = 1; i < (numberOfIPs) && i < numberofIPs; i++) { 231 | 232 | Integer ourIP = baseIP + i; 233 | 234 | String ip = convertNumericIpToSymbolic(ourIP); 235 | 236 | result.add(ip); 237 | } 238 | return result; 239 | } 240 | 241 | /** 242 | * Range of hosts 243 | * 244 | *@return 245 | */ 246 | public String getHostAddressRange() { 247 | 248 | int numberOfBits; 249 | for (numberOfBits = 0; numberOfBits < 32; numberOfBits++) { 250 | 251 | if ((netmaskNumeric << numberOfBits) == 0) 252 | break; 253 | } 254 | Integer numberOfIPs = 0; 255 | for (int n = 0; n < (32 - numberOfBits); n++) { 256 | 257 | numberOfIPs = numberOfIPs << 1; 258 | numberOfIPs = numberOfIPs | 0x01; 259 | 260 | } 261 | 262 | Integer baseIP = baseIPnumeric & netmaskNumeric; 263 | String firstIP = convertNumericIpToSymbolic(baseIP + 1); 264 | String lastIP = convertNumericIpToSymbolic(baseIP + numberOfIPs - 1); 265 | return firstIP + " - " + lastIP; 266 | } 267 | 268 | /** 269 | * ArrayList of Hosts 270 | * 271 | *@return 272 | */ 273 | public ArrayList getHostAddressList() { 274 | 275 | int numberOfBits; 276 | for (numberOfBits = 0; numberOfBits < 32; numberOfBits++) { 277 | 278 | if ((netmaskNumeric << numberOfBits) == 0) 279 | break; 280 | } 281 | Integer numberOfIPs = 0; 282 | for (int n = 0; n < (32 - numberOfBits); n++) { 283 | 284 | numberOfIPs = numberOfIPs << 1; 285 | numberOfIPs = numberOfIPs | 0x01; 286 | 287 | } 288 | 289 | Integer baseIP = baseIPnumeric & netmaskNumeric; 290 | ArrayList ips = new ArrayList<>(); 291 | for(int i = 1; i <= numberOfIPs - 1; i++) { 292 | if(baseIPnumeric == baseIP + i) 293 | continue; 294 | 295 | ips.add(convertNumericIpToSymbolic(baseIP + i)); 296 | } 297 | return ips; 298 | } 299 | 300 | /** 301 | * Returns number of hosts available in given range 302 | * 303 | *@return number of hosts 304 | */ 305 | public Long getNumberOfHosts() { 306 | int numberOfBits; 307 | 308 | for (numberOfBits = 0; numberOfBits < 32; numberOfBits++) { 309 | 310 | if ((netmaskNumeric << numberOfBits) == 0) 311 | break; 312 | 313 | } 314 | 315 | Double x = Math.pow(2, (32 - numberOfBits)); 316 | 317 | if (x == -1) 318 | x = 1D; 319 | 320 | return x.longValue(); 321 | } 322 | 323 | /** 324 | * The XOR of the netmask 325 | * 326 | *@return wildcard mask in text form, i.e. 0.0.15.255 327 | */ 328 | 329 | public String getWildcardMask() { 330 | Integer wildcardMask = netmaskNumeric ^ 0xffffffff; 331 | 332 | StringBuffer sb = new StringBuffer(15); 333 | for (int shift = 24; shift > 0; shift -= 8) { 334 | 335 | // process 3 bytes, from high order byte down. 336 | sb.append(Integer.toString((wildcardMask >>> shift) & 0xff)); 337 | 338 | sb.append('.'); 339 | } 340 | sb.append(Integer.toString(wildcardMask & 0xff)); 341 | 342 | return sb.toString(); 343 | 344 | } 345 | 346 | public String getBroadcastAddress() { 347 | 348 | if (netmaskNumeric == 0xffffffff) 349 | return "0.0.0.0"; 350 | 351 | int numberOfBits; 352 | for (numberOfBits = 0; numberOfBits < 32; numberOfBits++) { 353 | 354 | if ((netmaskNumeric << numberOfBits) == 0) 355 | break; 356 | 357 | } 358 | Integer numberOfIPs = 0; 359 | for (int n = 0; n < (32 - numberOfBits); n++) { 360 | 361 | numberOfIPs = numberOfIPs << 1; 362 | numberOfIPs = numberOfIPs | 0x01; 363 | } 364 | 365 | Integer baseIP = baseIPnumeric & netmaskNumeric; 366 | Integer ourIP = baseIP + numberOfIPs; 367 | 368 | String ip = convertNumericIpToSymbolic(ourIP); 369 | 370 | return ip; 371 | } 372 | 373 | private String getBinary(Integer number) { 374 | String result = ""; 375 | 376 | Integer ourMaskBitPattern = 1; 377 | for (int i = 1; i <= 32; i++) { 378 | 379 | if ((number & ourMaskBitPattern) != 0) { 380 | 381 | result = "1" + result; // the bit is 1 382 | } else { // the bit is 0 383 | 384 | result = "0" + result; 385 | } 386 | if ((i % 8) == 0 && i != 0 && i != 32) 387 | 388 | result = "." + result; 389 | ourMaskBitPattern = ourMaskBitPattern << 1; 390 | 391 | } 392 | return result; 393 | } 394 | 395 | public String getNetmaskInBinary() { 396 | 397 | return getBinary(netmaskNumeric); 398 | } 399 | 400 | /** 401 | * Checks if the given IP address contains in subnet 402 | * 403 | *@param IPaddress 404 | *@return 405 | */ 406 | public boolean contains(String IPaddress) { 407 | 408 | Integer checkingIP = 0; 409 | String[] st = IPaddress.split("\\."); 410 | 411 | if (st.length != 4) 412 | throw new NumberFormatException("Invalid IP address: " + IPaddress); 413 | 414 | int i = 24; 415 | for (int n = 0; n < st.length; n++) { 416 | 417 | int value = Integer.parseInt(st[n]); 418 | 419 | if (value != (value & 0xff)) { 420 | 421 | throw new NumberFormatException("Invalid IP address: " 422 | + IPaddress); 423 | } 424 | 425 | checkingIP += value << i; 426 | i -= 8; 427 | } 428 | 429 | if ((baseIPnumeric & netmaskNumeric) == (checkingIP & netmaskNumeric)) 430 | 431 | return true; 432 | else 433 | return false; 434 | } 435 | 436 | public boolean contains(IPv4 child) { 437 | 438 | Integer subnetID = child.baseIPnumeric; 439 | 440 | Integer subnetMask = child.netmaskNumeric; 441 | 442 | if ((subnetID & this.netmaskNumeric) == (this.baseIPnumeric & this.netmaskNumeric)) { 443 | 444 | if ((this.netmaskNumeric < subnetMask) == true 445 | && this.baseIPnumeric <= subnetID) { 446 | 447 | return true; 448 | } 449 | 450 | } 451 | return false; 452 | 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import RN, { NativeModules, DeviceEventEmitter } from 'react-native'; 2 | import { EventEmitter } from 'events' 3 | EventEmitter.defaultMaxListeners = Infinity; 4 | 5 | let RNLANScan = NativeModules.RNLANScan; 6 | 7 | export class LANScan extends EventEmitter { 8 | 9 | static listenerCount = 0; 10 | 11 | _connectedHosts; 12 | _availableHosts; 13 | 14 | constructor(props) { 15 | super(props); 16 | 17 | this._connectedHosts = []; 18 | this._availableHosts = {}; 19 | 20 | if(LANScan.listenerCount && LANScan.listenerCount > 0) { 21 | DeviceEventEmitter.removeAllListeners('RNLANScanStart'); 22 | DeviceEventEmitter.removeAllListeners('RNLANScanStop'); 23 | DeviceEventEmitter.removeAllListeners('RNLANScanStartFetch'); 24 | DeviceEventEmitter.removeAllListeners('RNLANScanInfoFetched'); 25 | DeviceEventEmitter.removeAllListeners('RNLANScanFetchError'); 26 | DeviceEventEmitter.removeAllListeners('RNLANScanStartPings'); 27 | DeviceEventEmitter.removeAllListeners('RNLANScanHostFoundPing'); 28 | DeviceEventEmitter.removeAllListeners('RNLANScanEndPings'); 29 | DeviceEventEmitter.removeAllListeners('RNLANScanPortOutOfRangeError'); 30 | DeviceEventEmitter.removeAllListeners('RNLANScanStartBroadcast'); 31 | DeviceEventEmitter.removeAllListeners('RNLANScanHostFound'); 32 | DeviceEventEmitter.removeAllListeners('RNLANScanEndBroadcast'); 33 | DeviceEventEmitter.removeAllListeners('RNLANScanEnd'); 34 | DeviceEventEmitter.removeAllListeners('RNLANScanError'); 35 | 36 | LANScan.listenerCount = 0; 37 | } 38 | 39 | if(LANScan.listenerCount === 0) { 40 | 41 | DeviceEventEmitter.addListener('RNLANScanStart', () => this.emit('start')); 42 | DeviceEventEmitter.addListener('RNLANScanStop', () => this.emit('stop')); 43 | DeviceEventEmitter.addListener('RNLANScanStartFetch', () => this.emit('start_fetch')); 44 | DeviceEventEmitter.addListener('RNLANScanInfoFetched', (info) => { 45 | if(!info || !info.ipAddress || !info.netmask) { 46 | this.emit('fetch_error', "No Info fetched from the device wifi state"); 47 | return false; 48 | } 49 | 50 | this.emit('info_fetched', info); 51 | }); 52 | DeviceEventEmitter.addListener('RNLANScanFetchError', (msg) => this.emit('fetch_error', msg)); 53 | DeviceEventEmitter.addListener('RNLANScanStartPings', () => this.emit('start_pings')); 54 | DeviceEventEmitter.addListener('RNLANScanHostFoundPing', (host) => { 55 | 56 | if(!host) 57 | return false; 58 | 59 | this._connectedHosts = [...this._connectedHosts, host]; 60 | 61 | this.emit('host_found_ping', host, this._connectedHosts); 62 | }); 63 | DeviceEventEmitter.addListener('RNLANScanEndPings', (number) => { 64 | 65 | if(number === null) 66 | return false; 67 | 68 | this.emit('end_pings', number); 69 | }); 70 | DeviceEventEmitter.addListener('RNLANScanPortOutOfRangeError', (msg) => this.emit('port_out_of_range_error', msg)); 71 | DeviceEventEmitter.addListener('RNLANScanStartBroadcast', () => this.emit('start_broadcast')); 72 | DeviceEventEmitter.addListener('RNLANScanHostFound', (host) => { 73 | 74 | if(!host || !host.host || typeof host.host != "string" || host.port === null) 75 | return false; 76 | 77 | if(this._availableHosts.hasOwnProperty(host.host)) { 78 | if(this._availableHosts[host.host].indexOf(host.port) == -1) { 79 | this._availableHosts[host.host].push(host.port); 80 | } 81 | } else { 82 | this._availableHosts[host.host] = [host.port]; 83 | } 84 | 85 | this.emit('host_found', host, this._availableHosts); 86 | }); 87 | DeviceEventEmitter.addListener('RNLANScanEndBroadcast', () => this.emit('end_broadcast')); 88 | DeviceEventEmitter.addListener('RNLANScanEnd', () => this.emit('end')); 89 | DeviceEventEmitter.addListener('RNLANScanError', (msg) => this.emit('error', msg)); 90 | 91 | } 92 | 93 | 94 | } 95 | 96 | scan(min_port, max_port, broadcast_timeout = 500, fallback = true, ping_ms = 50, port_ms = 500) { 97 | RNLANScan.scan(min_port, max_port, broadcast_timeout, fallback, ping_ms, port_ms); 98 | } 99 | 100 | stop() { 101 | RNLANScan.stop(); 102 | } 103 | 104 | fetchInfo() { 105 | RNLANScan.fetchInfo(); 106 | } 107 | 108 | getConnectedHosts() { 109 | return this._connectedHosts; 110 | } 111 | 112 | getAvailableHosts() { 113 | return this._availableHosts; 114 | } 115 | 116 | 117 | 118 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-lanscan", 3 | "version": "1.0.3", 4 | "description": "A React Native library for scanning LAN IPs and ports to find services via device Wi-Fi", 5 | "main": "main-tf.js", 6 | "scripts": { 7 | "postinstall": "babel main.js --out-file main-tf.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Odinvt/react-native-lanscan.git" 12 | }, 13 | "dependencies": { 14 | "babel-cli": "^6.9.0", 15 | "babel-core": "^6.9.1", 16 | "babel-plugin-transform-es2015-constants": "^6.1.4", 17 | "babel-preset-airbnb": "^2.1.1", 18 | "babel-preset-react-native": "^1.9.0" 19 | }, 20 | "devDependencies": { 21 | "babel-eslint": "^4.1.6", 22 | "eslint": "^1.10.3", 23 | "eslint-config-zavatta": "^2.0.2" 24 | }, 25 | "keywords": [ 26 | "react", 27 | "native", 28 | "react-native", 29 | "lanscan", 30 | "lan", 31 | "scan", 32 | "port", 33 | "wifi" 34 | ], 35 | "author": "Oussama El Bacha", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/Odinvt/react-native-lanscan/issues" 39 | }, 40 | "homepage": "https://github.com/Odinvt/react-native-lanscan#readme" 41 | } 42 | --------------------------------------------------------------------------------