6#include <QtCore/QCoreApplication>
7#include <QtCore/QRegularExpression>
16 return (ch >=
'a' && ch <=
'z') || (ch >=
'A' && ch <=
'Z') || (ch >=
'0' && ch <=
'9') ||
17 QByteArrayView(
"!#$%&'*+-.^_`|~").contains(ch);
20bool isFieldValue(
char ch)
22 const auto byte =
static_cast<unsigned char>(ch);
23 return ch ==
'\t' || (
byte >= 32 &&
byte != 127);
27std::optional<quint64> unsignedNumber(QByteArrayView text,
int base = 10)
29 const auto digit = [
base](
char ch) {
30 return (ch >=
'0' && ch <=
'9') || (
base == 16 && ((ch >=
'a' && ch <=
'f') || (ch >=
'A' && ch <=
'F')));
33 const quint64 value = text.toULongLong(&ok, base);
34 return ok && std::all_of(text.begin(), text.end(), digit) ? std::optional(value) : std::nullopt;
37std::chrono::milliseconds retryAfter(
const QHttpHeaders& headers,
const QDateTime& utcNow)
39 const QByteArrayView value = headers.value(QHttpHeaders::WellKnownHeader::RetryAfter);
40 if (
const auto seconds = unsignedNumber(value)) {
42 return std::chrono::seconds{(std::min<quint64>) (*seconds, maxSeconds)};
44 auto deadline = headers.dateTimeValue(QHttpHeaders::WellKnownHeader::RetryAfter);
45 if (!deadline || !utcNow.isValid()) {
49 if (value.indexOf(
',') > 3) {
50 *deadline = deadline->addYears(utcNow.date().year() / 100 * 100 - 1900);
51 if (*deadline > utcNow.addYears(50)) {
52 *deadline = deadline->addYears(-100);
56 return std::chrono::milliseconds{std::clamp(utcNow.msecsTo(*deadline), qint64(0), maxMs)};
59QString tr(
const char* text)
61 return QCoreApplication::translate(
"NTRIPHttpTransport", text);
69 if (!result.
error.isEmpty()) {
74 if (!url.isValid() || url.host().isEmpty()) {
75 result.
error = QCoreApplication::translate(
"NTRIPHttpTransport",
"Invalid NTRIP endpoint");
81 result.
bytes =
"GET " + url.path(QUrl::FullyEncoded).toLatin1() +
" HTTP/1.1\r\n" +
82 "Host: " + url.authority(QUrl::FullyEncoded).toLatin1() +
"\r\n" +
85 if (!
config.username.isEmpty() || !
config.password.isEmpty()) {
86 result.
bytes +=
"Authorization: Basic " +
90 result.
bytes +=
"\r\n";
101 static const QRegularExpression pattern(
102 QStringLiteral(
"^(HTTP/1\\.[01]|(?i:ICY|SOURCETABLE)) ([1-5][0-9]{2})(?: ([\\t\\x20-\\x7e\\x80-\\xff]*))?\\z"));
103 const auto match = pattern.match(QString::fromLatin1(line));
104 return match.hasMatch() ?
Status{match.captured(2).toInt(), match.captured(3),
true} :
Status{};
107void NTRIPHttpDecoder::_fail(Result& result,
const QString& detail,
NTRIPError code)
109 if (_pendingFailure) {
110 _finishError(result);
113 _state = State::Failed;
115 result.complete =
false;
119void NTRIPHttpDecoder::_finishError(Result& result)
121 static const QRegularExpression htmlTags(QStringLiteral(
"<[^>]*(?:>|$)"));
122 static const QRegularExpression controls(QStringLiteral(
"[\\x00-\\x08\\x0e-\\x1f\\x7f]"));
123 QString preview = QString::fromUtf8(_errorBody);
124 preview.remove(htmlTags);
125 preview.remove(controls);
127 result.failure = std::exchange(_pendingFailure, std::nullopt);
128 if (!preview.isEmpty()) {
129 result.failure->detail += QStringLiteral(
" \u2014 ") + preview;
131 _state = State::Failed;
135 result.connected =
false;
136 result.complete =
false;
139void NTRIPHttpDecoder::_beginIcyBody(Result& result)
142 result.body += std::exchange(_line, {});
148 while (!bytes.isEmpty() && _state != State::Failed) {
149 if (_state == State::Complete) {
150 _fail(result, tr(
"Unexpected data after HTTP response"));
153 if (_state == State::IcyHeaders) {
154 const auto first =
static_cast<unsigned char>(bytes.front());
155 const bool binary = first >= 127 || (first < 32 && first !=
'\t' && first !=
'\r' && first !=
'\n');
158 if (_headers.isEmpty() &&
159 (binary || (first ==
'\n' && !_line.endsWith(
'\r')) || (_line.endsWith(
'\r') && first !=
'\n'))) {
160 _beginIcyBody(result);
161 }
else if (!_headers.isEmpty() && _line.isEmpty() && binary) {
163 _lineReceived(result, utcNow);
167 if (_state == State::Identity || _state == State::ChunkData) {
168 const bool bounded = _state == State::ChunkData || _contentLength.has_value();
170 bounded ?
static_cast<qsizetype
>((std::min<quint64>) (bytes.size(), _remaining)) : bytes.size();
171 if (_pendingFailure) {
174 result.
body.append(bytes.data(), count);
176 bytes = bytes.sliced(count);
177 if (bounded && (_remaining -= count) == 0) {
178 _state = _state == State::ChunkData ? State::ChunkEnd : State::Complete;
179 result.
complete = _state == State::Complete;
181 if (_pendingFailure && (_state == State::Complete || _errorBody.size() ==
MAX_ERROR_BODY_BYTES)) {
182 _finishError(result);
186 if (_state == State::ChunkEnd) {
187 if (bytes.front() != (_chunkEndBytes == 0 ?
'\r' :
'\n')) {
188 _fail(result, tr(
"Invalid HTTP chunk terminator"));
191 bytes = bytes.sliced(1);
192 if (++_chunkEndBytes == 2) {
194 _state = State::ChunkSize;
198 const char ch = bytes.front();
199 bytes = bytes.sliced(1);
200 if ((ch ==
'\n' && !_line.endsWith(
'\r')) || (_line.endsWith(
'\r') && ch !=
'\n')) {
201 _fail(result, tr(
"Invalid HTTP line ending"));
206 _fail(result, tr(
"HTTP response header too large"), NTRIPError::HeaderTooLarge);
211 _lineReceived(result, utcNow);
218void NTRIPHttpDecoder::_lineReceived(Result& result,
const QDateTime& utcNow)
220 if (_state == State::Status) {
222 if (!_status.
valid) {
223 _fail(result, tr(
"Invalid HTTP status line"));
224 }
else if (_line.left(11).compare(
"SOURCETABLE", Qt::CaseInsensitive) == 0) {
226 _state = State::IcyHeaders;
227 result.connected =
true;
229 _fail(result, tr(
"Caster returned a source table; select a valid mountpoint"),
230 NTRIPError::InvalidMountpoint);
232 }
else if (_line.first(3).compare(
"ICY", Qt::CaseInsensitive) == 0 && _status.
code == 200) {
233 _state = State::IcyHeaders;
234 result.connected =
true;
236 _http10 = _line.startsWith(
"HTTP/1.0");
237 _state = State::Headers;
241 if (_state == State::ChunkSize) {
243 QByteArrayView size(_line);
244 if (
const qsizetype separator = size.indexOf(
';'); separator >= 0) {
245 size = size.first(separator);
246 while (size.endsWith(
' ') || size.endsWith(
'\t')) {
250 const auto length = unsignedNumber(size, 16);
252 _fail(result, tr(
"Invalid or oversized HTTP chunk"));
254 _remaining = *length;
255 _state = _remaining == 0 ? State::Trailers : State::ChunkData;
259 if (_line.isEmpty()) {
260 if (_state == State::Trailers) {
261 if (_pendingFailure) {
262 _finishError(result);
264 _state = State::Complete;
265 result.complete =
true;
269 if (_status.
code >= 300) {
270 const int status = _status.
code;
271 const auto code = status == 401 ? NTRIPError::AuthFailed
272 : status == 400 || status == 403 ? NTRIPError::RequestRejected
273 : NTRIPError::HttpError;
274 const QString detail = status == 401 ? tr(
"Authentication failed (401): check username and password")
275 : tr(
"HTTP %1: %2").arg(status).arg(_status.reason);
277 if (_headers.values(QHttpHeaders::WellKnownHeader::RetryAfter).size() == 1) {
278 _pendingFailure->retryAfter = retryAfter(_headers, utcNow);
280 if (code == NTRIPError::AuthFailed) {
281 _finishError(result);
285 const auto lengths = _headers.values(QHttpHeaders::WellKnownHeader::ContentLength);
286 for (
const auto& field : lengths) {
287 for (
const auto& value : field.split(
',')) {
288 const auto length = unsignedNumber(QByteArrayView(value).trimmed());
289 if (!length || (_contentLength && *_contentLength != *length)) {
290 _fail(result, tr(
"Invalid or conflicting HTTP content length"));
293 _contentLength = *length;
296 const auto transfers = _headers.values(QHttpHeaders::WellKnownHeader::TransferEncoding);
297 _chunked = !transfers.isEmpty();
298 if (_chunked && (_http10 || transfers.size() != 1 ||
299 transfers.first().compare(
"chunked", Qt::CaseInsensitive) != 0 || _contentLength)) {
300 _fail(result, tr(
"Unsupported or conflicting HTTP transfer encoding"));
303 if (_status.
code >= 100 && _status.
code < 200) {
304 if (_status.
code == 101 || _contentLength || _chunked || ++_informationalResponses > 4) {
305 _fail(result, tr(
"Unsupported HTTP informational response"));
308 _state = State::Status;
312 const auto encodings = _headers.values(QHttpHeaders::WellKnownHeader::ContentEncoding);
313 if (!encodings.isEmpty() &&
314 (encodings.size() != 1 || encodings.first().compare(
"identity", Qt::CaseInsensitive) != 0)) {
315 _fail(result, tr(
"Unsupported HTTP content encoding"));
318 if (_pendingFailure) {
322 for (
const auto& value : _headers.values(QHttpHeaders::WellKnownHeader::ContentType)) {
324 value.split(
';').first().trimmed().compare(
"gnss/sourcetable", Qt::CaseInsensitive) == 0) {
325 _fail(result, tr(
"Caster returned a source table; select a valid mountpoint"),
326 NTRIPError::InvalidMountpoint);
330 if (_status.
code == 204 || _status.
code == 205) {
331 if (_chunked || (_contentLength && *_contentLength != 0)) {
332 _fail(result, tr(
"Unexpected framing for an empty HTTP response"));
337 result.connected = result.connected || _state != State::IcyHeaders;
342 const qsizetype colon = _line.indexOf(
':');
343 const QByteArrayView
line(_line);
344 if (colon <= 0 || !std::all_of(
line.begin(),
line.begin() + colon, isToken) ||
345 !std::all_of(
line.begin() + colon + 1,
line.end(), isFieldValue)) {
346 if (_state == State::IcyHeaders && _headers.isEmpty()) {
348 _beginIcyBody(result);
351 _fail(result, tr(
"Invalid HTTP response header"));
354 if (_headers.size() >= 128) {
355 _fail(result, tr(
"Too many HTTP response headers"), NTRIPError::HeaderTooLarge);
358 const auto name =
line.first(colon);
359 const auto value =
line.sliced(colon + 1);
360 if (_state == State::Trailers &&
361 (name.compare(
"content-length", Qt::CaseInsensitive) == 0 ||
362 name.compare(
"transfer-encoding", Qt::CaseInsensitive) == 0 ||
363 name.compare(
"content-encoding", Qt::CaseInsensitive) == 0 ||
364 name.compare(
"content-type", Qt::CaseInsensitive) == 0 ||
365 name.compare(
"retry-after", Qt::CaseInsensitive) == 0 || name.compare(
"host", Qt::CaseInsensitive) == 0)) {
366 _fail(result, tr(
"Forbidden HTTP trailer"));
370 (void) _headers.append(QLatin1StringView(name.data(), name.size()), QLatin1StringView(value.data(), value.size()));
373void NTRIPHttpDecoder::_beginBody(Result& result)
375 _remaining = _contentLength.value_or(0);
376 _state = _chunked ? State::ChunkSize : State::Identity;
377 if (!_chunked && _contentLength && _remaining == 0) {
378 if (_pendingFailure) {
379 _finishError(result);
381 _state = State::Complete;
382 result.complete =
true;
390 if (_pendingFailure) {
391 _finishError(result);
392 }
else if ((_state == State::Identity && !_contentLength) ||
393 (_state == State::IcyHeaders && _line.isEmpty() && _headers.isEmpty()) || _state == State::Complete) {
394 _state = State::Complete;
396 }
else if (_state == State::Identity || _state == State::ChunkSize || _state == State::ChunkData ||
397 _state == State::ChunkEnd || _state == State::Trailers) {
398 _fail(result, tr(
"Caster closed the connection mid-transfer"));
399 }
else if (_state != State::Failed) {
400 _fail(result, tr(
"Caster disconnected before completing the HTTP response"));
Private transport parser; buffers framing, never correction payloads.
static constexpr quint64 MAX_CHUNK_BYTES
static constexpr qsizetype MAX_ERROR_PREVIEW_CHARS
Result feed(QByteArrayView bytes, const QDateTime &utcNow)
static constexpr qsizetype MAX_ERROR_BODY_BYTES
static constexpr qsizetype MAX_LINE_BYTES
static constexpr qsizetype MAX_HEADER_BYTES
static Status parseStatusLine(QByteArrayView line)
QByteArray line(std::string_view command)
command as one CR/LF-terminated line.
QString defaultUserAgent()
"<application>/<version> (Qt <version>)".
QString createBasicAuthCredentials(const QString &username, const QString &password)
Base64-encoded UTF-8 username:password, without the authentication scheme.
GPSCommandSequence base(QByteArrayView port, const GPSBaseStationConfig &base)
Streams RTCM3 corrections and the PVTGeodetic survey status over port in the mode base selects.
static constexpr std::chrono::milliseconds MAX_RETRY_AFTER
Longest caster Retry-After hint that may delay a reconnect.
QByteArray body
Retains valid prefixes when subsequent framing fails.
static NTRIPHttpRequest build(const NTRIPConnectionConfig &config, NTRIPHttpPurpose purpose=NTRIPHttpPurpose::Corrections)