UNPKG

@ntrip/caster

Version:
483 lines (482 loc) 21.2 kB
"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; }