| // 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. |
| |
| // +build linux |
| |
| 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{ |
| "auth": true, |
| "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()) |
| http.Handle("/insecure/", insecureRedirectHandler()) |
| http.Handle("/auth/", newAuthHandler(http.Dir(filepath.Join(*dir, "auth")))) |
| |
| handler := logger(http.HandlerFunc(loadAndHandle)) |
| |
| // If running under systemd, listen on 80 and 443 and serve TLS. |
| if listeners, _ := activation.ListenersWithNames(); len(listeners) == 2 { |
| httpListener := listeners["vcweb-http.socket"][0] |
| httpsListener := listeners["vcweb-https.socket"][0] |
| |
| go func() { |
| log.Fatal(http.Serve(httpListener, 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"), |
| } |
| s := &http.Server{ |
| Addr: ":https", |
| Handler: handler, |
| TLSConfig: &tls.Config{ |
| MinVersion: tls.VersionSSL30, |
| GetCertificate: fallbackSNI(m.GetCertificate, "vcs-test.golang.org"), |
| NextProtos: []string{ |
| "h2", "http/1.1", // enable HTTP/2 |
| acme.ALPNProto, // enable tls-alpn ACME challenges |
| }, |
| }, |
| } |
| |
| 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(httpsListener, "", "")) |
| } |
| |
| // 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(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 |
| } |
| return getCert(hello) |
| } |
| } |
| |
| 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) |
| } |
| } |