uri.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. """
  2. :mod:`websockets.uri` parses WebSocket URIs.
  3. See `section 3 of RFC 6455`_.
  4. .. _section 3 of RFC 6455: http://tools.ietf.org/html/rfc6455#section-3
  5. """
  6. import urllib.parse
  7. from typing import NamedTuple, Optional, Tuple
  8. from .exceptions import InvalidURI
  9. __all__ = ["parse_uri", "WebSocketURI"]
  10. # Consider converting to a dataclass when dropping support for Python < 3.7.
  11. class WebSocketURI(NamedTuple):
  12. """
  13. WebSocket URI.
  14. :param bool secure: secure flag
  15. :param str host: lower-case host
  16. :param int port: port, always set even if it's the default
  17. :param str resource_name: path and optional query
  18. :param str user_info: ``(username, password)`` tuple when the URI contains
  19. `User Information`_, else ``None``.
  20. .. _User Information: https://tools.ietf.org/html/rfc3986#section-3.2.1
  21. """
  22. secure: bool
  23. host: str
  24. port: int
  25. resource_name: str
  26. user_info: Optional[Tuple[str, str]]
  27. # Work around https://bugs.python.org/issue19931
  28. WebSocketURI.secure.__doc__ = ""
  29. WebSocketURI.host.__doc__ = ""
  30. WebSocketURI.port.__doc__ = ""
  31. WebSocketURI.resource_name.__doc__ = ""
  32. WebSocketURI.user_info.__doc__ = ""
  33. # All characters from the gen-delims and sub-delims sets in RFC 3987.
  34. DELIMS = ":/?#[]@!$&'()*+,;="
  35. def parse_uri(uri: str) -> WebSocketURI:
  36. """
  37. Parse and validate a WebSocket URI.
  38. :raises ValueError: if ``uri`` isn't a valid WebSocket URI.
  39. """
  40. parsed = urllib.parse.urlparse(uri)
  41. try:
  42. assert parsed.scheme in ["ws", "wss"]
  43. assert parsed.params == ""
  44. assert parsed.fragment == ""
  45. assert parsed.hostname is not None
  46. except AssertionError as exc:
  47. raise InvalidURI(uri) from exc
  48. secure = parsed.scheme == "wss"
  49. host = parsed.hostname
  50. port = parsed.port or (443 if secure else 80)
  51. resource_name = parsed.path or "/"
  52. if parsed.query:
  53. resource_name += "?" + parsed.query
  54. user_info = None
  55. if parsed.username is not None:
  56. # urllib.parse.urlparse accepts URLs with a username but without a
  57. # password. This doesn't make sense for HTTP Basic Auth credentials.
  58. if parsed.password is None:
  59. raise InvalidURI(uri)
  60. user_info = (parsed.username, parsed.password)
  61. try:
  62. uri.encode("ascii")
  63. except UnicodeEncodeError:
  64. # Input contains non-ASCII characters.
  65. # It must be an IRI. Convert it to a URI.
  66. host = host.encode("idna").decode()
  67. resource_name = urllib.parse.quote(resource_name, safe=DELIMS)
  68. if user_info is not None:
  69. user_info = (
  70. urllib.parse.quote(user_info[0], safe=DELIMS),
  71. urllib.parse.quote(user_info[1], safe=DELIMS),
  72. )
  73. return WebSocketURI(secure, host, port, resource_name, user_info)