blob: 383bf759ffcdc1ad44d22b15d695bdff519916d0 [file] [log] [blame]
// 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 vcweb
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"strings"
)
// authHandler serves requests only if the Basic Auth data sent with the request
// matches the contents of a ".access" file in the requested directory.
//
// For each request, the handler looks for a file 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.
type authHandler struct{}
type accessToken struct {
Username, Password string
StatusCode int // defaults to 401.
Message string
}
func (h *authHandler) Available() bool { return true }
func (h *authHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) {
fs := http.Dir(dir)
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
urlPath := req.URL.Path
if urlPath != "" && strings.HasPrefix(path.Base(urlPath), ".") {
http.Error(w, "filename contains leading dot", http.StatusBadRequest)
return
}
f, err := fs.Open(urlPath)
if err != nil {
if os.IsNotExist(err) {
http.NotFound(w, req)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
accessDir := urlPath
if fi, err := f.Stat(); err == nil && !fi.IsDir() {
accessDir = path.Dir(urlPath)
}
f.Close()
var accessFile http.File
for {
var err error
accessFile, err = fs.Open(path.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 = path.Dir(accessDir)
}
data, err := io.ReadAll(accessFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var token accessToken
if err := json.Unmarshal(data, &token); err != nil {
logger.Print(err)
http.Error(w, "malformed access file", http.StatusInternalServerError)
return
}
if username, password, ok := req.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(fs).ServeHTTP(w, req)
})
return handler, nil
}