Browse Source

Work on public demo server.

Brad Fitzpatrick 11 years ago
parent
commit
adee152a1a
4 changed files with 394 additions and 4 deletions
  1. 3 0
      h2demo/.gitignore
  2. 5 0
      h2demo/Makefile
  3. 107 4
      h2demo/h2demo.go
  4. 279 0
      h2demo/launch.go

+ 3 - 0
h2demo/.gitignore

@@ -0,0 +1,3 @@
+h2demo
+h2demo.linux
+

+ 5 - 0
h2demo/Makefile

@@ -0,0 +1,5 @@
+h2demo.linux: h2demo.go
+	GOOS=linux go build -o h2demo.linux .
+
+upload: h2demo
+	cat h2demo.linux | go run launch.go --write_object=http2-demo-server-tls/h2demo --write_object_is_public

+ 107 - 4
h2demo/h2demo.go

@@ -6,18 +6,119 @@
 package main
 
 import (
+	"crypto/tls"
 	"flag"
+	"fmt"
 	"io"
+	"io/ioutil"
 	"log"
+	"net"
 	"net/http"
 	"os/exec"
 	"runtime"
 	"time"
 
+	"camlistore.org/pkg/googlestorage"
 	"github.com/bradfitz/http2"
 )
 
-var openFirefox = flag.Bool("openff", false, "Open Firefox")
+var (
+	openFirefox = flag.Bool("openff", false, "Open Firefox")
+	prod        = flag.Bool("prod", false, "Whether to configure itself to be the production http2.golang.org server.")
+)
+
+func oldHTTPHandler(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, <b>you're not using HTTP/2 right now</b>.</p>
+<p>See code & instructions for connecting at <a href="https://github.com/bradfitz/http2">https://github.com/bradfitz/http2</a>.</p>
+
+</body></html>`)
+}
+
+func registerHandlers() {
+	mux := http.NewServeMux()
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		if r.TLS == nil {
+			http.Redirect(w, r, "https://http2.golang.org/", http.StatusFound)
+			return
+		}
+		if r.ProtoMajor == 1 {
+			oldHTTPHandler(w, r)
+			return
+		}
+		mux.ServeHTTP(w, r)
+	})
+	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		// TODO: more
+		io.WriteString(w, "<h1>Hello, world!</h1>Greetings from Go's HTTP/2 server. You're speaking HTTP/2.")
+	})
+}
+
+func serveProdTLS() error {
+	c, err := googlestorage.NewServiceClient()
+	if err != nil {
+		return err
+	}
+	slurp := func(key string) ([]byte, error) {
+		const bucket = "http2-demo-server-tls"
+		rc, _, err := c.GetObject(&googlestorage.Object{
+			Bucket: bucket,
+			Key:    key,
+		})
+		if err != nil {
+			return nil, fmt.Errorf("Error fetching GCS object %q in bucket %q: %v", key, bucket, err)
+		}
+		defer rc.Close()
+		return ioutil.ReadAll(rc)
+	}
+	certPem, err := slurp("http2.golang.org.chained.pem")
+	if err != nil {
+		return err
+	}
+	keyPem, err := slurp("http2.golang.org.key")
+	if err != nil {
+		return err
+	}
+	cert, err := tls.X509KeyPair(certPem, keyPem)
+	if err != nil {
+		return err
+	}
+	srv := &http.Server{
+		TLSConfig: &tls.Config{
+			Certificates: []tls.Certificate{cert},
+		},
+	}
+	http2.ConfigureServer(srv, &http2.Server{})
+	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
+}
 
 func main() {
 	var srv http.Server
@@ -25,9 +126,11 @@ func main() {
 	flag.StringVar(&srv.Addr, "addr", "localhost:4430", "host:port to listen on ")
 	flag.Parse()
 
-	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		io.WriteString(w, "<h1>Hello, world!</h1>Greetings from Go's HTTP/2 server.")
-	})
+	registerHandlers()
+
+	if *prod {
+		log.Fatal(serveProd())
+	}
 
 	url := "https://" + srv.Addr + "/"
 	log.Printf("Listening on " + url)

+ 279 - 0
h2demo/launch.go

@@ -0,0 +1,279 @@
+// 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 ignore
+
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"strings"
+	"time"
+
+	"code.google.com/p/goauth2/oauth"
+	compute "code.google.com/p/google-api-go-client/compute/v1"
+)
+
+var (
+	proj     = flag.String("project", "symbolic-datum-552", "name of Project")
+	zone     = flag.String("zone", "us-central1-a", "GCE zone")
+	mach     = flag.String("machinetype", "n1-standard-1", "Machine type")
+	instName = flag.String("instance_name", "http2-demo", "Name of VM instance.")
+	sshPub   = flag.String("ssh_public_key", "", "ssh public key file to authorize. Can modify later in Google's web UI anyway.")
+	staticIP = flag.String("static_ip", "130.211.116.44", "Static IP to use. If empty, automatic.")
+
+	writeObject  = flag.String("write_object", "", "If non-empty, a VM isn't created and the flag value is Google Cloud Storage bucket/object to write. The contents from stdin.")
+	publicObject = flag.Bool("write_object_is_public", false, "Whether the object created by --write_object should be public.")
+)
+
+func readFile(v string) string {
+	slurp, err := ioutil.ReadFile(v)
+	if err != nil {
+		log.Fatalf("Error reading %s: %v", v, err)
+	}
+	return strings.TrimSpace(string(slurp))
+}
+
+var config = &oauth.Config{
+	// The client-id and secret should be for an "Installed Application" when using
+	// the CLI. Later we'll use a web application with a callback.
+	ClientId:     readFile("client-id.dat"),
+	ClientSecret: readFile("client-secret.dat"),
+	Scope: strings.Join([]string{
+		compute.DevstorageFull_controlScope,
+		compute.ComputeScope,
+		"https://www.googleapis.com/auth/sqlservice",
+		"https://www.googleapis.com/auth/sqlservice.admin",
+	}, " "),
+	AuthURL:     "https://accounts.google.com/o/oauth2/auth",
+	TokenURL:    "https://accounts.google.com/o/oauth2/token",
+	RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
+}
+
+const baseConfig = `#cloud-config
+coreos:
+  units:
+    - name: h2demo.service
+      command: start
+      content: |
+        [Unit]
+        Description=HTTP2 Demo
+        
+        [Service]
+        ExecStartPre=/bin/bash -c 'mkdir -p /opt/bin && curl -s -o /opt/bin/h2demo http://storage.googleapis.com/http2-demo-server-tls/h2demo && chmod +x /opt/bin/h2demo'
+        ExecStart=/opt/bin/h2demo
+        RestartSec=5s
+        Restart=always
+        Type=simple
+        
+        [Install]
+        WantedBy=multi-user.target
+`
+
+func main() {
+	flag.Parse()
+	if *proj == "" {
+		log.Fatalf("Missing --project flag")
+	}
+	prefix := "https://www.googleapis.com/compute/v1/projects/" + *proj
+	machType := prefix + "/zones/" + *zone + "/machineTypes/" + *mach
+
+	tr := &oauth.Transport{
+		Config: config,
+	}
+
+	tokenCache := oauth.CacheFile("token.dat")
+	token, err := tokenCache.Token()
+	if err != nil {
+		if *writeObject != "" {
+			log.Fatalf("Can't use --write_object without a valid token.dat file already cached.")
+		}
+		log.Printf("Error getting token from %s: %v", string(tokenCache), err)
+		log.Printf("Get auth code from %v", config.AuthCodeURL("my-state"))
+		fmt.Print("\nEnter auth code: ")
+		sc := bufio.NewScanner(os.Stdin)
+		sc.Scan()
+		authCode := strings.TrimSpace(sc.Text())
+		token, err = tr.Exchange(authCode)
+		if err != nil {
+			log.Fatalf("Error exchanging auth code for a token: %v", err)
+		}
+		tokenCache.PutToken(token)
+	}
+
+	tr.Token = token
+	oauthClient := &http.Client{Transport: tr}
+	if *writeObject != "" {
+		writeCloudStorageObject(oauthClient)
+		return
+	}
+
+	computeService, _ := compute.New(oauthClient)
+
+	natIP := *staticIP
+	if natIP == "" {
+		// Try to find it by name.
+		aggAddrList, err := computeService.Addresses.AggregatedList(*proj).Do()
+		if err != nil {
+			log.Fatal(err)
+		}
+		// http://godoc.org/code.google.com/p/google-api-go-client/compute/v1#AddressAggregatedList
+	IPLoop:
+		for _, asl := range aggAddrList.Items {
+			for _, addr := range asl.Addresses {
+				if addr.Name == *instName+"-ip" && addr.Status == "RESERVED" {
+					natIP = addr.Address
+					break IPLoop
+				}
+			}
+		}
+	}
+
+	cloudConfig := baseConfig
+	if *sshPub != "" {
+		key := strings.TrimSpace(readFile(*sshPub))
+		cloudConfig += fmt.Sprintf("\nssh_authorized_keys:\n    - %s\n", key)
+	}
+	if os.Getenv("USER") == "bradfitz" {
+		cloudConfig += fmt.Sprintf("\nssh_authorized_keys:\n    - %s\n", "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAwks9dwWKlRC+73gRbvYtVg0vdCwDSuIlyt4z6xa/YU/jTDynM4R4W10hm2tPjy8iR1k8XhDv4/qdxe6m07NjG/By1tkmGpm1mGwho4Pr5kbAAy/Qg+NLCSdAYnnE00FQEcFOC15GFVMOW2AzDGKisReohwH9eIzHPzdYQNPRWXE= bradfitz@papag.bradfitz.com")
+	}
+	const maxCloudConfig = 32 << 10 // per compute API docs
+	if len(cloudConfig) > maxCloudConfig {
+		log.Fatalf("cloud config length of %d bytes is over %d byte limit", len(cloudConfig), maxCloudConfig)
+	}
+
+	instance := &compute.Instance{
+		Name:        *instName,
+		Description: "Go Builder",
+		MachineType: machType,
+		Disks:       []*compute.AttachedDisk{instanceDisk(computeService)},
+		Tags: &compute.Tags{
+			Items: []string{"http-server", "https-server"},
+		},
+		Metadata: &compute.Metadata{
+			Items: []*compute.MetadataItems{
+				{
+					Key:   "user-data",
+					Value: cloudConfig,
+				},
+			},
+		},
+		NetworkInterfaces: []*compute.NetworkInterface{
+			&compute.NetworkInterface{
+				AccessConfigs: []*compute.AccessConfig{
+					&compute.AccessConfig{
+						Type:  "ONE_TO_ONE_NAT",
+						Name:  "External NAT",
+						NatIP: natIP,
+					},
+				},
+				Network: prefix + "/global/networks/default",
+			},
+		},
+		ServiceAccounts: []*compute.ServiceAccount{
+			{
+				Email: "default",
+				Scopes: []string{
+					compute.DevstorageFull_controlScope,
+					compute.ComputeScope,
+				},
+			},
+		},
+	}
+
+	log.Printf("Creating instance...")
+	op, err := computeService.Instances.Insert(*proj, *zone, instance).Do()
+	if err != nil {
+		log.Fatalf("Failed to create instance: %v", err)
+	}
+	opName := op.Name
+	log.Printf("Created. Waiting on operation %v", opName)
+OpLoop:
+	for {
+		time.Sleep(2 * time.Second)
+		op, err := computeService.ZoneOperations.Get(*proj, *zone, opName).Do()
+		if err != nil {
+			log.Fatalf("Failed to get op %s: %v", opName, err)
+		}
+		switch op.Status {
+		case "PENDING", "RUNNING":
+			log.Printf("Waiting on operation %v", opName)
+			continue
+		case "DONE":
+			if op.Error != nil {
+				for _, operr := range op.Error.Errors {
+					log.Printf("Error: %+v", operr)
+				}
+				log.Fatalf("Failed to start.")
+			}
+			log.Printf("Success. %+v", op)
+			break OpLoop
+		default:
+			log.Fatalf("Unknown status %q: %+v", op.Status, op)
+		}
+	}
+
+	inst, err := computeService.Instances.Get(*proj, *zone, *instName).Do()
+	if err != nil {
+		log.Fatalf("Error getting instance after creation: %v", err)
+	}
+	ij, _ := json.MarshalIndent(inst, "", "    ")
+	log.Printf("Instance: %s", ij)
+}
+
+func instanceDisk(svc *compute.Service) *compute.AttachedDisk {
+	const imageURL = "https://www.googleapis.com/compute/v1/projects/coreos-cloud/global/images/coreos-stable-445-5-0-v20141016"
+	diskName := *instName + "-coreos-stateless-pd"
+
+	return &compute.AttachedDisk{
+		AutoDelete: true,
+		Boot:       true,
+		Type:       "PERSISTENT",
+		InitializeParams: &compute.AttachedDiskInitializeParams{
+			DiskName:    diskName,
+			SourceImage: imageURL,
+			DiskSizeGb:  50,
+		},
+	}
+}
+
+func writeCloudStorageObject(httpClient *http.Client) {
+	content := os.Stdin
+	const maxSlurp = 1 << 20
+	var buf bytes.Buffer
+	n, err := io.CopyN(&buf, content, maxSlurp)
+	if err != nil && err != io.EOF {
+		log.Fatalf("Error reading from stdin: %v, %v", n, err)
+	}
+	contentType := http.DetectContentType(buf.Bytes())
+
+	req, err := http.NewRequest("PUT", "https://storage.googleapis.com/"+*writeObject, io.MultiReader(&buf, content))
+	if err != nil {
+		log.Fatal(err)
+	}
+	req.Header.Set("x-goog-api-version", "2")
+	if *publicObject {
+		req.Header.Set("x-goog-acl", "public-read")
+	}
+	req.Header.Set("Content-Type", contentType)
+	res, err := httpClient.Do(req)
+	if err != nil {
+		log.Fatal(err)
+	}
+	if res.StatusCode != 200 {
+		res.Write(os.Stderr)
+		log.Fatalf("Failed.")
+	}
+	log.Printf("Success.")
+	os.Exit(0)
+}