Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete project refactor to typescript #44

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
node_modules/
npm-debug.log
node_modules/
51 changes: 34 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,50 @@

Port mapping via UPnP APIs

## Installation

```bash
npm i git+https://github.com/kaden-sharpin/node-nat-upnp.git
```

## Usage

```javascript
var natUpnp = require('nat-upnp');
// using ES modules
import { Client } from "nat-upnp";
const client = new Client();

var client = natUpnp.createClient();
// using node require
const natUpnp = require("nat-upnp");
const client = new natUpnp.Client();

client.portMapping({
public: 12345,
private: 54321,
ttl: 10
}, function(err) {
// Will be called once finished
});
client
.createMapping({
public: 12345,
private: 54321,
ttl: 10,
})
.then(() => {
// Will be called once finished
})
.catch(() => {
// Will be called on error
});

client.portUnmapping({
public: 12345
});
async () => {
await client.removeMapping({
public: 12345,
});
};

client.getMappings(function(err, results) {
});
client.getMappings();

client.getMappings({ local: true }, function(err, results) {
client.getMappings({
local: true,
description: "both of these fields are optional",
});

client.externalIp(function(err, ip) {
});
client.getPublicIp();
```

### License
Expand Down
27 changes: 27 additions & 0 deletions build/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Device as impDevice } from "./nat-upnp/device";
import { Client as impClient } from "./nat-upnp/client";
import { Ssdp as impSsdp } from "./nat-upnp/ssdp";
declare namespace natupnp {
const Ssdp: typeof impSsdp;
const Device: typeof impDevice;
const Client: typeof impClient;
}
export { Device } from "./nat-upnp/device";
export type { Service, RawService, RawDevice } from "./nat-upnp/device";
export { Ssdp } from "./nat-upnp/ssdp";
export type { SearchCallback, ISsdp, SsdpEmitter } from "./nat-upnp/ssdp";
export { Client } from "./nat-upnp/client";
export type { GetMappingOpts, Mapping, DeletePortMappingOpts, NewPortMappingOpts, StandardOpts, } from "./nat-upnp/client";
export default natupnp;
/**
* Raw SSDP/UPNP repsonse
* Entire SSDP/UPNP schema is beyond the scope of these typings.
* Please look up the protol documentation if you wanna do
* lower level communication.
*/
export declare type RawResponse = Partial<Record<string, {
"@": {
"xmlns:u": string;
};
[key: string]: unknown;
}>>;
19 changes: 19 additions & 0 deletions build/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = exports.Ssdp = exports.Device = void 0;
const device_1 = require("./nat-upnp/device");
const client_1 = require("./nat-upnp/client");
const ssdp_1 = require("./nat-upnp/ssdp");
var natupnp;
(function (natupnp) {
natupnp.Ssdp = ssdp_1.Ssdp;
natupnp.Device = device_1.Device;
natupnp.Client = client_1.Client;
})(natupnp || (natupnp = {}));
var device_2 = require("./nat-upnp/device");
Object.defineProperty(exports, "Device", { enumerable: true, get: function () { return device_2.Device; } });
var ssdp_2 = require("./nat-upnp/ssdp");
Object.defineProperty(exports, "Ssdp", { enumerable: true, get: function () { return ssdp_2.Ssdp; } });
var client_2 = require("./nat-upnp/client");
Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_2.Client; } });
exports.default = natupnp;
93 changes: 93 additions & 0 deletions build/src/nat-upnp/client.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { RawResponse } from "../index";
import Device from "./device";
import Ssdp from "./ssdp";
export declare class Client implements IClient {
readonly timeout: number;
readonly ssdp: Ssdp;
constructor(options?: {
timeout?: number;
});
createMapping(options: NewPortMappingOpts): Promise<RawResponse>;
removeMapping(options: DeletePortMappingOpts): Promise<RawResponse>;
getMappings(options?: GetMappingOpts): Promise<Mapping[]>;
getPublicIp(): Promise<string>;
getGateway(): Promise<{
gateway: Device;
address: string;
}>;
close(): void;
}
export default Client;
export interface Mapping {
public: {
host: string;
port: number;
};
private: {
host: string;
port: number;
};
protocol: string;
enabled: boolean;
description: string;
ttl: number;
local: boolean;
}
/**
* Standard options that many options use.
*/
export interface StandardOpts {
public?: number | {
port?: number;
host?: string;
};
private?: number | {
port?: number;
host?: string;
};
protocol?: string;
}
export interface NewPortMappingOpts extends StandardOpts {
description?: string;
ttl?: number;
}
export declare type DeletePortMappingOpts = StandardOpts;
export interface GetMappingOpts {
local?: boolean;
description?: RegExp | string;
}
/**
* Main client interface.
*/
export interface IClient {
/**
* Create a new port mapping
* @param options Options for the new port mapping
*/
createMapping(options: NewPortMappingOpts): Promise<RawResponse>;
/**
* Remove a port mapping
* @param options Specify which port mapping to remove
*/
removeMapping(options: DeletePortMappingOpts): Promise<RawResponse>;
/**
* Get a list of existing mappings
* @param options Filter mappings based on these options
*/
getMappings(options?: GetMappingOpts): Promise<Mapping[]>;
/**
* Fetch the external/public IP from the gateway
*/
getPublicIp(): Promise<string>;
/**
* Get the gateway device for communication
*/
getGateway(): Promise<{
gateway: Device;
address: string;
}>;
/**
* Close the underlaying sockets and resources
*/
close(): void;
}
170 changes: 170 additions & 0 deletions build/src/nat-upnp/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const device_1 = __importDefault(require("./device"));
const ssdp_1 = __importDefault(require("./ssdp"));
class Client {
constructor(options = {}) {
this.ssdp = new ssdp_1.default();
this.timeout = options.timeout || 1800;
}
createMapping(options) {
return __awaiter(this, void 0, void 0, function* () {
return this.getGateway().then(({ gateway, address }) => {
var _a;
const ports = normalizeOptions(options);
return gateway.run("AddPortMapping", [
["NewRemoteHost", ports.remote.host + ""],
["NewExternalPort", ports.remote.port + ""],
[
"NewProtocol",
options.protocol ? options.protocol.toUpperCase() : "TCP",
],
["NewInternalPort", ports.internal.port + ""],
["NewInternalClient", ports.internal.host || address],
["NewEnabled", 1],
["NewPortMappingDescription", options.description || "node:nat:upnp"],
["NewLeaseDuration", (_a = options.ttl) !== null && _a !== void 0 ? _a : 60 * 30],
]);
});
});
}
removeMapping(options) {
return __awaiter(this, void 0, void 0, function* () {
return this.getGateway().then(({ gateway }) => {
const ports = normalizeOptions(options);
return gateway.run("DeletePortMapping", [
["NewRemoteHost", ports.remote.host + ""],
["NewExternalPort", ports.remote.port + ""],
[
"NewProtocol",
options.protocol ? options.protocol.toUpperCase() : "TCP",
],
]);
});
});
}
getMappings(options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const { gateway, address } = yield this.getGateway();
let i = 0;
let end = false;
const results = [];
while (true) {
const data = (yield gateway
.run("GetGenericPortMappingEntry", [["NewPortMappingIndex", i++]])
.catch((err) => {
if (i !== 1) {
end = true;
}
}));
if (end)
break;
const key = Object.keys(data || {}).find((k) => /^GetGenericPortMappingEntryResponse/.test(k));
if (!key) {
throw new Error("Incorrect response");
}
const res = data[key];
const result = {
public: {
host: (typeof res.NewRemoteHost === "string" && res.NewRemoteHost) || "",
port: parseInt(res.NewExternalPort, 10),
},
private: {
host: res.NewInternalClient,
port: parseInt(res.NewInternalPort, 10),
},
protocol: res.NewProtocol.toLowerCase(),
enabled: res.NewEnabled === "1",
description: res.NewPortMappingDescription,
ttl: parseInt(res.NewLeaseDuration, 10),
// temporary, so typescript will compile
local: false,
};
result.local = result.private.host === address;
if (options.local && !result.local) {
continue;
}
if (options.description) {
if (typeof result.description !== "string")
continue;
if (options.description instanceof RegExp) {
if (!options.description.test(result.description))
continue;
}
else {
if (result.description.indexOf(options.description) === -1)
continue;
}
}
results.push(result);
}
return results;
});
}
getPublicIp() {
return __awaiter(this, void 0, void 0, function* () {
return this.getGateway().then(({ gateway, address }) => __awaiter(this, void 0, void 0, function* () {
var _a;
const data = yield gateway.run("GetExternalIPAddress", []);
const key = Object.keys(data || {}).find((k) => /^GetExternalIPAddressResponse$/.test(k));
if (!key)
throw new Error("Incorrect response");
return ((_a = data[key]) === null || _a === void 0 ? void 0 : _a.NewExternalIPAddress) + "";
}));
});
}
getGateway() {
return __awaiter(this, void 0, void 0, function* () {
let timeouted = false;
const p = this.ssdp.search("urn:schemas-upnp-org:device:InternetGatewayDevice:1");
return new Promise((s, r) => {
const timeout = setTimeout(() => {
timeouted = true;
p.emit("end");
r(new Error("Connection timed out while searching for the gateway."));
}, this.timeout);
p.on("device", (info, address) => {
if (timeouted)
return;
p.emit("end");
clearTimeout(timeout);
// Create gateway
s({ gateway: new device_1.default(info.location), address });
});
});
});
}
close() {
this.ssdp.close();
}
}
exports.Client = Client;
function normalizeOptions(options) {
function toObject(addr) {
if (typeof addr === "number")
return { port: addr };
if (typeof addr === "string" && !isNaN(addr))
return { port: Number(addr) };
if (typeof addr === "object")
return addr;
return {};
}
return {
remote: toObject(options.public),
internal: toObject(options.private),
};
}
exports.default = Client;
Loading