vcs-test/vcweb: add a handler that requires HTTPS Basic Auth

This endpoint will be used for integration testing golang/go#26232.

Updates golang/go#26232

Change-Id: I70cab336b885b82abfe8b77839bc9163600f15b0
Reviewed-on: https://go-review.googlesource.com/c/build/+/170581
Run-TryBot: Bryan C. Mills <bcmills@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/vcs-test/vcweb/auth.go b/vcs-test/vcweb/auth.go
new file mode 100644
index 0000000..d6ba58d
--- /dev/null
+++ b/vcs-test/vcweb/auth.go
@@ -0,0 +1,117 @@
+// 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 (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	pathpkg "path"
+	"strings"
+)
+
+type accessToken struct {
+	Username, Password string
+	StatusCode         int // defaults to 401.
+	Message            string
+}
+
+// newAuthHandler serves authenticated data from within dir.
+//
+// For each request, the handler looks for a file in a parent directory (still
+// within dir) named ".access" and parses it as a JSON-serialized accessToken.
+// If the credentials from the request match the accessToken, the file is served
+// normally; otherwise, it is rejected with the StatusCode and Message provided
+// by the token.
+func newAuthHandler(dir http.FileSystem) http.Handler {
+	return &authHandler{dir}
+}
+
+type authHandler struct {
+	dir http.FileSystem
+}
+
+func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if !strings.HasPrefix(r.URL.Path, "/auth/") {
+		http.Error(w, "path does not start with /auth/", http.StatusInternalServerError)
+		return
+	}
+
+	path := strings.TrimPrefix(r.URL.Path, "/auth/")
+	if path == "" {
+		http.NotFound(w, r)
+		return
+	}
+	if strings.HasPrefix(pathpkg.Base(path), ".") {
+		http.Error(w, "filename contains leading dot", http.StatusBadRequest)
+		return
+	}
+
+	f, err := h.dir.Open(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			http.NotFound(w, r)
+		} else {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+		return
+	}
+
+	accessDir := path
+	if fi, err := f.Stat(); err == nil && !fi.IsDir() {
+		accessDir = pathpkg.Dir(path)
+	}
+	f.Close()
+
+	var accessFile http.File
+	for {
+		var err error
+		accessFile, err = h.dir.Open(pathpkg.Join(accessDir, ".access"))
+		if err == nil {
+			break
+		}
+
+		if !os.IsNotExist(err) {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		if accessDir == "." {
+			http.Error(w, "failed to locate access file", http.StatusInternalServerError)
+			return
+		}
+		accessDir = pathpkg.Dir(accessDir)
+	}
+
+	data, err := ioutil.ReadAll(accessFile)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	var token accessToken
+	if err := json.Unmarshal(data, &token); err != nil {
+		log.Print(err)
+		http.Error(w, "malformed access file", http.StatusInternalServerError)
+		return
+	}
+	if username, password, ok := r.BasicAuth(); !ok || username != token.Username || password != token.Password {
+		code := token.StatusCode
+		if code == 0 {
+			code = http.StatusUnauthorized
+		}
+		if code == http.StatusUnauthorized {
+			w.Header().Add("WWW-Authenticate", fmt.Sprintf("basic realm=%s", accessDir))
+		}
+		http.Error(w, token.Message, code)
+		return
+	}
+
+	http.FileServer(h.dir).ServeHTTP(w, r)
+}
diff --git a/vcs-test/vcweb/insecure.go b/vcs-test/vcweb/insecure.go
index 9c64097..eb74348 100644
--- a/vcs-test/vcweb/insecure.go
+++ b/vcs-test/vcweb/insecure.go
@@ -18,6 +18,7 @@
 func insecureRedirectDispatch(w http.ResponseWriter, r *http.Request) {
 	if !strings.HasPrefix(r.URL.Path, "/insecure/") {
 		http.Error(w, "path does not start with /insecure/", http.StatusInternalServerError)
+		return
 	}
 
 	url := *r.URL
diff --git a/vcs-test/vcweb/main.go b/vcs-test/vcweb/main.go
index ad15a48..4ceafed 100644
--- a/vcs-test/vcweb/main.go
+++ b/vcs-test/vcweb/main.go
@@ -68,6 +68,7 @@
 	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))