UNPKG

@ntrip/caster

Version:
922 lines (921 loc) 41.3 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.NtripTransport = exports.NtripVersion = void 0; const __1 = require("../.."); __1.NtripHTTPParser.verify(); const caster_1 = require("../../caster"); const transport_1 = require("../transport"); const rtp_1 = require("../../util/rtp"); const sourcetable_1 = require("../../sourcetable"); const dgram = require("dgram"); const http = require("http"); const https = require("https"); const stream = require("stream"); const url = require("url"); const http_1 = require("http"); const tls_1 = require("tls"); const verror_1 = __importDefault(require("verror")); var NtripVersion; (function (NtripVersion) { NtripVersion[NtripVersion["V1"] = 1] = "V1"; NtripVersion[NtripVersion["V2"] = 2] = "V2"; })(NtripVersion = exports.NtripVersion || (exports.NtripVersion = {})); const STATUS_CODES = { 454: "Session Not Found", 455: "Method Not Valid in This State", 459: "Aggregate Operation Not Allowed", 461: "Unsupported Transport" }; /** @see c10410.1 NTRIP v2.0 - 2.6.2, 2.6.3 */ const NTRIP_HEADERS = ['ntrip-gga', 'ntrip-version', 'ntrip-str', 'ntrip-flags', 'str', 'source-agent']; const NTRIP_FLAGS = ['st_auth', 'st_strict', 'st_match', 'st_filter', 'rtsp', 'plain_rtp']; const NTRIP_RTP_PACKET_TIMESTAMP_PERIOD_NS = 125000; function newClientError(info, cause, message, ...params) { return new verror_1.default({ name: 'ClientError', info: info, cause: cause }, message, ...params); } /** * NTRIP caster server for HTTP/RTSP/Plain-RTP */ class NtripTransport extends transport_1.Transport { constructor(manager, options) { var _a, _b, _c, _d; super(manager, options); /** Map of remote addresses to UDP sockets if plainRtpSocket intercepts (happens on Windows) TODO: remove */ this.plainRtpClientSockets = new Map(); /** Map of session numbers to RTP sessions */ this.rtpSessions = new Map(); /** Transport options */ this.options = { port: -1, protocols: { http: true, rtsp: true, rtp: true }, versions: { [NtripVersion.V1]: true, [NtripVersion.V2]: true }, browserStreamAccess: false }; Object.assign(this.options, options); // Ensure at least one protocol is enabled if (!((_a = this.options.protocols) === null || _a === void 0 ? void 0 : _a.http) && !((_b = this.options.protocols) === null || _b === void 0 ? void 0 : _b.rtsp) && !((_c = this.options.protocols) === null || _c === void 0 ? void 0 : _c.rtp)) throw new Error("No protocols enabled"); // Plain RTP does not work with TLS TODO: Node.js DTLS? if (((_d = this.options.protocols) === null || _d === void 0 ? void 0 : _d.rtp) && this.options.tls !== undefined) throw new Error("Plain RTP protocol is not supported when using TLS (HTTPS/RTSPS)"); } get description() { var _a, _b, _c; let tls = this.options.tls !== undefined; let protocols = []; if ((_a = this.options.protocols) === null || _a === void 0 ? void 0 : _a.http) protocols.push('http' + (tls ? 's' : '')); if ((_b = this.options.protocols) === null || _b === void 0 ? void 0 : _b.rtsp) protocols.push('rtsp' + (tls ? 's' : '')); if ((_c = this.options.protocols) === null || _c === void 0 ? void 0 : _c.rtp) protocols.push('rtp'); return `ntrip[${protocols.join(',')},port=${this.options.port}]`; } connectionDescription(source) { return `${source.protocol}://${source.remote.host}:${source.remote.port}`; } static new(options) { return (caster) => new NtripTransport(caster, options); } open() { var _a, _b, _c; let openServer = null; let openPlainRtpSocket = null; // HTTP(S) server (used in all protocols) this.server = (this.options.tls !== undefined ? https : http).createServer({ IncomingMessage: NtripCasterRequest, ServerResponse: NtripCasterResponse, ...this.options.tls }); this.server.on('request', (req, res) => this.accept(req, res)); this.server.keepAliveTimeout = 10000; this.server.timeout = 10000; // Client error handling this.server.on('clientError', (err, socket) => { this.emit('clientError', newClientError({ remote: { host: socket.remoteAddress, port: socket.remotePort, family: socket.remoteFamily } }, err, "HTTP server client error")); if (socket.writable) socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); }); // Bind to port only if HTTP or RTSP are being used (RTP connections are injected manually) if (((_a = this.options.protocols) === null || _a === void 0 ? void 0 : _a.http) || ((_b = this.options.protocols) === null || _b === void 0 ? void 0 : _b.rtsp)) { openServer = new Promise((resolve, reject) => { this.server.once('listening', resolve); this.server.once('error', reject); }); this.server.listen(this.options.port); } // Plain RTP socket if ((_c = this.options.protocols) === null || _c === void 0 ? void 0 : _c.rtp) { this.plainRtpClientSockets = new Map(); this.plainRtpSocket = dgram.createSocket({ type: 'udp6', reuseAddr: true }); this.plainRtpSocket.on('message', (message, remote) => this.message(message, remote)); openPlainRtpSocket = new Promise((resolve, reject) => { this.plainRtpSocket.once('listening', resolve); this.plainRtpSocket.once('error', reject); }); this.plainRtpSocket.bind(this.options.port); } // Wait for server and socket to open Promise.all([openServer, openPlainRtpSocket]) .then(() => this.emit('open')); } close() { const closeServer = this.server === undefined ? null : new Promise((resolve, reject) => { var _a; return (_a = this.server) === null || _a === void 0 ? void 0 : _a.close(err => err ? reject(err) : resolve()); }); const closePlainRtpSocket = this.plainRtpSocket === undefined ? null : new Promise(resolve => { var _a; return (_a = this.plainRtpSocket) === null || _a === void 0 ? void 0 : _a.close(resolve); }); // Wait for server and socket to close Promise.all([closeServer, closePlainRtpSocket]) .then(() => this.emit('close')) .catch(err => this.emit('error', err)); // TODO } async accept(req, res) { var _a, _b, _c, _d; // Detect HTTP/RTSP/RTP req.protocol = ((_a = req.headers['@protocol']) !== null && _a !== void 0 ? _a : 'HTTP').toLowerCase(); // Parse URL if (req.url != undefined) req.query = url.parse(req.url, true, false); req.mountpoint = req.query.pathname.slice(1); if (req.mountpoint.length === 0) req.mountpoint = null; // Remote host req.remote = { host: req.socket.remoteAddress, port: req.socket.remotePort, family: req.socket.remoteFamily }; // Detect NTRIP version const headerVersion = (_b = singularHeader(req.headers['ntrip-version'])) === null || _b === void 0 ? void 0 : _b.toLowerCase(); if (headerVersion === 'ntrip/1.0') req.ntripVersion = NtripVersion.V1; else if (headerVersion === 'ntrip/2.0') req.ntripVersion = NtripVersion.V2; // Detect user agent req.agent = req.headers['user-agent']; if (req.agent === undefined && req.method === 'SOURCE') req.agent = singularHeader(req.headers['source-agent']); // Begin forming auth request req.authRequest = { mountpoint: req.mountpoint, host: (_c = req.headers.host) !== null && _c !== void 0 ? _c : null, source: req.remote, credentials: NtripTransport.parseCredentials(req) }; if (!((_d = this.options.protocols) === null || _d === void 0 ? void 0 : _d[req.protocol])) { res.error(501); this.emit('clientError', newClientError({ remote: req.remote }, undefined, "Protocol requested by client is disabled")); } else if (req.protocol === 'http') { await new NtripTransport.HttpRequestProcessor(this, req, res).accept(); } else if (req.protocol === 'rtsp') { await new NtripTransport.RtspRequestProcessor(this, req, res).accept(); } else if (req.protocol === 'rtp') { await new NtripTransport.RtpRequestProcessor(this, req, res).accept(); } else { res.error(400); } } message(message, remote) { // If a client is already listening on this remote, inject the message to its own socket const client = this.plainRtpClientSockets.get(remote.address + ':' + remote.port); if (client !== undefined) { client.emit('message', message, remote); return; } const emitClientError = (message, error) => { this.emit('clientError', newClientError({ remote: remote }, error, message)); }; let packet; try { packet = rtp_1.RtpPacket.fromBuffer(message); } catch (error) { return emitClientError("RTP: Invalid packet received on plain RTP socket", error); } if (packet.payloadType != rtp_1.NtripRtpMessageType.HTTP || packet.payload == null) return emitClientError("RTP: Unknown packet type or empty payload received on plain RTP socket"); // Replace protocol (HTTP) with RTP let request = packet.payload.toString(); const requestStatusEnd = request.indexOf('\r\n'); request = request.slice(0, request.lastIndexOf(' ', requestStatusEnd) + 1) + 'RTP/1.1' + request.slice(requestStatusEnd); const socket = this.plainRtpSocket; const time = process.hrtime.bigint(); const connection = new stream.Duplex({ read() { }, write(chunk, encoding, callback) { if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); // Since SSRC of response is tied to sequence/timestamp, return appropriately incremented values const sequenceNumber = (packet.sequenceNumber + 1) % 0xffff; const timestamp = (packet.timestamp + (Number(process.hrtime.bigint() - time) / NTRIP_RTP_PACKET_TIMESTAMP_PERIOD_NS)) % 0xffffffff; const response = new rtp_1.RtpPacket({ payloadType: rtp_1.NtripRtpMessageType.HTTP, sequenceNumber: sequenceNumber, timestamp: timestamp, ssrc: packet.ssrc, payload: chunk }); // Send response and destroy stream socket.send(rtp_1.RtpPacket.toBuffer(response), remote.port, remote.address, (error) => { callback(error); // Stream is no longer needed, and RTP session may have been made this.destroy(); }); } }); connection.push(request); connection.remoteAddress = remote.address; connection.remotePort = remote.port; connection.remoteFamily = remote.family; // Inject connection to server to parse HTTP request this.server.emit('connection', connection); } getSourcetable(query) { // Parse filters if provided let filters; if (query !== undefined) { let { auth, strict, match, filter } = query; if (auth !== undefined) auth = !!auth; if (strict !== undefined) strict = !!strict; if (match !== undefined) match = ((match instanceof Array) ? match : [match]) .map(filters => filters.split(';')); if (filter !== undefined) filter = ((filter instanceof Array) ? filter : [filter]) .map(filters => filters.split(';')) .map(sourcetable_1.Sourcetable.parseAdvancedFilters); filters = { auth: auth, strict: strict, simple: match, advanced: filter }; } return this.caster.generateSourcetable(filters); } static parseCredentials(req) { var _a, _b; const credentials = {}; credentials.anonymous = true; // Basic authentication if ((_a = req.headers['authorization']) === null || _a === void 0 ? void 0 : _a.startsWith('Basic ')) { credentials.anonymous = false; let basic = req.headers['authorization'].slice('Basic '.length); basic = Buffer.from(basic, 'base64').toString(); let separator = basic.indexOf(':'); if (separator >= 0) { credentials.basic = { username: basic.slice(0, separator), password: basic.slice(separator + 1) }; } } else if ((_b = req.headers['authorization']) === null || _b === void 0 ? void 0 : _b.startsWith('Bearer ')) { credentials.anonymous = false; credentials.bearer = req.headers['authorization'].slice('Bearer '.length); } // TLS client certificate if (req.socket instanceof tls_1.TLSSocket) { credentials.anonymous = false; credentials.certificate = req.socket.getPeerCertificate().fingerprint; } return credentials; } } exports.NtripTransport = NtripTransport; /** * 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. */ NtripTransport.RequestProcessor = class RequestProcessor { constructor(parent, req, res) { if (parent instanceof RequestProcessor) { this.transport = parent.transport; this.manager = parent.manager; this.req = parent.req; this.res = parent.res; } else { this.transport = parent; this.manager = parent.caster; this.req = req; this.res = res; } } ; async authenticate() { this.req.authResponse = await this.manager.authenticate(this.req.authRequest); return this.req.authResponse.authenticated; } connect(params) { let protocol = this.req.protocol.toLowerCase(); if (this.transport.options.tls !== undefined && (protocol === 'http' || protocol === 'rtsp')) protocol += 's'; const source = { protocol: protocol, version: this.req.ntripVersion, remote: this.req.remote, agent: this.req.agent, toString: () => this.transport.connectionDescription(source) }; return this.transport.connect({ ...params, source: source, mountpoint: this.req.mountpoint, gga: this.req.ntripGga, str: this.req.ntripStr }); } }; /** HTTP request instance processor */ NtripTransport.HttpRequestProcessor = (_a = class HttpRequestProcessor extends NtripTransport.RequestProcessor { async accept() { var _a, _b, _c; // Determine whether the connection is from an NTRIP agent or a browser this.req.ntripAgent = true; const headerAgent = (_a = this.req.headers['user-agent']) === null || _a === void 0 ? void 0 : _a.toUpperCase(); // If User-Agent is provided but doesn't start with NTRIP, and no NTRIP // specific headers are present, assume connection is from browser if (!NTRIP_HEADERS.some(h => h in this.req.headers) && !((_b = headerAgent === null || headerAgent === void 0 ? void 0 : headerAgent.startsWith('NTRIP')) !== null && _b !== void 0 ? _b : true)) { this.req.ntripAgent = false; // Browsers default to V2 this.req.ntripVersion = NtripVersion.V2; // Special case for favicon.ico if (this.req.mountpoint === 'favicon.ico') { const favicon = this.transport.options.browserFavicon; if (favicon !== undefined) return this.res.end(favicon()); else return this.res.error(404); } // Redirect to sourcetable unless browser stream access is requested if (this.transport.options.browserStreamAccess) this.req.mountpoint = null; } // Default to V1 if not defined if (this.req.ntripVersion === null) this.req.ntripVersion = NtripVersion.V1; // Do not allow disabled versions if (((_c = this.transport.options.versions) === null || _c === void 0 ? void 0 : _c[this.req.ntripVersion]) === false) return this.res.error(501); if (this.req.ntripVersion === NtripVersion.V1) { await new HttpRequestProcessor.V1Processor(this).accept(); } else if (this.req.ntripVersion === NtripVersion.V2) { await new HttpRequestProcessor.V2Processor(this).accept(); } else { this.res.error(501); } } }, /** HTTP NTRIP v1.0 request instance processor */ _a.V1Processor = class V1Processor extends NtripTransport.RequestProcessor { async accept() { this.res.statusVersion = 'ICY'; this.res.removeHeader('Content-Length'); this.res.removeHeader('Transfer-Encoding'); this.res.removeHeader('Connection'); this.res.sendDate = false; if (this.req.method === 'SOURCE') { await this.acceptServer(); } else if (this.req.method === 'GET') { await this.acceptClient(); } else { this.res.error(405); } } async acceptServer() { var _a; this.req.authRequest.type = 'server'; // Can't push to / if (this.req.mountpoint === null) return this.res.error(400); // Secret moved to header by NtripHTTPParser this.req.authRequest.credentials.secret = singularHeader(this.req.headers['ntrip-source-secret']); this.req.authRequest.credentials.anonymous = false; // Optional sourcetable entry this.req.ntripStr = singularHeader(this.req.headers['str']); if (!(await this.authenticate())) return this.res.error(401); // Remove listeners set by HTTP server (would return 400 Bad Request on data) this.req.socket.removeAllListeners('data'); try { this.connect({ type: 'server', input: this.req.socket, output: (_a = this.res.socket) !== null && _a !== void 0 ? _a : undefined }); } catch (err) { return this.res.error(500); // TODO } // Remove listeners set by HTTP server (would return 400 Bad Request on data) this.req.socket.removeAllListeners('data'); // Flush headers to confirm connection this.res.flushHeaders(); } async acceptClient() { this.req.authRequest.type = 'client'; // Respond with sourcetable if (this.req.mountpoint === null) return this.printSourcetable(); if (!(await this.authenticate())) return this.res.error(401); if (this.res.socket === null) return this.res.error(500); try { this.connect({ type: 'client', input: this.req.socket, output: this.res.socket }); } catch (err) { return this.res.error(500); } // Remove listeners set by HTTP server (would return 400 Bad Request on data) this.req.socket.removeAllListeners('data'); // Flush headers to confirm connection this.res.flushHeaders(); } async printSourcetable() { var _a; this.res.statusVersion = 'SOURCETABLE'; this.res.setHeader('Connection', 'close'); this.res.setHeader('Server', 'NTRIP ' + caster_1.Caster.NAME + '/1.0'); this.res.setHeader('Content-Type', 'text/plain'); this.res.sendDate = true; this.res.end(await this.transport.getSourcetable((_a = this.req.query) === null || _a === void 0 ? void 0 : _a.query)); } }, /** HTTP NTRIP v2.0 request instance processor */ _a.V2Processor = class V2Processor extends NtripTransport.RequestProcessor { async accept() { this.res.setHeader('Connection', 'close'); this.res.setHeader('Ntrip-Version', 'Ntrip/2.0'); this.res.setHeader('Ntrip-Flags', NTRIP_FLAGS.join(',')); this.res.setHeader('Server', 'NTRIP ' + caster_1.Caster.NAME); if (this.req.method === 'POST') { await this.acceptServer(); } else if (this.req.method === 'GET') { await this.acceptClient(); } else { this.res.error(405); } } async acceptServer() { this.req.authRequest.type = 'server'; // Can't push to / if (this.req.mountpoint === null) return this.res.error(400); // Optional sourcetable entry this.req.ntripStr = singularHeader(this.req.headers['ntrip-str']); // Authenticate if (!(await this.authenticate())) { this.res.setHeader('WWW-Authenticate', `Basic realm="/${this.req.mountpoint}"`); return this.res.error(401, `Mountpoint ${this.req.mountpoint} requires authentication, or provided credentials were invalid`); } try { this.connect({ type: 'server', input: this.req, output: this.res }); } catch (err) { return this.res.error(500); } // Flush headers to confirm connection this.res.flushHeaders(); } async acceptClient() { this.req.authRequest.type = 'client'; // Respond with sourcetable if (this.req.mountpoint === null) return this.printSourcetable(); // Client position for NMEA-requesting mountpoints this.req.ntripGga = singularHeader(this.req.headers['ntrip-gga']); // Authenticate if (!(await this.authenticate())) { this.res.setHeader('WWW-Authenticate', `Basic realm="/${this.req.mountpoint}"`); return this.res.error(401, `Mountpoint ${this.req.mountpoint} requires authentication, or provided credentials were invalid`); } try { this.connect({ type: 'client', input: this.req.socket, output: this.res }); } catch (err) { return this.res.error(500); } // Remove listeners set by HTTP server (would return 400 Bad Request on data) this.req.socket.removeAllListeners('data'); // Flush headers to confirm connection this.res.setHeader('Cache-Control', 'no-store, no-cache, max-age=0'); this.res.setHeader('Content-Type', this.req.ntripAgent ? 'gnss/data' : 'text/plain'); if (!this.req.ntripAgent) this.res.setHeader('X-Content-Type-Options', 'nosniff'); this.res.flushHeaders(); } async printSourcetable() { var _a; let sourcetable; try { sourcetable = await this.transport.getSourcetable((_a = this.req.query) === null || _a === void 0 ? void 0 : _a.query); } catch (error) { return this.res.error(400, `Error filtering sourcetable: ${error.message}`); } this.res.setHeader('Content-Type', this.req.ntripAgent ? 'gnss/sourcetable' : 'text/plain'); this.res.end(sourcetable); } }, _a); /** RTSP request instance processor */ NtripTransport.RtspRequestProcessor = (_b = class RtspRequestProcessor extends NtripTransport.RequestProcessor { async accept() { var _a, _b; this.res.statusVersion = 'RTSP/1.0'; this.res.setHeader('CSeq', (_a = this.req.headers['cseq']) !== null && _a !== void 0 ? _a : 0); this.res.removeHeader('Content-Length'); this.res.removeHeader('Transfer-Encoding'); this.res.removeHeader('Connection'); // Always set timeout to 60 seconds after request ends this.res.on('finish', () => { // Use process to next tick to set timeout after internal set timeout call process.nextTick(() => this.req.socket.setTimeout(20000)); }); // Default to V2 if not defined if (this.req.ntripVersion === null) this.req.ntripVersion = NtripVersion.V2; // Do not allow disabled versions if (((_b = this.transport.options.versions) === null || _b === void 0 ? void 0 : _b[this.req.ntripVersion]) === false) return this.res.error(501); if (this.req.ntripVersion === NtripVersion.V2) { await new RtspRequestProcessor.V2Processor(this).accept(); } else { this.res.error(501); } } }, /** RTSP NTRIP v2.0 request instance processor */ _b.V2Processor = class V2Processor extends NtripTransport.RequestProcessor { async accept() { const session = Number(this.req.headers['session']); if (!isNaN(session)) this.req.rtpSession = this.transport.rtpSessions.get(session); if (this.req.method === 'SETUP') { await this.setup(); } else if (['RECORD', 'PLAY', 'TEARDOWN', 'GET_PARAMETER'].includes(this.req.method)) { this.command(); } else { this.res.error(405); } } async setup() { var _a, _b, _c, _d, _e; // Can't setup new connection if already set up if (this.req.rtpSession !== undefined) return this.res.error(459); this.res.setHeader('Ntrip-Version', 'Ntrip/2.0'); this.res.setHeader('Ntrip-Flags', NTRIP_FLAGS.join(',')); this.res.setHeader('Server', 'NTRIP ' + caster_1.Caster.NAME); const component = (_a = singularHeader(this.req.headers['ntrip-component'])) === null || _a === void 0 ? void 0 : _a.toLowerCase(); if (component === undefined) return this.res.error(400, "Ntrip-Component header not included in request"); // TODO // Parse transport header, verify RTP/GNSS and client port are present this.req.rtspTransportParams = (_b = singularHeader(this.req.headers['transport'])) === null || _b === void 0 ? void 0 : _b.toLowerCase().split(';'); this.req.rtpRemotePort = Number((_d = (_c = this.req.rtspTransportParams) === null || _c === void 0 ? void 0 : _c.find(p => /^client_port=\d+$/.test(p))) === null || _d === void 0 ? void 0 : _d.slice('client_port='.length)); if (!((_e = this.req.rtspTransportParams) === null || _e === void 0 ? void 0 : _e.includes('rtp/gnss')) || isNaN(this.req.rtpRemotePort)) return this.res.error(461); if (!['ntripclient', 'ntripserver'].includes(component)) return this.res.error(400, "Invalid Ntrip-Component header sent"); this.req.authRequest.type = component === 'ntripclient' ? 'client' : 'server'; // Authenticate if (!(await this.authenticate())) return this.res.error(401); this.req.rtpSocket = dgram.createSocket({ type: 'udp6', reuseAddr: true }); try { await new Promise((resolve, reject) => { this.req.rtpSocket.once('connect', resolve); this.req.rtpSocket.once('error', reject); this.req.rtpSocket.connect(this.req.rtpRemotePort, this.req.remote.host); }); } catch (err) { return this.setupError(err); } await this.setupSocket(); } setupSocket() { const session = new rtp_1.NtripRtpSession(this.req.rtpSocket); // Keep regenerating SSRC until unused while (this.transport.rtpSessions.has(session.ssrc)) session.regenerateSsrc(); this.req.rtpSession = { session: session, type: this.req.authRequest.type, mountpoint: this.req.mountpoint }; this.transport.rtpSessions.set(session.ssrc, this.req.rtpSession); session.on('close', () => { var _a; this.transport.rtpSessions.delete(session.ssrc); (_a = this.res.socket) === null || _a === void 0 ? void 0 : _a.destroy(); }); this.res.setHeader('Transport', this.req.rtspTransportParams .filter(p => !/^server_port=.*$/.test(p)) .concat('server_port=' + this.req.rtpSocket.address().port) .join(';')); this.res.setHeader('Session', session.ssrc); this.res.setHeader('Connection', 'keep-alive'); this.res.end(); } setupError(err) { this.req.rtpSocket.close(); this.res.error(500); this.transport.emit('clientError', newClientError({ remote: { ...this.req.remote, port: this.req.rtpRemotePort } }, err, "RTP: Could not connect to client RTP port")); } command() { // Active connection must be available if (this.req.rtpSession === undefined) return this.res.error(454); this.res.setHeader('Session', this.req.rtpSession.session.ssrc); this.res.setHeader('Connection', 'keep-alive'); this.res.sendDate = false; if (this.req.method === 'RECORD' || this.req.method === 'PLAY') return this.start(); if (this.req.method === 'TEARDOWN') { this.req.rtpSession.session.end(); this.transport.rtpSessions.delete(this.req.rtpSession.session.ssrc); this.res.setHeader('Connection', 'close'); } else if (this.req.method === 'GET_PARAMETER') { // Allow clients to update their location with Ntrip-GGA if (this.req.rtpSession.type === 'client') { const gga = singularHeader(this.req.headers['ntrip-gga']); const connection = this.req.rtpSession.connection; if (gga !== undefined && connection !== undefined) connection.gga = gga; } } // Flush headers to confirm connection this.res.end(); } start() { var _a; let type; if (this.req.method === 'RECORD') { type = 'server'; // Optional sourcetable entry this.req.ntripStr = singularHeader(this.req.headers['ntrip-str']); } else if (this.req.method === 'PLAY') { type = 'client'; // Client position for NMEA-requesting mountpoints this.req.ntripGga = singularHeader(this.req.headers['ntrip-gga']); } else { return this.res.error(405); } if (((_a = this.req.rtpSession) === null || _a === void 0 ? void 0 : _a.type) != type) return this.res.error(455); try { this.req.rtpSession.connection = this.connect({ type: type, stream: this.req.rtpSession.session.dataStream }); } catch (err) { return this.res.error(500); } // Flush headers to confirm connection this.res.end(); } }, _b); /** RTP request instance processor */ NtripTransport.RtpRequestProcessor = (_c = class RtpRequestProcessor extends NtripTransport.RequestProcessor { async accept() { var _a; // Default to V2 if not defined if (this.req.ntripVersion === null) this.req.ntripVersion = NtripVersion.V2; // Do not allow disabled versions if (((_a = this.transport.options.versions) === null || _a === void 0 ? void 0 : _a[this.req.ntripVersion]) === false) return this.res.error(501); if (this.req.ntripVersion === NtripVersion.V2) { await new RtpRequestProcessor.V2Processor(this).accept(); } else { this.res.error(501); } } }, /** RTP NTRIP v2.0 request instance processor */ _c.V2Processor = class V2Processor extends NtripTransport.RequestProcessor { async accept() { this.res.removeHeader('Connection'); this.res.setHeader('Ntrip-Version', 'Ntrip/2.0'); this.res.setHeader('Ntrip-Flags', NTRIP_FLAGS.join(',')); this.res.setHeader('Server', 'NTRIP ' + caster_1.Caster.NAME); if (this.req.method === 'POST') { this.req.authRequest.type = 'server'; // Optional sourcetable entry this.req.ntripStr = singularHeader(this.req.headers['ntrip-str']); } else if (this.req.method === 'GET') { this.req.authRequest.type = 'client'; // Client position for NMEA-requesting mountpoints this.req.ntripGga = singularHeader(this.req.headers['ntrip-gga']); } else { return this.res.error(405); } // Authenticate if (!(await this.authenticate())) return this.res.error(401); this.req.rtpSocket = dgram.createSocket({ type: 'udp6', reuseAddr: true }); try { await new Promise((resolve, reject) => { this.req.rtpSocket.once('connect', resolve); this.req.rtpSocket.once('error', reject); // Bind to chosen port and then connect to client this.req.rtpSocket.bind(this.transport.options.port, undefined, () => { this.req.rtpSocket.connect(this.req.remote.port, this.req.remote.host); }); }); } catch (err) { this.setupError(err); } await this.setupSocket(); } setupSocket() { const remote = this.req.remote.host + ':' + this.req.remote.port; this.transport.plainRtpClientSockets.set(remote, this.req.rtpSocket); this.req.rtpSocket.on('close', () => this.transport.plainRtpClientSockets.delete(remote)); const session = new rtp_1.NtripRtpSession(this.req.rtpSocket); // Keep regenerating SSRC until unused while (this.transport.rtpSessions.has(session.ssrc)) session.regenerateSsrc(); this.req.rtpSession = { session: session, type: this.req.authRequest.type, mountpoint: this.req.mountpoint }; this.transport.rtpSessions.set(session.ssrc, this.req.rtpSession); session.on('close', () => this.transport.rtpSessions.delete(session.ssrc)); this.res.setHeader('Session', session.ssrc); try { this.connect({ type: this.req.authRequest.type, stream: this.req.rtpSession.session.dataStream }); } catch (err) { return this.res.error(500); } if (this.req.authRequest.type === 'client') this.res.setHeader('Content-Type', 'gnss/data'); // Flush headers to confirm connection this.res.end(); // Send keep-alive message every 20 seconds to avoid disconnection setInterval(() => { var _a; return (_a = this.req.rtpSession) === null || _a === void 0 ? void 0 : _a.session.dataStream.write(''); }, 20000); } setupError(err) { this.req.rtpSocket.close(); this.res.error(500); this.transport.emit('clientError', newClientError({ remote: { ...this.req.remote, port: this.req.rtpRemotePort } }, err, "RTSP: Could not connect to client RTP port")); } }, _c); class NtripCasterRequest extends http_1.IncomingMessage { constructor() { super(...arguments); this.mountpoint = null; this.ntripVersion = null; this.ntripAgent = false; } } class NtripCasterResponse extends http_1.ServerResponse { constructor() { super(...arguments); this.statusVersion = 'HTTP/1.1'; } // noinspection JSUnusedGlobalSymbols /** * Internal method that stores the response header. * Override to include NTRIP V1 responses such as ICY 200 OK and SOURCETABLE 200 OK. * * @param firstLine HTTP response status line * @param headers HTTP headers * @private */ _storeHeader(firstLine, headers) { firstLine = this.statusVersion + firstLine.slice(firstLine.indexOf(' ')); // @ts-ignore Call private _storeHeader super._storeHeader(firstLine, headers); //console.log(chalk.green(firstLine)); for (let header in headers) { // @ts-ignore //console.log(chalk.green(headers[header][0] + ": " + headers[header][1])); } } error(code, response) { this.statusCode = code; this.statusMessage = STATUS_CODES[code]; this.removeHeader('Connection'); if (response === undefined) { this.removeHeader('Content-Length'); this.removeHeader('Transfer-Encoding'); } this.end(response); } } function singularHeader(value) { if (value instanceof Array) return value[0]; return value; }