Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 1 addition & 121 deletions zeroconf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
ServiceNameAlreadyRegistered,
)
from .logger import QuietLogger, log
from .utils.name import service_type_name
from .utils.net import ( # noqa # import needed for backwards compat
add_multicast_member,
can_send_to,
Expand Down Expand Up @@ -156,127 +157,6 @@
# utility functions


def service_type_name(type_: str, *, strict: bool = True) -> str: # pylint: disable=too-many-branches
"""
Validate a fully qualified service name, instance or subtype. [rfc6763]

Returns fully qualified service name.

Domain names used by mDNS-SD take the following forms:

<sn> . <_tcp|_udp> . local.
<Instance> . <sn> . <_tcp|_udp> . local.
<sub>._sub . <sn> . <_tcp|_udp> . local.

1) must end with 'local.'

This is true because we are implementing mDNS and since the 'm' means
multi-cast, the 'local.' domain is mandatory.

2) local is preceded with either '_udp.' or '_tcp.' unless
strict is False

3) service name <sn> precedes <_tcp|_udp> unless
strict is False

The rules for Service Names [RFC6335] state that they may be no more
than fifteen characters long (not counting the mandatory underscore),
consisting of only letters, digits, and hyphens, must begin and end
with a letter or digit, must not contain consecutive hyphens, and
must contain at least one letter.

The instance name <Instance> and sub type <sub> may be up to 63 bytes.

The portion of the Service Instance Name is a user-
friendly name consisting of arbitrary Net-Unicode text [RFC5198]. It
MUST NOT contain ASCII control characters (byte values 0x00-0x1F and
0x7F) [RFC20] but otherwise is allowed to contain any characters,
without restriction, including spaces, uppercase, lowercase,
punctuation -- including dots -- accented characters, non-Roman text,
and anything else that may be represented using Net-Unicode.

:param type_: Type, SubType or service name to validate
:return: fully qualified service name (eg: _http._tcp.local.)
"""

if type_.endswith((_TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)):
remaining = type_[: -len(_TCP_PROTOCOL_LOCAL_TRAILER)].split('.')
trailer = type_[-len(_TCP_PROTOCOL_LOCAL_TRAILER) :]
has_protocol = True
elif strict:
raise BadTypeInNameException(
"Type '%s' must end with '%s' or '%s'"
% (type_, _TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)
)
elif type_.endswith(_LOCAL_TRAILER):
remaining = type_[: -len(_LOCAL_TRAILER)].split('.')
trailer = type_[-len(_LOCAL_TRAILER) + 1 :]
has_protocol = False
else:
raise BadTypeInNameException("Type '%s' must end with '%s'" % (type_, _LOCAL_TRAILER))

if strict or has_protocol:
service_name = remaining.pop()
if not service_name:
raise BadTypeInNameException("No Service name found")

if len(remaining) == 1 and len(remaining[0]) == 0:
raise BadTypeInNameException("Type '%s' must not start with '.'" % type_)

if service_name[0] != '_':
raise BadTypeInNameException("Service name (%s) must start with '_'" % service_name)

test_service_name = service_name[1:]

if len(test_service_name) > 15:
raise BadTypeInNameException("Service name (%s) must be <= 15 bytes" % test_service_name)

if '--' in test_service_name:
raise BadTypeInNameException("Service name (%s) must not contain '--'" % test_service_name)

if '-' in (test_service_name[0], test_service_name[-1]):
raise BadTypeInNameException(
"Service name (%s) may not start or end with '-'" % test_service_name
)

if not _HAS_A_TO_Z.search(test_service_name):
raise BadTypeInNameException(
"Service name (%s) must contain at least one letter (eg: 'A-Z')" % test_service_name
)

allowed_characters_re = (
_HAS_ONLY_A_TO_Z_NUM_HYPHEN if strict else _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE
)

if not allowed_characters_re.search(test_service_name):
raise BadTypeInNameException(
"Service name (%s) must contain only these characters: "
"A-Z, a-z, 0-9, hyphen ('-')%s" % (test_service_name, "" if strict else ", underscore ('_')")
)
else:
service_name = ''

if remaining and remaining[-1] == '_sub':
remaining.pop()
if len(remaining) == 0 or len(remaining[0]) == 0:
raise BadTypeInNameException("_sub requires a subtype name")

if len(remaining) > 1:
remaining = ['.'.join(remaining)]

if remaining:
length = len(remaining[0].encode('utf-8'))
if length > 63:
raise BadTypeInNameException("Too long: '%s'" % remaining[0])

if _HAS_ASCII_CONTROL_CHARS.search(remaining[0]):
raise BadTypeInNameException(
"Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" % remaining[0]
)

return service_name + trailer


def instance_name_from_service_info(info: "ServiceInfo") -> str:
"""Calculate the instance name from the ServiceInfo."""
# This is kind of funky because of the subtype based tests
Expand Down
153 changes: 153 additions & 0 deletions zeroconf/utils/name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine

This module provides a framework for the use of DNS Service Discovery
using IP multicast.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
USA
"""

from ..const import (
_HAS_ASCII_CONTROL_CHARS,
_HAS_A_TO_Z,
_HAS_ONLY_A_TO_Z_NUM_HYPHEN,
_HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE,
_LOCAL_TRAILER,
_NONTCP_PROTOCOL_LOCAL_TRAILER,
_TCP_PROTOCOL_LOCAL_TRAILER,
)
from ..exceptions import BadTypeInNameException


def service_type_name(type_: str, *, strict: bool = True) -> str: # pylint: disable=too-many-branches
"""
Validate a fully qualified service name, instance or subtype. [rfc6763]

Returns fully qualified service name.

Domain names used by mDNS-SD take the following forms:

<sn> . <_tcp|_udp> . local.
<Instance> . <sn> . <_tcp|_udp> . local.
<sub>._sub . <sn> . <_tcp|_udp> . local.

1) must end with 'local.'

This is true because we are implementing mDNS and since the 'm' means
multi-cast, the 'local.' domain is mandatory.

2) local is preceded with either '_udp.' or '_tcp.' unless
strict is False

3) service name <sn> precedes <_tcp|_udp> unless
strict is False

The rules for Service Names [RFC6335] state that they may be no more
than fifteen characters long (not counting the mandatory underscore),
consisting of only letters, digits, and hyphens, must begin and end
with a letter or digit, must not contain consecutive hyphens, and
must contain at least one letter.

The instance name <Instance> and sub type <sub> may be up to 63 bytes.

The portion of the Service Instance Name is a user-
friendly name consisting of arbitrary Net-Unicode text [RFC5198]. It
MUST NOT contain ASCII control characters (byte values 0x00-0x1F and
0x7F) [RFC20] but otherwise is allowed to contain any characters,
without restriction, including spaces, uppercase, lowercase,
punctuation -- including dots -- accented characters, non-Roman text,
and anything else that may be represented using Net-Unicode.

:param type_: Type, SubType or service name to validate
:return: fully qualified service name (eg: _http._tcp.local.)
"""

if type_.endswith((_TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)):
remaining = type_[: -len(_TCP_PROTOCOL_LOCAL_TRAILER)].split('.')
trailer = type_[-len(_TCP_PROTOCOL_LOCAL_TRAILER) :]
has_protocol = True
elif strict:
raise BadTypeInNameException(
"Type '%s' must end with '%s' or '%s'"
% (type_, _TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)
)
elif type_.endswith(_LOCAL_TRAILER):
remaining = type_[: -len(_LOCAL_TRAILER)].split('.')
trailer = type_[-len(_LOCAL_TRAILER) + 1 :]
has_protocol = False
else:
raise BadTypeInNameException("Type '%s' must end with '%s'" % (type_, _LOCAL_TRAILER))

if strict or has_protocol:
service_name = remaining.pop()
if not service_name:
raise BadTypeInNameException("No Service name found")

if len(remaining) == 1 and len(remaining[0]) == 0:
raise BadTypeInNameException("Type '%s' must not start with '.'" % type_)

if service_name[0] != '_':
raise BadTypeInNameException("Service name (%s) must start with '_'" % service_name)

test_service_name = service_name[1:]

if len(test_service_name) > 15:
raise BadTypeInNameException("Service name (%s) must be <= 15 bytes" % test_service_name)

if '--' in test_service_name:
raise BadTypeInNameException("Service name (%s) must not contain '--'" % test_service_name)

if '-' in (test_service_name[0], test_service_name[-1]):
raise BadTypeInNameException(
"Service name (%s) may not start or end with '-'" % test_service_name
)

if not _HAS_A_TO_Z.search(test_service_name):
raise BadTypeInNameException(
"Service name (%s) must contain at least one letter (eg: 'A-Z')" % test_service_name
)

allowed_characters_re = (
_HAS_ONLY_A_TO_Z_NUM_HYPHEN if strict else _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE
)

if not allowed_characters_re.search(test_service_name):
raise BadTypeInNameException(
"Service name (%s) must contain only these characters: "
"A-Z, a-z, 0-9, hyphen ('-')%s" % (test_service_name, "" if strict else ", underscore ('_')")
)
else:
service_name = ''

if remaining and remaining[-1] == '_sub':
remaining.pop()
if len(remaining) == 0 or len(remaining[0]) == 0:
raise BadTypeInNameException("_sub requires a subtype name")

if len(remaining) > 1:
remaining = ['.'.join(remaining)]

if remaining:
length = len(remaining[0].encode('utf-8'))
if length > 63:
raise BadTypeInNameException("Too long: '%s'" % remaining[0])

if _HAS_ASCII_CONTROL_CHARS.search(remaining[0]):
raise BadTypeInNameException(
"Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" % remaining[0]
)

return service_name + trailer