@ntrip/caster
Version:
NTRIP caster
483 lines (482 loc) • 21.2 kB
JavaScript
"use strict";
/*
* This file is part of the @ntrip/caster distribution (https://github.com/node-ntrip/caster).
* Copyright (c) 2020 Nebojsa Cvetkovic.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _a, _b, _c;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NtripPushPullTransport = void 0;
const __1 = require("../..");
__1.NtripHTTPParser.verify();
const transport_1 = require("../transport");
const __2 = require("../");
const caster_1 = require("../../caster");
const rtp_1 = require("../../util/rtp");
const dgram = require("dgram");
const dns = require("dns");
const http = require("http");
const https = require("https");
const http_1 = require("http");
const verror_1 = __importDefault(require("verror"));
class NtripPushPullTransport extends transport_1.SingleConnectionTransport {
constructor(manager, options) {
super(manager, options);
this.options = options;
// Plain RTP does not work with TLS TODO: Node.js DTLS?
if (options.protocol === 'rtp' && options.tls !== undefined)
throw new Error("Plain RTP protocol is not supported when using TLS");
}
get description() {
return `ntrip[${this.options.mode}]`;
}
connectionDescription(source) {
return `${source.protocol}://${source.remote.host}:${source.remote.port}`;
}
static new(options) {
return (caster) => new NtripPushPullTransport(caster, options);
}
open() {
var _a, _b;
if (this.options.protocol !== 'rtp') {
const options = {
maxSockets: 1,
keepAlive: true,
timeout: 60000,
keepAliveMsecs: 60000
};
this.agent = (this.options.tls === undefined
? new http.Agent(options)
: new https.Agent({ ...options, ...this.options.tls }));
}
const params = {
protocol: this.options.tls === undefined ? 'http:' : 'https:',
host: this.options.remote.host,
port: this.options.remote.port,
agent: this.agent,
headers: {},
rejectUnauthorized: false
};
if (((_a = this.options.credentials) === null || _a === void 0 ? void 0 : _a.basic) !== undefined) {
params.auth = this.options.credentials.basic.username + ':' + this.options.credentials.basic.password;
}
else if (((_b = this.options.credentials) === null || _b === void 0 ? void 0 : _b.bearer) !== undefined) {
params.headers['Authorization'] = 'Bearer ' + this.options.credentials.bearer;
}
if (this.options.protocol === 'http') {
new NtripPushPullTransport.HttpRequestFormer(this, params).form();
}
else if (this.options.protocol === 'rtsp') {
new NtripPushPullTransport.RtspRequestFormer(this, params).form();
}
else if (this.options.protocol === 'rtp') {
new NtripPushPullTransport.RtpRequestFormer(this, params).form();
}
else {
this.error(new Error(`Unknown protocol ${this.options.protocol}`));
}
}
close() {
var _a, _b, _c;
super.close();
(_a = this.agent) === null || _a === void 0 ? void 0 : _a.destroy();
(_b = this.rtpSession) === null || _b === void 0 ? void 0 : _b.destroy();
try {
(_c = this.rtpSocket) === null || _c === void 0 ? void 0 : _c.close();
}
catch (err) { }
}
}
exports.NtripPushPullTransport = NtripPushPullTransport;
/**
* Abstract request instance processor
*
* Mainly used to improve organization of code and avoid complex function names for various combinations of
* protocols and versions. Request processors are initially created by {@link NtripTransport#accept}.
*
* Contains properties for reference to the parent transport, overall caster manager, and request/response objects.
*/
NtripPushPullTransport.RequestFormer = class RequestFormer {
constructor(parent, params) {
if (parent instanceof RequestFormer) {
this.transport = parent.transport;
this.manager = parent.manager;
this.options = parent.transport.options;
this.params = parent.params;
this.req = parent.req;
this.res = parent.res;
}
else {
this.transport = parent;
this.manager = parent.caster;
this.options = parent.options;
this.params = params;
}
this.type = this.options.mode == 'pull' ? 'server' : 'client';
}
;
send(flushOnly = false) {
this.req = new NtripClientRequest(this.params, response => {
this.res = response;
this.response();
});
if (flushOnly) {
this.req.flushHeaders();
}
else {
this.req.end('');
}
}
response() {
if (this.res.statusCode != 200)
return this.transport.error(new Error(`Could not connect to caster, response was ${this.res.statusCode} ${this.res.statusMessage}`));
if (this.options.mode == 'push') {
return this.responseServer();
}
else { // if (this.options.mode == 'pull') {
return this.responseClient();
}
}
responseServer() {
this.connect({
type: 'client',
input: this.res,
output: this.req,
});
}
responseClient() {
this.connect({
type: 'server',
input: this.res,
output: this.req
});
}
connect(params) {
let protocol = this.options.protocol;
if (this.options.tls !== undefined && (protocol === 'http' || protocol === 'rtsp'))
protocol += 's';
const source = {
protocol: protocol,
version: this.options.ntripVersion,
remote: {
host: this.options.remote.host,
port: this.options.remote.port,
family: this.res.socket.remoteFamily
},
toString: () => this.transport.connectionDescription(source)
};
return this.transport.connect({
...params,
source: source,
mountpoint: this.options.localMountpoint,
gga: this.options.localGga,
str: this.options.localStr
});
}
setNtripStrHeader(header = 'Ntrip-STR') {
if (this.options.remoteStr === undefined)
return;
this.params.headers[header] = this.options.remoteStr;
}
setNtripGgaHeader(header = 'Ntrip-GGA') {
if (this.options.remoteGga === undefined)
return;
this.params.headers[header] = this.options.remoteGga;
}
};
/** HTTP request instance processor */
NtripPushPullTransport.HttpRequestFormer = (_a = class HttpRequestFormer extends NtripPushPullTransport.RequestFormer {
form() {
this.params.statusVersion = 'HTTP/1.1';
this.params.path = '/' + this.options.remoteMountpoint;
if (this.options.ntripVersion == __2.NtripVersion.V1) {
return new HttpRequestFormer.V1Processor(this).form();
}
else if (this.options.ntripVersion == __2.NtripVersion.V2) {
return new HttpRequestFormer.V2Processor(this).form();
}
else {
return this.transport.error(new Error(`Unknown NTRIP version ${this.options.ntripVersion}`));
}
}
},
/** HTTP NTRIP v1.0 request instance former */
_a.V1Processor = class V1Processor extends NtripPushPullTransport.RequestFormer {
form() {
if (this.options.mode == 'push') {
return this.formServer();
}
else { // if (this.options.mode == 'pull') {
return this.formClient();
}
}
formServer() {
var _a, _b;
this.params.method = 'SOURCE';
this.params.headers['Source-Agent'] = 'NTRIP ' + caster_1.Caster.NAME;
if (((_a = this.options.credentials) === null || _a === void 0 ? void 0 : _a.secret) === undefined)
return this.transport.error(new Error("NTRIP v1 SOURCE request secret not provided"));
this.params.sourceSecret = (_b = this.options.credentials) === null || _b === void 0 ? void 0 : _b.secret;
this.setNtripStrHeader('STR');
this.send(true);
}
formClient() {
this.params.method = 'GET';
this.params.headers['User-Agent'] = 'NTRIP ' + caster_1.Caster.NAME;
this.setNtripGgaHeader();
this.send(true);
}
responseServer() {
this.connect({
type: 'client',
stream: this.res.socket
});
}
responseClient() {
this.res.socket.removeAllListeners('data');
this.connect({
type: 'server',
stream: this.res.socket
});
}
},
/** HTTP NTRIP v2.0 request instance former */
_a.V2Processor = class V2Processor extends NtripPushPullTransport.RequestFormer {
form() {
this.params.headers['Ntrip-Version'] = 'Ntrip/2.0';
this.params.headers['User-Agent'] = 'NTRIP ' + caster_1.Caster.NAME;
this.params.headers['Connection'] = 'close';
if (this.options.mode == 'push') {
this.params.method = 'POST';
this.setNtripStrHeader();
}
else { // if (this.options.mode == 'pull') {
this.params.method = 'GET';
this.setNtripGgaHeader();
}
this.send(true);
}
},
_a);
/** RTSP request instance processor */
NtripPushPullTransport.RtspRequestFormer = (_b = class RtspRequestFormer extends NtripPushPullTransport.RequestFormer {
form() {
this.params.statusVersion = 'RTSP/1.0';
this.params.path = 'rtsp://' + this.options.remote.host + ':' + this.options.remote.port + '/' + this.options.remoteMountpoint;
this.params.headers['CSeq'] = 1;
if (this.options.ntripVersion == __2.NtripVersion.V2) {
return new RtspRequestFormer.V2Processor(this).form();
}
else {
this.transport.error(new Error('RTSP only supports NTRIP V2 requests'));
}
}
},
/** RTSP NTRIP v2.0 request instance former */
_b.V2Processor = class V2Processor extends NtripPushPullTransport.RequestFormer {
constructor() {
super(...arguments);
this.active = false;
}
form() {
this.params.method = 'SETUP';
this.params.headers['Ntrip-Version'] = 'Ntrip/2.0';
this.params.headers['User-Agent'] = 'NTRIP ' + caster_1.Caster.NAME;
this.params.headers['Connection'] = 'keep-alive';
this.params.timeout = 60000;
this.socket = this.transport.rtpSocket = dgram.createSocket({
type: 'udp6',
reuseAddr: true,
// TODO: https://github.com/nodejs/node/issues/33331
lookup: (hostname, options, callback) => dns.lookup(hostname, 0, (err, address, family) => callback(err, family === 4 ? '::ffff:' + address : address, family))
});
new Promise((resolve, reject) => {
this.socket.once('listening', resolve);
this.socket.once('error', reject);
}).then(() => {
const address = this.socket.address();
this.params.headers['Transport'] = 'RTP/GNSS;unicast;client_port=' + address.port;
if (this.options.mode == 'push') {
this.params.headers['Ntrip-Component'] = 'Ntripserver';
this.setNtripStrHeader();
}
else { // if (this.options.mode == 'pull') {
this.params.headers['Ntrip-Component'] = 'Ntripclient';
this.setNtripGgaHeader();
}
this.send();
}).catch(err => {
this.socket.close();
this.transport.error(new Error(`Could not open RTP socket: ${err.message}`));
});
// Bind to random port and then connect to client
this.socket.bind();
}
response() {
var _a;
if (this.res.statusCode != 200)
return this.transport.error(new Error(`Could not connect to caster, response was ${this.res.statusCode} ${this.res.statusMessage}`));
if (this.session === undefined) {
let ssrc = parseInt(this.res.headers['session']);
if (isNaN(ssrc))
return this.transport.error(new Error("Caster did not respond with (valid) RTP session code"));
// Parse transport header, verify RTP/GNSS and client port are present
const transport = singularHeader(this.res.headers['transport']);
const rtspTransportParams = transport === null || transport === void 0 ? void 0 : transport.toLowerCase().split(';');
const serverPort = Number((_a = rtspTransportParams === null || rtspTransportParams === void 0 ? void 0 : rtspTransportParams.find(p => /^server_port=\d+$/.test(p))) === null || _a === void 0 ? void 0 : _a.slice('server_port='.length));
if (isNaN(serverPort))
return this.transport.error(new Error("Caster did not respond with target RTP port"));
new Promise((resolve, reject) => {
this.socket.once('connect', resolve);
this.socket.once('error', reject);
}).then(() => {
this.session = this.transport.rtpSession = new rtp_1.NtripRtpSession(this.socket);
this.session.on('close', () => this.transport.close());
// If expecting data from caster, send initial empty packet to allow connection through firewall
if (this.options.mode === 'pull')
this.session.dataStream.write('');
this.params.headers = {};
this.params.headers['Connection'] = 'keep-alive';
this.params.method = this.options.mode === 'push' ? 'RECORD' : 'PLAY';
this.params.headers['CSeq'] = 2;
this.params.headers['Session'] = ssrc;
this.send();
}).catch(err => {
this.socket.close();
this.transport.error(new verror_1.default({
cause: err,
info: {
remote: this.transport.options.remote
}
}, "Could not connect to caster RTP port"));
});
this.socket.connect(serverPort, this.options.remote.host);
}
else if (!this.active) {
this.active = true;
this.connect({
type: this.type,
stream: this.session.dataStream
});
// Send keep-alive message every 30 seconds to avoid disconnection
this.params.method = 'GET_PARAMETER';
setInterval(() => {
this.params.headers['CSeq']++;
this.send();
}, 30000);
}
// Required to allow next request to be sent
this.res.emit('end');
}
},
_b);
/** RTP request instance processor */
NtripPushPullTransport.RtpRequestFormer = (_c = class RtpRequestFormer extends NtripPushPullTransport.RequestFormer {
form() {
this.params.statusVersion = 'HTTP/1.1';
this.params.path = '/' + this.options.remoteMountpoint;
if (this.options.ntripVersion == __2.NtripVersion.V2) {
return new RtpRequestFormer.V2Processor(this).form();
}
else {
this.transport.error(new Error('RTP only supports NTRIP V2 requests'));
}
}
},
/** RTP NTRIP v2.0 request instance former */
_c.V2Processor = class V2Processor extends NtripPushPullTransport.RequestFormer {
form() {
this.params.headers['Ntrip-Version'] = 'Ntrip/2.0';
this.params.headers['User-Agent'] = 'NTRIP ' + caster_1.Caster.NAME;
this.params.headers['Connection'] = 'keep-alive';
this.params.createConnection = ((options, onCreate) => {
this.socket = this.transport.rtpSocket = dgram.createSocket({
type: 'udp6',
// TODO: https://github.com/nodejs/node/issues/33331
lookup: (hostname, options, callback) => dns.lookup(hostname, 0, (err, address, family) => callback(err, family === 4 ? '::ffff:' + address : address, family))
});
this.socket.once('connect', () => onCreate(undefined, this.createInjectionSocket()));
this.socket.once('error', (err) => onCreate(err));
this.socket.connect(this.options.remote.port, this.options.remote.host);
}); // Signature of createConnection is incorrect
if (this.options.mode == 'push') {
this.params.method = 'POST';
this.setNtripStrHeader();
}
else { // if (this.options.mode == 'pull') {
this.params.method = 'GET';
this.setNtripGgaHeader();
}
this.send();
}
createInjectionSocket() {
var _a, _b, _c;
this.session = this.transport.rtpSession = new rtp_1.NtripRtpSession(this.socket);
const connection = this.session.httpStream;
connection.remoteAddress = (_a = this.socket) === null || _a === void 0 ? void 0 : _a.remoteAddress().address;
connection.remotePort = (_b = this.socket) === null || _b === void 0 ? void 0 : _b.remoteAddress().port;
connection.remoteFamily = (_c = this.socket) === null || _c === void 0 ? void 0 : _c.remoteAddress().family;
// Inject as a socket, only remote* properties of net.Socket will be accessed
return connection;
}
response() {
if (this.res.statusCode != 200)
return this.transport.error(new Error(`Could not connect to caster, response was ${this.res.statusCode} ${this.res.statusMessage}`));
let ssrc = parseInt(this.res.headers['session']);
if (isNaN(ssrc))
return this.transport.error(new Error("Caster did not respond with (valid) RTP session code"));
this.session.ssrc = ssrc;
this.connect({
type: this.type,
stream: this.session.dataStream
});
// Send keep-alive message every 20 seconds to avoid disconnection
setInterval(() => { var _a; return (_a = this.session) === null || _a === void 0 ? void 0 : _a.dataStream.write(''); }, 20000);
}
},
_c);
class NtripClientRequest extends http_1.ClientRequest {
constructor(options, cb) {
super(options, cb);
this.statusVersion = options.statusVersion;
this.sourceSecret = options.sourceSecret;
}
// noinspection JSUnusedGlobalSymbols
/**
* Internal method that stores the request header.
* Override to include RTSP in status line.
*
* @param firstLine HTTP request status line
* @param headers HTTP headers
* @private
*/
_storeHeader(firstLine, headers) {
if (this.statusVersion !== undefined)
firstLine = firstLine.slice(0, firstLine.lastIndexOf(' ') + 1) + this.statusVersion + '\r\n';
if (this.sourceSecret !== undefined)
firstLine = firstLine.slice(0, firstLine.indexOf(' ') + 1) +
this.sourceSecret + firstLine.slice(firstLine.indexOf(' '));
// @ts-ignore Call private _storeHeader
super._storeHeader(firstLine, headers);
}
}
function singularHeader(value) {
if (value instanceof Array)
return value[0];
return value;
}