rest-server/handlers.go

204 lines
5.6 KiB
Go
Raw Permalink Normal View History

package restserver
2015-09-18 17:13:38 +02:00
import (
"errors"
"log"
2015-09-18 17:13:38 +02:00
"net/http"
"path"
2015-09-18 17:13:38 +02:00
"path/filepath"
2016-12-28 00:57:25 +01:00
"strings"
"sync"
2017-10-25 12:32:25 +08:00
"github.com/restic/rest-server/quota"
"github.com/restic/rest-server/repo"
2015-09-18 17:13:38 +02:00
)
// 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
TLSMinVer string
TLS bool
NoAuth bool
ProxyAuthUsername string
AppendOnly bool
PrivateRepos bool
Prometheus bool
PrometheusNoAuth bool
Debug bool
MaxRepoSize int64
PanicOnError bool
NoVerifyUpload bool
GroupAccessibleRepos bool
htpasswdFile *HtpasswdFile
quotaManager *quota.Manager
fsyncWarning sync.Once
2016-12-28 00:57:25 +01:00
}
// 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
2015-09-18 17:13:38 +02:00
}
// 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
2017-01-16 23:39:56 +01:00
}
2015-09-18 17:13:38 +02:00
}
// 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,
GroupAccessible: s.GroupAccessibleRepos,
}
if s.Prometheus {
opt.BlobMetricFunc = makeBlobMetricFunc(username, folderPath)
2017-09-02 20:16:21 -05:00
}
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
2017-01-16 23:39:56 +01:00
}
}
for _, tpe := range repo.FileTypes {
if name == tpe {
return true
2017-01-16 23:39:56 +01:00
}
Add Prometheus metrics Exposes a few metrics for Prometheus under /metrics if started with --prometheus. Example: # HELP rest_server_blob_read_bytes_total Total number of bytes read from blobs # TYPE rest_server_blob_read_bytes_total counter rest_server_blob_read_bytes_total{repo="test",type="data"} 2.13557024e+09 rest_server_blob_read_bytes_total{repo="test",type="index"} 1.198653e+06 rest_server_blob_read_bytes_total{repo="test",type="keys"} 5388 rest_server_blob_read_bytes_total{repo="test",type="locks"} 1975 rest_server_blob_read_bytes_total{repo="test",type="snapshots"} 10018 # HELP rest_server_blob_read_total Total number of blobs read # TYPE rest_server_blob_read_total counter rest_server_blob_read_total{repo="test",type="data"} 3985 rest_server_blob_read_total{repo="test",type="index"} 21 rest_server_blob_read_total{repo="test",type="keys"} 12 rest_server_blob_read_total{repo="test",type="locks"} 12 rest_server_blob_read_total{repo="test",type="snapshots"} 32 # HELP rest_server_blob_write_bytes_total Total number of bytes written to blobs # TYPE rest_server_blob_write_bytes_total counter rest_server_blob_write_bytes_total{repo="test",type="data"} 1.063726179e+09 rest_server_blob_write_bytes_total{repo="test",type="index"} 395586 rest_server_blob_write_bytes_total{repo="test",type="locks"} 1975 rest_server_blob_write_bytes_total{repo="test",type="snapshots"} 1933 # HELP rest_server_blob_write_total Total number of blobs written # TYPE rest_server_blob_write_total counter rest_server_blob_write_total{repo="test",type="data"} 226 rest_server_blob_write_total{repo="test",type="index"} 6 rest_server_blob_write_total{repo="test",type="locks"} 12 rest_server_blob_write_total{repo="test",type="snapshots"} 6
2017-10-24 23:03:50 +08:00
}
return false
2015-09-18 17:13:38 +02:00
}
// 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)
2017-05-01 20:01:52 +02:00
// taken from net/http.Dir
for _, name := range names {
if !valid(name) {
return "", errors.New("invalid character in path")
2017-01-16 23:39:56 +01:00
}
2017-05-01 20:01:52 +02:00
clean = append(clean, filepath.FromSlash(path.Clean("/"+name)))
2015-09-18 17:13:38 +02:00
}
Add Prometheus metrics Exposes a few metrics for Prometheus under /metrics if started with --prometheus. Example: # HELP rest_server_blob_read_bytes_total Total number of bytes read from blobs # TYPE rest_server_blob_read_bytes_total counter rest_server_blob_read_bytes_total{repo="test",type="data"} 2.13557024e+09 rest_server_blob_read_bytes_total{repo="test",type="index"} 1.198653e+06 rest_server_blob_read_bytes_total{repo="test",type="keys"} 5388 rest_server_blob_read_bytes_total{repo="test",type="locks"} 1975 rest_server_blob_read_bytes_total{repo="test",type="snapshots"} 10018 # HELP rest_server_blob_read_total Total number of blobs read # TYPE rest_server_blob_read_total counter rest_server_blob_read_total{repo="test",type="data"} 3985 rest_server_blob_read_total{repo="test",type="index"} 21 rest_server_blob_read_total{repo="test",type="keys"} 12 rest_server_blob_read_total{repo="test",type="locks"} 12 rest_server_blob_read_total{repo="test",type="snapshots"} 32 # HELP rest_server_blob_write_bytes_total Total number of bytes written to blobs # TYPE rest_server_blob_write_bytes_total counter rest_server_blob_write_bytes_total{repo="test",type="data"} 1.063726179e+09 rest_server_blob_write_bytes_total{repo="test",type="index"} 395586 rest_server_blob_write_bytes_total{repo="test",type="locks"} 1975 rest_server_blob_write_bytes_total{repo="test",type="snapshots"} 1933 # HELP rest_server_blob_write_total Total number of blobs written # TYPE rest_server_blob_write_total counter rest_server_blob_write_total{repo="test",type="data"} 226 rest_server_blob_write_total{repo="test",type="index"} 6 rest_server_blob_write_total{repo="test",type="locks"} 12 rest_server_blob_write_total{repo="test",type="snapshots"} 6
2017-10-24 23:03:50 +08:00
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:
2025-02-02 22:45:41 +01:00
//
// ["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):]
2015-09-18 17:13:38 +02:00
}
// 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
}