Skip to content

Commit

Permalink
v0.0.4 -- Allow user-input ports and host names
Browse files Browse the repository at this point in the history
  • Loading branch information
belltown committed Mar 13, 2017
1 parent 6eb7705 commit 84f22d7
Show file tree
Hide file tree
Showing 13 changed files with 686 additions and 63 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

VioletBug is a cross-platform desktop application providing a graphical interface to the Roku Debugger as an alternative to Telnet. It is similar to PurpleBug, https://belltown-roku.tk/PurpleBug, which is still supported; however, PurpleBug only runs on Windows PCs, and is closed-source. VioletBug, in contrast, is open-source running under [Electron](http://electron.atom.io/) and [Node.js](https://nodejs.org), written entirely in HTML, CSS and JavaScript. The source code can be found on [GitHub](https://github.com/belltown/violetbug).

Note that VioletBug is not intended as a general-purpose Telnet client. Its features are geared towards debugging on a Roku. Currently, Rokus can only be addressed by IP address, not by host name; and only well-known ports used by the Roku are supported.

## Features

* Runs under Windows (7+), macOS (10.9+), and linux
* Automatic discovery of Rokus on the local network
* A drop-down menu of well-known Roku ports
* Manual input of IP address or host name for undiscovered devices
* A drop-down menu of well-known Roku ports (other ports can be added in the Settings menu)
* Separate tabs for each Roku/port connection
* Floating tabs (right-click the tab header)
* Session logging for each tab
Expand Down
4 changes: 2 additions & 2 deletions doc/README.md.html
Original file line number Diff line number Diff line change
Expand Up @@ -489,12 +489,12 @@
<hr>
<h2 id="windows-macos-linux">Windows — macOS — linux</h2>
<p>VioletBug is a cross-platform desktop application providing a graphical interface to the Roku Debugger as an alternative to Telnet. It is similar to PurpleBug, <a href="https://belltown-roku.tk/PurpleBug">https://belltown-roku.tk/PurpleBug</a>, which is still supported; however, PurpleBug only runs on Windows PCs, and is closed-source. VioletBug, in contrast, is open-source running under <a href="http://electron.atom.io/">Electron</a> and <a href="https://nodejs.org">Node.js</a>, written entirely in HTML, CSS and JavaScript. The source code can be found on <a href="https://github.com/belltown/violetbug">GitHub</a>.</p>
<p>Note that VioletBug is not intended as a general-purpose Telnet client. Its features are geared towards debugging on a Roku. Currently, Rokus can only be addressed by IP address, not by host name; and only well-known ports used by the Roku are supported.</p>
<h2 id="features">Features</h2>
<ul>
<li>Runs under Windows (7+), macOS (10.9+), and linux</li>
<li>Automatic discovery of Rokus on the local network</li>
<li>A drop-down menu of well-known Roku ports</li>
<li>Manual input of IP address or host name for undiscovered devices</li>
<li>A drop-down menu of well-known Roku ports (other ports can be added in the Settings menu)</li>
<li>Separate tabs for each Roku/port connection</li>
<li>Floating tabs (right-click the tab header)</li>
<li>Session logging for each tab</li>
Expand Down
8 changes: 3 additions & 5 deletions source/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,12 @@
<td>
<select id="connectToPort" name="portComboBox"
tabindex="0">
<!--
Ports are now stored in the config file
<option value="8085">8085 (Main Debug)</option>
<option value="8080">8080 (Genkey)</option>
<option value="8087">8087 (Screensaver)</option>
<option value="8089">8089 (Scene Graph) - not used</option>
<option value="8090">8090 (Task 1) - not used</option>
<option value="8091">8091 (Task 2) - not used</option>
<option value="8092">8092 (Task 3) - not used</option>
<option value="8093">8093 (Task 4+) - not used</option>
-->
</select>
</td>
</tr>
Expand Down
106 changes: 102 additions & 4 deletions source/lib/VBDiscover.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

// Node.js modules
const os = require('os') // To get network interfaces
const http = require('http') // ECP requests
const dgram = require('dgram') // SSDP M-SEARCH and NOTIFY

