Skip to content

Commit

Permalink
#15 Refactoring of upnp, remove axios
Browse files Browse the repository at this point in the history
  • Loading branch information
Stefan Werfling committed Sep 25, 2023
1 parent a966692 commit 8e152ca
Show file tree
Hide file tree
Showing 27 changed files with 873 additions and 972 deletions.
1 change: 0 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"@influxdata/influxdb-client": "^1.33.2",
"adm-zip": "^0.5.10",
"async-exit-hook": "^2.0.1",
"axios": "^1.3.4",
"bcrypt": "^5.1.0",
"cookie-parser": "^1.4.6",
"delay": "^5.0.0",
Expand Down
2 changes: 1 addition & 1 deletion backend/src/inc/Cache/UpnpNatCache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {UpnpNatCacheMapping} from 'flyingfish_schemas';
import NodeCache from 'node-cache';
import {Mapping} from '../Net/Upnp/UpnpNatClient.js';
import {Mapping} from '../Net/UpnpNat/Mapping/Mapping.js';

/**
* UpnpNatCache
Expand Down
265 changes: 265 additions & 0 deletions backend/src/inc/Net/Ssdp/Ssdp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import dgram from 'dgram';
import EventEmitter from 'events';
import os from 'os';
import {nextTick} from 'process';
import {SsdpEmitter} from './SsdpEmitter.js';
import {SsdpOptions} from './SsdpOptions.js';
import {SsdpSearchCallback} from './SsdpSearchCallback.js';

/**
* Simple Service Discovery Protocol (SSDP).
*/
export class Ssdp {

/**
* Source port, default 0.
* @member {number}
*/
protected _sourcePort: number = 0;

/**
* Socket list for requests by any network interface.
* @member {dgram.Socket[]}
*/
protected _sockets: dgram.Socket[] = [];

/**
* Closed request, all incoming are ignored.
* @member {number}
*/
protected _closed: boolean = false;

/**
*
* @member {number}
*/
protected _boundCount: number = 0;

/**
*
* @member {boolean}
*/
protected _bound: boolean = false;

protected _queue: [string, SsdpEmitter][] = [];

/**
* ssdpEmitter
* @private
*/
protected _ssdpEmitter: SsdpEmitter = new EventEmitter();

/**
* Multicast address.
* @member {string}
*/
protected _multicast: string = '239.255.255.250';

/**
* Direct address to device.
* @member {string}
*/
protected _directAddress: string = '';

/**
* Port for ssdp.
* @member {number}
*/
protected _port: number = 1900;

/**
* Constructor for Ssdp object.
* @param {SsdpOptions} options - Options for Ssdp object.
*/
public constructor(options?: SsdpOptions) {
if (options) {
if (options.sourcePort) {
this._sourcePort = options.sourcePort;
}
}

const interfaces = os.networkInterfaces();

Object.keys(interfaces).forEach((name: string): void => {
const tInterface = interfaces[name];

if (tInterface) {
for (const interfaceInfo of tInterface) {
// no use internal (loopback etc ...)
if (!interfaceInfo.internal) {
this._sockets.push(this._createSocket(interfaceInfo));
}
}
}
});
}

/**
* Set a direct address to a device.
* @param {string} address
*/
public setDirectAddress(address: string): void {
this._directAddress = address;
}

/**
* Get the address from a direct device.
* @returns {string}
*/
public getDirectAddress(): string {
return this._directAddress;
}

/**
* Create a socket to interface information.
* @param {os.NetworkInterfaceInfo} ifaceInfo - Interface information.
* @returns {dgram.Socket} Returns a Socket from dgram.
*/
protected _createSocket(ifaceInfo: os.NetworkInterfaceInfo): dgram.Socket {
const socket = dgram.createSocket(ifaceInfo.family === 'IPv4' ? 'udp4' : 'udp6');

socket.on('message', (message: Buffer): void => {
if (this._closed) {
return;
}

this._parseResponse(message.toString(), ifaceInfo.address);
});

nextTick((): void => {
const onReady = (): void => {
if (this._boundCount < this._sockets.length) {
return;
}

this._bound = true;
this._queue.forEach(([device, emitter]) => this.search(device, emitter));
};

socket.on('listening', (): void => {
this._boundCount += 1;
onReady();
});

socket.once('error', (): void => {
socket.close();

this._sockets.splice(
this._sockets.indexOf(socket),
1
);

onReady();
});

socket.bind(this._sourcePort, ifaceInfo.address);
});

return socket;
}

/**
* Parse the response by socket.
* @param {string} response
* @param {string} addr
*/
protected _parseResponse(response: string, addr: string): void {
if (!(/^(HTTP|NOTIFY)/mu).test(response)) {
return;
}

const headers = Ssdp.parseMimeHeader(response);

if (!headers.st) {
return;
}

this._ssdpEmitter.emit('device', headers, addr);
}

/**
* Search a device, build request and send on socket.
* @param {string} device
* @param {SsdpEmitter} emitter
* @returns {SsdpEmitter}
*/
public search(device: string, emitter?: SsdpEmitter): SsdpEmitter {
let nEmitter: SsdpEmitter;

if (emitter) {
nEmitter = emitter;
} else {
nEmitter = new EventEmitter();
nEmitter._ended = false;
nEmitter.once('end', () => {
nEmitter!._ended = true;
});
}

if (!this._bound) {
this._queue.push([device, nEmitter]);
return nEmitter;
}

const query = Buffer.from(
'M-SEARCH * HTTP/1.1\r\n' +
`HOST: ${this._multicast}:${this._port}\r\n` +
'MAN: "ssdp:discover"\r\n' +
'MX: 1\r\n' +
`ST: ${device}\r\n` +
'\r\n'
);

let destination = this._multicast;

if (this._directAddress !== '') {
destination = this._directAddress;
}

this._sockets.forEach((socket: dgram.Socket): void => {
socket.send(query, 0, query.length, this._port, destination);
});

const ondevice: SsdpSearchCallback = (headers, address) => {
if (!nEmitter || nEmitter._ended || headers.st !== device) {
return;
}

nEmitter.emit('device', headers, address);
};

this._ssdpEmitter.on('device', ondevice);

nEmitter.once('end', () => this._ssdpEmitter.removeListener('device', ondevice));

return nEmitter;
}

/**
* Close all sockets.
*/
public close(): void {
this._sockets.forEach((socket: dgram.Socket) => socket.close());
this._closed = true;
}

/**
* Prase mime header.
* @param {string} headerStr
* @returns {Record<string, string>}
*/
public static parseMimeHeader(headerStr: string): Record<string, string> {
const lines = headerStr.split(/\r\n/gu);

return lines.reduce<Record<string, string>>((headers, line) => {
const [, key, value] = line.match(/^([^:]*)\s*:\s*(.*)$/u) ?? [];

if (key && value) {
headers[key.toLowerCase()] = value;
}

return headers;
}, {});
}

}
18 changes: 18 additions & 0 deletions backend/src/inc/Net/Ssdp/SsdpEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import EventEmitter from 'events';
import {SsdpEventListener} from './SsdpEventListener.js';
import {SsdpSearchEvent} from './SsdpSearchEvent.js';

