3#include <QtCore/QDateTime>
4#include <QtCore/QRegularExpression>
5#include <QtNetwork/QSslError>
6#include <QtNetwork/QSslSocket>
21 _rtcmParser.setWhitelist(whitelist);
22 qCDebug(NTRIPHttpTransportLog) <<
"RTCM message filter:" << whitelist;
23 if (whitelist.empty()) {
24 qCDebug(NTRIPHttpTransportLog) <<
"Message filter empty; all RTCM message IDs will be forwarded.";
27 _connectTimeoutTimer.setSingleShot(
true);
28 _connectTimeoutTimer.setInterval(kConnectTimeout);
29 _connectTimeoutTimer.callOnTimeout(
this, [
this]() {
30 qCWarning(NTRIPHttpTransportLog) <<
"Connection timeout";
31 emit
error(NTRIPError::ConnectionTimeout, QStringLiteral(
"Connection timeout"));
34 _dataWatchdogTimer.setSingleShot(
true);
35 _dataWatchdogTimer.setInterval(kDataWatchdog);
36 _dataWatchdogTimer.callOnTimeout(
this, [
this]() {
37 const auto secs = std::chrono::duration_cast<std::chrono::seconds>(kDataWatchdog).count();
38 qCWarning(NTRIPHttpTransportLog) <<
"No data received for" << secs <<
"seconds";
39 emit
error(NTRIPError::DataWatchdog, tr(
"No data received for %1 seconds").arg(secs));
57 _connectTimeoutTimer.stop();
58 _dataWatchdogTimer.stop();
61 _socket->disconnect(
this);
62 _socket->disconnectFromHost();
64 _socket->deleteLater();
74 QByteArray& req = result.
bytes;
76 req +=
"Host: " +
config.
host.toUtf8() +
"\r\n";
77 req +=
"Ntrip-Version: Ntrip/2.0\r\n";
78 req +=
"User-Agent: NTRIP QGroundControl/1.0\r\n";
82 const QByteArray authB64 =
84 req +=
"Authorization: Basic " + authB64 +
"\r\n";
91void NTRIPHttpTransport::_sendHttpRequest()
93 if (!_socket || _stopped) {
99 qCDebug(NTRIPHttpTransportLog) <<
"Sending HTTP request";
102 if (request.credentialsInClear) {
109 qCWarning(NTRIPHttpTransportLog) <<
"Sending credentials without TLS — data is not encrypted";
113 _socket->write(request.bytes);
115 qCDebug(NTRIPHttpTransportLog) <<
"HTTP request sent for mount:" << _config.
mountpoint;
117 qCWarning(NTRIPHttpTransportLog) <<
"No mountpoint configured, connecting without RTCM stream request";
118 _httpHandshakeDone =
true;
122 qCDebug(NTRIPHttpTransportLog) <<
"Socket connected"
123 <<
"local" << _socket->localAddress().toString() <<
":" << _socket->localPort()
124 <<
"-> peer" << _socket->peerAddress().toString() <<
":" << _socket->peerPort();
127void NTRIPHttpTransport::_failFatal(
NTRIPError code,
const QString& msg, QAbstractSocket* socket)
132 _connectTimeoutTimer.stop();
133 _dataWatchdogTimer.stop();
134 emit
error(code, msg);
138void NTRIPHttpTransport::_connect()
145 qCWarning(NTRIPHttpTransportLog) <<
"Socket already exists, aborting connect";
149 qCDebug(NTRIPHttpTransportLog) <<
"connectToHost" << _config.
host <<
":" << _config.
port
152 _httpHandshakeDone =
false;
153 _httpResponseBuf.clear();
157 QSslSocket* sslSocket =
new QSslSocket(
this);
159 connect(sslSocket, &QSslSocket::sslErrors,
this, [
this, sslSocket](
const QList<QSslError>& errors) {
161 QList<QSslError> ignorable;
163 for (
const QSslError& e : errors) {
164 qCWarning(NTRIPHttpTransportLog) <<
"TLS error:" << e.errorString();
165 msgs.append(e.errorString());
166 if (e.error() == QSslError::SelfSignedCertificate ||
167 e.error() == QSslError::SelfSignedCertificateInChain) {
174 _failFatal(NTRIPError::SslError, msgs.join(QStringLiteral(
"; ")), sslSocket);
176 qCWarning(NTRIPHttpTransportLog) <<
"Accepting self-signed certificate (user opted in)";
178 sslSocket->ignoreSslErrors(ignorable);
180 qCWarning(NTRIPHttpTransportLog)
181 <<
"Rejecting self-signed certificate (enable 'Accept self-signed certificates' to allow)";
182 _failFatal(NTRIPError::SslError,
183 tr(
"Self-signed certificate rejected. Enable 'Accept self-signed "
184 "certificates' in NTRIP settings to allow."),
189 _socket =
new QTcpSocket(
this);
191 _socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
192 _socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
193 _socket->setReadBufferSize(0);
195 connect(_socket, &QTcpSocket::errorOccurred,
this, [
this](QAbstractSocket::SocketError code) {
196 if (_stopped || !_socket) {
199 _connectTimeoutTimer.stop();
201 QString msg = _socket->errorString();
202 if (code == QAbstractSocket::RemoteHostClosedError && !_httpHandshakeDone) {
204 msg +=
" (peer closed before HTTP response; check mountpoint and credentials)";
208 qCWarning(NTRIPHttpTransportLog) <<
"Socket error code:" << int(code) <<
" msg:" << msg;
209 emit
error(NTRIPError::SocketError, msg);
212 connect(_socket, &QTcpSocket::disconnected,
this,
214 if (_stopped || !_socket) {
217 _connectTimeoutTimer.stop();
219 const QByteArray trailing = _socket->readAll();
221 if (!trailing.isEmpty()) {
222 reason = QString::fromUtf8(trailing).trimmed();
224 reason = QStringLiteral(
"Server disconnected");
227 qCWarning(NTRIPHttpTransportLog)
229 <<
"reason=" << reason <<
"ms_since_200="
230 << (_postOkTimestampMs > 0 ? QDateTime::currentMSecsSinceEpoch() - _postOkTimestampMs : -1);
231 emit
error(NTRIPError::ServerDisconnected, reason);
234 connect(_socket, &QTcpSocket::readyRead,
this, &NTRIPHttpTransport::_readBytes);
237 QSslSocket* sslSocket = qobject_cast<QSslSocket*>(_socket);
238 connect(sslSocket, &QSslSocket::encrypted,
this, [
this]() {
239 _connectTimeoutTimer.stop();
242 sslSocket->connectToHostEncrypted(_config.
host,
static_cast<quint16
>(_config.
port));
244 connect(_socket, &QTcpSocket::connected,
this, [
this]() {
245 _connectTimeoutTimer.stop();
248 _socket->connectToHost(_config.
host,
static_cast<quint16
>(_config.
port));
250 _connectTimeoutTimer.start();
253void NTRIPHttpTransport::_parseRtcm(
const QByteArray& buffer)
259 for (
char ch : buffer) {
260 const uint8_t
byte =
static_cast<uint8_t
>(
static_cast<unsigned char>(ch));
262 if (!_rtcmParser.
addByte(
byte)) {
267 qCWarning(NTRIPHttpTransportLog) <<
"RTCM CRC mismatch, dropping message id" << _rtcmParser.
messageId();
273 const uint16_t
id = _rtcmParser.
messageId();
276 qCDebug(NTRIPHttpTransportLog) <<
"RTCM packet id" <<
id <<
"len" << message.length();
279 qCDebug(NTRIPHttpTransportLog) <<
"Ignoring RTCM" << id;
286void NTRIPHttpTransport::_readBytes()
288 if (_stopped || !_socket) {
292 if (!_httpHandshakeDone) {
293 _handleHttpResponse();
297 if (!_stopped && _httpHandshakeDone && _socket && (_socket->bytesAvailable() > 0)) {
305void NTRIPHttpTransport::_handleHttpResponse()
308 const qint64 budget =
static_cast<qint64
>(
kMaxHttpHeaderSize) - _httpResponseBuf.size();
310 qCWarning(NTRIPHttpTransportLog) <<
"HTTP response header too large, dropping";
311 _httpResponseBuf.clear();
312 emit
error(NTRIPError::HeaderTooLarge, tr(
"HTTP response header too large"));
315 _httpResponseBuf.append(_socket->read(budget));
316 if (_httpResponseBuf.isEmpty()) {
324 int hdrEnd = _httpResponseBuf.indexOf(
"\r\n\r\n");
326 const int firstLineEnd = _httpResponseBuf.indexOf(
"\r\n");
327 if (firstLineEnd > 0) {
328 const QString firstLine = QString::fromUtf8(_httpResponseBuf.left(firstLineEnd));
329 if (firstLine.startsWith(QStringLiteral(
"ICY "), Qt::CaseInsensitive)) {
332 qCDebug(NTRIPHttpTransportLog) <<
"NTRIP v1 ICY response:" << firstLine;
333 _postOkTimestampMs = QDateTime::currentMSecsSinceEpoch();
334 _httpHandshakeDone =
true;
336 _dataWatchdogTimer.start();
338 const QByteArray remainingData = _httpResponseBuf.mid(firstLineEnd + 2);
339 _httpResponseBuf.clear();
340 if (!remainingData.isEmpty()) {
341 _parseRtcm(remainingData);
348 qCWarning(NTRIPHttpTransportLog) <<
"HTTP response header too large, dropping";
349 _httpResponseBuf.clear();
350 emit
error(NTRIPError::HeaderTooLarge, tr(
"HTTP response header too large"));
355 const QString header = QString::fromUtf8(_httpResponseBuf.left(hdrEnd));
356 qCDebug(NTRIPHttpTransportLog) <<
"HTTP response received:" << header.left(200);
358 const QStringList lines = header.split(
'\n');
359 for (
const QString& line : lines) {
366 qCDebug(NTRIPHttpTransportLog) <<
"HTTP" << status.code << status.reason;
367 _postOkTimestampMs = QDateTime::currentMSecsSinceEpoch();
368 _httpHandshakeDone =
true;
370 qCDebug(NTRIPHttpTransportLog) <<
"HTTP handshake complete";
373 _dataWatchdogTimer.start();
375 const QByteArray remainingData = _httpResponseBuf.mid(hdrEnd + 4);
376 _httpResponseBuf.clear();
378 if (!remainingData.isEmpty()) {
379 qCDebug(NTRIPHttpTransportLog) <<
"Processing trailing data:" << remainingData.size() <<
"bytes";
380 _parseRtcm(remainingData);
385 const QString body = QString::fromUtf8(_httpResponseBuf.mid(hdrEnd + 4)).trimmed();
386 _httpResponseBuf.clear();
388 if (status.code == 401) {
389 qCWarning(NTRIPHttpTransportLog) <<
"Authentication failed:" << status.reason;
390 emit
error(NTRIPError::AuthFailed, tr(
"Authentication failed (401): check username and password"));
394 qCWarning(NTRIPHttpTransportLog) <<
"HTTP error" << status.code << status.reason <<
"body:" << body.left(200);
395 QString msg = status.reason.isEmpty() ? tr(
"HTTP %1").arg(status.code)
396 : tr(
"HTTP %1: %2").arg(status.code).arg(status.reason);
397 if (!body.isEmpty()) {
398 QString cleanBody = body.left(500);
399 static const QRegularExpression htmlTags(QStringLiteral(
"<[^>]*>"));
400 cleanBody.remove(htmlTags);
401 cleanBody = cleanBody.simplified().left(200);
402 if (!cleanBody.isEmpty()) {
403 msg += QStringLiteral(
" — ") + cleanBody;
406 emit
error(NTRIPError::HttpError, msg);
410 qCWarning(NTRIPHttpTransportLog) <<
"No HTTP status line found in response. First line:"
411 << (lines.isEmpty() ? QStringLiteral(
"(empty)") : lines.first().left(120));
412 _httpResponseBuf.clear();
413 emit
error(NTRIPError::InvalidHttpResponse, tr(
"Invalid HTTP response from caster"));
416void NTRIPHttpTransport::_handleRtcmData()
418 const QByteArray bytes = _socket->readAll();
419 if (!bytes.isEmpty()) {
420 _dataWatchdogTimer.start();
421 qCDebug(NTRIPHttpTransportLog) <<
"rx bytes:" << bytes.size();
431 if (!_socket || _socket->state() != QAbstractSocket::ConnectedState) {
436 qCDebug(NTRIPHttpTransportLog) <<
"Sent NMEA:" << QString::fromUtf8(line.trimmed());
437 _socket->write(line);
442 static const QRegularExpression re(QStringLiteral(
"^\\S+\\s+(\\d{3})(?:\\s+(.*))?$"));
443 const QRegularExpressionMatch match = re.match(line.trimmed());
445 if (!match.hasMatch()) {
449 return HttpStatus{match.captured(1).toInt(), match.captured(2).trimmed(),
true};
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static HttpStatus parseHttpStatusLine(const QString &line)
void sendNMEA(const QByteArray &nmea) override
static HttpRequest buildHttpRequest(const NTRIPTransportConfig &config)
const NTRIPTransportConfig & config() const
~NTRIPHttpTransport() override
static bool isHttpSuccess(int code)
static constexpr int kMaxHttpHeaderSize
void plaintextCredentialsWarning()
void RTCMDataUpdate(const QByteArray &message, int messageId)
QByteArray currentFrame() const
bool isWhitelisted(uint16_t id) const
uint16_t messageId() const
bool addByte(uint8_t byte)
QByteArray repairChecksum(const QByteArray &sentence)
Repair or append a valid NMEA checksum and ensure CRLF termination.
QString createBasicAuthCredentials(const QString &username, const QString &password)
bool credentialsInClear
Credentials are present and the channel is not TLS — caller must warn.
static QVector< int > parseWhitelist(const QString &csv)
bool allowSelfSignedCerts