cutelyst  3.7.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-2022 Matthias Fehring <mf@huessenbergnetz.de>
3  * SPDX-License-Identifier: BSD-3-Clause
4  */
5 
6 #include "validatordomain_p.h"
7 #include <QUrl>
8 #include <QStringList>
9 #include <QEventLoop>
10 #include <QDnsLookup>
11 #include <QTimer>
12 
13 using namespace Cutelyst;
14 
15 ValidatorDomain::ValidatorDomain(const QString &field, bool checkDNS, const ValidatorMessages &messages, const QString &defValKey) :
16  ValidatorRule(* new ValidatorDomainPrivate(field, checkDNS, messages, defValKey))
17 {
18 }
19 
21 {
22 }
23 
24 bool ValidatorDomain::validate(const QString &value, bool checkDNS, Cutelyst::ValidatorDomain::Diagnose *diagnose, QString *extractedValue)
25 {
26  bool valid = true;
27 
28  Diagnose diag = Valid;
29 
30  QString _v = value;
31  bool hasRootDot = false;
32  if (_v.endsWith(u'.')) {
33  hasRootDot = true;
34  _v.chop(1);
35  }
36 
37  // convert to lower case puny code
38  const QString v = QString::fromLatin1(QUrl::toAce(_v)).toLower();
39 
40  // split up the utf8 string into parts to get the non puny code TLD
41  const QStringList nonAceParts = _v.split(QLatin1Char('.'));
42  if (!nonAceParts.empty()) {
43  const QString tld = nonAceParts.last();
44  if (!tld.isEmpty()) {
45  // there are no TLDs with digits inside, but IDN TLDs can
46  // have digits in their puny code representation, so we have
47  // to check at first if the IDN TLD contains digits before
48  // checking the ACE puny code
49  for (const QChar &ch : tld) {
50  const ushort &uc = ch.unicode();
51  if (((uc > 47) && (uc < 58)) || (uc == 45)) {
52  diag = InvalidTLD;
53  valid = false;
54  break;
55  }
56  }
57 
58  if (valid) {
59  if (!v.isEmpty()) {
60  // maximum length of the name in the DNS is 253 without the last dot
61  if (v.length() < 254) {
62  const QStringList parts = v.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  if (valid) {
69  const QString part = parts.at(i);
70  if (!part.isEmpty()) {
71  // labels/parts can have a maximum length of 63 chars
72  if (part.length() < 64) {
73  bool isTld = (i == (parts.size() -1));
74  bool isPunyCode = part.startsWith(u"xn--");
75  for (int j = 0; j < part.size(); ++j) {
76  const ushort &uc = part.at(j).unicode();
77  const bool isDigit = ((uc > 47) && (uc < 58));
78  const bool isDash = (uc == 45);
79  // no part/label can start with a digit or a dash
80  if ((j == 0) && (isDash || isDigit)) {
81  valid = false;
82  diag = isDash ? DashStart : DigitStart;
83  break;
84  }
85  // no part/label can end with a dash
86  if ((j == (part.size() - 1)) && isDash) {
87  valid = false;
88  diag = DashEnd;
89  break;
90  }
91  const bool isChar = ((uc > 96) && (uc < 123));
92  if (!isTld) {
93  // if it is not the tld, it can have a-z 0-9 and -
94  if (!(isDigit || isDash || isChar)) {
95  valid = false;
96  diag = InvalidChars;
97  break;
98  }
99  } else {
100  if (isPunyCode) {
101  if (!(isDigit || isDash || isChar)) {
102  valid = false;
103  diag = InvalidTLD;
104  break;
105  }
106  } else {
107  if (!isChar) {
108  valid = false;
109  diag = InvalidTLD;
110  break;
111  }
112  }
113  }
114  }
115  } else {
116  valid = false;
117  diag = LabelTooLong;
118  break;
119  }
120  } else {
121  valid = false;
122  diag = EmptyLabel;
123  break;
124  }
125  } else {
126  break;
127  }
128  }
129  } else {
130  valid = false;
131  diag = InvalidTLD;
132  }
133  } else {
134  valid = false;
135  diag = InvalidLabelCount;
136  }
137  } else {
138  valid = false;
139  diag = TooLong;
140  }
141  } else {
142  valid = false;
143  diag = EmptyLabel;
144  }
145  }
146  } else {
147  valid = false;
148  diag = EmptyLabel;
149  }
150  } else {
151  valid = false;
152  diag = EmptyLabel;
153  }
154 
155 
156  if (valid && checkDNS) {
157  QDnsLookup alookup(QDnsLookup::A, v);
158  QEventLoop aloop;
159  QObject::connect(&alookup, &QDnsLookup::finished, &aloop, &QEventLoop::quit);
160  QTimer::singleShot(3100, &alookup, &QDnsLookup::abort);
161  alookup.lookup();
162  aloop.exec();
163 
164  if (((alookup.error() != QDnsLookup::NoError) && (alookup.error() != QDnsLookup::OperationCancelledError)) || alookup.hostAddressRecords().empty()) {
165  QDnsLookup aaaaLookup(QDnsLookup::AAAA, v);
166  QEventLoop aaaaLoop;
167  QObject::connect(&aaaaLookup, &QDnsLookup::finished, &aaaaLoop, &QEventLoop::quit);
168  QTimer::singleShot(3100, &aaaaLookup, &QDnsLookup::abort);
169  aaaaLookup.lookup();
170  aaaaLoop.exec();
171 
172  if (((aaaaLookup.error() != QDnsLookup::NoError) && (aaaaLookup.error() != QDnsLookup::OperationCancelledError)) || aaaaLookup.hostAddressRecords().empty()) {
173  valid = false;
174  diag = MissingDNS;
175  } else if (aaaaLookup.error() == QDnsLookup::OperationCancelledError) {
176  valid = false;
177  diag = DNSTimeout;
178  }
179  } else if (alookup.error() == QDnsLookup::OperationCancelledError) {
180  valid = false;
181  diag = DNSTimeout;
182  }
183  }
184 
185  if (diagnose) {
186  *diagnose = diag;
187  }
188 
189  if (valid && extractedValue) {
190  if (hasRootDot) {
191  *extractedValue = v + QLatin1Char('.');
192  } else {
193  *extractedValue = v;
194  }
195  }
196 
197  return valid;
198 }
199 
200 QString ValidatorDomain::diagnoseString(Context *c, Diagnose diagnose, const QString &label)
201 {
202  QString error;
203 
204  if (label.isEmpty()) {
205  switch (diagnose) {
206  case MissingDNS:
207  error = c->translate("Cutelyst::ValidatorDomain", "The domain name seems to be valid but could not be found in the domain name system.");
208  break;
209  case InvalidChars:
210  error = c->translate("Cutelyst::ValidatorDomain", "The domain name contains characters that are not allowed.");
211  break;
212  case LabelTooLong:
213  error = c->translate("Cutelyst::ValidatorDomain", "At least one of the sections separated by dots exceeds the maximum allowed length of 63 characters. Note that internationalized domain names can be longer internally than they are displayed.");
214  break;
215  case TooLong:
216  error = c->translate("Cutelyst::ValidatorDomain", "The full name of the domain must not be longer than 253 characters. Note that internationalized domain names can be longer internally than they are displayed.");
217  break;
218  case InvalidLabelCount:
219  error = c->translate("Cutelyst::ValidatorDomain", "This is not a valid domain name because it has either no parts (is empty) or only has a top level domain.");
220  break;
221  case EmptyLabel:
222  error = c->translate("Cutelyst::ValidatorDomain", "At least one of the sections separated by dots is empty. Check whether you have entered two dots consecutively.");
223  break;
224  case InvalidTLD:
225  error = c->translate("Cutelyst::ValidatorDomain", "The top level domain (last part) contains characters that are not allowed, like digits and/or dashes.");
226  break;
227  case DashStart:
228  error = c->translate("Cutelyst::ValidatorDomain", "Domain name sections are not allowed to start with a dash.");
229  break;
230  case DashEnd:
231  error = c->translate("Cutelyst::ValidatorDomain", "Domain name sections are not allowed to end with a dash.");
232  break;
233  case DigitStart:
234  error = c->translate("Cutelyst::ValidatorDomain", "Domain name sections are not allowed to start with a digit.");
235  break;
236  case Valid:
237  error = c->translate("Cutelyst::ValidatorDomain", "The domain name is valid.");
238  break;
239  case DNSTimeout:
240  error = c->translate("Cutelyst::ValidatorDomain", "The DNS lookup was aborted because it took too long.");
241  break;
242  default:
243  Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
244  break;
245  }
246  } else {
247  switch (diagnose) {
248  case MissingDNS:
249  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1“ field seems to be valid but could not be found in the domain name system.").arg(label);
250  break;
251  case InvalidChars:
252  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1“ field contains characters that are not allowed.").arg(label);
253  break;
254  case LabelTooLong:
255  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1“ field is not valid because at least one of the sections separated by dots exceeds the maximum allowed length of 63 characters. Note that internationalized domain names can be longer internally than they are displayed.").arg(label);
256  break;
257  case TooLong:
258  error = c->translate("Cutelyst::ValidatorDomain", "The full name of the domain in the “%1” field must not be longer than 253 characters. Note that internationalized domain names can be longer internally than they are displayed.").arg(label);
259  break;
260  case InvalidLabelCount:
261  error = c->translate("Cutelyst::ValidatorDomain", "The “%1” field does not contain a valid domain name because it has either no parts (is empty) or only has a top level domain.").arg(label);
262  break;
263  case EmptyLabel:
264  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1“ field is not valid because at least one of the sections separated by dots is empty. Check whether you have entered two dots consecutively.").arg(label);
265  break;
266  case InvalidTLD:
267  error = c->translate("Cutelyst::ValidatorDomain", "The top level domain (last part) of the domain name in the “%1” field contains characters that are not allowed, like digits and or dashes.").arg(label);
268  break;
269  case DashStart:
270  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1“ field is not valid because domain name sections are not allowed to start with a dash.").arg(label);
271  break;
272  case DashEnd:
273  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1“ field is not valid because domain name sections are not allowed to end with a dash.").arg(label);
274  break;
275  case DigitStart:
276  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1“ field is not valid because domain name sections are not allowed to start with a digit.").arg(label);
277  break;
278  case Valid:
279  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1” field is valid.").arg(label);
280  break;
281  case DNSTimeout:
282  error = c->translate("Cutelyst::ValidatorDomain", "The DNS lookup for the domain name in the “%1” field was aborted because it took too long.").arg(label);
283  break;
284  default:
285  Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
286  break;
287  }
288  }
289 
290  return error;
291 }
292 
294 {
295  ValidatorReturnType result;
296 
297  const QString &v = value(params);
298 
299  if (!v.isEmpty()) {
300  Q_D(const ValidatorDomain);
301  QString exVal;
302  Diagnose diag;
303  if (ValidatorDomain::validate(v, d->checkDNS, &diag, &exVal)) {
304  result.value.setValue(exVal);
305  } else {
306  result.errorMessage = validationError(c, diag);
307  }
308  } else {
309  defaultValue(c, &result, "ValidatorDomain");
310  }
311 
312  return result;
313 }
314 
315 QString ValidatorDomain::genericValidationError(Context *c, const QVariant &errorData) const
316 {
317  QString error;
318  const QString _label = label(c);
319  const Diagnose diag = errorData.value<Diagnose>();
320  error = ValidatorDomain::diagnoseString(c, diag, _label);
321  return error;
322 }
323 
324 #include "moc_validatordomain.cpp"
The Cutelyst Context.
Definition: context.h:39
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
Definition: context.cpp:477
Checks if the value of the input field contains 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())
Returns a human readable description of a Diagnose.
ValidatorDomain(const QString &field, bool checkDNS=false, const ValidatorMessages &messages=ValidatorMessages(), const QString &defValKey=QString())
Constructs a new ValidatorDomain with the given parameters.
~ValidatorDomain() override
Deconstructs ValidatorDomain.
Diagnose
Possible diagnose information for the checked domain.
Base class for all validator rules.
QString label(Context *c) const
Returns the human readable field label used for generic error messages.
void defaultValue(Context *c, ValidatorReturnType *result, const char *validatorName) const
I a defValKey has been set in the constructor, this will try to get the default value from the stash ...
QString value(const ParamsMultiMap &params) const
Returns the value of the field from the input params.
QString validationError(Context *c, const QVariant &errorData=QVariant()) const
Returns a descriptive error message if validation failed.
static bool validate(const QString &value, bool checkDNS, Diagnose *diagnose=nullptr, QString *extractedValue=nullptr)
Returns true if value is a valid domain name.
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:8
QMultiMap< QString, QString > ParamsMultiMap
Stores custom error messages and the input field label.
Contains the result of a single input parameter validation.
Definition: validatorrule.h:49