Skip to content

Instantly share code, notes, and snippets.

@JLChnToZ
Last active November 1, 2018 13:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLChnToZ/eb928f52678b5660f833a6d1d4858557 to your computer and use it in GitHub Desktop.
Save JLChnToZ/eb928f52678b5660f833a6d1d4858557 to your computer and use it in GitHub Desktop.
import { createSocket, Socket } from 'dgram';
import { isIP } from 'net';
import { createServer } from 'http';
import { URL } from 'url';
import { lookup as dnsLookup } from 'dns';
import { unescape as unescapeQuery } from 'querystring';
import { promisify } from 'util';
const macAddressFormat = /[^0-9A-F]/g;
const dnsLookupAsync = promisify(dnsLookup);
async function resolveHostPort(hostPort: string) {
// Make the host/port looks like a url for processing...
const url = new URL(`http://${hostPort || '255.255.255.255'}`);
const port = Number.parseInt(url.port);
let { hostname } = url;
if(!isIP(url.hostname))
try {
hostname = (await dnsLookupAsync(hostname)).address;
} catch {}
return { hostname, port };
}
/** Create WOL magic packet for wake up specified device */
function createMagicPacket(mac: number | string) {
if(typeof mac === 'string') {
if(mac.length <= 10)
// Base-36 encoded MAC address
mac = Number.parseInt(mac, 36);
else
// Normal form MAC address string
mac = Number.parseInt(mac.replace(macAddressFormat, ''), 16);
}
if(!Number.isInteger(mac) || mac < 0 || mac > 281474976710655)
throw new TypeError('Invalid MAC address');
// 6 times 0xFF, followed by 16 times MAC address, total 102 bytes.
const magic = Buffer.allocUnsafe(102);
magic.fill(0xFF, 0, 6);
for(let i = 1; i <= 16; i++)
magic.writeUIntBE(mac, 6 * i, 6);
return magic;
}
/** Wake up the device */
function wakeup(mac: number | string, port?: number, host?: string) {
return new Promise<void>((resolve, reject) => {
let client: Socket;
const magic = createMagicPacket(mac);
switch(isIP(host || '')) {
// Default host is ipv4 broadcast
default: host = '255.255.255.255';
case 4: client = createSocket('udp4'); break;
case 6: client = createSocket('udp6'); break;
}
client.send(magic, 0, magic.length, port || 9, host, err => {
try {
if(err) reject(err);
else resolve();
} finally {
client.close();
}
});
client.on('error', err => {
try {
reject(err);
} finally {
client.close();
}
});
client.once('listening', onSocketListen);
});
}
function onSocketListen(this: Socket) {
this.setBroadcast(true);
}
const macMatcher = /^((?:[0-9A-F:.-]{12,17})|(?:[0-9A-Z]{1,10}))$/i;
createServer(async(req, res) => {
const parsedUrl = req.url && req.url.split('/');
let targetMAC: string;
if(!parsedUrl || !macMatcher.test(targetMAC = unescapeQuery(parsedUrl[1]))) {
res.writeHead(404, 'Not found');
res.end();
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
try {
const { hostname, port } = await resolveHostPort(unescapeQuery(parsedUrl[2] || ''));
await wakeup(targetMAC, port, hostname);
res.write(JSON.stringify({ success: true }));
} catch(e) {
res.write(JSON.stringify({ success: false, error: e.message || e }));
console.error(e.stack || e);
}
res.end();
}).listen(9999);
@JLChnToZ
Copy link
Author

JLChnToZ commented Oct 3, 2018

Usage: Assume this script is running on a server with domain example.com and you want to wake up a device with MAC address AA:BB:CC:DD:EE:FF within the same network of that server, you may do the request remotely like this:

  • http://example.com:9999/AA:BB:CC:DD:EE:FF: Use broadcast IPv4 and UDP 9.
  • http://example.com:9999/AA:BB:CC:DD:EE:FF/192.168.1.1: Send to specified IP address and default UDP (9).
  • http://example.com:9999/AA:BB:CC:DD:EE:FF/192.168.1.1/7: Send to specified IP address and UDP.
  • http://example.com:9999/AA:BB:CC:DD:EE:FF//7: Use broadcast IPv4 and specified UDP.
  • http://example.com:9999/1ujj0ob3lr: MAC address can be base-36 encoded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment