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

add a simple integration test using Selenium and a headless Chrome #28

Merged
merged 2 commits into from
Sep 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
on: [push, pull_request]
name: Interop

jobs:
interop:
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- uses: nanasess/setup-chromedriver@v1
with:
# Optional: do not specify to match Chrome's version
chromedriver-version: '105.0.5195.52'
- uses: actions/setup-go@v3
with:
go-version: "1.19.x"
- name: Build interop server
run: go build -o interopserver interop/main.go
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Python dependencies
run: pip install -r interop/requirements.txt
- name: Run interop tests
run: |
./interopserver &
timeout 120 python interop/interop.py
63 changes: 63 additions & 0 deletions interop/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<html>
<body>
<script>
function testSucceeded() {
let elemDiv = document.createElement('div');
elemDiv.id = "done";
document.body.appendChild(elemDiv);
}

async function establishSession(url) {
const transport = new WebTransport(url, {
"serverCertificateHashes": [{
"algorithm": "sha-256",
"value": new Uint8Array(%%CERTHASH%%)
}]
});

// Optionally, set up functions to respond to
// the connection closing:
transport.closed.then(() => {
console.log(`The HTTP/3 connection to ${url} closed gracefully.`);
}).catch((error) => {
console.error(`The HTTP/3 connection to ${url} closed due to ${error}.`);
});

// Once .ready fulfills, the connection can be used.
await transport.ready;
return transport;
}

// In this test, we open 5 unidirectional streams, and send the data back to the server.
async function runUnidirectionalTest() {
const transport = await establishSession('https://127.0.0.1:12345/unidirectional');
const data = new Uint8Array(%%DATA%%);

let failed = false
for(let i = 0; i < 5; i++) {
const stream = await transport.createUnidirectionalStream();
console.log(`Opened stream ${i}.`)
const writer = stream.getWriter();
writer.write(data);
try {
await writer.close();
console.log(`All data has been sent on stream ${i}.`);
} catch (error) {
console.error(`An error occurred: ${error}`);
failed = true
}
}
if(!failed) { testSucceeded() }
transport.close()
}

(async function() {
switch("%%TEST%%") {
case "unidirectional":
await runUnidirectionalTest()
break
}
})()
</script>
</body>
</html>
56 changes: 56 additions & 0 deletions interop/interop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env python3

import sys

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException

chrome_loc = "/usr/bin/google-chrome"
if sys.platform == "darwin":
chrome_loc = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"

options = webdriver.ChromeOptions()
options.gpu = False
options.headless = True
options.binary_location = chrome_loc
options.add_argument("--no-sandbox")
options.add_argument("--enable-quic")
options.add_argument("--origin-to-force-quic-on=localhost:12345")
options.add_argument("--host-resolver-rules='MAP localhost:12345 127.0.0.1:12345'")

dc = DesiredCapabilities.CHROME
dc["goog:loggingPrefs"] = {"browser": "ALL"}

driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=options,
desired_capabilities=dc,
)
driver.get("http://localhost:8080/webtransport")

delay = 5
failed = False
try:
# when the test finishes successfully, it adds a div#done to the body
myElem = WebDriverWait(driver, delay).until(
expected_conditions.presence_of_element_located((By.ID, "done"))
)
print("Test succeeded!")
except TimeoutException:
failed = True
print("Test timed out.")

# for debugging, print all the console messages
for entry in driver.get_log("browser"):
print(entry)

driver.quit()

if failed:
sys.exit(1)
160 changes: 160 additions & 0 deletions interop/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package main

import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
_ "embed"
"encoding/binary"
"fmt"
"io"
"log"
"math/big"
"net/http"
"strings"
"time"

"github.com/lucas-clemente/quic-go/http3"
"github.com/marten-seemann/webtransport-go"
)

//go:embed index.html
var indexHTML string

var data []byte

func init() {
data = make([]byte, 1<<20)
rand.Read(data)
}

func main() {
tlsConf, err := getTLSConf(time.Now(), time.Now().Add(10*24*time.Hour))
if err != nil {
log.Fatal(err)
}
hash := sha256.Sum256(tlsConf.Certificates[0].Leaf.Raw)

go runHTTPServer(hash)

wmux := http.NewServeMux()
s := webtransport.Server{
H3: http3.Server{
TLSConfig: tlsConf,
Addr: "localhost:12345",
Handler: wmux,
},
CheckOrigin: func(r *http.Request) bool { return true },
}
defer s.Close()

wmux.HandleFunc("/unidirectional", func(w http.ResponseWriter, r *http.Request) {
conn, err := s.Upgrade(w, r)
if err != nil {
log.Printf("upgrading failed: %s", err)
w.WriteHeader(500)
return
}
runUnidirectionalTest(conn)
})
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

func runHTTPServer(certHash [32]byte) {
mux := http.NewServeMux()
mux.HandleFunc("/webtransport", func(w http.ResponseWriter, _ *http.Request) {
fmt.Println("handler hit")
content := strings.ReplaceAll(indexHTML, "%%CERTHASH%%", formatByteSlice(certHash[:]))
content = strings.ReplaceAll(content, "%%DATA%%", formatByteSlice(data))
content = strings.ReplaceAll(content, "%%TEST%%", "unidirectional")
w.Write([]byte(content))
})
http.ListenAndServe("localhost:8080", mux)
}

func runUnidirectionalTest(sess *webtransport.Session) {
for i := 0; i < 5; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

str, err := sess.AcceptUniStream(ctx)
if err != nil {
log.Fatalf("failed to accept unidirectional stream: %v", err)
}
rvcd, err := io.ReadAll(str)
if err != nil {
log.Fatalf("failed to read all data: %v", err)
}
if !bytes.Equal(rvcd, data) {
log.Fatal("data doesn't match")
}
}
select {
case <-sess.Context().Done():
fmt.Println("done")
case <-time.After(5 * time.Second):
log.Fatal("timed out waiting for the session to be closed")
}
}

func getTLSConf(start, end time.Time) (*tls.Config, error) {
cert, priv, err := generateCert(start, end)
if err != nil {
return nil, err
}
return &tls.Config{
Certificates: []tls.Certificate{{
Certificate: [][]byte{cert.Raw},
PrivateKey: priv,
Leaf: cert,
}},
}, nil
}

func generateCert(start, end time.Time) (*x509.Certificate, *ecdsa.PrivateKey, error) {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
return nil, nil, err
}
serial := int64(binary.BigEndian.Uint64(b))
if serial < 0 {
serial = -serial
}
certTempl := &x509.Certificate{
SerialNumber: big.NewInt(serial),
Subject: pkix.Name{},
NotBefore: start,
NotAfter: end,
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &caPrivateKey.PublicKey, caPrivateKey)
if err != nil {
return nil, nil, err
}
ca, err := x509.ParseCertificate(caBytes)
if err != nil {
return nil, nil, err
}
return ca, caPrivateKey, nil
}

func formatByteSlice(b []byte) string {
s := strings.ReplaceAll(fmt.Sprintf("%#v", b[:]), "[]byte{", "[")
s = strings.ReplaceAll(s, "}", "]")
return s
}
2 changes: 2 additions & 0 deletions interop/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
selenium
webdriver_manager