aboutsummaryrefslogtreecommitdiff
path: root/handlers.go
blob: 1bccc6088e843c64917e89c69b087e6fb596aa3d (plain) (blame)
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package restserver

import (
	"errors"
	"log"
	"net/http"
	"path"
	"path/filepath"
	"strings"
	"sync"

	"github.com/restic/rest-server/quota"
	"github.com/restic/rest-server/repo"
)

// Server encapsulates the rest-server's settings and repo management logic
type Server struct {
	Path             string
	HtpasswdPath     string
	Listen           string
	Log              string
	CPUProfile       string
	TLSKey           string
	TLSCert          string
	TLS              bool
	NoAuth           bool
	AppendOnly       bool
	PrivateRepos     bool
	Prometheus       bool
	PrometheusNoAuth bool
	Debug            bool
	MaxRepoSize      int64
	PanicOnError     bool
	NoVerifyUpload   bool

	htpasswdFile *HtpasswdFile
	quotaManager *quota.Manager
	fsyncWarning sync.Once
}

// MaxFolderDepth is the maxDepth param passed to splitURLPath.
// A max depth of 2 mean that we accept folders like: '/', '/foo' and '/foo/bar'
// TODO: Move to a Server option
const MaxFolderDepth = 2

// httpDefaultError write a HTTP error with the default description
func httpDefaultError(w http.ResponseWriter, code int) {
	http.Error(w, http.StatusText(code), code)
}

// ServeHTTP makes this server an http.Handler. It handlers the administrative
// part of the request (figuring out the filesystem location, performing
// authentication, etc) and then passes it on to repo.Handler for actual
// REST API processing.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// First of all, check auth (will always pass if NoAuth is set)
	username, ok := s.checkAuth(r)
	if !ok {
		httpDefaultError(w, http.StatusUnauthorized)
		return
	}

	// Perform the path parsing to determine the repo folder and remainder for the
	// repo handler.
	folderPath, remainder := splitURLPath(r.URL.Path, MaxFolderDepth)
	if !folderPathValid(folderPath) {
		log.Printf("Invalid request path: %s", r.URL.Path)
		httpDefaultError(w, http.StatusNotFound)
		return
	}

	// Check if the current user is allowed to access this path
	if !s.NoAuth && s.PrivateRepos {
		if len(folderPath) == 0 || folderPath[0] != username {
			httpDefaultError(w, http.StatusUnauthorized)
			return
		}
	}

	// Determine filesystem path for this repo
	fsPath, err := join(s.Path, folderPath...)
	if err != nil {
		// We did not expect an error at this stage, because we just checked the path
		log.Printf("Unexpected join error for path %q", r.URL.Path)
		httpDefaultError(w, http.StatusNotFound)
		return
	}

	// Pass the request to the repo.Handler
	opt := repo.Options{
		AppendOnly:     s.AppendOnly,
		Debug:          s.Debug,
		QuotaManager:   s.quotaManager, // may be nil
		PanicOnError:   s.PanicOnError,
		NoVerifyUpload: s.NoVerifyUpload,
		FsyncWarning:   &s.fsyncWarning,
	}
	if s.Prometheus {
		opt.BlobMetricFunc = makeBlobMetricFunc(username, folderPath)
	}
	repoHandler, err := repo.New(fsPath, opt)
	if err != nil {
		log.Printf("repo.New error: %v", err)
		httpDefaultError(w, http.StatusInternalServerError)
		return
	}
	r.URL.Path = remainder // strip folderPath for next handler
	repoHandler.ServeHTTP(w, r)
}

func valid(name string) bool {
	// taken from net/http.Dir
	if strings.Contains(name, "\x00") {
		return false
	}

	if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
		return false
	}

	return true
}

func isValidType(name string) bool {
	for _, tpe := range repo.ObjectTypes {
		if name == tpe {
			return true
		}
	}
	for _, tpe := range repo.FileTypes {
		if name == tpe {
			return true
		}
	}
	return false
}

// join takes a number of path names, sanitizes them, and returns them joined
// with base for the current operating system to use (dirs separated by
// filepath.Separator). The returned path is always either equal to base or a
// subdir of base.
func join(base string, names ...string) (string, error) {
	clean := make([]string, 0, len(names)+1)
	clean = append(clean, base)

	// taken from net/http.Dir
	for _, name := range names {
		if !valid(name) {
			return "", errors.New("invalid character in path")
		}

		clean = append(clean, filepath.FromSlash(path.Clean("/"+name)))
	}

	return filepath.Join(clean...), nil
}

// splitURLPath splits the URL path into a folderPath of the subrepo, and
// a remainder that can be passed to repo.Handler.
// Example: /foo/bar/locks/0123... will be split into:
//
//	["foo", "bar"] and "/locks/0123..."
func splitURLPath(urlPath string, maxDepth int) (folderPath []string, remainder string) {
	if !strings.HasPrefix(urlPath, "/") {
		// Really should start with "/"
		return nil, urlPath
	}
	p := strings.SplitN(urlPath, "/", maxDepth+2)
	// Skip the empty first one and the remainder in the last one
	for _, name := range p[1 : len(p)-1] {
		if isValidType(name) {
			// We found a part that is a special repo file or dir
			break
		}
		folderPath = append(folderPath, name)
	}
	// If the folder path is empty, the whole path is the remainder (do not strip '/')
	if len(folderPath) == 0 {
		return nil, urlPath
	}
	// Check that the urlPath starts with the reconstructed path, which should
	// always be the case.
	fullFolderPath := "/" + strings.Join(folderPath, "/")
	if !strings.HasPrefix(urlPath, fullFolderPath) {
		return nil, urlPath
	}
	return folderPath, urlPath[len(fullFolderPath):]
}

// folderPathValid checks if a folderPath returned by splitURLPath is valid and
// safe.
func folderPathValid(folderPath []string) bool {
	for _, name := range folderPath {
		if name == "" || name == ".." || name == "." || !valid(name) {
			return false
		}
	}
	return true
}