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