Merge pull request #820 from balena-io/typescript-network

Convert network module to typescript
This commit is contained in:
CameronDiver 2018-11-28 17:32:24 +01:00 committed by GitHub
commit 3e6e2fc9a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 242 additions and 132 deletions

107
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "balena-supervisor",
"version": "8.4.1",
"version": "8.4.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1125,6 +1125,27 @@
"type-is": "~1.6.16"
}
},
"bonjour": {
"version": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c",
"from": "git+https://github.com/resin-io/bonjour.git#fixed-mdns",
"dev": true,
"requires": {
"array-flatten": "^2.1.0",
"deep-equal": "^1.0.1",
"dns-equal": "^1.0.0",
"dns-txt": "^2.0.2",
"multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc",
"multicast-dns-service-types": "^1.1.0"
},
"dependencies": {
"array-flatten": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz",
"integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=",
"dev": true
}
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -1274,6 +1295,12 @@
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"buffer-indexof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
"integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
"dev": true
},
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
@ -2159,6 +2186,12 @@
}
}
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
},
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@ -2402,6 +2435,31 @@
"path-type": "^3.0.0"
}
},
"dns-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
"integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
"dev": true
},
"dns-packet": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
"integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
"dev": true,
"requires": {
"ip": "^1.1.0",
"safe-buffer": "^5.0.1"
}
},
"dns-txt": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
"integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
"dev": true,
"requires": {
"buffer-indexof": "^1.0.0"
}
},
"docker-delta": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/docker-delta/-/docker-delta-2.2.2.tgz",
@ -6471,6 +6529,21 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"multicast-dns": {
"version": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc",
"from": "git+https://github.com/resin-io-modules/multicast-dns.git#listen-on-all-interfaces",
"dev": true,
"requires": {
"dns-packet": "^1.0.1",
"thunky": "^0.1.0"
}
},
"multicast-dns-service-types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
"mute-stream": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
@ -6565,9 +6638,9 @@
"dev": true
},
"network-checker": {
"version": "0.0.6",
"resolved": "http://registry.npmjs.org/network-checker/-/network-checker-0.0.6.tgz",
"integrity": "sha1-165Y5NuVJE0tJnbM1LIlJRf5Mq4=",
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/network-checker/-/network-checker-0.1.1.tgz",
"integrity": "sha512-dc/LiwC0pp37njpe8TA+oRa5BWkH8+WyFVY0aeuDZqMLHq4kaLHqWJowEfKBI7KT39vmTapWJN0SoHxhx6aL4A==",
"dev": true,
"requires": {
"bluebird": "^3.0.0",
@ -8193,27 +8266,9 @@
"dev": true,
"requires": {
"bluebird": "^3.0.0",
"bonjour": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c",
"ip": "^1.1.4",
"lodash": "^4.17.4"
},
"dependencies": {
"array-flatten": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz",
"integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY="
},
"bonjour": {
"version": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c",
"from": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c",
"requires": {
"array-flatten": "^2.1.0",
"deep-equal": "^1.0.1",
"dns-equal": "^1.0.0",
"dns-txt": "^2.0.2",
"multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc",
"multicast-dns-service-types": "^1.1.0"
}
}
}
},
"resin-errors": {
@ -10301,6 +10356,12 @@
"integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=",
"dev": true
},
"thunky": {
"version": "0.1.0",
"resolved": "http://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz",
"integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=",
"dev": true
},
"tildify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tildify/-/tildify-1.0.0.tgz",

View File

@ -70,7 +70,7 @@
"mocha": "^5.1.1",
"mochainon": "^2.0.0",
"mz": "^2.7.0",
"network-checker": "~0.0.5",
"network-checker": "^0.1.1",
"pinejs-client": "^2.4.0",
"prettier": "^1.14.3",
"register-coffee-coverage": "0.0.1",

View File

