368 lines
8.9 KiB
JavaScript
368 lines
8.9 KiB
JavaScript
'use strict';
|
|
|
|
const net = require('net');
|
|
const nbind = require('@mcesystems/nbind')
|
|
const ZeroTier = nbind.init().lib.ZeroTier
|
|
|
|
|
|
/*
|
|
* EXAMPLE USAGE
|
|
* Usage: `nc -lv 4444`
|
|
*/
|
|
|
|
function example() {
|
|
// Start ZeroTier service
|
|
ZeroTier.start(".zerotier", 9994);
|
|
|
|
// Join virtual network
|
|
ZeroTier.join("8056c2e21c000001");
|
|
|
|
// Open the socket
|
|
let fd = ZeroTier.connectStream("29.49.7.203", 4444);
|
|
|
|
// Send some data
|
|
ZeroTier.send(fd, Buffer.from("Name?\n", 'utf8'), 0)
|
|
|
|
// Set blocking read mode
|
|
// ZeroTier.fcntlSetBlocking(fd, true);
|
|
let heartbeat = setInterval(() => process.stderr.write('.'), 100)
|
|
|
|
// Receive some data
|
|
const _read = () => {
|
|
const buf = Buffer.alloc(32)
|
|
let bytes = -1
|
|
do {
|
|
bytes = ZeroTier.recv(fd, buf, 0)
|
|
if (bytes > 0) { process.stdout.write(buf.toString('utf8')) }
|
|
} while (bytes > 0);
|
|
|
|
if (!ZeroTier.getMyNode().online || buf.toString('utf8').includes("exit")) {
|
|
// Close the socket
|
|
ZeroTier.close(fd)
|
|
// Stop ZeroTier service
|
|
ZeroTier.stop()
|
|
// Clear the interval
|
|
clearInterval(heartbeat)
|
|
} else {
|
|
setTimeout(_read, 500)
|
|
}
|
|
}
|
|
_read()
|
|
}
|
|
|
|
|
|
// Target API:
|
|
//
|
|
// let s = net.connect({port: 80, host: 'google.com'}, function() {
|
|
// ...
|
|
// });
|
|
//
|
|
// There are various forms:
|
|
//
|
|
// connect(options, [cb])
|
|
// connect(port, [host], [cb])
|
|
// connect(path, [cb]);
|
|
//
|
|
function connect(...args) {
|
|
const normalized = net._normalizeArgs(args);
|
|
const options = normalized[0];
|
|
// debug('createConnection', normalized);
|
|
const socket = new Socket(options);
|
|
|
|
if (options.timeout) {
|
|
socket.setTimeout(options.timeout);
|
|
}
|
|
|
|
return socket.connect(normalized);
|
|
}
|
|
|
|
/*
|
|
* https://github.com/nodejs/node/blob/v12.18.3/lib/net.js#L1107
|
|
*/
|
|
function afterConnect(status, self, req, readable, writable) {
|
|
// const self = handle[owner_symbol];
|
|
|
|
// Callback may come after call to destroy
|
|
if (self.destroyed) {
|
|
return;
|
|
}
|
|
|
|
// debug('afterConnect');
|
|
|
|
// assert(self.connecting);
|
|
self.connecting = false;
|
|
self._sockname = null;
|
|
|
|
if (status === 0) {
|
|
self.readable = readable;
|
|
if (!self._writableState.ended)
|
|
self.writable = writable;
|
|
self._unrefTimer();
|
|
|
|
self.emit('connect');
|
|
self.emit('ready');
|
|
|
|
// Start the first read, or get an immediate EOF.
|
|
// this doesn't actually consume any bytes, because len=0.
|
|
if (readable && !self.isPaused())
|
|
self.read(0);
|
|
|
|
} else {
|
|
self.connecting = false;
|
|
let details;
|
|
if (req.localAddress && req.localPort) {
|
|
details = req.localAddress + ':' + req.localPort;
|
|
}
|
|
const ex = new Error(status,
|
|
'connect',
|
|
req.address,
|
|
req.port,
|
|
details);
|
|
if (details) {
|
|
ex.localAddress = req.localAddress;
|
|
ex.localPort = req.localPort;
|
|
}
|
|
self.destroy(ex);
|
|
}
|
|
}
|
|
|
|
function writeGeneric(self, chunk, encoding, callback) {
|
|
const decodeStrings = self._writableState && self._writableState.decodeStrings
|
|
const buf = (!decodeStrings && !Buffer.isBuffer(chunk)) ? Buffer.from(chunk, encoding) : chunk
|
|
|
|
let bytes
|
|
const err = ZeroTier.send(self._fd, buf, 0)
|
|
switch (err) {
|
|
case -1:
|
|
callback(new Error("ZeroTier Socket error"))
|
|
break
|
|
case -2:
|
|
callback(new Error("ZeroTier Service error"))
|
|
break
|
|
case -3:
|
|
callback(new Error("ZeroTier Invalid argument"))
|
|
break
|
|
default:
|
|
bytes = err
|
|
callback(null)
|
|
}
|
|
|
|
return {
|
|
async: true,
|
|
bytes: bytes,
|
|
}
|
|
}
|
|
|
|
function writevGeneric(self, chunks, callback) {
|
|
const decodeStrings = self._writableState && self._writableState.decodeStrings
|
|
const bufs = chunks.map(({ chunk, encoding }) => (!decodeStrings && !Buffer.isBuffer(chunk)) ? Buffer.from(chunk, encoding) : chunk)
|
|
|
|
let bytes
|
|
const err = ZeroTier.writev(self._fd, bufs)
|
|
switch (err) {
|
|
case -1:
|
|
callback(new Error("ZeroTier Socket error"))
|
|
break
|
|
case -2:
|
|
callback(new Error("ZeroTier Service error"))
|
|
break
|
|
case -3:
|
|
callback(new Error("ZeroTier Invalid argument"))
|
|
break
|
|
default:
|
|
bytes = err
|
|
callback(null)
|
|
}
|
|
|
|
return {
|
|
async: true,
|
|
bytes: bytes,
|
|
}
|
|
}
|
|
|
|
class Socket extends net.Socket {
|
|
/*
|
|
* https://github.com/nodejs/node/blob/v12.18.3/lib/net.js#L929
|
|
*/
|
|
connect(...args) {
|
|
let normalized;
|
|
// If passed an array, it's treated as an array of arguments that have
|
|
// already been normalized (so we don't normalize more than once). This has
|
|
// been solved before in https://github.com/nodejs/node/pull/12342, but was
|
|
// reverted as it had unintended side effects.
|
|
if (Array.isArray(args[0])) {
|
|
normalized = args[0];
|
|
} else {
|
|
normalized = net._normalizeArgs(args);
|
|
}
|
|
const options = normalized[0];
|
|
const cb = normalized[1];
|
|
|
|
// if (this.write !== net.Socket.prototype.write)
|
|
// this.write = net.Socket.prototype.write;
|
|
|
|
if (this.destroyed) {
|
|
this._handle = null;
|
|
this._peername = null;
|
|
this._sockname = null;
|
|
}
|
|
|
|
// const { path } = options;
|
|
// const pipe = !!path;
|
|
// debug('pipe', pipe, path);
|
|
|
|
// if (!this._handle) {
|
|
// this._handle = pipe ?
|
|
// new Pipe(PipeConstants.SOCKET) :
|
|
// new TCP(TCPConstants.SOCKET);
|
|
// initSocketHandle(this);
|
|
// }
|
|
|
|
if (cb !== null) {
|
|
this.once('connect', cb);
|
|
}
|
|
|
|
this._unrefTimer();
|
|
|
|
this.connecting = true;
|
|
this.writable = true;
|
|
|
|
// if (pipe) {
|
|
// validateString(path, 'options.path');
|
|
// defaultTriggerAsyncIdScope(
|
|
// this[async_id_symbol], internalConnect, this, path
|
|
// );
|
|
// } else {
|
|
// lookupAndConnect(this, options);
|
|
// }
|
|
|
|
const { host, port } = options;
|
|
// If host is an IP, skip performing a lookup
|
|
const addressType = net.isIP(host);
|
|
if (addressType) {
|
|
this._fd = ZeroTier.connectStream(host, port);
|
|
afterConnect(0, this, {}, true, true);
|
|
} else {
|
|
throw new Error("DNS LOOKUP NOT IMPLEMENTED");
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/*
|
|
* https://nodejs.org/docs/latest-v12.x/api/stream.html#stream_readable_read_size_1
|
|
*/
|
|
_read(size) {
|
|
// debug('_read');
|
|
|
|
if (this.connecting) {
|
|
// debug('_read wait for connection');
|
|
this.once('connect', () => this._read(size));
|
|
return
|
|
}
|
|
|
|
if (!this.readChunk || this.readChunk.length < size) {
|
|
this.readChunk = Buffer.alloc(size)
|
|
}
|
|
|
|
let bytes = -1
|
|
let moreData = true
|
|
do {
|
|
bytes = ZeroTier.recv(this._fd, this.readChunk, 0)
|
|
switch (bytes) {
|
|
case -2:
|
|
throw new Error("ZeroTier Service error")
|
|
case -3:
|
|
throw new Error("ZeroTier Invalid argument")
|
|
default:
|
|
if (bytes > 0) {
|
|
// this.bytesRead += bytes
|
|
moreData = this.push(this.readChunk)
|
|
}
|
|
}
|
|
} while (bytes > 0 && moreData)
|
|
|
|
if (moreData) { setTimeout(() => this._read(size), 500) }
|
|
}
|
|
|
|
/*
|
|
* https://nodejs.org/docs/latest-v12.x/api/stream.html#stream_writable_writev_chunks_callback
|
|
*/
|
|
_writev(chunks, cb) {
|
|
this._writeGeneric(true, chunks, '', cb);
|
|
}
|
|
|
|
/*
|
|
* https://nodejs.org/docs/latest-v12.x/api/stream.html#stream_writable_write_chunk_encoding_callback_1
|
|
*/
|
|
_write(data, encoding, cb) {
|
|
this._writeGeneric(false, data, encoding, cb);
|
|
}
|
|
|
|
/*
|
|
* https://nodejs.org/docs/latest-v12.x/api/stream.html#stream_writable_final_callback
|
|
*/
|
|
_final(callback) {
|
|
const err = ZeroTier.close(this._fd)
|
|
|
|
switch (err) {
|
|
case -1:
|
|
return callback(new Error("ZeroTier Socket error"))
|
|
break
|
|
case -2:
|
|
return callback(new Error("ZeroTier Service error"))
|
|
break
|
|
default:
|
|
return super._final(callback)
|
|
}
|
|
}
|
|
|
|
/*
|
|
* https://github.com/nodejs/node/blob/v12.18.3/lib/net.js#L760
|
|
*/
|
|
_writeGeneric(writev, data, encoding, cb) {
|
|
// If we are still connecting, then buffer this for later.
|
|
// The Writable logic will buffer up any more writes while
|
|
// waiting for this one to be done.
|
|
if (this.connecting) {
|
|
this._pendingData = data;
|
|
this._pendingEncoding = encoding;
|
|
this.once('connect', function connect() {
|
|
this._writeGeneric(writev, data, encoding, cb);
|
|
});
|
|
return;
|
|
}
|
|
this._pendingData = null;
|
|
this._pendingEncoding = '';
|
|
|
|
// if (!this._handle) {
|
|
// cb(new ERR_SOCKET_CLOSED());
|
|
// return false;
|
|
// }
|
|
|
|
this._unrefTimer();
|
|
|
|
let req;
|
|
if (writev)
|
|
req = writevGeneric(this, data, cb);
|
|
else
|
|
req = writeGeneric(this, data, encoding, cb);
|
|
if (req.async) {
|
|
// this[kLastWriteQueueSize] = req.bytes;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
example,
|
|
start: ZeroTier.start,
|
|
join: ZeroTier.join,
|
|
connect,
|
|
createConnection: connect,
|
|
Socket,
|
|
Stream: Socket, // Legacy naming
|
|
restart: ZeroTier.restart,
|
|
stop: ZeroTier.stop,
|
|
free: ZeroTier.free,
|
|
};
|