nat-upnp-rejetto 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/modules.xml +8 -0
- package/.idea/nat-upnp-ts.iml +12 -0
- package/.idea/vcs.xml +11 -0
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/build/src/index.d.ts +27 -0
- package/build/src/index.js +19 -0
- package/build/src/nat-upnp-ts/client.d.ts +93 -0
- package/build/src/nat-upnp-ts/client.js +164 -0
- package/build/src/nat-upnp-ts/device.d.ts +77 -0
- package/build/src/nat-upnp-ts/device.js +120 -0
- package/build/src/nat-upnp-ts/ssdp.d.ts +49 -0
- package/build/src/nat-upnp-ts/ssdp.js +127 -0
- package/package.json +24 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/nat-upnp-ts.iml" filepath="$PROJECT_DIR$/.idea/nat-upnp-ts.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
8
|
+
</content>
|
|
9
|
+
<orderEntry type="inheritedJdk" />
|
|
10
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
package/.idea/vcs.xml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="GitSharedSettings">
|
|
4
|
+
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
|
|
5
|
+
<list />
|
|
6
|
+
</option>
|
|
7
|
+
</component>
|
|
8
|
+
<component name="VcsDirectoryMappings">
|
|
9
|
+
<mapping directory="" vcs="Git" />
|
|
10
|
+
</component>
|
|
11
|
+
</project>
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Kaden Sharpin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# UPnP TS
|
|
2
|
+
|
|
3
|
+
Port mapping via UPnP APIs
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i nat-upnp-ts
|
|
9
|
+
OR
|
|
10
|
+
npm i git+https://github.com/kaden-sharpin/nat-upnp-ts.git
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// using ES modules
|
|
17
|
+
import { Client } from "nat-upnp-ts";
|
|
18
|
+
const client = new Client();
|
|
19
|
+
|
|
20
|
+
// using node require
|
|
21
|
+
const upnp = require("nat-upnp-ts");
|
|
22
|
+
const client = new upnp.Client();
|
|
23
|
+
|
|
24
|
+
client
|
|
25
|
+
.createMapping({
|
|
26
|
+
public: 12345,
|
|
27
|
+
private: 54321,
|
|
28
|
+
ttl: 10,
|
|
29
|
+
})
|
|
30
|
+
.then(() => {
|
|
31
|
+
// Will be called once finished
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {
|
|
34
|
+
// Will be called on error
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async () => {
|
|
38
|
+
await client.removeMapping({
|
|
39
|
+
public: 12345,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
client.getMappings();
|
|
44
|
+
|
|
45
|
+
client.getMappings({
|
|
46
|
+
local: true,
|
|
47
|
+
description: "both of these fields are optional",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
client.getPublicIp();
|
|
51
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Device as impDevice } from "./nat-upnp-ts/device";
|
|
2
|
+
import { Client as impClient } from "./nat-upnp-ts/client";
|
|
3
|
+
import { Ssdp as impSsdp } from "./nat-upnp-ts/ssdp";
|
|
4
|
+
declare namespace natupnp {
|
|
5
|
+
const Ssdp: typeof impSsdp;
|
|
6
|
+
const Device: typeof impDevice;
|
|
7
|
+
const Client: typeof impClient;
|
|
8
|
+
}
|
|
9
|
+
export { Device } from "./nat-upnp-ts/device";
|
|
10
|
+
export type { Service, RawService, RawDevice } from "./nat-upnp-ts/device";
|
|
11
|
+
export { Ssdp } from "./nat-upnp-ts/ssdp";
|
|
12
|
+
export type { SearchCallback, ISsdp, SsdpEmitter } from "./nat-upnp-ts/ssdp";
|
|
13
|
+
export { Client } from "./nat-upnp-ts/client";
|
|
14
|
+
export type { GetMappingOpts, Mapping, DeletePortMappingOpts, NewPortMappingOpts, StandardOpts, } from "./nat-upnp-ts/client";
|
|
15
|
+
export default natupnp;
|
|
16
|
+
/**
|
|
17
|
+
* Raw SSDP/UPNP repsonse
|
|
18
|
+
* Entire SSDP/UPNP schema is beyond the scope of these typings.
|
|
19
|
+
* Please look up the protol documentation if you wanna do
|
|
20
|
+
* lower level communication.
|
|
21
|
+
*/
|
|
22
|
+
export type RawResponse = Partial<Record<string, {
|
|
23
|
+
"@": {
|
|
24
|
+
"xmlns:u": string;
|
|
25
|
+
};
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Client = exports.Ssdp = exports.Device = void 0;
|
|
4
|
+
const device_1 = require("./nat-upnp-ts/device");
|
|
5
|
+
const client_1 = require("./nat-upnp-ts/client");
|
|
6
|
+
const ssdp_1 = require("./nat-upnp-ts/ssdp");
|
|
7
|
+
var natupnp;
|
|
8
|
+
(function (natupnp) {
|
|
9
|
+
natupnp.Ssdp = ssdp_1.Ssdp;
|
|
10
|
+
natupnp.Device = device_1.Device;
|
|
11
|
+
natupnp.Client = client_1.Client;
|
|
12
|
+
})(natupnp || (natupnp = {}));
|
|
13
|
+
var device_2 = require("./nat-upnp-ts/device");
|
|
14
|
+
Object.defineProperty(exports, "Device", { enumerable: true, get: function () { return device_2.Device; } });
|
|
15
|
+
var ssdp_2 = require("./nat-upnp-ts/ssdp");
|
|
16
|
+
Object.defineProperty(exports, "Ssdp", { enumerable: true, get: function () { return ssdp_2.Ssdp; } });
|
|
17
|
+
var client_2 = require("./nat-upnp-ts/client");
|
|
18
|
+
Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_2.Client; } });
|
|
19
|
+
exports.default = natupnp;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { RawResponse } from "../index";
|
|
2
|
+
import Device from "./device";
|
|
3
|
+
import Ssdp from "./ssdp";
|
|
4
|
+
export declare class Client implements IClient {
|
|
5
|
+
readonly timeout: number;
|
|
6
|
+
readonly ssdp: Ssdp;
|
|
7
|
+
constructor(options?: {
|
|
8
|
+
timeout?: number;
|
|
9
|
+
});
|
|
10
|
+
createMapping(options: NewPortMappingOpts): Promise<RawResponse>;
|
|
11
|
+
removeMapping(options: DeletePortMappingOpts): Promise<RawResponse>;
|
|
12
|
+
getMappings(options?: GetMappingOpts): Promise<Mapping[]>;
|
|
13
|
+
getPublicIp(): Promise<string>;
|
|
14
|
+
getGateway(): Promise<{
|
|
15
|
+
gateway: Device;
|
|
16
|
+
address: string;
|
|
17
|
+
}>;
|
|
18
|
+
close(): void;
|
|
19
|
+
}
|
|
20
|
+
export default Client;
|
|
21
|
+
export interface Mapping {
|
|
22
|
+
public: {
|
|
23
|
+
host: string;
|
|
24
|
+
port: number;
|
|
25
|
+
};
|
|
26
|
+
private: {
|
|
27
|
+
host: string;
|
|
28
|
+
port: number;
|
|
29
|
+
};
|
|
30
|
+
protocol: string;
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
description: string;
|
|
33
|
+
ttl: number;
|
|
34
|
+
local: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Standard options that many options use.
|
|
38
|
+
*/
|
|
39
|
+
export interface StandardOpts {
|
|
40
|
+
public?: number | {
|
|
41
|
+
port?: number;
|
|
42
|
+
host?: string;
|
|
43
|
+
};
|
|
44
|
+
private?: number | {
|
|
45
|
+
port?: number;
|
|
46
|
+
host?: string;
|
|
47
|
+
};
|
|
48
|
+
protocol?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface NewPortMappingOpts extends StandardOpts {
|
|
51
|
+
description?: string;
|
|
52
|
+
ttl?: number;
|
|
53
|
+
}
|
|
54
|
+
export type DeletePortMappingOpts = StandardOpts;
|
|
55
|
+
export interface GetMappingOpts {
|
|
56
|
+
local?: boolean;
|
|
57
|
+
description?: RegExp | string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Main client interface.
|
|
61
|
+
*/
|
|
62
|
+
export interface IClient {
|
|
63
|
+
/**
|
|
64
|
+
* Create a new port mapping
|
|
65
|
+
* @param options Options for the new port mapping
|
|
66
|
+
*/
|
|
67
|
+
createMapping(options: NewPortMappingOpts): Promise<RawResponse>;
|
|
68
|
+
/**
|
|
69
|
+
* Remove a port mapping
|
|
70
|
+
* @param options Specify which port mapping to remove
|
|
71
|
+
*/
|
|
72
|
+
removeMapping(options: DeletePortMappingOpts): Promise<RawResponse>;
|
|
73
|
+
/**
|
|
74
|
+
* Get a list of existing mappings
|
|
75
|
+
* @param options Filter mappings based on these options
|
|
76
|
+
*/
|
|
77
|
+
getMappings(options?: GetMappingOpts): Promise<Mapping[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Fetch the external/public IP from the gateway
|
|
80
|
+
*/
|
|
81
|
+
getPublicIp(): Promise<string>;
|
|
82
|
+
/**
|
|
83
|
+
* Get the gateway device for communication
|
|
84
|
+
*/
|
|
85
|
+
getGateway(): Promise<{
|
|
86
|
+
gateway: Device;
|
|
87
|
+
address: string;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Close the underlaying sockets and resources
|
|
91
|
+
*/
|
|
92
|
+
close(): void;
|
|
93
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.Client = void 0;
|
|
16
|
+
const device_1 = __importDefault(require("./device"));
|
|
17
|
+
const ssdp_1 = __importDefault(require("./ssdp"));
|
|
18
|
+
class Client {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.ssdp = new ssdp_1.default();
|
|
21
|
+
this.timeout = options.timeout || 1800;
|
|
22
|
+
}
|
|
23
|
+
createMapping(options) {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
return this.getGateway().then(({ gateway, address }) => {
|
|
26
|
+
var _a;
|
|
27
|
+
const ports = normalizeOptions(options);
|
|
28
|
+
return gateway.run("AddPortMapping", [
|
|
29
|
+
["NewRemoteHost", ports.remote.host + ""],
|
|
30
|
+
["NewExternalPort", ports.remote.port + ""],
|
|
31
|
+
[
|
|
32
|
+
"NewProtocol",
|
|
33
|
+
options.protocol ? options.protocol.toUpperCase() : "TCP",
|
|
34
|
+
],
|
|
35
|
+
["NewInternalPort", ports.internal.port + ""],
|
|
36
|
+
["NewInternalClient", ports.internal.host || address],
|
|
37
|
+
["NewEnabled", 1],
|
|
38
|
+
["NewPortMappingDescription", options.description || "node:nat:upnp"],
|
|
39
|
+
["NewLeaseDuration", (_a = options.ttl) !== null && _a !== void 0 ? _a : 60 * 30],
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
removeMapping(options) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
return this.getGateway().then(({ gateway }) => {
|
|
47
|
+
const ports = normalizeOptions(options);
|
|
48
|
+
return gateway.run("DeletePortMapping", [
|
|
49
|
+
["NewRemoteHost", ports.remote.host + ""],
|
|
50
|
+
["NewExternalPort", ports.remote.port + ""],
|
|
51
|
+
[
|
|
52
|
+
"NewProtocol",
|
|
53
|
+
options.protocol ? options.protocol.toUpperCase() : "TCP",
|
|
54
|
+
],
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
getMappings(options = {}) {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
const { gateway, address } = yield this.getGateway();
|
|
62
|
+
let i = 0;
|
|
63
|
+
const results = [];
|
|
64
|
+
while (true) {
|
|
65
|
+
const data = yield gateway.run("GetGenericPortMappingEntry", [["NewPortMappingIndex", i++]])
|
|
66
|
+
.catch(() => { });
|
|
67
|
+
if (!data)
|
|
68
|
+
break; // finished
|
|
69
|
+
const key = Object.keys(data).find((k) => k.startsWith('GetGenericPortMappingEntryResponse'));
|
|
70
|
+
if (!key) {
|
|
71
|
+
throw new Error("Incorrect response");
|
|
72
|
+
}
|
|
73
|
+
const res = data[key];
|
|
74
|
+
const result = {
|
|
75
|
+
public: {
|
|
76
|
+
host: res.NewRemoteHost || "",
|
|
77
|
+
port: Number(res.NewExternalPort),
|
|
78
|
+
},
|
|
79
|
+
private: {
|
|
80
|
+
host: res.NewInternalClient,
|
|
81
|
+
port: Number(res.NewInternalPort),
|
|
82
|
+
},
|
|
83
|
+
protocol: res.NewProtocol.toLowerCase(),
|
|
84
|
+
enabled: res.NewEnabled == 1,
|
|
85
|
+
description: res.NewPortMappingDescription,
|
|
86
|
+
ttl: Number(res.NewLeaseDuration),
|
|
87
|
+
// temporary, so typescript will compile
|
|
88
|
+
local: false,
|
|
89
|
+
};
|
|
90
|
+
result.local = result.private.host === address;
|
|
91
|
+
if (options.local && !result.local) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (options.description) {
|
|
95
|
+
if (typeof result.description !== "string")
|
|
96
|
+
continue;
|
|
97
|
+
if (options.description instanceof RegExp) {
|
|
98
|
+
if (!options.description.test(result.description))
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (result.description.indexOf(options.description) === -1)
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
results.push(result);
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
getPublicIp() {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
return this.getGateway().then(({ gateway, address }) => __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
var _a;
|
|
115
|
+
const data = yield gateway.run("GetExternalIPAddress", []);
|
|
116
|
+
const key = Object.keys(data || {}).find((k) => /^GetExternalIPAddressResponse$/.test(k));
|
|
117
|
+
if (!key)
|
|
118
|
+
throw new Error("Incorrect response");
|
|
119
|
+
return ((_a = data[key]) === null || _a === void 0 ? void 0 : _a.NewExternalIPAddress) + "";
|
|
120
|
+
}));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
getGateway() {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
let timeouted = false;
|
|
126
|
+
const p = this.ssdp.search("urn:schemas-upnp-org:device:InternetGatewayDevice:1");
|
|
127
|
+
return new Promise((s, r) => {
|
|
128
|
+
const timeout = setTimeout(() => {
|
|
129
|
+
timeouted = true;
|
|
130
|
+
p.emit("end");
|
|
131
|
+
r(new Error("Connection timed out while searching for the gateway."));
|
|
132
|
+
}, this.timeout);
|
|
133
|
+
p.on("device", (info, address) => {
|
|
134
|
+
if (timeouted)
|
|
135
|
+
return;
|
|
136
|
+
p.emit("end");
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
// Create gateway
|
|
139
|
+
s({ gateway: new device_1.default(info.location), address });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
close() {
|
|
145
|
+
this.ssdp.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
exports.Client = Client;
|
|
149
|
+
function normalizeOptions(options) {
|
|
150
|
+
function toObject(addr) {
|
|
151
|
+
if (typeof addr === "number")
|
|
152
|
+
return { port: addr };
|
|
153
|
+
if (typeof addr === "string" && !isNaN(addr))
|
|
154
|
+
return { port: Number(addr) };
|
|
155
|
+
if (typeof addr === "object")
|
|
156
|
+
return addr;
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
remote: toObject(options.public),
|
|
161
|
+
internal: toObject(options.private),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
exports.default = Client;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { RawResponse } from "../index";
|
|
2
|
+
export declare class Device implements IDevice {
|
|
3
|
+
readonly description: string;
|
|
4
|
+
readonly services: string[];
|
|
5
|
+
constructor(url: string);
|
|
6
|
+
private getXML;
|
|
7
|
+
getService(types: string[]): Promise<{
|
|
8
|
+
service: string;
|
|
9
|
+
SCPDURL: string;
|
|
10
|
+
controlURL: string;
|
|
11
|
+
}>;
|
|
12
|
+
run(action: string, args: (string | number)[][]): Promise<RawResponse>;
|
|
13
|
+
parseDescription(info: {
|
|
14
|
+
device?: RawDevice;
|
|
15
|
+
}): {
|
|
16
|
+
services: RawService[];
|
|
17
|
+
devices: RawDevice[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export default Device;
|
|
21
|
+
export interface Service {
|
|
22
|
+
service: string;
|
|
23
|
+
SCPDURL: string;
|
|
24
|
+
controlURL: string;
|
|
25
|
+
}
|
|
26
|
+
export interface RawService {
|
|
27
|
+
serviceType: string;
|
|
28
|
+
serviceId: string;
|
|
29
|
+
controlURL?: string;
|
|
30
|
+
eventSubURL?: string;
|
|
31
|
+
SCPDURL?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface RawDevice {
|
|
34
|
+
deviceType: string;
|
|
35
|
+
presentationURL: string;
|
|
36
|
+
friendlyName: string;
|
|
37
|
+
manufacturer: string;
|
|
38
|
+
manufacturerURL: string;
|
|
39
|
+
modelDescription: string;
|
|
40
|
+
modelName: string;
|
|
41
|
+
modelNumber: string;
|
|
42
|
+
modelURL: string;
|
|
43
|
+
serialNumber: string;
|
|
44
|
+
UDN: string;
|
|
45
|
+
UPC: string;
|
|
46
|
+
serviceList?: {
|
|
47
|
+
service: RawService | RawService[];
|
|
48
|
+
};
|
|
49
|
+
deviceList?: {
|
|
50
|
+
device: RawDevice | RawDevice[];
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export interface IDevice {
|
|
54
|
+
/**
|
|
55
|
+
* Get the available services on the network device
|
|
56
|
+
* @param types List of service types to look for
|
|
57
|
+
*/
|
|
58
|
+
getService(types: string[]): Promise<Service>;
|
|
59
|
+
/**
|
|
60
|
+
* Parse out available services
|
|
61
|
+
* and devices from a root device
|
|
62
|
+
* @param info
|
|
63
|
+
* @returns the available devices and services in array form
|
|
64
|
+
*/
|
|
65
|
+
parseDescription(info: {
|
|
66
|
+
device?: RawDevice;
|
|
67
|
+
}): {
|
|
68
|
+
services: RawService[];
|
|
69
|
+
devices: RawDevice[];
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Perform a SSDP/UPNP request
|
|
73
|
+
* @param action the action to perform
|
|
74
|
+
* @param kvpairs arguments of said action
|
|
75
|
+
*/
|
|
76
|
+
run(action: string, kvpairs: (string | number)[][]): Promise<RawResponse>;
|
|
77
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.Device = void 0;
|
|
16
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
17
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
18
|
+
const consumers_1 = require("node:stream/consumers");
|
|
19
|
+
const url_1 = require("url");
|
|
20
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
21
|
+
class Device {
|
|
22
|
+
constructor(url) {
|
|
23
|
+
this.description = url;
|
|
24
|
+
this.services = [
|
|
25
|
+
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
|
26
|
+
"urn:schemas-upnp-org:service:WANIPConnection:2",
|
|
27
|
+
"urn:schemas-upnp-org:service:WANPPPConnection:1",
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
getXML(url) {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
return httpRequest(url).then(consumers_1.text).then(data => new fast_xml_parser_1.XMLParser().parse(data))
|
|
33
|
+
.catch(() => new Error("Failed to lookup device description"));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
getService(types) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
return this.getXML(this.description).then(({ root: xml }) => {
|
|
39
|
+
const services = this.parseDescription(xml).services.filter(({ serviceType }) => types.includes(serviceType));
|
|
40
|
+
if (services.length === 0 ||
|
|
41
|
+
!services[0].controlURL ||
|
|
42
|
+
!services[0].SCPDURL) {
|
|
43
|
+
throw new Error("Service not found");
|
|
44
|
+
}
|
|
45
|
+
const baseUrl = new url_1.URL(xml.baseURL, this.description);
|
|
46
|
+
const prefix = (url) => new url_1.URL(url, baseUrl.toString()).toString();
|
|
47
|
+
return {
|
|
48
|
+
service: services[0].serviceType,
|
|
49
|
+
SCPDURL: prefix(services[0].SCPDURL),
|
|
50
|
+
controlURL: prefix(services[0].controlURL),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
run(action, args) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
const info = yield this.getService(this.services);
|
|
58
|
+
const body = '<?xml version="1.0"?>' +
|
|
59
|
+
"<s:Envelope " +
|
|
60
|
+
'xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" ' +
|
|
61
|
+
's:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
|
|
62
|
+
"<s:Body>" +
|
|
63
|
+
"<u:" +
|
|
64
|
+
action +
|
|
65
|
+
" xmlns:u=" +
|
|
66
|
+
JSON.stringify(info.service) +
|
|
67
|
+
">" +
|
|
68
|
+
args.reduce((p, [a, b]) => p + `<${a !== null && a !== void 0 ? a : ""}>${b !== null && b !== void 0 ? b : ""}</${a !== null && a !== void 0 ? a : ""}>`, "") +
|
|
69
|
+
"</u:" +
|
|
70
|
+
action +
|
|
71
|
+
">" +
|
|
72
|
+
"</s:Body>" +
|
|
73
|
+
"</s:Envelope>";
|
|
74
|
+
return httpRequest(info.controlURL, {
|
|
75
|
+
method: 'post',
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": 'text/xml; charset="utf-8"',
|
|
78
|
+
"Content-Length": "" + Buffer.byteLength(body),
|
|
79
|
+
Connection: "close",
|
|
80
|
+
SOAPAction: JSON.stringify(info.service + "#" + action),
|
|
81
|
+
},
|
|
82
|
+
}, body)
|
|
83
|
+
.then(consumers_1.text).then(data => new fast_xml_parser_1.XMLParser({ removeNSPrefix: true }).parse(data).Envelope.Body);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
parseDescription(info) {
|
|
87
|
+
const services = [];
|
|
88
|
+
const devices = [];
|
|
89
|
+
function traverseDevices(device) {
|
|
90
|
+
var _a, _b, _c, _d;
|
|
91
|
+
if (!device)
|
|
92
|
+
return;
|
|
93
|
+
const serviceList = (_b = (_a = device.serviceList) === null || _a === void 0 ? void 0 : _a.service) !== null && _b !== void 0 ? _b : [];
|
|
94
|
+
const deviceList = (_d = (_c = device.deviceList) === null || _c === void 0 ? void 0 : _c.device) !== null && _d !== void 0 ? _d : [];
|
|
95
|
+
devices.push(device);
|
|
96
|
+
if (Array.isArray(serviceList)) {
|
|
97
|
+
services.push(...serviceList);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
services.push(serviceList);
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(deviceList)) {
|
|
103
|
+
deviceList.forEach(traverseDevices);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
traverseDevices(deviceList);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
traverseDevices(info.device);
|
|
110
|
+
return {
|
|
111
|
+
services,
|
|
112
|
+
devices,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.Device = Device;
|
|
117
|
+
exports.default = Device;
|
|
118
|
+
function httpRequest(url, options = {}, body = '') {
|
|
119
|
+
return new Promise((resolve, reject) => (url.startsWith('https:') ? node_https_1.default : node_http_1.default).request(url, options, (res) => __awaiter(this, void 0, void 0, function* () { return !res.statusCode || res.statusCode >= 400 ? reject(res) : resolve(res); })).on('error', reject).end(body));
|
|
120
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import EventEmitter from "events";
|
|
3
|
+
export declare class Ssdp implements ISsdp {
|
|
4
|
+
private options?;
|
|
5
|
+
private sourcePort;
|
|
6
|
+
private bound;
|
|
7
|
+
private boundCount;
|
|
8
|
+
private closed;
|
|
9
|
+
private readonly queue;
|
|
10
|
+
private readonly multicast;
|
|
11
|
+
private readonly port;
|
|
12
|
+
private readonly sockets;
|
|
13
|
+
private readonly ssdpEmitter;
|
|
14
|
+
constructor(options?: {
|
|
15
|
+
sourcePort?: number | undefined;
|
|
16
|
+
} | undefined);
|
|
17
|
+
private createSocket;
|
|
18
|
+
private parseResponse;
|
|
19
|
+
search(device: string, emitter?: SsdpEmitter): SsdpEmitter;
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
22
|
+
export default Ssdp;
|
|
23
|
+
type SearchArgs = [Record<string, string>, string];
|
|
24
|
+
export type SearchCallback = (...args: SearchArgs) => void;
|
|
25
|
+
type SearchEvent = <E extends Events>(ev: E, ...args: E extends "device" ? SearchArgs : []) => boolean;
|
|
26
|
+
type Events = "device" | "end";
|
|
27
|
+
type Event<E extends Events> = E extends "device" ? SearchCallback : () => void;
|
|
28
|
+
type EventListener<T> = <E extends Events>(ev: E, callback: Event<E>) => T;
|
|
29
|
+
export interface SsdpEmitter extends EventEmitter {
|
|
30
|
+
removeListener: EventListener<this>;
|
|
31
|
+
addListener: EventListener<this>;
|
|
32
|
+
once: EventListener<this>;
|
|
33
|
+
on: EventListener<this>;
|
|
34
|
+
emit: SearchEvent;
|
|
35
|
+
_ended?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface ISsdp {
|
|
38
|
+
/**
|
|
39
|
+
* Search for a SSDP compatible server on the network
|
|
40
|
+
* @param device Search Type (ST) header, specifying which device to search for
|
|
41
|
+
* @param emitter An existing EventEmitter to emit event on
|
|
42
|
+
* @returns The event emitter provided in Promise, or a newly instantiated one.
|
|
43
|
+
*/
|
|
44
|
+
search(device: string, emitter?: SsdpEmitter): SsdpEmitter;
|
|
45
|
+
/**
|
|
46
|
+
* Close all sockets
|
|
47
|
+
*/
|
|
48
|
+
close(): void;
|
|
49
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Ssdp = void 0;
|
|
7
|
+
const dgram_1 = __importDefault(require("dgram"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const events_1 = __importDefault(require("events"));
|
|
10
|
+
class Ssdp {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
var _a;
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.sourcePort = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.sourcePort) || 0;
|
|
15
|
+
this.bound = false;
|
|
16
|
+
this.boundCount = 0;
|
|
17
|
+
this.closed = false;
|
|
18
|
+
this.queue = [];
|
|
19
|
+
this.multicast = "239.255.255.250";
|
|
20
|
+
this.port = 1900;
|
|
21
|
+
this.ssdpEmitter = new events_1.default();
|
|
22
|
+
// Create sockets on all external interfaces
|
|
23
|
+
const interfaces = os_1.default.networkInterfaces();
|
|
24
|
+
this.sockets = Object.keys(interfaces).reduce((arr, key) => {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
return arr.concat((_b = (_a = interfaces[key]) === null || _a === void 0 ? void 0 : _a.filter((item) => !item.internal).map((item) => this.createSocket(item))) !== null && _b !== void 0 ? _b : []);
|
|
27
|
+
}, []);
|
|
28
|
+
}
|
|
29
|
+
createSocket(iface) {
|
|
30
|
+
const socket = dgram_1.default.createSocket(iface.family === "IPv4" ? "udp4" : "udp6");
|
|
31
|
+
socket.on("message", (message) => {
|
|
32
|
+
// Ignore messages after closing sockets
|
|
33
|
+
if (this.closed)
|
|
34
|
+
return;
|
|
35
|
+
// Parse response
|
|
36
|
+
this.parseResponse(message.toString(), socket.address);
|
|
37
|
+
});
|
|
38
|
+
// Bind in next tick (sockets should be me in this.sockets array)
|
|
39
|
+
process.nextTick(() => {
|
|
40
|
+
// Unqueue this._queue once all sockets are ready
|
|
41
|
+
const onready = () => {
|
|
42
|
+
if (this.boundCount < this.sockets.length)
|
|
43
|
+
return;
|
|
44
|
+
this.bound = true;
|
|
45
|
+
this.queue.forEach(([device, emitter]) => this.search(device, emitter));
|
|
46
|
+
};
|
|
47
|
+
socket.on("listening", () => {
|
|
48
|
+
this.boundCount += 1;
|
|
49
|
+
onready();
|
|
50
|
+
});
|
|
51
|
+
// On error - remove socket from list and execute items from queue
|
|
52
|
+
socket.once("error", () => {
|
|
53
|
+
socket.close();
|
|
54
|
+
this.sockets.splice(this.sockets.indexOf(socket), 1);
|
|
55
|
+
onready();
|
|
56
|
+
});
|
|
57
|
+
socket.address = iface.address;
|
|
58
|
+
socket.bind(this.sourcePort, iface.address);
|
|
59
|
+
});
|
|
60
|
+
return socket;
|
|
61
|
+
}
|
|
62
|
+
parseResponse(response, addr) {
|
|
63
|
+
// Ignore incorrect packets
|
|
64
|
+
if (!/^(HTTP|NOTIFY)/m.test(response))
|
|
65
|
+
return;
|
|
66
|
+
const headers = parseMimeHeader(response);
|
|
67
|
+
// We are only interested in messages that can be matched against the original
|
|
68
|
+
// search target
|
|
69
|
+
if (!headers.st)
|
|
70
|
+
return;
|
|
71
|
+
this.ssdpEmitter.emit("device", headers, addr);
|
|
72
|
+
}
|
|
73
|
+
search(device, emitter) {
|
|
74
|
+
if (!emitter) {
|
|
75
|
+
emitter = new events_1.default();
|
|
76
|
+
emitter._ended = false;
|
|
77
|
+
emitter.once("end", () => {
|
|
78
|
+
emitter._ended = true;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!this.bound) {
|
|
82
|
+
this.queue.push([device, emitter]);
|
|
83
|
+
return emitter;
|
|
84
|
+
}
|
|
85
|
+
const query = Buffer.from("M-SEARCH * HTTP/1.1\r\n" +
|
|
86
|
+
"HOST: " +
|
|
87
|
+
this.multicast +
|
|
88
|
+
":" +
|
|
89
|
+
this.port +
|
|
90
|
+
"\r\n" +
|
|
91
|
+
'MAN: "ssdp:discover"\r\n' +
|
|
92
|
+
"MX: 1\r\n" +
|
|
93
|
+
"ST: " +
|
|
94
|
+
device +
|
|
95
|
+
"\r\n" +
|
|
96
|
+
"\r\n");
|
|
97
|
+
// Send query on each socket
|
|
98
|
+
this.sockets.forEach((socket) => socket.send(query, 0, query.length, this.port, this.multicast));
|
|
99
|
+
const ondevice = (headers, address) => {
|
|
100
|
+
if (!emitter || emitter._ended || headers.st !== device)
|
|
101
|
+
return;
|
|
102
|
+
emitter.emit("device", headers, address);
|
|
103
|
+
};
|
|
104
|
+
this.ssdpEmitter.on("device", ondevice);
|
|
105
|
+
// Detach listener after receiving 'end' event
|
|
106
|
+
emitter.once("end", () => this.ssdpEmitter.removeListener("device", ondevice));
|
|
107
|
+
return emitter;
|
|
108
|
+
}
|
|
109
|
+
close() {
|
|
110
|
+
this.sockets.forEach((socket) => socket.close());
|
|
111
|
+
this.closed = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.Ssdp = Ssdp;
|
|
115
|
+
function parseMimeHeader(headerStr) {
|
|
116
|
+
const lines = headerStr.split(/\r\n/g);
|
|
117
|
+
// Parse headers from lines to hashmap
|
|
118
|
+
return lines.reduce((headers, line) => {
|
|
119
|
+
var _a;
|
|
120
|
+
const [_, key, value] = (_a = line.match(/^([^:]*)\s*:\s*(.*)$/)) !== null && _a !== void 0 ? _a : [];
|
|
121
|
+
if (key && value) {
|
|
122
|
+
headers[key.toLowerCase()] = value;
|
|
123
|
+
}
|
|
124
|
+
return headers;
|
|
125
|
+
}, {});
|
|
126
|
+
}
|
|
127
|
+
exports.default = Ssdp;
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nat-upnp-rejetto",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"main": "build/src/index",
|
|
5
|
+
"author": "Fedor Indutny <fedor@indutny.com>, SimplyLinn <https://github.com/SimplyLinn>, Kaden Sharpin <http://github.com/kaden-sharpin>, Massimo Melina <a@rejetto.com>",
|
|
6
|
+
"homepage": "https://github.com/kaden-sharpin/nat-upnp-ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "http://github.com/rejetto/nat-upnp-ts.git"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^17.0.6",
|
|
14
|
+
"typescript": "^4.5.4"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"watch": "tsc --watch",
|
|
19
|
+
"test": "node ./build/test/index.test.js"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"fast-xml-parser": "^4.0.0-beta.8"
|
|
23
|
+
}
|
|
24
|
+
}
|