DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 ntrip_caster.cpp
6 * @brief NTRIP caster protocol codec - request parse + response / source-table build. See ntrip_caster.h.
7 */
8
10
11#if DETWS_ENABLE_NTRIP_CASTER
12
13#include <stdio.h>
14#include <string.h>
15
16namespace
17{
18char lower(char c)
19{
20 return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
21}
22
23// Case-insensitive: does the line at [s,end) begin with prefix?
24bool ci_prefix(const char *s, const char *end, const char *prefix)
25{
26 while (*prefix)
27 {
28 if (s >= end || lower(*s) != lower(*prefix))
29 return false;
30 s++;
31 prefix++;
32 }
33 return true;
34}
35
36// Skip spaces / tabs.
37const char *skip_ws(const char *s, const char *end)
38{
39 while (s < end && (*s == ' ' || *s == '\t'))
40 s++;
41 return s;
42}
43
44// Format a degree value to 2 decimals with integer math (newlib-nano often stubs %f).
45void fmt_deg2(char *out, size_t cap, double v)
46{
47 int hundredths = (int)(v * 100.0 + (v >= 0.0 ? 0.5 : -0.5));
48 int whole = hundredths / 100;
49 int frac = hundredths % 100;
50 if (frac < 0)
51 frac = -frac;
52 const char *sign = (v < 0.0 && whole == 0) ? "-" : ""; // preserve "-0.xx"
53 snprintf(out, cap, "%s%d.%02d", sign, whole, frac);
54}
55
56// Find the request header block terminator (CRLFCRLF, or bare LFLF fallback). On success sets *hend
57// just past the blank line and returns true; returns false if the block is incomplete (need more bytes).
58bool find_header_end(const char *buf, size_t len, size_t *hend)
59{
60 for (size_t i = 0; i < len; i++)
61 {
62 if (i + 3 < len && buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n')
63 {
64 *hend = i + 4;
65 return true;
66 }
67 if (i + 1 < len && buf[i] == '\n' && buf[i + 1] == '\n')
68 {
69 *hend = i + 2;
70 return true;
71 }
72 }
73 return false;
74}
75
76// Scan the header lines in [buf,end) for Ntrip-Version and Authorization: Basic, filling out.
77void scan_headers(const char *buf, const char *end, NtripRequest *out)
78{
79 const char *line = buf;
80 while (line < end)
81 {
82 const char *le = line;
83 while (le < end && *le != '\n')
84 le++;
85 const char *lend = le; // exclusive; trim a trailing '\r'
86 if (lend > line && *(lend - 1) == '\r')
87 lend--;
88
89 if (ci_prefix(line, lend, "ntrip-version:"))
90 {
91 const char *v = line + 14;
92 while (v + 2 < lend && !(v[0] == '2' && v[1] == '.' && v[2] == '0'))
93 v++;
94 if (v + 2 < lend) // found "Ntrip/2.0"
95 out->version = NtripVersion::NTRIP_V2;
96 }
97 else if (ci_prefix(line, lend, "authorization:"))
98 {
99 const char *v = skip_ws(line + 14, lend);
100 if (ci_prefix(v, lend, "basic "))
101 {
102 v = skip_ws(v + 6, lend);
103 out->auth_b64 = v;
104 out->auth_b64_len = (uint16_t)(lend - v);
105 }
106 }
107 line = le + 1;
108 }
109}
110} // namespace
111
112bool ntrip_request_parse(const char *buf, size_t len, NtripRequest *out)
113{
114 memset(out, 0, sizeof(*out));
115 out->version = NtripVersion::NTRIP_V1;
116
117 // Find the end of the request header block (blank line): CRLFCRLF, or bare LFLF as a fallback.
118 size_t hend = 0;
119 if (!find_header_end(buf, len, &hend))
120 return false; // need more bytes
121 out->complete = true;
122
123 const char *end = buf + hend;
124
125 // Request line: "GET <target> HTTP/1.x".
126 const char *p = buf;
127 if (!ci_prefix(p, end, "GET "))
128 {
129 out->is_get = false; // malformed / unsupported method
130 return true;
131 }
132 out->is_get = true;
133 p = skip_ws(p + 4, end);
134 const char *target = p;
135 while (p < end && *p != ' ' && *p != '\r' && *p != '\n' && *p != '?')
136 p++;
137 size_t tlen = (size_t)(p - target);
138
139 if (tlen == 0 || (tlen == 1 && target[0] == '/'))
140 {
141 out->want_sourcetable = true; // "GET /" -> list the source table
142 }
143 else
144 {
145 const char *mp = target;
146 size_t mlen = tlen;
147 if (mp[0] == '/') // strip the leading slash
148 {
149 mp++;
150 mlen--;
151 }
152 if (mlen >= sizeof(out->mountpoint))
153 mlen = sizeof(out->mountpoint) - 1;
154 memcpy(out->mountpoint, mp, mlen);
155 out->mountpoint[mlen] = '\0';
156 }
157
158 // Scan the header lines for Ntrip-Version and Authorization.
159 scan_headers(buf, end, out);
160 return true;
161}
162
163size_t ntrip_build_stream_response(char *out, size_t cap, NtripVersion version)
164{
165 int n;
166 if (version == NtripVersion::NTRIP_V2)
167 n = snprintf(out, cap,
168 "HTTP/1.1 200 OK\r\n"
169 "Ntrip-Version: Ntrip/2.0\r\n"
170 "Server: DetWebServer\r\n"
171 "Content-Type: gnss/data\r\n"
172 "Connection: close\r\n\r\n");
173 else
174 n = snprintf(out, cap, "ICY 200 OK\r\n\r\n");
175 if (n < 0 || (size_t)n >= cap)
176 return 0;
177 return (size_t)n;
178}
179
180size_t ntrip_build_error_response(char *out, size_t cap, NtripVersion version)
181{
182 int n;
183 if (version == NtripVersion::NTRIP_V2)
184 n = snprintf(out, cap, "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
185 else
186 n = snprintf(out, cap, "ERROR - Bad Request\r\n");
187 if (n < 0 || (size_t)n >= cap)
188 return 0;
189 return (size_t)n;
190}
191
192size_t ntrip_build_unauthorized_response(char *out, size_t cap, NtripVersion version)
193{
194 int n;
195 if (version == NtripVersion::NTRIP_V2)
196 n = snprintf(out, cap,
197 "HTTP/1.1 401 Unauthorized\r\n"
198 "WWW-Authenticate: Basic realm=\"NTRIP\"\r\n"
199 "Content-Length: 0\r\n"
200 "Connection: close\r\n\r\n");
201 else
202 n = snprintf(out, cap, "ERROR - Bad Password\r\n");
203 if (n < 0 || (size_t)n >= cap)
204 return 0;
205 return (size_t)n;
206}
207
208size_t ntrip_build_str_record(char *out, size_t cap, const NtripMount *m)
209{
210 if (!m || !m->mountpoint)
211 return 0;
212 char lat[16];
213 char lon[16];
214 fmt_deg2(lat, sizeof(lat), m->lat_deg);
215 fmt_deg2(lon, sizeof(lon), m->lon_deg);
216 const char *ident = m->identifier ? m->identifier : "";
217 const char *fmtd = m->format_details ? m->format_details : "1005(1)";
218 const char *nav = m->nav_system ? m->nav_system : "GPS";
219 const char *ctry = m->country ? m->country : "";
220 const char *gen = m->generator ? m->generator : "DetWebServer";
221 // STR;mount;identifier;format;format-details;carrier;nav;network;country;lat;lon;nmea;solution;
222 // generator;compr;auth;fee;bitrate;misc (carrier 0 = station reference only, no observations)
223 int n = snprintf(out, cap, "STR;%s;%s;RTCM 3.3;%s;0;%s;none;%s;%s;%s;%d;0;%s;none;N;N;9600;", m->mountpoint, ident,
224 fmtd, nav, ctry, lat, lon, m->nmea_required ? 1 : 0, gen);
225 if (n < 0 || (size_t)n >= cap)
226 return 0;
227 return (size_t)n;
228}
229
230size_t ntrip_build_sourcetable(char *out, size_t cap, NtripVersion version, const NtripMount *mounts,
231 size_t mount_count)
232{
233 static const char ENDLINE[] = "ENDSOURCETABLE\r\n";
234
235 // Pass 1: compute the body length (records + CRLFs + ENDSOURCETABLE) with a scratch record buffer.
236 size_t body_len = 0;
237 char rec[192];
238 for (size_t i = 0; i < mount_count; i++)
239 {
240 size_t rn = ntrip_build_str_record(rec, sizeof(rec), &mounts[i]);
241 if (rn == 0)
242 return 0;
243 body_len += rn + 2; // + CRLF
244 }
245 body_len += sizeof(ENDLINE) - 1;
246
247 // Pass 2: header with the computed length, then the records, then ENDSOURCETABLE.
248 int hn;
249 if (version == NtripVersion::NTRIP_V2)
250 hn = snprintf(out, cap,
251 "HTTP/1.1 200 OK\r\n"
252 "Ntrip-Version: Ntrip/2.0\r\n"
253 "Server: DetWebServer\r\n"
254 "Content-Type: gnss/sourcetable\r\n"
255 "Content-Length: %u\r\n"
256 "Connection: close\r\n\r\n",
257 (unsigned)body_len);
258 else
259 hn = snprintf(out, cap,
260 "SOURCETABLE 200 OK\r\n"
261 "Server: DetWebServer\r\n"
262 "Content-Type: text/plain\r\n"
263 "Content-Length: %u\r\n\r\n",
264 (unsigned)body_len);
265 if (hn < 0 || (size_t)hn >= cap)
266 return 0;
267
268 size_t pos = (size_t)hn;
269 for (size_t i = 0; i < mount_count; i++)
270 {
271 size_t rn = ntrip_build_str_record(out + pos, cap - pos, &mounts[i]);
272 if (rn == 0 || pos + rn + 2 >= cap)
273 return 0;
274 pos += rn;
275 out[pos++] = '\r';
276 out[pos++] = '\n';
277 }
278 if (pos + (sizeof(ENDLINE) - 1) >= cap)
279 return 0;
280 memcpy(out + pos, ENDLINE, sizeof(ENDLINE) - 1);
281 pos += sizeof(ENDLINE) - 1;
282 out[pos] = '\0';
283 return pos;
284}
285
286#endif // DETWS_ENABLE_NTRIP_CASTER
NTRIP caster protocol codec (DETWS_ENABLE_NTRIP_CASTER) - the pure, host-tested core.