vcs-test: code for vcs-test.golang.org

Change-Id: I48e8648fc9f3e3b64a548743fd2c2480f881bfb1
Reviewed-on: https://go-review.googlesource.com/73332
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Ross Light <light@google.com>
diff --git a/vcs-test/README.md b/vcs-test/README.md
new file mode 100644
index 0000000..6a527fa
--- /dev/null
+++ b/vcs-test/README.md
@@ -0,0 +1,72 @@
+# vcs-test
+
+We run a version control server for testing at `vcs-test.golang.org`.
+
+## Machine initialization
+
+The machine should just run. You should not need these instructions very often.
+In particular you do not need them just to make a change to `vcweb`.
+Skip ahead to the next section.
+
+The VM runs in the builder project “symbolic-datum-552” in zone `us-central1-a`,
+where it has a reserved static IP address named `vcs-test`.
+
+To destroy the current VM (if any) and rebuild a fresh one in its place, run:
+
+	./rebuild-server.sh && ./rebuild-systemd.sh && ./redeploy-vcweb.sh
+
+You should not need to do this unless you have changed rebuild-server.sh and want to test it.
+
+To delete the VM's current systemd configuration for `vcweb` and upload the configuration
+from the local directory (specifically, `vcweb.service` and `vcweb*.socket`), run:
+
+	./rebuild-systemd.sh && ./redeploy-vcweb.sh
+
+You should not need to do this unless you have changed the systemd configuration files.
+
+## vcweb
+
+The Go program that runs the actual server is in the subdirectory `vcweb`.
+For local development:
+
+	go build -o vcweb.exe ./vcweb && ./vcweb.exe
+
+It maintains files in `/tmp/vcweb` and serves localhost:8088.
+
+Once you are happy with local testing, deploy to the VM by running `./redeploy-vcweb.sh`.
+
+## Repositories
+
+The server can serve Bazaar, Fossil, Git, Mercurial, and Subversion repositories.
+The root of each repository is `http://vcs-test.golang.org/VCS/REPONAME`,
+where `VCS` is the version control system's command name (`bzr` for Bazaar, and so on),
+and `REPONAME` is the repository name.
+
+To serve a particular repository, the server downloads
+`gs://vcs-test/VCS/REPONAME.zip` from Google Cloud Storage and unzips it
+into an empty directory.
+The result should be a valid repository directory for the given version control system.
+If the needed format of the zip file is unclear, download and inspect `gs://vcs-test/VCS/hello.zip`
+from `https://vcs-test.storage.googleapis.com/VCS/hello.zip`.
+
+Stale data may be served for up to five minutes after a zip file is updated in the
+Google Cloud Storage bucket. To force a rescan of Google Cloud Storage,
+fetch `http://vcs-test.golang.org/VCS/REPONAME?vcweb-force-reload=1`.
+
+## Static files
+
+The URL space `http://vcs-test.golang.org/go/NAME` is served by static files,
+fetched from `gs://vcs-test/go/NAME.zip`.
+The main use for static files is to write redirect HTML.
+See `gs://vcs-test/go/hello.zip` for examples.
+Note that because the server uses `http.DetectContentType` to deduce
+the content type from file data, it is not necessary to
+name HTML files with a `.html` suffix.
+
+## HTTPS
+
+The server fetches an HTTPS certificate on demand from Let's Encrypt,
+using `golang.org/x/crypto/acme/autocert`.
+It caches the certificates in `gs://vcs-test-autocert` using
+`golang.org/x/build/autocertcache`.
+
diff --git a/vcs-test/rebuild-server.sh b/vcs-test/rebuild-server.sh
new file mode 100755
index 0000000..7fe2f0c
--- /dev/null
+++ b/vcs-test/rebuild-server.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+# Copyright 2017 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.
+
+set -e
+
+gcloud compute instances delete vcs-test --zone=us-central1-a
+gcloud compute instances create vcs-test --zone=us-central1-a \
+	--address vcs-test \
+	--image-project debian-cloud --image-family debian-9 \
+	--machine-type n1-standard-1 \
+	--service-account=vcs-test@symbolic-datum-552.iam.gserviceaccount.com \
+	--tags=allow-ssh,http-server,https-server
+
+while sleep 5 && ! gcloud compute ssh vcs-test -- date; do
+	echo 'waiting for machine to respond to ssh...'
+done
+
+gcloud compute ssh vcs-test -- sudo -n bash -c \''
+	mkdir -p /home/vcweb/svn
+	chown -R uucp:uucp /home/vcweb
+	chown -R 777 /home/vcweb
+	apt-get update
+	apt-get install -y mercurial fossil bzr git apache2 ed subversion libapache2-mod-svn
+	perl -pie 's/80/8888/' /etc/apache2/ports.conf
+	echo "
+	    <Location /svn>
+	      DAV svn
+	      SVNParentPath /home/vcweb/svn
+	      <LimitExcept GET PROPFIND OPTIONS REPORT>
+	        Require all denied
+	      </LimitExcept>
+	    </Location>
+	" >/etc/apache2/mods-enabled/dav_svn.conf
+	apache2ctl restart
+	systemctl enable apache2.service
+'\'
diff --git a/vcs-test/rebuild-systemd.sh b/vcs-test/rebuild-systemd.sh
new file mode 100755
index 0000000..e858698
--- /dev/null
+++ b/vcs-test/rebuild-systemd.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Copyright 2017 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.
+
+set -e
+
+gcloud beta compute scp vcweb.service vcweb-*.socket vcs-test:
+
+gcloud compute ssh vcs-test -- sudo -n bash -c \''
+	systemctl stop vcweb.service
+	systemctl disable vcweb.service
+	rm -f /lib/systemd/system/vcweb* /etc/systemd/system/*/vcweb*
+	
+	mv vcweb.exe /usr/bin/vcweb
+	mv vcweb.service vcweb-*.socket /lib/systemd/system
+	systemctl enable vcweb.service
+'\'
diff --git a/vcs-test/redeploy-vcweb.sh b/vcs-test/redeploy-vcweb.sh
new file mode 100755
index 0000000..38f9675
--- /dev/null
+++ b/vcs-test/redeploy-vcweb.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Copyright 2017 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.
+
+set -e
+
+info="$USER $(date)"
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build "-ldflags=\"-X=main.buildInfo=$info\"" -o vcweb.exe ./vcweb
+trap "rm -f vcweb.exe" EXIT
+
+gcloud beta compute scp vcweb.exe vcs-test:
+
+gcloud compute ssh vcs-test -- sudo -n bash -c \''
+	mv vcweb.exe /usr/bin/vcweb
+	systemctl restart vcweb.service
+	systemctl status -l vcweb.service
+'\'
diff --git a/vcs-test/vcweb-http.socket b/vcs-test/vcweb-http.socket
new file mode 100644
index 0000000..28c6f4e
--- /dev/null
+++ b/vcs-test/vcweb-http.socket
@@ -0,0 +1,10 @@
+[Unit]
+Description=version control server HTTP socket
+PartOf=vcweb.service
+
+[Socket]
+ListenStream=80
+Service=vcweb.service
+
+[Install]
+WantedBy=sockets.target
diff --git a/vcs-test/vcweb-https.socket b/vcs-test/vcweb-https.socket
new file mode 100644
index 0000000..0c3738d
--- /dev/null
+++ b/vcs-test/vcweb-https.socket
@@ -0,0 +1,10 @@
+[Unit]
+Description=version control server HTTPS socket
+PartOf=vcweb.service
+
+[Socket]
+ListenStream=443
+Service=vcweb.service
+
+[Install]
+WantedBy=sockets.target
diff --git a/vcs-test/vcweb.service b/vcs-test/vcweb.service
new file mode 100644
index 0000000..9ef2651
--- /dev/null
+++ b/vcs-test/vcweb.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=vcs-test web server
+Requires=vcweb-http.socket vcweb-https.socket
+
+[Service]
+Type=notify
+ExecStart=/usr/bin/vcweb -d /home/vcweb
+WatchdogSec=30s
+Restart=always
+StandardOutput=syslog
+StandardError=syslog
+SyslogIdentifier=vcweb
+User=uucp
+
+[Install]
+WantedBy=multi-user.target
diff --git a/vcs-test/vcweb/bzr.go b/vcs-test/vcweb/bzr.go
new file mode 100644
index 0000000..4064fb3
--- /dev/null
+++ b/vcs-test/vcweb/bzr.go
@@ -0,0 +1,14 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+	"net/http"
+	"path/filepath"
+)
+
+func bzrHandler() http.Handler {
+	return http.StripPrefix("/bzr/", http.FileServer(http.Dir(filepath.Join(*dir, "bzr"))))
+}
diff --git a/vcs-test/vcweb/fossil.go b/vcs-test/vcweb/fossil.go
new file mode 100644
index 0000000..ea7d9c4
--- /dev/null
+++ b/vcs-test/vcweb/fossil.go
@@ -0,0 +1,45 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/http/cgi"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+func fossilHandler() http.Handler {
+	return http.HandlerFunc(fossilDispatch)
+}
+
+func fossilDispatch(w http.ResponseWriter, r *http.Request) {
+	if !strings.HasPrefix(r.URL.Path, "/fossil/") {
+		w.WriteHeader(404)
+		return
+	}
+	name := strings.TrimPrefix(r.URL.Path, "/fossil/")
+	if i := strings.Index(name, "/"); i >= 0 {
+		name = name[:i]
+	}
+
+	db := filepath.Join(*dir, "fossil/"+name+"/"+name+".fossil")
+	_, err := os.Stat(db)
+	if err != nil {
+		w.WriteHeader(404)
+	}
+	if _, err := os.Stat(db + ".cgi"); err != nil {
+		ioutil.WriteFile(db+".cgi", []byte("#!/usr/bin/fossil\nrepository: "+db+"\n"), 0777)
+	}
+
+	h := &cgi.Handler{
+		Path: db + ".cgi",
+		Root: "/fossil/" + name,
+		Dir:  filepath.Join(*dir, "fossil"),
+	}
+	h.ServeHTTP(w, r)
+}
diff --git a/vcs-test/vcweb/git.go b/vcs-test/vcweb/git.go
new file mode 100644
index 0000000..ef08c07
--- /dev/null
+++ b/vcs-test/vcweb/git.go
@@ -0,0 +1,31 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+	"log"
+	"net/http"
+	"net/http/cgi"
+	"os"
+	"os/exec"
+	"path/filepath"
+)
+
+func gitHandler() http.Handler {
+	os.Mkdir(filepath.Join(*dir, "git"), 0777)
+	path, err := exec.LookPath("git")
+	if err != nil {
+		log.Fatal(err)
+	}
+	return &cgi.Handler{
+		Path: path,
+		Args: []string{"http-backend"},
+		Dir:  filepath.Join(*dir, "git"),
+		Env: []string{
+			"GIT_PROJECT_ROOT=" + filepath.Join(*dir),
+			"GIT_HTTP_EXPORT_ALL=1",
+		},
+	}
+}
diff --git a/vcs-test/vcweb/hg.go b/vcs-test/vcweb/hg.go
new file mode 100644
index 0000000..4f2d5ce
--- /dev/null
+++ b/vcs-test/vcweb/hg.go
@@ -0,0 +1,42 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/cgi"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+var hgwebPy = `#!/usr/bin/python
+from mercurial import demandimport; demandimport.enable()
+from mercurial.hgweb import hgweb, wsgicgi
+wsgicgi.launch(hgweb("../hgweb.cfg"))
+`
+
+var hgwebCfg = `
+[paths]
+/hg/ = /DIR/hg/*
+`
+
+func hgHandler() http.Handler {
+	py := filepath.Join(*dir, "hgweb.py")
+	if err := ioutil.WriteFile(py, []byte(hgwebPy), 0777); err != nil {
+		log.Fatal(err)
+	}
+	if err := ioutil.WriteFile(filepath.Join(*dir, "hgweb.cfg"), []byte(strings.Replace(hgwebCfg, "DIR", *dir, -1)), 0777); err != nil {
+		log.Fatal(err)
+	}
+	os.Mkdir(filepath.Join(*dir, "hg"), 0777)
+
+	return &cgi.Handler{
+		Path: py,
+		Dir:  filepath.Join(*dir, "hg"),
+	}
+}
diff --git a/vcs-test/vcweb/load.go b/vcs-test/vcweb/load.go
new file mode 100644
index 0000000..630f5df
--- /dev/null
+++ b/vcs-test/vcweb/load.go
@@ -0,0 +1,137 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+	"archive/zip"
+	"bytes"
+	"context"
+	"crypto/md5"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"cloud.google.com/go/storage"
+)
+
+var (
+	ctx    = context.Background()
+	client *storage.Client
+	bucket *storage.BucketHandle
+)
+
+var cache struct {
+	sync.Mutex
+	entry map[string]*cacheEntry
+}
+
+type cacheEntry struct {
+	sync.Mutex
+	expire time.Time
+	md5    []byte
+}
+
+func init() {
+	var err error
+	client, err = storage.NewClient(ctx)
+	if err != nil {
+		log.Fatal(err)
+	}
+	bucket = client.Bucket("vcs-test")
+	cache.entry = map[string]*cacheEntry{}
+}
+
+func loadFS(dir1, dir2 string, force bool) {
+	name := dir1 + "/" + dir2
+	defer func() {
+		if err := recover(); err != nil {
+			log.Printf("%s: %v", name, err)
+		}
+	}()
+	check := func(err error) {
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	check(os.MkdirAll(filepath.Join(*dir, dir1), 0777))
+
+	cache.Lock()
+	entry := cache.entry[name]
+	if entry == nil {
+		entry = new(cacheEntry)
+		cache.entry[name] = entry
+	}
+	cache.Unlock()
+
+	entry.Lock()
+	defer entry.Unlock()
+
+	if time.Now().Before(entry.expire) && !force {
+		return
+	}
+
+	entry.expire = time.Now().Add(5 * time.Minute)
+
+	obj := bucket.Object(name + ".zip")
+	attrs, err := obj.Attrs(ctx)
+	check(err)
+	if bytes.Equal(attrs.MD5, entry.md5) {
+		return
+	}
+
+	r, err := obj.NewReader(ctx)
+	check(err)
+	defer r.Close()
+
+	zipFile := filepath.Join(*dir, name+".zip")
+	zf, err := os.Create(zipFile)
+	check(err)
+
+	h := md5.New()
+	_, err = io.Copy(io.MultiWriter(zf, h), r)
+	check(zf.Close())
+	check(err)
+	sum := h.Sum(nil)
+
+	if !bytes.Equal(attrs.MD5, sum) {
+		panic(fmt.Sprintf("load: unexpected md5 %x != %x", sum, attrs.MD5))
+	}
+
+	zf, err = os.Open(zipFile)
+	check(err)
+	info, err := zf.Stat()
+	check(err)
+	zr, err := zip.NewReader(zf, info.Size())
+	check(err)
+
+	tmp := filepath.Join(*dir, dir1+"/_"+dir2)
+	check(os.RemoveAll(tmp))
+	check(os.MkdirAll(tmp, 0777))
+
+	for _, f := range zr.File {
+		if strings.HasSuffix(f.Name, "/") {
+			check(os.MkdirAll(filepath.Join(tmp, f.Name), 0777))
+			continue
+		}
+		w, err := os.Create(filepath.Join(tmp, f.Name))
+		check(err)
+		r, err := f.Open()
+		check(err)
+		_, err = io.Copy(w, r)
+		check(err)
+		check(w.Close())
+	}
+
+	real := filepath.Join(*dir, dir1+"/"+dir2)
+	check(os.RemoveAll(real))
+	check(os.Rename(tmp, real))
+	entry.md5 = sum
+}
diff --git a/vcs-test/vcweb/main.go b/vcs-test/vcweb/main.go
new file mode 100644
index 0000000..334d571
--- /dev/null
+++ b/vcs-test/vcweb/main.go
@@ -0,0 +1,252 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+	"crypto/tls"
+	"flag"
+	"fmt"
+	"html"
+	"log"
+	"net"
+	"net/http"
+	"os"
+	"path/filepath"
+	"regexp"
+	"sort"
+	"strings"
+	"text/tabwriter"
+	"time"
+
+	"github.com/coreos/go-systemd/activation"
+	"github.com/coreos/go-systemd/daemon"
+	"golang.org/x/build/autocertcache"
+	"golang.org/x/crypto/acme"
+	"golang.org/x/crypto/acme/autocert"
+)
+
+var (
+	dir     = flag.String("d", "/tmp/vcweb", "directory holding vcweb data")
+	staging = flag.Bool("staging", false, "use staging letsencrypt server")
+)
+
+var buildInfo string
+
+func usage() {
+	fmt.Fprintf(os.Stderr, "usage: vcsweb [-d dir] [-staging]\n")
+	os.Exit(2)
+}
+
+var isLoadDir = map[string]bool{
+	"go":     true,
+	"git":    true,
+	"hg":     true,
+	"svn":    true,
+	"fossil": true,
+	"bzr":    true,
+}
+
+func main() {
+	flag.Usage = usage
+	flag.Parse()
+	if flag.NArg() != 0 {
+		usage()
+	}
+
+	if err := os.MkdirAll(*dir, 0777); err != nil {
+		log.Fatal(err)
+	}
+
+	http.Handle("/go/", http.StripPrefix("/go/", http.FileServer(http.Dir(filepath.Join(*dir, "go")))))
+	http.Handle("/git/", gitHandler())
+	http.Handle("/hg/", hgHandler())
+	http.Handle("/svn/", svnHandler())
+	http.Handle("/fossil/", fossilHandler())
+	http.Handle("/bzr/", bzrHandler())
+
+	handler := logger(http.HandlerFunc(loadAndHandle))
+
+	// If running under systemd, listen on 80 and 443 and serve TLS.
+	if listeners, _ := activation.Listeners(true); len(listeners) == 2 {
+		// Want listeners[0] is port 80, listeners[1] is port 443.
+		// There's no guaranteed order of the listeners!
+		// Sometimes we get 80, 443; other times we get 443, 80.
+		names := strings.Split(os.Getenv("LISTEN_FDNAMES"), ":")
+		if strings.Contains(names[0], "https") {
+			listeners[0], listeners[1] = listeners[1], listeners[0]
+		}
+
+		go func() {
+			log.Fatal(http.Serve(listeners[0], handler))
+		}()
+		dir := acme.LetsEncryptURL
+		if *staging {
+			dir = "https://acme-staging.api.letsencrypt.org/directory"
+		}
+		m := autocert.Manager{
+			Client:     &acme.Client{DirectoryURL: dir},
+			Cache:      autocertcache.NewGoogleCloudStorageCache(client, "vcs-test-autocert"),
+			Prompt:     autocert.AcceptTOS,
+			HostPolicy: autocert.HostWhitelist("vcs-test.golang.org"),
+		}
+		mRSA := autocert.Manager{
+			Client:     &acme.Client{DirectoryURL: dir},
+			Cache:      autocertcache.NewGoogleCloudStorageCache(client, "vcs-test-autocert-rsa"),
+			Prompt:     autocert.AcceptTOS,
+			HostPolicy: autocert.HostWhitelist("vcs-test.golang.org"),
+			ForceRSA:   true,
+		}
+		s := &http.Server{
+			Addr:    ":https",
+			Handler: handler,
+			TLSConfig: &tls.Config{
+				MinVersion:     tls.VersionSSL30,
+				GetCertificate: fallbackSNI(mRSA.GetCertificate, m.GetCertificate, "vcs-test.golang.org"),
+			},
+		}
+
+		dt, err := daemon.SdWatchdogEnabled(true)
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		daemon.SdNotify(false, "READY=1")
+		go func() {
+			for range time.NewTicker(dt / 2).C {
+				daemon.SdNotify(false, "WATCHDOG=1")
+			}
+		}()
+		log.Fatal(s.ServeTLS(listeners[1], "", ""))
+	}
+
+	// Local development on :8088.
+	l, err := net.Listen("tcp", "127.0.0.1:8088")
+	if err != nil {
+		log.Fatal(err)
+	}
+	log.Fatal(http.Serve(l, handler))
+}
+
+var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`)
+
+func loadAndHandle(w http.ResponseWriter, r *http.Request) {
+	if r.URL.Path == "/tls" {
+		handleTLS(w, r)
+		return
+	}
+	addTLSLog(w, r)
+	if r.URL.Path == "/" {
+		overview(w, r)
+		return
+	}
+	elem := strings.Split(r.URL.Path, "/")
+	if len(elem) >= 3 && elem[0] == "" && isLoadDir[elem[1]] && nameRE.MatchString(elem[2]) {
+		loadFS(elem[1], elem[2], r.URL.Query().Get("vcweb-force-reload") == "1" || r.URL.Query().Get("go-get") == "1")
+	}
+	http.DefaultServeMux.ServeHTTP(w, r)
+}
+
+func overview(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "<html>\n")
+	fmt.Fprintf(w, "<title>vcs-test.golang.org</title>\n<pre>\n")
+	fmt.Fprintf(w, "<b>vcs-test.golang.org</b>\n\n")
+	fmt.Fprintf(w, "This server serves various version control repos for testing the go command.\n\n")
+
+	fmt.Fprintf(w, "Date: %s\n", time.Now().Format(time.UnixDate))
+	fmt.Fprintf(w, "Build: %s\n\n", html.EscapeString(buildInfo))
+
+	fmt.Fprintf(w, "<b>cache</b>\n")
+
+	var all []string
+	cache.Lock()
+	for name, entry := range cache.entry {
+		all = append(all, fmt.Sprintf("%s\t%x\t%s\n", name, entry.md5, entry.expire.Format(time.UnixDate)))
+	}
+	cache.Unlock()
+	sort.Strings(all)
+	tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
+	for _, line := range all {
+		tw.Write([]byte(line))
+	}
+	tw.Flush()
+}
+
+func fallbackSNI(getCertRSA, getCert func(*tls.ClientHelloInfo) (*tls.Certificate, error), host string) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
+	return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+		saveHello(hello)
+		if hello.ServerName == "" {
+			h := *hello
+			hello = &h
+			hello.ServerName = host
+		}
+		var cert *tls.Certificate
+		var err error
+		if len(hello.SupportedVersions) > 0 && hello.SupportedVersions[0] >= tls.VersionTLS12 {
+			cert, err = getCert(hello)
+			if strings.HasSuffix(hello.ServerName, ".acme.invalid") && err != nil {
+				cert, err = getCertRSA(hello)
+			}
+		} else {
+			cert, err = getCertRSA(hello)
+		}
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "getCert: %v\n", err)
+		}
+		return cert, err
+	}
+}
+
+type loggingResponseWriter struct {
+	code int
+	size int64
+	http.ResponseWriter
+}
+
+func (l *loggingResponseWriter) WriteHeader(code int) {
+	l.code = code
+	l.ResponseWriter.WriteHeader(code)
+}
+
+func (l *loggingResponseWriter) Write(data []byte) (int, error) {
+	n, err := l.ResponseWriter.Write(data)
+	l.size += int64(n)
+	return n, err
+}
+
+func dashOr(s string) string {
+	if s == "" {
+		return "-"
+	}
+	return s
+}
+
+func logger(h http.Handler) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		l := &loggingResponseWriter{
+			code:           200,
+			ResponseWriter: w,
+		}
+		startTime := time.Now().Format("02/Jan/2006:15:04:05 -0700")
+		defer func() {
+			err := recover()
+			if err != nil {
+				l.code = 999
+			}
+			fmt.Fprintf(os.Stderr, "%s - - [%s] %q %03d %d %q %q %q\n",
+				dashOr(r.RemoteAddr),
+				startTime,
+				r.Method+" "+r.URL.String()+" "+r.Proto,
+				l.code,
+				l.size,
+				r.Header.Get("Referer"),
+				r.Header.Get("User-Agent"),
+				r.Host)
+			if err != nil {
+				panic(err)
+			}
+		}()
+		h.ServeHTTP(l, r)
+	}
+}
diff --git a/vcs-test/vcweb/svn.go b/vcs-test/vcweb/svn.go
new file mode 100644
index 0000000..b959a8e
--- /dev/null
+++ b/vcs-test/vcweb/svn.go
@@ -0,0 +1,22 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+	"log"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+)
+
+// Subversion is so complicated it can only run from inside Apache.
+// Assume an appropriately configured Apache is on 8888.
+func svnHandler() http.Handler {
+	u, err := url.Parse("http://127.0.0.1:8888/")
+	if err != nil {
+		log.Fatal(err)
+	}
+	return httputil.NewSingleHostReverseProxy(u)
+}
diff --git a/vcs-test/vcweb/tls.go b/vcs-test/vcweb/tls.go
new file mode 100644
index 0000000..42ab28b
--- /dev/null
+++ b/vcs-test/vcweb/tls.go
@@ -0,0 +1,648 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+	"crypto/tls"
+	"fmt"
+	"html"
+	"io"
+	"net"
+	"net/http"
+	"os"
+	"reflect"
+	"runtime/debug"
+	"strconv"
+	"sync"
+	"time"
+)
+
+func handleTLS(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "<html>\n")
+	fmt.Fprintf(w, "<title>vcs-test.golang.org/tls</title>\n<pre>\n")
+	fmt.Fprintf(w, "<b>vcs-test.golang.org/tls</b>\n\n")
+	fmt.Fprintf(w, "This page shows debug information about TLS client connections.\n\n")
+	fmt.Fprintf(w, "\n<b><u>Your Request</u></b>\n\n")
+	showTLSRequest(w, time.Now(), r, r.TLS, findHello(w, true))
+	showTLSLog(w)
+	showHelloLog(w)
+}
+
+const maxTLSLog = 100
+
+var tlsLog struct {
+	mu  sync.Mutex
+	w   int64
+	buf [maxTLSLog]tlsLogEntry
+}
+
+type tlsLogEntry struct {
+	time  time.Time
+	r     *http.Request
+	cs    *tls.ConnectionState
+	hello *tls.ClientHelloInfo
+}
+
+const maxHelloLog = 100
+
+var recentHello struct {
+	mu  sync.Mutex
+	w   int64
+	buf [maxHelloLog]helloLogEntry
+}
+
+type helloLogEntry struct {
+	time  time.Time
+	hello *tls.ClientHelloInfo
+	conn  net.Conn
+	used  bool
+}
+
+func saveHello(hello *tls.ClientHelloInfo) {
+	recentHello.mu.Lock()
+	recentHello.buf[recentHello.w%maxHelloLog] = helloLogEntry{time.Now(), hello, hello.Conn, false}
+	recentHello.w++
+	recentHello.mu.Unlock()
+}
+
+func findHello(w http.ResponseWriter, dbg bool) *tls.ClientHelloInfo {
+	defer func() {
+		if err := recover(); err != nil && dbg {
+			fmt.Fprintf(w, "PANIC: %s\n%s\n", err, debug.Stack())
+		}
+	}()
+	rw := reflect.ValueOf(w) // *loggingResponseWriter
+	rw = rw.Elem().FieldByName("ResponseWriter").Elem()
+	switch rw.Type().String() {
+	case "*http.http2responseWriter":
+		rw = rw.Elem().FieldByName("rws").Elem().FieldByName("conn").Elem().FieldByName("conn")
+	case "*http.response":
+		rw = rw.Elem().FieldByName("conn").Elem().FieldByName("rwc")
+	}
+	// now a net.Conn implemented by *tls.Conn
+	rw = rw.Elem().Elem().FieldByName("conn").Elem()
+	// now a net.Conn implementation of some kind
+	if rw.Kind() != reflect.Ptr || rw.IsNil() {
+		if dbg {
+			fmt.Fprintf(w, "FINDHELLO STOP: %s\n", rw.Type().String())
+		}
+		return nil
+	}
+	ptr := rw.Pointer()
+	recentHello.mu.Lock()
+	defer recentHello.mu.Unlock()
+	for i, e := range recentHello.buf {
+		if e.conn == nil || e.hello == nil {
+			continue
+		}
+		rc := reflect.ValueOf(e.conn)
+		if rc.Kind() != reflect.Ptr || rc.IsNil() || rc.Type() != rw.Type() {
+			if dbg {
+				fmt.Fprintf(w, "- findhello wrong %s vs %s\n", rc.Type().String(), rw.Type())
+			}
+			continue
+		}
+		if rc.Pointer() == ptr {
+			recentHello.buf[i].used = true
+			return e.hello
+		}
+	}
+	if dbg {
+		fmt.Fprintf(w, "FINDHELLO: %s\n", rw.Type().String())
+	}
+	fmt.Fprintf(os.Stderr, "FINDHELLO: %s\n", rw.Type().String())
+	return nil
+}
+
+func addTLSLog(w http.ResponseWriter, r *http.Request) {
+	if r.TLS == nil {
+		return
+	}
+	hello := findHello(w, false)
+	tlsLog.mu.Lock()
+	tlsLog.buf[tlsLog.w%maxTLSLog] = tlsLogEntry{time.Now(), r, r.TLS, hello}
+	tlsLog.w++
+	tlsLog.mu.Unlock()
+}
+
+func showTLSLog(w http.ResponseWriter) {
+	all := make([]tlsLogEntry, 0, maxTLSLog)
+	tlsLog.mu.Lock()
+	i := tlsLog.w - maxTLSLog
+	if i < 0 {
+		i = 0
+	}
+	for ; i < tlsLog.w; i++ {
+		all = append(all, tlsLog.buf[i%maxTLSLog])
+	}
+	tlsLog.mu.Unlock()
+
+	fmt.Fprintf(w, "<b><u>Recent Requests</u></b>\n\n")
+	for _, e := range all {
+		showTLSRequest(w, e.time, e.r, e.cs, e.hello)
+	}
+}
+
+func showHelloLog(w http.ResponseWriter) {
+	all := make([]helloLogEntry, 0, maxHelloLog)
+	recentHello.mu.Lock()
+	i := recentHello.w - maxHelloLog
+	if i < 0 {
+		i = 0
+	}
+	for ; i < recentHello.w; i++ {
+		all = append(all, recentHello.buf[i%maxTLSLog])
+	}
+	recentHello.mu.Unlock()
+
+	fmt.Fprintf(w, "<b><u>Recent unmatched ClientHelloInfo</u></b>\n\n")
+	for _, e := range all {
+		if e.conn == nil || e.hello == nil || e.used {
+			continue
+		}
+		fmt.Fprintf(w, "<b>%s # %s</b>\n", e.conn.RemoteAddr(), e.time.Format(time.RFC3339))
+		printTLSHello(w, e.hello)
+		fmt.Fprintf(w, "\n")
+	}
+}
+
+func showTLSRequest(w http.ResponseWriter, t time.Time, r *http.Request, cs *tls.ConnectionState, hello *tls.ClientHelloInfo) {
+	var e = html.EscapeString
+	var q = strconv.Quote
+
+	defer fmt.Fprintf(w, "\n")
+	fmt.Fprintf(w, "<b>%s %s %s %s # %s</b>\n", e(r.RemoteAddr), e(r.Method), e(r.RequestURI), e(r.Proto), t.Format(time.RFC3339))
+	fmt.Fprintf(w, "\tUser-Agent: %s\n", e(r.Header.Get("User-Agent")))
+	if cs == nil {
+		fmt.Fprintf(w, "\tNo TLS\n")
+		return
+	}
+	fmt.Fprintf(w, "\tTLS ConnectionState:\n")
+	fmt.Fprintf(w, "\t\tVersion: %s\n", nameOrValue(int(cs.Version), versionNames, "%#x"))
+	fmt.Fprintf(w, "\t\tCipherSuite: %s\n", nameOrValue(int(cs.CipherSuite), suiteNames, "%#x"))
+	fmt.Fprintf(w, "\t\tNegotiatedProtocol: %s\n", e(q(cs.NegotiatedProtocol)))
+	fmt.Fprintf(w, "\t\tServerName: %s\n", e(q(cs.ServerName)))
+	fmt.Fprintf(w, "\tTLS ClientHello:\n")
+	if hello == nil {
+		fmt.Fprintf(w, "\t\tunavailable\n")
+		return
+	}
+	findHello(w, false) // so it doesn't show up as unmatched
+	printTLSHello(w, hello)
+}
+
+func printTLSHello(w io.Writer, hello *tls.ClientHelloInfo) {
+	var e = html.EscapeString
+	var q = strconv.Quote
+
+	fmt.Fprintf(w, "\t\tServerName: %s\n", e(q(hello.ServerName)))
+	fmt.Fprintf(w, "\t\tCipherSuites:")
+	for _, suite := range hello.CipherSuites {
+		fmt.Fprintf(w, "\n\t\t\t%s", nameOrValue(int(suite), suiteNames, "%#x"))
+	}
+	fmt.Fprintf(w, "\n")
+	fmt.Fprintf(w, "\t\tSupportedCurves:")
+	for _, curve := range hello.SupportedCurves {
+		fmt.Fprintf(w, " %s", nameOrValue(int(curve), curveNames, "%d"))
+	}
+	fmt.Fprintf(w, "\n")
+	fmt.Fprintf(w, "\t\tSupportedPoints:")
+	for _, pt := range hello.SupportedPoints {
+		fmt.Fprintf(w, " %s", nameOrValue(int(pt), pointNames, "%d"))
+	}
+	fmt.Fprintf(w, "\n")
+	fmt.Fprintf(w, "\t\tSignatureSchemes:")
+	for _, sig := range hello.SignatureSchemes {
+		fmt.Fprintf(w, " %s", nameOrValue(int(sig), sigNames, "%#x"))
+	}
+	fmt.Fprintf(w, "\n")
+	fmt.Fprintf(w, "\t\tSupportedVersions:")
+	for _, v := range hello.SupportedVersions {
+		fmt.Fprintf(w, " %s", nameOrValue(int(v), versionNames, "%#x"))
+	}
+	fmt.Fprintf(w, "\n")
+	fmt.Fprintf(w, "\t\tSupportedProtos:")
+	for _, s := range hello.SupportedProtos {
+		fmt.Fprintf(w, " %s", e(q(s)))
+	}
+	fmt.Fprintf(w, "\n")
+}
+
+func nameOrValue(x int, names map[int]string, format string) string {
+	name := names[x]
+	if name != "" {
+		return name
+	}
+	return fmt.Sprintf(format, x)
+}
+
+var suiteNames = map[int]string{
+	// https://www.iana.org/assignments/tls-parameters/tls-parameters-4.csv
+	// csv2tsv tls-parameters-4.csv  | awk '{print "\t"$1": \"" $2 "\","}'|  sed 's/,0x//'  | egrep -v 'Unassigned|Reserved'
+	0x0000: "TLS_NULL_WITH_NULL_NULL",
+	0x0001: "TLS_RSA_WITH_NULL_MD5",
+	0x0002: "TLS_RSA_WITH_NULL_SHA",
+	0x0003: "TLS_RSA_EXPORT_WITH_RC4_40_MD5",
+	0x0004: "TLS_RSA_WITH_RC4_128_MD5",
+	0x0005: "TLS_RSA_WITH_RC4_128_SHA",
+	0x0006: "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5",
+	0x0007: "TLS_RSA_WITH_IDEA_CBC_SHA",
+	0x0008: "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA",
+	0x0009: "TLS_RSA_WITH_DES_CBC_SHA",
+	0x000A: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
+	0x000B: "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA",
+	0x000C: "TLS_DH_DSS_WITH_DES_CBC_SHA",
+	0x000D: "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA",
+	0x000E: "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA",
+	0x000F: "TLS_DH_RSA_WITH_DES_CBC_SHA",
+	0x0010: "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA",
+	0x0011: "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
+	0x0012: "TLS_DHE_DSS_WITH_DES_CBC_SHA",
+	0x0013: "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
+	0x0014: "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
+	0x0015: "TLS_DHE_RSA_WITH_DES_CBC_SHA",
+	0x0016: "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
+	0x0017: "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5",
+	0x0018: "TLS_DH_anon_WITH_RC4_128_MD5",
+	0x0019: "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA",
+	0x001A: "TLS_DH_anon_WITH_DES_CBC_SHA",
+	0x001B: "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA",
+	0x001E: "TLS_KRB5_WITH_DES_CBC_SHA",
+	0x001F: "TLS_KRB5_WITH_3DES_EDE_CBC_SHA",
+	0x0020: "TLS_KRB5_WITH_RC4_128_SHA",
+	0x0021: "TLS_KRB5_WITH_IDEA_CBC_SHA",
+	0x0022: "TLS_KRB5_WITH_DES_CBC_MD5",
+	0x0023: "TLS_KRB5_WITH_3DES_EDE_CBC_MD5",
+	0x0024: "TLS_KRB5_WITH_RC4_128_MD5",
+	0x0025: "TLS_KRB5_WITH_IDEA_CBC_MD5",
+	0x0026: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA",
+	0x0027: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA",
+	0x0028: "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
+	0x0029: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5",
+	0x002A: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5",
+	0x002B: "TLS_KRB5_EXPORT_WITH_RC4_40_MD5",
+	0x002C: "TLS_PSK_WITH_NULL_SHA",
+	0x002D: "TLS_DHE_PSK_WITH_NULL_SHA",
+	0x002E: "TLS_RSA_PSK_WITH_NULL_SHA",
+	0x002F: "TLS_RSA_WITH_AES_128_CBC_SHA",
+	0x0030: "TLS_DH_DSS_WITH_AES_128_CBC_SHA",
+	0x0031: "TLS_DH_RSA_WITH_AES_128_CBC_SHA",
+	0x0032: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
+	0x0033: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
+	0x0034: "TLS_DH_anon_WITH_AES_128_CBC_SHA",
+	0x0035: "TLS_RSA_WITH_AES_256_CBC_SHA",
+	0x0036: "TLS_DH_DSS_WITH_AES_256_CBC_SHA",
+	0x0037: "TLS_DH_RSA_WITH_AES_256_CBC_SHA",
+	0x0038: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
+	0x0039: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
+	0x003A: "TLS_DH_anon_WITH_AES_256_CBC_SHA",
+	0x003B: "TLS_RSA_WITH_NULL_SHA256",
+	0x003C: "TLS_RSA_WITH_AES_128_CBC_SHA256",
+	0x003D: "TLS_RSA_WITH_AES_256_CBC_SHA256",
+	0x003E: "TLS_DH_DSS_WITH_AES_128_CBC_SHA256",
+	0x003F: "TLS_DH_RSA_WITH_AES_128_CBC_SHA256",
+	0x0040: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256",
+	0x0041: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA",
+	0x0042: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA",
+	0x0043: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA",
+	0x0044: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA",
+	0x0045: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA",
+	0x0046: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA",
+	0x0067: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
+	0x0068: "TLS_DH_DSS_WITH_AES_256_CBC_SHA256",
+	0x0069: "TLS_DH_RSA_WITH_AES_256_CBC_SHA256",
+	0x006A: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256",
+	0x006B: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
+	0x006C: "TLS_DH_anon_WITH_AES_128_CBC_SHA256",
+	0x006D: "TLS_DH_anon_WITH_AES_256_CBC_SHA256",
+	0x0084: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA",
+	0x0085: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA",
+	0x0086: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA",
+	0x0087: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA",
+	0x0088: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA",
+	0x0089: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA",
+	0x008A: "TLS_PSK_WITH_RC4_128_SHA",
+	0x008B: "TLS_PSK_WITH_3DES_EDE_CBC_SHA",
+	0x008C: "TLS_PSK_WITH_AES_128_CBC_SHA",
+	0x008D: "TLS_PSK_WITH_AES_256_CBC_SHA",
+	0x008E: "TLS_DHE_PSK_WITH_RC4_128_SHA",
+	0x008F: "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA",
+	0x0090: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA",
+	0x0091: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA",
+	0x0092: "TLS_RSA_PSK_WITH_RC4_128_SHA",
+	0x0093: "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA",
+	0x0094: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA",
+	0x0095: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA",
+	0x0096: "TLS_RSA_WITH_SEED_CBC_SHA",
+	0x0097: "TLS_DH_DSS_WITH_SEED_CBC_SHA",
+	0x0098: "TLS_DH_RSA_WITH_SEED_CBC_SHA",
+	0x0099: "TLS_DHE_DSS_WITH_SEED_CBC_SHA",
+	0x009A: "TLS_DHE_RSA_WITH_SEED_CBC_SHA",
+	0x009B: "TLS_DH_anon_WITH_SEED_CBC_SHA",
+	0x009C: "TLS_RSA_WITH_AES_128_GCM_SHA256",
+	0x009D: "TLS_RSA_WITH_AES_256_GCM_SHA384",
+	0x009E: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
+	0x009F: "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
+	0x00A0: "TLS_DH_RSA_WITH_AES_128_GCM_SHA256",
+	0x00A1: "TLS_DH_RSA_WITH_AES_256_GCM_SHA384",
+	0x00A2: "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",
+	0x00A3: "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384",
+	0x00A4: "TLS_DH_DSS_WITH_AES_128_GCM_SHA256",
+	0x00A5: "TLS_DH_DSS_WITH_AES_256_GCM_SHA384",
+	0x00A6: "TLS_DH_anon_WITH_AES_128_GCM_SHA256",
+	0x00A7: "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
+	0x00A8: "TLS_PSK_WITH_AES_128_GCM_SHA256",
+	0x00A9: "TLS_PSK_WITH_AES_256_GCM_SHA384",
+	0x00AA: "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256",
+	0x00AB: "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384",
+	0x00AC: "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256",
+	0x00AD: "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384",
+	0x00AE: "TLS_PSK_WITH_AES_128_CBC_SHA256",
+	0x00AF: "TLS_PSK_WITH_AES_256_CBC_SHA384",
+	0x00B0: "TLS_PSK_WITH_NULL_SHA256",
+	0x00B1: "TLS_PSK_WITH_NULL_SHA384",
+	0x00B2: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256",
+	0x00B3: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384",
+	0x00B4: "TLS_DHE_PSK_WITH_NULL_SHA256",
+	0x00B5: "TLS_DHE_PSK_WITH_NULL_SHA384",
+	0x00B6: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256",
+	0x00B7: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384",
+	0x00B8: "TLS_RSA_PSK_WITH_NULL_SHA256",
+	0x00B9: "TLS_RSA_PSK_WITH_NULL_SHA384",
+	0x00BA: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+	0x00BB: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256",
+	0x00BC: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+	0x00BD: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256",
+	0x00BE: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+	0x00BF: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256",
+	0x00C0: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+	0x00C1: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256",
+	0x00C2: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+	0x00C3: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256",
+	0x00C4: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+	0x00C5: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256",
+	0x00FF: "TLS_EMPTY_RENEGOTIATION_INFO_SCSV",
+	0x5600: "TLS_FALLBACK_SCSV",
+	0xC001: "TLS_ECDH_ECDSA_WITH_NULL_SHA",
+	0xC002: "TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
+	0xC003: "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
+	0xC004: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
+	0xC005: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA",
+	0xC006: "TLS_ECDHE_ECDSA_WITH_NULL_SHA",
+	0xC007: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
+	0xC008: "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
+	0xC009: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
+	0xC00A: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
+	0xC00B: "TLS_ECDH_RSA_WITH_NULL_SHA",
+	0xC00C: "TLS_ECDH_RSA_WITH_RC4_128_SHA",
+	0xC00D: "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA",
+	0xC00E: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA",
+	0xC00F: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA",
+	0xC010: "TLS_ECDHE_RSA_WITH_NULL_SHA",
+	0xC011: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
+	0xC012: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
+	0xC013: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
+	0xC014: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
+	0xC015: "TLS_ECDH_anon_WITH_NULL_SHA",
+	0xC016: "TLS_ECDH_anon_WITH_RC4_128_SHA",
+	0xC017: "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA",
+	0xC018: "TLS_ECDH_anon_WITH_AES_128_CBC_SHA",
+	0xC019: "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
+	0xC01A: "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA",
+	0xC01B: "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA",
+	0xC01C: "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA",
+	0xC01D: "TLS_SRP_SHA_WITH_AES_128_CBC_SHA",
+	0xC01E: "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA",
+	0xC01F: "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA",
+	0xC020: "TLS_SRP_SHA_WITH_AES_256_CBC_SHA",
+	0xC021: "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA",
+	0xC022: "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA",
+	0xC023: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+	0xC024: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
+	0xC025: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
+	0xC026: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384",
+	0xC027: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+	0xC028: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
+	0xC029: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256",
+	0xC02A: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384",
+	0xC02B: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+	0xC02C: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+	0xC02D: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+	0xC02E: "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384",
+	0xC02F: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+	0xC030: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+	0xC031: "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256",
+	0xC032: "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384",
+	0xC033: "TLS_ECDHE_PSK_WITH_RC4_128_SHA",
+	0xC034: "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA",
+	0xC035: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA",
+	0xC036: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA",
+	0xC037: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256",
+	0xC038: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384",
+	0xC039: "TLS_ECDHE_PSK_WITH_NULL_SHA",
+	0xC03A: "TLS_ECDHE_PSK_WITH_NULL_SHA256",
+	0xC03B: "TLS_ECDHE_PSK_WITH_NULL_SHA384",
+	0xC03C: "TLS_RSA_WITH_ARIA_128_CBC_SHA256",
+	0xC03D: "TLS_RSA_WITH_ARIA_256_CBC_SHA384",
+	0xC03E: "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256",
+	0xC03F: "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384",
+	0xC040: "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256",
+	0xC041: "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384",
+	0xC042: "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256",
+	0xC043: "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384",
+	0xC044: "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256",
+	0xC045: "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384",
+	0xC046: "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256",
+	0xC047: "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384",
+	0xC048: "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256",
+	0xC049: "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384",
+	0xC04A: "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256",
+	0xC04B: "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384",
+	0xC04C: "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256",
+	0xC04D: "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384",
+	0xC04E: "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256",
+	0xC04F: "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384",
+	0xC050: "TLS_RSA_WITH_ARIA_128_GCM_SHA256",
+	0xC051: "TLS_RSA_WITH_ARIA_256_GCM_SHA384",
+	0xC052: "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256",
+	0xC053: "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384",
+	0xC054: "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256",
+	0xC055: "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384",
+	0xC056: "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256",
+	0xC057: "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384",
+	0xC058: "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256",
+	0xC059: "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384",
+	0xC05A: "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256",
+	0xC05B: "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384",
+	0xC05C: "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256",
+	0xC05D: "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384",
+	0xC05E: "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256",
+	0xC05F: "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384",
+	0xC060: "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256",
+	0xC061: "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384",
+	0xC062: "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256",
+	0xC063: "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384",
+	0xC064: "TLS_PSK_WITH_ARIA_128_CBC_SHA256",
+	0xC065: "TLS_PSK_WITH_ARIA_256_CBC_SHA384",
+	0xC066: "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256",
+	0xC067: "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384",
+	0xC068: "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256",
+	0xC069: "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384",
+	0xC06A: "TLS_PSK_WITH_ARIA_128_GCM_SHA256",
+	0xC06B: "TLS_PSK_WITH_ARIA_256_GCM_SHA384",
+	0xC06C: "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256",
+	0xC06D: "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384",
+	0xC06E: "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256",
+	0xC06F: "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384",
+	0xC070: "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256",
+	0xC071: "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384",
+	0xC072: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256",
+	0xC073: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384",
+	0xC074: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256",
+	0xC075: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384",
+	0xC076: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+	0xC077: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384",
+	0xC078: "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+	0xC079: "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384",
+	0xC07A: "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC07B: "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC07C: "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC07D: "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC07E: "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC07F: "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC080: "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC081: "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC082: "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC083: "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC084: "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC085: "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC086: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC087: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC088: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC089: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC08A: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC08B: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC08C: "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC08D: "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC08E: "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC08F: "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC090: "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC091: "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC092: "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+	0xC093: "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+	0xC094: "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+	0xC095: "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+	0xC096: "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+	0xC097: "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+	0xC098: "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+	0xC099: "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+	0xC09A: "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+	0xC09B: "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+	0xC09C: "TLS_RSA_WITH_AES_128_CCM",
+	0xC09D: "TLS_RSA_WITH_AES_256_CCM",
+	0xC09E: "TLS_DHE_RSA_WITH_AES_128_CCM",
+	0xC09F: "TLS_DHE_RSA_WITH_AES_256_CCM",
+	0xC0A0: "TLS_RSA_WITH_AES_128_CCM_8",
+	0xC0A1: "TLS_RSA_WITH_AES_256_CCM_8",
+	0xC0A2: "TLS_DHE_RSA_WITH_AES_128_CCM_8",
+	0xC0A3: "TLS_DHE_RSA_WITH_AES_256_CCM_8",
+	0xC0A4: "TLS_PSK_WITH_AES_128_CCM",
+	0xC0A5: "TLS_PSK_WITH_AES_256_CCM",
+	0xC0A6: "TLS_DHE_PSK_WITH_AES_128_CCM",
+	0xC0A7: "TLS_DHE_PSK_WITH_AES_256_CCM",
+	0xC0A8: "TLS_PSK_WITH_AES_128_CCM_8",
+	0xC0A9: "TLS_PSK_WITH_AES_256_CCM_8",
+	0xC0AA: "TLS_PSK_DHE_WITH_AES_128_CCM_8",
+	0xC0AB: "TLS_PSK_DHE_WITH_AES_256_CCM_8",
+	0xC0AC: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM",
+	0xC0AD: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM",
+	0xC0AE: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8",
+	0xC0AF: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8",
+	0xCCA8: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+	0xCCA9: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
+	0xCCAA: "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+	0xCCAB: "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256",
+	0xCCAC: "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
+	0xCCAD: "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
+	0xCCAE: "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256",
+	0xD001: "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256",
+	0xD002: "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384",
+	0xD003: "TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256",
+	0xD005: "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256",
+}
+
+var versionNames = map[int]string{
+	tls.VersionSSL30: "SSL3.0",
+	tls.VersionTLS10: "TLS1.0",
+	tls.VersionTLS11: "TLS1.1",
+	tls.VersionTLS12: "TLS1.2",
+}
+
+var pointNames = map[int]string{
+	// csv2tsv tls-parameters-9.csv | awk '{print "\t" $1 ": \"" $2 "\","}'| egrep -v 'Unassigned|Reserved'
+	0: "uncompressed",
+	1: "ansiX962_compressed_prime",
+	2: "ansiX962_compressed_char2",
+}
+
+var curveNames = map[int]string{
+	// csv2tsv tls-parameters-8.csv | awk '{print "\t" $1 ": \"" $2 "\","}'| egrep -v 'Unassigned|Reserved'
+	1:     "sect163k1",
+	2:     "sect163r1",
+	3:     "sect163r2",
+	4:     "sect193r1",
+	5:     "sect193r2",
+	6:     "sect233k1",
+	7:     "sect233r1",
+	8:     "sect239k1",
+	9:     "sect283k1",
+	10:    "sect283r1",
+	11:    "sect409k1",
+	12:    "sect409r1",
+	13:    "sect571k1",
+	14:    "sect571r1",
+	15:    "secp160k1",
+	16:    "secp160r1",
+	17:    "secp160r2",
+	18:    "secp192k1",
+	19:    "secp192r1",
+	20:    "secp224k1",
+	21:    "secp224r1",
+	22:    "secp256k1",
+	23:    "secp256r1",
+	24:    "secp384r1",
+	25:    "secp521r1",
+	26:    "brainpoolP256r1",
+	27:    "brainpoolP384r1",
+	28:    "brainpoolP512r1",
+	29:    "x25519",
+	30:    "x448",
+	256:   "ffdhe2048",
+	257:   "ffdhe3072",
+	258:   "ffdhe4096",
+	259:   "ffdhe6144",
+	260:   "ffdhe8192",
+	65281: "arbitrary_explicit_prime_curves",
+	65282: "arbitrary_explicit_char2_curves",
+}
+
+var sigNames = map[int]string{
+	// https://tools.ietf.org/html/draft-ietf-tls-tls13-18#section-4.2.3
+	0x0201: "rsa_pkcs1_sha1",
+	0x0401: "rsa_pkcs1_sha256",
+	0x0501: "rsa_pkcs1_sha384",
+	0x0601: "rsa_pkcs1_sha512",
+
+	0x0403: "ecdsa_secp256r1_sha256",
+	0x0503: "ecdsa_secp384r1_sha384",
+	0x0603: "ecdsa_secp521r1_sha512",
+
+	0x0804: "rsa_pss_sha256",
+	0x0805: "rsa_pss_sha384",
+	0x0806: "rsa_pss_sha512",
+
+	0x0807: "ed25519",
+	0x0808: "ed448",
+}