summaryrefslogtreecommitdiff
path: root/src/cmd/go/internal/vcweb/auth.go
blob: 383bf759ffcdc1ad44d22b15d695bdff519916d0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 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
}