QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
NTRIPHttpTransport.cc
Go to the documentation of this file.
3
4#include <QtCore/QDateTime>
5#include <QtCore/QRegularExpression>
6#include <QtNetwork/QSslError>
7#include <QtNetwork/QSslSocket>
8
9QGC_LOGGING_CATEGORY(NTRIPHttpTransportLog, "GPS.NTRIPHttpTransport")
10
12 : QObject(parent)
13 , _hostAddress(config.host)
14 , _port(config.port)
15 , _username(config.username)
16 , _password(config.password)
17 , _mountpoint(config.mountpoint)
18 , _useTls(config.useTls)
19{
20 for (const auto& msg : config.whitelist.split(',')) {
21 int msg_int = msg.toInt();
22 if (msg_int)
23 _whitelist.append(msg_int);
24 }
25 qCDebug(NTRIPHttpTransportLog) << "RTCM message filter:" << _whitelist;
26 if (_whitelist.empty()) {
27 qCDebug(NTRIPHttpTransportLog) << "Message filter empty; all RTCM message IDs will be forwarded.";
28 }
29
30 _connectTimeoutTimer = new QTimer(this);
31 _connectTimeoutTimer->setSingleShot(true);
32 connect(_connectTimeoutTimer, &QTimer::timeout, this, [this]() {
33 qCWarning(NTRIPHttpTransportLog) << "Connection timeout";
34 emit error(QStringLiteral("Connection timeout"));
35 });
36
37 _dataWatchdogTimer = new QTimer(this);
38 _dataWatchdogTimer->setSingleShot(true);
39 _dataWatchdogTimer->setInterval(kDataWatchdogMs);
40 connect(_dataWatchdogTimer, &QTimer::timeout, this, [this]() {
41 qCWarning(NTRIPHttpTransportLog) << "No data received for" << kDataWatchdogMs / 1000 << "seconds";
42 emit error(tr("No data received for %1 seconds").arg(kDataWatchdogMs / 1000));
43 });
44}
45
50
52{
53 _stopped = false;
54 _connect();
55}
56
58{
59 _stopped = true;
60 _connectTimeoutTimer->stop();
61 _dataWatchdogTimer->stop();
62
63 if (_socket) {
64 _socket->disconnect(this);
65 _socket->disconnectFromHost();
66 _socket->close();
67 _socket->deleteLater();
68 _socket = nullptr;
69 }
70
71 emit finished();
72}
73
74void NTRIPHttpTransport::_sendHttpRequest()
75{
76 if (!_socket || _stopped) {
77 return;
78 }
79
80 if (!_mountpoint.isEmpty()) {
81 static const QRegularExpression controlChars(QStringLiteral("[\\r\\n\\x00-\\x1f]"));
82 if (_mountpoint.contains(controlChars)) {
83 qCWarning(NTRIPHttpTransportLog) << "Mountpoint contains control characters, rejecting";
84 emit error(tr("Invalid mountpoint name (contains control characters)"));
85 return;
86 }
87
88 qCDebug(NTRIPHttpTransportLog) << "Sending HTTP request";
89 QByteArray req;
90 req += "GET /" + _mountpoint.toUtf8() + " HTTP/1.0\r\n";
91 req += "Host: " + _hostAddress.toUtf8() + "\r\n";
92 req += "Ntrip-Version: Ntrip/2.0\r\n";
93 req += "User-Agent: NTRIP QGroundControl/1.0\r\n";
94
95 if (!_username.isEmpty() || !_password.isEmpty()) {
96 const QByteArray authB64 = (_username + ":" + _password).toUtf8().toBase64();
97 req += "Authorization: Basic " + authB64 + "\r\n";
98 }
99
100 req += "\r\n";
101 _socket->write(req);
102 _socket->flush();
103
104 qCDebug(NTRIPHttpTransportLog) << "HTTP request sent for mount:" << _mountpoint;
105 } else {
106 _httpHandshakeDone = true;
107 emit connected();
108 _dataWatchdogTimer->start();
109 }
110
111 qCDebug(NTRIPHttpTransportLog) << "Socket connected"
112 << "local" << _socket->localAddress().toString() << ":" << _socket->localPort()
113 << "-> peer" << _socket->peerAddress().toString() << ":" << _socket->peerPort();
114}
115
116void NTRIPHttpTransport::_connect()
117{
118 if (_stopped) {
119 return;
120 }
121
122 if (_socket) {
123 qCWarning(NTRIPHttpTransportLog) << "Socket already exists, aborting connect";
124 return;
125 }
126
127 qCDebug(NTRIPHttpTransportLog) << "connectToHost" << _hostAddress << ":" << _port << " mount=" << _mountpoint;
128
129 _httpHandshakeDone = false;
130 _httpResponseBuf.clear();
131 _rtcmParser.reset();
132
133 if (_useTls) {
134 QSslSocket* sslSocket = new QSslSocket(this);
135 _socket = sslSocket;
136 connect(sslSocket, QOverload<const QList<QSslError>&>::of(&QSslSocket::sslErrors),
137 this, [](const QList<QSslError>& errors) {
138 for (const QSslError& e : errors) {
139 qCWarning(NTRIPHttpTransportLog) << "TLS error:" << e.errorString();
140 }
141 });
142 } else {
143 _socket = new QTcpSocket(this);
144 }
145 _socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
146 _socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
147 _socket->setReadBufferSize(0);
148
149 connect(_socket, &QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError code) {
150 if (_stopped || !_socket) {
151 return;
152 }
153 _connectTimeoutTimer->stop();
154
155 QString msg = _socket->errorString();
156 if (code == QAbstractSocket::RemoteHostClosedError && !_httpHandshakeDone) {
157 if (!_mountpoint.isEmpty()) {
158 msg += " (peer closed before HTTP response; check mountpoint and credentials)";
159 }
160 }
161
162 qCWarning(NTRIPHttpTransportLog) << "Socket error code:" << int(code) << " msg:" << msg;
163 emit error(msg);
164 });
165
166 connect(_socket, &QTcpSocket::disconnected, this, [this]() {
167 if (_stopped || !_socket) {
168 return;
169 }
170 _connectTimeoutTimer->stop();
171
172 const QByteArray trailing = _socket->readAll();
173 QString reason;
174 if (!trailing.isEmpty()) {
175 reason = QString::fromUtf8(trailing).trimmed();
176 } else {
177 reason = QStringLiteral("Server disconnected");
178 }
179
180 qCWarning(NTRIPHttpTransportLog) << "Disconnected:"
181 << "reason=" << reason
182 << "ms_since_200=" << (_postOkTimestampMs > 0 ? QDateTime::currentMSecsSinceEpoch() - _postOkTimestampMs : -1);
183 emit error(reason);
184 });
185
186 connect(_socket, &QTcpSocket::readyRead, this, &NTRIPHttpTransport::_readBytes);
187
188 if (_useTls) {
189 QSslSocket* sslSocket = qobject_cast<QSslSocket*>(_socket);
190 connect(sslSocket, &QSslSocket::encrypted, this, [this]() {
191 _connectTimeoutTimer->stop();
192 _sendHttpRequest();
193 });
194 sslSocket->connectToHostEncrypted(_hostAddress, static_cast<quint16>(_port));
195 } else {
196 connect(_socket, &QTcpSocket::connected, this, [this]() {
197 _connectTimeoutTimer->stop();
198 _sendHttpRequest();
199 });
200 _socket->connectToHost(_hostAddress, static_cast<quint16>(_port));
201 }
202 _connectTimeoutTimer->start(kConnectTimeoutMs);
203}
204
205void NTRIPHttpTransport::_parseRtcm(const QByteArray& buffer)
206{
207 if (_stopped) {
208 return;
209 }
210
211 for (char ch : buffer) {
212 const uint8_t byte = static_cast<uint8_t>(static_cast<unsigned char>(ch));
213
214 if (!_rtcmParser.addByte(byte)) {
215 continue;
216 }
217
218 if (!_rtcmParser.validateCrc()) {
219 qCWarning(NTRIPHttpTransportLog) << "RTCM CRC mismatch, dropping message id" << _rtcmParser.messageId();
220 _rtcmParser.reset();
221 continue;
222 }
223
224 constexpr int kRtcmHeaderSize = 3;
225 const int payload_len = static_cast<int>(_rtcmParser.messageLength());
226 QByteArray message(reinterpret_cast<const char*>(_rtcmParser.message()),
227 kRtcmHeaderSize + payload_len);
228
229 const uint8_t* crc_ptr = _rtcmParser.crcBytes();
230 message.append(reinterpret_cast<const char*>(crc_ptr), RTCMParser::kCrcSize);
231
232 const uint16_t id = _rtcmParser.messageId();
233
234 if (_whitelist.empty() || _whitelist.contains(id)) {
235 qCDebug(NTRIPHttpTransportLog) << "RTCM packet id" << id << "len" << message.length();
236 emit RTCMDataUpdate(message);
237 } else {
238 qCDebug(NTRIPHttpTransportLog) << "Ignoring RTCM" << id;
239 }
240
241 _rtcmParser.reset();
242 }
243}
244
245void NTRIPHttpTransport::_readBytes()
246{
247 if (_stopped || !_socket) {
248 return;
249 }
250
251 if (!_httpHandshakeDone) {
252 _httpResponseBuf.append(_socket->readAll());
253 if (_httpResponseBuf.isEmpty()) {
254 return;
255 }
256
257 const int hdrEnd = _httpResponseBuf.indexOf("\r\n\r\n");
258 if (hdrEnd < 0) {
259 if (_httpResponseBuf.size() > kMaxHttpHeaderSize) {
260 qCWarning(NTRIPHttpTransportLog) << "HTTP response header too large, dropping";
261 _httpResponseBuf.clear();
262 emit error(tr("HTTP response header too large"));
263 }
264 return;
265 }
266
267 const QString header = QString::fromUtf8(_httpResponseBuf.left(hdrEnd));
268 qCDebug(NTRIPHttpTransportLog) << "HTTP response received:" << header.left(200);
269
270 const QStringList lines = header.split('\n');
271 for (const QString& line : lines) {
272 const HttpStatus status = parseHttpStatusLine(line);
273 if (!status.valid) {
274 continue;
275 }
276
277 if (isHttpSuccess(status.code)) {
278 qCDebug(NTRIPHttpTransportLog) << "HTTP" << status.code << status.reason;
279 _postOkTimestampMs = QDateTime::currentMSecsSinceEpoch();
280 _httpHandshakeDone = true;
281
282 qCDebug(NTRIPHttpTransportLog) << "HTTP handshake complete";
283 emit connected();
284
285 _dataWatchdogTimer->start();
286
287 const QByteArray remainingData = _httpResponseBuf.mid(hdrEnd + 4);
288 _httpResponseBuf.clear();
289
290 if (!remainingData.isEmpty()) {
291 qCDebug(NTRIPHttpTransportLog) << "Processing trailing data:" << remainingData.size() << "bytes";
292 _parseRtcm(remainingData);
293 }
294 return;
295 }
296
297 const QString body = QString::fromUtf8(_httpResponseBuf.mid(hdrEnd + 4)).trimmed();
298 _httpResponseBuf.clear();
299
300 if (status.code == 401) {
301 qCWarning(NTRIPHttpTransportLog) << "Authentication failed:" << status.reason;
302 emit error(tr("Authentication failed (401): check username and password"));
303 return;
304 }
305
306 qCWarning(NTRIPHttpTransportLog) << "HTTP error" << status.code << status.reason
307 << "body:" << body.left(200);
308 QString msg = status.reason.isEmpty()
309 ? tr("HTTP %1").arg(status.code)
310 : tr("HTTP %1: %2").arg(status.code).arg(status.reason);
311 if (!body.isEmpty()) {
312 QString cleanBody = body.left(500);
313 static const QRegularExpression htmlTags(QStringLiteral("<[^>]*>"));
314 cleanBody.remove(htmlTags);
315 cleanBody = cleanBody.simplified().left(200);
316 if (!cleanBody.isEmpty()) {
317 msg += QStringLiteral(" — ") + cleanBody;
318 }
319 }
320 emit error(msg);
321 return;
322 }
323
324 qCWarning(NTRIPHttpTransportLog) << "No HTTP status line found in response. First line:"
325 << (lines.isEmpty() ? QStringLiteral("(empty)") : lines.first().left(120));
326 _httpResponseBuf.clear();
327 emit error(tr("Invalid HTTP response from caster"));
328 return;
329 }
330
331 QByteArray bytes = _socket->readAll();
332 if (!bytes.isEmpty()) {
333 _dataWatchdogTimer->start();
334 qCDebug(NTRIPHttpTransportLog) << "rx bytes:" << bytes.size();
335 _parseRtcm(bytes);
336 }
337}
338
339void NTRIPHttpTransport::sendNMEA(const QByteArray& nmea)
340{
341 if (_stopped) {
342 return;
343 }
344 if (!_socket || _socket->state() != QAbstractSocket::ConnectedState) {
345 return;
346 }
347
348 const QByteArray line = repairNmeaChecksum(nmea);
349 qCDebug(NTRIPHttpTransportLog) << "Sent NMEA:" << QString::fromUtf8(line.trimmed());
350 _socket->write(line);
351 _socket->flush();
352}
353
355{
356 static const QRegularExpression re(QStringLiteral("^\\S+\\s+(\\d{3})(?:\\s+(.*))?$"));
357 const QRegularExpressionMatch match = re.match(line.trimmed());
358
359 if (!match.hasMatch()) {
360 return HttpStatus{0, {}, false};
361 }
362
363 return HttpStatus{
364 match.captured(1).toInt(),
365 match.captured(2).trimmed(),
366 true
367 };
368}
369
370QByteArray NTRIPHttpTransport::repairNmeaChecksum(const QByteArray& sentence)
371{
372 QByteArray line = sentence;
373
374 if (line.size() >= 5 && line.at(0) == '$') {
375 int star = line.lastIndexOf('*');
376 if (star > 1) {
377 quint8 calc = 0;
378 for (int i = 1; i < star; ++i) {
379 calc ^= static_cast<quint8>(line.at(i));
380 }
381
382 QByteArray calcCks = QByteArray::number(calc, 16)
383 .rightJustified(2, '0')
384 .toUpper();
385
386 bool needsRepair = false;
387 if (star + 3 > line.size()) {
388 needsRepair = true;
389 } else {
390 QByteArray txCks = line.mid(star + 1, 2).toUpper();
391 if (txCks != calcCks) {
392 needsRepair = true;
393 }
394 }
395
396 if (needsRepair) {
397 line = line.left(star + 1) + calcCks;
398 }
399 } else {
400 quint8 calc = 0;
401 for (int i = 1; i < line.size(); ++i) {
402 calc ^= static_cast<quint8>(line.at(i));
403 }
404 QByteArray calcCks = QByteArray::number(calc, 16)
405 .rightJustified(2, '0')
406 .toUpper();
407 line.append('*').append(calcCks);
408 }
409 }
410
411 if (!line.endsWith("\r\n")) {
412 line.append("\r\n");
413 }
414
415 return line;
416}
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static HttpStatus parseHttpStatusLine(const QString &line)
void sendNMEA(const QByteArray &nmea)
static constexpr int kConnectTimeoutMs
void RTCMDataUpdate(const QByteArray &message)
static bool isHttpSuccess(int code)
static constexpr int kMaxHttpHeaderSize
static QByteArray repairNmeaChecksum(const QByteArray &sentence)
void reset()
Definition RTCMParser.cc:8
bool validateCrc() const
Definition RTCMParser.cc:86
static constexpr int kCrcSize
Definition RTCMParser.h:18
uint8_t * message()
Definition RTCMParser.h:14
const uint8_t * crcBytes() const
Definition RTCMParser.h:17
uint16_t messageId() const
Definition RTCMParser.cc:62
uint16_t messageLength() const
Definition RTCMParser.h:15
bool addByte(uint8_t byte)
Definition RTCMParser.cc:17