node-red-contrib-ntrip 0.2.3 → 0.2.8

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.
Files changed (42) hide show
  1. package/.github/dependabot.yml +23 -0
  2. package/.github/workflows/node.js.yml +22 -21
  3. package/.github/workflows/release.yml +65 -0
  4. package/.prettierignore +20 -0
  5. package/.prettierrc.json +9 -0
  6. package/CHANGELOG.md +69 -0
  7. package/CLAUDE.md +73 -0
  8. package/README.md +185 -18
  9. package/doc/architecture/README.md +19 -0
  10. package/doc/architecture/adr/0001-single-registration-file.md +64 -0
  11. package/doc/architecture/adr/0002-ntrip-uploader-extension.md +71 -0
  12. package/doc/architecture/adr/0003-two-output-decoder-design.md +73 -0
  13. package/doc/architecture/adr/0004-stateful-handshake-interception.md +74 -0
  14. package/doc/architecture/adr/0005-rtcm-partial-frame-buffer.md +87 -0
  15. package/doc/architecture/adr/0006-nmea-multi-sentence-split.md +66 -0
  16. package/doc/architecture/adr/0007-mocha-test-helper-stack.md +67 -0
  17. package/doc/architecture/adr/0008-coordinate-gating-sentinel.md +76 -0
  18. package/doc/architecture/adr/README.md +18 -0
  19. package/doc/architecture/architecture-decisions.md +60 -0
  20. package/doc/architecture/behavioural-design.md +226 -0
  21. package/doc/architecture/errors-and-weaknesses.md +71 -0
  22. package/doc/architecture/future-improvements.md +130 -0
  23. package/doc/architecture/overview.md +77 -0
  24. package/doc/architecture/refactoring-recommendations.md +114 -0
  25. package/doc/architecture/statistics.md +118 -0
  26. package/doc/architecture/structural-design.md +141 -0
  27. package/eslint.config.js +36 -0
  28. package/examples/rtcm-encode.json +1 -0
  29. package/ntrip/99-ntrip.html +289 -31
  30. package/ntrip/99-ntrip.js +15 -12
  31. package/ntrip/lib/ntrip-client.js +23 -18
  32. package/ntrip/nodes/nmea-decoder-node.js +77 -34
  33. package/ntrip/nodes/nmea-encoder-node.js +68 -47
  34. package/ntrip/nodes/ntrip-client-node.js +143 -123
  35. package/ntrip/nodes/rtcm-decoder-node.js +65 -47
  36. package/ntrip/nodes/rtcm-encoder-node.js +90 -0
  37. package/package.json +20 -2
  38. package/test/nmea-decoder.spec.js +146 -0
  39. package/test/nmea-encoder.spec.js +104 -0
  40. package/test/rtcm-decoder.spec.js +116 -0
  41. package/test/rtcm-encoder.spec.js +144 -0
  42. package/.github/workflows/npm-publish.yml +0 -33
@@ -13,8 +13,8 @@
13
13
  passthrough: { value: false, required: false },
14
14
 
15
15
  port: { value: 2101, required: false, validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 65535))) }},
16
- host: { value:"" },
17
- mountpoint: { value:"" },
16
+ host: { value:"", required: true, validate: function(v) { return typeof v === "string" && v.trim().length > 0; } },
17
+ mountpoint: { value:"", required: true, validate: function(v) { return typeof v === "string" && v.trim().length > 0; } },
18
18
  interval: { value: 1000, required: false, validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 100000))) }},
19
19
  xcoordinate: { value: 0, required: false, validate:function(v) { return ((v === "") || (RED.validators.number(v) )) }},
20
20
  ycoordinate: { value: 0, required: false, validate:function(v) { return ((v === "") || (RED.validators.number(v) )) }},
@@ -108,7 +108,7 @@
108
108
  </div>
109
109
  <div class="form-row">
110
110
  <label for="node-input-password"><i class="fa fa-key"></i> Password</label>
111
- <input type="text" id="node-input-password" placeholder="Enter the password here)">
111
+ <input type="password" id="node-input-password" placeholder="Enter the password here">
112
112
  </div>
113
113
 
114
114
  <hr align="middle"/>
@@ -140,18 +140,83 @@
140
140
  </script>