@ -1,107 +0,0 @@
Promise = require 'bluebird'
_ = require 'lodash'
url = require 'url'
networkCheck = require 'network-checker'
os = require 'os'
fs = Promise.promisifyAll(require('fs'))
constants = require './lib/constants'
{ checkTruthy } = require './lib/validation'
blink = require './lib/blink'
{ EEXIST } = require './lib/errors'
networkPattern =
blinks: 4
pause: 1000
pauseConnectivityCheck = false
enableConnectivityCheck = true
# options: An object of net.connect options, with the addition of:
# timeout: 10s
checkHost = (options) ->
if !enableConnectivityCheck or pauseConnectivityCheck
return true
else
return networkCheck.checkHost(options)
# Custom monitor that uses checkHost function above.
customMonitor = (options, fn) ->
networkCheck.monitor(checkHost, options, fn)
# enable: A Boolean to enable/disable the connectivity checks
exports.enableCheck = enableCheck = (enable) ->
enableConnectivityCheck = enable
# Call back for inotify triggered when the VPN status is changed.
vpnStatusInotifyCallback = ->
fs.lstatAsync(constants.vpnStatusPath + '/active')
.then ->
pauseConnectivityCheck = true
.catch ->
pauseConnectivityCheck = false
exports.startConnectivityCheck = _.once (apiEndpoint, enable, onChangeCallback) ->
exports.enableConnectivityCheck(enable)
if !apiEndpoint?
console.log('No API endpoint specified, skipping connectivity check')
return
parsedUrl = url.parse(apiEndpoint)
fs.mkdirAsync(constants.vpnStatusPath)
.catch EEXIST, (err) ->
console.log('VPN status path exists.')
.then ->
fs.watch(constants.vpnStatusPath, vpnStatusInotifyCallback)
# Manually trigger the call back to detect cases when VPN was switched on before the supervisor starts.
vpnStatusInotifyCallback() if enable
customMonitor
host: parsedUrl.hostname
port: parsedUrl.port ? (if parsedUrl.protocol is 'https:' then 443 else 80)
interval: 10 * 1000
(connected) ->
onChangeCallback?(connected)
if connected
console.log('Internet Connectivity: OK')
blink.pattern.stop()
else
console.log('Waiting for connectivity...')
blink.pattern.start(networkPattern)
# Callback function to enable/disable tcp pings
exports.enableConnectivityCheck = (val) ->
enabled = checkTruthy(val) ? true
enableCheck(enabled)
console.log("Connectivity check enabled: #{enabled}")
exports.connectivityCheckEnabled = Promise.method ->
return enableConnectivityCheck
exports.getIPAddresses = ->
# We get IP addresses but ignore:
# - docker and balena bridges (docker0, docker1, balena0, etc)
# - legacy rce bridges (rce0, etc)
# - tun interfaces like the legacy vpn
# - the resin VPN interface (resin-vpn)
# - loopback interface (lo)
# - the bridge for dnsmasq (resin-dns)
# - the docker network for the supervisor API (supervisor0)
# - custom docker network bridges (br- + 12 hex characters)
_.flatten(_.map(_.omitBy(os.networkInterfaces(), (interfaceFields, interfaceName) ->
/^(?:balena|docker|rce|tun)[0-9]+|tun[0-9]+|resin-vpn|lo|resin-dns|supervisor0|balena-redsocks|resin-redsocks|br-[0-9a-f]{12}$/.test(interfaceName))
, (validInterfaces) ->
_.map(_.pickBy(validInterfaces, family: 'IPv4'), 'address'))
)
exports.startIPAddressUpdate = do ->
_lastIPValues = null
return (callback, interval) ->
getAndReportIP = ->
ips = exports.getIPAddresses()
if !_.isEmpty(_.xor(ips , _lastIPValues))
_lastIPValues = ips
callback(ips)
setInterval( ->
getAndReportIP()
, interval)
getAndReportIP()

156
src/network.ts Normal file
View File

