mirror of
https://github.com/restic/rest-server.git
synced 2025-10-19 07:33:21 +00:00

- Helper method for internal server errors with consistent logging. - Add PanicOnError option to panic on internal server errors. This makes it easier to traces where the condition was hit in testing.
191 lines
5.2 KiB
Go
191 lines
5.2 KiB
Go
package restserver
|
|
|
|
import (
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"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
|
|
Listen string
|
|
Log string
|
|
CPUProfile string
|
|
TLSKey string
|
|
TLSCert string
|
|
TLS bool
|
|
NoAuth bool
|
|
AppendOnly bool
|
|
PrivateRepos bool
|
|
Prometheus bool
|
|
Debug bool
|
|
MaxRepoSize int64
|
|
PanicOnError bool
|
|
|
|
htpasswdFile *HtpasswdFile
|
|
quotaManager *quota.Manager
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
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
|
|
}
|