141
141
 
142
142
  <script type="text/x-red" data-help-name="NtripClient">
143
- <p>A node that connects to a NTRIP server.</p>
143
+ <p>Connects to an NTRIP caster as either a client (download) or a server (upload).
144
+ In download mode the node emits the bytes received from the caster (typically
145
+ RTCM correction data). In upload mode the node forwards bytes from its input
146
+ to the caster on the configured mountpoint.</p>
144
147
 
145
148
  <h3>Configuration</h3>
149
+ <dl class="message-properties">
150
+ <dt>Host <span class="property-type">string</span></dt>
151
+ <dd>Hostname or IP address of the NTRIP caster (e.g. <code>rtk2go.com</code>).</dd>
152
+
153
+ <dt>Port <span class="property-type">number</span></dt>
154
+ <dd>TCP port of the caster. Defaults to <code>2101</code>.</dd>
155
+
156
+ <dt>Mountpoint <span class="property-type">string</span></dt>
157
+ <dd>The stream name on the caster. Enter the name without a leading slash.</dd>
158
+
159
+ <dt>Mode <span class="property-type">"download" | "upload"</span></dt>
160
+ <dd><b>Download</b>: receive correction data from the caster.<br>
161
+ <b>Upload</b>: push correction data to a mountpoint on the caster.</dd>
162
+
163
+ <dt>Authentication Mode <span class="property-type">string</span></dt>
164
+ <dd>Upload only. Selects the handshake the caster expects:
165
+ <ul>
166
+ <li><b>Legacy (Plain Text)</b> — <code>SOURCE</code> with no auth header, accepted by rtk2go.</li>
167
+ <li><b>Legacy (Basic Auth)</b> — <code>SOURCE</code> plus an <code>Authorization: Basic</code> header, accepted by SNIP.</li>
168
+ <li><b>NTRIP V1</b> — <code>POST</code> over HTTP/1.0 with Basic Auth.</li>
169
+ <li><b>NTRIP V2</b> — <code>POST</code> over HTTP/1.1 with Basic Auth and <code>Ntrip-Version: Ntrip/2.0</code>.</li>
170
+ </ul>
171
+ </dd>
172
+
173
+ <dt>Pass through data <span class="property-type">boolean</span></dt>
174
+ <dd>Upload only. If enabled, bytes written to the caster are also re-emitted
175
+ on the node's output for downstream processing.</dd>
176
+
177
+ <dt>Username / Password <span class="property-type">credential</span></dt>
178
+ <dd>Caster credentials. Stored encrypted by Node-RED.</dd>
179
+
180
+ <dt>X / Y / Z <span class="property-type">number</span></dt>
181
+ <dd>Optional ECEF coordinate triple. If set (i.e. not all zero) the underlying
182
+ client generates and periodically sends a NMEA <code>GGA</code> sentence to
183
+ the caster — required by some VRS / network-RTK services.</dd>
184
+
185
+ <dt>Interval <span class="property-type">number (ms)</span></dt>
186
+ <dd>Period between <code>GGA</code> emissions when X/Y/Z are configured.</dd>
187
+ </dl>
146
188
 
147
- <p>Description of the device.</p>
148
-
149
189
  <h3>Inputs</h3>
150
- <p>Content is directly passed to the ntrip server.</p>
151
-
190
+ <dl class="message-properties">
191
+ <dt>payload <span class="property-type">Buffer | string</span></dt>
192
+ <dd>Written verbatim to the caster (upload mode). In download mode the input
193
+ is typically only used to refresh the position via the form below.</dd>
194
+
195
+ <dt>payload <span class="property-type">number[]</span></dt>
196
+ <dd>An array <code>[x, y, z]</code> is interpreted as a coordinate update and
197
+ forwarded to <code>client.setXYZ()</code> rather than being written to the
198
+ caster. Useful for streaming position fixes into the GGA emitter.</dd>
199
+ </dl>
200
+
152
201
  <h3>Outputs</h3>
