| 
									
										
										
										
											2017-06-21 15:23:33 -06:00
										 |  |  | package restserver | 
					
						
							| 
									
										
										
										
											2015-09-18 17:13:38 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2017-07-30 14:30:18 +02:00
										 |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2016-12-27 01:35:45 +01:00
										 |  |  | 	"log" | 
					
						
							| 
									
										
										
										
											2015-09-18 17:13:38 +02:00
										 |  |  | 	"net/http" | 
					
						
							| 
									
										
										
										
											2017-07-30 14:30:18 +02:00
										 |  |  | 	"path" | 
					
						
							| 
									
										
										
										
											2015-09-18 17:13:38 +02:00
										 |  |  | 	"path/filepath" | 
					
						
							| 
									
										
										
										
											2016-12-28 00:57:25 +01:00
										 |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2023-05-13 21:50:39 +02:00
										 |  |  | 	"sync" | 
					
						
							| 
									
										
										
										
											2017-10-25 12:32:25 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	"github.com/restic/rest-server/quota" | 
					
						
							|  |  |  | 	"github.com/restic/rest-server/repo" | 
					
						
							| 
									
										
										
										
											2015-09-18 17:13:38 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | // Server encapsulates the rest-server's settings and repo management logic | 
					
						
							| 
									
										
										
										
											2018-04-15 08:31:50 -06:00
										 |  |  | type Server struct { | 
					
						
							| 
									
										
										
										
											2025-02-17 22:17:54 +01:00
										 |  |  | 	Path                 string | 
					
						
							|  |  |  | 	HtpasswdPath         string | 
					
						
							|  |  |  | 	Listen               string | 
					
						
							|  |  |  | 	Log                  string | 
					
						
							|  |  |  | 	CPUProfile           string | 
					
						
							|  |  |  | 	TLSKey               string | 
					
						
							|  |  |  | 	TLSCert              string | 
					
						
							|  |  |  | 	TLS                  bool | 
					
						
							|  |  |  | 	NoAuth               bool | 
					
						
							| 
									
										
										
										
											2024-11-01 02:31:43 +01:00
										 |  |  | 	ProxyAuthUsername    string | 
					
						
							| 
									
										
										
										
											2025-02-17 22:17:54 +01:00
										 |  |  | 	AppendOnly           bool | 
					
						
							|  |  |  | 	PrivateRepos         bool | 
					
						
							|  |  |  | 	Prometheus           bool | 
					
						
							|  |  |  | 	PrometheusNoAuth     bool | 
					
						
							|  |  |  | 	Debug                bool | 
					
						
							|  |  |  | 	MaxRepoSize          int64 | 
					
						
							|  |  |  | 	PanicOnError         bool | 
					
						
							|  |  |  | 	NoVerifyUpload       bool | 
					
						
							|  |  |  | 	GroupAccessibleRepos bool | 
					
						
							| 
									
										
										
										
											2017-07-30 14:30:18 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	htpasswdFile *HtpasswdFile | 
					
						
							|  |  |  | 	quotaManager *quota.Manager | 
					
						
							| 
									
										
										
										
											2023-05-13 21:50:39 +02:00
										 |  |  | 	fsyncWarning sync.Once | 
					
						
							| 
									
										
										
										
											2016-12-28 00:57:25 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08: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) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | // 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) { | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08:00
										 |  |  | 	// First of all, check auth (will always pass if NoAuth is set) | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	username, ok := s.checkAuth(r) | 
					
						
							| 
									
										
										
										
											2017-10-29 22:19:11 +08:00
										 |  |  | 	if !ok { | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08:00
										 |  |  | 		httpDefaultError(w, http.StatusUnauthorized) | 
					
						
							|  |  |  | 		return | 
					
						
							| 
									
										
										
										
											2017-07-30 14:30:18 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	// Perform the path parsing to determine the repo folder and remainder for the | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08:00
										 |  |  | 	// repo handler. | 
					
						
							|  |  |  | 	folderPath, remainder := splitURLPath(r.URL.Path, MaxFolderDepth) | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	if !folderPathValid(folderPath) { | 
					
						
							|  |  |  | 		log.Printf("Invalid request path: %s", r.URL.Path) | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08:00
										 |  |  | 		httpDefaultError(w, http.StatusNotFound) | 
					
						
							| 
									
										
										
										
											2016-12-27 13:42:43 +01:00
										 |  |  | 		return | 
					
						
							| 
									
										
										
										
											2015-09-18 17:13:38 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	// Check if the current user is allowed to access this path | 
					
						
							|  |  |  | 	if !s.NoAuth && s.PrivateRepos { | 
					
						
							|  |  |  | 		if len(folderPath) == 0 || folderPath[0] != username { | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08:00
										 |  |  | 			httpDefaultError(w, http.StatusUnauthorized) | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 			return | 
					
						
							| 
									
										
										
										
											2017-01-16 23:39:56 +01:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2015-09-18 17:13:38 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2018-04-02 13:02:16 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	// Determine filesystem path for this repo | 
					
						
							|  |  |  | 	fsPath, err := join(s.Path, folderPath...) | 
					
						
							| 
									
										
										
										
											2018-04-02 13:02:16 +02:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 		// 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) | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08:00
										 |  |  | 		httpDefaultError(w, http.StatusNotFound) | 
					
						
							| 
									
										
										
										
											2018-04-02 13:02:16 +02:00
										 |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	// Pass the request to the repo.Handler | 
					
						
							|  |  |  | 	opt := repo.Options{ | 
					
						
							| 
									
										
										
										
											2025-02-17 22:17:54 +01:00
										 |  |  | 		AppendOnly:      s.AppendOnly, | 
					
						
							|  |  |  | 		Debug:           s.Debug, | 
					
						
							|  |  |  | 		QuotaManager:    s.quotaManager, // may be nil | 
					
						
							|  |  |  | 		PanicOnError:    s.PanicOnError, | 
					
						
							|  |  |  | 		NoVerifyUpload:  s.NoVerifyUpload, | 
					
						
							|  |  |  | 		FsyncWarning:    &s.fsyncWarning, | 
					
						
							|  |  |  | 		GroupAccessible: s.GroupAccessibleRepos, | 
					
						
							| 
									
										
										
										
											2017-03-18 13:11:29 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	if s.Prometheus { | 
					
						
							|  |  |  | 		opt.BlobMetricFunc = makeBlobMetricFunc(username, folderPath) | 
					
						
							| 
									
										
										
										
											2017-09-02 20:16:21 -05:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	repoHandler, err := repo.New(fsPath, opt) | 
					
						
							| 
									
										
										
										
											2017-07-30 14:30:18 +02:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 		log.Printf("repo.New error: %v", err) | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08:00
										 |  |  | 		httpDefaultError(w, http.StatusInternalServerError) | 
					
						
							| 
									
										
										
										
											2017-07-30 14:30:18 +02:00
										 |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	r.URL.Path = remainder // strip folderPath for next handler | 
					
						
							|  |  |  | 	repoHandler.ServeHTTP(w, r) | 
					
						
							| 
									
										
										
										
											2018-01-23 23:23:16 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | func valid(name string) bool { | 
					
						
							|  |  |  | 	// taken from net/http.Dir | 
					
						
							|  |  |  | 	if strings.Contains(name, "\x00") { | 
					
						
							|  |  |  | 		return false | 
					
						
							| 
									
										
										
										
											2018-01-22 17:13:01 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { | 
					
						
							|  |  |  | 		return false | 
					
						
							| 
									
										
										
										
											2018-01-22 17:13:01 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	return true | 
					
						
							| 
									
										
										
										
											2018-01-22 17:13:01 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | func isValidType(name string) bool { | 
					
						
							|  |  |  | 	for _, tpe := range repo.ObjectTypes { | 
					
						
							|  |  |  | 		if name == tpe { | 
					
						
							|  |  |  | 			return true | 
					
						
							| 
									
										
										
										
											2017-01-16 23:39:56 +01:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2016-12-27 13:42:43 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08: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
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	return false | 
					
						
							| 
									
										
										
										
											2015-09-18 17:13:38 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08: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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08: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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08: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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	return filepath.Join(clean...), nil | 
					
						
							| 
									
										
										
										
											2016-12-27 13:42:43 +01:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2016-11-06 14:02:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | // 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..." | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | func splitURLPath(urlPath string, maxDepth int) (folderPath []string, remainder string) { | 
					
						
							|  |  |  | 	if !strings.HasPrefix(urlPath, "/") { | 
					
						
							|  |  |  | 		// Really should start with "/" | 
					
						
							|  |  |  | 		return nil, urlPath | 
					
						
							| 
									
										
										
										
											2016-12-27 13:42:43 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	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 | 
					
						
							| 
									
										
										
										
											2017-10-29 22:19:11 +08:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 		folderPath = append(folderPath, name) | 
					
						
							| 
									
										
										
										
											2017-10-29 22:19:11 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	// If the folder path is empty, the whole path is the remainder (do not strip '/') | 
					
						
							|  |  |  | 	if len(folderPath) == 0 { | 
					
						
							|  |  |  | 		return nil, urlPath | 
					
						
							| 
									
										
										
										
											2017-03-18 13:11:29 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	// 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 | 
					
						
							| 
									
										
										
										
											2017-10-29 22:19:11 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	return folderPath, urlPath[len(fullFolderPath):] | 
					
						
							| 
									
										
										
										
											2015-09-18 17:13:38 +02:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2017-01-25 20:59:18 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | // folderPathValid checks if a folderPath returned by splitURLPath is valid and | 
					
						
							|  |  |  | // safe. | 
					
						
							|  |  |  | func folderPathValid(folderPath []string) bool { | 
					
						
							|  |  |  | 	for _, name := range folderPath { | 
					
						
							| 
									
										
										
										
											2020-05-31 21:36:39 +08:00
										 |  |  | 		if name == "" || name == ".." || name == "." || !valid(name) { | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 			return false | 
					
						
							| 
									
										
										
										
											2017-05-01 20:10:46 +02:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-05-04 01:28:13 +08:00
										 |  |  | 	return true | 
					
						
							| 
									
										
										
										
											2017-01-25 20:59:18 +01:00
										 |  |  | } |