cutelyst 5.0.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
validatordomain.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2018-2025 Matthias Fehring <mf@huessenbergnetz.de>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5
6#include "validatordomain_p.h"
7
8#include <QDnsLookup>
9#include <QEventLoop>
10#include <QHostAddress>
11#include <QStringList>
12#include <QTimer>
13#include <QUrl>
14
15using namespace Cutelyst;
16
18 Options options,
19 const ValidatorMessages &messages,
20 const QString &defValKey)
21 : ValidatorRule(*new ValidatorDomainPrivate(field, options, messages, defValKey))
22{
23}
24
26
29 QString *extractedValue)
30{
31 Diagnose diag = Valid;
32
33 const bool hasRootDot = value.endsWith(u'.');
34 const QString withoutRootDot = hasRootDot ? value.chopped(1) : value;
35
36 // convert to lower case puny code
37 const QString ace = QString::fromLatin1(QUrl::toAce(withoutRootDot)).toLower();
38
39 // split up the utf8 string into parts to get the non puny code TLD
40 const QStringList nonAceParts = withoutRootDot.split(QLatin1Char('.'));
41 if (!nonAceParts.empty()) {
42 const QString tld = nonAceParts.last();
43 if (!tld.isEmpty()) {
44 // there are no TLDs with digits inside, but IDN TLDs can
45 // have digits in their puny code representation, so we have
46 // to check at first if the IDN TLD contains digits before
47 // checking the ACE puny code
48 for (const QChar &ch : tld) {
49 const char16_t uc = ch.unicode();
50 if (((uc >= ValidatorRulePrivate::ascii_0) &&
51 (uc <= ValidatorRulePrivate::ascii_9)) ||
52 (uc == ValidatorRulePrivate::ascii_dash)) {
53 diag = InvalidTLD;
54 break;
55 }
56 }
57
58 if (diag == Valid) {
59 if (!ace.isEmpty()) {
60 // maximum length of the name in the DNS is 253 without the last dot
61 if (ace.length() <= ValidatorDomainPrivate::maxDnsNameWithLastDot) {
62 const QStringList parts = ace.split(QLatin1Char('.'), Qt::KeepEmptyParts);
63 // there has to be more than only the TLD
64 if (parts.size() > 1) {
65 // the TLD can not have only 1 char
66 if (parts.last().length() > 1) {
67 for (int i = 0; i < parts.size(); ++i) {
68
69 if (diag != Valid) {
70 break;
71 }
72
73 const QString &part = parts.at(i);
74
75 if (part.isEmpty()) {
76 diag = EmptyLabel;
77 break;
78 }
79
80 // labels/parts can have a maximum length of 63 chars
81 if (part.length() > ValidatorDomainPrivate::maxDnsLabelLength) {
82 diag = LabelTooLong;
83 break;
84 }
85
86 const bool isTld = (i == (parts.size() - 1));
87 const bool isPunyCode = part.startsWith(u"xn--");
88 const qsizetype partEnd = part.size() - 1;
89
90 for (int j = 0; j < part.size(); ++j) {
91 const char16_t uc = part.at(j).unicode();
92 const bool isDigit =
93 ((uc >= ValidatorRulePrivate::ascii_0) &&
94 (uc <= ValidatorRulePrivate::ascii_9));
95 const bool isDash =
96 (uc == ValidatorRulePrivate::ascii_dash);
97 // no part/label can start with a digit or a
98 // dash
99 if (j == 0 && (isDash || isDigit)) {
100 diag = isDash ? DashStart : DigitStart;
101 break;
102 }
103 // no part/label can end with a dash
104 if (j == partEnd && isDash) {
105 diag = DashEnd;
106 break;
107 }
108 const bool isChar =
109 ((uc >= ValidatorRulePrivate::ascii_a) &&
110 (uc <= ValidatorRulePrivate::ascii_z));
111 if (!isTld) {
112 // if it is not the tld, it can have a-z 0-9
113 // and -
114 if (!(isDigit || isDash || isChar)) {
115 diag = InvalidChars;
116 break;
117 }
118 } else {
119 if (isPunyCode) {
120 if (!(isDigit || isDash || isChar)) {
121 diag = InvalidTLD;
122 break;
123 }
124 } else {
125 if (!isChar) {
126 diag = InvalidTLD;
127 break;
128 }
129 }
130 }
131 }
132 }
133 } else {
134 diag = InvalidTLD;
135 }
136 } else {
137 diag = InvalidLabelCount;
138 }
139 } else {
140 diag = TooLong;
141 }
142 } else {
143 diag = EmptyLabel;
144 }
145 }
146 } else {
147 diag = EmptyLabel;
148 }
149 } else {
150 diag = EmptyLabel;
151 }
152
153 if (diagnose) {
154 *diagnose = diag;
155 }
156
157 if (diag == Valid && extractedValue) {
158 if (hasRootDot) {
159 *extractedValue = ace + QLatin1Char('.');
160 } else {
161 *extractedValue = ace;
162 }
163 }
164
165 return diag == Valid;
166}
167
169 const QString &value,
170 Options options,
171 std::function<void(Diagnose diagnose, const QString &extractedValue)> cb)
172{
173 Diagnose diag;
174 QString extracted;
175
176 bool isValid = ValidatorDomain::validate(value, &diag, &extracted);
177 if (!isValid) {
178 cb(diag, {});
179 return;
180 }
181
182 if (!options.testAnyFlag(CheckDNS)) {
183 cb(diag, extracted);
184 return;
185 }
186
187 if (options.testFlag(CheckARecord)) {
188
189 auto dns = new QDnsLookup{QDnsLookup::A, extracted};
190 QObject::connect(dns, &QDnsLookup::finished, [dns, options, cb, extracted] {
191 if (dns->error() == QDnsLookup::NoError) {
192 if (dns->hostAddressRecords().empty()) {
193 cb(MissingDNS, extracted);
194 } else {
195 if (!options.testFlag(CheckAAAARecord)) {
196 cb(Valid, extracted);
197 } else {
198
199 auto dns2 = new QDnsLookup{QDnsLookup::AAAA, extracted};
201 dns2, &QDnsLookup::finished, [dns2, options, cb, extracted] {
202 if (dns2->error() == QDnsLookup::NoError) {
203 if (dns2->hostAddressRecords().empty()) {
204 cb(MissingDNS, extracted);
205 } else {
206 cb(Valid, extracted);
207 }
208 } else if (dns2->error() == QDnsLookup::OperationCancelledError) {
209 cb(DNSTimeout, extracted);
210 } else {
211 cb(DNSError, extracted);
212 }
213 dns2->deleteLater();
214 });
216 ValidatorDomainPrivate::dnsLookupTimeout, dns2, &QDnsLookup::abort);
217 dns2->lookup();
218 }
219 }
220 } else if (dns->error() == QDnsLookup::OperationCancelledError) {
221 cb(DNSTimeout, extracted);
222 } else {
223 cb(DNSError, extracted);
224 }
225 dns->deleteLater();
226 });
227 QTimer::singleShot(ValidatorDomainPrivate::dnsLookupTimeout, dns, &QDnsLookup::abort);
228 dns->lookup();
229
230 } else if (options.testFlag(CheckAAAARecord)) {
231
232 auto dns2 = new QDnsLookup{QDnsLookup::AAAA, extracted};
233 QObject::connect(dns2, &QDnsLookup::finished, [dns2, options, cb, extracted] {
234 if (dns2->error() == QDnsLookup::NoError) {
235 if (dns2->hostAddressRecords().empty()) {
236 cb(MissingDNS, extracted);
237 } else {
238 cb(Valid, extracted);
239 }
240 } else if (dns2->error() == QDnsLookup::OperationCancelledError) {
241 cb(DNSTimeout, extracted);
242 } else {
243 cb(DNSError, extracted);
244 }
245 dns2->deleteLater();
246 });
247 QTimer::singleShot(ValidatorDomainPrivate::dnsLookupTimeout, dns2, &QDnsLookup::abort);
248 dns2->lookup();
249 }
250}
251
253{
254 if (label.isEmpty()) {
255 switch (diagnose) {
256 case MissingDNS:
257 //% "The domain name seems to be valid but could not be found in the "
258 //% "domain name system."
259 return c->qtTrId("cutelyst-valdomain-diag-missingdns");
260 case InvalidChars:
261 //% "The domain name contains characters that are not allowed."
262 return c->qtTrId("cutelyst-valdomain-diag-invalidchars");
263 case LabelTooLong:
264 //% "At least one of the sections separated by dots exceeds the maximum "
265 //% "allowed length of 63 characters. Note that internationalized domain "
266 //% "names with non-ASCII characters can be longer internally than they are "
267 //% "displayed."
268 return c->qtTrId("cutelyst-valdomain-diag-labeltoolong");
269 case TooLong:
270 //% "The full name of the domain must not be longer than 253 characters. Note that "
271 //% "internationalized domain names with non-ASCII character can be longer internally "
272 //% "than they are displayed."
273 return c->qtTrId("cutelyst-valdomain-diag-toolong");
275 //% "This is not a valid domain name because it has either no parts "
276 //% "(is empty) or only has a top level domain."
277 return c->qtTrId("cutelyst-valdomain-diag-invalidlabelcount");
278 case EmptyLabel:
279 //% "At least one of the sections separated by dots is empty. Check "
280 //% "whether you have entered two dots consecutively."
281 return c->qtTrId("cutelyst-valdomain-diag-emptylabel");
282 case InvalidTLD:
283 //% "The top level domain (last part) contains characters that are "
284 //% "not allowed, like digits and/or dashes."
285 return c->qtTrId("cutelyst-valdomain-diag-invalidtld");
286 case DashStart:
287 //% "Domain name sections are not allowed to start with a dash."
288 return c->qtTrId("cutelyst-valdomain-diag-dashstart");
289 case DashEnd:
290 //% "Domain name sections are not allowed to end with a dash."
291 return c->qtTrId("cutelyst-valdomain-diag-dashend");
292 case DigitStart:
293 //% "Domain name sections are not allowed to start with a digit."
294 return c->qtTrId("cutelyst-valdomain-diag-digitstart");
295 case Valid:
296 //% "The domain name is valid."
297 return c->qtTrId("cutelyst-valdomain-diag-valid");
298 case DNSTimeout:
299 //% "The DNS lookup was aborted because it took too long."
300 return c->qtTrId("cutelyst-valdomain-diag-dnstimeout");
301 case DNSError:
302 //% "An error occured while performing the DNS lookup."
303 return c->qtTrId("cutelyst-valdomain-diag-dnserror");
304 default:
305 Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
306 return {};
307 }
308 } else {
309 switch (diagnose) {
310 case MissingDNS:
311 //% "The domain name in the “%1“ field seems to be valid but could "
312 //% "not be found in the domain name system."
313 return c->qtTrId("cutelyst-valdomain-diag-missingdns-label").arg(label);
314 case InvalidChars:
315 //% "The domain name in the “%1“ field contains characters that are not allowed."
316 return c->qtTrId("cutelyst-valdomain-diag-invalidchars-label").arg(label);
317 case LabelTooLong:
318 //% "The domain name in the “%1“ field is not valid because at least "
319 //% "one of the sections separated by dots exceeds the maximum "
320 //% "allowed length of 63 characters. Note that internationalized "
321 //% "domain names with non-ASCII characters can be longer internally "
322 //% "than they are displayed."
323 return c->qtTrId("cutelyst-valdomain-diag-labeltoolong-label").arg(label);
324 case TooLong:
325 //% "The full name of the domain in the “%1” field must not be longer "
326 //% "than 253 characters. Note that internationalized domain names "
327 //% "with non-ASCII characters can be longer internally than they are displayed."
328 return c->qtTrId("cutelyst-valdomain-diag-toolong-label").arg(label);
330 //% "The “%1” field does not contain a valid domain name because it "
331 //% "has either no parts (is empty) or only has a top level domain."
332 return c->qtTrId("cutelyst-valdomain-diag-invalidlabelcount-label").arg(label);
333 case EmptyLabel:
334 //% "The domain name in the “%1“ field is not valid because at least "
335 //% "one of the sections separated by dots is empty. Check whether "
336 //% "you have entered two dots consecutively."
337 return c->qtTrId("cutelyst-valdomain-diag-emptylabel-label").arg(label);
338 case InvalidTLD:
339 //% "The top level domain (last part) of the domain name in the “%1” field "
340 //% "contains characters that are not allowed, like digits and or dashes."
341 return c->qtTrId("cutelyst-valdomain-diag-invalidtld-label").arg(label);
342 case DashStart:
343 //% "The domain name in the “%1“ field is not valid because domain "
344 //% "name sections are not allowed to start with a dash."
345 return c->qtTrId("cutelyst-valdomain-diag-dashstart-label").arg(label);
346 case DashEnd:
347 //% "The domain name in the “%1“ field is not valid because domain "
348 //% "name sections are not allowed to end with a dash."
349 return c->qtTrId("cutelyst-valdomain-diag-dashend-label").arg(label);
350 case DigitStart:
351 //% "The domain name in the “%1“ field is not valid because domain "
352 //% "name sections are not allowed to start with a digit."
353 return c->qtTrId("cutelyst-valdomain-diag-digitstart-label").arg(label);
354 case Valid:
355 //% "The domain name in the “%1” field is valid."
356 return c->qtTrId("cutelyst-valdomain-diag-valid-label").arg(label);
357 case DNSTimeout:
358 //% "The DNS lookup for the domain name in the “%1” field was aborted "
359 //% "because it took too long."
360 return c->qtTrId("cutelyst-valdomain-diag-dnstimeout-label").arg(label);
361 case DNSError:
362 //% "The DNS lookup for the domain name in the “%1” field failed "
363 //% "becaus of an error in the DNS resolution."
364 return c->qtTrId("cutelyst-valdomain-diag-dnserror-label");
365 default:
366 Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
367 return {};
368 }
369 }
370}
371
372void writeDebugString(const QString &valInfo, ValidatorDomain::Diagnose diag, const QString &v)
373{
374 switch (diag) {
376 break;
378 qCDebug(C_VALIDATOR).noquote() << valInfo << "Can not find valid DNS entry for" << v;
379 break;
381 qCDebug(C_VALIDATOR).noquote()
382 << valInfo << "The domain name contains characters that are not allowed";
383 break;
385 qCDebug(C_VALIDATOR).noquote()
386 << valInfo << "At least on of the domain name labels exceeds the maximum" << "size of"
387 << ValidatorDomainPrivate::maxDnsLabelLength << "characters";
388 break;
390 qCDebug(C_VALIDATOR).noquote()
391 << valInfo << "The domain name exceeds the maximum size of"
392 << ValidatorDomainPrivate::maxDnsNameWithLastDot << "characters";
393 break;
395 qCDebug(C_VALIDATOR).noquote()
396 << valInfo << "Invalid label count. Either no labels or only TLD";
397 break;
399 qCDebug(C_VALIDATOR).noquote()
400 << valInfo << "At least one of the domain name labels is empty";
401 break;
403 qCDebug(C_VALIDATOR).noquote()
404 << valInfo << "The TLD label contains characters that are not allowed";
405 break;
407 qCDebug(C_VALIDATOR).noquote() << valInfo << "At least one label starts with a dash";
408 break;
410 qCDebug(C_VALIDATOR).noquote() << valInfo << "At least one label ends with a dash";
411 break;
413 qCDebug(C_VALIDATOR).noquote() << valInfo << "At least one label starts with a digit";
414 break;
416 qCDebug(C_VALIDATOR).noquote() << valInfo << "The DNS lookup exceeds the timeout of"
417#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
418 << ValidatorDomainPrivate::dnsLookupTimeout;
419#else
420 << ValidatorDomainPrivate::dnsLookupTimeout.count()
421 << "milliseconds";
422#endif
424 qCDebug(C_VALIDATOR).noquote()
425 << valInfo << "The DNS lookup failed because of errors in the"
426 << "DNS resolution";
427 }
428}
429
431{
432 ValidatorReturnType result;
433
434 const QString &v = value(params);
435
436 if (!v.isEmpty()) {
437 Q_D(const ValidatorDomain);
438 QString exVal;
439 Diagnose diag{Valid};
440 if (ValidatorDomain::validate(v, &diag, &exVal)) {
441 result.value.setValue(exVal);
442 } else {
443 result.errorMessage = validationError(c, diag);
444 if (C_VALIDATOR().isDebugEnabled()) {
445 writeDebugString(debugString(c), diag, v);
446 }
447 }
448 } else {
449 defaultValue(c, &result);
450 }
451
452 return result;
453}
454
456{
457 const QString v = value(params);
458
459 if (!v.isEmpty()) {
460 Q_D(const ValidatorDomain);
462 v, d->options, [cb, this, c, v](Diagnose diagnose, const QString &extractedValue) {
463 if (diagnose == Valid) {
464 cb({.errorMessage = {}, .value = extractedValue});
465 } else {
466 if (C_VALIDATOR().isDebugEnabled()) {
467 writeDebugString(debugString(c), diagnose, v);
468 }
469 cb({.errorMessage = validationError(c, diagnose)});
470 }
471 });
472 } else {
473 defaultValue(c, cb);
474 }
475}
476
478{
479 return ValidatorDomain::diagnoseString(c, errorData.value<Diagnose>(), label(c));
480}
481
482#include "moc_validatordomain.cpp"
The Cutelyst Context.
Definition context.h:42
QString qtTrId(const char *id, int n=-1) const
Definition context.h:657
Checks if the value of the input field contains a FQDN according to RFC 1035.
QString genericValidationError(Context *c, const QVariant &errorData=QVariant()) const override
Returns a generic error message if validation failed.
static QString diagnoseString(Context *c, Diagnose diagnose, const QString &label={})
ValidatorDomain(const QString &field, Options options=NoOption, const ValidatorMessages &messages={}, const QString &defValKey={})
Constructs a new ValidatorDomain object with the given parameters.
Diagnose
Possible diagnose information for the checked domain.
Base class for all validator rules.
QString validationError(Context *c, const QVariant &errorData={}) const
QString label(const Context *c) const
QString debugString(const Context *c) const
std::function< void(ValidatorReturnType &&result)> ValidatorRtFn
Void callback function for validator rules that processes the ValidatorReturnType.
void defaultValue(Context *c, ValidatorReturnType *result) const
QString value(const ParamsMultiMap &params) const
static void validateCb(const QString &value, Options options, std::function< void(Diagnose diagnose, const QString &extractedValue)> cb)
Checks if value is a vaid fully qualified domain name and writes the result to the callback cb.
static bool validate(const QString &value, Diagnose *diagnose=nullptr, QString *extractedValue=nullptr)
Returns true if value is a valid fully qualified domain name.
The Cutelyst namespace holds all public Cutelyst API.
char16_t & unicode()
void abort()
void finished()
QList::const_reference at(qsizetype i) const const
bool empty() const const
T & last()
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
qsizetype count() const const
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
QString chopped(qsizetype len) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
qsizetype length() const const
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toLower() const const
KeepEmptyParts
QByteArray toAce(const QString &domain, QUrl::AceProcessingOptions options)
void setValue(QVariant &&value)
T value() const const
Stores custom error messages and the input field label.
Contains the result of a single input parameter validation.