|
|
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build h2demo
package main
import ( "bytes" "crypto/tls" "flag" "fmt" "hash/crc32" "image" "image/jpeg" "io" "io/ioutil" "log" "net" "net/http" "os" "path" "regexp" "runtime" "strconv" "strings" "sync" "time"
"go4.org/syncutil/singleflight" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/http2" )
var ( prod = flag.Bool("prod", false, "Whether to configure itself to be the production http2.golang.org server.")
httpsAddr = flag.String("https_addr", "localhost:4430", "TLS address to listen on ('host:port' or ':port'). Required.") httpAddr = flag.String("http_addr", "", "Plain HTTP address to listen on ('host:port', or ':port'). Empty means no HTTP.")
hostHTTP = flag.String("http_host", "", "Optional host or host:port to use for http:// links to this service. By default, this is implied from -http_addr.") hostHTTPS = flag.String("https_host", "", "Optional host or host:port to use for http:// links to this service. By default, this is implied from -https_addr.") )
func homeOldHTTP(w http.ResponseWriter, r *http.Request) { io.WriteString(w, `<html> <body> <h1>Go + HTTP/2</h1> <p>Welcome to <a href="https://golang.org/">the Go language</a>'s <a href="https://http2.github.io/">HTTP/2</a> demo & interop server.</p> <p>Unfortunately, you're <b>not</b> using HTTP/2 right now. To do so:</p> <ul> <li>Use Firefox Nightly or go to <b>about:config</b> and enable "network.http.spdy.enabled.http2draft"</li> <li>Use Google Chrome Canary and/or go to <b>chrome://flags/#enable-spdy4</b> to <i>Enable SPDY/4</i> (Chrome's name for HTTP/2)</li>
</ul> <p>See code & instructions for connecting at <a href="https://github.com/golang/net/tree/master/http2">https://github.com/golang/net/tree/master/http2</a>.</p>
</body></html>`) }
func home(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } io.WriteString(w, `<html> <body> <h1>Go + HTTP/2</h1>
<p>Welcome to <a href="https://golang.org/">the Go language</a>'s <a href="https://http2.github.io/">HTTP/2</a> demo & interop server.</p>
<p>Congratulations, <b>you're using HTTP/2 right now</b>.</p>
<p>This server exists for others in the HTTP/2 community to test their HTTP/2 client implementations and point out flaws in our server.</p>
<p> The code is at <a href="https://golang.org/x/net/http2">golang.org/x/net/http2</a> and is used transparently by the Go standard library from Go 1.6 and later. </p>
<p>Contact info: <i>bradfitz@golang.org</i>, or <a href="https://golang.org/s/http2bug">file a bug</a>.</p>
<h2>Handlers for testing</h2> <ul> <li>GET <a href="/reqinfo">/reqinfo</a> to dump the request + headers received</li> <li>GET <a href="/clockstream">/clockstream</a> streams the current time every second</li> <li>GET <a href="/gophertiles">/gophertiles</a> to see a page with a bunch of images</li> <li>GET <a href="/serverpush">/serverpush</a> to see a page with server push</li> <li>GET <a href="/file/gopher.png">/file/gopher.png</a> for a small file (does If-Modified-Since, Content-Range, etc)</li> <li>GET <a href="/file/go.src.tar.gz">/file/go.src.tar.gz</a> for a larger file (~10 MB)</li> <li>GET <a href="/redirect">/redirect</a> to redirect back to / (this page)</li> <li>GET <a href="/goroutines">/goroutines</a> to see all active goroutines in this server</li> <li>PUT something to <a href="/crc32">/crc32</a> to get a count of number of bytes and its CRC-32</li> <li>PUT something to <a href="/ECHO">/ECHO</a> and it will be streamed back to you capitalized</li> </ul>
</body></html>`) }
func reqInfoHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") fmt.Fprintf(w, "Method: %s\n", r.Method) fmt.Fprintf(w, "Protocol: %s\n", r.Proto) fmt.Fprintf(w, "Host: %s\n", r.Host) fmt.Fprintf(w, "RemoteAddr: %s\n", r.RemoteAddr) fmt.Fprintf(w, "RequestURI: %q\n", r.RequestURI) fmt.Fprintf(w, "URL: %#v\n", r.URL) fmt.Fprintf(w, "Body.ContentLength: %d (-1 means unknown)\n", r.ContentLength) fmt.Fprintf(w, "Close: %v (relevant for HTTP/1 only)\n", r.Close) fmt.Fprintf(w, "TLS: %#v\n", r.TLS) fmt.Fprintf(w, "\nHeaders:\n") r.Header.Write(w) }
func crcHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { http.Error(w, "PUT required.", 400) return } crc := crc32.NewIEEE() n, err := io.Copy(crc, r.Body) if err == nil { w.Header().Set("Content-Type", "text/plain") fmt.Fprintf(w, "bytes=%d, CRC32=%x", n, crc.Sum(nil)) } }
type capitalizeReader struct { r io.Reader }
func (cr capitalizeReader) Read(p []byte) (n int, err error) { n, err = cr.r.Read(p) for i, b := range p[:n] { if b >= 'a' && b <= 'z' { p[i] = b - ('a' - 'A') } } return }
type flushWriter struct { w io.Writer }
func (fw flushWriter) Write(p []byte) (n int, err error) { n, err = fw.w.Write(p) if f, ok := fw.w.(http.Flusher); ok { f.Flush() } return }
func echoCapitalHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { http.Error(w, "PUT required.", 400) return } io.Copy(flushWriter{w}, capitalizeReader{r.Body}) }
var ( fsGrp singleflight.Group fsMu sync.Mutex // guards fsCache
fsCache = map[string]http.Handler{} )
// fileServer returns a file-serving handler that proxies URL.
// It lazily fetches URL on the first access and caches its contents forever.
func fileServer(url string, latency time.Duration) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if latency > 0 { time.Sleep(latency) } hi, err := fsGrp.Do(url, func() (interface{}, error) { fsMu.Lock() if h, ok := fsCache[url]; ok { fsMu.Unlock() return h, nil } fsMu.Unlock()
res, err := http.Get(url) if err != nil { return nil, err } defer res.Body.Close() slurp, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err }
modTime := time.Now() var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.ServeContent(w, r, path.Base(url), modTime, bytes.NewReader(slurp)) }) fsMu.Lock() fsCache[url] = h fsMu.Unlock() return h, nil }) if err != nil { http.Error(w, err.Error(), 500) return } hi.(http.Handler).ServeHTTP(w, r) }) }
func clockStreamHandler(w http.ResponseWriter, r *http.Request) { clientGone := w.(http.CloseNotifier).CloseNotify() w.Header().Set("Content-Type", "text/plain") ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() fmt.Fprintf(w, "# ~1KB of junk to force browsers to start rendering immediately: \n") io.WriteString(w, strings.Repeat("# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n", 13))
for { fmt.Fprintf(w, "%v\n", time.Now()) w.(http.Flusher).Flush() select { case <-ticker.C: case <-clientGone: log.Printf("Client %v disconnected from the clock", r.RemoteAddr) return } } }
func registerHandlers() { tiles := newGopherTilesHandler() push := newPushHandler()
mux2 := http.NewServeMux() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/gophertiles": tiles.ServeHTTP(w, r) // allow HTTP/2 + HTTP/1.x
return case strings.HasPrefix(r.URL.Path, "/serverpush"): push.ServeHTTP(w, r) // allow HTTP/2 + HTTP/1.x
return case r.TLS == nil: // do not allow HTTP/1.x for anything else
http.Redirect(w, r, "https://"+httpsHost()+"/", http.StatusFound) return } if r.ProtoMajor == 1 { if r.URL.Path == "/reqinfo" { reqInfoHandler(w, r) return } homeOldHTTP(w, r) return } mux2.ServeHTTP(w, r) }) mux2.HandleFunc("/", home) mux2.Handle("/file/gopher.png", fileServer("https://golang.org/doc/gopher/frontpage.png", 0)) mux2.Handle("/file/go.src.tar.gz", fileServer("https://storage.googleapis.com/golang/go1.4.1.src.tar.gz", 0)) mux2.HandleFunc("/reqinfo", reqInfoHandler) mux2.HandleFunc("/crc32", crcHandler) mux2.HandleFunc("/ECHO", echoCapitalHandler) mux2.HandleFunc("/clockstream", clockStreamHandler) mux2.Handle("/gophertiles", tiles) mux2.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusFound) }) stripHomedir := regexp.MustCompile(`/(Users|home)/\w+`) mux2.HandleFunc("/goroutines", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") buf := make([]byte, 2<<20) w.Write(stripHomedir.ReplaceAll(buf[:runtime.Stack(buf, true)], nil)) }) }
var pushResources = map[string]http.Handler{ "/serverpush/static/jquery.min.js": fileServer("https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js", 100*time.Millisecond), "/serverpush/static/godocs.js": fileServer("https://golang.org/lib/godoc/godocs.js", 100*time.Millisecond), "/serverpush/static/playground.js": fileServer("https://golang.org/lib/godoc/playground.js", 100*time.Millisecond), "/serverpush/static/style.css": fileServer("https://golang.org/lib/godoc/style.css", 100*time.Millisecond), }
func newPushHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for path, handler := range pushResources { if r.URL.Path == path { handler.ServeHTTP(w, r) return } }
cacheBust := time.Now().UnixNano() if pusher, ok := w.(http.Pusher); ok { for path := range pushResources { url := fmt.Sprintf("%s?%d", path, cacheBust) if err := pusher.Push(url, nil); err != nil { log.Printf("Failed to push %v: %v", path, err) } } } time.Sleep(100 * time.Millisecond) // fake network latency + parsing time
if err := pushTmpl.Execute(w, struct { CacheBust int64 HTTPSHost string HTTPHost string }{ CacheBust: cacheBust, HTTPSHost: httpsHost(), HTTPHost: httpHost(), }); err != nil { log.Printf("Executing server push template: %v", err) } }) }
func newGopherTilesHandler() http.Handler { const gopherURL = "https://blog.golang.org/go-programming-language-turns-two_gophers.jpg" res, err := http.Get(gopherURL) if err != nil { log.Fatal(err) } if res.StatusCode != 200 { log.Fatalf("Error fetching %s: %v", gopherURL, res.Status) } slurp, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } im, err := jpeg.Decode(bytes.NewReader(slurp)) if err != nil { if len(slurp) > 1024 { slurp = slurp[:1024] } log.Fatalf("Failed to decode gopher image: %v (got %q)", err, slurp) }
type subImager interface { SubImage(image.Rectangle) image.Image } const tileSize = 32 xt := im.Bounds().Max.X / tileSize yt := im.Bounds().Max.Y / tileSize var tile [][][]byte // y -> x -> jpeg bytes
for yi := 0; yi < yt; yi++ { var row [][]byte for xi := 0; xi < xt; xi++ { si := im.(subImager).SubImage(image.Rectangle{ Min: image.Point{xi * tileSize, yi * tileSize}, Max: image.Point{(xi + 1) * tileSize, (yi + 1) * tileSize}, }) buf := new(bytes.Buffer) if err := jpeg.Encode(buf, si, &jpeg.Options{Quality: 90}); err != nil { log.Fatal(err) } row = append(row, buf.Bytes()) } tile = append(tile, row) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ms, _ := strconv.Atoi(r.FormValue("latency")) const nanosPerMilli = 1e6 if r.FormValue("x") != "" { x, _ := strconv.Atoi(r.FormValue("x")) y, _ := strconv.Atoi(r.FormValue("y")) if ms <= 1000 { time.Sleep(time.Duration(ms) * nanosPerMilli) } if x >= 0 && x < xt && y >= 0 && y < yt { http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(tile[y][x])) return } } io.WriteString(w, "<html><body onload='showtimes()'>") fmt.Fprintf(w, "A grid of %d tiled images is below. Compare:<p>", xt*yt) for _, ms := range []int{0, 30, 200, 1000} { d := time.Duration(ms) * nanosPerMilli fmt.Fprintf(w, "[<a href='https://%s/gophertiles?latency=%d'>HTTP/2, %v latency</a>] [<a href='http://%s/gophertiles?latency=%d'>HTTP/1, %v latency</a>]<br>\n", httpsHost(), ms, d, httpHost(), ms, d, ) } io.WriteString(w, "<p>\n") cacheBust := time.Now().UnixNano() for y := 0; y < yt; y++ { for x := 0; x < xt; x++ { fmt.Fprintf(w, "<img width=%d height=%d src='/gophertiles?x=%d&y=%d&cachebust=%d&latency=%d'>", tileSize, tileSize, x, y, cacheBust, ms) } io.WriteString(w, "<br/>\n") } io.WriteString(w, `<p><div id='loadtimes'></div></p> <script> function showtimes() { var times = 'Times from connection start:<br>' times += 'DOM loaded: ' + (window.performance.timing.domContentLoadedEventEnd - window.performance.timing.connectStart) + 'ms<br>' times += 'DOM complete (images loaded): ' + (window.performance.timing.domComplete - window.performance.timing.connectStart) + 'ms<br>' document.getElementById('loadtimes').innerHTML = times } </script> <hr><a href='/'><< Back to Go HTTP/2 demo server</a></body></html>`) }) }
func httpsHost() string { if *hostHTTPS != "" { return *hostHTTPS } if v := *httpsAddr; strings.HasPrefix(v, ":") { return "localhost" + v } else { return v } }
func httpHost() string { if *hostHTTP != "" { return *hostHTTP } if v := *httpAddr; strings.HasPrefix(v, ":") { return "localhost" + v } else { return v } }
func serveProdTLS() error { const cacheDir = "/var/cache/autocert" if err := os.MkdirAll(cacheDir, 0700); err != nil { return err } m := autocert.Manager{ Cache: autocert.DirCache(cacheDir), Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist("http2.golang.org"), } srv := &http.Server{ TLSConfig: &tls.Config{ GetCertificate: m.GetCertificate, }, } http2.ConfigureServer(srv, &http2.Server{ NewWriteScheduler: func() http2.WriteScheduler { return http2.NewPriorityWriteScheduler(nil) }, }) ln, err := net.Listen("tcp", ":443") if err != nil { return err } return srv.Serve(tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, srv.TLSConfig)) }
type tcpKeepAliveListener struct { *net.TCPListener }
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc, err := ln.AcceptTCP() if err != nil { return } tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(3 * time.Minute) return tc, nil }
func serveProd() error { errc := make(chan error, 2) go func() { errc <- http.ListenAndServe(":80", nil) }() go func() { errc <- serveProdTLS() }() return <-errc }
const idleTimeout = 5 * time.Minute const activeTimeout = 10 * time.Minute
// TODO: put this into the standard library and actually send
// PING frames and GOAWAY, etc: golang.org/issue/14204
func idleTimeoutHook() func(net.Conn, http.ConnState) { var mu sync.Mutex m := map[net.Conn]*time.Timer{} return func(c net.Conn, cs http.ConnState) { mu.Lock() defer mu.Unlock() if t, ok := m[c]; ok { delete(m, c) t.Stop() } var d time.Duration switch cs { case http.StateNew, http.StateIdle: d = idleTimeout case http.StateActive: d = activeTimeout default: return } m[c] = time.AfterFunc(d, func() { log.Printf("closing idle conn %v after %v", c.RemoteAddr(), d) go c.Close() }) } }
func main() { var srv http.Server flag.BoolVar(&http2.VerboseLogs, "verbose", false, "Verbose HTTP/2 debugging.") flag.Parse() srv.Addr = *httpsAddr srv.ConnState = idleTimeoutHook()
registerHandlers()
if *prod { *hostHTTP = "http2.golang.org" *hostHTTPS = "http2.golang.org" log.Fatal(serveProd()) }
url := "https://" + httpsHost() + "/" log.Printf("Listening on " + url) http2.ConfigureServer(&srv, &http2.Server{})
if *httpAddr != "" { go func() { log.Printf("Listening on http://" + httpHost() + "/ (for unencrypted HTTP/1)") log.Fatal(http.ListenAndServe(*httpAddr, nil)) }() }
go func() { log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key")) }() select {} }
|