diff --git a/appengine/README b/appengine/README new file mode 100644 index 0000000..bec9fcb --- /dev/null +++ b/appengine/README @@ -0,0 +1,27 @@ +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. + https://cloud.google.com/sdk/docs/#linux +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/dev_appserver.py app.yaml +The app will be running at http://127.0.0.1:8080/. + +To deploy to App Engine, first create a new project and app. You have to +think of a unique name (marked as "" 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 + google-cloud-sdk/bin/gcloud app create --project= +Then to deploy the project, run: + google-cloud-sdk/bin/gcloud app deploy --project= + +To configure the Snowflake client to talk to the App Engine app, provide +"https://.appspot.com/" as the --url option. + UseBridges 1 + Bridge snowflake 0.0.2.0:1 + ClientTransportPlugin snowflake exec ./client -url https://.appspot.com/ -front www.google.com diff --git a/broker/app.yaml b/appengine/app.yaml similarity index 50% rename from broker/app.yaml rename to appengine/app.yaml index 14fcf0a..44df436 100644 --- a/broker/app.yaml +++ b/appengine/app.yaml @@ -1,6 +1,3 @@ -# override this with appcfg.py -A $YOUR_APP_ID -application: snowflake-reg -version: 1 runtime: go api_version: go1 diff --git a/appengine/reflect.go b/appengine/reflect.go new file mode 100644 index 0000000..f6b5336 --- /dev/null +++ b/appengine/reflect.go @@ -0,0 +1,108 @@ +// A web app for Google App Engine that proxies HTTP requests and responses to +// the Snowflake broker. +package reflect + +import ( + "io" + "net/http" + "net/url" + "time" + + "appengine" + "appengine/urlfetch" +) + +const ( + forwardURL = "https://snowflake-broker.bamsoftware.com/" + // 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{ + "Content-Type", + "X-Session-Id", +} + +// 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 "https://example.com/reflect". + 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) + return + } + // 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) + return + } + defer resp.Body.Close() + for _, key := range reflectedHeaderFields { + values, ok := resp.Header[key] + if ok { + for _, value := range values { + w.Header().Add(key, value) + } + } + } + w.WriteHeader(resp.StatusCode) + 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) +} diff --git a/broker/README.md b/broker/README.md index ab6af99..38a8029 100644 --- a/broker/README.md +++ b/broker/README.md @@ -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.) +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](https://en.wikipedia.org/wiki/Let'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. -To run on localhost, run `dev_appserver.py` or equivalent from this -directory. (on arch, I use the wrapper script `dev_appserver-go`) +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. -To run on appengine, you can spin up your own instance with an arbitrary -name, and use `appcfg.py`. - -In both cases, you'll need to provide the URL of the custom broker +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 -[here](https://cloud.google.com/appengine/docs/go/). diff --git a/broker/broker.go b/broker/broker.go index a56f40b..6cb08d9 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -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 ( "container/heap" + "crypto/tls" + "flag" "fmt" "io/ioutil" "log" "net" "net/http" + "strings" "time" + + "golang.org/x/crypto/acme/autocert" ) 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") + w.Write([]byte(remoteAddr)) +} + +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") + flag.Parse() + 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 { + log.Fatal(err) + } } diff --git a/broker/metrics.go b/broker/metrics.go index f64d1cc..002fdfe 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -1,4 +1,4 @@ -package snowflake_broker +package main import ( // "golang.org/x/net/internal/timeseries" diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 44940a3..109d7df 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -1,4 +1,4 @@ -package snowflake_broker +package main import ( "bytes" diff --git a/broker/snowflake-heap.go b/broker/snowflake-heap.go index cf249fe..419956f 100644 --- a/broker/snowflake-heap.go +++ b/broker/snowflake-heap.go @@ -2,7 +2,7 @@ Keeping track of pending available snowflake proxies. */ -package snowflake_broker +package main /* The Snowflake struct contains a single interaction