tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

client.py (30708B)


      1 # mypy: allow-untyped-defs
      2 
      3 from typing import Dict
      4 from urllib import parse as urlparse
      5 
      6 from . import error
      7 from . import protocol
      8 from . import transport
      9 from .bidi.client import BidiSession
     10 
     11 
     12 class Timeouts:
     13 
     14    def __init__(self, session):
     15        self.session = session
     16 
     17    def _get(self, key=None):
     18        timeouts = self.session.send_session_command("GET", "timeouts")
     19        if key is not None:
     20            return timeouts[key]
     21        return timeouts
     22 
     23    def _set(self, key, secs):
     24        body = {key: secs * 1000}
     25        self.session.send_session_command("POST", "timeouts", body)
     26        return None
     27 
     28    @property
     29    def script(self):
     30        return self._get("script")
     31 
     32    @script.setter
     33    def script(self, secs):
     34        return self._set("script", secs)
     35 
     36    @property
     37    def page_load(self):
     38        return self._get("pageLoad")
     39 
     40    @page_load.setter
     41    def page_load(self, secs):
     42        return self._set("pageLoad", secs)
     43 
     44    @property
     45    def implicit(self):
     46        return self._get("implicit")
     47 
     48    @implicit.setter
     49    def implicit(self, secs):
     50        return self._set("implicit", secs)
     51 
     52    def __str__(self):
     53        name = "%s.%s" % (self.__module__, self.__class__.__name__)
     54        return "<%s script=%d, load=%d, implicit=%d>" % (
     55            name,
     56            self.script,
     57            self.page_load,
     58            self.implicit,
     59        )
     60 
     61 
     62 class ActionSequence:
     63    """API for creating and performing action sequences.
     64 
     65    Each action method adds one or more actions to a queue. When perform()
     66    is called, the queued actions fire in order.
     67 
     68    May be chained together as in::
     69 
     70         ActionSequence(session, "key", id) \
     71            .key_down("a") \
     72            .key_up("a") \
     73            .perform()
     74    """
     75 
     76    def __init__(self, session, action_type, input_id, pointer_params=None):
     77        """Represents a sequence of actions of one type for one input source.
     78 
     79        :param session: WebDriver session.
     80        :param action_type: Action type; may be "none", "key", or "pointer".
     81        :param input_id: ID of input source.
     82        :param pointer_params: Optional dictionary of pointer parameters.
     83        """
     84        self.session = session
     85        self._id = input_id
     86        self._type = action_type
     87        self._actions = []
     88        self._pointer_params = pointer_params
     89 
     90    @property
     91    def dict(self):
     92        d = {
     93            "type": self._type,
     94            "id": self._id,
     95            "actions": self._actions,
     96        }
     97        if self._pointer_params is not None:
     98            d["parameters"] = self._pointer_params
     99        return d
    100 
    101    def perform(self):
    102        """Perform all queued actions."""
    103        self.session.actions.perform([self.dict])
    104 
    105    def _key_action(self, subtype, value):
    106        self._actions.append({"type": subtype, "value": value})
    107 
    108    def _pointer_action(
    109        self,
    110        subtype,
    111        button=None,
    112        x=None,
    113        y=None,
    114        duration=None,
    115        origin=None,
    116        width=None,
    117        height=None,
    118        pressure=None,
    119        tangential_pressure=None,
    120        tilt_x=None,
    121        tilt_y=None,
    122        twist=None,
    123        altitude_angle=None,
    124        azimuth_angle=None,
    125    ):
    126        action = {"type": subtype}
    127        if button is not None:
    128            action["button"] = button
    129        if x is not None:
    130            action["x"] = x
    131        if y is not None:
    132            action["y"] = y
    133        if duration is not None:
    134            action["duration"] = duration
    135        if origin is not None:
    136            action["origin"] = origin
    137        if width is not None:
    138            action["width"] = width
    139        if height is not None:
    140            action["height"] = height
    141        if pressure is not None:
    142            action["pressure"] = pressure
    143        if tangential_pressure is not None:
    144            action["tangentialPressure"] = tangential_pressure
    145        if tilt_x is not None:
    146            action["tiltX"] = tilt_x
    147        if tilt_y is not None:
    148            action["tiltY"] = tilt_y
    149        if twist is not None:
    150            action["twist"] = twist
    151        if altitude_angle is not None:
    152            action["altitudeAngle"] = altitude_angle
    153        if azimuth_angle is not None:
    154            action["azimuthAngle"] = azimuth_angle
    155        self._actions.append(action)
    156 
    157    def pause(self, duration):
    158        self._actions.append({"type": "pause", "duration": duration})
    159        return self
    160 
    161    def pointer_move(
    162        self,
    163        x,
    164        y,
    165        duration=None,
    166        origin=None,
    167        width=None,
    168        height=None,
    169        pressure=None,
    170        tangential_pressure=None,
    171        tilt_x=None,
    172        tilt_y=None,
    173        twist=None,
    174        altitude_angle=None,
    175        azimuth_angle=None,
    176    ):
    177        """Queue a pointerMove action.
    178 
    179        :param x: Destination x-axis coordinate of pointer in CSS pixels.
    180        :param y: Destination y-axis coordinate of pointer in CSS pixels.
    181        :param duration: Number of milliseconds over which to distribute the
    182                         move. If None, remote end defaults to 0.
    183        :param origin: Origin of coordinates, either "viewport", "pointer" or
    184                       an Element. If None, remote end defaults to "viewport".
    185        """
    186        self._pointer_action(
    187            "pointerMove",
    188            x=x,
    189            y=y,
    190            duration=duration,
    191            origin=origin,
    192            width=width,
    193            height=height,
    194            pressure=pressure,
    195            tangential_pressure=tangential_pressure,
    196            tilt_x=tilt_x,
    197            tilt_y=tilt_y,
    198            twist=twist,
    199            altitude_angle=altitude_angle,
    200            azimuth_angle=azimuth_angle,
    201        )
    202        return self
    203 
    204    def pointer_up(self, button=0):
    205        """Queue a pointerUp action for `button`.
    206 
    207        :param button: Pointer button to perform action with.
    208                       Default: 0, which represents main device button.
    209        """
    210        self._pointer_action("pointerUp", button=button)
    211        return self
    212 
    213    def pointer_down(
    214        self,
    215        button=0,
    216        width=None,
    217        height=None,
    218        pressure=None,
    219        tangential_pressure=None,
    220        tilt_x=None,
    221        tilt_y=None,
    222        twist=None,
    223        altitude_angle=None,
    224        azimuth_angle=None,
    225    ):
    226        """Queue a pointerDown action for `button`.
    227 
    228        :param button: Pointer button to perform action with.
    229                       Default: 0, which represents main device button.
    230        """
    231        self._pointer_action(
    232            "pointerDown",
    233            button=button,
    234            width=width,
    235            height=height,
    236            pressure=pressure,
    237            tangential_pressure=tangential_pressure,
    238            tilt_x=tilt_x,
    239            tilt_y=tilt_y,
    240            twist=twist,
    241            altitude_angle=altitude_angle,
    242            azimuth_angle=azimuth_angle,
    243        )
    244        return self
    245 
    246    def click(self, element=None, button=0):
    247        """Queue a click with the specified button.
    248 
    249        If an element is given, move the pointer to that element first,
    250        otherwise click current pointer coordinates.
    251 
    252        :param element: Optional element to click.
    253        :param button: Integer representing pointer button to perform action
    254                       with. Default: 0, which represents main device button.
    255        """
    256        if element:
    257            self.pointer_move(0, 0, origin=element)
    258        return self.pointer_down(button).pointer_up(button)
    259 
    260    def key_up(self, value):
    261        """Queue a keyUp action for `value`.
    262 
    263        :param value: Character to perform key action with.
    264        """
    265        self._key_action("keyUp", value)
    266        return self
    267 
    268    def key_down(self, value):
    269        """Queue a keyDown action for `value`.
    270 
    271        :param value: Character to perform key action with.
    272        """
    273        self._key_action("keyDown", value)
    274        return self
    275 
    276    def send_keys(self, keys):
    277        """Queue a keyDown and keyUp action for each character in `keys`.
    278 
    279        :param keys: String of keys to perform key actions with.
    280        """
    281        for c in keys:
    282            self.key_down(c)
    283            self.key_up(c)
    284        return self
    285 
    286    def scroll(self, x, y, delta_x, delta_y, duration=None, origin=None):
    287        """Queue a scroll action.
    288 
    289        :param x: Destination x-axis coordinate of pointer in CSS pixels.
    290        :param y: Destination y-axis coordinate of pointer in CSS pixels.
    291        :param delta_x: scroll delta on x-axis in CSS pixels.
    292        :param delta_y: scroll delta on y-axis in CSS pixels.
    293        :param duration: Number of milliseconds over which to distribute the
    294                         scroll. If None, remote end defaults to 0.
    295        :param origin: Origin of coordinates, either "viewport" or an Element.
    296                       If None, remote end defaults to "viewport".
    297        """
    298        action = {
    299            "type": "scroll",
    300            "x": x,
    301            "y": y,
    302            "deltaX": delta_x,
    303            "deltaY": delta_y,
    304        }
    305        if duration is not None:
    306            action["duration"] = duration
    307        if origin is not None:
    308            action["origin"] = origin
    309        self._actions.append(action)
    310        return self
    311 
    312 
    313 class Actions:
    314    def __init__(self, session):
    315        self.session = session
    316 
    317    def perform(self, actions=None):
    318        """Performs actions by tick from each action sequence in `actions`.
    319 
    320        :param actions: List of input source action sequences. A single action
    321                        sequence may be created with the help of
    322                        ``ActionSequence.dict``.
    323        """
    324        body = {"actions": [] if actions is None else actions}
    325        actions = self.session.send_session_command("POST", "actions", body)
    326        return actions
    327 
    328    def release(self):
    329        return self.session.send_session_command("DELETE", "actions")
    330 
    331    def sequence(self, *args, **kwargs):
    332        """Return an empty ActionSequence of the designated type.
    333 
    334        See ActionSequence for parameter list.
    335        """
    336        return ActionSequence(self.session, *args, **kwargs)
    337 
    338 
    339 class BrowserWindow:
    340    def __init__(self, session):
    341        self.session = session
    342 
    343    def close(self):
    344        handles = self.session.send_session_command("DELETE", "window")
    345        if handles is not None and len(handles) == 0:
    346            # With no more open top-level browsing contexts, the session is closed.
    347            self.session.session_id = None
    348 
    349        return handles
    350 
    351    @property
    352    def rect(self):
    353        return self.session.send_session_command("GET", "window/rect")
    354 
    355    @rect.setter
    356    def rect(self, new_rect):
    357        self.session.send_session_command("POST", "window/rect", new_rect)
    358 
    359    @property
    360    def size(self):
    361        """Gets the window size as a tuple of `(width, height)`."""
    362        rect = self.rect
    363        return (rect["width"], rect["height"])
    364 
    365    @size.setter
    366    def size(self, new_size):
    367        """Set window size by passing a tuple of `(width, height)`."""
    368        try:
    369            width, height = new_size
    370            body = {"width": width, "height": height}
    371            self.session.send_session_command("POST", "window/rect", body)
    372        except (error.UnknownErrorException, error.InvalidArgumentException):
    373            # silently ignore this error as the command is not implemented
    374            # for Android. Revert this once it is implemented.
    375            pass
    376 
    377    @property
    378    def position(self):
    379        """Gets the window position as a tuple of `(x, y)`."""
    380        rect = self.rect
    381        return (rect["x"], rect["y"])
    382 
    383    @position.setter
    384    def position(self, new_position):
    385        """Set window position by passing a tuple of `(x, y)`."""
    386        try:
    387            x, y = new_position
    388            body = {"x": x, "y": y}
    389            self.session.send_session_command("POST", "window/rect", body)
    390        except error.UnknownErrorException:
    391            # silently ignore this error as the command is not implemented
    392            # for Android. Revert this once it is implemented.
    393            pass
    394 
    395    def maximize(self):
    396        return self.session.send_session_command("POST", "window/maximize")
    397 
    398    def minimize(self):
    399        return self.session.send_session_command("POST", "window/minimize")
    400 
    401    def fullscreen(self):
    402        return self.session.send_session_command("POST", "window/fullscreen")
    403 
    404 
    405 class Find:
    406    def __init__(self, session):
    407        self.session = session
    408 
    409    def css(self, element_selector, all=True):
    410        elements = self._find_element("css selector", element_selector, all)
    411        return elements
    412 
    413    def _find_element(self, strategy, selector, all):
    414        route = "elements" if all else "element"
    415        body = {"using": strategy, "value": selector}
    416        return self.session.send_session_command("POST", route, body)
    417 
    418 
    419 class UserPrompt:
    420    def __init__(self, session):
    421        self.session = session
    422 
    423    def dismiss(self):
    424        self.session.send_session_command("POST", "alert/dismiss")
    425 
    426    def accept(self):
    427        self.session.send_session_command("POST", "alert/accept")
    428 
    429    @property
    430    def text(self):
    431        return self.session.send_session_command("GET", "alert/text")
    432 
    433    @text.setter
    434    def text(self, value):
    435        body = {"text": value}
    436        self.session.send_session_command("POST", "alert/text", body=body)
    437 
    438 
    439 class Session:
    440    def __init__(
    441        self,
    442        host,
    443        port,
    444        url_prefix="/",
    445        enable_bidi=False,
    446        capabilities=None,
    447        extension=None,
    448    ):
    449 
    450        if enable_bidi:
    451            if capabilities is not None:
    452                capabilities.setdefault("alwaysMatch", {}).update(
    453                    {"webSocketUrl": True}
    454                )
    455            else:
    456                capabilities = {"alwaysMatch": {"webSocketUrl": True}}
    457 
    458        self.transport = transport.HTTPWireProtocol(host, port, url_prefix)
    459        self.requested_capabilities = capabilities
    460        self.capabilities = None
    461        self.session_id = None
    462        self.timeouts = None
    463        self.window = None
    464        self.find = None
    465        self.enable_bidi = enable_bidi
    466        self.bidi_session = None
    467        self.extension = None
    468        self.extension_cls = extension
    469 
    470        self.timeouts = Timeouts(self)
    471        self.window = BrowserWindow(self)
    472        self.find = Find(self)
    473        self.alert = UserPrompt(self)
    474        self.actions = Actions(self)
    475        self.web_extensions = WebExtensions(self)
    476 
    477    def __repr__(self):
    478        return "<%s %s>" % (
    479            self.__class__.__name__,
    480            self.session_id or "(disconnected)",
    481        )
    482 
    483    def __eq__(self, other):
    484        return (
    485            self.session_id is not None
    486            and isinstance(other, Session)
    487            and self.session_id == other.session_id
    488        )
    489 
    490    def __enter__(self):
    491        self.start()
    492        return self
    493 
    494    def __exit__(self, *args, **kwargs):
    495        self.end()
    496 
    497    def __del__(self):
    498        self.end()
    499 
    500    def match(self, capabilities):
    501        return self.requested_capabilities == capabilities
    502 
    503    def start(self):
    504        """Start a new WebDriver session.
    505 
    506        :return: Dictionary with `capabilities` and `sessionId`.
    507 
    508        :raises error.WebDriverException: If the remote end returns
    509            an error.
    510        """
    511        if self.session_id is not None:
    512            return
    513 
    514        self.transport.close()
    515 
    516        body = {"capabilities": {}}
    517 
    518        if self.requested_capabilities is not None:
    519            body["capabilities"] = self.requested_capabilities
    520 
    521        try:
    522            value = self.send_command("POST", "session", body=body)
    523            assert isinstance(value["sessionId"], str)
    524            assert isinstance(value["capabilities"], Dict)
    525 
    526            self.session_id = value["sessionId"]
    527            self.capabilities = value["capabilities"]
    528 
    529            if "webSocketUrl" in self.capabilities:
    530                self.bidi_session = BidiSession.from_http(
    531                    self.session_id, self.capabilities
    532                )
    533            elif self.enable_bidi:
    534                self.end()
    535                raise error.SessionNotCreatedException(
    536                    "Requested bidi session, but webSocketUrl capability not found"
    537                )
    538 
    539            if self.extension_cls:
    540                self.extension = self.extension_cls(self)
    541 
    542            return value
    543 
    544        except Exception:
    545            # Make sure we end up back in a consistent state.
    546            self.end()
    547            raise
    548 
    549    def end(self):
    550        """Try to close the active session."""
    551        try:
    552            if self.session_id is not None:
    553                self.send_command("DELETE", "session/%s" % self.session_id)
    554        except (OSError, error.InvalidSessionIdException):
    555            pass
    556        finally:
    557            self.session_id = None
    558            self.capabilities = None
    559            self.bidi_session = None
    560            self.extension = None
    561            self.transport.close()
    562 
    563    def send_command(self, method, url, body=None, timeout=None):
    564        """
    565        Send a command to the remote end and validate its success.
    566 
    567        :param method: HTTP method to use in request.
    568        :param uri: "Command part" of the HTTP request URL,
    569            e.g. `window/rect`.
    570        :param body: Optional body of the HTTP request.
    571 
    572        :return: `None` if the HTTP response body was empty, otherwise
    573            the `value` field returned after parsing the response
    574            body as JSON.
    575 
    576        :raises error.WebDriverException: If the remote end returns
    577            an error.
    578        :raises ValueError: If the response body does not contain a
    579            `value` key.
    580        """
    581 
    582        response = self.transport.send(
    583            method,
    584            url,
    585            body,
    586            encoder=protocol.Encoder,
    587            decoder=protocol.Decoder,
    588            session=self,
    589            timeout=timeout,
    590        )
    591 
    592        if response.status != 200:
    593            err = error.from_response(response)
    594 
    595            if isinstance(err, error.InvalidSessionIdException):
    596                # The driver could have already been deleted the session.
    597                self.session_id = None
    598 
    599            raise err
    600 
    601        if "value" in response.body:
    602            value = response.body["value"]
    603            """
    604            Edge does not yet return the w3c session ID.
    605            We want the tests to run in Edge anyway to help with REC.
    606            In order to run the tests in Edge, we need to hack around
    607            bug:
    608            https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14641972
    609            """
    610            if (
    611                url == "session"
    612                and method == "POST"
    613                and "sessionId" in response.body
    614                and "sessionId" not in value
    615            ):
    616                value["sessionId"] = response.body["sessionId"]
    617        else:
    618            raise ValueError("Expected 'value' key in response body:\n" "%s" % response)
    619 
    620        return value
    621 
    622    def send_session_command(self, method, uri, body=None, timeout=None):
    623        """
    624        Send a command to an established session and validate its success.
    625 
    626        :param method: HTTP method to use in request.
    627        :param url: "Command part" of the HTTP request URL,
    628            e.g. `window/rect`.
    629        :param body: Optional body of the HTTP request.  Must be JSON
    630            serialisable.
    631 
    632        :return: `None` if the HTTP response body was empty, otherwise
    633            the result of parsing the body as JSON.
    634 
    635        :raises error.WebDriverException: If the remote end returns
    636            an error.
    637        """
    638        if not isinstance(self.session_id, str):
    639            raise TypeError(
    640                "Session.session_id must be a str to send a session command"
    641            )
    642 
    643        url = urlparse.urljoin("session/%s/" % self.session_id, uri)
    644        return self.send_command(method, url, body, timeout)
    645 
    646    @property
    647    def url(self):
    648        return self.send_session_command("GET", "url")
    649 
    650    @url.setter
    651    def url(self, url):
    652        if urlparse.urlsplit(url).netloc is None:
    653            return self.url(url)
    654        body = {"url": url}
    655        return self.send_session_command("POST", "url", body)
    656 
    657    def back(self):
    658        return self.send_session_command("POST", "back")
    659 
    660    def forward(self):
    661        return self.send_session_command("POST", "forward")
    662 
    663    def refresh(self):
    664        return self.send_session_command("POST", "refresh")
    665 
    666    @property
    667    def title(self):
    668        return self.send_session_command("GET", "title")
    669 
    670    @property
    671    def source(self):
    672        return self.send_session_command("GET", "source")
    673 
    674    def new_window(self, type_hint="tab"):
    675        body = {"type": type_hint}
    676        value = self.send_session_command("POST", "window/new", body)
    677 
    678        return value["handle"]
    679 
    680    @property
    681    def window_handle(self):
    682        return self.send_session_command("GET", "window")
    683 
    684    @window_handle.setter
    685    def window_handle(self, handle):
    686        body = {"handle": handle}
    687        return self.send_session_command("POST", "window", body=body)
    688 
    689    def switch_to_frame(self, frame):
    690        body = {"id": frame}
    691        return self.send_session_command("POST", "frame", body=body)
    692 
    693    def switch_to_parent_frame(self):
    694        return self.send_session_command("POST", "frame/parent")
    695 
    696    @property
    697    def handles(self):
    698        return self.send_session_command("GET", "window/handles")
    699 
    700    @property
    701    def active_element(self):
    702        return self.send_session_command("GET", "element/active")
    703 
    704    def cookies(self, name=None):
    705        if name is None:
    706            url = "cookie"
    707        elif isinstance(name, str):
    708            url = "cookie/%s" % name
    709        else:
    710            raise TypeError("cookie name must be a str or None")
    711        return self.send_session_command("GET", url, {})
    712 
    713    def set_cookie(
    714        self,
    715        name,
    716        value,
    717        path=None,
    718        domain=None,
    719        secure=None,
    720        expiry=None,
    721        http_only=None,
    722    ):
    723        body = {
    724            "name": name,
    725            "value": value,
    726        }
    727 
    728        if domain is not None:
    729            body["domain"] = domain
    730        if expiry is not None:
    731            body["expiry"] = expiry
    732        if http_only is not None:
    733            body["httpOnly"] = http_only
    734        if path is not None:
    735            body["path"] = path
    736        if secure is not None:
    737            body["secure"] = secure
    738        self.send_session_command("POST", "cookie", {"cookie": body})
    739 
    740    def delete_cookie(self, name=None):
    741        if name is None:
    742            url = "cookie"
    743        elif isinstance(name, str):
    744            url = "cookie/%s" % name
    745        else:
    746            raise TypeError("cookie name must be a str or None")
    747        self.send_session_command("DELETE", url, {})
    748 
    749    # [...]
    750 
    751    def set_global_privacy_control(self, gpc):
    752        body = {
    753            "gpc": gpc,
    754        }
    755        return self.send_session_command("POST", "privacy", body)
    756 
    757    def get_global_privacy_control(self):
    758        return self.send_session_command("GET", "privacy")
    759 
    760    # [...]
    761 
    762    def execute_script(self, script, args=None):
    763        if args is None:
    764            args = []
    765 
    766        body = {"script": script, "args": args}
    767        return self.send_session_command("POST", "execute/sync", body)
    768 
    769    def execute_async_script(self, script, args=None):
    770        if args is None:
    771            args = []
    772 
    773        body = {"script": script, "args": args}
    774        return self.send_session_command("POST", "execute/async", body)
    775 
    776    # [...]
    777 
    778    def screenshot(self):
    779        return self.send_session_command("GET", "screenshot")
    780 
    781    def print(
    782        self,
    783        background=None,
    784        margin=None,
    785        orientation=None,
    786        page=None,
    787        page_ranges=None,
    788        scale=None,
    789        shrink_to_fit=None,
    790    ):
    791        body = {}
    792        for prop, value in {
    793            "background": background,
    794            "margin": margin,
    795            "orientation": orientation,
    796            "page": page,
    797            "pageRanges": page_ranges,
    798            "scale": scale,
    799            "shrinkToFit": shrink_to_fit,
    800        }.items():
    801            if value is not None:
    802                body[prop] = value
    803        return self.send_session_command("POST", "print", body)
    804 
    805 
    806 class ShadowRoot:
    807    identifier = "shadow-6066-11e4-a52e-4f735466cecf"
    808 
    809    def __init__(self, session, id):
    810        """
    811        Construct a new shadow root representation.
    812 
    813        :param id: Shadow root UUID which must be unique across
    814            all browsing contexts.
    815        :param session: Current ``webdriver.Session``.
    816        """
    817        self.id = id
    818        self.session = session
    819 
    820    @classmethod
    821    def from_json(cls, json, session):
    822        uuid = json[ShadowRoot.identifier]
    823        assert isinstance(uuid, str)
    824 
    825        return cls(session, uuid)
    826 
    827    def send_shadow_command(self, method, uri, body=None):
    828        if not isinstance(self.id, str):
    829            raise TypeError("self.id must be a str")
    830 
    831        if not isinstance(uri, str):
    832            raise TypeError("uri must be a str")
    833 
    834        url = f"shadow/{self.id}/{uri}"
    835        return self.session.send_session_command(method, url, body)
    836 
    837    def find_element(self, strategy, selector):
    838        body = {"using": strategy, "value": selector}
    839        return self.send_shadow_command("POST", "element", body)
    840 
    841    def find_elements(self, strategy, selector):
    842        body = {"using": strategy, "value": selector}
    843        return self.send_shadow_command("POST", "elements", body)
    844 
    845 
    846 class WebElement:
    847    """
    848    Representation of a web element.
    849 
    850    A web element is an abstraction used to identify an element when
    851    it is transported via the protocol, between remote- and local ends.
    852    """
    853 
    854    identifier = "element-6066-11e4-a52e-4f735466cecf"
    855 
    856    def __init__(self, session, id):
    857        """
    858        Construct a new web element representation.
    859 
    860        :param session: Current ``webdriver.Session``.
    861        :param id: Web element UUID which must be unique across all browsing contexts.
    862        """
    863        self.id = id
    864        self.session = session
    865 
    866    def __repr__(self):
    867        return "<%s %s>" % (self.__class__.__name__, self.id)
    868 
    869    def __eq__(self, other):
    870        return (
    871            isinstance(other, WebElement)
    872            and self.id == other.id
    873            and self.session == other.session
    874        )
    875 
    876    @classmethod
    877    def from_json(cls, json, session):
    878        uuid = json[WebElement.identifier]
    879        assert isinstance(uuid, str)
    880 
    881        return cls(session, uuid)
    882 
    883    def send_element_command(self, method, uri, body=None):
    884        if not isinstance(self.id, str):
    885            raise TypeError("WebElement.id must be a str")
    886 
    887        if not isinstance(uri, str):
    888            raise TypeError("uri must be a str")
    889 
    890        url = "element/%s/%s" % (self.id, uri)
    891        return self.session.send_session_command(method, url, body)
    892 
    893    def find_element(self, strategy, selector):
    894        body = {"using": strategy, "value": selector}
    895        return self.send_element_command("POST", "element", body)
    896 
    897    def click(self):
    898        self.send_element_command("POST", "click", {})
    899 
    900    def tap(self):
    901        self.send_element_command("POST", "tap", {})
    902 
    903    def clear(self):
    904        self.send_element_command("POST", "clear", {})
    905 
    906    def send_keys(self, text):
    907        return self.send_element_command("POST", "value", {"text": text})
    908 
    909    @property
    910    def text(self):
    911        return self.send_element_command("GET", "text")
    912 
    913    @property
    914    def name(self):
    915        return self.send_element_command("GET", "name")
    916 
    917    def style(self, property_name):
    918        if not isinstance(property_name, str):
    919            raise TypeError("property_name must be a str")
    920 
    921        return self.send_element_command("GET", "css/%s" % property_name)
    922 
    923    @property
    924    def rect(self):
    925        return self.send_element_command("GET", "rect")
    926 
    927    @property
    928    def selected(self):
    929        return self.send_element_command("GET", "selected")
    930 
    931    def screenshot(self):
    932        return self.send_element_command("GET", "screenshot")
    933 
    934    @property
    935    def shadow_root(self):
    936        return self.send_element_command("GET", "shadow")
    937 
    938    def attribute(self, name):
    939        if not isinstance(name, str):
    940            raise TypeError("name must be a str")
    941 
    942        return self.send_element_command("GET", "attribute/%s" % name)
    943 
    944    def get_computed_label(self):
    945        return self.send_element_command("GET", "computedlabel")
    946 
    947    def get_computed_role(self):
    948        return self.send_element_command("GET", "computedrole")
    949 
    950    # This MUST come last because otherwise @property decorators above
    951    # will be overridden by this.
    952    def property(self, name):
    953        if not isinstance(name, str):
    954            raise TypeError("name must be a str")
    955 
    956        return self.send_element_command("GET", "property/%s" % name)
    957 
    958 
    959 class WebExtensions:
    960    def __init__(self, session):
    961        self.session = session
    962 
    963    def install(self, type, path=None, value=None):
    964        body = {"type": type}
    965        if path is not None:
    966            body["path"] = path
    967        elif value is not None:
    968            body["value"] = value
    969        return self.session.send_session_command("POST", "webextension", body)
    970 
    971    def uninstall(self, extension_id):
    972        return self.session.send_session_command(
    973            "DELETE", "webextension/%s" % extension_id
    974        )
    975 
    976 
    977 class WebFrame:
    978    identifier = "frame-075b-4da1-b6ba-e579c2d3230a"
    979 
    980    def __init__(self, session, id):
    981        self.id = id
    982        self.session = session
    983 
    984    def __repr__(self):
    985        return "<%s %s>" % (self.__class__.__name__, self.id)
    986 
    987    def __eq__(self, other):
    988        return (
    989            isinstance(other, WebFrame)
    990            and self.id == other.id
    991            and self.session == other.session
    992        )
    993 
    994    @classmethod
    995    def from_json(cls, json, session):
    996        uuid = json[WebFrame.identifier]
    997        assert isinstance(uuid, str)
    998 
    999        return cls(session, uuid)
   1000 
   1001 
   1002 class WebWindow:
   1003    identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f"
   1004 
   1005    def __init__(self, session, id):
   1006        self.id = id
   1007        self.session = session
   1008 
   1009    def __repr__(self):
   1010        return "<%s %s>" % (self.__class__.__name__, self.id)
   1011 
   1012    def __eq__(self, other):
   1013        return (
   1014            isinstance(other, WebWindow)
   1015            and self.id == other.id
   1016            and self.session == other.session
   1017        )
   1018 
   1019    @classmethod
   1020    def from_json(cls, json, session):
   1021        uuid = json[WebWindow.identifier]
   1022        assert isinstance(uuid, str)
   1023 
   1024        return cls(session, uuid)