cutelyst 4.0.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
14using 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
26bool ValidatorDomain::validate(const QString &value,
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
43 const QString v = QString::fromLatin1(QUrl::toAce(_v)).toLower();
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;
173 QObject::connect(&alookup, &QDnsLookup::finished, &aloop, &QEventLoop::quit);
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);
184 QTimer::singleShot(
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
219QString ValidatorDomain::diagnoseString(Context *c, Diagnose diagnose, const QString &label)
220{
221 QString error;
222
223 if (label.isEmpty()) {
224 switch (diagnose) {
225 case MissingDNS:
226 error = c->translate("Cutelyst::ValidatorDomain",
227 "The domain name seems to be valid but could not be found in the "
228 "domain name system.");
229 break;
230 case InvalidChars:
231 error = c->translate("Cutelyst::ValidatorDomain",
232 "The domain name contains characters that are not allowed.");
233 break;
234 case LabelTooLong:
235 error =
236 c->translate("Cutelyst::ValidatorDomain",
237 "At least one of the sections separated by dots exceeds the maximum "
238 "allowed length of 63 characters. Note that internationalized domain "
239 "names can be longer internally than they are displayed.");
240 break;
241 case TooLong:
242 error = c->translate(
243 "Cutelyst::ValidatorDomain",
244 "The full name of the domain must not be longer than 253 characters. Note that "
245 "internationalized domain names can be longer internally than they are displayed.");
246 break;
248 error = c->translate("Cutelyst::ValidatorDomain",
249 "This is not a valid domain name because it has either no parts "
250 "(is empty) or only has a top level domain.");
251 break;
252 case EmptyLabel:
253 error = c->translate("Cutelyst::ValidatorDomain",
254 "At least one of the sections separated by dots is empty. Check "
255 "whether you have entered two dots consecutively.");
256 break;
257 case InvalidTLD:
258 error = c->translate("Cutelyst::ValidatorDomain",
259 "The top level domain (last part) contains characters that are "
260 "not allowed, like digits and/or dashes.");
261 break;
262 case DashStart:
263 error = c->translate("Cutelyst::ValidatorDomain",
264 "Domain name sections are not allowed to start with a dash.");
265 break;
266 case DashEnd:
267 error = c->translate("Cutelyst::ValidatorDomain",
268 "Domain name sections are not allowed to end with a dash.");
269 break;
270 case DigitStart:
271 error = c->translate("Cutelyst::ValidatorDomain",
272 "Domain name sections are not allowed to start with a digit.");
273 break;
274 case Valid:
275 error = c->translate("Cutelyst::ValidatorDomain", "The domain name is valid.");
276 break;
277 case DNSTimeout:
278 error = c->translate("Cutelyst::ValidatorDomain",
279 "The DNS lookup was aborted because it took too long.");
280 break;
281 default:
282 Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
283 break;
284 }
285 } else {
286 switch (diagnose) {
287 case MissingDNS:
288 error = c->translate("Cutelyst::ValidatorDomain",
289 "The domain name in the “%1“ field seems to be valid but could "
290 "not be found in the domain name system.")
291 .arg(label);
292 break;
293 case InvalidChars:
294 error =
295 c->translate(
296 "Cutelyst::ValidatorDomain",
297 "The domain name in the “%1“ field contains characters that are not allowed.")
298 .arg(label);
299 break;
300 case LabelTooLong:
301 error = c->translate("Cutelyst::ValidatorDomain",
302 "The domain name in the “%1“ field is not valid because at least "
303 "one of the sections separated by dots exceeds the maximum "
304 "allowed length of 63 characters. Note that internationalized "
305 "domain names can be longer internally than they are displayed.")
306 .arg(label);
307 break;
308 case TooLong:
309 error = c->translate("Cutelyst::ValidatorDomain",
310 "The full name of the domain in the “%1” field must not be longer "
311 "than 253 characters. Note that internationalized domain names "
312 "can be longer internally than they are displayed.")
313 .arg(label);
314 break;
316 error = c->translate("Cutelyst::ValidatorDomain",
317 "The “%1” field does not contain a valid domain name because it "
318 "has either no parts (is empty) or only has a top level domain.")
319 .arg(label);
320 break;
321 case EmptyLabel:
322 error = c->translate("Cutelyst::ValidatorDomain",
323 "The domain name in the “%1“ field is not valid because at least "
324 "one of the sections separated by dots is empty. Check whether "
325 "you have entered two dots consecutively.")
326 .arg(label);
327 break;
328 case InvalidTLD:
329 error = c->translate(
330 "Cutelyst::ValidatorDomain",
331 "The top level domain (last part) of the domain name in the “%1” field "
332 "contains characters that are not allowed, like digits and or dashes.")
333 .arg(label);
334 break;
335 case DashStart:
336 error = c->translate("Cutelyst::ValidatorDomain",
337 "The domain name in the “%1“ field is not valid because domain "
338 "name sections are not allowed to start with a dash.")
339 .arg(label);
340 break;
341 case DashEnd:
342 error = c->translate("Cutelyst::ValidatorDomain",
343 "The domain name in the “%1“ field is not valid because domain "
344 "name sections are not allowed to end with a dash.")
345 .arg(label);
346 break;
347 case DigitStart:
348 error = c->translate("Cutelyst::ValidatorDomain",
349 "The domain name in the “%1“ field is not valid because domain "
350 "name sections are not allowed to start with a digit.")
351 .arg(label);
352 break;
353 case Valid:
354 error = c->translate("Cutelyst::ValidatorDomain",
355 "The domain name in the “%1” field is valid.")
356 .arg(label);
357 break;
358 case DNSTimeout:
359 error = c->translate("Cutelyst::ValidatorDomain",
360 "The DNS lookup for the domain name in the “%1” field was aborted "
361 "because it took too long.")
362 .arg(label);
363 break;
364 default:
365 Q_ASSERT_X(false, "domain validation diagnose", "invalid diagnose");
366 break;
367 }
368 }
369
370 return error;
371}
372
374{
375 ValidatorReturnType result;
376
377 const QString &v = value(params);
378
379 if (!v.isEmpty()) {
380 Q_D(const ValidatorDomain);
381 QString exVal;
382 Diagnose diag{Valid};
383 if (ValidatorDomain::validate(v, d->checkDNS, &diag, &exVal)) {
384 result.value.setValue(exVal);
385 } else {
386 result.errorMessage = validationError(c, diag);
387 if (C_VALIDATOR().isDebugEnabled()) {
388 switch (diag) {
389 case Valid:
390 break;
391 case MissingDNS:
392 qCDebug(C_VALIDATOR).noquote()
393 << debugString(c) << "Can not find valid DNS entry for" << v;
394 break;
395 case InvalidChars:
396 qCDebug(C_VALIDATOR).noquote()
397 << debugString(c)
398 << "The domain name contains characters that are not allowed";
399 break;
400 case LabelTooLong:
401 qCDebug(C_VALIDATOR).noquote()
402 << debugString(c)
403 << "At least on of the domain name labels exceeds the maximum"
404 << "size of" << ValidatorDomainPrivate::maxDnsLabelLength << "characters";
405 break;
406 case TooLong:
407 qCDebug(C_VALIDATOR).noquote()
408 << debugString(c) << "The domain name exceeds the maximum size of"
409 << ValidatorDomainPrivate::maxDnsNameWithLastDot << "characters";
410 break;
412 qCDebug(C_VALIDATOR).noquote()
413 << debugString(c) << "Invalid label count. Either no labels or only TLD";
414 break;
415 case EmptyLabel:
416 qCDebug(C_VALIDATOR).noquote()
417 << debugString(c) << "At least one of the domain name labels is empty";
418 break;
419 case InvalidTLD:
420 qCDebug(C_VALIDATOR).noquote()
421 << debugString(c)
422 << "The TLD label contains characters that are not allowed";
423 break;
424 case DashStart:
425 qCDebug(C_VALIDATOR).noquote()
426 << debugString(c) << "At least one label starts with a dash";
427 break;
428 case DashEnd:
429 qCDebug(C_VALIDATOR).noquote()
430 << debugString(c) << "At least one label ends with a dash";
431 break;
432 case DigitStart:
433 qCDebug(C_VALIDATOR).noquote()
434 << debugString(c) << "At least one label start with a digit";
435 break;
436 case DNSTimeout:
437 qCDebug(C_VALIDATOR).noquote()
438 << debugString(c) << "The DNS lookup exceeds the timeout of"
439#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
440 << ValidatorDomainPrivate::dnsLookupTimeout;
441#else
442 << ValidatorDomainPrivate::dnsLookupTimeout.count() << "milliseconds";
443#endif
444 }
445 }
446 }
447 } else {
448 defaultValue(c, &result);
449 }
450
451 return result;
452}
453
454QString ValidatorDomain::genericValidationError(Context *c, const QVariant &errorData) const
455{
456 return ValidatorDomain::diagnoseString(c, errorData.value<Diagnose>(), label(c));
457}
458
459#include "moc_validatordomain.cpp"
The Cutelyst Context.
Definition: context.h:38
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
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.
QString debugString(Context *c) const
Returns a string that can be used for debug output if validation fails.
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