QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
NTRIPHttpSession.cc
Go to the documentation of this file.
1#include "NTRIPHttpSession.h"
2
3#include <utility>
4
5#include <QtCore/QPointer>
6#include <QtNetwork/QSslError>
7#include <QtNetwork/QSslSocket>
8#include <QtNetwork/QTcpSocket>
9
10#include "NTRIPConfiguration.h"
11#include "NTRIPTlsPolicy_p.h"
12#include "QGCLoggingCategory.h"
13#include "QGCNetworkClient.h"
14#include "RuntimeScheduler.h"
15
16QGC_LOGGING_CATEGORY(NTRIPHttpSessionLog, "GPS.NTRIP.NTRIPHttpSession")
17
19 : QObject(parent)
20 , _scheduler(RuntimeScheduler::orDefault(scheduler, this))
21 , _purpose(purpose)
22 , _errorBodyTask(_scheduler, this)
23 , _decoder(purpose)
24{}
25
27{
28 if (_socket || _ended) {
29 return {};
30 }
32 if (!request.error.isEmpty()) {
33 return request.error;
34 }
35 _request = std::move(request.bytes);
36 const auto port = static_cast<quint16>(config.port);
37 _endpoint = QStringLiteral("%1:%2").arg(config.host).arg(port);
38 _pinnedCertificate = config.pinnedCertificate;
39 _allowSelfSignedCerts = config.allowSelfSignedCerts;
40 qCDebug(NTRIPHttpSessionLog) << "connectToHost" << config.host << ":" << config.port << "TLS" << config.useTls;
41 if (!config.useTls) {
42 auto* socket = new QTcpSocket(this);
43 _attach(socket);
44 socket->connectToHost(config.host, port);
45 return {};
46 }
47 auto* socket = new QSslSocket(this);
48 // No ALPN: NTRIP 1.0 casters do not speak HTTP/1.1, and a strict ALPN server may reject an unknown protocol.
49 socket->setSslConfiguration(QGCNetworkHelper::createSslConfig());
50 _attach(socket);
51 socket->connectToHostEncrypted(config.host, port, config.host);
52 return {};
53}
54
55void NTRIPHttpSession::_attach(QTcpSocket* socket)
56{
57 socket->setParent(this);
58 _socket = socket;
59 socket->setReadBufferSize(READ_BUFFER_BYTES);
60
61 connect(socket, &QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError code) {
62 // closed() follows once the remaining data is delivered.
63 if (_ended || !_socket || code == QAbstractSocket::RemoteHostClosedError) {
64 return;
65 }
66 const QString message = _socket->errorString();
67 qCWarning(NTRIPHttpSessionLog) << "Socket error code:" << code << "msg:" << message;
68 // Like a rejected certificate, a failed handshake or a missing TLS backend recurs on every attempt.
69 const bool tls = code == QAbstractSocket::SslHandshakeFailedError || code == QAbstractSocket::SslInternalError;
70 _fail(tls ? NTRIPError::SslError : NTRIPError::SocketError, message);
71 });
72 connect(socket, &QTcpSocket::readyRead, this, &NTRIPHttpSession::_read);
73 connect(socket, &QTcpSocket::disconnected, this, &NTRIPHttpSession::_read);
74
75 auto* sslSocket = qobject_cast<QSslSocket*>(socket);
76 if (!sslSocket) {
77 connect(socket, &QTcpSocket::connected, this, &NTRIPHttpSession::_establish);
78 return;
79 }
80 connect(sslSocket, &QSslSocket::encrypted, this, &NTRIPHttpSession::_establish);
81 connect(sslSocket, &QSslSocket::sslErrors, this,
82 [this, sslSocket](const QList<QSslError>& errors) { _verifyCertificate(sslSocket, errors); });
83}
84
85void NTRIPHttpSession::_verifyCertificate(QSslSocket* socket, const QList<QSslError>& errors)
86{
87 if (_ended) {
88 return;
89 }
90 QStringList messages;
91 for (const QSslError& error : errors) {
92 qCWarning(NTRIPHttpSessionLog) << "TLS error:" << error.errorString();
93 messages.append(error.errorString());
94 }
95 const QSslCertificate certificate = socket->peerCertificate();
96 if (!NTRIPTlsPolicy::isSelfSignedOnly(errors) || certificate.isNull()) {
97 _fail(NTRIPError::SslError, messages.join(QStringLiteral("; ")));
98 return;
99 }
100 if (!_allowSelfSignedCerts) {
101 qCWarning(NTRIPHttpSessionLog)
102 << "Rejecting self-signed certificate (enable 'Accept self-signed certificates' to allow)";
103 _fail(NTRIPError::SslError, tr("Self-signed certificate rejected. Enable 'Accept self-signed "
104 "certificates' in NTRIP settings to allow."));
105 return;
106 }
107 const QString pin = NTRIPTlsPolicy::certificatePin(_endpoint, certificate);
108 if (pin == _pinnedCertificate) {
109 qCDebug(NTRIPHttpSessionLog) << "Accepting the pinned self-signed certificate";
110 } else if (_pinnedCertificate.startsWith(_endpoint + QLatin1Char('|'))) {
111 qCWarning(NTRIPHttpSessionLog) << "Rejecting self-signed certificate that differs from the pinned one";
112 _fail(NTRIPError::SslError,
113 tr("The caster's certificate changed since it was first trusted. If the change is expected, turn "
114 "'Accept self-signed certificates' off and on again to trust the new certificate."));
115 return;
116 } else {
117 qCWarning(NTRIPHttpSessionLog) << "Accepting self-signed certificate (user opted in)";
118 _newPin = pin;
119 }
120 // Only the reported self-signed errors are ignored; any other error remains fatal.
121 socket->ignoreSslErrors(errors);
122}
123
124void NTRIPHttpSession::_establish()
125{
126 if (_ended || !_socket) {
127 return;
128 }
129 qCDebug(NTRIPHttpSessionLog) << "Socket connected"
130 << "local" << _socket->localAddress().toString() << ":" << _socket->localPort()
131 << "-> peer" << _socket->peerAddress().toString() << ":" << _socket->peerPort();
132 // Qt ignores socket options set before the native socket exists.
133 _socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
134 _socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
135 if (const QString pin = std::exchange(_newPin, {}); !pin.isEmpty()) {
136 const QPointer<NTRIPHttpSession> self(this);
137 emit certificatePinned(pin);
138 if (!self || _ended || !_socket) {
139 return;
140 }
141 }
142 if (!write(_request)) {
143 _fail(NTRIPError::SocketError, tr("Socket did not accept the complete NTRIP write"));
144 return;
145 }
146 qCDebug(NTRIPHttpSessionLog) << "HTTP request sent to" << _endpoint;
147}
148
149bool NTRIPHttpSession::write(const QByteArray& bytes)
150{
151 const auto socket = _socket;
152 if (_ended || !socket) {
153 return false;
154 }
155 return socket->write(bytes) == bytes.size();
156}
157
159{
160 return !_ended && _socket && _socket->state() == QAbstractSocket::ConnectedState;
161}
162
163void NTRIPHttpSession::_read()
164{
165 const auto socket = _socket;
166 if (_ended || _reading || !socket) {
167 return;
168 }
169 const QPointer<NTRIPHttpSession> self(this);
170 _reading = true;
171 bool current = true;
172 while (current && socket->bytesAvailable() > 0) {
173 const qint64 receivedAtMs = _scheduler->nowMs();
174 const QByteArray bytes = socket->read(READ_CHUNK_BYTES);
175 if (bytes.isEmpty()) {
176 break;
177 }
178 _receive(bytes, receivedAtMs);
179 current = self && !_ended;
180 }
181 if (!self) {
182 return;
183 }
184 _reading = false;
185 if (current && socket->state() == QAbstractSocket::UnconnectedState) {
186 _finishResponse();
187 }
188}
189
190void NTRIPHttpSession::_receive(QByteArrayView bytes, qint64 receivedAtMs, const QDateTime& utcNow)
191{
192 if (!_ended) {
193 _publish(_decoder.feed(bytes, utcNow), receivedAtMs);
194 }
195}
196
197void NTRIPHttpSession::_publish(const NTRIPHttpDecoder::Result& result, qint64 receivedAtMs)
198{
199 // Observers may abort or retire this session from each signal.
200 const QPointer<NTRIPHttpSession> self(this);
201 if (result.connected) {
202 emit responseStarted();
203 if (!self || _ended) {
204 return;
205 }
206 }
207 if (!result.body.isEmpty()) {
208 emit bodyReceived(result.body, receivedAtMs);
209 if (!self || _ended) {
210 return;
211 }
212 }
213 if (result.failure) {
214 _end();
215 emit failed(*result.failure);
216 } else if (result.complete) {
217 _end();
218 emit finished();
219 } else if (_decoder.awaitingErrorBody() && !_errorBodyTask.active()) {
220 _errorBodyTask.schedule(ERROR_BODY_TIMEOUT, [this]() { _finishResponse(); });
221 }
222}
223
224void NTRIPHttpSession::_finishResponse()
225{
226 // A delivery in progress publishes the rest of the error body first.
227 if (!_ended && !_reading) {
228 _publish(_decoder.finish(), _scheduler->nowMs());
229 }
230}
231
232void NTRIPHttpSession::_fail(NTRIPError code, const QString& message)
233{
234 if (_ended) {
235 return;
236 }
237 // An HTTP error status explains the failure better than the event that cut its body short.
238 if (_decoder.awaitingErrorBody()) {
239 _publish(_decoder.finish(), _scheduler->nowMs());
240 return;
241 }
242 _end();
243 emit failed(NTRIPFailure{code, message});
244}
245
246void NTRIPHttpSession::_end()
247{
248 _ended = true;
249 _errorBodyTask.cancel();
250}
251
253{
254 _end();
255 if (const auto socket = _socket) {
256 socket->abort();
257 }
258}
259
261{
262 if (QObject* owner = parent()) {
263 disconnect(owner);
264 setParent(nullptr);
265 }
266 deleteLater();
267 abort();
268}
Config config
NTRIPHttpPurpose
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
std::array< MessageId, 4 > messages
Definition UBXPlan.cc:14
bool awaitingErrorBody() const
Result feed(QByteArrayView bytes, const QDateTime &utcNow)
static constexpr qint64 READ_CHUNK_BYTES
void certificatePinned(const QString &pin)
A newly trusted self-signed certificate, as NTRIPConnectionConfig::pinnedCertificate; precedes the re...
static constexpr qint64 READ_BUFFER_BYTES
QString open(const NTRIPConnectionConfig &config)
static constexpr std::chrono::milliseconds ERROR_BODY_TIMEOUT
bool write(const QByteArray &bytes)
False unless the socket accepted every byte; the owner decides how to report it.
void finished()
The response ended where its HTTP framing allows.
void bodyReceived(const QByteArray &body, qint64 receivedAtMs)
Body bytes, with the scheduler time at which the bytes carrying them arrived.
void responseStarted()
The caster accepted the request; its body follows.
void abort()
Ends the connection without further signals.
bool isConnected() const
void failed(const NTRIPFailure &failure)
A socket, TLS or HTTP failure, including an error status with the caster's explanation.
qint64 nowMs() const
bool schedule(std::chrono::microseconds delay, RuntimeScheduler::Callback callback)
bool active() const
QString certificatePin(const QString &endpoint, const QSslCertificate &certificate)
Binds the certificate's SHA-256 digest to one caster endpoint ("host:port").
bool isSelfSignedOnly(const QList< QSslError > &errors)
Callers must also require opt-in and ignore only the supplied error objects.
QSslConfiguration createSslConfig()
The default TLS configuration restricted to TLS 1.2 or later.
QByteArray body
Retains valid prefixes when subsequent framing fails.
std::optional< NTRIPFailure > failure
static NTRIPHttpRequest build(const NTRIPConnectionConfig &config, NTRIPHttpPurpose purpose=NTRIPHttpPurpose::Corrections)