Remove the need to use Chrome Canary.
Barely works but getting there.
This commit is contained in:
parent
476f0fbce1
commit
ff73adc537
|
@ -1,2 +1,3 @@
|
||||||
*.mp4
|
*.mp4
|
||||||
logs/
|
logs/
|
||||||
|
.DS_Store
|
||||||
|
|
16
README.md
16
README.md
|
@ -50,7 +50,7 @@ wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBun
|
||||||
|
|
||||||
Use ffmpeg to create a LL-DASH playlist. This creates a segment every 2s and MP4 fragment every 10ms.
|
Use ffmpeg to create a LL-DASH playlist. This creates a segment every 2s and MP4 fragment every 10ms.
|
||||||
```
|
```
|
||||||
ffmpeg -i media/source.mp4 -f dash -use_timeline 0 -r:v 24 -g:v 48 -keyint_min:v 48 -sc_threshold:v 0 -tune zerolatency -streaming 1 -ldash 1 -seg_duration 2 -frag_duration 0.01 -frag_type duration media/playlist.mpd
|
./media/generate
|
||||||
```
|
```
|
||||||
|
|
||||||
You can increase the `frag_duration` (microseconds) to slightly reduce the file size in exchange for higher latency.
|
You can increase the `frag_duration` (microseconds) to slightly reduce the file size in exchange for higher latency.
|
||||||
|
@ -62,10 +62,10 @@ If you have a valid certificate you can use it instead of self-signing. The go b
|
||||||
|
|
||||||
Otherwise, use [mkcert](https://github.com/FiloSottile/mkcert) to install a self-signed CA:
|
Otherwise, use [mkcert](https://github.com/FiloSottile/mkcert) to install a self-signed CA:
|
||||||
```
|
```
|
||||||
mkcert -install
|
./generate/cert
|
||||||
```
|
```
|
||||||
|
|
||||||
With no arguments, the server will generate self-signed cert using this root CA.
|
With no arguments, the server will generate self-signed cert using this root CA. This certificate is only valid for *2 weeks* due to how WebTransport performs certificate fingerprinting.
|
||||||
|
|
||||||
## Server
|
## Server
|
||||||
The Warp server supports WebTransport, pushing media over streams once a connection has been established. A more refined implementation would load content based on the WebTransport URL or some other messaging scheme.
|
The Warp server supports WebTransport, pushing media over streams once a connection has been established. A more refined implementation would load content based on the WebTransport URL or some other messaging scheme.
|
||||||
|
@ -89,13 +89,3 @@ yarn serve
|
||||||
These can be accessed on `https://localhost:4444` by default.
|
These can be accessed on `https://localhost:4444` by default.
|
||||||
|
|
||||||
If you use a custom domain for the Warp server, make sure to override the server URL with the `url` query string parameter, e.g. `https://localhost:4444/?url=https://warp.demo`.
|
If you use a custom domain for the Warp server, make sure to override the server URL with the `url` query string parameter, e.g. `https://localhost:4444/?url=https://warp.demo`.
|
||||||
|
|
||||||
## Chrome
|
|
||||||
Now we need to make Chrome accept these certificates, which normally would involve trusting a root CA but this was not working with WebTransport when I last tried.
|
|
||||||
|
|
||||||
Instead, we need to run a *fresh instance* of Chrome, instructing it to allow our self-signed certificate. This command will not work if Chrome is already running, so it's easier to use Chrome Canary instead.
|
|
||||||
|
|
||||||
Launch a new instance of Chrome Canary:
|
|
||||||
```
|
|
||||||
/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --allow-insecure-localhost --origin-to-force-quic-on=localhost:4443 https://localhost:4444
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
HOST="localhost"
|
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
|
||||||
|
|
||||||
# Outputs the certificate fingerprint in the format Chrome expects
|
|
||||||
openssl x509 -pubkey -noout -in "${HOST}.crt" |
|
|
||||||
openssl rsa -pubin -outform der 2>/dev/null |
|
|
||||||
openssl dgst -sha256 -binary |
|
|
||||||
base64
|
|
|
@ -3,4 +3,20 @@ set -euxo pipefail
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
mkcert -cert-file localhost.crt -key-file localhost.key localhost 127.0.0.1 ::1
|
# Generate a new RSA key/cert for local development
|
||||||
|
HOST="localhost"
|
||||||
|
CRT="$HOST.crt"
|
||||||
|
KEY="$HOST.key"
|
||||||
|
|
||||||
|
# Install the system certificate if it's not already
|
||||||
|
mkcert -install
|
||||||
|
|
||||||
|
# Generate a new certificate for localhost
|
||||||
|
mkcert -ecdsa -cert-file "$CRT" -key-file "$KEY" localhost 127.0.0.1 ::1
|
||||||
|
|
||||||
|
# Reduce the expiration time of the certificate to 14 days; the WebTransport maximum.
|
||||||
|
# TODO https://github.com/FiloSottile/mkcert/pull/513
|
||||||
|
openssl x509 -days 14 -in "$CRT" -signkey "$KEY" -out "$CRT"
|
||||||
|
|
||||||
|
# Compute the sha256 fingerprint of the certificate for WebTransport
|
||||||
|
# openssl x509 -in "$CRT" -outform der | openssl dgst -sha256
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"source": "src/index.html",
|
"source": "src/index.html",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "parcel serve --https --host localhost --port 4444 --cert ../cert/localhost.crt --key ../cert/localhost.key",
|
"serve": "parcel serve --https --cert ../cert/localhost.crt --key ../cert/localhost.key --host localhost --port 4444 --open",
|
||||||
"build": "parcel build",
|
"build": "parcel build",
|
||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta charset = "UTF-8">
|
<meta charset = "UTF-8">
|
||||||
<title>WARP</title>
|
<title>WARP</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="player.css">
|
<link rel="stylesheet" href="index.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -29,54 +29,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script src="index.ts" type="module"></script>
|
||||||
import { Player } from "./player.ts"
|
|
||||||
|
|
||||||
// This is so ghetto but I'm too lazy to improve it right now
|
|
||||||
const vidRef = document.getElementById("vid")
|
|
||||||
const liveRef = document.getElementById("live")
|
|
||||||
const throttleRef = document.getElementById("throttle")
|
|
||||||
const statsRef = document.getElementById("stats")
|
|
||||||
const playRef = document.getElementById("play")
|
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
|
|
||||||
const player = new Player({
|
|
||||||
url: params.get("url") || "https://localhost:4443",
|
|
||||||
|
|
||||||
vid: vidRef,
|
|
||||||
stats: statsRef,
|
|
||||||
throttle: throttleRef,
|
|
||||||
})
|
|
||||||
|
|
||||||
liveRef.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
player.goLive()
|
|
||||||
})
|
|
||||||
|
|
||||||
throttleRef.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
player.throttle()
|
|
||||||
})
|
|
||||||
|
|
||||||
playRef.addEventListener('click', (e) => {
|
|
||||||
vidRef.play()
|
|
||||||
e.preventDefault()
|
|
||||||
})
|
|
||||||
|
|
||||||
function playFunc(e) {
|
|
||||||
playRef.style.display = "none"
|
|
||||||
//player.goLive()
|
|
||||||
|
|
||||||
// Only fire once to restore pause/play functionality
|
|
||||||
vidRef.removeEventListener('play', playFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
vidRef.addEventListener('play', playFunc)
|
|
||||||
vidRef.volume = 0.5
|
|
||||||
|
|
||||||
// Try to autoplay but ignore errors on mobile; they need to click
|
|
||||||
//vidRef.play().catch((e) => console.warn(e))
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Player } from "./player"
|
||||||
|
|
||||||
|
// This is so ghetto but I'm too lazy to improve it right now
|
||||||
|
const videoRef = document.querySelector<HTMLVideoElement>("video#vid")!;
|
||||||
|
const liveRef = document.querySelector<HTMLElement>("#live")!;
|
||||||
|
const throttleRef = document.querySelector<HTMLElement>("#throttle")!;
|
||||||
|
const statsRef = document.querySelector<HTMLElement>("#stats")!;
|
||||||
|
const playRef = document.querySelector<HTMLElement>("#play")!;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
|
const url = params.get("url") || "https://localhost:4443/watch"
|
||||||
|
|
||||||
|
const player = new Player({
|
||||||
|
url: url,
|
||||||
|
videoRef: videoRef,
|
||||||
|
statsRef: statsRef,
|
||||||
|
throttleRef: throttleRef,
|
||||||
|
})
|
||||||
|
|
||||||
|
liveRef.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
player.goLive()
|
||||||
|
})
|
||||||
|
|
||||||
|
throttleRef.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
player.throttle()
|
||||||
|
})
|
||||||
|
|
||||||
|
playRef.addEventListener('click', (e) => {
|
||||||
|
videoRef.play()
|
||||||
|
e.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
|
function playFunc(e: Event) {
|
||||||
|
playRef.style.display = "none"
|
||||||
|
//player.goLive()
|
||||||
|
|
||||||
|
// Only fire once to restore pause/play functionality
|
||||||
|
videoRef.removeEventListener('play', playFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
videoRef.addEventListener('play', playFunc)
|
||||||
|
videoRef.volume = 0.5
|
||||||
|
|
||||||
|
// Try to autoplay but ignore errors on mobile; they need to click
|
||||||
|
//vidRef.play().catch((e) => console.warn(e))
|
|
@ -7,6 +7,18 @@ import { Message, MessageInit, MessageSegment } from "./message"
|
||||||
|
|
||||||
///<reference path="./types/webtransport.d.ts"/>
|
///<reference path="./types/webtransport.d.ts"/>
|
||||||
|
|
||||||
|
export interface PlayerInit {
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
videoRef: HTMLVideoElement;
|
||||||
|
statsRef: HTMLElement;
|
||||||
|
throttleRef: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
mediaSource: MediaSource;
|
mediaSource: MediaSource;
|
||||||
|
|
||||||
|
@ -20,17 +32,17 @@ export class Player {
|
||||||
// References to elements in the DOM
|
// References to elements in the DOM
|
||||||
vidRef: HTMLVideoElement; // The video element itself
|
vidRef: HTMLVideoElement; // The video element itself
|
||||||
statsRef: HTMLElement; // The stats div
|
statsRef: HTMLElement; // The stats div
|
||||||
throttleRef: HTMLButtonElement; // The throttle button
|
throttleRef: HTMLElement; // The throttle button
|
||||||
throttleCount: number; // number of times we've clicked the button in a row
|
throttleCount: number; // number of times we've clicked the button in a row
|
||||||
|
|
||||||
interval: number;
|
interval: number;
|
||||||
|
|
||||||
timeRef?: DOMHighResTimeStamp;
|
timeRef?: DOMHighResTimeStamp;
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: PlayerInit) {
|
||||||
this.vidRef = props.vid
|
this.vidRef = props.videoRef
|
||||||
this.statsRef = props.stats
|
this.statsRef = props.statsRef
|
||||||
this.throttleRef = props.throttle
|
this.throttleRef = props.throttleRef
|
||||||
this.throttleCount = 0
|
this.throttleCount = 0
|
||||||
|
|
||||||
this.mediaSource = new MediaSource()
|
this.mediaSource = new MediaSource()
|
||||||
|
@ -43,8 +55,7 @@ export class Player {
|
||||||
this.interval = setInterval(this.tick.bind(this), 100)
|
this.interval = setInterval(this.tick.bind(this), 100)
|
||||||
this.vidRef.addEventListener("waiting", this.tick.bind(this))
|
this.vidRef.addEventListener("waiting", this.tick.bind(this))
|
||||||
|
|
||||||
const quic = new WebTransport(props.url)
|
this.quic = this.connect(props.url)
|
||||||
this.quic = quic.ready.then(() => { return quic });
|
|
||||||
|
|
||||||
// Create a unidirectional stream for all of our messages
|
// Create a unidirectional stream for all of our messages
|
||||||
this.api = this.quic.then((q) => {
|
this.api = this.quic.then((q) => {
|
||||||
|
@ -63,6 +74,38 @@ export class Player {
|
||||||
(await this.quic).close()
|
(await this.quic).close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connect(url: string): Promise<WebTransport> {
|
||||||
|
// TODO remove this when WebTransport supports the system CA pool
|
||||||
|
const fingerprintURL = new URL(url);
|
||||||
|
fingerprintURL.pathname = "/fingerprint"
|
||||||
|
|
||||||
|
const response = await fetch(fingerprintURL)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('failed to get server fingerprint');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hex = await response.text()
|
||||||
|
|
||||||
|
// Convert the hex to binary.
|
||||||
|
let fingerprint = [];
|
||||||
|
for (let c = 0; c < hex.length; c += 2) {
|
||||||
|
fingerprint.push(parseInt(hex.substring(c, c+2), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
//const fingerprint = Uint8Array.from(atob(hex), c => c.charCodeAt(0))
|
||||||
|
|
||||||
|
const quic = new WebTransport(url, {
|
||||||
|
"serverCertificateHashes": [{
|
||||||
|
"algorithm": "sha-256",
|
||||||
|
"value": new Uint8Array(fingerprint),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
await quic.ready
|
||||||
|
|
||||||
|
return quic
|
||||||
|
}
|
||||||
|
|
||||||
async sendMessage(msg: any) {
|
async sendMessage(msg: any) {
|
||||||
const payload = JSON.stringify(msg)
|
const payload = JSON.stringify(msg)
|
||||||
const size = payload.length + 8
|
const size = payload.length + 8
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"include": ["src/**/*"],
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2021",
|
"target": "es2021",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
@ -9,4 +11,3 @@
|
||||||
"allowJs": true
|
"allowJs": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
module github.com/kixelated/warp-demo/server
|
module github.com/kixelated/warp/server
|
||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ github.com/kisielk/errcheck v1.4.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/kixelated/invoker v1.0.0 h1:0wYlvK39yQPbkwIFy+YN41AhF89WOtGyWqV2pZB39xw=
|
github.com/kixelated/invoker v1.0.0 h1:0wYlvK39yQPbkwIFy+YN41AhF89WOtGyWqV2pZB39xw=
|
||||||
github.com/kixelated/invoker v1.0.0/go.mod h1:RjG3iqm/sKwZjOpcW4SGq+l+4DJCDR/yUtc70VjCRB8=
|
github.com/kixelated/invoker v1.0.0/go.mod h1:RjG3iqm/sKwZjOpcW4SGq+l+4DJCDR/yUtc70VjCRB8=
|
||||||
github.com/kixelated/quic-go v1.31.0 h1:O3JomeXPnLNSCNpZF415NWOyfpzbFfuvP6dlIDg8VEA=
|
github.com/kixelated/quic-go v1.31.0 h1:p2vq3Otvtmz+0EP23vjumnO/HU4Q/DFxNF6xNryVfmA=
|
||||||
github.com/kixelated/quic-go v1.31.0/go.mod h1:AO7pURnb8HXHmdalp5e09UxQfsuwseEhl0NLmwiSOFY=
|
github.com/kixelated/quic-go v1.31.0/go.mod h1:AO7pURnb8HXHmdalp5e09UxQfsuwseEhl0NLmwiSOFY=
|
||||||
github.com/kixelated/webtransport-go v1.4.1 h1:ZtY3P7hVe1wK5fAt71b+HHnNISFDcQ913v+bvaNATxA=
|
github.com/kixelated/webtransport-go v1.4.1 h1:ZtY3P7hVe1wK5fAt71b+HHnNISFDcQ913v+bvaNATxA=
|
||||||
github.com/kixelated/webtransport-go v1.4.1/go.mod h1:6RV5pTXF7oP53T83bosSDsLdSdw31j5cfpMDqsO4D5k=
|
github.com/kixelated/webtransport-go v1.4.1/go.mod h1:6RV5pTXF7oP53T83bosSDsLdSdw31j5cfpMDqsO4D5k=
|
||||||
|
|
|
@ -22,18 +22,21 @@ import (
|
||||||
type Server struct {
|
type Server struct {
|
||||||
inner *webtransport.Server
|
inner *webtransport.Server
|
||||||
media *Media
|
media *Media
|
||||||
|
|
||||||
sessions invoker.Tasks
|
sessions invoker.Tasks
|
||||||
|
cert *tls.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type Config struct {
|
||||||
Addr string
|
Addr string
|
||||||
Cert *tls.Certificate
|
Cert *tls.Certificate
|
||||||
LogDir string
|
LogDir string
|
||||||
|
Media *Media
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(config ServerConfig, media *Media) (s *Server, err error) {
|
func New(config Config) (s *Server, err error) {
|
||||||
s = new(Server)
|
s = new(Server)
|
||||||
|
s.cert = config.Cert
|
||||||
|
s.media = config.Media
|
||||||
|
|
||||||
quicConfig := &quic.Config{}
|
quicConfig := &quic.Config{}
|
||||||
|
|
||||||
|
@ -52,10 +55,12 @@ func NewServer(config ServerConfig, media *Media) (s *Server, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
Certificates: []tls.Certificate{*config.Cert},
|
Certificates: []tls.Certificate{*s.cert},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host a HTTP/3 server to serve the WebTransport endpoint
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/watch", s.handleWatch)
|
||||||
|
|
||||||
s.inner = &webtransport.Server{
|
s.inner = &webtransport.Server{
|
||||||
H3: http3.Server{
|
H3: http3.Server{
|
||||||
|
@ -67,9 +72,24 @@ func NewServer(config ServerConfig, media *Media) (s *Server, err error) {
|
||||||
CheckOrigin: func(r *http.Request) bool { return true },
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
}
|
}
|
||||||
|
|
||||||
s.media = media
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) runServe(ctx context.Context) (err error) {
|
||||||
|
return s.inner.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) runShutdown(ctx context.Context) (err error) {
|
||||||
|
<-ctx.Done()
|
||||||
|
s.inner.Close() // close on context shutdown
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Run(ctx context.Context) (err error) {
|
||||||
|
return invoker.Run(ctx, s.runServe, s.runShutdown, s.sessions.Repeat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleWatch(w http.ResponseWriter, r *http.Request) {
|
||||||
hijacker, ok := w.(http3.Hijacker)
|
hijacker, ok := w.(http3.Hijacker)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("unable to hijack connection: must use kixelated/quic-go")
|
panic("unable to hijack connection: must use kixelated/quic-go")
|
||||||
|
@ -83,30 +103,13 @@ func NewServer(config ServerConfig, media *Media) (s *Server, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.serve(r.Context(), conn, sess)
|
err = s.serveSession(r.Context(), conn, sess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) runServe(ctx context.Context) (err error) {
|
func (s *Server) serveSession(ctx context.Context, conn quic.Connection, sess *webtransport.Session) (err error) {
|
||||||
return s.inner.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) runShutdown(ctx context.Context) (err error) {
|
|
||||||
<-ctx.Done()
|
|
||||||
s.inner.Close()
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Run(ctx context.Context) (err error) {
|
|
||||||
return invoker.Run(ctx, s.runServe, s.runShutdown, s.sessions.Repeat)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) serve(ctx context.Context, conn quic.Connection, sess *webtransport.Session) (err error) {
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sess.CloseWithError(1, err.Error())
|
sess.CloseWithError(1, err.Error())
|
|
@ -0,0 +1,64 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kixelated/invoker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
inner http.Server
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Addr string
|
||||||
|
CertFile string
|
||||||
|
KeyFile string
|
||||||
|
Fingerprint string // the TLS certificate fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) (s *Server) {
|
||||||
|
s = new(Server)
|
||||||
|
s.config = config
|
||||||
|
|
||||||
|
s.inner = http.Server{
|
||||||
|
Addr: config.Addr,
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/fingerprint", s.handleFingerprint)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Run(ctx context.Context) (err error) {
|
||||||
|
return invoker.Run(ctx, s.runServe, s.runShutdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) runServe(context.Context) (err error) {
|
||||||
|
// NOTE: Doesn't support context, which is why we need runShutdown
|
||||||
|
err = s.inner.ListenAndServeTLS(s.config.CertFile, s.config.KeyFile)
|
||||||
|
log.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gracefully shut down the server when the context is cancelled
|
||||||
|
func (s *Server) runShutdown(ctx context.Context) (err error) {
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = s.inner.Shutdown(timeout)
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the sha256 of the certificate as a temporary work-around for local development.
|
||||||
|
// TODO remove this when WebTransport uses the system CA
|
||||||
|
func (s *Server) handleFingerprint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
_, _ = w.Write([]byte(s.config.Fingerprint))
|
||||||
|
}
|
|
@ -2,13 +2,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/kixelated/invoker"
|
"github.com/kixelated/invoker"
|
||||||
"github.com/kixelated/warp-demo/server/internal/warp"
|
"github.com/kixelated/warp/server/internal/warp"
|
||||||
|
"github.com/kixelated/warp/server/internal/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -38,18 +41,31 @@ func run(ctx context.Context) (err error) {
|
||||||
return fmt.Errorf("failed to load TLS certificate: %w", err)
|
return fmt.Errorf("failed to load TLS certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config := warp.ServerConfig{
|
warpConfig := warp.Config{
|
||||||
Addr: *addr,
|
Addr: *addr,
|
||||||
Cert: &tlsCert,
|
Cert: &tlsCert,
|
||||||
LogDir: *logDir,
|
LogDir: *logDir,
|
||||||
|
Media: media,
|
||||||
}
|
}
|
||||||
|
|
||||||
ws, err := warp.NewServer(config, media)
|
warpServer, err := warp.New(warpConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create warp server: %w", err)
|
return fmt.Errorf("failed to create warp server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hash := sha256.Sum256(tlsCert.Certificate[0])
|
||||||
|
fingerprint := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
|
webConfig := web.Config{
|
||||||
|
Addr: *addr,
|
||||||
|
CertFile: *cert,
|
||||||
|
KeyFile: *key,
|
||||||
|
Fingerprint: fingerprint,
|
||||||
|
}
|
||||||
|
|
||||||
|
webServer := web.New(webConfig)
|
||||||
|
|
||||||
log.Printf("listening on %s", *addr)
|
log.Printf("listening on %s", *addr)
|
||||||
|
|
||||||
return invoker.Run(ctx, invoker.Interrupt, ws.Run)
|
return invoker.Run(ctx, invoker.Interrupt, warpServer.Run, webServer.Run)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue