diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..48a0555 --- /dev/null +++ b/auth.go @@ -0,0 +1,10 @@ +package main + +import ( + "log" + "net/http" +) + +func Authorize(r *http.Request) bool { + return true +} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 0194ede..0000000 --- a/config/config.go +++ /dev/null @@ -1,37 +0,0 @@ -package config - -import ( - "path/filepath" - - "github.com/restic/restic/backend" -) - -var root string - -func Init(path string) { - root = path -} - -func ConfigPath(repository string) string { - return filepath.Join(root, repository, string(backend.Config)) -} - -func DataPath(repository string) string { - return filepath.Join(root, repository, string(backend.Data)) -} - -func SnapshotPath(repository string) string { - return filepath.Join(root, repository, string(backend.Snapshot)) -} - -func IndexPath(repository string) string { - return filepath.Join(root, repository, string(backend.Index)) -} - -func LockPath(repository string) string { - return filepath.Join(root, repository, string(backend.Lock)) -} - -func KeyPath(repository string) string { - return filepath.Join(root, repository, string(backend.Key)) -} diff --git a/context.go b/context.go new file mode 100644 index 0000000..dadeb09 --- /dev/null +++ b/context.go @@ -0,0 +1,17 @@ +package main + +import () + +// A Context specifies the root directory where all repositories are stored +type Context struct { + path string +} + +func NewContext(path string) Context { + return Context{path} +} + +// Creates the file structure of the Context +func (c *Context) Init() { + +} diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..41bb284 --- /dev/null +++ b/handlers.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "net/http" +) + +type Handler func(w http.ResponseWriter, r *http.Request, c *Context) + +func HeadConfig(w http.ResponseWriter, r *http.Request, c *Context) { + fmt.Fprintln(w, "head config") +} + +func GetConfig(w http.ResponseWriter, r *http.Request, c *Context) { + fmt.Fprintln(w, "get config") +} + +func PostConfig(w http.ResponseWriter, r *http.Request, c *Context) { + fmt.Fprintln(w, "post config") +} + +func ListBlob(w http.ResponseWriter, r *http.Request, c *Context) { + fmt.Fprintln(w, "list blob") +} + +func HeadBlob(w http.ResponseWriter, r *http.Request, c *Context) { + fmt.Fprintln(w, "head blob") +} + +func GetBlob(w http.ResponseWriter, r *http.Request, c *Context) { + fmt.Fprintln(w, "get blob") +} + +func PostBlob(w http.ResponseWriter, r *http.Request, c *Context) { + fmt.Fprintln(w, "post blob") +} + +func DeleteBlob(w http.ResponseWriter, r *http.Request, c *Context) { + fmt.Fprintln(w, "delete blob") +} diff --git a/handlers/config.go b/handlers/config.go deleted file mode 100644 index cf60eee..0000000 --- a/handlers/config.go +++ /dev/null @@ -1,56 +0,0 @@ -package handlers - -import ( - "io/ioutil" - "net/http" - "os" - - "github.com/bchapuis/restic-server/config" -) - -func HeadConfig(w http.ResponseWriter, r *http.Request) { - repo, err := ExtractRepository(r) - if err != nil { - http.Error(w, "403 invalid repository", 403) - return - } - file := config.ConfigPath(repo) - if _, err := os.Stat(file); err != nil { - http.Error(w, "404 repository not found", 404) - return - } -} - -func GetConfig(w http.ResponseWriter, r *http.Request) { - repo, err := ExtractRepository(r) - if err != nil { - http.Error(w, "403 invalid repository", 403) - return - } - file := config.ConfigPath(repo) - if _, err := os.Stat(file); err == nil { - bytes, _ := ioutil.ReadFile(file) - w.Write(bytes) - return - } else { - http.Error(w, "404 repository not found", 404) - return - } -} - -func PostConfig(w http.ResponseWriter, r *http.Request) { - repo, err := ExtractRepository(r) - if err != nil { - http.Error(w, "403 invalid repository", 403) - return - } - file := config.ConfigPath(repo) - if _, err := os.Stat(file); err == nil { - http.Error(w, "409 repository already initialized", 409) - return - } else { - bytes, _ := ioutil.ReadAll(r.Body) - ioutil.WriteFile(file, bytes, 0600) - return - } -} diff --git a/handlers/data.go b/handlers/data.go deleted file mode 100644 index f1cdba6..0000000 --- a/handlers/data.go +++ /dev/null @@ -1,54 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" - "os" - "path/filepath" - - "github.com/bchapuis/restic-server/config" -) - -func HeadData(w http.ResponseWriter, r *http.Request) { - repo, err := ExtractRepository(r) - if err != nil { - http.Error(w, "403 invalid repository", 403) - return - } - id, err := ExtractID(r) - if err != nil { - http.Error(w, "403 invalid ID", 403) - return - } - file := filepath.Join(config.DataPath(repo), id.String()) - if _, err := os.Stat(file); err != nil { - http.Error(w, "404 repository not found", 404) - return - } -} - -func GetData(w http.ResponseWriter, r *http.Request) { - repo, err := ExtractRepository(r) - if err != nil { - http.Error(w, "403 invalid repository", 403) - return - } - id, err := ExtractID(r) - if err != nil { - http.Error(w, "403 invalid ID", 403) - return - } - file := filepath.Join(config.DataPath(repo), id.String()) - if _, err := os.Stat(file); err != nil { - http.Error(w, "404 repository not found", 404) - return - } -} - -func PostData(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "data") -} - -func DeleteData(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "data") -} diff --git a/handlers/index.go b/handlers/index.go deleted file mode 100644 index e5555dc..0000000 --- a/handlers/index.go +++ /dev/null @@ -1,22 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" -) - -func HeadIndex(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "data") -} - -func GetIndex(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "index") -} - -func PostIndex(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "index") -} - -func DeleteIndex(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "index") -} diff --git a/handlers/key.go b/handlers/key.go deleted file mode 100644 index aa9af52..0000000 --- a/handlers/key.go +++ /dev/null @@ -1,22 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" -) - -func HeadKey(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "data") -} - -func GetKey(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "key") -} - -func PostKey(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "key") -} - -func DeleteKey(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "key") -} diff --git a/handlers/lock.go b/handlers/lock.go deleted file mode 100644 index 256289e..0000000 --- a/handlers/lock.go +++ /dev/null @@ -1,22 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" -) - -func HeadLock(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "data") -} - -func GetLock(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "lock") -} - -func PostLock(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "lock") -} - -func DeleteLock(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "lock") -} diff --git a/handlers/logger.go b/handlers/logger.go deleted file mode 100644 index 75340d7..0000000 --- a/handlers/logger.go +++ /dev/null @@ -1,10 +0,0 @@ -package handlers - -import ( - "log" - "net/http" -) - -func RequestLogger(w http.ResponseWriter, r *http.Request) { - log.Printf("%v %v", r.Method, r.URL.String()) -} diff --git a/handlers/snapshot.go b/handlers/snapshot.go deleted file mode 100644 index a4e49ab..0000000 --- a/handlers/snapshot.go +++ /dev/null @@ -1,22 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" -) - -func HeadSnapshot(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "data") -} - -func GetSnapshot(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "snapshot") -} - -func PostSnapshot(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "snapshot") -} - -func DeleteSnapshot(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "snapshot") -} diff --git a/handlers/variables.go b/handlers/variables.go deleted file mode 100644 index 540f3b8..0000000 --- a/handlers/variables.go +++ /dev/null @@ -1,25 +0,0 @@ -package handlers - -import ( - "errors" - "net/http" - "strings" - - "github.com/restic/restic/backend" -) - -func ExtractUser(r *http.Request) (string, string, error) { - return "username", "password", nil -} - -func ExtractRepository(r *http.Request) (string, error) { - return "repository", nil -} - -func ExtractID(r *http.Request) (backend.ID, error) { - path := strings.Split(r.URL.String(), "/") - if len(path) != 3 { - return backend.ID{}, errors.New("invalid request path") - } - return backend.ParseID(path[2]) -} diff --git a/repository.go b/repository.go new file mode 100644 index 0000000..57cf0f9 --- /dev/null +++ b/repository.go @@ -0,0 +1,13 @@ +package main + +import () + +// A Repository is the place where backups are stored +type Repository struct { + path string +} + +// Creates the file structure of the Repository +func (r *Repository) Init() { + +} diff --git a/restic-server b/restic-server new file mode 100755 index 0000000..ead96f5 Binary files /dev/null and b/restic-server differ diff --git a/router.go b/router.go new file mode 100644 index 0000000..96ad7d4 --- /dev/null +++ b/router.go @@ -0,0 +1,110 @@ +package main + +import ( + "errors" + "log" + "net/http" + "regexp" + "strings" + + "github.com/restic/restic/backend" +) + +// Route all the server requests +func Router(w http.ResponseWriter, r *http.Request) { + m := r.Method + u := r.RequestURI + + log.Println("%s %s", m, u) + + if Authorize(r) { + if handler := RestAPI(m, u); handler != nil { + handler(w, r, nil) + } else { + http.Error(w, "not found", 404) + } + } else { + http.Error(w, "unauthorized", 403) + } +} + +// Returns the repository name for a given path +func RepositoryName(u string) (string, error) { + s := strings.Split(u, "/") + if len(s) <= 1 { + return "", errors.New("path does not contain repository name") + } + if len(s[1]) < 1 { + return "", errors.New("repository name should contain at least 1 character") + } + match, err := regexp.MatchString("^[a-zA-Z0-9_-]*$", s[1]) + if !match || err != nil { + return "", errors.New("repository name should not contains special characters") + } + return s[1], nil +} + +// Returns the backend type for a given path +func BackendType(u string) backend.Type { + s := strings.Split(u, "/") + var bt backend.Type + if len(s) > 2 { + bt, _ = backend.ParseType(s[2]) + } + return bt +} + +// Returns the blob ID for a given path +func BlobID(u string) backend.ID { + s := strings.Split(u, "/") + var id backend.ID + if len(s) > 3 { + id, _ = backend.ParseID(s[3]) + } + return id +} + +// The Rest API returns a Handler when a match occur or nil. +func RestAPI(m string, u string) Handler { + s := strings.Split(u, "/") + + // Check for valid repository name + _, err := RepositoryName(u) + if err != nil { + return nil + } + + // Route config requests + bt := BackendType(u) + if len(s) == 3 && bt == backend.Config { + switch m { + case "HEAD": + return HeadConfig + case "GET": + return GetConfig + case "POST": + return PostConfig + } + } + + // Route blob requests + id := BlobID(u) + if len(s) == 4 && !bt.IsNull() && bt != backend.Config { + if s[3] == "" && m == "GET" { + return ListBlob + } else if !id.IsNull() { + switch m { + case "HEAD": + return HeadBlob + case "GET": + return GetBlob + case "POST": + return PostBlob + case "DELETE": + return DeleteBlob + } + } + } + + return nil +} diff --git a/router/router.go b/router/router.go deleted file mode 100644 index 8614008..0000000 --- a/router/router.go +++ /dev/null @@ -1,54 +0,0 @@ -package router - -import ( - "net/http" - "strings" -) - -type Route struct { - method string - pattern string - handler http.Handler -} - -type Router struct { - filters []http.Handler - routes []Route -} - -func NewRouter() Router { - filters := []http.Handler{} - routes := []Route{} - return Router{filters, routes} -} - -func (router *Router) Filter(handler http.Handler) { - router.filters = append(router.filters, handler) -} - -func (router *Router) FilterFunc(handlerFunc http.HandlerFunc) { - router.Filter(handlerFunc) -} - -func (router *Router) Handle(method string, pattern string, handler http.Handler) { - router.routes = append(router.routes, Route{method, pattern, handler}) -} - -func (router *Router) HandleFunc(method string, pattern string, handlerFunc http.HandlerFunc) { - router.Handle(method, pattern, handlerFunc) -} - -func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { - for i := 0; i < len(router.filters); i++ { - filter := router.filters[i] - filter.ServeHTTP(w, r) - } - for i := 0; i < len(router.routes); i++ { - route := router.routes[i] - if route.method == r.Method && strings.HasPrefix(r.URL.String(), route.pattern) { - route.handler.ServeHTTP(w, r) - return - } - } - http.NotFound(w, r) -} diff --git a/router_test.go b/router_test.go new file mode 100644 index 0000000..9122cd8 --- /dev/null +++ b/router_test.go @@ -0,0 +1,168 @@ +package main + +import ( + "testing" + + "github.com/restic/restic/backend" +) + +func TestRepositoryName(t *testing.T) { + var name string + var err error + + name, err = RepositoryName("") + if err == nil { + t.Error("empty string should produce an error") + } + + name, err = RepositoryName("/") + if err == nil { + t.Error("empty repository name should produce an error") + } + + name, err = RepositoryName("//") + if err == nil { + t.Error("empty repository name should produce an error") + } + + name, err = RepositoryName("/$test") + if err == nil { + t.Error("special characters should produce an error") + } + + name, err = RepositoryName("/test") + if name != "test" { + t.Errorf("repository name is %s but should be test", name) + } + + name, err = RepositoryName("/test-1234") + if name != "test-1234" { + t.Errorf("repository name is %s but should be test-1234", name) + } + + name, err = RepositoryName("/test_1234") + if name != "test_1234" { + t.Errorf("repository name is %s but should be test_1234", name) + } +} + +func TestBackendType(t *testing.T) { + var bt backend.Type + + bt = BackendType("/") + if !bt.IsNull() { + t.Error("backend type should be nil") + } + + bt = BackendType("/test") + if !bt.IsNull() { + t.Error("backend type should be nil") + } + + bt = BackendType("/test/config") + if bt != backend.Config { + t.Error("backend type should be config") + } + + bt = BackendType("/test/config/") + if bt != backend.Config { + t.Error("backend type should be config") + } + + bt = BackendType("/test/config/test") + if bt != backend.Config { + t.Error("backend type should be config") + } +} + +func TestBlobID(t *testing.T) { + var id backend.ID + + id = BlobID("/") + if !id.IsNull() { + t.Error("blob id should be nil") + } + + id = BlobID("/test") + if !id.IsNull() { + t.Error("blob id should be nil") + } + + id = BlobID("/test/data") + if !id.IsNull() { + t.Error("blob id should be nil") + } + + id = BlobID("/test/data/") + if !id.IsNull() { + t.Error("blob id should be nil") + } + + id = BlobID("/test/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + if id.IsNull() { + t.Error("blob id should not be nil") + } +} + +func TestRestAPI(t *testing.T) { + type route struct { + method string + path string + } + + validEndpoints := []route{ + route{"HEAD", "/repo/config"}, + route{"GET", "/repo/config"}, + route{"POST", "/repo/config"}, + route{"GET", "/repo/data/"}, + route{"HEAD", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"POST", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"DELETE", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/snapshot/"}, + route{"HEAD", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"POST", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"DELETE", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/index/"}, + route{"HEAD", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"POST", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"DELETE", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/snapshot/"}, + route{"HEAD", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"POST", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"DELETE", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/key/"}, + route{"HEAD", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"POST", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"DELETE", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + } + + for _, route := range validEndpoints { + if RestAPI(route.method, route.path) == nil { + t.Errorf("request %s %s should return a handler", route.method, route.path) + } + } + + invalidEndpoints := []route{ + route{"GET", "/"}, + route{"GET", "/repo"}, + route{"GET", "/repo/config/"}, + route{"GET", "/repo/config/aaaa"}, + route{"GET", "/repo/data"}, + route{"GET", "/repo/data/aaaaaaa"}, + route{"GET", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + route{"GET", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/"}, + route{"GET", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/test"}, + } + + for _, route := range invalidEndpoints { + if RestAPI(route.method, route.path) != nil { + t.Errorf("request %s %s should return nil", route.method, route.path) + } + } + +} diff --git a/server.go b/server.go index 1b15ec8..6281204 100644 --- a/server.go +++ b/server.go @@ -1,52 +1,10 @@ package main -import ( - "io/ioutil" - "net/http" - - "github.com/bchapuis/restic-server/config" - "github.com/bchapuis/restic-server/handlers" - "github.com/bchapuis/restic-server/router" -) +import () func main() { - path, _ := ioutil.TempDir("", "restic-repository-") + //path, _ := ioutil.TempDir("", "restic-repository-") - config.Init(path) - - r := router.NewRouter() - - r.FilterFunc(handlers.RequestLogger) - - r.HandleFunc("HEAD", "/config", handlers.HeadConfig) - r.HandleFunc("GET", "/config", handlers.GetConfig) - r.HandleFunc("POST", "/config", handlers.PostConfig) - - r.HandleFunc("HEAD", "/data", handlers.HeadData) - r.HandleFunc("GET", "/data", handlers.GetData) - r.HandleFunc("POST", "/data", handlers.PostData) - r.HandleFunc("DELETE", "/data", handlers.DeleteData) - - r.HandleFunc("HEAD", "/snapshot", handlers.HeadSnapshot) - r.HandleFunc("GET", "/snapshot", handlers.GetSnapshot) - r.HandleFunc("POST", "/snapshot", handlers.PostSnapshot) - r.HandleFunc("DELETE", "/snapshot", handlers.DeleteSnapshot) - - r.HandleFunc("HEAD", "/index", handlers.HeadIndex) - r.HandleFunc("GET", "/index", handlers.GetIndex) - r.HandleFunc("POST", "/index", handlers.PostIndex) - r.HandleFunc("DELETE", "/index", handlers.DeleteIndex) - - r.HandleFunc("HEAD", "/lock", handlers.HeadLock) - r.HandleFunc("GET", "/lock", handlers.GetLock) - r.HandleFunc("POST", "/lock", handlers.PostLock) - r.HandleFunc("DELETE", "/lock", handlers.DeleteLock) - - r.HandleFunc("HEAD", "/key", handlers.HeadKey) - r.HandleFunc("GET", "/key", handlers.GetKey) - r.HandleFunc("POST", "/key", handlers.PostKey) - r.HandleFunc("DELETE", "/key", handlers.DeleteKey) - - http.ListenAndServe(":8000", r) + //http.ListenAndServe(":8000", r) }