Skip to content

Commit

Permalink
Fix websockets with Firefox.
Browse files Browse the repository at this point in the history
Add webdriver tests.
  • Loading branch information
floitsch committed Aug 22, 2024
1 parent bf293aa commit e294b3b
Show file tree
Hide file tree
Showing 15 changed files with 871 additions and 9 deletions.
9 changes: 8 additions & 1 deletion src/web-socket.toit
Original file line number Diff line number Diff line change
Expand Up @@ -302,14 +302,21 @@ class WebSocket:
*/
static check-server-upgrade-request_ request/RequestIncoming response-writer/ResponseWriter -> string?:
connection-header := request.headers.single "Connection"
// A connection header may have multiple tokens. Firefox, for example, sends
// "keep-alive, Upgrade".
connection-tokens := connection-header
? (connection-header.split ",").map: it.trim
: []
has-upgrade-connection-token := connection-tokens.any: | token/string |
token.to-ascii-lower == "upgrade"
upgrade-header := request.headers.single "Upgrade"
version-header := request.headers.single "Sec-WebSocket-Version"
nonce := request.headers.single "Sec-WebSocket-Key"
message := null
if nonce == null: message = "No nonce"
else if nonce.size != 24: message = "Bad nonce size"
else if not connection-header or not upgrade-header: message = "No upgrade headers"
else if (Headers.ascii-normalize_ connection-header) != "Upgrade": message = "No Connection: Upgrade"
else if not has-upgrade-connection-token: message = "No Connection: Upgrade"
else if (Headers.ascii-normalize_ upgrade-header) != "Websocket": message = "No Upgrade: websocket"
else if version-header != "13": message = "Unrecognized Websocket version"
else:
Expand Down
34 changes: 29 additions & 5 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ file(GLOB HTTPBIN_TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*-test-httpbin.toi

set(TOIT_EXEC "toit.run${CMAKE_EXECUTABLE_SUFFIX}" CACHE FILEPATH "The executable used to run the tests")
set(TPKG_EXEC "toit.pkg${CMAKE_EXECUTABLE_SUFFIX}" CACHE FILEPATH "The executable used to install the packages")
set(TEST_TIMEOUT 40 CACHE STRING "The maximal amount of time each test is allowed to run")
set(TEST_TIMEOUT 120 CACHE STRING "The maximal amount of time each test is allowed to run")
set(ENABLE_HTTPBIN_TESTS ON CACHE BOOL "Whether to run tests that depend on httpbin.org docker")
set(USE_HTTPBIN_DOCKER OFF CACHE BOOL "Whether to use the httpbin.org docker container")

Expand Down Expand Up @@ -41,16 +41,40 @@ include(fail.cmake OPTIONAL)
message("Failing tests: ${FAILING_TESTS}")
message("Skipped tests: ${SKIP_TESTS}")

set(BROWSERS "chrome")
if (APPLE)
# The macOS GitHub runner doesn't come with geckodriver anymore:
# https://github.com/actions/runner-images/issues/9974
# Since that browser is already tested on the other platforms, we skip it here.
list(APPEND BROWSERS "safari")
elseif (WIN32)
list(APPEND BROWSERS "edge" "firefox")
else()
list(APPEND BROWSERS "firefox")
endif()

foreach(file ${TESTS})
set(test_name "/tests/${file}")
if("${test_name}" IN_LIST SKIP_TESTS)
continue()
endif()
add_test(
NAME "${test_name}"
COMMAND "${TOIT_EXEC}" "-Xenable-asserts" "${file}"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
if ("${file}" MATCHES "webdriver-")
foreach (browser ${BROWSERS})
set(test_name "/tests/${file}-${browser}")
add_test(
NAME "${test_name}"
COMMAND "${TOIT_EXEC}" "-Xenable-asserts" "${file}" "${browser}"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
set_tests_properties("${test_name}" PROPERTIES TIMEOUT ${TEST_TIMEOUT})
endforeach()
else()
add_test(
NAME "${test_name}"
COMMAND "${TOIT_EXEC}" "-Xenable-asserts" "${file}"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
endif()
set_tests_properties(${test_name} PROPERTIES TIMEOUT ${TEST_TIMEOUT})
endforeach()

Expand Down
4 changes: 2 additions & 2 deletions tests/http-standalone-test.toit
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,11 @@ listen server server-socket my-port other-port:
response-writer.headers.set "Content-Type" "text/plain"
while data := request.body.read:
writer.write data
else if request.query.resource == "/get_with_parameters":
else if resource == "/get_with_parameters":
response-writer.headers.set "Content-Type" "text/plain"
writer.write "Response with parameters"
POST-DATA.do: | key/string value/string |
expect-equals value request.query.parameters[key]
else:
print "request.query.resource = '$request.query.resource'"
print "request.query.resource = '$resource'"
response-writer.write-headers http.STATUS-NOT-FOUND --message="Not Found"
16 changes: 15 additions & 1 deletion tests/package.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
sdk: ^2.0.0-alpha.91
sdk: ^2.0.0-alpha.144
prefixes:
certificate_roots: toit-cert-roots
fs: pkg-fs
host: pkg-host
http: ..
packages:
..:
path: ..
pkg-fs:
url: github.com/toitlang/pkg-fs
name: fs
version: 2.3.1
hash: 60836d4500317af2093d59d50c117d612c33f1fa
prefixes:
host: pkg-host
pkg-host:
url: github.com/toitlang/pkg-host
name: host
version: 1.15.1
hash: ff187c2c19d695e66c3dc1d9c09b4dc6bec09088
toit-cert-roots:
url: github.com/toitware/toit-cert-roots
name: certificate_roots
Expand Down
6 changes: 6 additions & 0 deletions tests/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@ dependencies:
certificate_roots:
url: github.com/toitware/toit-cert-roots
version: ^1.6.1
fs:
url: github.com/toitlang/pkg-fs
version: ^2.3.1
host:
url: github.com/toitlang/pkg-host
version: ^1.15.1
http:
path: ..
33 changes: 33 additions & 0 deletions tests/third-party/ephemeral-port-reserve/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[run]
branch = True
source =
.
omit =
.tox/*
/usr/*
*/tmp*
setup.py
# Don't complain if non-runnable code isn't run
*/__main__.py
yelp_styleguide/templates/*.py
# Coverage is incorrectly reported for distutils/__init__.py only when in venv, causing < 100%
*/distutils/__init__.py

[report]
exclude_lines =
# Have to re-enable the standard pragma
\#\s*pragma: no cover

# Don't complain if tests don't hit defensive assertion code:
^\s*raise AssertionError\b
^\s*raise NotImplementedError\b
^\s*return NotImplemented\b
^\s*raise$

# Don't complain if non-runnable code isn't run:
^if __name__ == ['"]__main__['"]:$
[html]
directory = coverage-html
# vim:ft=dosini
17 changes: 17 additions & 0 deletions tests/third-party/ephemeral-port-reserve/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.cache
._*
*.egg
*.eggs
*.egg-info
*.iml
*.py[co]
.*.sw[a-z]
.coverage
.idea
.project
.pydevproject
.tox
.venv.touch
/venv*
coverage-html
dist
25 changes: 25 additions & 0 deletions tests/third-party/ephemeral-port-reserve/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: debug-statements
- id: name-tests-test
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.4.4
hooks:
- id: autopep8
- repo: https://github.com/pycqa/flake8
rev: 3.7.9
hooks:
- id: flake8
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.8.0
hooks:
- id: reorder-python-imports
args: [
'--add-import', 'from __future__ import absolute_import',
'--add-import', 'from __future__ import unicode_literals',
]
13 changes: 13 additions & 0 deletions tests/third-party/ephemeral-port-reserve/.travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
language: python
matrix:
include:
- env: TOXENV=py27
python: 2.7
- env: TOXENV=py35
python: 3.5
- env: TOXENV=py36
python: 3.6
- env: TOXENV=pypy
python: pypy
install: pip install coveralls tox
script: tox
21 changes: 21 additions & 0 deletions tests/third-party/ephemeral-port-reserve/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2016 Yelp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
38 changes: 38 additions & 0 deletions tests/third-party/ephemeral-port-reserve/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `ephemeral-port-reserve`
Sometimes you need a networked program to bind to a port that can't be hard-coded.
Generally this is when you want to run several of them in parallel; if they all
bind to port 8080, only one of them can succeed.

The usual solution is the "port 0 trick". If you bind to port 0, your kernel will
find some arbitrary high-numbered port that's unused and bind to that. Afterward
you can query the actual port that was bound to if you need to use the port number
elsewhere. However, there are cases where the port 0 trick won't work. For example,
mysqld takes port 0 to mean "the port configured in my.cnf". Docker can bind your
containers to port 0, but uses its own implementation to find a free port which
races and fails in the face of parallelism.

`ephemeral-port-reserve` provides an implementation of the port 0 trick which
is reliable and race-free. You can use it like so:

```!bash
PORT="$(ephemeral-port-reserve)"
docker run -p 127.0.0.1:$PORT:5000 registry:2
```


`ephemeral-port-reserve` is a utility to bind to an ephemeral port, force it into
the `TIME_WAIT` state, and unbind it.

This means that further ephemeral port alloctions won't pick this "reserved" port,
but subprocesses can still bind to it explicitly, given that they use `SO_REUSEADDR`.
By default on linux you have a grace period of 60 seconds to reuse this port.
To check your own particular value:

```!bash
$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
```

**NOTE:** By default, the port returned is *specifically* for `localhost`, aka `127.0.0.1`.
If you bind instead to `0.0.0.0`, you may encounter a port conflict. If you need to
bind to a non-localhost IP, you can pass it as the first argument.
16 changes: 16 additions & 0 deletions tests/third-party/ephemeral-port-reserve/README.toit
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Name: Ephemeral Port Reserve
URL: https://github.com/Yelp/ephemeral-port-reserve
Copy: https://github.com/toitware/ephemeral-port-reserve
Revision: 6cf5addc2f1d5c0a25be4ba4af62104da1d8fc51
Date: 2024-08-22
License: MIT

Description:
A utility to bind to an ephemeral port, force it into the TIME_WAIT state, and unbind it.

This means that further ephemeral port alloctions won't pick this "reserved" port, but
subprocesses can still bind to it explicitly, given that they use SO_REUSEADDR. By
default on linux you have a grace period of 60 seconds to reuse this port.

Changes:
No changes except to remove unnecessary files.
61 changes: 61 additions & 0 deletions tests/third-party/ephemeral-port-reserve/ephemeral_port_reserve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import unicode_literals

import contextlib
import errno
from socket import error as SocketError
from socket import SO_REUSEADDR
from socket import socket
from socket import SOL_SOCKET

LOCALHOST = '127.0.0.1'


def reserve(ip=LOCALHOST, port=0):
"""Bind to an ephemeral port, force it into the TIME_WAIT state, and unbind it.
This means that further ephemeral port alloctions won't pick this "reserved" port,
but subprocesses can still bind to it explicitly, given that they use SO_REUSEADDR.
By default on linux you have a grace period of 60 seconds to reuse this port.
To check your own particular value:
$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
By default, the port will be reserved for localhost (aka 127.0.0.1).
To reserve a port for a different ip, provide the ip as the first argument.
Note that IP 0.0.0.0 is interpreted as localhost.
"""
port = int(port)
with contextlib.closing(socket()) as s:
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
try:
s.bind((ip, port))
except SocketError as e:
# socket.error: EADDRINUSE Address already in use
if e.errno == errno.EADDRINUSE and port != 0:
s.bind((ip, 0))
else:
raise

# the connect below deadlocks on kernel >= 4.4.0 unless this arg is greater than zero
s.listen(1)

sockname = s.getsockname()

# these three are necessary just to get the port into a TIME_WAIT state
with contextlib.closing(socket()) as s2:
s2.connect(sockname)
sock, _ = s.accept()
with contextlib.closing(sock):
return sockname[1]


def main(): # pragma: no cover
from sys import argv
port = reserve(*argv[1:])
print(port)


if __name__ == '__main__':
exit(main())
Loading

0 comments on commit e294b3b

Please sign in to comment.