Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get examples/cross-browser.py to run on top of Firefox’s BiDi implementation #110

Closed
mathiasbynens opened this issue Apr 27, 2022 · 14 comments

Comments

@mathiasbynens
Copy link
Member

With Firefox Nighty:

$ ./firefox --remote-debugging-port=9222
WebDriver BiDi listening on ws://localhost:9222
DevTools listening on ws://localhost:9222/devtools/browser/6cf65b2b-54e9-444e-a36e-0c04f12b47c9
…

$ # in another terminal / tab

$ PORT=9222 python3 examples/cross-browser.py
Traceback (most recent call last):
  File "~/projects/chromium-bidi/examples/cross-browser.py", line 150, in <module>
    result = loop.run_until_complete(main())
  File "~/homebrew/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "~/projects/chromium-bidi/examples/cross-browser.py", line 53, in main
    websocket = await get_websocket()
  File "~/projects/chromium-bidi/examples/cross-browser.py", line 28, in get_websocket
    return await websockets.connect(url)
  File "~/Library/Python/3.9/lib/python/site-packages/websockets/client.py", line 542, in __await_impl__
    await protocol.handshake(
  File "~/Library/Python/3.9/lib/python/site-packages/websockets/client.py", line 296, in handshake
    raise InvalidStatusCode(status_code)
websockets.exceptions.InvalidStatusCode: server rejected WebSocket connection: HTTP 200

We should figure out what’s going on here.

@whimboo
Copy link

whimboo commented Apr 27, 2022

@mathiasbynens this might be related to the host and origin header restrictions which have been discussed on the WebDriver BiDi issue tracker. There is w3c/webdriver-bidi#155 for getting it merged.

Basically WebSocket clients should send an empty Origin header. If that's not done by the client then the connection will be denied. Maybe enable DUMPIO=1 if it's available here as well similar to Puppeteer to see the Firefox stdout logging.

In case of a non-empty Origin header there is the --remote-allow-origins argument for Firefox which can be used to add specific origins to the allow list. Maybe your client sends a null origin? In such a case make use of --remote-allow-origins=null when starting Firefox.

Please let me know if this is the issue. If not the stdout logging from Firefox will be very helpful. Maybe you could even set the remote.log.level preference to Trace when pre-creating the Firefox profile.

@mathiasbynens
Copy link
Member Author

mathiasbynens commented Apr 27, 2022

I added some logging to my local cross-browser.py copy:

import logging

logging.basicConfig(
    format="%(message)s",
    level=logging.DEBUG,
)

Successful connection (with Chromium BiDi):

$ PORT=8080 python3 examples/cross-browser.py # with Chromium-BiDi
Using selector: KqueueSelector
client - state = CONNECTING
client - event = connection_made(<_SelectorSocketTransport fd=8 read=idle write=<idle, bufsize=0>>)
client > GET / HTTP/1.1
client > Headers([('Host', 'localhost:8080'), ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Key', 'jRrnZ+8O9mGjDrIelMc6Ng=='), ('Sec-WebSocket-Version', '13'), ('Sec-WebSocket-Extensions', 'permessage-deflate; client_max_window_bits'), ('User-Agent', 'Python/3.9 websockets/8.1')])
client - event = data_received(<129 bytes>)
client < HTTP/1.1 101 Switching Protocols
client < Headers([('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Accept', '1pDVP6gqcpumF8buMDUfjACRZQY=')])
client - state = OPEN
[…]

Failed connection with Firefox:

$ PORT=9222 python3 examples/cross-browser.py # Firefox
Using selector: KqueueSelector
client - state = CONNECTING
client - event = connection_made(<_SelectorSocketTransport fd=8 read=idle write=<idle, bufsize=0>>)
client > GET / HTTP/1.1
client > Headers([('Host', 'localhost:9222'), ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Key', 'FPPJ9BiwgX6daTSuOpcviw=='), ('Sec-WebSocket-Version', '13'), ('Sec-WebSocket-Extensions', 'permessage-deflate; client_max_window_bits'), ('User-Agent', 'Python/3.9 websockets/8.1')])
client - event = data_received(<153 bytes>)
client < HTTP/1.1 200 OK
client < Headers([('content-type', 'text/html;charset=utf-8'), ('connection', 'close'), ('server', 'httpd.js'), ('date', 'Wed, 27 Apr 2022 07:16:09 GMT'), ('content-length', '361')])
client ! failing CONNECTING WebSocket connection with code 1006
client x half-closing TCP connection
client - event = data_received(<361 bytes>)
client - event = eof_received()
client - event = connection_lost(None)
client - state = CLOSED
client x code = 1006, reason = [no reason]
Traceback (most recent call last):
  File "~/projects/chromium-bidi/examples/cross-browser.py", line 157, in <module>
    result = loop.run_until_complete(main())
  File "~/homebrew/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "~/projects/chromium-bidi/examples/cross-browser.py", line 60, in main
    websocket = await get_websocket()
  File "~/projects/chromium-bidi/examples/cross-browser.py", line 35, in get_websocket
    return await websockets.connect(url)
  File "~/Library/Python/3.9/lib/python/site-packages/websockets/client.py", line 542, in __await_impl__
    await protocol.handshake(
  File "~/Library/Python/3.9/lib/python/site-packages/websockets/client.py", line 296, in handshake
    raise InvalidStatusCode(status_code)
websockets.exceptions.InvalidStatusCode: server rejected WebSocket connection: HTTP 200

The relevant difference is:

# Chromium:
client < HTTP/1.1 101 Switching Protocols
client < Headers([('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Accept', '1pDVP6gqcpumF8buMDUfjACRZQY=')])
client - state = OPEN

# Firefox:
client < HTTP/1.1 200 OK
client < Headers([('content-type', 'text/html;charset=utf-8'), ('connection', 'close'), ('server', 'httpd.js'), ('date', 'Wed, 27 Apr 2022 07:16:09 GMT'), ('content-length', '361')])
client ! failing CONNECTING WebSocket connection with code 1006
client x half-closing TCP connection
client - event = data_received(<361 bytes>)
client - event = eof_received()
client - event = connection_lost(None)
client - state = CLOSED
client x code = 1006, reason = [no reason]

@whimboo
Copy link

whimboo commented Apr 27, 2022

Interesting. We seem to return the wrong HTTP header here. But so far none of the WebSocket clients that we used actually run into this problem. I'll check that.

Also I can see that you are using version 8.1 of websockets. But the latest release is 10.3. Maybe you could try again with that one in parallel?

@whimboo
Copy link

whimboo commented Apr 27, 2022

There is https://bugzilla.mozilla.org/show_bug.cgi?id=1766581 on file to get the HTTP response status fixed.

@whimboo
Copy link

whimboo commented Apr 27, 2022

@mathiasbynens actually we do send the correct response when the connection occurs for the correct client request which has to include the /session as URL. And that seems to be the problem:

https://github.com/GoogleChromeLabs/chromium-bidi/blob/main/examples/cross-browser.py#L27

@whimboo
Copy link

whimboo commented Apr 27, 2022

You can find the details in the WebDriver spec at https://w3c.github.io/webdriver-bidi/#transport under step 2. As such it looks like that Chrome requires a fix to not accept WebSocket connections for any URL (resource name).

mathiasbynens added a commit that referenced this issue Apr 27, 2022
Per step 2 of https://w3c.github.io/webdriver-bidi/#transport, the endpoint is supposed to be `/session` and nothing else.

Issue: #110
mathiasbynens added a commit that referenced this issue Apr 27, 2022
Per step 2 of https://w3c.github.io/webdriver-bidi/#transport, the endpoint is supposed to be `/session` and nothing else.

Issue: #110
mathiasbynens added a commit that referenced this issue Apr 27, 2022
Per step 2 of https://w3c.github.io/webdriver-bidi/#transport, the endpoint is supposed to be `/session` and nothing else.

Issue: #110
mathiasbynens added a commit that referenced this issue Apr 27, 2022
Per step 2 of https://w3c.github.io/webdriver-bidi/#transport, the endpoint is supposed to be `/session` and nothing else.

Issue: #110
mathiasbynens added a commit that referenced this issue Apr 27, 2022
Per step 2 of https://w3c.github.io/webdriver-bidi/#transport, the endpoint is supposed to be `/session` and nothing else.

Issue: #110
mathiasbynens added a commit that referenced this issue Apr 27, 2022
Per step 2 of https://w3c.github.io/webdriver-bidi/#transport, the
endpoint is supposed to be `/session` (with an optional query string
attached to it).

Issue: #110
sadym-chromium pushed a commit that referenced this issue Apr 27, 2022
Per step 2 of https://w3c.github.io/webdriver-bidi/#transport, the
endpoint is supposed to be `/session` (with an optional query string
attached to it).

Issue: #110
@whimboo
Copy link

whimboo commented Apr 27, 2022

@mathiasbynens so I assume that this issue is fixed when using the right end-point? Or is something else not working yet?

@mathiasbynens
Copy link
Member Author

mathiasbynens commented Apr 28, 2022

@whimboo Not fully fixed yet, but it got us further along for sure:

$ PORT=9222 python3 examples/cross-browser.py # Firefox
Using selector: KqueueSelector
client - state = CONNECTING
client - event = connection_made(<_SelectorSocketTransport fd=8 read=idle write=<idle, bufsize=0>>)
client > GET /session HTTP/1.1
client > Headers([('Host', 'localhost:9222'), ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Key', 'Q1p4Y7uTFZRGFAStdtJNFg=='), ('Sec-WebSocket-Version', '13'), ('Sec-WebSocket-Extensions', 'permessage-deflate; client_max_window_bits'), ('User-Agent', 'Python/3.9 websockets/8.1')])
client - event = data_received(<166 bytes>)
client < HTTP/1.1 101 Switching Protocols
client < Headers([('Server', 'httpd.js'), ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Accept', 'tLxRXEQ8lRBaUuVA/exKJ2v0g10='), ('Content-Length', '0')])
client - state = OPEN
client > Frame(fin=True, opcode=1, data=b'{"id": 1000, "method": "browsingContext.create", "params": {}}', rsv1=False, rsv2=False, rsv3=False)
client - event = data_received(<653 bytes>)
client < Frame(fin=True, opcode=1, data=b'{"id":1000,"error":"invalid session id","message":"WebDriver session does not exist, or is not active","stacktrace":"WebDriverError@chrome://remote/content/shared/webdriver/Errors.jsm:183:5\\nInvalidSessionIDError@chrome://remote/content/shared/webdriver/Errors.jsm:354:5\\nassert.that/<@chrome://remote/content/shared/webdriver/Assert.jsm:445:13\\nassert.session@chrome://remote/content/shared/webdriver/Assert.jsm:43:4\\nonPacket@chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.jsm:172:16\\nonMessage@chrome://remote/content/server/WebSocketTransport.jsm:89:18\\nhandleEvent@chrome://remote/content/server/WebSocketTransport.jsm:71:14\\n"}', rsv1=False, rsv2=False, rsv3=False)
Traceback (most recent call last):
  File "~/projects/chromium-bidi/examples/cross-browser.py", line 159, in <module>
    result = loop.run_until_complete(main())
  File "~/homebrew/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "~/projects/chromium-bidi/examples/cross-browser.py", line 89, in main
    context_id = command_result['result']['context']
KeyError: 'result'

This is likely because we’re hardcoding things like command IDs in this repo, e.g.

command_result = await run_and_wait_command({
"id": 1000,
"method": "browsingContext.create",
"params": {}}, websocket)

@whimboo
Copy link

whimboo commented Apr 28, 2022

Note that browsingContext.create isn't available yet in a Nightly build of Firefox. But it will be tomorrow, so maybe try it again after downloading a build that contains the patches from bug 1759559.

Note that there is also another problem when sending commands other than session.new immediately after starting the browser. I already pushed a fix in bug 1766802 which will be in the Nightly build for tomorrow as well.

@whimboo
Copy link

whimboo commented Apr 28, 2022

Also the visible failure is invalid session id. Did the test actually send the session.new command first?

mathiasbynens added a commit that referenced this issue Apr 29, 2022
This is required per spec, and also necessary for the Firefox
implementation to accept any non-static commands.

Issue: #110, #118
@mathiasbynens
Copy link
Member Author

@whimboo Thanks. With the changes in #119, we’re making progress towards running the complete example in Firefox. (We still have to implement session.new ourselves; I’ve filed #118 for that.) The next missing piece on the Firefox implementation side is script.callFunction:

DEBUG:websockets.protocol:client > Frame(fin=True, opcode=1, data=b'{"id": 1001, "method": "browsingContext.navigate", "params": {"url": "file:///Users/mathiasb/projects/chromium-bidi/examples/app.html", "context": "bb2ee8f9-72c2-4e53-bbfb-4ada6bc9e412"}}', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - event = data_received(<114 bytes>)
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=1, data=b'{"id":1001,"result":{"navigation":null,"url":"file:///Users/mathiasb/projects/chromium-bidi/examples/app.html"}}', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=1, data=b'{"id": 1002, "method": "script.callFunction", "params": {"functionDeclaration": "(resultsSelector) => {\\n                const anchors = Array.from(document.querySelectorAll(resultsSelector));\\n                return anchors.map((anchor) => {\\n                    const title = anchor.textContent.trim();\\n                    return `${title} - ${anchor.href}`;\\n                });\\n            }", "args": [{"type": "string", "value": ".titlelink"}], "target": {"context": "bb2ee8f9-72c2-4e53-bbfb-4ada6bc9e412"}}}', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - event = data_received(<539 bytes>)
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=1, data=b'{"id":1002,"error":"unknown command","message":"script.callFunction","stacktrace":"WebDriverError@chrome://remote/content/shared/webdriver/Errors.jsm:183:5\\nUnknownCommandError@chrome://remote/content/shared/webdriver/Errors.jsm:499:5\\nexecute@chrome://remote/content/shared/webdriver/Session.jsm:234:13\\nonPacket@chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.jsm:180:37\\nonMessage@chrome://remote/content/server/WebSocketTransport.jsm:89:18\\nhandleEvent@chrome://remote/content/server/WebSocketTransport.jsm:71:14\\n"}', rsv1=False, rsv2=False, rsv3=False)

I’ll subscribe to https://bugzilla.mozilla.org/show_bug.cgi?id=1750541 for updates.

@mathiasbynens mathiasbynens self-assigned this Apr 29, 2022
@whimboo
Copy link

whimboo commented Apr 29, 2022

The next missing piece on the Firefox implementation side is script.callFunction

That is a bit more complicated. It's not a command that you will get pretty soon (see bug 1750541). It's on our list for the next milestone but will be blocked by the implementation of script.evaluate, which as well isn't done yet. Further we can only serialize and deserialize primitive objects yet.

Maybe we can get an example working that uses the logging events? Looks like the relevant PR for chromium-bidi is also close to get landed.

@sadym-chromium
Copy link
Collaborator

FWIW: I added script using script.evaluate instead of script.callFunction, and it works both in Chromium and Firefox: https://github.com/GoogleChromeLabs/chromium-bidi/blob/main/examples/cross-browser-simplified.py

Meaning the issue can be closed after script.callFunction is implemented.

@whimboo
Copy link

whimboo commented Jun 27, 2022

That sounds good! We will start soon on script.callFunction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants