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  				defer accessFile.Close()
    67  				break
    68  			}
    69  
    70  			if !os.IsNotExist(err) {
    71  				http.Error(w, err.Error(), http.StatusInternalServerError)
    72  				return
    73  			}
    74  			if accessDir == "." {
    75  				http.Error(w, "failed to locate access file", http.StatusInternalServerError)
    76  				return
    77  			}
    78  			accessDir = path.Dir(accessDir)
    79  		}
    80  
    81  		data, err := io.ReadAll(accessFile)
    82  		if err != nil {
    83  			http.Error(w, err.Error(), http.StatusInternalServerError)
    84  			return
    85  		}
    86  
    87  		var token accessToken
    88  		if err := json.Unmarshal(data, &token); err != nil {
    89  			logger.Print(err)
    90  			http.Error(w, "malformed access file", http.StatusInternalServerError)
    91  			return
    92  		}
    93  		if username, password, ok := req.BasicAuth(); !ok || username != token.Username || password != token.Password {
    94  			code := token.StatusCode
    95  			if code == 0 {
    96  				code = http.StatusUnauthorized
    97  			}
    98  			if code == http.StatusUnauthorized {
    99  				w.Header().Add("WWW-Authenticate", fmt.Sprintf("basic realm=%s", accessDir))
   100  			}
   101  			http.Error(w, token.Message, code)
   102  			return
   103  		}
   104  
   105  		http.FileServer(fs).ServeHTTP(w, req)
   106  	})
   107  
   108  	return handler, nil
   109  }
   110  

View as plain text