153
- <p>Messages received from the ntrip server.</p>
154
- <p><code>msg.payload</code></p>
202
+ <dl class="message-properties">
203
+ <dt>payload <span class="property-type">Buffer</span></dt>
204
+ <dd>Bytes received from the caster. In download mode this is the correction
205
+ stream (RTCM). In upload mode with <i>Pass through data</i> enabled, written
206
+ bytes are also re-emitted here.</dd>
207
+ </dl>
208
+
209
+ <h3>Status</h3>
210
+ <p>The node shows <code>connecting…</code> until the caster acknowledges the
211
+ handshake with <code>ICY 200 OK</code>. After that it shows the running
212
+ <code>Rx</code> / <code>Tx</code> counters. A red badge indicates a rejected
213
+ connection, a missing mountpoint, or a write failure.</p>
214
+
215
+ <h3>Details</h3>
216
+ <p>The first <code>ICY 200 OK</code>, <code>ICY 406</code>, or
217
+ <code>SOURCETABLE 200 OK</code> reply is consumed by the node to set its
218
+ status; any RTCM bytes that arrive in the same TCP segment as the
219
+ <code>ICY 200 OK</code> reply are forwarded on the output.</p>
155
220
  </script>
156
221
 
157
222
 
@@ -186,18 +251,134 @@
186
251
  </script>
187
252
 
188
253
  <script type="text/x-red" data-help-name="RtcmDecoder">
189
- <p>A node that decodes RTCM messages.</p>
254
+ <p>Decodes binary RTCM (Radio Technical Commission for Maritime Services)
255
+ correction frames using the <code>@gnss/rtcm</code> library. One input
256
+ message may contain a single frame or several concatenated frames; each
257
+ decoded frame is emitted as its own output message.</p>
258
+ <p>The node keeps an internal buffer so frames that straddle TCP packet
259
+ boundaries are reassembled across consecutive input events.</p>
190
260
 
191
261
  <h3>Configuration</h3>
262
+ <dl class="message-properties">
263
+ <dt>Description <span class="property-type">string</span></dt>
264
+ <dd>Optional label shown in the flow editor.</dd>
265
+ </dl>
192
266
 
193
- <p>Description of the device.</p>
194
-
195
267
  <h3>Inputs</h3>
196
- <p>.</p>
197
-
268
+ <dl class="message-properties">
269
+ <dt>payload <span class="property-type">Buffer</span></dt>
270
+ <dd>Raw RTCM bytes, typically produced by the upstream <i>NTRIP Client</i>
271
+ node. Strings are accepted but converted via <code>Buffer.from</code>.</dd>
272
+ </dl>
273
+
198
274
  <h3>Outputs</h3>
