cutelyst  4.4.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-2023 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 <QStringList>
11 #include <QTimer>
12 #include <QUrl>
13 
14 using namespace Cutelyst;
15 
17  bool checkDNS,
18  const ValidatorMessages &messages,
19  const QString &defValKey)
20  : ValidatorRule(*new ValidatorDomainPrivate(field, checkDNS, messages, defValKey))
21 {
22 }
23 
25 
27  bool checkDNS,
29  QString *extractedValue)
30 {
31  bool valid = true;
32 
33  Diagnose diag = Valid;
34 
35  QString _v = value;
36  bool hasRootDot = false;
37  if (_v.endsWith(u'.')) {
38  hasRootDot = true;
39  _v.chop(1);
40  }
41 
42  // convert to lower case puny code
44 
45  // split up the utf8 string into parts to get the non puny code TLD
46  const QStringList nonAceParts = _v.split(QLatin1Char('.'));
47  if (!nonAceParts.empty()) {
48  const QString tld = nonAceParts.last();
49  if (!tld.isEmpty()) {
50  // there are no TLDs with digits inside, but IDN TLDs can
51  // have digits in their puny code representation, so we have
52  // to check at first if the IDN TLD contains digits before
53  // checking the ACE puny code
54  for (const QChar &ch : tld) {
55  const ushort &uc = ch.unicode();
56  if (((uc >= ValidatorRulePrivate::ascii_0) &&
57  (uc <= ValidatorRulePrivate::ascii_9)) ||
58  (uc == ValidatorRulePrivate::ascii_dash)) {
59  diag = InvalidTLD;
60  valid = false;
61  break;
62  }
63  }
64 
65  if (valid) {
66  if (!v.isEmpty()) {
67  // maximum length of the name in the DNS is 253 without the last dot
68  if (v.length() <= ValidatorDomainPrivate::maxDnsNameWithLastDot) {
69  const QStringList parts = v.split(QLatin1Char('.'), Qt::KeepEmptyParts);
70  // there has to be more than only the TLD
71  if (parts.size() > 1) {
72  // the TLD can not have only 1 char
73  if (parts.last().length() > 1) {
74  for (int i = 0; i < parts.size(); ++i) {
75  if (valid) {
76  const QString &part = parts.at(i);
77  if (!part.isEmpty()) {
78  // labels/parts can have a maximum length of 63 chars
79  if (part.length() <=
80  ValidatorDomainPrivate::maxDnsLabelLength) {
81  bool isTld = (i == (parts.size() - 1));
82  bool isPunyCode = part.startsWith(u"xn--");
83  for (int j = 0; j < part.size(); ++j) {
84  const ushort &uc = part.at(j).unicode();
85  const bool isDigit =
86  ((uc >= ValidatorRulePrivate::ascii_0) &&
87  (uc <= ValidatorRulePrivate::ascii_9));
88  const bool isDash =
89  (uc == ValidatorRulePrivate::ascii_dash);
90  // no part/label can start with a digit or a
91  // dash
92  if ((j == 0) && (isDash || isDigit)) {
93  valid = false;
94  diag = isDash ? DashStart : DigitStart;
95  break;
96  }
97  // no part/label can end with a dash
98  if ((j == (part.size() - 1)) && isDash) {
99  valid = false;
100  diag = DashEnd;
101  break;
102  }
103  const bool isChar =
104  ((uc >= ValidatorRulePrivate::ascii_a) &&
105  (uc <= ValidatorRulePrivate::ascii_z));
106  if (!isTld) {
107  // if it is not the tld, it can have a-z 0-9
108  // and -
109  if (!(isDigit || isDash || isChar)) {
110  valid = false;
111  diag = InvalidChars;
112  break;
113  }
114  } else {
115  if (isPunyCode) {
116  if (!(isDigit || isDash || isChar)) {
117  valid = false;
118  diag = InvalidTLD;
119  break;
120  }
121  } else {
122  if (!isChar) {
123  valid = false;
124  diag = InvalidTLD;
125  break;
126  }
127  }
128  }
129  }
130  } else {
131  valid = false;
132  diag = LabelTooLong;
133  break;
134  }
135  } else {
136  valid = false;
137  diag = EmptyLabel;
138  break;
139  }
140  } else {
141  break;
142  }
143  }
144  } else {
145  valid = false;
146  diag = InvalidTLD;
147  }
148  } else {
149  valid = false;
150  diag = InvalidLabelCount;
151  }
152  } else {
153  valid = false;
154  diag = TooLong;
155  }
156  } else {
157  valid = false;
158  diag = EmptyLabel;
159  }
160  }
161  } else {
162  valid = false;
163  diag = EmptyLabel;
164  }
165  } else {
166  valid = false;
167  diag = EmptyLabel;
168  }
169 
170  if (valid && checkDNS) {
171  QDnsLookup alookup(QDnsLookup::A, v);
172  QEventLoop aloop;
174  QTimer::singleShot(ValidatorDomainPrivate::dnsLookupTimeout, &alookup, &QDnsLookup::abort);
175  alookup.lookup();
176  aloop.exec();
177 
178  if (((alookup.error() != QDnsLookup::NoError) &&
179  (alookup.error() != QDnsLookup::OperationCancelledError)) ||
180  alookup.hostAddressRecords().empty()) {
181  QDnsLookup aaaaLookup(QDnsLookup::AAAA, v);
182  QEventLoop aaaaLoop;
183  QObject::connect(&aaaaLookup, &QDnsLookup::finished, &aaaaLoop, &QEventLoop::quit);
185  ValidatorDomainPrivate::dnsLookupTimeout, &aaaaLookup, &QDnsLookup::abort);
186  aaaaLookup.lookup();
187  aaaaLoop.exec();
188 
189  if (((aaaaLookup.error() != QDnsLookup::NoError) &&
190  (aaaaLookup.error() != QDnsLookup::OperationCancelledError)) ||
191  aaaaLookup.hostAddressRecords().empty()) {
192  valid = false;
193  diag = MissingDNS;
194  } else if (aaaaLookup.error() == QDnsLookup::OperationCancelledError) {
195  valid = false;
196  diag = DNSTimeout;
197  }
198  } else if (alookup.error() == QDnsLookup::OperationCancelledError) {
199  valid = false;
200  diag = DNSTimeout;
201  }
202  }
203 
204  if (diagnose) {
205  *diagnose = diag;
206  }
207 
208  if (valid && extractedValue) {
209  if (hasRootDot) {
210  *extractedValue = v + QLatin1Char('.');
211  } else {
212  *extractedValue = v;
213  }
214  }
215 
216  return valid;
217 }
218 
220 {
221  if (label.isEmpty()) {
222  switch (diagnose) {
223  case MissingDNS:
224  //% "The domain name seems to be valid but could not be found in the "
225  //% "domain name system."
226  return c->qtTrId("cutelyst-valdomain-diag-missingdns");
227  case InvalidChars:
228  //% "The domain name contains characters that are not allowed."
229  return c->qtTrId("cutelyst-valdomain-diag-invalidchars");
230  case LabelTooLong:
231  //% "At least one of the sections separated by dots exceeds the maximum "
232  //% "allowed length of 63 characters. Note that internationalized domain "
233  //% "names with non-ASCII characters can be longer internally than they are "
234  //% "displayed."
235  return c->qtTrId("cutelyst-valdomain-diag-labeltoolong");
236  case TooLong:
237  //% "The full name of the domain must not be longer than 253 characters. Note that "
238  //% "internationalized domain names with non-ASCII character can be longer internally "
239  //% "than they are displayed."
240  return c->qtTrId("cutelyst-valdomain-diag-toolong");
241  case InvalidLabelCount:
242  //% "This is not a valid domain name because it has either no parts "
243  //% "(is empty) or only has a top level domain."
244  return c->qtTrId("cutelyst-valdomain-diag-invalidlabelcount");
245  case EmptyLabel:
246  //% "At least one of the sections separated by dots is empty. Check "
247  //% "whether you have entered two dots consecutively."
248  return c->qtTrId("cutelyst-valdomain-diag-emptylabel");
249  case InvalidTLD:
250  //% "The top level domain (last part) contains characters that are "
251  //% "not allowed, like digits and/or dashes."
252  return c->qtTrId("cutelyst-valdomain-diag-invalidtld");
253  case DashStart:
254  //% "Domain name sections are not allowed to start with a dash."
255  return c->qtTrId("cutelyst-valdomain-diag-dashstart");
256  case DashEnd:
257  //% "Domain name sections are not allowed to end with a dash."
258  return c->qtTrId("cutelyst-valdomain-diag-dashend");
259  case DigitStart:
260  //% "Domain name sections are not allowed to start with a digit."
261  return c->qtTrId("cutelyst-valdomain-diag-digitstart");
262  case Valid:
263  //% "The domain name is valid."
264  return c->qtTrId("cutelyst-valdomain-diag-valid");
265  case DNSTimeout:
266  //% "The DNS lookup was aborted because it took too long."
267  return c->qtTrId("cutelyst-valdomain-diag-dnstimeout");
268  default:
269  Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
270  return {};
271  }
272  } else {
273  switch (diagnose) {
274  case MissingDNS:
275  //% "The domain name in the “%1“ field seems to be valid but could "
276  //% "not be found in the domain name system."
277  return c->qtTrId("cutelyst-valdomain-diag-missingdns-label").arg(label);
278  case InvalidChars:
279  //% "The domain name in the “%1“ field contains characters that are not allowed."
280  return c->qtTrId("cutelyst-valdomain-diag-invalidchars-label").arg(label);
281  case LabelTooLong:
282  //% "The domain name in the “%1“ field is not valid because at least "
283  //% "one of the sections separated by dots exceeds the maximum "
284  //% "allowed length of 63 characters. Note that internationalized "
285  //% "domain names with non-ASCII characters can be longer internally "
286  //% "than they are displayed."
287  return c->qtTrId("cutelyst-valdomain-diag-labeltoolong-label").arg(label);
288  case TooLong:
289  //% "The full name of the domain in the “%1” field must not be longer "
290  //% "than 253 characters. Note that internationalized domain names "
291  //% "with non-ASCII characters can be longer internally than they are displayed."
292  return c->qtTrId("cutelyst-valdomain-diag-toolong-label").arg(label);
293  case InvalidLabelCount:
294  //% "The “%1” field does not contain a valid domain name because it "
295  //% "has either no parts (is empty) or only has a top level domain."
296  return c->qtTrId("cutelyst-valdomain-diag-invalidlabelcount-label").arg(label);
297  case EmptyLabel:
298  //% "The domain name in the “%1“ field is not valid because at least "
299  //% "one of the sections separated by dots is empty. Check whether "
300  //% "you have entered two dots consecutively."
301  return c->qtTrId("cutelyst-valdomain-diag-emptylabel-label").arg(label);
302  case InvalidTLD:
303  //% "The top level domain (last part) of the domain name in the “%1” field "
304  //% "contains characters that are not allowed, like digits and or dashes."
305  return c->qtTrId("cutelyst-valdomain-diag-invalidtld-label").arg(label);
306  case DashStart:
307  //% "The domain name in the “%1“ field is not valid because domain "
308  //% "name sections are not allowed to start with a dash."
309  return c->qtTrId("cutelyst-valdomain-diag-dashstart-label").arg(label);
310  case DashEnd:
311  //% "The domain name in the “%1“ field is not valid because domain "
312  //% "name sections are not allowed to end with a dash."
313  return c->qtTrId("cutelyst-valdomain-diag-dashend-label").arg(label);
314  case DigitStart:
315  //% "The domain name in the “%1“ field is not valid because domain "
316  //% "name sections are not allowed to start with a digit."
317  return c->qtTrId("cutelyst-valdomain-diag-digitstart-label").arg(label);
318  case Valid:
319  //% "The domain name in the “%1” field is valid."
320  return c->qtTrId("cutelyst-valdomain-diag-valid-label").arg(label);
321  case DNSTimeout:
322  //% "The DNS lookup for the domain name in the “%1” field was aborted "
323  //% "because it took too long."
324  return c->qtTrId("cutelyst-valdomain-diag-dnstimeout-label").arg(label);
325  default:
326  Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
327  return {};
328  }
329  }
330 }
331 
333 {
334  ValidatorReturnType result;
335 
336  const QString &v = value(params);
337 
338  if (!v.isEmpty()) {
339  Q_D(const ValidatorDomain);
340  QString exVal;
341  Diagnose diag{Valid};
342  if (ValidatorDomain::validate(v, d->checkDNS, &diag, &exVal)) {
343  result.value.setValue(exVal);
344  } else {
345  result.errorMessage = validationError(c, diag);
346  if (C_VALIDATOR().isDebugEnabled()) {
347  switch (diag) {
348  case Valid:
349  break;
350  case MissingDNS:
351  qCDebug(C_VALIDATOR).noquote()
352  << debugString(c) << "Can not find valid DNS entry for" << v;
353  break;
354  case InvalidChars:
355  qCDebug(C_VALIDATOR).noquote()
356  << debugString(c)
357  << "The domain name contains characters that are not allowed";
358  break;
359  case LabelTooLong:
360  qCDebug(C_VALIDATOR).noquote()
361  << debugString(c)
362  << "At least on of the domain name labels exceeds the maximum" << "size of"
363  << ValidatorDomainPrivate::maxDnsLabelLength << "characters";
364  break;
365  case TooLong:
366  qCDebug(C_VALIDATOR).noquote()
367  << debugString(c) << "The domain name exceeds the maximum size of"
368  << ValidatorDomainPrivate::maxDnsNameWithLastDot << "characters";
369  break;
370  case InvalidLabelCount:
371  qCDebug(C_VALIDATOR).noquote()
372  << debugString(c) << "Invalid label count. Either no labels or only TLD";
373  break;
374  case EmptyLabel:
375  qCDebug(C_VALIDATOR).noquote()
376  << debugString(c) << "At least one of the domain name labels is empty";
377  break;
378  case InvalidTLD:
379  qCDebug(C_VALIDATOR).noquote()
380  << debugString(c)
381  << "The TLD label contains characters that are not allowed";
382  break;
383  case DashStart:
384  qCDebug(C_VALIDATOR).noquote()
385  << debugString(c) << "At least one label starts with a dash";
386  break;
387  case DashEnd:
388  qCDebug(C_VALIDATOR).noquote()
389  << debugString(c) << "At least one label ends with a dash";
390  break;
391  case DigitStart:
392  qCDebug(C_VALIDATOR).noquote()
393  << debugString(c) << "At least one label starts with a digit";
394  break;
395  case DNSTimeout:
396  qCDebug(C_VALIDATOR).noquote()
397  << debugString(c) << "The DNS lookup exceeds the timeout of"
398 #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
399  << ValidatorDomainPrivate::dnsLookupTimeout;
400 #else
401  << ValidatorDomainPrivate::dnsLookupTimeout.count() << "milliseconds";
402 #endif
403  }
404  }
405  }
406  } else {
407  defaultValue(c, &result);
408  }
409 
410  return result;
411 }
412 
414 {
415  return ValidatorDomain::diagnoseString(c, errorData.value<Diagnose>(), label(c));
416 }
417 
418 #include "moc_validatordomain.cpp"
The Cutelyst Context.
Definition: context.h:42
QString qtTrId(const char *id, int n=-1) const
Definition: context.h:656
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=QString())
ValidatorDomain(const QString &field, bool checkDNS=false, const ValidatorMessages &messages=ValidatorMessages(), const QString &defValKey=QString())
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(Context *c) const
void defaultValue(Context *c, ValidatorReturnType *result) const
QString value(const ParamsMultiMap &params) const
QString debugString(Context *c) const
static bool validate(const QString &value, bool checkDNS, 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< QDnsHostAddressRecord > hostAddressRecords() const const
void lookup()
int exec(QEventLoop::ProcessEventsFlags flags)
void quit()
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
void chop(qsizetype n)
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.
Definition: validatorrule.h:49