Commit 36debdfd authored by David Fifield's avatar David Fifield
Browse files

Merge branch 'standalone-broker'

parents 3e3e4b8d 827972e2
This component runs on Google App Engine. It reflects domain-fronted
requests from a client to the Snowflake broker.
You need the Go App Engine SDK in order to deploy the app.
After unpacking, install the app-engine-go component:
google-cloud-sdk/bin/gcloud components install app-engine-go
To test locally, run
google-cloud-sdk/bin/ app.yaml
The app will be running at
To deploy to App Engine, first create a new project and app. You have to
think of a unique name (marked as "<appname>" in the commands). You only
have to do the "create" step once; subsequent times you can go straight
to the "deploy" step. This command will open a browser window so you can
log in to a Google account.
google-cloud-sdk/bin/gcloud projects create <appname>
google-cloud-sdk/bin/gcloud app create --project=<appname>
Then to deploy the project, run:
google-cloud-sdk/bin/gcloud app deploy --project=<appname>
To configure the Snowflake client to talk to the App Engine app, provide
"https://<appname>" as the --url option.
UseBridges 1
Bridge snowflake
ClientTransportPlugin snowflake exec ./client -url https://<appname> -front
# override this with -A $YOUR_APP_ID
application: snowflake-reg
version: 1
runtime: go
api_version: go1
// A web app for Google App Engine that proxies HTTP requests and responses to
// the Snowflake broker.
package reflect
import (
const (
forwardURL = ""
// A timeout of 0 means to use the App Engine default (5 seconds).
urlFetchTimeout = 20 * time.Second
var context appengine.Context
// Join two URL paths.
func pathJoin(a, b string) string {
if len(a) > 0 && a[len(a)-1] == '/' {
a = a[:len(a)-1]
if len(b) == 0 || b[0] != '/' {
b = "/" + b
return a + b
// We reflect only a whitelisted set of header fields. Otherwise, we may copy
// headers like Transfer-Encoding that interfere with App Engine's own
// hop-by-hop headers.
var reflectedHeaderFields = []string{
// Make a copy of r, with the URL being changed to be relative to forwardURL,
// and including only the headers in reflectedHeaderFields.
func copyRequest(r *http.Request) (*http.Request, error) {
u, err := url.Parse(forwardURL)
if err != nil {
return nil, err
// Append the requested path to the path in forwardURL, so that
// forwardURL can be something like "".
u.Path = pathJoin(u.Path, r.URL.Path)
c, err := http.NewRequest(r.Method, u.String(), r.Body)
if err != nil {
return nil, err
for _, key := range reflectedHeaderFields {
values, ok := r.Header[key]
if ok {
for _, value := range values {
c.Header.Add(key, value)
return c, nil
func handler(w http.ResponseWriter, r *http.Request) {
context = appengine.NewContext(r)
fr, err := copyRequest(r)
if err != nil {
context.Errorf("copyRequest: %s", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
// Use urlfetch.Transport directly instead of urlfetch.Client because we
// want only a single HTTP transaction, not following redirects.
transport := urlfetch.Transport{
Context: context,
// Despite the name, Transport.Deadline is really a timeout and
// not an absolute deadline as used in the net package. In
// other words it is a time.Duration, not a time.Time.
Deadline: urlFetchTimeout,
resp, err := transport.RoundTrip(fr)
if err != nil {
context.Errorf("RoundTrip: %s", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
defer resp.Body.Close()
for _, key := range reflectedHeaderFields {
values, ok := resp.Header[key]
if ok {
for _, value := range values {
w.Header().Add(key, value)
n, err := io.Copy(w, resp.Body)
if err != nil {
context.Errorf("io.Copy after %d bytes: %s", n, err)
func init() {
http.HandleFunc("/", handler)
......@@ -22,18 +22,27 @@ The Broker expects:
### Running your own
You can run your own Broker on either localhost or appengine.
(Other CDNs will be supported soon.)
To run on localhost, run `` or equivalent from this
directory. (on arch, I use the wrapper script `dev_appserver-go`)
To run on appengine, you can spin up your own instance with an arbitrary
name, and use ``.
In both cases, you'll need to provide the URL of the custom broker
The server uses TLS by default.
There is a `--disable-tls` option for testing purposes,
but you should use TLS in production.
The server automatically fetches certificates
from [Let's Encrypt]('s_Encrypt) as needed.
Use the `--acme-hostnames` option to tell the server
what hostnames it may request certificates for.
You can optionally provide a contact email address,
using the `--acme-email` option,
so that Let's Encrypt can inform you of any problems.
In order to fetch certificates automatically,
the server needs to be listening on port 443 (the default).
On Linux, you can use the `setcap` program,
part of libcap2, to enable the broker to bind to low-numbered ports
without having to run as root:
setcap 'cap_net_bind_service=+ep' /usr/local/bin/broker
You can control the listening port with the --addr option.
You'll need to provide the URL of the custom broker
to the client plugin using the `--url $URL` flag.
See more detailed appengine instructions
......@@ -3,16 +3,21 @@ Broker acts as the HTTP signaling channel.
It matches clients and snowflake proxies by passing corresponding
SessionDescriptions in order to negotiate a WebRTC connection.
package snowflake_broker
package main
import (
const (
......@@ -217,7 +222,27 @@ func robotsTxtHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("User-agent: *\nDisallow:\n"))
func init() {
func ipHandler(w http.ResponseWriter, r *http.Request) {
remoteAddr := r.RemoteAddr
if net.ParseIP(remoteAddr).To4() == nil {
remoteAddr = "[" + remoteAddr + "]"
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
func main() {
var acmeEmail string
var acmeHostnamesCommas string
var addr string
var disableTLS bool
flag.StringVar(&acmeEmail, "acme-email", "", "optional contact email for Let's Encrypt notifications")
flag.StringVar(&acmeHostnamesCommas, "acme-hostnames", "", "comma-separated hostnames for TLS certificate")
flag.StringVar(&addr, "addr", ":443", "address to listen on")
flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS")
log.SetFlags(log.LstdFlags | log.LUTC)
ctx := NewBrokerContext()
......@@ -230,4 +255,31 @@ func init() {
http.Handle("/client", SnowflakeHandler{ctx, clientOffers})
http.Handle("/answer", SnowflakeHandler{ctx, proxyAnswers})
http.Handle("/debug", SnowflakeHandler{ctx, debugHandler})
var err error
server := http.Server{
Addr: addr,
if acmeHostnamesCommas != "" {
acmeHostnames := strings.Split(acmeHostnamesCommas, ",")
log.Printf("ACME hostnames: %q", acmeHostnames)
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(acmeHostnames...),
Email: acmeEmail,
server.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate}
err = server.ListenAndServeTLS("", "")
} else if disableTLS {
err = server.ListenAndServe()
} else {
log.Fatal("the --acme-hostnames or --disable-tls option is required")
if err != nil {
package snowflake_broker
package main
import (
// ""
package snowflake_broker
package main
import (
......@@ -2,7 +2,7 @@
Keeping track of pending available snowflake proxies.
package snowflake_broker
package main
The Snowflake struct contains a single interaction
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment