node-red-contrib-ntrip 0.2.3 → 0.2.7

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 (39) 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 +61 -0
  7. package/CLAUDE.md +73 -0
  8. package/README.md +145 -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/ntrip/99-ntrip.html +207 -31
  29. package/ntrip/99-ntrip.js +12 -12
  30. package/ntrip/lib/ntrip-client.js +23 -18
  31. package/ntrip/nodes/nmea-decoder-node.js +77 -34
  32. package/ntrip/nodes/nmea-encoder-node.js +68 -47
  33. package/ntrip/nodes/ntrip-client-node.js +143 -123
  34. package/ntrip/nodes/rtcm-decoder-node.js +65 -47
  35. package/package.json +20 -2
  36. package/test/nmea-decoder.spec.js +146 -0
  37. package/test/nmea-encoder.spec.js +104 -0
  38. package/test/rtcm-decoder.spec.js +116 -0
  39. package/.github/workflows/npm-publish.yml +0 -33
@@ -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
+ };
@@ -1,16 +1,16 @@
1
1
  module.exports = function (RED) {
2
2
  'use strict';
3
3
 
4
- const {
5
- NmeaTransport,
4
+ const {
5
+ NmeaTransport,
6
6
  NmeaMessageUnknown,
7
7
  NmeaMessage,
8
8
  NmeaMessageDtm,
9
9
  NmeaMessageGbs,
10
10
  NmeaMessageGga,
11
11
  NmeaMessageGll,
12
- NmeaMessageGns,
13
- NmeaMessageGrs,
12
+ NmeaMessageGns,
13
+ NmeaMessageGrs,
14
14
  NmeaMessageGsa,
15
15
  NmeaMessageGst,
16
16
  NmeaMessageGsv,
@@ -30,24 +30,53 @@ module.exports = function (RED) {
30
30
  node.nmeaMessagesReceived = 0;
31
31
  node.invalidMessagesReceived = 0;
32
32
 
33
+ function updateStatus(ok) {
34
+ node.status({
35
+ fill: ok ? 'green' : 'red',
36
+ shape: 'ring',
37
+ text: 'NMEA: ' + node.nmeaMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
38
+ });
39
+ }
40
+
41
+ function safeToString(value) {
42
+ if (value === undefined || value === null) {
43
+ return '';
44
+ }
45
+ try {
46
+ return String(value);
47
+ } catch {
48
+ return '';
49
+ }
50
+ }
51
+
33
52
  this.on('input', function (msg) {
53
+ let payload = msg.payload;
54
+
55
+ if (payload == null || payload.nmeaMessage == null || payload.messageType == null) {
56
+ let errMsg = {
57
+ payload: {
58
+ error: 'Invalid input. Expected payload with nmeaMessage and messageType properties.',
59
+ input: payload,
60
+ inputString: safeToString(payload),
61
+ },
62
+ };
63
+ node.send([null, errMsg]);
64
+ node.invalidMessagesReceived++;
65
+ updateStatus(false);
66
+ return;
67
+ }
68
+
69
+ let input = payload.nmeaMessage;
70
+
71
+ try {
72
+ let messageType = String(payload.messageType).toUpperCase();
34
73
 
35
- let input = msg.payload.nmeaMessage;
36
- let isNmeaMessage = input.prototype instanceof NmeaMessage;
37
- let messageType = msg.payload.messageType;
38
- messageType = messageType.toUpperCase();
39
-
40
- try
41
- {
42
- let message;
43
74
  let nmeaMessage;
44
- if(isNmeaMessage){
45
- nmeaMessage = input; // is already a NmeaMessage
46
- }
47
- else {
48
-
75
+ if (input instanceof NmeaMessage) {
76
+ nmeaMessage = input;
77
+ } else {
49
78
  switch (messageType) {
50
- case 'Object':
79
+ case 'OBJECT':
51
80
  nmeaMessage = NmeaMessageUnknown.construct(input);
52
81
  break;
53
82
  case 'DTM':
@@ -102,50 +131,42 @@ module.exports = function (RED) {
102
131
  nmeaMessage = NmeaMessageZda.construct(input);
103
132
  break;
104
133
  default:
105
- // message could not be converted.
106
- break;
134
+ throw new Error('Unsupported NMEA message type: ' + messageType);
107
135
  }
108
136
  }
109
137
 
110
- if (nmeaMessage !== undefined) {
111
- message = NmeaTransport.encode(nmeaMessage);
112
- }
113
-
114
- let msg = {
115
- payload : {
116
- nmeaMessage : message,
117
- input : input
138
+ let message = NmeaTransport.encode(nmeaMessage);
139
+
140
+ let outMsg = {
141
+ payload: {
142
+ nmeaMessage: message,
143
+ messageType: payload.messageType,
144
+ input: input,
118
145
  },
119
146
  };
120
147
 
121
- node.send([msg, null]);
148
+ node.send([outMsg, null]);
122
149
  node.nmeaMessagesReceived++;
123
- }
124
- catch(ex)
125
- {
126
- let msg = {
127
- payload : {
128
- error : ex,
129
- input : input,
130
- inputString : input.toString()
131
- }
150
+ updateStatus(true);
151
+ } catch (ex) {
152
+ let errMsg = {
153
+ payload: {
154
+ error: ex,
155
+ input: input,
156
+ inputString: safeToString(input),
157
+ },
132
158
  };
133
- node.send([null, msg]);
159
+ node.send([null, errMsg]);
134
160
  node.invalidMessagesReceived++;
161
+ updateStatus(false);
135
162
  }
136
-
137
- node.status({
138
- fill: 'green',
139
- shape: 'ring',
140
- text: 'NMEA: ' + node.nmeaMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
141
- });
142
163
  });
143
164
 
144
- this.on('close', function(done) {
165
+ this.on('close', function (done) {
145
166
  node.status({});
146
167
  done();
147
168
  });
148
169
  }
149
170
 
150
171
  return NmeaEncoderNode;
151
- };
172
+ };