/**
* Ssdp emitter object.
*/
export interface SsdpEmitter extends EventEmitter {

removeListener: SsdpEventListener<this>;
addListener: SsdpEventListener<this>;
once: SsdpEventListener<this>;
on: SsdpEventListener<this>;

emit: SsdpSearchEvent;

_ended?: boolean;
}
11 changes: 11 additions & 0 deletions backend/src/inc/Net/Ssdp/SsdpEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {SsdpSearchCallback} from './SsdpSearchCallback.js';

/**
* Ssdp events.
*/
export type SsdpEvents = 'device' | 'end';

/**
* SsdpEvent
*/
export type SsdpEvent <E extends SsdpEvents> = E extends 'device' ? SsdpSearchCallback : () => void;
3 changes: 3 additions & 0 deletions backend/src/inc/Net/Ssdp/SsdpEventListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {SsdpEvent, SsdpEvents} from './SsdpEvent.js';

export type SsdpEventListener<T> = <E extends SsdpEvents>(ev: E, callback: SsdpEvent<E>) => T;
6 changes: 6 additions & 0 deletions backend/src/inc/Net/Ssdp/SsdpOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Ssdp Options.
*/
export type SsdpOptions = {
sourcePort?: number;
};
9 changes: 9 additions & 0 deletions backend/src/inc/Net/Ssdp/SsdpSearchCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* SsdpSearchArgs
*/
export type SsdpSearchArgs = [Record<string, string>, string];

/**
* SsdpSearchCallback
*/
export type SsdpSearchCallback = (...args: SsdpSearchArgs) => void;
7 changes: 7 additions & 0 deletions backend/src/inc/Net/Ssdp/SsdpSearchEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {SsdpEvents} from './SsdpEvent.js';
import {SsdpSearchArgs} from './SsdpSearchCallback.js';

/**
* Ssdp search event type.
*/
export type SsdpSearchEvent = <E extends SsdpEvents>(ev: E, ...args: E extends 'device' ? SsdpSearchArgs : []) => boolean;
15 changes: 0 additions & 15 deletions backend/src/inc/Net/Upnp/RawResponse.ts

This file was deleted.

Loading

0 comments on commit 8e152ca

Please sign in to comment.