__init__.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import re
  2. from fqdn._compat import cached_property
  3. class FQDN:
  4. """
  5. From https://tools.ietf.org/html/rfc1035#page-9, RFC 1035 3.1. Name space
  6. definitions:
  7. Domain names in messages are expressed in terms of a sequence of
  8. labels. Each label is represented as a one octet length field followed
  9. by that number of octets. Since every domain name ends with the null
  10. label of the root, a domain name is terminated by a length byte of
  11. zero. The high order two bits of every length octet must be zero, and
  12. the remaining six bits of the length field limit the label to 63 octets
  13. or less.
  14. To simplify implementations, the total length of a domain name (i.e.,
  15. label octets and label length octets) is restricted to 255 octets or
  16. less.
  17. Therefore the max length of a domain name is actually 253 ASCII bytes
  18. without the trailing null byte or the leading length byte, and the max
  19. length of a label is 63 bytes without the leading length byte.
  20. """
  21. PREFERRED_NAME_SYNTAX_REGEXSTR = (
  22. r"^((?![-])[-A-Z\d]{1,63}(?<!-)[.])*(?!-)[-A-Z\d]{1,63}(?<!-)[.]?$"
  23. )
  24. ALLOW_UNDERSCORES_REGEXSTR = (
  25. r"^((?![-])[-_A-Z\d]{1,63}(?<!-)[.])*(?!-)[-_A-Z\d]{1,63}(?<!-)[.]?$"
  26. )
  27. def __init__(self, fqdn, *nothing, **kwargs):
  28. if nothing:
  29. raise ValueError("got extra positional parameter, try kwargs")
  30. unknown_kwargs = set(kwargs.keys()) - {"allow_underscores", "min_labels"}
  31. if unknown_kwargs:
  32. raise ValueError("got extra kwargs: {}".format(unknown_kwargs))
  33. if not (fqdn and isinstance(fqdn, str)):
  34. raise ValueError("fqdn must be str")
  35. self._fqdn = fqdn.lower()
  36. self._allow_underscores = kwargs.get("allow_underscores", False)
  37. self._min_labels = kwargs.get("min_labels", 2)
  38. def __str__(self):
  39. """
  40. The FQDN as a string in absolute form
  41. """
  42. return self.absolute
  43. @property
  44. def _regex(self):
  45. regexstr = (
  46. FQDN.PREFERRED_NAME_SYNTAX_REGEXSTR
  47. if not self._allow_underscores
  48. else FQDN.ALLOW_UNDERSCORES_REGEXSTR
  49. )
  50. return re.compile(regexstr, re.IGNORECASE)
  51. @cached_property
  52. def is_valid(self):
  53. """
  54. True for a validated fully-qualified domain nam (FQDN), in full
  55. compliance with RFC 1035, and the "preferred form" specified in RFC
  56. 3686 s. 2, whether relative or absolute.
  57. https://tools.ietf.org/html/rfc3696#section-2
  58. https://tools.ietf.org/html/rfc1035
  59. If and only if the FQDN ends with a dot (in place of the RFC1035
  60. trailing null byte), it may have a total length of 254 bytes, still it
  61. must be less than 253 bytes.
  62. """
  63. length = len(self._fqdn)
  64. if self._fqdn.endswith("."):
  65. length -= 1
  66. if length > 253:
  67. return False
  68. regex_pass = self._regex.match(self._fqdn)
  69. if not regex_pass:
  70. return False
  71. return self.labels_count >= self._min_labels
  72. @property
  73. def labels_count(self):
  74. has_terminal_dot = self._fqdn[-1] == "."
  75. count = self._fqdn.count(".") + (0 if has_terminal_dot else 1)
  76. return count
  77. @cached_property
  78. def is_valid_absolute(self):
  79. """
  80. True for a fully-qualified domain name (FQDN) that is RFC
  81. preferred-form compliant and ends with a `.`.
  82. With relative FQDNS in DNS lookups, the current hosts domain name or
  83. search domains may be appended.
  84. """
  85. return self._fqdn.endswith(".") and self.is_valid
  86. @cached_property
  87. def is_valid_relative(self):
  88. """
  89. True for a validated fully-qualified domain name that compiles with the
  90. RFC preferred-form and does not ends with a `.`.
  91. """
  92. return not self._fqdn.endswith(".") and self.is_valid
  93. @cached_property
  94. def absolute(self):
  95. """
  96. The FQDN as a string in absolute form
  97. """
  98. if not self.is_valid:
  99. raise ValueError("invalid FQDN `{0}`".format(self._fqdn))
  100. if self.is_valid_absolute:
  101. return self._fqdn
  102. return "{0}.".format(self._fqdn)
  103. @cached_property
  104. def relative(self):
  105. """
  106. The FQDN as a string in relative form
  107. """
  108. if not self.is_valid:
  109. raise ValueError("invalid FQDN `{0}`".format(self._fqdn))
  110. if self.is_valid_absolute:
  111. return self._fqdn[:-1]
  112. return self._fqdn
  113. def __eq__(self, other):
  114. if isinstance(other, FQDN):
  115. return self.absolute == other.absolute
  116. def __hash__(self):
  117. return hash(self.absolute) + hash("fqdn")