Expand Down Expand Up @@ -32,14 +33,19 @@ function parseDeviceDetails(ipAddr, sn, data) {
// Send an ECP request to the device to get its details
// Invoke the callback to pass the device details back to the caller
function deviceDiscovered(ipAddr, serialNumber, discoveryCallback) {
let remoteAddress = ''
const bufferList = []
const req = http.request({host: ipAddr, port: 8060, family: 4}, (res) => {
res.on('data', (chunk) => {
bufferList.push(chunk)
})
res.on('end', () => {
const response = Buffer.concat(bufferList).toString()
const details = parseDeviceDetails(ipAddr, serialNumber, response)
// Use the remoteAddress obtained from the socket event in case
// an ECP request has been issued specifying a host name
// rather than an IP address
const ip = remoteAddress || ipAddr
const details = parseDeviceDetails(ip, serialNumber, response)
if (details.serialNumber) {
discoveryCallback(details)
}
Expand All @@ -51,6 +57,9 @@ function deviceDiscovered(ipAddr, serialNumber, discoveryCallback) {
// This is instead of setting the timeout when http.request() is called,
// which would only be emitted after the socket is assigned and is connected,
// and would not detect a timeout while trying to establish the connection
// Additionally, we'll need to listen for the socket connect event so we
// can obtain the remote IP address in case an ECP request was issued to
// a host name rather than an IP address
req.on('socket', (socket) => {
socket.setTimeout(10000)
socket.on('timeout', () => {
Expand All @@ -59,16 +68,27 @@ function deviceDiscovered(ipAddr, serialNumber, discoveryCallback) {
// This will cause a createHangUpError error to be emitted on the request
req.abort()
})
// Listen for the socket connect event so we can determine the
// IP address of the Roku, which will be necessary if an ECP request
// was issue to a host name rather than an IP address; this will
// be the earliest time we can determine what the IP address is
socket.on('connect', () => {
remoteAddress = socket.remoteAddress
})
})

// Even if there is an error on the ECP request, invoke the
// discoveryCallback with the known ip address and serial number
req.on('error', (error) => {
const details = parseDeviceDetails(ipAddr, serialNumber, '')
// Use the remoteAddress obtained from the socket connect event in case
// an ECP request has been issued specifying a host name
// rather than an IP address
const ip = remoteAddress || ipAddr
const details = parseDeviceDetails(ip, serialNumber, '')
if (details.serialNumber) {
discoveryCallback(details)
}
console.log('deviceDiscovered error: %O', error)
//console.log('deviceDiscovered error: %O', error)
})

// The ECP request has an empty body
Expand Down Expand Up @@ -166,12 +186,90 @@ function ssdpSearch(discoveryCallback) {
setTimeout(ssdpSearchRequest, 30000, discoveryCallback)
}

// Convert an IP address from a dotted decimal string to a 32-bit integer
function ipAddrTo32 (ipAddr) {
let ip32 = 0
const ma = ipAddr.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
if (Array.isArray(ma) && ma.length === 5) {
const d0 = parseInt(ma[1], 10)
const d1 = parseInt(ma[2], 10)
const d2 = parseInt(ma[3], 10)
const d3 = parseInt(ma[4], 10)
ip32 = ((d0 * 256 + d1) * 256 + d2) * 256 + d3
}
return ip32
}

// Convert a 32-bit IP address to a dotted decimal string
function ip32ToAddr(ip32) {
const d3 = ip32 % 256
let r = (ip32 - d3) / 256
const d2 = r % 256
r = (r - d2) / 256
const d1 = r % 256
const d0 = (r - d1) / 256
return d0 + '.' + d1 + '.' + d2 + '.' + d3
}

// Send an ECP request to each potential host on the network
function subnetScan(discoveryCallback, hostLimit = 256) {

// Get the list of all network interfaces
const interfaceList = os.networkInterfaces()

// Examine each network interface
for (let interfaceListEntry of Object.values(interfaceList)) {

// Get the list of IP addresses handled by this interface
for (let interfaceItem of Object.values(interfaceListEntry)) {

// Only handle non-internal (not loopback), IPv4 addresses
if (!interfaceItem.internal && interfaceItem.family === 'IPv4') {

// Convert the interface ip address and subnet mask
// from dotted decimal to 32-bit integers
const ip32 = ipAddrTo32(interfaceItem.address)
const mask32 = ipAddrTo32(interfaceItem.netmask)

// Only continue if the ip address and subnet mask are valid
if (ip32 > 0 && mask32 > 0) {

// Use the subnet mask to determine the maximum number of
// hosts to scan for on this subnet
const maxHosts = (2 ** 32) - mask32

// Limit the maximum number of hosts addressed
const lastHost = maxHosts > hostLimit ? hostLimit : maxHosts

// Compute the base address for all hosts on this subnet
const base32 = ip32 - (ip32 % maxHosts)

// Scan each host on this subnet (don't scan first and last)
for (let i = 1; i < lastHost - 1; i++) {

// Generate next host ip address
const host32 = base32 + i

// Don't send an ECP request to ourself
if (host32 !== ip32) {
const ipAddr = ip32ToAddr(host32)
// Send an ECP request to the host ip address
deviceDiscovered(ipAddr, '', discoveryCallback)
}
}
}
}
}
}
}

class VBDiscover {

// Initiate SSDP discovery
static discover(discoveryCallback) {
static discover(discoveryCallback, maxHostsToScan = 256) {
ssdpSearch(discoveryCallback)
ssdpNotify(discoveryCallback)
subnetScan(discoveryCallback, maxHostsToScan)
}

// Attempt to acquire device details from a user-entered, non-discovered
Expand Down
Loading

0 comments on commit 84f22d7

Please sign in to comment.