4#include <QtCore/QDateTime>
5#include <QtCore/QRegularExpression>
6#include <QtNetwork/QSslError>
7#include <QtNetwork/QSslSocket>
13 , _hostAddress(config.host)
15 , _username(config.username)
16 , _password(config.password)
17 , _mountpoint(config.mountpoint)
18 , _useTls(config.useTls)
20 for (
const auto& msg : config.whitelist.split(
',')) {
21 int msg_int = msg.toInt();
23 _whitelist.append(msg_int);
25 qCDebug(NTRIPHttpTransportLog) <<
"RTCM message filter:" << _whitelist;
26 if (_whitelist.empty()) {
27 qCDebug(NTRIPHttpTransportLog) <<
"Message filter empty; all RTCM message IDs will be forwarded.";
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"));
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));
60 _connectTimeoutTimer->stop();
61 _dataWatchdogTimer->stop();
64 _socket->disconnect(
this);
65 _socket->disconnectFromHost();
67 _socket->deleteLater();
74void NTRIPHttpTransport::_sendHttpRequest()
76 if (!_socket || _stopped) {
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)"));
88 qCDebug(NTRIPHttpTransportLog) <<
"Sending HTTP request";
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";
95 if (!_username.isEmpty() || !_password.isEmpty()) {
96 const QByteArray authB64 = (_username +
":" + _password).toUtf8().toBase64();
97 req +=
"Authorization: Basic " + authB64 +
"\r\n";
104 qCDebug(NTRIPHttpTransportLog) <<
"HTTP request sent for mount:" << _mountpoint;
106 _httpHandshakeDone =
true;
108 _dataWatchdogTimer->start();
111 qCDebug(NTRIPHttpTransportLog) <<
"Socket connected"
112 <<
"local" << _socket->localAddress().toString() <<
":" << _socket->localPort()
113 <<
"-> peer" << _socket->peerAddress().toString() <<
":" << _socket->peerPort();
116void NTRIPHttpTransport::_connect()
123 qCWarning(NTRIPHttpTransportLog) <<
"Socket already exists, aborting connect";
127 qCDebug(NTRIPHttpTransportLog) <<
"connectToHost" << _hostAddress <<
":" << _port <<
" mount=" << _mountpoint;
129 _httpHandshakeDone =
false;
130 _httpResponseBuf.clear();
134 QSslSocket* sslSocket =
new QSslSocket(
this);
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();
143 _socket =
new QTcpSocket(
this);
145 _socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
146 _socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
147 _socket->setReadBufferSize(0);
149 connect(_socket, &QTcpSocket::errorOccurred,
this, [
this](QAbstractSocket::SocketError code) {
150 if (_stopped || !_socket) {
153 _connectTimeoutTimer->stop();
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)";
162 qCWarning(NTRIPHttpTransportLog) <<
"Socket error code:" << int(code) <<
" msg:" << msg;
166 connect(_socket, &QTcpSocket::disconnected,
this, [
this]() {
167 if (_stopped || !_socket) {
170 _connectTimeoutTimer->stop();
172 const QByteArray trailing = _socket->readAll();
174 if (!trailing.isEmpty()) {
175 reason = QString::fromUtf8(trailing).trimmed();
177 reason = QStringLiteral(
"Server disconnected");
180 qCWarning(NTRIPHttpTransportLog) <<
"Disconnected:"
181 <<
"reason=" << reason
182 <<
"ms_since_200=" << (_postOkTimestampMs > 0 ? QDateTime::currentMSecsSinceEpoch() - _postOkTimestampMs : -1);
186 connect(_socket, &QTcpSocket::readyRead,
this, &NTRIPHttpTransport::_readBytes);
189 QSslSocket* sslSocket = qobject_cast<QSslSocket*>(_socket);
190 connect(sslSocket, &QSslSocket::encrypted,
this, [
this]() {
191 _connectTimeoutTimer->stop();
194 sslSocket->connectToHostEncrypted(_hostAddress,
static_cast<quint16
>(_port));
196 connect(_socket, &QTcpSocket::connected,
this, [
this]() {
197 _connectTimeoutTimer->stop();
200 _socket->connectToHost(_hostAddress,
static_cast<quint16
>(_port));
205void NTRIPHttpTransport::_parseRtcm(
const QByteArray& buffer)
211 for (
char ch : buffer) {
212 const uint8_t
byte =
static_cast<uint8_t
>(
static_cast<unsigned char>(ch));
214 if (!_rtcmParser.
addByte(
byte)) {
219 qCWarning(NTRIPHttpTransportLog) <<
"RTCM CRC mismatch, dropping message id" << _rtcmParser.
messageId();
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);
229 const uint8_t* crc_ptr = _rtcmParser.
crcBytes();
232 const uint16_t
id = _rtcmParser.
messageId();
234 if (_whitelist.empty() || _whitelist.contains(
id)) {
235 qCDebug(NTRIPHttpTransportLog) <<
"RTCM packet id" <<
id <<
"len" << message.length();
238 qCDebug(NTRIPHttpTransportLog) <<
"Ignoring RTCM" << id;
245void NTRIPHttpTransport::_readBytes()
247 if (_stopped || !_socket) {
251 if (!_httpHandshakeDone) {
252 _httpResponseBuf.append(_socket->readAll());
253 if (_httpResponseBuf.isEmpty()) {
257 const int hdrEnd = _httpResponseBuf.indexOf(
"\r\n\r\n");
260 qCWarning(NTRIPHttpTransportLog) <<
"HTTP response header too large, dropping";
261 _httpResponseBuf.clear();
262 emit
error(tr(
"HTTP response header too large"));
267 const QString header = QString::fromUtf8(_httpResponseBuf.left(hdrEnd));
268 qCDebug(NTRIPHttpTransportLog) <<
"HTTP response received:" << header.left(200);
270 const QStringList lines = header.split(
'\n');
271 for (
const QString& line : lines) {
278 qCDebug(NTRIPHttpTransportLog) <<
"HTTP" << status.code << status.reason;
279 _postOkTimestampMs = QDateTime::currentMSecsSinceEpoch();
280 _httpHandshakeDone =
true;
282 qCDebug(NTRIPHttpTransportLog) <<
"HTTP handshake complete";
285 _dataWatchdogTimer->start();
287 const QByteArray remainingData = _httpResponseBuf.mid(hdrEnd + 4);
288 _httpResponseBuf.clear();
290 if (!remainingData.isEmpty()) {
291 qCDebug(NTRIPHttpTransportLog) <<
"Processing trailing data:" << remainingData.size() <<
"bytes";
292 _parseRtcm(remainingData);
297 const QString body = QString::fromUtf8(_httpResponseBuf.mid(hdrEnd + 4)).trimmed();
298 _httpResponseBuf.clear();
300 if (status.code == 401) {
301 qCWarning(NTRIPHttpTransportLog) <<
"Authentication failed:" << status.reason;
302 emit
error(tr(
"Authentication failed (401): check username and password"));
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;
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"));
331 QByteArray bytes = _socket->readAll();
332 if (!bytes.isEmpty()) {
333 _dataWatchdogTimer->start();
334 qCDebug(NTRIPHttpTransportLog) <<
"rx bytes:" << bytes.size();
344 if (!_socket || _socket->state() != QAbstractSocket::ConnectedState) {
349 qCDebug(NTRIPHttpTransportLog) <<
"Sent NMEA:" << QString::fromUtf8(line.trimmed());
350 _socket->write(line);
356 static const QRegularExpression re(QStringLiteral(
"^\\S+\\s+(\\d{3})(?:\\s+(.*))?$"));
357 const QRegularExpressionMatch match = re.match(line.trimmed());
359 if (!match.hasMatch()) {
364 match.captured(1).toInt(),
365 match.captured(2).trimmed(),
372 QByteArray line = sentence;
374 if (line.size() >= 5 && line.at(0) ==
'$') {
375 int star = line.lastIndexOf(
'*');
378 for (
int i = 1; i < star; ++i) {
379 calc ^=
static_cast<quint8
>(line.at(i));
382 QByteArray calcCks = QByteArray::number(calc, 16)
383 .rightJustified(2,
'0')
386 bool needsRepair =
false;
387 if (star + 3 > line.size()) {
390 QByteArray txCks = line.mid(star + 1, 2).toUpper();
391 if (txCks != calcCks) {
397 line = line.left(star + 1) + calcCks;
401 for (
int i = 1; i < line.size(); ++i) {
402 calc ^=
static_cast<quint8
>(line.at(i));
404 QByteArray calcCks = QByteArray::number(calc, 16)
405 .rightJustified(2,
'0')
407 line.append(
'*').append(calcCks);
411 if (!line.endsWith(
"\r\n")) {
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static HttpStatus parseHttpStatusLine(const QString &line)
void sendNMEA(const QByteArray &nmea)
static constexpr int kConnectTimeoutMs
~NTRIPHttpTransport() override
void RTCMDataUpdate(const QByteArray &message)
static bool isHttpSuccess(int code)
static constexpr int kMaxHttpHeaderSize
static QByteArray repairNmeaChecksum(const QByteArray &sentence)
static constexpr int kCrcSize
const uint8_t * crcBytes() const
uint16_t messageId() const
uint16_t messageLength() const
bool addByte(uint8_t byte)