Skip to content

Commit

Permalink
add a simple integration test using Selenium and a headless Chrome (#28)
Browse files Browse the repository at this point in the history
* add an integration test using unidirectional streams

* run interop tests on CI
  • Loading branch information
marten-seemann committed Sep 10, 2022
1 parent edc888a commit b0873bb
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 0 deletions.
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

0 comments on commit b0873bb

Please sign in to comment.