You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
109 lines
3.1 KiB
109 lines
3.1 KiB
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import urllib.parse
|
|
from typing import Optional, Tuple
|
|
|
|
from . import exceptions
|
|
|
|
|
|
__all__ = ["parse_uri", "WebSocketURI"]
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class WebSocketURI:
|
|
"""
|
|
WebSocket URI.
|
|
|
|
Attributes:
|
|
secure: :obj:`True` for a ``wss`` URI, :obj:`False` for a ``ws`` URI.
|
|
host: Normalized to lower case.
|
|
port: Always set even if it's the default.
|
|
path: May be empty.
|
|
query: May be empty if the URI doesn't include a query component.
|
|
username: Available when the URI contains `User Information`_.
|
|
password: Available when the URI contains `User Information`_.
|
|
|
|
.. _User Information: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.1
|
|
|
|
"""
|
|
|
|
secure: bool
|
|
host: str
|
|
port: int
|
|
path: str
|
|
query: str
|
|
username: Optional[str]
|
|
password: Optional[str]
|
|
|
|
@property
|
|
def resource_name(self) -> str:
|
|
if self.path:
|
|
resource_name = self.path
|
|
else:
|
|
resource_name = "/"
|
|
if self.query:
|
|
resource_name += "?" + self.query
|
|
return resource_name
|
|
|
|
@property
|
|
def user_info(self) -> Optional[Tuple[str, str]]:
|
|
if self.username is None:
|
|
return None
|
|
assert self.password is not None
|
|
return (self.username, self.password)
|
|
|
|
|
|
# All characters from the gen-delims and sub-delims sets in RFC 3987.
|
|
DELIMS = ":/?#[]@!$&'()*+,;="
|
|
|
|
|
|
def parse_uri(uri: str) -> WebSocketURI:
|
|
"""
|
|
Parse and validate a WebSocket URI.
|
|
|
|
Args:
|
|
uri: WebSocket URI.
|
|
|
|
Returns:
|
|
WebSocketURI: Parsed WebSocket URI.
|
|
|
|
Raises:
|
|
InvalidURI: if ``uri`` isn't a valid WebSocket URI.
|
|
|
|
"""
|
|
parsed = urllib.parse.urlparse(uri)
|
|
if parsed.scheme not in ["ws", "wss"]:
|
|
raise exceptions.InvalidURI(uri, "scheme isn't ws or wss")
|
|
if parsed.hostname is None:
|
|
raise exceptions.InvalidURI(uri, "hostname isn't provided")
|
|
if parsed.fragment != "":
|
|
raise exceptions.InvalidURI(uri, "fragment identifier is meaningless")
|
|
|
|
secure = parsed.scheme == "wss"
|
|
host = parsed.hostname
|
|
port = parsed.port or (443 if secure else 80)
|
|
path = parsed.path
|
|
query = parsed.query
|
|
username = parsed.username
|
|
password = parsed.password
|
|
# urllib.parse.urlparse accepts URLs with a username but without a
|
|
# password. This doesn't make sense for HTTP Basic Auth credentials.
|
|
if username is not None and password is None:
|
|
raise exceptions.InvalidURI(uri, "username provided without password")
|
|
|
|
try:
|
|
uri.encode("ascii")
|
|
except UnicodeEncodeError:
|
|
# Input contains non-ASCII characters.
|
|
# It must be an IRI. Convert it to a URI.
|
|
host = host.encode("idna").decode()
|
|
path = urllib.parse.quote(path, safe=DELIMS)
|
|
query = urllib.parse.quote(query, safe=DELIMS)
|
|
if username is not None:
|
|
assert password is not None
|
|
username = urllib.parse.quote(username, safe=DELIMS)
|
|
password = urllib.parse.quote(password, safe=DELIMS)
|
|
|
|
return WebSocketURI(secure, host, port, path, query, username, password)
|