-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#15 Refactoring of upnp, remove axios
- Loading branch information
Stefan Werfling
committed
Sep 25, 2023
1 parent
a966692
commit 8e152ca
Showing
27 changed files
with
873 additions
and
972 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, {}); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/** | ||
* Ssdp Options. | ||
*/ | ||
export type SsdpOptions = { | ||
sourcePort?: number; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.