Cutelyst  3.5.0
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(QLatin1Char('.'))) {
33  hasRootDot = true;
34  _v.chop(1);
35  }
36 
37  // convert to lower case puny code
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 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
63  const QStringList parts = v.split(QLatin1Char('.'), Qt::KeepEmptyParts);
64 #else
65  const QStringList parts = v.split(QLatin1Char('.'), QString::KeepEmptyParts);
66 #endif
67  // there has to be more than only the TLD
68  if (parts.size() > 1) {
69  // the TLD can not have only 1 char
70  if (parts.last().length() > 1) {
71  for (int i = 0; i < parts.size(); ++i) {
72  if (valid) {
73  const QString part = parts.at(i);
74  if (!part.isEmpty()) {
75  // labels/parts can have a maximum length of 63 chars
76  if (part.length() < 64) {
77  bool isTld = (i == (parts.size() -1));
78  bool isPunyCode = part.startsWith(u"xn--");
79  for (int j = 0; j < part.size(); ++j) {
80  const ushort &uc = part.at(j).unicode();
81  const bool isDigit = ((uc > 47) && (uc < 58));
82  const bool isDash = (uc == 45);
83  // no part/label can start with a digit or a dash
84  if ((j == 0) && (isDash || isDigit)) {
85  valid = false;
86  diag = isDash ? DashStart : DigitStart;
87  break;
88  }
89  // no part/label can end with a dash
90  if ((j == (part.size() - 1)) && isDash) {
91  valid = false;
92  diag = DashEnd;
93  break;
94  }
95  const bool isChar = ((uc > 96) && (uc < 123));
96  if (!isTld) {
97  // if it is not the tld, it can have a-z 0-9 and -
98  if (!(isDigit || isDash || isChar)) {
99  valid = false;
100  diag = InvalidChars;
101  break;
102  }
103  } else {
104  if (isPunyCode) {
105  if (!(isDigit || isDash || isChar)) {
106  valid = false;
107  diag = InvalidTLD;
108  break;
109  }
110  } else {
111  if (!isChar) {
112  valid = false;
113  diag = InvalidTLD;
114  break;
115  }
116  }
117  }
118  }
119  } else {
120  valid = false;
121  diag = LabelTooLong;
122  break;
123  }
124  } else {
125  valid = false;
126  diag = EmptyLabel;
127  break;
128  }
129  } else {
130  break;
131  }
132  }
133  } else {
134  valid = false;
135  diag = InvalidTLD;
136  }
137  } else {
138  valid = false;
139  diag = InvalidLabelCount;
140  }
141  } else {
142  valid = false;
143  diag = TooLong;
144  }
145  } else {
146  valid = false;
147  diag = EmptyLabel;
148  }
149  }
150  } else {
151  valid = false;
152  diag = EmptyLabel;
153  }
154  } else {
155  valid = false;
156  diag = EmptyLabel;
157  }
158 
159 
160  if (valid && checkDNS) {
161  QDnsLookup alookup(QDnsLookup::A, v);
162  QEventLoop aloop;
164  QTimer::singleShot(3100, &alookup, &QDnsLookup::abort);
165  alookup.lookup();
166  aloop.exec();
167 
168  if (((alookup.error() != QDnsLookup::NoError) && (alookup.error() != QDnsLookup::OperationCancelledError)) || alookup.hostAddressRecords().empty()) {
169  QDnsLookup aaaaLookup(QDnsLookup::AAAA, v);
170  QEventLoop aaaaLoop;
171  QObject::connect(&aaaaLookup, &QDnsLookup::finished, &aaaaLoop, &QEventLoop::quit);
172  QTimer::singleShot(3100, &aaaaLookup, &QDnsLookup::abort);
173  aaaaLookup.lookup();
174  aaaaLoop.exec();
175 
176  if (((aaaaLookup.error() != QDnsLookup::NoError) && (aaaaLookup.error() != QDnsLookup::OperationCancelledError)) || aaaaLookup.hostAddressRecords().empty()) {
177  valid = false;
178  diag = MissingDNS;
179  } else if (aaaaLookup.error() == QDnsLookup::OperationCancelledError) {
180  valid = false;
181  diag = DNSTimeout;
182  }
183  } else if (alookup.error() == QDnsLookup::OperationCancelledError) {
184  valid = false;
185  diag = DNSTimeout;
186  }
187  }
188 
189  if (diagnose) {
190  *diagnose = diag;
191  }
192 
193  if (valid && extractedValue) {
194  if (hasRootDot) {
195  *extractedValue = v + QLatin1Char('.');
196  } else {
197  *extractedValue = v;
198  }
199  }
200 
201  return valid;
202 }
203 
205 {
206  QString error;
207 
208  if (label.isEmpty()) {
209  switch (diagnose) {
210  case MissingDNS:
211  error = c->translate("Cutelyst::ValidatorDomain", "The domain name seems to be valid but could not be found in the domain name system.");
212  break;
213  case InvalidChars:
214  error = c->translate("Cutelyst::ValidatorDomain", "The domain name contains characters that are not allowed.");
215  break;
216  case LabelTooLong:
217  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.");
218  break;
219  case TooLong:
220  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.");
221  break;
222  case InvalidLabelCount:
223  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.");
224  break;
225  case EmptyLabel:
226  error = c->translate("Cutelyst::ValidatorDomain", "At least one of the sections separated by dots is empty. Check whether you have entered two dots consecutively.");
227  break;
228  case InvalidTLD:
229  error = c->translate("Cutelyst::ValidatorDomain", "The top level domain (last part) contains characters that are not allowed, like digits and/or dashes.");
230  break;
231  case DashStart:
232  error = c->translate("Cutelyst::ValidatorDomain", "Domain name sections are not allowed to start with a dash.");
233  break;
234  case DashEnd:
235  error = c->translate("Cutelyst::ValidatorDomain", "Domain name sections are not allowed to end with a dash.");
236  break;
237  case DigitStart:
238  error = c->translate("Cutelyst::ValidatorDomain", "Domain name sections are not allowed to start with a digit.");
239  break;
240  case Valid:
241  error = c->translate("Cutelyst::ValidatorDomain", "The domain name is valid.");
242  break;
243  case DNSTimeout:
244  error = c->translate("Cutelyst::ValidatorDomain", "The DNS lookup was aborted because it took too long.");
245  break;
246  default:
247  Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
248  break;
249  }
250  } else {
251  switch (diagnose) {
252  case MissingDNS:
253  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);
254  break;
255  case InvalidChars:
256  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1“ field contains characters that are not allowed.").arg(label);
257  break;
258  case LabelTooLong:
259  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);
260  break;
261  case TooLong:
262  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);
263  break;
264  case InvalidLabelCount:
265  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);
266  break;
267  case EmptyLabel:
268  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);
269  break;
270  case InvalidTLD:
271  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);
272  break;
273  case DashStart:
274  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);
275  break;
276  case DashEnd:
277  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);
278  break;
279  case DigitStart:
280  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);
281  break;
282  case Valid:
283  error = c->translate("Cutelyst::ValidatorDomain", "The domain name in the “%1” field is valid.").arg(label);
284  break;
285  case DNSTimeout:
286  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);
287  break;
288  default:
289  Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
290  break;
291  }
292  }
293 
294  return error;
295 }
296 
298 {
299  ValidatorReturnType result;
300 
301  const QString &v = value(params);
302 
303  if (!v.isEmpty()) {
304  Q_D(const ValidatorDomain);
305  QString exVal;
306  Diagnose diag;
307  if (ValidatorDomain::validate(v, d->checkDNS, &diag, &exVal)) {
308  result.value.setValue(exVal);
309  } else {
310  result.errorMessage = validationError(c, diag);
311  }
312  } else {
313  defaultValue(c, &result, "ValidatorDomain");
314  }
315 
316  return result;
317 }
318 
320 {
321  QString error;
322  const QString _label = label(c);
323  const Diagnose diag = errorData.value<Diagnose>();
324  error = ValidatorDomain::diagnoseString(c, diag, _label);
325  return error;
326 }
327 
328 #include "moc_validatordomain.cpp"
QString genericValidationError(Context *c, const QVariant &errorData=QVariant()) const override
Returns a generic error message if validation failed.
void quit()
Checks if the value of the input field contains FQDN according to RFC 1035.
QString validationError(Context *c, const QVariant &errorData=QVariant()) const
Returns a descriptive error message if validation failed.
Stores custom error messages and the input field label.
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
const T & at(int i) const const
int size() const const
T value() const const
void chop(int n)
int size() const const
void finished()
The Cutelyst Context.
Definition: context.h:38
void abort()
bool empty() const const
int exec(QEventLoop::ProcessEventsFlags flags)
QByteArray toAce(const QString &domain)
bool isEmpty() const const
void lookup()
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
Definition: context.cpp:471
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:7
static QString diagnoseString(Context *c, Diagnose diagnose, const QString &label=QString())
Returns a human readable description of a Diagnose.
Base class for all validator rules.
Diagnose
Possible diagnose information for the checked domain.
QString label(Context *c) const
Returns the human readable field label used for generic error messages.
ValidatorDomain(const QString &field, bool checkDNS=false, const ValidatorMessages &messages=ValidatorMessages(), const QString &defValKey=QString())
Constructs a new ValidatorDomain with the given parameters.
ushort unicode() const const
QString toLower() const const
QList< QDnsHostAddressRecord > hostAddressRecords() const const
void setValue(const T &value)
QString value(const ParamsMultiMap &params) const
Returns the value of the field from the input params.
static bool validate(const QString &value, bool checkDNS, Diagnose *diagnose=nullptr, QString *extractedValue=nullptr)
Returns true if value is a valid domain name.
const QChar at(int position) const const
T & last()
int length() const const
QString fromLatin1(const char *str, int size)
~ValidatorDomain() override
Deconstructs ValidatorDomain.
Contains the result of a single input parameter validation.
Definition: validatorrule.h:49
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
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 ...