ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ntrip_caster.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file pc_ntrip_caster.cpp
6 * @brief NTRIP caster protocol codec - request parse + response / source-table build. See pc_ntrip_caster.h.
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_NTRIP_CASTER
13
14#include <stdio.h>
15#include <string.h>
16
17namespace
18{
19char lower(char c)
20{
21 return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
22}
23
24// Case-insensitive: does the line at [s,end) begin with prefix?
25bool ci_prefix(const char *s, const char *end, const char *prefix)
26{
27 while (*prefix)
28 {
29 if (s >= end || lower(*s) != lower(*prefix))
30 {
31 return false;
32 }
33 s++;
34 prefix++;
35 }
36 return true;
37}
38
39// Skip spaces / tabs.
40const char *skip_ws(const char *s, const char *end)
41{
42 while (s < end && (*s == ' ' || *s == '\t'))
43 {
44 s++;
45 }
46 return s;
47}
48
49// Format a degree value to 2 decimals with integer math (newlib-nano often stubs %f).
50void fmt_deg2(char *out, size_t cap, double v)
51{
52 int hundredths = (int)(v * 100.0 + (v >= 0.0 ? 0.5 : -0.5));
53 int whole = hundredths / 100;
54 int frac = hundredths % 100;
55 if (frac < 0)
56 {
57 frac = -frac;
58 }
59 const char *sign = (v < 0.0 && whole == 0) ? "-" : ""; // preserve "-0.xx"
60 pc_sb sb_out = {out, cap, 0, true};
61 pc_sb_put(&sb_out, sign);
62 pc_sb_i64(&sb_out, (int64_t)(whole));
63 pc_sb_put(&sb_out, ".");
64 pc_sb_u32w(&sb_out, (uint32_t)(frac), 2);
65 if (pc_sb_finish(&sb_out) == 0)
66 {
67 out[0] = '\0';
68 }
69}
70
71// Find the request header block terminator (CRLFCRLF, or bare LFLF fallback). On success sets *hend
72// just past the blank line and returns true; returns false if the block is incomplete (need more bytes).
73bool find_header_end(const char *buf, size_t len, size_t *hend)
74{
75 for (size_t i = 0; i < len; i++)
76 {
77 if (i + 3 < len && buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n')
78 {
79 *hend = i + 4;
80 return true;
81 }
82 if (i + 1 < len && buf[i] == '\n' && buf[i + 1] == '\n')
83 {
84 *hend = i + 2;
85 return true;
86 }
87 }
88 return false;
89}
90
91// Scan the header lines in [buf,end) for Ntrip-Version and Authorization: Basic, filling out.
92void scan_headers(const char *buf, const char *end, NtripRequest *out)
93{
94 const char *line = buf;
95 while (line < end)
96 {
97 const char *le = line;
98 // find_header_end only ever reports a block that ends ON the terminating '\n' (hend = i+4 past
99 // CRLFCRLF, or i+2 past LFLF), so end[-1] == '\n' and every line in [buf,end) is LF-terminated:
100 // the scan always stops on a newline and the `le < end` bound has no true exit to reach.
101 while (le < end && *le != '\n') // GCOVR_EXCL_LINE le < end cannot go false, see above
102 {
103 le++;
104 }
105 const char *lend = le; // exclusive; trim a trailing '\r'
106 if (lend > line && *(lend - 1) == '\r')
107 {
108 lend--;
109 }
110
111 if (ci_prefix(line, lend, "ntrip-version:"))
112 {
113 const char *v = line + 14;
114 while (v + 2 < lend && !(v[0] == '2' && v[1] == '.' && v[2] == '0'))
115 {
116 v++;
117 }
118 if (v + 2 < lend) // found "Ntrip/2.0"
119 {
120 out->version = NtripVersion::NTRIP_V2;
121 }
122 }
123 else if (ci_prefix(line, lend, "authorization:"))
124 {
125 const char *v = skip_ws(line + 14, lend);
126 if (ci_prefix(v, lend, "basic "))
127 {
128 v = skip_ws(v + 6, lend);
129 out->auth_b64 = v;
130 out->auth_b64_len = (uint16_t)(lend - v);
131 }
132 }
133 line = le + 1;
134 }
135}
136} // namespace
137
138bool pc_ntrip_request_parse(const char *buf, size_t len, NtripRequest *out)
139{
140 memset(out, 0, sizeof(*out));
141 out->version = NtripVersion::NTRIP_V1;
142
143 // Find the end of the request header block (blank line): CRLFCRLF, or bare LFLF as a fallback.
144 size_t hend = 0;
145 if (!find_header_end(buf, len, &hend))
146 {
147 return false; // need more bytes
148 }
149 out->complete = true;
150
151 const char *end = buf + hend;
152
153 // Request line: "GET <target> HTTP/1.x".
154 const char *p = buf;
155 if (!ci_prefix(p, end, "GET "))
156 {
157 out->is_get = false; // malformed / unsupported method
158 return true;
159 }
160 out->is_get = true;
161 p = skip_ws(p + 4, end);
162 const char *target = p;
163 // Same invariant as scan_headers: end[-1] is the '\n' that closed the header block, so the target
164 // scan always stops on that newline at the latest and the `p < end` bound never fires.
165 while (p < end && *p != ' ' && *p != '\r' && *p != '\n' && *p != '?') // GCOVR_EXCL_LINE see above
166 {
167 p++;
168 }
169 size_t tlen = (size_t)(p - target);
170
171 if (tlen == 0 || (tlen == 1 && target[0] == '/'))
172 {
173 out->want_sourcetable = true; // "GET /" -> list the source table
174 }
175 else
176 {
177 const char *mp = target;
178 size_t mlen = tlen;
179 if (mp[0] == '/') // strip the leading slash
180 {
181 mp++;
182 mlen--;
183 }
184 if (mlen >= sizeof(out->mountpoint))
185 {
186 mlen = sizeof(out->mountpoint) - 1;
187 }
188 memcpy(out->mountpoint, mp, mlen);
189 out->mountpoint[mlen] = '\0';
190 }
191
192 // Scan the header lines for Ntrip-Version and Authorization.
193 scan_headers(buf, end, out);
194 return true;
195}
196
197size_t pc_ntrip_build_stream_response(char *out, size_t cap, NtripVersion version)
198{
199 // One builder, branching only on which response line goes in it: the version picks the text,
200 // not a separate copy of the build-and-check.
201 pc_sb sb_out = {out, cap, 0, true};
202 pc_sb_put(&sb_out, (version == NtripVersion::NTRIP_V2)
203 ? "HTTP/1.1 200 OK\r\nNtrip-Version: Ntrip/2.0\r\nServer: PC\r\nContent-Type: "
204 "gnss/data\r\nConnection: close\r\n\r\n"
205 : "ICY 200 OK\r\n\r\n");
206 size_t n = pc_sb_finish(&sb_out);
207 if (!sb_out.ok)
208 {
209 return 0;
210 }
211 return n;
212}
213
214size_t pc_ntrip_build_error_response(char *out, size_t cap, NtripVersion version)
215{
216 int n;
217 if (version == NtripVersion::NTRIP_V2)
218 {
219 pc_sb sb_out4 = {out, cap, 0, true};
220 pc_sb_put(&sb_out4, "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
221 n = (int)pc_sb_finish(&sb_out4);
222 }
223 else
224 {
225 pc_sb sb_out5 = {out, cap, 0, true};
226 pc_sb_put(&sb_out5, "ERROR - Bad Request\r\n");
227 n = (int)pc_sb_finish(&sb_out5);
228 }
229 if (n == 0) // the frame is a fixed literal, so a zero length can only mean it did not fit
230 {
231 return 0;
232 }
233 return (size_t)n;
234}
235
236size_t pc_ntrip_build_unauthorized_response(char *out, size_t cap, NtripVersion version)
237{
238 int n;
239 if (version == NtripVersion::NTRIP_V2)
240 {
241 pc_sb sb_out6 = {out, cap, 0, true};
242 pc_sb_put(&sb_out6, "HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"NTRIP\"\r\nContent-Length: "
243 "0\r\nConnection: close\r\n\r\n");
244 n = (int)pc_sb_finish(&sb_out6);
245 }
246 else
247 {
248 pc_sb sb_out7 = {out, cap, 0, true};
249 pc_sb_put(&sb_out7, "ERROR - Bad Password\r\n");
250 n = (int)pc_sb_finish(&sb_out7);
251 }
252 if (n == 0) // the frame is a fixed literal, so a zero length can only mean it did not fit
253 {
254 return 0;
255 }
256 return (size_t)n;
257}
258
259size_t pc_ntrip_build_str_record(char *out, size_t cap, const NtripMount *m)
260{
261 if (!m || !m->mountpoint)
262 {
263 return 0;
264 }
265 char lat[16];
266 char lon[16];
267 fmt_deg2(lat, sizeof(lat), m->lat_deg);
268 fmt_deg2(lon, sizeof(lon), m->lon_deg);
269 const char *ident = m->identifier ? m->identifier : "";
270 const char *fmtd = m->format_details ? m->format_details : "1005(1)";
271 const char *nav = m->nav_system ? m->nav_system : "GPS";
272 const char *ctry = m->country ? m->country : "";
273 const char *gen = m->generator ? m->generator : "PC";
274 // STR;mount;identifier;format;format-details;carrier;nav;network;country;lat;lon;nmea;solution;
275 // generator;compr;auth;fee;bitrate;misc (carrier 0 = station reference only, no observations)
276 pc_sb sb_out8 = {out, cap, 0, true};
277 pc_sb_put(&sb_out8, "STR;");
278 pc_sb_put(&sb_out8, m->mountpoint);
279 pc_sb_put(&sb_out8, ";");
280 pc_sb_put(&sb_out8, ident);
281 pc_sb_put(&sb_out8, ";RTCM 3.3;");
282 pc_sb_put(&sb_out8, fmtd);
283 pc_sb_put(&sb_out8, ";0;");
284 pc_sb_put(&sb_out8, nav);
285 pc_sb_put(&sb_out8, ";none;");
286 pc_sb_put(&sb_out8, ctry);
287 pc_sb_put(&sb_out8, ";");
288 pc_sb_put(&sb_out8, lat);
289 pc_sb_put(&sb_out8, ";");
290 pc_sb_put(&sb_out8, lon);
291 pc_sb_put(&sb_out8, ";");
292 pc_sb_i64(&sb_out8, (int64_t)(m->nmea_required ? 1 : 0));
293 pc_sb_put(&sb_out8, ";0;");
294 pc_sb_put(&sb_out8, gen);
295 pc_sb_put(&sb_out8, ";none;N;N;9600;");
296 int n = (int)pc_sb_finish(&sb_out8);
297 if (!sb_out8.ok) // GCOVR_EXCL_LINE n<0 needs an snprintf encoding failure, see above
298 {
299 return 0;
300 }
301 return (size_t)n;
302}
303
304size_t pc_ntrip_build_sourcetable(char *out, size_t cap, NtripVersion version, const NtripMount *mounts,
305 size_t mount_count)
306{
307 static const char ENDLINE[] = "ENDSOURCETABLE\r\n";
308
309 // Pass 1: compute the body length (records + CRLFs + ENDSOURCETABLE) with a scratch record buffer.
310 size_t body_len = 0;
311 char rec[192];
312 for (size_t i = 0; i < mount_count; i++)
313 {
314 size_t rn = pc_ntrip_build_str_record(rec, sizeof(rec), &mounts[i]);
315 if (rn == 0)
316 {
317 return 0;
318 }
319 body_len += rn + 2; // + CRLF
320 }
321 body_len += sizeof(ENDLINE) - 1;
322
323 // Pass 2: header with the computed length, then the records, then ENDSOURCETABLE.
324 int hn;
325 if (version == NtripVersion::NTRIP_V2)
326 {
327 pc_sb sb_out9 = {out, cap, 0, true};
328 pc_sb_put(&sb_out9, "HTTP/1.1 200 OK\r\nNtrip-Version: Ntrip/2.0\r\nServer: PC\r\nContent-Type: "
329 "gnss/sourcetable\r\nContent-Length: ");
330 pc_sb_u32(&sb_out9, (uint32_t)((unsigned)body_len));
331 pc_sb_put(&sb_out9, "\r\nConnection: close\r\n\r\n");
332 hn = (int)pc_sb_finish(&sb_out9);
333 }
334 else
335 {
336 pc_sb sb_out10 = {out, cap, 0, true};
337 pc_sb_put(&sb_out10, "SOURCETABLE 200 OK\r\nServer: PC\r\nContent-Type: text/plain\r\nContent-Length: ");
338 pc_sb_u32(&sb_out10, (uint32_t)((unsigned)body_len));
339 pc_sb_put(&sb_out10, "\r\n\r\n");
340 hn = (int)pc_sb_finish(&sb_out10);
341 }
342 if (hn == 0) // the frame always has a literal prefix, so a zero length means it did not fit
343 {
344 return 0;
345 }
346
347 size_t pos = (size_t)hn;
348 for (size_t i = 0; i < mount_count; i++)
349 {
350 size_t rn = pc_ntrip_build_str_record(out + pos, cap - pos, &mounts[i]);
351 if (rn == 0 || pos + rn + 2 >= cap)
352 {
353 return 0;
354 }
355 pos += rn;
356 out[pos++] = '\r';
357 out[pos++] = '\n';
358 }
359 if (pos + (sizeof(ENDLINE) - 1) >= cap)
360 {
361 return 0;
362 }
363 memcpy(out + pos, ENDLINE, sizeof(ENDLINE) - 1);
364 pos += sizeof(ENDLINE) - 1;
365 out[pos] = '\0';
366 return pos;
367}
368
369#endif // PC_ENABLE_NTRIP_CASTER
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_u32w(pc_sb *b, uint32_t v, unsigned min_digits)
Append v as decimal, zero-padded to at least min_digits (printf "%0Nu").
Definition strbuf.h:291
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
Definition strbuf.h:315
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
bool ok
Definition strbuf.h:34