Source file src/cmd/go/internal/vcweb/auth.go

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package vcweb
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"log"
    12  	"net/http"
    13  	"os"
    14  	"path"
    15  	"strings"
    16  )
    17  
    18  // authHandler serves requests only if the Basic Auth data sent with the request
    19  // matches the contents of a ".access" file in the requested directory.
    20  //
    21  // For each request, the handler looks for a file named ".access" and parses it
    22  // as a JSON-serialized accessToken. If the credentials from the request match
    23  // the accessToken, the file is served normally; otherwise, it is rejected with
    24  // the StatusCode and Message provided by the token.
    25  type authHandler struct{}
    26  
    27  type accessToken struct {
    28  	Username, Password string
    29  	StatusCode         int // defaults to 401.
    30  	Message            string
    31  }
    32  
    33  func (h *authHandler) Available() bool { return true }
    34  
    35  func (h *authHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) {
    36  	fs := http.Dir(dir)
    37  
    38  	handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    39  		urlPath := req.URL.Path
    40  		if urlPath != "" && strings.HasPrefix(path.Base(urlPath), ".") {
    41  			http.Error(w, "filename contains leading dot", http.StatusBadRequest)
    42  			return
    43  		}
    44  
    45  		f, err := fs.Open(urlPath)
    46  		if err != nil {
    47  			if os.IsNotExist(err) {
    48  				http.NotFound(w, req)
    49  			} else {
    50  				http.Error(w, err.Error(), http.StatusInternalServerError)
    51  			}
    52  			return
    53  		}
    54  
    55  		accessDir := urlPath
    56  		if fi, err := f.Stat(); err == nil && !fi.IsDir() {
    57  			accessDir = path.Dir(urlPath)
    58  		}
    59  		f.Close()
    60  
    61  		var accessFile http.File
    62  		for {
    63  			var err error
    64  			accessFile, err = fs.Open(path.Join(accessDir, ".access"))
    65  			if err == nil {
    66  				break
    67  			}
    68  
    69  			if !os.IsNotExist(err) {
    70  				http.Error(w, err.Error(), http.StatusInternalServerError)
    71  				return
    72  			}
    73  			if accessDir == "." {
    74  				http.Error(w, "failed to locate access file", http.StatusInternalServerError)
    75  				return
    76  			}
    77  			accessDir = path.Dir(accessDir)
    78  		}
    79  
    80  		data, err := io.ReadAll(accessFile)
    81  		if err != nil {
    82  			http.Error(w, err.Error(), http.StatusInternalServerError)
    83  			return
    84  		}
    85  
    86  		var token accessToken
    87  		if err := json.Unmarshal(data, &token); err != nil {
    88  			logger.Print(err)
    89  			http.Error(w, "malformed access file", http.StatusInternalServerError)
    90  			return
    91  		}
    92  		if username, password, ok := req.BasicAuth(); !ok || username != token.Username || password != token.Password {
    93  			code := token.StatusCode
    94  			if code == 0 {
    95  				code = http.StatusUnauthorized
    96  			}
    97  			if code == http.StatusUnauthorized {
    98  				w.Header().Add("WWW-Authenticate", fmt.Sprintf("basic realm=%s", accessDir))
    99  			}
   100  			http.Error(w, token.Message, code)
   101  			return
   102  		}
   103  
   104  		http.FileServer(fs).ServeHTTP(w, req)
   105  	})
   106  
   107  	return handler, nil
   108  }
   109  

View as plain text