199
- <p>.</p>
200
- <p><code>msg.payload</code></p>
275
+ <p>Two outputs: successfully decoded frames go to the first; decode failures go to the second.</p>
276
+ <ol class="node-ports">
277
+ <li>Decoded
278
+ <dl class="message-properties">
279
+ <dt>payload.rtcm <span class="property-type">number</span></dt>
280
+ <dd>The RTCM message type identifier (e.g. <code>1005</code>, <code>1077</code>).</dd>
281
+ <dt>payload.messageType <span class="property-type">string</span></dt>
282
+ <dd>The constructor name of the decoded message (e.g. <code>1005</code> → <code>Stationary</code>).</dd>
283
+ <dt>payload.message <span class="property-type">object</span></dt>
284
+ <dd>The decoded message object with all fields.</dd>
285
+ <dt>payload.input <span class="property-type">Buffer</span></dt>
286
+ <dd>The bytes consumed for this frame.</dd>
287
+ </dl>
288
+ </li>
289
+ <li>Error
290
+ <dl class="message-properties">
291
+ <dt>payload.error <span class="property-type">Error</span></dt>
292
+ <dd>The exception thrown by the decoder.</dd>
293
+ <dt>payload.input <span class="property-type">Buffer</span></dt>
294
+ <dd>The remaining buffer at the point of failure.</dd>
295
+ <dt>payload.inputString <span class="property-type">string</span></dt>
296
+ <dd>A debug string representation of the buffer.</dd>
297
+ </dl>
298
+ </li>
299
+ </ol>
300
+ </script>
301
+
302
+ <!-- ------------------------------------------------------------------------------------------ -->
303
+
304
+ <script type="text/javascript">
305
+ RED.nodes.registerType('RtcmEncoder', {
306
+ category: 'GNSS',
307
+ color: '#C0DEED',
308
+ defaults: {
309
+ description: { value:"" },
310
+ },
311
+ inputs: 1,
312
+ outputs: 2,
313
+ icon: "function.png",
314
+ paletteLabel: "RTCM Encoder",
315
+ label: function () {
316
+ return this.description || "RTCM Encoder";
317
+ },
318
+ labelStyle: function() {
319
+ return this.description?"node_label_italic":"";
320
+ }
321
+ });
322
+
323
+ </script>
324
+
325
+ <script type="text/x-red" data-template-name="RtcmEncoder">
326
+ <div class="form-row">
327
+ <label for="node-input-description"><i class="fa fa-comment"></i> Description</label>
328
+ <input type="text" id="node-input-description" placeholder="The description of the node (optional)">
329
+ </div>
330
+ </script>
331
+
332
+ <script type="text/x-red" data-help-name="RtcmEncoder">
333
+ <p>Encodes an <code>RtcmMessage</code> instance back into a binary RTCM 3
334
+ frame using the <code>@gnss/rtcm</code> library. The inverse of the
335
+ <i>RTCM Decoder</i> — useful for re-emitting a decoded frame after
336
+ modification (e.g. rewriting a station ID before forwarding upstream).</p>
337
+ <p>This encoder does not construct an <code>RtcmMessage</code> from plain
338
+ fields. The input must already be an instance — typically the
339
+ <code>msg.payload.message</code> emitted by the decoder.</p>
340
+
341
+ <h3>Configuration</h3>
342
+ <dl class="message-properties">
343
+ <dt>Description <span class="property-type">string</span></dt>
344
+ <dd>Optional label shown in the flow editor.</dd>
345
+ </dl>
346
+
347
+ <h3>Inputs</h3>
348
+ <dl class="message-properties">
349
+ <dt>payload <span class="property-type">RtcmMessage</span></dt>
350
+ <dd>An <code>RtcmMessage</code> instance, encoded directly.</dd>
351
+ <dt>payload.message <span class="property-type">RtcmMessage</span></dt>
352
+ <dd>Round-trip shape from the <i>RTCM Decoder</i> — the encoder picks
353
+ up <code>msg.payload.message</code> and encodes that.</dd>
354
+ </dl>
355
+
356
+ <h3>Outputs</h3>
357
+ <p>Two outputs: successfully encoded frames go to the first; failures go to the second.</p>
358
+ <ol class="node-ports">
359
+ <li>Encoded
360
+ <dl class="message-properties">
361
+ <dt>payload.rtcmMessage <span class="property-type">Buffer</span></dt>
362
+ <dd>The encoded RTCM 3 frame, ready to forward to an NTRIP
363
+ uploader or a TCP sink.</dd>
364
+ <dt>payload.rtcm <span class="property-type">number</span></dt>
365
+ <dd>The RTCM message type identifier (e.g. <code>1005</code>).</dd>
366
+ <dt>payload.messageType <span class="property-type">string</span></dt>
367
+ <dd>The constructor name (e.g. <code>StationArp</code>).</dd>
368
+ <dt>payload.input <span class="property-type">any</span></dt>
369
+ <dd>The original input as supplied.</dd>
370
+ </dl>
371
+ </li>
372
+ <li>Error
373
+ <dl class="message-properties">
374
+ <dt>payload.error <span class="property-type">Error | string</span></dt>
375
+ <dd>The exception thrown by the encoder, or a string describing
376
+ invalid input.</dd>
377
+ <dt>payload.input <span class="property-type">any</span></dt>
378
+ <dd>The offending input.</dd>
379
+ </dl>
380
+ </li>
381
+ </ol>
201
382
  </script>
202
383
 
203
384
  <!-- ------------------------------------------------------------------------------------------ -->
@@ -231,18 +412,52 @@
231
412
  </script>
232
413
 
233
414
  <script type="text/x-red" data-help-name="NmeaDecoder">
234
- <p>A node that decodes NMEA messages.</p>
415
+ <p>Decodes NMEA 0183 sentences (<code>GGA</code>, <code>RMC</code>,
416
+ <code>GSV</code>, …) using the <code>@gnss/nmea</code> library. A single
417
+ input message may contain several <code>\r\n</code>-delimited sentences;
418
+ each is decoded individually and emitted as its own output message.</p>
235
419
 