@ -0,0 +1,156 @@
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import { fs } from 'mz';
import * as networkCheck from 'network-checker';
import * as os from 'os';
import * as url from 'url';
import * as constants from './lib/constants';
import { EEXIST } from './lib/errors';
import { checkTruthy } from './lib/validation';
import blink = require('./lib/blink');
const networkPattern = {
blinks: 4,
pause: 1000,
};
let isConnectivityCheckPaused = false;
let isConnectivityCheckEnabled = true;
function checkHost(
opts: networkCheck.ConnectOptions,
): boolean | PromiseLike<boolean> {
return (
!isConnectivityCheckEnabled ||
isConnectivityCheckPaused ||
networkCheck.checkHost(opts)
);
}
function customMonitor(
options: networkCheck.ConnectOptions,
fn: networkCheck.MonitorChangeFunction,
) {
return networkCheck.monitor(checkHost, options, fn);
}
export function enableCheck(enable: boolean) {
isConnectivityCheckEnabled = enable;
}
async function vpnStatusInotifyCallback(): Promise<void> {
try {
await fs.lstat(constants.vpnStatusPath);
isConnectivityCheckPaused = true;
} catch {
isConnectivityCheckPaused = false;
}
}
export const startConnectivityCheck = _.once(
async (
apiEndpoint: string,
enable: boolean,
onChangeCallback?: networkCheck.MonitorChangeFunction,
) => {
enableConnectivityCheck(enable);
if (!apiEndpoint) {
console.log('No API endpoint specified, skipping connectivity check');
return;
}
await Bluebird.resolve(fs.mkdir(constants.vpnStatusPath))
.catch(EEXIST, () => {
console.log('VPN status path exists.');
})
.then(() => {
fs.watch(constants.vpnStatusPath, vpnStatusInotifyCallback);
});
if (enable) {
vpnStatusInotifyCallback();
}
const parsedUrl = url.parse(apiEndpoint);
const port = parseInt(parsedUrl.port!, 10);
customMonitor(
{
host: parsedUrl.hostname,
port: port || (parsedUrl.protocol === 'https' ? 443 : 80),
path: parsedUrl.path || '/',
interval: 10 * 1000,
},
connected => {
if (_.isFunction(onChangeCallback)) {
onChangeCallback(connected);
}
if (connected) {
console.log('Internet Connectivity: OK');
blink.pattern.stop();
} else {
console.log('Waiting for connectivity...');
blink.pattern.start(networkPattern);
}
},
);
},
);
export function enableConnectivityCheck(enable: boolean) {
const boolEnable = checkTruthy(enable);
enable = boolEnable != null ? boolEnable : true;
enableCheck(enable);
console.log(`Connectivity check enabled: ${enable}`);
}
export const connectivityCheckEnabled = Bluebird.method(
() => isConnectivityCheckEnabled,
);
const IP_REGEX = /^(?:balena|docker|rce|tun)[0-9]+|tun[0-9]+|resin-vpn|lo|resin-dns|supervisor0|balena-redsocks|resin-redsocks|br-[0-9a-f]{12}$/;
export function getIPAddresses(): string[] {
// We get IP addresses but ignore:
// - docker and balena bridges (docker0, docker1, balena0, etc)
// - legacy rce bridges (rce0, etc)
// - tun interfaces like the legacy vpn
// - the resin VPN interface (resin-vpn)
// - loopback interface (lo)
// - the bridge for dnsmasq (resin-dns)
// - the docker network for the supervisor API (supervisor0)
// - custom docker network bridges (br- + 12 hex characters)
return _(os.networkInterfaces())
.omitBy((_interfaceFields, interfaceName) => IP_REGEX.test(interfaceName))
.flatMap(validInterfaces => {
return _(validInterfaces)
.pickBy({ family: 'IPv4' })
.map('address')
.value();
})
.value();
}
export function startIPAddressUpdate(): (
callback: (ips: string[]) => void,
interval: number,
) => void {
let lastIPValues: string[] | null = null;
return (cb, interval) => {
const getAndReportIP = () => {
const ips = getIPAddresses();
if (
!_(ips)
.xor(lastIPValues)
.isEmpty()
) {
lastIPValues = ips;
cb(ips);
}
};
setInterval(getAndReportIP, interval);
getAndReportIP();
};
}

View File

@ -11,7 +11,7 @@ declare module 'blinking' {
stop: () => void;
}
function blinking(ledFile: string): Blink;
function blinking(ledFile: string): { pattern: Blink };
export = blinking;
}