/src/gpsd/gpsd-3.27.1~dev/gpsd/net_ntrip.c
Line | Count | Source |
1 | | /* net_ntrip.c -- gather and dispatch DGNSS data from NTRIP broadcasters |
2 | | * |
3 | | * This file is Copyright by the GPSD project |
4 | | * SPDX-License-Identifier: BSD-2-clause |
5 | | * |
6 | | * See: |
7 | | * https://igs.bkg.bund.de/root_ftp/NTRIP/documentation/NtripDocumentation.pdf |
8 | | * |
9 | | * NTRIP is not an open protocol. So this file is based on guesswork. |
10 | | */ |
11 | | |
12 | | #include "../include/gpsd_config.h" // must be before all includes |
13 | | |
14 | | #include <errno.h> |
15 | | #include <fcntl.h> |
16 | | #include <math.h> |
17 | | #include <netdb.h> |
18 | | #include <stdbool.h> |
19 | | #include <stdio.h> |
20 | | #include <stdlib.h> |
21 | | #include <string.h> |
22 | | #include <strings.h> |
23 | | #include <sys/socket.h> |
24 | | #include <sys/stat.h> |
25 | | #include <sys/types.h> |
26 | | #include <unistd.h> |
27 | | |
28 | | #include "../include/gpsd.h" |
29 | | #include "../include/strfuncs.h" |
30 | | |
31 | | // NTRIP 1.0 caster responses. Based on Icecast audio servers |
32 | 0 | #define NTRIP_SOURCETABLE "SOURCETABLE 200 OK\r\n" |
33 | 0 | #define NTRIP_ENDSOURCETABLE "ENDSOURCETABLE" |
34 | 0 | #define NTRIP_ICY "ICY 200 OK\r\n" |
35 | | |
36 | | // NTRIP 2.0 caster responses. Based on HTTP 1.1 |
37 | 0 | #define NTRIP_SOURCETABLE2 "Content-Type: gnss/sourcetable\r\n" |
38 | 0 | #define NTRIP_BODY "\r\n\r\n" |
39 | 0 | #define NTRIP_HTTP "HTTP/1.1 200 OK\r\n" |
40 | | |
41 | | // sourcetable stuff |
42 | 0 | #define NTRIP_CAS "CAS;" |
43 | 0 | #define NTRIP_NET "NET;" |
44 | 0 | #define NTRIP_STR "STR;" |
45 | 0 | #define NTRIP_BR "\r\n" |
46 | 0 | #define NTRIP_QSC "\";\"" |
47 | | |
48 | | // HTTP 1.1 |
49 | 0 | #define NTRIP_UNAUTH "401 Unauthorized\r\n" |
50 | 0 | #define NTRIP_CHUNKED "Transfer-Encoding: chunked\r\n" |
51 | | |
52 | | // ntrip_state() -- stringify conn_state |
53 | | static const char *ntrip_state(unsigned state) |
54 | 0 | { |
55 | | // NTRIP conn_states. See include/gpsd.h |
56 | 0 | const char *ntrip_states[] = { |
57 | 0 | "INIT", |
58 | 0 | "SENT_PROBE", |
59 | 0 | "SENT_GET", |
60 | 0 | "ESTABLISHED", |
61 | 0 | "ERR", |
62 | 0 | "CLOSED", |
63 | 0 | "INPROGRESS", |
64 | 0 | "UNKNOWN", |
65 | 0 | }; |
66 | 0 | unsigned num_states = sizeof(ntrip_states)/sizeof(ntrip_states[0]); |
67 | |
|
68 | 0 | if (num_states <= state) { |
69 | | // huh? |
70 | 0 | state = num_states - 1; |
71 | 0 | } |
72 | 0 | return ntrip_states[state]; |
73 | 0 | } |
74 | | |
75 | | // table to convert format string to enum ntrip_fmt |
76 | | static struct ntrip_fmt_s { |
77 | | const char *string; |
78 | | const enum ntrip_fmt format; |
79 | | } const ntrip_fmts[] = { |
80 | | {"CMR+", FMT_CMRP}, |
81 | | // RTCM1 required for the SAPOS server in Gemany, confirmed as RTCM2.3 |
82 | | {"RTCM1_", FMT_RTCM2_3}, |
83 | | {"RTCM 2.0", FMT_RTCM2_0}, |
84 | | {"RTCM 2.1", FMT_RTCM2_1}, |
85 | | {"RTCM 2.2", FMT_RTCM2_2}, |
86 | | {"RTCM22", FMT_RTCM2_2}, |
87 | | {"RTCM 2.3", FMT_RTCM2_3}, |
88 | | {"RTCM2.3", FMT_RTCM2_3}, |
89 | | {"RTCM 2", FMT_RTCM2}, |
90 | | {"RTCM2", FMT_RTCM2}, |
91 | | {"RTCM 3.0", FMT_RTCM3_0}, |
92 | | {"RTCM3.0", FMT_RTCM3_0}, |
93 | | {"RTCM 3.1", FMT_RTCM3_1}, |
94 | | {"RTCM3.1", FMT_RTCM3_1}, |
95 | | {"RTCM 3.2", FMT_RTCM3_2}, |
96 | | {"RTCM3.2", FMT_RTCM3_2}, // for http://sapos.geonord-od.de:2101/ |
97 | | {"RTCM32", FMT_RTCM3_2}, |
98 | | {"RTCM 3.3", FMT_RTCM3_3}, |
99 | | {"RTCM 3", FMT_RTCM3_0}, |
100 | | {"RTCM3", FMT_RTCM3_0}, |
101 | | // {"SPARTN", FMT_SPARTN}, // u-blox, SPAARTN v1 // UNSUPPORTED! |
102 | | {"SPARTN 2.0", FMT_SPARTN_2}, // u-blox, SPAARTN v2 |
103 | | {NULL, FMT_UNKNOWN}, |
104 | | }; |
105 | | |
106 | | /* Return pointer to one NUL terminated source table field |
107 | | * Return NULL on error |
108 | | * fields are separated by a semicolon (;) |
109 | | */ |
110 | | static char *ntrip_field_iterate(char *start, |
111 | | char *prev, |
112 | | const char *eol, |
113 | | const struct gpsd_errout_t *errout) |
114 | 0 | { |
115 | 0 | char *s, *t, *u; |
116 | |
|
117 | 0 | if (start) { |
118 | 0 | s = start; |
119 | 0 | } else { |
120 | 0 | if (!prev) { |
121 | 0 | return NULL; |
122 | 0 | } |
123 | 0 | s = prev + strnlen(prev, BUFSIZ) + 1; |
124 | 0 | if (s >= eol) { |
125 | 0 | return NULL; |
126 | 0 | } |
127 | 0 | } |
128 | | |
129 | | // ignore any quoted ; chars as they are part of the field content |
130 | 0 | t = s; |
131 | 0 | while ((u = strstr(t, NTRIP_QSC))) { |
132 | 0 | t = u + sizeof(NTRIP_QSC) - 1; |
133 | 0 | } |
134 | |
|
135 | 0 | if ((t = strstr(t, ";"))) { |
136 | 0 | *t = '\0'; |
137 | 0 | } |
138 | |
|
139 | 0 | GPSD_LOG(LOG_RAW, errout, "NTRIP: Next source table field %s\n", s); |
140 | |
|
141 | 0 | return s; |
142 | 0 | } |
143 | | |
144 | | |
145 | | /* Decode a stream record from the sourcetable |
146 | | * See: http://software.rtcm-ntrip.org/wiki/STR |
147 | | */ |
148 | | static void ntrip_str_parse(char *str, size_t len, |
149 | | struct ntrip_stream_t *hold, |
150 | | const struct gpsd_errout_t *errout) |
151 | 0 | { |
152 | 0 | char *s, *eol = str + len; |
153 | |
|
154 | 0 | memset(hold, 0, sizeof(*hold)); |
155 | | |
156 | | // <mountpoint> |
157 | 0 | if (NULL != (s = ntrip_field_iterate(str, NULL, eol, errout))) { |
158 | 0 | (void)strlcpy(hold->mountpoint, s, sizeof(hold->mountpoint)); |
159 | 0 | } |
160 | | // <identifier> |
161 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
162 | | // <format> |
163 | 0 | if (NULL != (s = ntrip_field_iterate(NULL, s, eol, errout))) { |
164 | 0 | struct ntrip_fmt_s const *pfmt; |
165 | |
|
166 | 0 | hold->format = FMT_UNKNOWN; |
167 | 0 | for (pfmt = ntrip_fmts; NULL != pfmt->string; pfmt++) { |
168 | 0 | if (0 == strcasecmp(pfmt->string, s)) { |
169 | 0 | hold->format = pfmt->format; |
170 | 0 | break; |
171 | 0 | } |
172 | 0 | } |
173 | 0 | if (FMT_UNKNOWN == hold->format) { |
174 | 0 | GPSD_LOG(LOG_WARN, errout, "NTRIP: Got unknown format '%s'\n", s); |
175 | 0 | } |
176 | 0 | } |
177 | | // <format-details> |
178 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
179 | | // <carrier> |
180 | 0 | if (NULL != (s = ntrip_field_iterate(NULL, s, eol, errout))) { |
181 | 0 | hold->carrier = atoi(s); |
182 | 0 | } |
183 | | // <nav-system> |
184 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
185 | | // <network> |
186 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
187 | | // <country> |
188 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
189 | | // <latitude> |
190 | 0 | hold->latitude = NAN; |
191 | 0 | if (NULL != (s = ntrip_field_iterate(NULL, s, eol, errout))) { |
192 | 0 | hold->latitude = safe_atof(s); |
193 | 0 | } |
194 | | // <longitude> |
195 | 0 | hold->longitude = NAN; |
196 | 0 | if (NULL != (s = ntrip_field_iterate(NULL, s, eol, errout))) { |
197 | 0 | hold->longitude = safe_atof(s); |
198 | 0 | } |
199 | | // <nmea> 0 == do not send GGA, 1 == send GGA |
200 | 0 | if (NULL != (s = ntrip_field_iterate(NULL, s, eol, errout))) { |
201 | 0 | hold->nmea = atoi(s); |
202 | 0 | } |
203 | | // <solution> |
204 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
205 | | // <generator> |
206 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
207 | | // <compr-encryp> |
208 | 0 | if (NULL != (s = ntrip_field_iterate(NULL, s, eol, errout))) { |
209 | |
|
210 | 0 | if (('\0' == s[0]) || |
211 | 0 | (0 == strcmp(" ", s)) || |
212 | 0 | (0 == strcasecmp("none", s))) { |
213 | 0 | hold->compr_encryp = CMP_ENC_NONE; |
214 | 0 | } else { |
215 | 0 | hold->compr_encryp = CMP_ENC_UNKNOWN; |
216 | 0 | GPSD_LOG(LOG_WARN, errout, |
217 | 0 | "NTRIP: Got unknown {compress,encrypt}ion '%s'\n", s); |
218 | 0 | } |
219 | 0 | } else { |
220 | 0 | GPSD_LOG(LOG_WARN, errout, |
221 | 0 | "NTRIP: STR missing encryption and authentication fields\n"); |
222 | 0 | return; // done |
223 | 0 | } |
224 | | // <authentication> |
225 | 0 | if (NULL != (s = ntrip_field_iterate(NULL, s, eol, errout))) { |
226 | 0 | if (0 == strcasecmp("N", s)) { |
227 | 0 | hold->authentication = AUTH_NONE; |
228 | 0 | } else if (0 == strcasecmp("B", s)) { |
229 | 0 | hold->authentication = AUTH_BASIC; |
230 | 0 | } else if (0 == strcasecmp("D", s)) { |
231 | 0 | hold->authentication = AUTH_DIGEST; |
232 | 0 | } else { |
233 | 0 | hold->authentication = AUTH_UNKNOWN; |
234 | 0 | GPSD_LOG(LOG_WARN, errout, |
235 | 0 | "NTRIP: Got unknown authenticatiion '%s'\n", s); |
236 | 0 | } |
237 | 0 | } else { |
238 | 0 | GPSD_LOG(LOG_WARN, errout, |
239 | 0 | "NTRIP: STR missing authenticatiion field\n"); |
240 | 0 | return; // done |
241 | 0 | } |
242 | | // <fee> |
243 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
244 | 0 | if (NULL == s) { |
245 | | // done, no more |
246 | 0 | return; |
247 | 0 | } |
248 | 0 | hold->fee = atoi(s); |
249 | | |
250 | | // <bitrate> |
251 | 0 | s = ntrip_field_iterate(NULL, s, eol, errout); |
252 | 0 | if (NULL == s) { |
253 | | // done, no more |
254 | 0 | return; |
255 | 0 | } |
256 | 0 | hold->bitrate = atoi(s); |
257 | | |
258 | | // ...<misc> |
259 | | // we don't care about extra fields |
260 | 0 | } |
261 | | |
262 | | /* Parse the sourcetable, looking for a match to requested stream. |
263 | | * |
264 | | * Return 1 -- found match |
265 | | * 0 -- no match, maybe more data to parse? |
266 | | * less than zero -- error |
267 | | */ |
268 | | static int ntrip_sourcetable_parse(struct gps_device_t *device) |
269 | 0 | { |
270 | 0 | struct ntrip_stream_t hold; |
271 | 0 | ssize_t llen, len = 0; |
272 | 0 | char *line; |
273 | 0 | char buf[BUFSIZ / 2]; // half of BUFSIZE, so we can GPSD_LOG() it |
274 | 0 | socket_t fd = (socket_t)device->gpsdata.gps_fd; |
275 | |
|
276 | 0 | for (;;) { |
277 | 0 | ssize_t rlen; |
278 | 0 | size_t buf_avail; |
279 | |
|
280 | 0 | if (0 > len || |
281 | 0 | sizeof(buf) <= (size_t)len) { |
282 | | // Pacify Coverity 498046, 638266, etc. |
283 | 0 | buf_avail = sizeof(buf); |
284 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
285 | 0 | "NTRIP: on fd %ld len %zd buf_aavail %zu\n", |
286 | 0 | (long)fd, len, buf_avail); |
287 | 0 | len = 0; |
288 | 0 | } else { |
289 | 0 | buf_avail = sizeof(buf) - len; |
290 | 0 | } |
291 | 0 | memset(&buf[len], 0, buf_avail); |
292 | 0 | errno = 0; // paranoia |
293 | | // read, leave room for trailing NUL |
294 | 0 | rlen = read(fd, &buf[len], buf_avail - 1); |
295 | | // cast for 32-bit ints |
296 | 0 | GPSD_LOG(LOG_RAW, &device->context->errout, |
297 | 0 | "NTRIP: on fd %ld len %zd tried %zd, got %zd\n", |
298 | 0 | (long)fd, len, sizeof(buf) - (size_t)(1 + len), rlen); |
299 | 0 | if (0 > rlen) { |
300 | 0 | if (EINTR == errno) { |
301 | 0 | continue; |
302 | 0 | } |
303 | 0 | if (device->ntrip.sourcetable_parse && |
304 | 0 | EAGAIN == errno) { |
305 | | // not found a match, but there is no more data |
306 | 0 | return 0; |
307 | 0 | } |
308 | | // cast for 32-bit ints |
309 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
310 | 0 | "NTRIP: stream read error %s(%d) on fd %ld\n", |
311 | 0 | strerror(errno), errno, (long)fd); |
312 | 0 | return -1; |
313 | 0 | } |
314 | 0 | if (0 == rlen) { // server closed the connection |
315 | | // cast for 32-bit ints |
316 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
317 | 0 | "NTRIP: stream unexpected close %s(%d) on fd %ld " |
318 | 0 | "during sourcetable read\n", |
319 | 0 | strerror(errno), errno, (long)fd); |
320 | 0 | return -2; |
321 | 0 | } |
322 | | |
323 | 0 | line = buf; |
324 | 0 | len += rlen; |
325 | 0 | rlen = len; |
326 | | // line points to the next char in buf to analyze |
327 | | // rlen is length of all data in buf |
328 | | // len is length of remaining data in buf, |
329 | |
|
330 | 0 | GPSD_LOG(LOG_IO, &device->context->errout, |
331 | 0 | "NTRIP: source table buffer >%.*s<\n", (int)rlen, buf); |
332 | |
|
333 | 0 | line[rlen] = '\0'; // pacify coverity that this is NUL terminated |
334 | |
|
335 | 0 | if (!device->ntrip.sourcetable_parse) { |
336 | | /* For ntrip v1 the very first line s/b: |
337 | | * "SOURCETABLE 200 OK\r\n" |
338 | | * For ntrip v2, the header should contain: |
339 | | * "Content-Type: gnss/sourcetable\r\n" |
340 | | */ |
341 | |
|
342 | 0 | if (str_starts_with(line, NTRIP_SOURCETABLE)) { |
343 | | // parse SOURCETABLE, NTRIP 1.0 |
344 | 0 | device->ntrip.sourcetable_parse = true; |
345 | 0 | } else if (NULL != strstr(line, NTRIP_SOURCETABLE2)) { |
346 | | // parse sourcetable, NTRIP 2.0 |
347 | 0 | device->ntrip.sourcetable_parse = true; |
348 | 0 | } else { |
349 | 0 | GPSD_LOG(LOG_WARN, &device->context->errout, |
350 | 0 | "NTRIP: Unexpected reply: %s.\n", |
351 | 0 | buf); |
352 | 0 | return -3; |
353 | 0 | } |
354 | 0 | line = strstr(line, NTRIP_BODY); |
355 | 0 | if (NULL == line) { |
356 | 0 | return -4; |
357 | 0 | } |
358 | 0 | line += 4; // point to 1st line of body |
359 | 0 | len = rlen - (line - buf); |
360 | 0 | } |
361 | | |
362 | 0 | while (0 < len) { |
363 | 0 | char *eol; |
364 | |
|
365 | 0 | if (str_starts_with(line, NTRIP_ENDSOURCETABLE)) { |
366 | | // found ENDSOURCETABLE |
367 | | // we got to the end of source table |
368 | 0 | return -5; |
369 | 0 | } |
370 | | |
371 | 0 | eol = strstr(line, NTRIP_BR); |
372 | 0 | if (NULL == eol){ |
373 | | // no full line in the buffer |
374 | 0 | break; |
375 | 0 | } |
376 | | |
377 | 0 | *eol = '\0'; |
378 | 0 | llen = (ssize_t)(eol - line); |
379 | |
|
380 | 0 | GPSD_LOG(LOG_IO, &device->context->errout, |
381 | 0 | "NTRIP: checking: >%s<\n", line); |
382 | |
|
383 | 0 | if (str_starts_with(line, NTRIP_STR)) { |
384 | | // parse STR |
385 | 0 | ntrip_str_parse(line + sizeof(NTRIP_STR) - 1, |
386 | 0 | (size_t)(llen - (sizeof(NTRIP_STR) - 1)), |
387 | 0 | &hold, &device->context->errout); |
388 | |
|
389 | 0 | if (0 == strcmp(device->ntrip.stream.mountpoint, |
390 | 0 | hold.mountpoint)) { |
391 | | // Found a match to requested stream |
392 | | |
393 | | // TODO: support for more formats. Not that we care |
394 | | // about the format. |
395 | 0 | if (FMT_UNKNOWN == hold.format) { |
396 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
397 | 0 | "NTRIP: stream %s format not supported\n", |
398 | 0 | line); |
399 | 0 | return -6; |
400 | 0 | } |
401 | | // TODO: support encryption and compression algorithms |
402 | 0 | if (CMP_ENC_NONE != hold.compr_encryp) { |
403 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
404 | 0 | "NTRIP. stream %s compression/encryption " |
405 | 0 | "algorithm not supported\n", |
406 | 0 | line); |
407 | 0 | return -7; |
408 | 0 | } |
409 | | // TODO: support digest authentication |
410 | 0 | if (AUTH_NONE != hold.authentication && |
411 | 0 | AUTH_BASIC != hold.authentication) { |
412 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
413 | 0 | "NTRIP. stream %s authentication method " |
414 | 0 | "not supported\n", |
415 | 0 | line); |
416 | 0 | return -8; |
417 | 0 | } |
418 | | // no memcpy, so we can keep the other infos |
419 | 0 | device->ntrip.stream.format = hold.format; |
420 | 0 | device->ntrip.stream.carrier = hold.carrier; |
421 | 0 | device->ntrip.stream.latitude = hold.latitude; |
422 | 0 | device->ntrip.stream.longitude = hold.longitude; |
423 | 0 | device->ntrip.stream.nmea = hold.nmea; |
424 | 0 | device->ntrip.stream.compr_encryp = hold.compr_encryp; |
425 | 0 | device->ntrip.stream.authentication = hold.authentication; |
426 | 0 | device->ntrip.stream.fee = hold.fee; |
427 | 0 | device->ntrip.stream.bitrate = hold.bitrate; |
428 | 0 | device->ntrip.stream.set = true; |
429 | 0 | return 1; |
430 | 0 | } |
431 | | /* TODO: compare stream location to own location to |
432 | | * find nearest stream if user hasn't provided one */ |
433 | 0 | } else if (str_starts_with(line, NTRIP_CAS)) { |
434 | | // TODO: parse CAS, why? |
435 | | // See: http://software.rtcm-ntrip.org/wiki/CAS |
436 | 0 | GPSD_LOG(LOG_IO, &device->context->errout, |
437 | 0 | "NTRIP: Skipping: '%s'\n", line); |
438 | 0 | } else if (str_starts_with(line, NTRIP_NET)) { |
439 | | // TODO: parse NET, why? |
440 | | // See: http://software.rtcm-ntrip.org/wiki/NET |
441 | 0 | GPSD_LOG(LOG_IO, &device->context->errout, |
442 | 0 | "NTRIP: Skipping '%s'\n", line); |
443 | 0 | } |
444 | | // else ??? |
445 | | |
446 | 0 | llen += sizeof(NTRIP_BR) - 1; |
447 | 0 | line += llen; // point to start of next line |
448 | 0 | len -= llen; // calculate remaining data in buf. |
449 | 0 | GPSD_LOG(LOG_IO, &device->context->errout, |
450 | 0 | "NTRIP: Remaining source table buffer len %zd\n", len); |
451 | 0 | } |
452 | | |
453 | 0 | GPSD_LOG(LOG_IO, &device->context->errout, |
454 | 0 | "NTRIP: Remaining source table buffer len %zd\n", len); |
455 | |
|
456 | 0 | if (0 < len) { |
457 | | // shuffle any remaining fragment to front of buf |
458 | | // line points to the last fragment in buf |
459 | 0 | memmove(buf, line, (size_t)len); |
460 | 0 | } |
461 | 0 | } |
462 | | |
463 | | // fell out of loop, no joy |
464 | 0 | return -9; |
465 | 0 | } |
466 | | |
467 | | /* Connect to NTRIP caster |
468 | | * |
469 | | * Warning: Blocking. if the host is unresponsive, this will hang forever. |
470 | | * |
471 | | * Return: fntrip_stream_get_parseile descriptor of connection |
472 | | * negative number on failure |
473 | | */ |
474 | | static int ntrip_stream_req_probe(const struct ntrip_stream_t *stream, |
475 | | struct gpsd_errout_t *errout) |
476 | 0 | { |
477 | 0 | int dsock; |
478 | 0 | ssize_t r, blen; |
479 | 0 | char buf[BUFSIZ]; |
480 | 0 | char outbuf[BUFSIZ]; |
481 | | |
482 | | // open blocking |
483 | 0 | dsock = netlib_connectsock(AF_UNSPEC, stream->host, stream->port, "tcp"); |
484 | 0 | if (0 > dsock) { |
485 | 0 | GPSD_LOG(LOG_ERROR, errout, |
486 | 0 | "NTRIP: ntrip_stream_req_probe(%s) connect error %s(%d)\n", |
487 | 0 | stream->url, netlib_errstr(dsock), dsock); |
488 | 0 | return -1; |
489 | 0 | } |
490 | 0 | blen = snprintf(buf, sizeof(buf), |
491 | 0 | "GET / HTTP/1.1\r\n" |
492 | 0 | "Ntrip-Version: Ntrip/2.0\r\n" |
493 | 0 | "User-Agent: NTRIP gpsd/%s\r\n" |
494 | 0 | "Host: %s\r\n" |
495 | 0 | "Connection: close\r\n" |
496 | 0 | "\r\n", VERSION, stream->host); |
497 | 0 | if (1 > blen) { |
498 | 0 | GPSD_LOG(LOG_ERROR, errout, |
499 | 0 | "NTRIP: ntrip_stream_req_probe(%s) snprintf() fail\n", |
500 | 0 | stream->url); |
501 | 0 | return -1; |
502 | 0 | } |
503 | | |
504 | 0 | GPSD_LOG(LOG_IO, errout, |
505 | 0 | "NTRIP: ntrip_stream_req_probe(%s) fd %d sending >%s<\n", |
506 | 0 | stream->url, dsock, |
507 | 0 | gps_visibilize(outbuf, sizeof(outbuf), buf, blen)); |
508 | |
|
509 | 0 | r = write(dsock, buf, blen); |
510 | 0 | if (blen != r) { |
511 | 0 | GPSD_LOG(LOG_ERROR, errout, |
512 | 0 | "NTRIP: stream write error %s(%d) on fd %d " |
513 | 0 | "during probe request %zd\n", |
514 | 0 | strerror(errno), errno, dsock, r); |
515 | 0 | (void)close(dsock); |
516 | 0 | return -1; |
517 | 0 | } |
518 | | // coverity[leaked_handle] This is an intentional allocation |
519 | 0 | return dsock; |
520 | 0 | } |
521 | | |
522 | | /* ntrip_auth_encode() - compute the HTTP auth string, if required. |
523 | | * |
524 | | * Return: 0 == OK |
525 | | * -1 = auth error |
526 | | */ |
527 | | static int ntrip_auth_encode(struct ntrip_stream_t *stream) |
528 | 0 | { |
529 | 0 | char authenc[64]; // base 64 encoding of auth (username:password) |
530 | 0 | int ret = 0; |
531 | |
|
532 | 0 | memset(stream->authStr, 0, sizeof(stream->authStr)); |
533 | 0 | switch (stream->authentication) { |
534 | 0 | case AUTH_NONE: |
535 | 0 | if ('\0' == *stream->credentials) { |
536 | | // nothing to do. |
537 | 0 | break; |
538 | 0 | } |
539 | | /* The user provided a user:password, but the SOURCETABLE did |
540 | | * not request AUTH_BASIC. The RTKLIB Ver 2.4.2 (2013) str2str |
541 | | * forgets to ask for basic auth even when needed. |
542 | | * So if the user gave us a u:p, send it anyway |
543 | | */ |
544 | 0 | FALLTHROUGH |
545 | 0 | case AUTH_BASIC: |
546 | | // RFC 7617 Basic Access Authentication. |
547 | | // username may not contain a colon (") |
548 | 0 | memset(authenc, 0, sizeof(authenc)); |
549 | 0 | if (0 > b64_ntop((const unsigned char *)stream->credentials, |
550 | 0 | strnlen(stream->credentials, |
551 | 0 | sizeof(stream->credentials)), |
552 | 0 | authenc, sizeof(authenc) - 1)) { |
553 | 0 | ret = -1; |
554 | 0 | break; |
555 | 0 | } |
556 | 0 | (void)snprintf(stream->authStr, sizeof(stream->authStr), |
557 | 0 | "Authorization: Basic %s\r\n", authenc); |
558 | 0 | break; |
559 | 0 | case AUTH_DIGEST: |
560 | | // TODO: support digest authentication, who needs it? |
561 | | // Possibly: RFC 2617 |
562 | | // HTTP Authentication: Basic and Digest Access Authentication) |
563 | | /* WWW-Authenticate: Digest realm="testrealm@host.com", |
564 | | * qop="auth,auth-int", |
565 | | * nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", |
566 | | * opaque="5ccc069c403ebaf9f0171e9517f40e41" |
567 | | */ |
568 | 0 | ret = -1; |
569 | 0 | break; |
570 | 0 | case AUTH_UNKNOWN: |
571 | | // WTF? |
572 | 0 | FALLTHROUGH |
573 | 0 | default: |
574 | 0 | ret = -1; |
575 | 0 | break; |
576 | 0 | } |
577 | | #if 0 |
578 | | GPSD_LOG(LOG_PROG, errout, |
579 | | "NTRIP: ntrip_auth_encode() auth %s ret %d\n", |
580 | | stream->authStr, ret); |
581 | | #endif |
582 | 0 | return ret; |
583 | 0 | } |
584 | | |
585 | | /* netlib_connectsock() open a blocking socket to host. |
586 | | * |
587 | | * Return: socket to ntrip server on success |
588 | | * less than zero on error |
589 | | */ |
590 | | static socket_t ntrip_stream_get_req(const struct ntrip_stream_t *stream, |
591 | | const struct gpsd_errout_t *errout) |
592 | 0 | { |
593 | 0 | int dsock; |
594 | 0 | char buf[BUFSIZ]; |
595 | 0 | char outbuf[BUFSIZ]; |
596 | 0 | ssize_t cnt, cnt1; |
597 | | |
598 | | // open blocking |
599 | 0 | dsock = netlib_connectsock(AF_UNSPEC, stream->host, stream->port, "tcp"); |
600 | 0 | if (BAD_SOCKET(dsock)) { |
601 | 0 | GPSD_LOG(LOG_ERROR, errout, |
602 | 0 | "NTRIP: stream connect error %ss(%d)\n", |
603 | 0 | netlib_errstr(dsock), dsock); |
604 | 0 | return -1; |
605 | 0 | } |
606 | | |
607 | 0 | GPSD_LOG(LOG_SPIN, errout, |
608 | 0 | "NTRIP: netlib_connectsock() returns socket on fd %d\n", |
609 | 0 | dsock); |
610 | |
|
611 | 0 | cnt = snprintf(buf, sizeof(buf), |
612 | 0 | "GET /%s HTTP/1.1\r\n" |
613 | 0 | "Ntrip-Version: Ntrip/2.0\r\n" |
614 | 0 | "User-Agent: NTRIP gpsd/%s\r\n" |
615 | 0 | "Host: %s\r\n" |
616 | 0 | "Accept: rtk/rtcm, dgps/rtcm\r\n" |
617 | 0 | "%s" |
618 | 0 | "Connection: close\r\n" |
619 | 0 | "\r\n", stream->mountpoint, VERSION, stream->host, |
620 | 0 | stream->authStr); |
621 | 0 | if (1 > cnt) { |
622 | 0 | GPSD_LOG(LOG_ERROR, errout, |
623 | 0 | "NTRIP: netlib_connectsock() snprintf fail<\n"); |
624 | 0 | return -1; |
625 | 0 | } |
626 | | |
627 | 0 | GPSD_LOG(LOG_IO, errout, |
628 | 0 | "NTRIP: netlib_connectsock() sending >%s<\n", |
629 | 0 | gps_visibilize(outbuf, sizeof(outbuf), buf, cnt)); |
630 | |
|
631 | 0 | cnt1 = write(dsock, buf, cnt); |
632 | 0 | if (cnt != cnt1) { |
633 | 0 | GPSD_LOG(LOG_ERROR, errout, |
634 | 0 | "NTRIP: stream write error %s(%d) on fd %d during " |
635 | 0 | "get request\n", |
636 | 0 | strerror(errno), errno, dsock); |
637 | 0 | (void)close(dsock); |
638 | 0 | return -1; |
639 | 0 | } |
640 | 0 | return dsock; |
641 | 0 | } |
642 | | |
643 | | /* lexer_getline() -- get one line, ending in \n or \0, from lexer->inbuffer, |
644 | | * put in lexer->outbuffer. NUL terminate outbuffer. |
645 | | * |
646 | | * Assume: inbufptr is correct. inbuffer is NUL terminated. |
647 | | * |
648 | | * Can not handle buffer wrap. |
649 | | * |
650 | | * Return: void |
651 | | */ |
652 | | static void lexer_getline(struct gps_lexer_t *lexer) |
653 | 0 | { |
654 | 0 | unsigned i; |
655 | |
|
656 | 0 | for (i = 0; i < sizeof(lexer->outbuffer) - 2; i++) { |
657 | 0 | unsigned char u; |
658 | |
|
659 | 0 | if (0 == lexer->inbuflen || |
660 | 0 | sizeof(lexer->inbuffer) <= lexer->inbuflen) { // paranoia |
661 | | // nothing left to read, ending not found |
662 | 0 | break; |
663 | 0 | } |
664 | 0 | u = *lexer->inbufptr++; |
665 | 0 | lexer->outbuffer[i] = u; |
666 | 0 | lexer->inbuflen--; |
667 | |
|
668 | 0 | if ('\0' == u) { |
669 | | // found NUL |
670 | 0 | break; |
671 | 0 | } |
672 | 0 | if ('\n' == u) { |
673 | | // found return |
674 | 0 | i++; |
675 | 0 | break; |
676 | 0 | } |
677 | 0 | } |
678 | 0 | lexer->outbuffer[i] = '\0'; // Ensure a NUL |
679 | 0 | lexer->outbuflen = i; |
680 | 0 | } |
681 | | |
682 | | /* ntrip_stream_get_parse(s) -- read, then parse, the stream header. |
683 | | * Assume the entire header is ready to be read, and is less than |
684 | | * 1024 bytes. |
685 | | * |
686 | | * Return: 0 == OK |
687 | | * less than zero == failure |
688 | | */ |
689 | | static int ntrip_stream_get_parse(struct gps_device_t *device) |
690 | 0 | { |
691 | 0 | char dbgbuf[128]; |
692 | 0 | int opts; |
693 | 0 | const struct ntrip_stream_t *stream = &device->ntrip.stream; |
694 | | // (int) to shut up cadacy about USE_QT. |
695 | 0 | const int dsock = (int)device->gpsdata.gps_fd; |
696 | 0 | const struct gpsd_errout_t *errout = &device->context->errout; |
697 | 0 | ssize_t read_ret; // value retuend from read() |
698 | 0 | struct gps_lexer_t *lexer = &device->lexer; |
699 | 0 | char *ibuf = (char *)lexer->inbuffer; |
700 | 0 | char *obuf = (char *)lexer->outbuffer; |
701 | 0 | bool got_header; |
702 | |
|
703 | 0 | GPSD_LOG(LOG_PROG, errout, |
704 | 0 | "NTRIP: ntrip_stream_get_parse(fd %d)\n", dsock); |
705 | 0 | lexer_init(lexer, &device->context->errout); |
706 | 0 | if (FMT_SPARTN_2 == device->ntrip.stream.format) { |
707 | | // Allow the cursed protocol |
708 | 0 | device->lexer.type_mask &= ~PACKET_TYPEMASK(SPARTN_PACKET); |
709 | 0 | } else { |
710 | 0 | device->lexer.type_mask |= PACKET_TYPEMASK(SPARTN_PACKET); |
711 | 0 | } |
712 | | |
713 | | /* We expect the header comes in as one TCP packet. |
714 | | * dsock is still blocking, so get exactly 1024 bytes */ |
715 | 0 | while (0 >= (read_ret = read(dsock, ibuf, 1024))) { |
716 | 0 | if (EINTR == errno) { |
717 | 0 | continue; |
718 | 0 | } |
719 | 0 | GPSD_LOG(LOG_ERROR, errout, |
720 | 0 | "NTRIP: stream read error %s(%d) on fd %d during get rsp\n", |
721 | 0 | strerror(errno), errno, dsock); |
722 | 0 | return -1; |
723 | 0 | } |
724 | 0 | ibuf[read_ret] = '\0'; // Make a nice NUL terminated string. |
725 | 0 | lexer->inbuflen = (size_t)read_ret; |
726 | 0 | lexer_getline(lexer); |
727 | 0 | GPSD_LOG(LOG_IO, errout, |
728 | 0 | "NTRIP: lexer_getline() >%s<\n", |
729 | 0 | gps_visibilize(dbgbuf, sizeof(dbgbuf), |
730 | 0 | (char *)lexer->outbuffer, lexer->outbuflen)); |
731 | | |
732 | | /* check for which of the 4 things we expect to start the reply: |
733 | | * |
734 | | * 401 Unauthorized\r\n -- missing or wrong authentication |
735 | | * SOURCETABLE 200 OK\r\n -- incorrect mount point requested |
736 | | * ICY 200 OK\r\n -- NTRIP v1 |
737 | | * HTTP/1.1 200 OK\r\n -- NTRIP v2 |
738 | | * |
739 | | * Anything else is not understood. |
740 | | */ |
741 | |
|
742 | 0 | if (0 == strncmp(obuf, NTRIP_UNAUTH, sizeof(NTRIP_UNAUTH))) { |
743 | 0 | GPSD_LOG(LOG_ERROR, errout, |
744 | 0 | "NTRIP: not authorized for %s\n", stream->url); |
745 | 0 | return -1; |
746 | 0 | } |
747 | | // parse "ICY 200 OK" or "HTTP/1.1 200 OK" |
748 | 0 | if (0 != strncmp(obuf, NTRIP_ICY, sizeof(NTRIP_ICY)) && |
749 | 0 | 0 != strncmp(obuf, NTRIP_HTTP, sizeof(NTRIP_HTTP))) { |
750 | 0 | GPSD_LOG(LOG_ERROR, errout, |
751 | 0 | "NTRIP: Unknown reply %s from caster: %s:%s/%s\n", obuf, |
752 | 0 | stream->host, stream->port, stream->mountpoint); |
753 | 0 | return -1; |
754 | 0 | } |
755 | | |
756 | | // first line is good. |
757 | | |
758 | | /* The NTRIP v2.0 is heavily based on HTTP/1.1, with some casters |
759 | | * also using chunked transfers, as defined by RFC 9112, chap. 7.1, |
760 | | * Chunked Transfer Coding, like so: |
761 | | * |
762 | | * HTTP/1.1 200 OK\r\n |
763 | | * [...headers...]\r\n |
764 | | * Transfer-Encoding: chunked\r\n |
765 | | * \r\n |
766 | | * 64\r\n |
767 | | * x64-bytes-worth-of-binary-message |
768 | | * \r\n |
769 | | * 27;\r\n |
770 | | * x27-bytes-worth-of-binary-message |
771 | | * \r\n |
772 | | * 42;foo=bar\r\n |
773 | | * x42-bytes-worth-of-binary-message |
774 | | * \r\n |
775 | | * |
776 | | * Annoyingly the chunks are NOT aligned on NTRIP message boundaries. |
777 | | * So one possible benefit is lost. |
778 | | * |
779 | | * http/2 removed support for chunking. Good riddance! |
780 | | * |
781 | | */ |
782 | | |
783 | 0 | got_header = false; |
784 | 0 | while (0 < lexer->inbuflen) { |
785 | 0 | lexer_getline(lexer); |
786 | 0 | GPSD_LOG(LOG_IO, errout, |
787 | 0 | "NTRIP: lexer_getline() >%s<\n", |
788 | 0 | gps_visibilize(dbgbuf, sizeof(dbgbuf), |
789 | 0 | (char *)lexer->outbuffer, lexer->outbuflen)); |
790 | | |
791 | | // Chunking needed? |
792 | 0 | if (0 == strncmp(obuf, NTRIP_CHUNKED, sizeof(NTRIP_CHUNKED))) { |
793 | 0 | GPSD_LOG(LOG_PROG, errout, |
794 | 0 | "NTRIP: caster sends chunked data\n"); |
795 | 0 | lexer->chunked = true; |
796 | 0 | } |
797 | 0 | if ('\0' == *lexer->outbuffer) { |
798 | | // done, never got end of headers. |
799 | 0 | break; |
800 | 0 | } |
801 | 0 | if (0 == strncmp(obuf, NTRIP_BR, sizeof(NTRIP_BR))) { |
802 | | // done |
803 | 0 | got_header = true; |
804 | 0 | break; |
805 | 0 | } |
806 | 0 | } |
807 | 0 | if (false == got_header) { |
808 | 0 | GPSD_LOG(LOG_WARN, errout, |
809 | 0 | "NTRIP: did not get end of headers.\n"); |
810 | | /* do something about it? If we are not chunked it'll work out |
811 | | * anyway. */ |
812 | 0 | } |
813 | |
|
814 | 0 | opts = fcntl(dsock, F_GETFL); |
815 | |
|
816 | 0 | if (-1 == opts) { |
817 | 0 | GPSD_LOG(LOG_ERROR, errout, "NTRIP: fcntl(%d) %s(%d)\n", |
818 | 0 | dsock, strerror(errno), errno); |
819 | 0 | } else { |
820 | 0 | (void)fcntl(dsock, F_SETFL, opts | O_NONBLOCK); |
821 | 0 | } |
822 | | // The excess data from this first read is now in device->lexer. |
823 | | // So far we have only seen zero here. |
824 | 0 | GPSD_LOG(LOG_IO, errout, |
825 | 0 | "NTRIP: ntrip_stream_get_parse(), %zu leftover bytes\n", |
826 | 0 | lexer->inbuflen); |
827 | 0 | if (0 == lexer->inbuflen || |
828 | 0 | sizeof(lexer->inbuffer) <= lexer->inbuflen) { // paranoia |
829 | 0 | packet_reset(lexer); |
830 | 0 | } else { |
831 | | /* The "leftover" is the start of the datastream. Chunked or |
832 | | * unchunked. */ |
833 | 0 | if (lexer->inbufptr != lexer->inbuffer) { |
834 | | // Shift inbufptr to the start. Yes, a bit brutal. |
835 | 0 | memmove(lexer->inbuffer, lexer->inbufptr, lexer->inbuflen); |
836 | 0 | lexer->inbufptr = lexer->inbuffer; |
837 | 0 | } |
838 | 0 | GPSD_LOG(LOG_IO, errout, |
839 | 0 | "NTRIP: leftover: >%s<\n", |
840 | 0 | gps_visibilize(dbgbuf, sizeof(dbgbuf), |
841 | 0 | (char *)lexer->inbuffer, lexer->inbuflen)); |
842 | 0 | } |
843 | | // empty the outbuffer of the ehader stuff |
844 | 0 | lexer->inbufptr = lexer->inbuffer; |
845 | 0 | return 0; |
846 | 0 | } |
847 | | |
848 | | /* |
849 | | * parse an ntrip:// url, "ntrip://" already stripped off |
850 | | * see tests/test_timespec.c for possible inputs and results |
851 | | * FIXME: merge with test_parse_uri_dest() |
852 | | * |
853 | | * Return 0 on success |
854 | | * less than zero on failure |
855 | | */ |
856 | | int ntrip_parse_url(const struct gpsd_errout_t *errout, |
857 | | struct ntrip_stream_t *stream, const char *fullurl) |
858 | 0 | { |
859 | 0 | char dup[256]; // working copy of url |
860 | 0 | char *at; // pointer to at |
861 | 0 | char *colon; // pointer to colon |
862 | 0 | char *slash; // pointer to slash |
863 | 0 | char *lsb; // pointer to left square bracket ([) |
864 | 0 | char *rsb; // pointer to right square bracket (]) |
865 | 0 | char *auth = NULL; // user:pass |
866 | 0 | char *host = NULL; // hostname, IPv4 or IPv6 |
867 | 0 | char *port = NULL; |
868 | 0 | char *mountpoint = NULL; // mount point |
869 | | |
870 | | // save original URL |
871 | 0 | strlcpy(stream->url, fullurl, sizeof(stream->url) - 1); |
872 | | |
873 | | // make a local copy |
874 | 0 | strlcpy(dup, fullurl, sizeof(dup) - 1); |
875 | | |
876 | | // find mountpoint, searching from right to left |
877 | 0 | if (NULL == (slash = strrchr(dup, '/'))) { |
878 | 0 | GPSD_LOG(LOG_ERROR, errout, |
879 | 0 | "NTRIP: can't extract mountpoint from url: %s\n", dup); |
880 | 0 | return -1; |
881 | 0 | } |
882 | 0 | *slash = '\0'; |
883 | 0 | mountpoint = slash + 1; |
884 | | // dup now ends in host or host:port |
885 | |
|
886 | 0 | if ('\0' == mountpoint[0]) { |
887 | | // this also handle the trailing / case |
888 | 0 | GPSD_LOG(LOG_ERROR, errout, |
889 | 0 | "NTRIP: ntrip_parse_url(%s) missing mountpoint.\n", fullurl); |
890 | 0 | return -1; |
891 | 0 | } |
892 | 0 | (void)strlcpy(stream->mountpoint, |
893 | 0 | mountpoint, sizeof(stream->mountpoint)); |
894 | | |
895 | | // dup now contains in order any of username, password, host and port |
896 | | // we know "host" has a dot (hostname or IPv4) or a ] (IPv6) |
897 | 0 | at = strrchr(dup, '@'); // user@pass |
898 | 0 | colon = strrchr(dup, ':'); // pass:host:port, pass:host, host:port |
899 | 0 | rsb = strrchr(dup, ']'); // host is IPv6 literal |
900 | 0 | lsb = strrchr(dup, '['); // host is IPv6 literal |
901 | |
|
902 | 0 | if (NULL == colon) { |
903 | | // no port (:2101), no auth (user:pass@), not IPv6 [fe80::] |
904 | 0 | port = NULL; |
905 | 0 | auth = NULL; |
906 | 0 | host = dup; |
907 | 0 | } else { |
908 | | /* have a colon, could be: |
909 | | * user@pass:host |
910 | | * user@pass:host:port |
911 | | * [fe80::] |
912 | | * [fe80::]:port |
913 | | * user:pass@[fe80::]:port |
914 | | * user:pass@[fe80::] |
915 | | */ |
916 | |
|
917 | 0 | if (NULL == at) { |
918 | | // no @, so no auth |
919 | | // host:port, [fe80::], [fe80::]:port |
920 | 0 | if (NULL == rsb || |
921 | 0 | NULL == lsb) { |
922 | | // allow one of lsb or rsb, could be in the password |
923 | | // host:port |
924 | 0 | auth = NULL; |
925 | 0 | host = dup; |
926 | 0 | *colon = '\0'; |
927 | 0 | port = colon + 1; |
928 | 0 | } else { |
929 | | // [fe80::], [fe80::]:port |
930 | 0 | auth = NULL; |
931 | 0 | host = dup + 1; |
932 | 0 | *rsb = '\0'; |
933 | 0 | if (rsb < colon) { |
934 | | // [fe80::]:port |
935 | 0 | *colon = '\0'; |
936 | 0 | port = colon + 1; |
937 | 0 | } else { |
938 | | // [fe80::] |
939 | 0 | port = NULL; |
940 | 0 | } |
941 | 0 | } |
942 | 0 | } else { |
943 | | // NULL != at, have @, so have auth |
944 | 0 | auth = dup; |
945 | 0 | if (colon < at) { |
946 | | // user:pass@host, can't be IPv6, can't have port |
947 | | // better not be a colon in the password! |
948 | 0 | *at = '\0'; |
949 | 0 | host = at + 1; |
950 | 0 | port = NULL; |
951 | 0 | } else { |
952 | | // colon > at |
953 | | // user:pass@host:port |
954 | | // user:pass@[fe80::1] |
955 | | // user:pass@[fe80::1]:2101 |
956 | 0 | *at = '\0'; |
957 | 0 | if (NULL == rsb || |
958 | 0 | NULL == lsb) { |
959 | | // user:pass@host:port |
960 | | // allow one of lsb or rsb, could be in the password |
961 | 0 | host = at + 1; |
962 | 0 | *colon = '\0'; |
963 | 0 | port = colon + 1; |
964 | 0 | } else { |
965 | | // have lsb and rsb |
966 | | // user:pass@[fe80::1] |
967 | | // user:pass@[fe80::1]:2101 |
968 | 0 | host = lsb + 1; |
969 | 0 | *rsb = '\0'; |
970 | 0 | if (rsb < colon) { |
971 | | // user:pass@[fe80::1]:2101 |
972 | 0 | port = rsb + 2; |
973 | 0 | } else { |
974 | | // user:pass@[fe80::1] |
975 | 0 | port = NULL; |
976 | 0 | } |
977 | 0 | } |
978 | 0 | } |
979 | 0 | } |
980 | 0 | } |
981 | 0 | if (NULL != auth) { |
982 | 0 | (void)strlcpy(stream->credentials, auth, sizeof(stream->credentials)); |
983 | 0 | } |
984 | |
|
985 | 0 | if (NULL == port || |
986 | 0 | '\0' == port[0]) { |
987 | 0 | port = "rtcm-sc104"; |
988 | 0 | if (NULL == getservbyname(port, "tcp")) { |
989 | | // Debian does not have rtcm-sc104 in /etc/services!! |
990 | 0 | port = DEFAULT_RTCM_PORT; |
991 | 0 | } |
992 | 0 | } |
993 | | // port ought to be non-NULL by now, but just in case appease Coverity. |
994 | 0 | if (NULL != port) { |
995 | 0 | (void)strlcpy(stream->port, port, sizeof(stream->port)); |
996 | 0 | } |
997 | | |
998 | | // host ought to be non-NULL by now, but just in case appease Coverity. |
999 | 0 | if (NULL != host) { |
1000 | 0 | (void)strlcpy(stream->host, host, sizeof(stream->host)); |
1001 | 0 | } |
1002 | |
|
1003 | 0 | GPSD_LOG(LOG_PROG, errout, |
1004 | 0 | "NTRIP: ntrip_parse_url(%s) credentials %s host %s port %s " |
1005 | 0 | "moutpoint %s\n", fullurl, |
1006 | 0 | stream->credentials, |
1007 | 0 | stream->host, |
1008 | 0 | stream->port, |
1009 | 0 | stream->mountpoint); |
1010 | 0 | return 0; |
1011 | 0 | } |
1012 | | |
1013 | | /* reopen a nonblocking connection to an NTRIP broadcaster |
1014 | | * Need to already have the sourcetable from a successful ntrip_open() |
1015 | | * |
1016 | | * Return: socket on success |
1017 | | * -1 on error |
1018 | | * PLACEHOLDING_FD (-2) on no connect |
1019 | | */ |
1020 | | static int ntrip_reconnect(struct gps_device_t *device) |
1021 | 0 | { |
1022 | 0 | #if defined(SOCK_NONBLOCK) |
1023 | 0 | socket_t dsock = -1; |
1024 | 0 | char addrbuf[50]; // INET6_ADDRSTRLEN |
1025 | |
|
1026 | 0 | GPSD_LOG(LOG_PROG, &device->context->errout, |
1027 | 0 | "NTRIP: ntrip_reconnect() %.60s\n", |
1028 | 0 | device->gpsdata.dev.path); |
1029 | 0 | dsock = netlib_connectsock1(AF_UNSPEC, device->ntrip.stream.host, |
1030 | 0 | device->ntrip.stream.port, |
1031 | 0 | "tcp", 1, false, |
1032 | 0 | addrbuf, sizeof(addrbuf)); |
1033 | 0 | device->gpsdata.gps_fd = dsock; |
1034 | | // nonblocking means we have the fd, but the connection is not |
1035 | | // finished yet. Connection may fail, later. |
1036 | 0 | if (0 > dsock) { |
1037 | | /* no way to recover from this, except wait and try again later |
1038 | | * cast for 32-bit ints. */ |
1039 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
1040 | 0 | "NTRIP: ntrip_reconnect(%s) IP %s, failed: %s(%ld)\n", |
1041 | 0 | device->gpsdata.dev.path, addrbuf, |
1042 | 0 | netlib_errstr(dsock), (long)dsock); |
1043 | | // set time for retry |
1044 | 0 | (void)clock_gettime(CLOCK_REALTIME, &device->ntrip.stream.stream_time); |
1045 | | // leave in connextion closed state for later retry. |
1046 | 0 | device->ntrip.conn_state = NTRIP_CONN_CLOSED; |
1047 | 0 | return PLACEHOLDING_FD; |
1048 | 0 | } |
1049 | | /* will have to wait for select() to confirm connection, then send |
1050 | | * the ntrip request again. |
1051 | | * cast for 32-bit ints */ |
1052 | 0 | device->ntrip.conn_state = NTRIP_CONN_INPROGRESS; |
1053 | 0 | GPSD_LOG(LOG_PROG, &device->context->errout, |
1054 | 0 | "NTRIP: ntrip_reconnect(%s) IP %s, fd %ld " |
1055 | 0 | "NTRIP_CONN_INPROGRESS \n", |
1056 | 0 | device->gpsdata.dev.path, addrbuf, (long)dsock); |
1057 | | #else // no SOCK_NONBLOCK |
1058 | | GPSD_LOG(LOG_PROG, &device->context->errout, |
1059 | | "NTRIP: ntrip_reconnect(%s) no SOCK_NONBLOCK, can't reconnect.\n", |
1060 | | device->gpsdata.dev.path); |
1061 | | device->gpsdata.gps_fd = -1; |
1062 | | #endif // no SOCK_NONBLOCK |
1063 | 0 | return (int)device->gpsdata.gps_fd; |
1064 | 0 | } |
1065 | | |
1066 | | /* open a connection to a NTRIP broadcaster |
1067 | | * orig contains full url |
1068 | | * |
1069 | | * Return: 0 on success, or the new fd |
1070 | | * less than zero on failure |
1071 | | */ |
1072 | | socket_t ntrip_open(struct gps_device_t *device, char *orig) |
1073 | 0 | { |
1074 | 0 | socket_t ret = -1; |
1075 | 0 | char buf[BUFSIZ]; |
1076 | 0 | char outbuf[BUFSIZ]; |
1077 | 0 | ssize_t blen; |
1078 | | |
1079 | | // cast for 32-bit ints |
1080 | 0 | GPSD_LOG(LOG_PROG, &device->context->errout, |
1081 | 0 | "NTRIP: ntrip_open(%s) fd %ld state = %s(%d)\n", |
1082 | 0 | orig, (long)device->gpsdata.gps_fd, |
1083 | 0 | ntrip_state(device->ntrip.conn_state), |
1084 | 0 | device->ntrip.conn_state); |
1085 | |
|
1086 | 0 | switch (device->ntrip.conn_state) { |
1087 | 0 | case NTRIP_CONN_INIT: // state = 0 |
1088 | | /* this has to be done here, |
1089 | | * because it is needed for multi-stage connection */ |
1090 | | // strlcpy() ensures dup is NUL terminated. |
1091 | 0 | device->servicetype = SERVICE_NTRIP; |
1092 | 0 | device->ntrip.works = false; |
1093 | 0 | device->ntrip.sourcetable_parse = false; |
1094 | 0 | device->ntrip.stream.set = false; |
1095 | 0 | device->gpsdata.gps_fd = PLACEHOLDING_FD; |
1096 | |
|
1097 | 0 | ret = ntrip_parse_url(&device->context->errout, &device->ntrip.stream, |
1098 | 0 | orig); |
1099 | 0 | if (0 > ret) { |
1100 | | // failed to parse url |
1101 | 0 | device->gpsdata.gps_fd = PLACEHOLDING_FD; |
1102 | 0 | device->ntrip.conn_state = NTRIP_CONN_ERR; |
1103 | 0 | return -1; |
1104 | 0 | } |
1105 | | |
1106 | 0 | ret = ntrip_stream_req_probe(&device->ntrip.stream, |
1107 | 0 | &device->context->errout); |
1108 | | // cast for 32-bit intptr_t |
1109 | 0 | GPSD_LOG(LOG_PROG, &device->context->errout, |
1110 | 0 | "NTRIP: ntrip_stream_req_probe(%s) ret %ld\n", |
1111 | 0 | device->ntrip.stream.url, (long)ret); |
1112 | 0 | if (-1 == ret) { |
1113 | 0 | device->gpsdata.gps_fd = PLACEHOLDING_FD; |
1114 | 0 | device->ntrip.conn_state = NTRIP_CONN_ERR; |
1115 | 0 | return -1; |
1116 | 0 | } |
1117 | | // set timeouts to give time for caster to reply. |
1118 | | // can't use device->lexer.pkt_time and gpsd_clear() reset it |
1119 | 0 | (void)clock_gettime(CLOCK_REALTIME, &device->ntrip.stream.stream_time); |
1120 | |
|
1121 | 0 | device->gpsdata.gps_fd = (gps_fd_t)ret; |
1122 | 0 | device->ntrip.conn_state = NTRIP_CONN_SENT_PROBE; |
1123 | 0 | return ret; |
1124 | 0 | case NTRIP_CONN_SENT_PROBE: // state = 1 |
1125 | 0 | ret = ntrip_sourcetable_parse(device); |
1126 | | // cast for 32-bit intptr_t |
1127 | 0 | GPSD_LOG(LOG_PROG, &device->context->errout, |
1128 | 0 | "NTRIP: ntrip_sourcetable_parse(%s) = %ld\n", |
1129 | 0 | device->ntrip.stream.mountpoint, (long)ret); |
1130 | 0 | if (0 > ret) { |
1131 | 0 | device->ntrip.conn_state = NTRIP_CONN_ERR; |
1132 | 0 | return -1; |
1133 | 0 | } |
1134 | 0 | if (0 == ret && |
1135 | 0 | false == device->ntrip.stream.set) { |
1136 | 0 | return ret; |
1137 | 0 | } |
1138 | 0 | if (NULL != device->gpsdata.update_fd) { |
1139 | 0 | device->gpsdata.update_fd(device->gpsdata.gps_fd, false); |
1140 | 0 | } |
1141 | 0 | (void)close(device->gpsdata.gps_fd); |
1142 | 0 | device->gpsdata.gps_fd = PLACEHOLDING_FD; |
1143 | 0 | GPSD_LOG(LOG_PROG, &device->context->errout, |
1144 | 0 | "NTRIP: found %s: %s: %d,%d,%f,%f,%d,%d,%d,%d,%d\n", |
1145 | 0 | device->ntrip.stream.url, |
1146 | 0 | device->ntrip.stream.mountpoint, |
1147 | 0 | device->ntrip.stream.format, |
1148 | 0 | device->ntrip.stream.carrier, |
1149 | 0 | device->ntrip.stream.latitude, |
1150 | 0 | device->ntrip.stream.longitude, |
1151 | 0 | device->ntrip.stream.nmea, |
1152 | 0 | device->ntrip.stream.compr_encryp, |
1153 | 0 | device->ntrip.stream.authentication, |
1154 | 0 | device->ntrip.stream.fee, |
1155 | 0 | device->ntrip.stream.bitrate); |
1156 | 0 | if (0 != ntrip_auth_encode(&device->ntrip.stream)) { |
1157 | 0 | device->ntrip.conn_state = NTRIP_CONN_ERR; |
1158 | 0 | return -1; |
1159 | 0 | } |
1160 | 0 | ret = ntrip_stream_get_req(&device->ntrip.stream, |
1161 | 0 | &device->context->errout); |
1162 | 0 | if (-1 == ret) { |
1163 | 0 | device->gpsdata.gps_fd = PLACEHOLDING_FD; |
1164 | 0 | device->ntrip.conn_state = NTRIP_CONN_ERR; |
1165 | 0 | return -1; |
1166 | 0 | } |
1167 | 0 | device->gpsdata.gps_fd = ret; |
1168 | 0 | if (NULL != device->gpsdata.update_fd) { |
1169 | 0 | device->gpsdata.update_fd(device->gpsdata.gps_fd, true); |
1170 | 0 | } |
1171 | 0 | device->ntrip.conn_state = NTRIP_CONN_SENT_GET; |
1172 | 0 | break; |
1173 | 0 | case NTRIP_CONN_SENT_GET: // state = 2 |
1174 | 0 | ret = ntrip_stream_get_parse(device); |
1175 | 0 | if (-1 == ret) { |
1176 | 0 | (void)close(device->gpsdata.gps_fd); |
1177 | 0 | device->gpsdata.gps_fd = PLACEHOLDING_FD; |
1178 | 0 | device->ntrip.conn_state = NTRIP_CONN_ERR; |
1179 | 0 | return -1; |
1180 | 0 | } |
1181 | 0 | device->ntrip.conn_state = NTRIP_CONN_ESTABLISHED; |
1182 | 0 | device->ntrip.works = true; // we know, this worked. |
1183 | 0 | break; |
1184 | 0 | case NTRIP_CONN_CLOSED: // state = 5 |
1185 | 0 | if (6 > llabs((time(NULL) - device->ntrip.stream.stream_time.tv_sec))) { |
1186 | | // wait a bit longer |
1187 | 0 | ret = PLACEHOLDING_FD; |
1188 | 0 | break; |
1189 | 0 | } |
1190 | 0 | ret = ntrip_reconnect(device); |
1191 | 0 | if (0 <= ret && |
1192 | 0 | NULL != device->gpsdata.update_fd) { |
1193 | 0 | device->gpsdata.update_fd(ret, true); |
1194 | 0 | } |
1195 | 0 | break; |
1196 | 0 | case NTRIP_CONN_INPROGRESS: // state = 6 |
1197 | | // Need to send GET within about 40 seconds or caster times out. |
1198 | | // FIXME: partially duplicates ntrip_stream_get_req() |
1199 | | // try a write, it will fail if connection still in process, or failed. |
1200 | 0 | blen = snprintf(buf, sizeof(buf), |
1201 | 0 | "GET /%s HTTP/1.1\r\n" |
1202 | 0 | "Ntrip-Version: Ntrip/2.0\r\n" |
1203 | 0 | "User-Agent: NTRIP gpsd/%s\r\n" |
1204 | 0 | "Host: %s\r\n" |
1205 | 0 | "Accept: rtk/rtcm, dgps/rtcm\r\n" |
1206 | 0 | "%s" |
1207 | 0 | "Connection: close\r\n" |
1208 | 0 | "\r\n", device->ntrip.stream.mountpoint, VERSION, |
1209 | 0 | device->ntrip.stream.host, |
1210 | 0 | device->ntrip.stream.authStr); |
1211 | |
|
1212 | 0 | if (1 > blen) { |
1213 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
1214 | 0 | "NTRIP: ntrip_open() snprintf fail<\n"); |
1215 | 0 | return -1; |
1216 | 0 | } |
1217 | | |
1218 | 0 | GPSD_LOG(LOG_IO, &device->context->errout, |
1219 | 0 | "NTRIP: ntrip_open() sending >%s<\n", |
1220 | 0 | gps_visibilize(outbuf, sizeof(outbuf), buf, blen)); |
1221 | |
|
1222 | 0 | if (blen != write(device->gpsdata.gps_fd, buf, blen)) { |
1223 | | // cast for 32-bit intptr_t |
1224 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
1225 | 0 | "NTRIP: stream write error %s(%d) on fd %ld during " |
1226 | 0 | "get request\n", |
1227 | 0 | strerror(errno), errno, (long)device->gpsdata.gps_fd); |
1228 | 0 | device->ntrip.conn_state = NTRIP_CONN_ERR; |
1229 | | // leave FD so deactivate_device() can remove from the |
1230 | | // select() loop |
1231 | 0 | } else { |
1232 | 0 | GPSD_LOG(LOG_ERROR, &device->context->errout, |
1233 | 0 | "NTRIP: stream write success get request\n"); |
1234 | 0 | device->ntrip.conn_state = NTRIP_CONN_SENT_GET; |
1235 | 0 | } |
1236 | 0 | ret = (int)device->gpsdata.gps_fd; |
1237 | 0 | break; |
1238 | 0 | case NTRIP_CONN_ESTABLISHED: // state = 3 |
1239 | 0 | FALLTHROUGH |
1240 | 0 | case NTRIP_CONN_ERR: // state = 4 |
1241 | 0 | return -1; |
1242 | 0 | } |
1243 | 0 | return ret; |
1244 | 0 | } |
1245 | | |
1246 | | // may be time to ship a GGA report to the NTRIP caster |
1247 | | void ntrip_report(struct gps_context_t *context, |
1248 | | struct gps_device_t *gps, |
1249 | | struct gps_device_t *caster) |
1250 | 0 | { |
1251 | 0 | static int count = 0; |
1252 | 0 | char buf[BUFSIZ]; |
1253 | 0 | ssize_t blen; |
1254 | |
|
1255 | 0 | if (0 == caster->ntrip.stream.nmea) { |
1256 | 0 | return; // no need to be here... |
1257 | 0 | } |
1258 | | // cast for 32-bit intptr_t |
1259 | 0 | GPSD_LOG(LOG_IO, &context->errout, |
1260 | 0 | "NTRIP: = ntrip_report() fixcnt %d count %d caster %ld\n", |
1261 | 0 | context->fixcnt, count, (long)caster->gpsdata.gps_fd); |
1262 | | |
1263 | | /* 10 is an arbitrary number, the point is to have gotten several good |
1264 | | * fixes before reporting usage to our NTRIP caster. |
1265 | | */ |
1266 | 0 | if (10 > context->fixcnt) { |
1267 | 0 | return; // no good fix to send... |
1268 | 0 | } |
1269 | | |
1270 | | /* count % 5 is as arbitrary a number as the fix dump delay. |
1271 | | * But some delay * was needed here |
1272 | | */ |
1273 | 0 | count++; |
1274 | 0 | if (0 != (count % 5)) { |
1275 | 0 | return; // wait some more |
1276 | 0 | } |
1277 | 0 | if (0 > caster->gpsdata.gps_fd) { |
1278 | 0 | return; // huh? No NTRIP fd to write to?? |
1279 | 0 | } |
1280 | | |
1281 | 0 | blen = gpsd_position_fix_dump(gps, buf, sizeof(buf)); |
1282 | 0 | if (0 < blen) { |
1283 | 0 | ssize_t ret; |
1284 | |
|
1285 | 0 | ret = write(caster->gpsdata.gps_fd, buf, blen); |
1286 | 0 | if (blen == ret) { |
1287 | 0 | GPSD_LOG(LOG_IO, &context->errout, "NTRIP: => caster %s\n", |
1288 | 0 | buf); |
1289 | 0 | } else if (0 > ret) { |
1290 | | // cast for 32-bit intptr_t |
1291 | 0 | GPSD_LOG(LOG_ERROR, &context->errout, |
1292 | 0 | "NTRIP: ntrip_report() write(%ld) error %s(%d)\n", |
1293 | 0 | (long)caster->gpsdata.gps_fd, strerror(errno), errno); |
1294 | 0 | } else { |
1295 | | // cast for 32-bit intptr_t |
1296 | 0 | GPSD_LOG(LOG_ERROR, &context->errout, |
1297 | 0 | "NTRIP: ntrip_report() short write(%ld) = %zd\n", |
1298 | 0 | (long)caster->gpsdata.gps_fd, ret); |
1299 | 0 | } |
1300 | 0 | } |
1301 | 0 | } |
1302 | | |
1303 | | // Close an ntrip connection |
1304 | | void ntrip_close(struct gps_device_t *session) |
1305 | 0 | { |
1306 | 0 | if (0 > session->gpsdata.gps_fd) { |
1307 | | // UNALLOCATED_FD (-1) or PLACEHOLDING_FD (-2). Nothing to do. |
1308 | | // cast for 32-bit intptr_t |
1309 | 0 | GPSD_LOG(LOG_ERROR, &session->context->errout, |
1310 | 0 | "NTRIP: ntrip_close(%s), close(%ld) bad fd\n", |
1311 | 0 | session->gpsdata.dev.path, (long)session->gpsdata.gps_fd); |
1312 | 0 | session->gpsdata.gps_fd = PLACEHOLDING_FD; |
1313 | 0 | return; |
1314 | 0 | } |
1315 | | |
1316 | 0 | if (-1 == close(session->gpsdata.gps_fd)) { |
1317 | | // cast for 32-bit intptr_t |
1318 | 0 | GPSD_LOG(LOG_ERROR, &session->context->errout, |
1319 | 0 | "NTRIP: ntrip_close(%s), close(%ld), %s(%d)\n", |
1320 | 0 | session->gpsdata.dev.path, |
1321 | 0 | (long)session->gpsdata.gps_fd, strerror(errno), errno); |
1322 | 0 | } else { |
1323 | | // cast for 32-bit intptr_t |
1324 | 0 | GPSD_LOG(LOG_IO, &session->context->errout, |
1325 | 0 | "NTRIP: ntrip_close(%s), close(%ld)\n", |
1326 | 0 | session->gpsdata.dev.path, |
1327 | 0 | (long)session->gpsdata.gps_fd); |
1328 | 0 | } |
1329 | | // Prepare for a retry, don't use opentime as that gets reset elsewhere |
1330 | 0 | (void)clock_gettime(CLOCK_REALTIME, &session->ntrip.stream.stream_time); |
1331 | |
|
1332 | 0 | session->gpsdata.gps_fd = PLACEHOLDING_FD; |
1333 | 0 | session->ntrip.conn_state = NTRIP_CONN_CLOSED; |
1334 | 0 | } |
1335 | | // vim: set expandtab shiftwidth=4 |