236
420
  <h3>Configuration</h3>
421
+ <dl class="message-properties">
422
+ <dt>Description <span class="property-type">string</span></dt>
423
+ <dd>Optional label shown in the flow editor.</dd>
424
+ </dl>
237
425
 
238
- <p>Description of the device.</p>
239
-
240
426
  <h3>Inputs</h3>
241
- <p>.</p>
242
-
427
+ <dl class="message-properties">
428
+ <dt>payload <span class="property-type">string | Buffer</span></dt>
429
+ <dd>A NMEA sentence or a chunk containing several sentences. Buffers are
430
+ decoded as UTF-8.</dd>
431
+ <dt>payload.nmeaMessage <span class="property-type">string | Buffer</span></dt>
432
+ <dd>Alternative location for the input. If <code>msg.payload</code> is an
433
+ object with an <code>nmeaMessage</code> property, that value is used
434
+ instead of <code>msg.payload</code> itself.</dd>
435
+ </dl>
436
+
243
437
  <h3>Outputs</h3>
244
- <p>.</p>
245
- <p><code>msg.payload</code></p>
438
+ <p>Two outputs: successfully decoded sentences go to the first; decode failures go to the second.</p>
439
+ <ol class="node-ports">
440
+ <li>Decoded
441
+ <dl class="message-properties">
442
+ <dt>payload.messageType <span class="property-type">string</span></dt>
443
+ <dd>Sentence type in upper case, e.g. <code>GGA</code>, <code>RMC</code>, <code>GSV</code>.</dd>
444
+ <dt>payload.nmeaMessage <span class="property-type">object</span></dt>
445
+ <dd>The decoded sentence object with all fields.</dd>
446
+ <dt>payload.input <span class="property-type">string</span></dt>
447
+ <dd>The raw sentence as it was parsed.</dd>
448
+ </dl>
449
+ </li>
450
+ <li>Error
451
+ <dl class="message-properties">
452
+ <dt>payload.error <span class="property-type">Error | string</span></dt>
453
+ <dd>The exception or reason for the decode failure.</dd>
454
+ <dt>payload.input <span class="property-type">string</span></dt>
455
+ <dd>The sentence that failed to decode.</dd>
456
+ <dt>payload.inputString <span class="property-type">string</span></dt>
457
+ <dd>String representation of the input for logging.</dd>
458
+ </dl>
459
+ </li>
460
+ </ol>
246
461
  </script>
247
462
 
248
463
  <!-- ------------------------------------------------------------------------------------------ -->
@@ -276,16 +491,59 @@
276
491
  </script>
277
492
 
278
493
  <script type="text/x-red" data-help-name="NmeaEncoder">
279
- <p>A node that encodes NMEA messages.</p>
494
+ <p>Encodes a NMEA sentence object back to its textual form using the
495
+ <code>@gnss/nmea</code> library. The inverse of the <i>NMEA Decoder</i>
496
+ node — useful for round-tripping or for emitting handcrafted sentences
497
+ (e.g. a <code>GGA</code> fix into an NTRIP uploader).</p>
280
498
 
281
499
  <h3>Configuration</h3>
500
+ <dl class="message-properties">
501
+ <dt>Description <span class="property-type">string</span></dt>
502
+ <dd>Optional label shown in the flow editor.</dd>
503
+ </dl>
282
504
 
283
- <p>Description of the device.</p>
284
-
285
505
  <h3>Inputs</h3>
286
- <p>.</p>
287
-
506
+ <dl class="message-properties">
507
+ <dt>payload.messageType <span class="property-type">string</span></dt>
508
+ <dd>Sentence type, e.g. <code>GGA</code>, <code>RMC</code>, <code>VTG</code>,
509
+ <code>GSV</code>, <code>ZDA</code>. Case-insensitive. Supported types:
510
+ <code>DTM</code>, <code>GBS</code>, <code>GGA</code>, <code>GLL</code>,
511
+ <code>GNS</code>, <code>GRS</code>, <code>GSA</code>, <code>GST</code>,
512
+ <code>GSV</code>, <code>RMC</code>, <code>THS</code>, <code>TXT</code>,
513
+ <code>VHW</code>, <code>VLW</code>, <code>VPW</code>, <code>VTG</code>,
514
+ <code>ZDA</code>, plus <code>OBJECT</code> for the generic
515
+ <code>NmeaMessageUnknown</code> form.</dd>
516
+ <dt>payload.nmeaMessage <span class="property-type">object | NmeaMessage</span></dt>
517
+ <dd>The sentence content. Either a plain field object that matches the
518
+ target sentence schema, or an already-constructed <code>NmeaMessage</code>
519
+ instance (which is forwarded as-is to <code>NmeaTransport.encode</code>).</dd>
520
+ </dl>
521
+
288
522
  <h3>Outputs</h3>
