cutelyst  5.0.1
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 
15 using 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(u'.');
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(u'.', 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 + u'.';
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");
274  case InvalidLabelCount:
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);
329  case InvalidLabelCount:
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 
372 void 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 
477 QString ValidatorDomain::genericValidationError(Context *c, const QVariant &errorData) const
478 {
479  return ValidatorDomain::diagnoseString(c, errorData.value<Diagnose>(), label(c));
480 }
481 
482 #include "moc_validatordomain.cpp"
Checks if the value of the input field contains a FQDN according to RFC 1035.
ValidatorDomain(const QString &field, Options options=NoOption, const ValidatorMessages &messages={}, const QString &defValKey={})
Constructs a new ValidatorDomain object with the given parameters.
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
Stores custom error messages and the input field label.
const_reference at(qsizetype i) const const
qsizetype size() const const
T value() const const
static QString diagnoseString(const Context *c, Diagnose diagnose, const QString &label={})
qsizetype size() const const
void finished()
The Cutelyst Context.
Definition: context.h:42
void defaultValue(Context *c, ValidatorReturnType *result) const
QString chopped(qsizetype len) const const
void abort()
bool empty() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toAce(const QString &domain, AceProcessingOptions options)
bool isEmpty() const const
The Cutelyst namespace holds all public Cutelyst API.
QString debugString(const Context *c) const
Base class for all validator rules.
char16_t & unicode()
Diagnose
Possible diagnose information for the checked domain.
QString toLower() const const
KeepEmptyParts
qsizetype count() const const
QString value(const ParamsMultiMap &params) const
QString label(const Context *c) const
std::function< void(ValidatorReturnType &&result)> ValidatorRtFn
Void callback function for validator rules that processes the ValidatorReturnType.
Definition: validatorrule.h:82
QString fromLatin1(QByteArrayView str)
QString validationError(Context *c, const QVariant &errorData={}) const
QString qtTrId(const char *id, int n=-1) const
Definition: context.h:658
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
const QChar at(qsizetype position) const const
T & last()
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype length() const const
Contains the result of a single input parameter validation.
Definition: validatorrule.h:52
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...
QString arg(Args &&... args) const const
void setValue(QVariant &&value)
static bool validate(const QString &value, Diagnose *diagnose=nullptr, QString *extractedValue=nullptr)
Returns true if value is a valid fully qualified domain name.