289
- <p>.</p>
290
- <p><code>msg.payload</code></p>
523
+ <p>Two outputs: successfully encoded sentences go to the first; failures go to the second.</p>
524
+ <ol class="node-ports">
525
+ <li>Encoded
526
+ <dl class="message-properties">
527
+ <dt>payload.nmeaMessage <span class="property-type">string</span></dt>
528
+ <dd>The encoded NMEA sentence (including <code>$</code> prefix and
529
+ checksum).</dd>
530
+ <dt>payload.messageType <span class="property-type">string</span></dt>
531
+ <dd>The sentence type, echoed back as it was supplied.</dd>
532
+ <dt>payload.input <span class="property-type">object</span></dt>
533
+ <dd>The original input object.</dd>
534
+ </dl>
535
+ </li>
536
+ <li>Error
537
+ <dl class="message-properties">
538
+ <dt>payload.error <span class="property-type">Error | string</span></dt>
539
+ <dd>The exception or reason for the failure. Unknown
540
+ <code>messageType</code> values, missing input fields, and
541
+ encoder errors all surface here.</dd>
542
+ <dt>payload.input <span class="property-type">any</span></dt>
543
+ <dd>The offending input.</dd>
544
+ <dt>payload.inputString <span class="property-type">string</span></dt>
545
+ <dd>String representation of the input for logging.</dd>
546
+ </dl>
547
+ </li>
548
+ </ol>
291
549
  </script>
package/ntrip/99-ntrip.js CHANGED
@@ -1,27 +1,30 @@
1
1
  /**
2
- * Created by Karl-Heinz Wind
3
- **/
2
+ * Created by Karl-Heinz Wind
3
+ **/
4
4
 
5
5
  module.exports = function (RED) {
6
- "use strict";
7
-
6
+ 'use strict';
7
+
8
8
  const pkg = require('./../package.json');
9
9
  RED.log.info('node-red-contrib-ntrip version: v' + pkg.version);
10
10
 
11
11
  const NtripClientNode = require('./nodes/ntrip-client-node.js')(RED);
12
- RED.nodes.registerType("NtripClient", NtripClientNode, {
12
+ RED.nodes.registerType('NtripClient', NtripClientNode, {
13
13
  credentials: {
14
- username: { type: "text" },
15
- password: { type: "password" },
16
- }
14
+ username: { type: 'text' },
15
+ password: { type: 'password' },
16
+ },
17
17
  });
18
18
 
19
19
  const RtcmDecoderNode = require('./nodes/rtcm-decoder-node.js')(RED);
20
- RED.nodes.registerType("RtcmDecoder", RtcmDecoderNode);
20
+ RED.nodes.registerType('RtcmDecoder', RtcmDecoderNode);
21
+
22
+ const RtcmEncoderNode = require('./nodes/rtcm-encoder-node.js')(RED);
23
+ RED.nodes.registerType('RtcmEncoder', RtcmEncoderNode);
21
24
 
22
25
  const NmeaDecoderNode = require('./nodes/nmea-decoder-node.js')(RED);
23
- RED.nodes.registerType("NmeaDecoder", NmeaDecoderNode);
26
+ RED.nodes.registerType('NmeaDecoder', NmeaDecoderNode);
24
27
 
25
28
  const NmeaEncoderNode = require('./nodes/nmea-encoder-node.js')(RED);
26
- RED.nodes.registerType("NmeaEncoder", NmeaEncoderNode);
27
- }
29
+ RED.nodes.registerType('NmeaEncoder', NmeaEncoderNode);
30
+ };
@@ -6,9 +6,18 @@ const net = require('net');
6
6
 
7
7
  // Orginal class is extended to be able to send data to the caster.
8
8
  class NtripClientUploader extends NtripClient {
9
- constructor(options) {
9
+ constructor(options) {
10
10
  super(options);
11
11
  this.authmode = options.authmode || 'legacy';
12
+
13
+ // Reject CR/LF in user-supplied fields that are interpolated into the
14
+ // handshake string, to prevent header injection.
15
+ for (const field of ['mountpoint', 'username', 'password']) {
16
+ const value = this[field];
17
+ if (typeof value === 'string' && /[\r\n]/.test(value)) {
18
+ throw new Error('Invalid character in ' + field + ': CR/LF not allowed');
19
+ }
20
+ }
12
21
  }
13
22
 
14
23
  _connect() {
@@ -19,12 +28,12 @@ class NtripClientUploader extends NtripClient {
19
28
  // init connection of client
20
29
  this.client = net.createConnection({
21
30
  host: this.host,
22
- port: this.port
31
+ port: this.port,
23
32
  });
24
33
 
25
34
  // on timeout event
26
35
  this.client.on('timeout', () => {
27
- this._onError('socket timeouted');
36
+ this._onError('socket timeout');
28
37
  });
29
38
 
30
39
  // on connect event
@@ -36,23 +45,19 @@ class NtripClientUploader extends NtripClient {
36
45
  const host = this.host;
37
46
  const port = this.port;
38
47
  const authmode = this.authmode;
39
- const authorization = Buffer.from(
40
- username + ':' + password,
41
- 'utf8'
42
- ).toString('base64');
43
-
48
+ const authorization = Buffer.from(username + ':' + password, 'utf8').toString('base64');
49
+
44
50
  let data;
45
51
  switch (authmode) {
46
- case 'legacy': // Legacy --> rtk2Go accepts this.
52
+ case 'legacy': // Legacy --> rtk2Go accepts this.
47
53
  if (username === '' && password === '') {
48
54
  data = `SOURCE ${mountpoint}\r\n\r\n`;
49
- }
50
- else {
51
- data = `SOURCE ${password} /${mountpoint}\r\nSource-Agent: NTRIP ${username}\r\n\r\n`;
55
+ } else {
56
+ data = `SOURCE ${password} /${mountpoint}\r\nSource-Agent: NTRIP ${username}\r\n\r\n`;
52
57
  }
53
58
  break;
54
59
  case 'hybrid': // hybrid (legacy + http auth) --> SNIP accepts this.
55
- data = `SOURCE ${mountpoint}\r\nAuthorization: Basic ${authorization}\r\n\r\n`;
60
+ data = `SOURCE ${mountpoint}\r\nAuthorization: Basic ${authorization}\r\n\r\n`;
56
61
  break;
57
62
  case 'ntripv1': // NTRIP V1 POST
58
63
  data = `POST /${mountpoint} HTTP/1.0\r\nUser-Agent: NTRIP ${userAgent}\r\nAuthorization: Basic ${authorization}\r\nContent-Type: gnss/data\r\n\r\n`;
@@ -61,7 +66,7 @@ class NtripClientUploader extends NtripClient {
61
66
  data = `POST /${mountpoint} HTTP/1.1\r\nHost: ${host}:${port}\r\nUser-Agent: NTRIP ${userAgent}\r\nAuthorization: Basic ${authorization}\r\nNtrip-Version: Ntrip/2.0\r\nContent-Type: gnss/data\r\nConnection: keep-alive\r\n\r\n`;
62
67
  break;
63
68
  default:
64
- throw new Error("Auth mode is not supported " + authmode);
69
+ throw new Error('Auth mode is not supported ' + authmode);
65
70
  }
66
71
 
67
72
  this.client.write(data);
@@ -93,14 +98,14 @@ class NtripClientUploader extends NtripClient {
93
98
  }
94
99
 
95
100
  function createDownloader(options) {
96
- return new NtripClient(options)
101
+ return new NtripClient(options);
97
102
  }
98
103
 
99
104
  function createUploader(options) {
100
- return new NtripClientUploader(options)
105
+ return new NtripClientUploader(options);
101
106
  }
102
107
 
103
108
  module.exports = {
104
109
  createDownloader,
105
- createUploader
106
- };
110
+ createUploader,
111
+ };
@@ -9,54 +9,97 @@ module.exports = function (RED) {
9
9
  node.nmeaMessagesReceived = 0;
10
10
  node.invalidMessagesReceived = 0;
11
11
 
12
- this.on('input', function (msg) {
12
+ function updateStatus() {
13
+ node.status({
14
+ fill: 'green',
15
+ shape: 'ring',
16
+ text: 'NMEA: ' + node.nmeaMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
17
+ });
18
+ }
13
19
 
14
- let buffer = msg.payload.nmeaMessage ?? msg.payload;
15
- if(Buffer.isBuffer(buffer)){
16
- buffer = buffer.toString('utf8');
17
- }
18
-
19
- try
20
- {
21
- let nmeaMessage = NmeaTransport.decode(buffer);
20
+ function decodeOne(sentence, rawInput, rawString) {
21
+ try {
22
+ let nmeaMessage = NmeaTransport.decode(sentence);
22
23
  let messageType = nmeaMessage.constructor.name.replace('NmeaMessage', '').toUpperCase();
23
24
 
24
- let msg = {
25
- payload : {
26
- messageType : messageType,
27
- nmeaMessage : nmeaMessage,
28
- input : buffer
25
+ node.send([
26
+ {
27
+ payload: {
28
+ messageType: messageType,
29
+ nmeaMessage: nmeaMessage,
30
+ input: sentence,
31
+ },
29
32
  },
30
- };
31
-
32
- node.send([msg, null]);
33
+ null,
34
+ ]);
33
35
  node.nmeaMessagesReceived++;
36
+ } catch (ex) {
37
+ node.send([
38
+ null,
39
+ {
40
+ payload: {
41
+ error: ex,
42
+ input: rawInput,
43
+ inputString: rawString,
44
+ },
45
+ },
46
+ ]);
47
+ node.invalidMessagesReceived++;
48
+ }
49
+ }
50
+
51
+ this.on('input', function (msg) {
52
+ if (msg.payload === undefined || msg.payload === null) {
53
+ return;
34
54
  }
35
- catch(ex)
36
- {
37
- let msg = {
38
- payload : {
39
- error : ex,
40
- input : buffer,
41
- inputString : buffer.toString()
42
- }
43
- };
44
- node.send([null, msg]);
55
+
56
+ // rawInput is the value the user provided (Buffer, string, ...).
57
+ // Keep it untouched so the error output can return it as-is.
58
+ let rawInput = typeof msg.payload === 'object' && msg.payload.nmeaMessage !== undefined ? msg.payload.nmeaMessage : msg.payload;
59
+
60
+ let rawString;
61
+ if (Buffer.isBuffer(rawInput)) {
62
+ rawString = rawInput.toString('utf8');
63
+ } else if (typeof rawInput === 'string') {
64
+ rawString = rawInput;
65
+ } else {
66
+ node.send([
67
+ null,
68
+ {
69
+ payload: {
70
+ error: 'Payload is neither string nor Buffer',
71
+ input: rawInput,
72
+ inputString: String(rawInput),
73
+ },
74
+ },
75
+ ]);
45
76
  node.invalidMessagesReceived++;
77
+ updateStatus();
78
+ return;
46
79
  }
47
80
 
48
- node.status({
49
- fill: 'green',
50
- shape: 'ring',
51
- text: 'NMEA: ' + node.nmeaMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
52
- });
81
+ // A single chunk may carry several sentences delimited by \r\n.
82
+ let sentences = rawString.split(/\r?\n/);
83
+ let any = false;
84
+ for (let i = 0; i < sentences.length; i++) {
85
+ let s = sentences[i].trim();
86
+ if (s.length === 0) {
87
+ continue;
88
+ }
89
+ decodeOne(s, rawInput, rawString);
90
+ any = true;
91
+ }
92
+
93
+ if (any) {
94
+ updateStatus();
95
+ }
53
96
  });
54
97
 
55
- this.on('close', function(done) {
98
+ this.on('close', function (done) {
56
99
  node.status({});
57
100
  done();
58
101
  });
59
102
  }
60
103
 
61
104
  return NmeaDecoderNode;
62
- };
105
+ };