From b2b27b2c3d04cd7383d1a975d10185e4aa0cc7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lie=20DELHAIE?= Date: Thu, 15 May 2025 00:46:57 +0200 Subject: [PATCH] server --- cmd/cli/commands/list/list.go | 4 +- cmd/cli/commands/sync/sync.go | 9 +- cmd/cli/commands/version/version.go | 38 +++++ cmd/cli/main.go | 2 + cmd/server/api/api.go | 181 ++++++++++++++++++++++ cmd/server/api/middlewares.go | 46 ++++++ cmd/server/api/responses.go | 187 +++++++++++++++++++++++ cmd/server/api/system.go | 26 ++++ cmd/server/main.go | 19 +++ cmd/server/main_windows.go | 17 +++ cmd/server/runner.go | 34 +++++ cmd/server/security/htpasswd/htpasswd.go | 37 +++++ go.mod | 9 +- go.sum | 89 +---------- pkg/constants/constants.go | 5 + pkg/remote/sync.go | 99 ------------ pkg/sync/ssh/ssh.go | 32 ---- pkg/tools/windows/windows_windows.go | 22 +++ 18 files changed, 622 insertions(+), 234 deletions(-) create mode 100644 cmd/cli/commands/version/version.go create mode 100644 cmd/server/api/api.go create mode 100644 cmd/server/api/middlewares.go create mode 100644 cmd/server/api/responses.go create mode 100644 cmd/server/api/system.go create mode 100644 cmd/server/main.go create mode 100644 cmd/server/main_windows.go create mode 100644 cmd/server/runner.go create mode 100644 cmd/server/security/htpasswd/htpasswd.go create mode 100644 pkg/constants/constants.go delete mode 100644 pkg/remote/sync.go delete mode 100644 pkg/sync/ssh/ssh.go create mode 100644 pkg/tools/windows/windows_windows.go diff --git a/cmd/cli/commands/list/list.go b/cmd/cli/commands/list/list.go index ecbe152..9c72367 100644 --- a/cmd/cli/commands/list/list.go +++ b/cmd/cli/commands/list/list.go @@ -12,20 +12,18 @@ import ( type ( ListCmd struct { - name string } ) func (*ListCmd) Name() string { return "list" } func (*ListCmd) Synopsis() string { return "list all game registered" } func (*ListCmd) Usage() string { - return `add: + return `list: List all game registered ` } func (p *ListCmd) SetFlags(f *flag.FlagSet) { - f.StringVar(&p.name, "name", "", "Override the name of the game") } func (p *ListCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { diff --git a/cmd/cli/commands/sync/sync.go b/cmd/cli/commands/sync/sync.go index 5a809c0..8dd477f 100644 --- a/cmd/cli/commands/sync/sync.go +++ b/cmd/cli/commands/sync/sync.go @@ -2,7 +2,6 @@ package sync import ( "cloudsave/pkg/remote" - "cloudsave/pkg/sync/ssh" "context" "flag" "fmt" @@ -28,15 +27,15 @@ func (p *SyncCmd) SetFlags(f *flag.FlagSet) { } func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { - remotes, err := remote.All() + _, err := remote.All() if err != nil { fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err) return subcommands.ExitFailure } - for _, remote := range remotes { - ssh.SFTPSyncer{}.Sync(remote) - } + /*for _, remote := range remotes { + + }*/ return subcommands.ExitSuccess } diff --git a/cmd/cli/commands/version/version.go b/cmd/cli/commands/version/version.go new file mode 100644 index 0000000..e20a341 --- /dev/null +++ b/cmd/cli/commands/version/version.go @@ -0,0 +1,38 @@ +package version + +import ( + "cloudsave/pkg/constants" + "context" + "flag" + "fmt" + "runtime" + "strconv" + + "github.com/google/subcommands" +) + +type ( + VersionCmd struct { + } +) + +func (*VersionCmd) Name() string { return "version" } +func (*VersionCmd) Synopsis() string { return "show version and system information" } +func (*VersionCmd) Usage() string { + return `add: + Show version and system information +` +} + +func (p *VersionCmd) SetFlags(f *flag.FlagSet) { +} + +func (p *VersionCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + fmt.Println("Client: CloudSave cli") + fmt.Println(" Version: " + constants.Version) + fmt.Println(" API version: " + strconv.Itoa(constants.ApiVersion)) + fmt.Println(" Go version: " + runtime.Version()) + fmt.Println(" OS/Arch: " + runtime.GOOS + "/" + runtime.GOARCH) + + return subcommands.ExitSuccess +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 0c68389..1a2d07a 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -7,6 +7,7 @@ import ( "cloudsave/cmd/cli/commands/remove" "cloudsave/cmd/cli/commands/run" "cloudsave/cmd/cli/commands/sync" + "cloudsave/cmd/cli/commands/version" "context" "flag" "os" @@ -18,6 +19,7 @@ func main() { subcommands.Register(subcommands.HelpCommand(), "help") subcommands.Register(subcommands.FlagsCommand(), "help") subcommands.Register(subcommands.CommandsCommand(), "help") + subcommands.Register(&version.VersionCmd{}, "help") subcommands.Register(&add.AddCmd{}, "management") subcommands.Register(&run.RunCmd{}, "management") diff --git a/cmd/server/api/api.go b/cmd/server/api/api.go new file mode 100644 index 0000000..2cd015f --- /dev/null +++ b/cmd/server/api/api.go @@ -0,0 +1,181 @@ +package api + +import ( + "cloudsave/pkg/game" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +type ( + HTTPServer struct { + Server *http.Server + documentRoot string + } +) + +// NewServer start the http server +func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServer { + if !filepath.IsAbs(documentRoot) { + panic("the document root is not an absolute path") + } + s := &HTTPServer{ + documentRoot: documentRoot, + } + router := chi.NewRouter() + router.NotFound(func(writer http.ResponseWriter, request *http.Request) { + notFound("This route does not exist", writer, request) + }) + router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) { + methodNotAllowed(writer, request) + }) + router.Use(middleware.Logger) + router.Use(recoverMiddleware) + router.Use(middleware.Compress(5, "application/gzip")) + router.Use(BasicAuth("cloudsave", creds)) + router.Use(middleware.Heartbeat("/heartbeat")) + router.Route("/api", func(routerAPI chi.Router) { + routerAPI.Route("/v1", func(r chi.Router) { + // Get information about the server + r.Get("/version", s.Information) + // Secured routes + r.Group(func(secureRouter chi.Router) { + // Save files routes + secureRouter.Route("/games", func(gameRouter chi.Router) { + // List all available saves + gameRouter.Get("/", s.all) + // Data routes + gameRouter.Group(func(uploadRouter chi.Router) { + uploadRouter.Post("/{id}/data", s.upload) + uploadRouter.Get("/{id}/data", s.download) + }) + }) + }) + }) + }) + s.Server = &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: router, + } + return s +} + +func (s HTTPServer) all(w http.ResponseWriter, r *http.Request) { + ds, err := os.ReadDir(s.documentRoot) + if err != nil { + fmt.Fprintln(os.Stderr, "failed to open datastore (", s.documentRoot, "):", err) + internalServerError(w, r) + return + } + + datastore := make([]game.Metadata, 0) + for _, d := range ds { + content, err := os.ReadFile(filepath.Join(s.documentRoot, d.Name(), "metadata.json")) + if err != nil { + continue + } + + var m game.Metadata + err = json.Unmarshal(content, &m) + if err != nil { + fmt.Fprintf(os.Stderr, "corrupted datastore: failed to parse %s/metadata.json: %s", d.Name(), err) + internalServerError(w, r) + } + + datastore = append(datastore, m) + } + + ok(datastore, w, r) +} + +func (s HTTPServer) download(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + path := filepath.Clean(filepath.Join(s.documentRoot, "data", id)) + + sdir, err := os.Stat(path) + if err != nil { + notFound("id not found", w, r) + return + } + + if !sdir.IsDir() { + notFound("id not found", w, r) + return + } + + path = filepath.Join(path, "data.tar.gz") + + f, err := os.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + notFound("id not found", w, r) + return + } + defer f.Close() + + // Get file info to set headers + fi, err := f.Stat() + if err != nil || fi.IsDir() { + internalServerError(w, r) + return + } + + // Set headers + w.Header().Set("Content-Disposition", "attachment; filename=\"data.tar.gz\"") + w.Header().Set("Content-Type", "application/gzip") + w.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10)) + w.WriteHeader(200) + + // Stream the file content + http.ServeContent(w, r, "data.tar.gz", fi.ModTime(), f) +} + +func (s HTTPServer) upload(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + path := filepath.Clean(filepath.Join(s.documentRoot, "data", id, "data.tar.gz")) + + // Limit max upload size (e.g., 500 MB) + r.Body = http.MaxBytesReader(w, r.Body, 500<<20) + + // Parse multipart form + err := r.ParseMultipartForm(500 << 20) // 500 MB + if err != nil { + fmt.Fprintln(os.Stderr, "error: failed to load payload:", err) + badRequest("bad payload", w, r) + return + } + + // Retrieve file + file, _, err := r.FormFile("payload") + if err != nil { + fmt.Fprintln(os.Stderr, "error: cannot find payload in the form:", err) + badRequest("payload not found", w, r) + return + } + defer file.Close() + + // Create destination file + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0640) + if err != nil { + fmt.Fprintln(os.Stderr, "error: failed to open file:", err) + internalServerError(w, r) + return + } + defer f.Close() + + // Copy the uploaded content to the file + if _, err := io.Copy(f, file); err != nil { + fmt.Fprintln(os.Stderr, "error: an error occured while downloading data:", err) + internalServerError(w, r) + return + } + + // Respond success + w.WriteHeader(http.StatusCreated) +} diff --git a/cmd/server/api/middlewares.go b/cmd/server/api/middlewares.go new file mode 100644 index 0000000..5fd02c1 --- /dev/null +++ b/cmd/server/api/middlewares.go @@ -0,0 +1,46 @@ +package api + +import ( + "fmt" + "net/http" + + "golang.org/x/crypto/bcrypt" +) + +func recoverMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + err := recover() + if err != nil { + internalServerError(w, r) + } + }() + next.ServeHTTP(w, r) + }) +} + +// BasicAuth implements a simple middleware handler for adding basic http auth to a route. +func BasicAuth(realm string, creds map[string]string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok { + basicAuthFailed(w, r, realm) + return + } + + credPass := creds[user] + if err := bcrypt.CompareHashAndPassword([]byte(credPass), []byte(pass)); err != nil { + basicAuthFailed(w, r, realm) + return + } + + next.ServeHTTP(w, r) + }) + } +} + +func basicAuthFailed(w http.ResponseWriter, r *http.Request, realm string) { + w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm)) + unauthorized(w, r) +} diff --git a/cmd/server/api/responses.go b/cmd/server/api/responses.go new file mode 100644 index 0000000..b7bdc66 --- /dev/null +++ b/cmd/server/api/responses.go @@ -0,0 +1,187 @@ +package api + +import ( + "encoding/json" + "log" + "net/http" + "time" +) + +type ( + httpCore struct { + Status int `json:"status"` + Timestamp time.Time `json:"timestamp"` + Path string `json:"path"` + } + + httpError struct { + httpCore + Error string `json:"error"` + Message string `json:"message"` + } + + httpObject struct { + httpCore + Data any `json:"data"` + } +) + +func internalServerError(w http.ResponseWriter, r *http.Request) { + e := httpError{ + httpCore: httpCore{ + Status: http.StatusInternalServerError, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Internal Server Error", + Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.", + } + + payload, err := json.Marshal(e) + if err != nil { + log.Println(err) + } + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, err = w.Write(payload) + if err != nil { + log.Println(err) + } +} + +func notFound(message string, w http.ResponseWriter, r *http.Request) { + e := httpError{ + httpCore: httpCore{ + Status: http.StatusNotFound, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Not Found", + Message: message, + } + + payload, err := json.Marshal(e) + if err != nil { + log.Println(err) + } + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, err = w.Write(payload) + if err != nil { + log.Println(err) + } +} + +func methodNotAllowed(w http.ResponseWriter, r *http.Request) { + e := httpError{ + httpCore: httpCore{ + Status: http.StatusMethodNotAllowed, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Method Not Allowed", + Message: "The server knows the request method, but the target resource doesn't support this method", + } + + payload, err := json.Marshal(e) + if err != nil { + log.Println(err) + } + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + _, err = w.Write(payload) + if err != nil { + log.Println(err) + } +} + +func unauthorized(w http.ResponseWriter, r *http.Request) { + e := httpError{ + httpCore: httpCore{ + Status: http.StatusUnauthorized, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Unauthorized", + Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.", + } + + payload, err := json.Marshal(e) + if err != nil { + log.Println(err) + } + w.Header().Add("Content-Type", "application/json") + w.Header().Add("WWW-Authenticate", "Custom realm=\"loginUserHandler via /api/login\"") + w.WriteHeader(http.StatusUnauthorized) + _, err = w.Write(payload) + if err != nil { + log.Println(err) + } +} + +func forbidden(w http.ResponseWriter, r *http.Request) { + e := httpError{ + httpCore: httpCore{ + Status: http.StatusForbidden, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Forbidden", + Message: "The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.", + } + + payload, err := json.Marshal(e) + if err != nil { + log.Println(err) + } + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, err = w.Write(payload) + if err != nil { + log.Println(err) + } +} + +func ok(obj interface{}, w http.ResponseWriter, r *http.Request) { + e := httpObject{ + httpCore: httpCore{ + Status: http.StatusOK, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Data: obj, + } + + payload, err := json.Marshal(e) + if err != nil { + log.Println(err) + } + w.Header().Add("Content-Type", "application/json") + _, err = w.Write(payload) + if err != nil { + log.Println(err) + } +} + +func badRequest(message string, w http.ResponseWriter, r *http.Request) { + e := httpError{ + httpCore: httpCore{ + Status: http.StatusBadRequest, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Bad Request", + Message: message, + } + + payload, err := json.Marshal(e) + if err != nil { + log.Println(err) + } + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, err = w.Write(payload) + if err != nil { + log.Println(err) + } +} diff --git a/cmd/server/api/system.go b/cmd/server/api/system.go new file mode 100644 index 0000000..a094135 --- /dev/null +++ b/cmd/server/api/system.go @@ -0,0 +1,26 @@ +package api + +import ( + "cloudsave/pkg/constants" + "net/http" + "runtime" +) + +type information struct { + Version string `json:"version"` + APIVersion int `json:"api_version"` + GoVersion string `json:"go_version"` + OSName string `json:"os_name"` + OSArchitecture string `json:"os_architecture"` +} + +func (s *HTTPServer) Information(w http.ResponseWriter, r *http.Request) { + info := information{ + Version: constants.Version, + APIVersion: constants.ApiVersion, + GoVersion: runtime.Version(), + OSName: runtime.GOOS, + OSArchitecture: runtime.GOARCH, + } + ok(info, w, r) +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..a8c865c --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,19 @@ +//go:build !windows + +package main + +import ( + "fmt" + "os" +) + +const defaultDocumentRoot string = "/var/lib/cloudsave" + +func main() { + run() +} + +func fatal(message string, exitCode int) { + fmt.Fprintln(os.Stderr, message) + os.Exit(exitCode) +} diff --git a/cmd/server/main_windows.go b/cmd/server/main_windows.go new file mode 100644 index 0000000..e7e7a85 --- /dev/null +++ b/cmd/server/main_windows.go @@ -0,0 +1,17 @@ +package main + +import ( + "cloudsave/pkg/tools/windows" + "os" +) + +const defaultDocumentRoot string = "C:/ProgramData/CloudSave" + +func main() { + run() +} + +func fatal(message string, exitCode int) { + windows.MessageBox(windows.NULL, message, "CloudSave", windows.MB_OK) + os.Exit(exitCode) +} diff --git a/cmd/server/runner.go b/cmd/server/runner.go new file mode 100644 index 0000000..5063dfa --- /dev/null +++ b/cmd/server/runner.go @@ -0,0 +1,34 @@ +package main + +import ( + "cloudsave/cmd/server/api" + "cloudsave/cmd/server/security/htpasswd" + "cloudsave/pkg/constants" + "flag" + "fmt" + "path/filepath" + "runtime" + "strconv" +) + +func run() { + fmt.Printf("CloudSave server -- v%s.%s.%s\n\n", constants.Version, runtime.GOOS, runtime.GOARCH) + + var documentRoot string + var port int + flag.StringVar(&documentRoot, "document-root", defaultDocumentRoot, "Define the path to the document root") + flag.IntVar(&port, "port", 8080, "Define the port of the server") + flag.Parse() + + h, err := htpasswd.Open(filepath.Join(documentRoot, ".htpasswd")) + if err != nil { + fatal("failed to load .htpasswd: "+err.Error(), 1) + } + + server := api.NewServer(documentRoot, h.Content(), port) + + fmt.Println("starting server at :" + strconv.Itoa(port)) + if err := server.Server.ListenAndServe(); err != nil { + fatal("failed to start server: "+err.Error(), 1) + } +} diff --git a/cmd/server/security/htpasswd/htpasswd.go b/cmd/server/security/htpasswd/htpasswd.go new file mode 100644 index 0000000..075ac2c --- /dev/null +++ b/cmd/server/security/htpasswd/htpasswd.go @@ -0,0 +1,37 @@ +package htpasswd + +import ( + "os" + "strings" +) + +type ( + File struct { + data map[string]string + } +) + +func Open(path string) (File, error) { + c, err := os.ReadFile(path) + if err != nil { + return File{}, err + } + + f := File{ + data: make(map[string]string), + } + creds := strings.Split(string(c), "\n") + for _, cred := range creds { + kv := strings.Split(cred, ":") + if len(kv) != 2 { + continue + } + f.data[kv[0]] = kv[1] + } + + return f, nil +} + +func (f File) Content() map[string]string { + return f.data +} diff --git a/go.mod b/go.mod index e02d1b0..121bfdd 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,7 @@ module cloudsave go 1.24 require ( + github.com/go-chi/chi/v5 v5.2.1 github.com/google/subcommands v1.2.0 - github.com/pkg/sftp v1.13.9 golang.org/x/crypto v0.38.0 - golang.org/x/term v0.32.0 -) - -require ( - github.com/kr/fs v0.1.0 // indirect - github.com/stretchr/testify v1.10.0 // indirect - golang.org/x/sys v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index 960b8cc..020c1a8 100644 --- a/go.sum +++ b/go.sum @@ -1,91 +1,6 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= -github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go new file mode 100644 index 0000000..4e14d23 --- /dev/null +++ b/pkg/constants/constants.go @@ -0,0 +1,5 @@ +package constants + +const Version = "0.0.1" + +const ApiVersion = 1 diff --git a/pkg/remote/sync.go b/pkg/remote/sync.go deleted file mode 100644 index 38fefd7..0000000 --- a/pkg/remote/sync.go +++ /dev/null @@ -1,99 +0,0 @@ -package remote - -import ( - "fmt" - "log" - "os" - "path/filepath" - "syscall" - - "github.com/pkg/sftp" - "golang.org/x/crypto/ssh" - "golang.org/x/term" -) - -func ConnectWithKey(host, user string) (*sftp.Client, error) { - authMethods := loadSshKeys() - - // Create SSH client configuration - config := &ssh.ClientConfig{ - User: user, - Auth: authMethods, - } - - return connect(host, config) -} - -func ConnectWithPassword(host, user string) (*sftp.Client, error) { - fmt.Printf("%s@%s's password:", user, host) - bytePassword, err := term.ReadPassword(int(syscall.Stdin)) - if err != nil { - return nil, err - } - - // Create SSH client configuration - config := &ssh.ClientConfig{ - User: user, - Auth: []ssh.AuthMethod{ - ssh.Password(string(bytePassword)), - }, - } - - return connect(host, config) -} - -func connect(host string, config *ssh.ClientConfig) (*sftp.Client, error) { - // Connect to the SSH server - conn, err := ssh.Dial("tcp", host, config) - if err != nil { - return nil, fmt.Errorf("cannot connect to ssh server: %w", err) - } - defer conn.Close() - - // Open SFTP session - sftpClient, err := sftp.NewClient(conn) - if err != nil { - return nil, fmt.Errorf("cannot connect to ssh server: %w", err) - } - return sftpClient, nil -} - -func loadSshKeys() []ssh.AuthMethod { - dirname, err := os.UserHomeDir() - if err != nil { - log.Fatal(err) - } - - var auths []ssh.AuthMethod - entries, err := os.ReadDir(filepath.Join(dirname, ".ssh")) - if err != nil { - return auths - } - - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - keyPath := filepath.Join(dirname, name) - keyData, err := os.ReadFile(keyPath) - if err != nil { - continue - } - signer, err := ssh.ParsePrivateKey(keyData) - if err != nil { - fmt.Printf("%s's passphrase:", entry.Name()) - bytePassword, err := term.ReadPassword(int(syscall.Stdin)) - if err != nil { - continue - } - signer, err = ssh.ParsePrivateKeyWithPassphrase(keyData, bytePassword) - if err != nil { - continue - } - } - auths = append(auths, ssh.PublicKeys(signer)) - } - - return auths -} diff --git a/pkg/sync/ssh/ssh.go b/pkg/sync/ssh/ssh.go deleted file mode 100644 index 48ba216..0000000 --- a/pkg/sync/ssh/ssh.go +++ /dev/null @@ -1,32 +0,0 @@ -package ssh - -import ( - "cloudsave/pkg/remote" - "fmt" - "log" - "os/user" -) - -type ( - SFTPSyncer struct { - } -) - -func (SFTPSyncer) Sync(r remote.Remote) error { - currentUser, err := user.Current() - if err != nil { - log.Fatalf("Failed to get current user: %v", err) - } - cli, err := remote.ConnectWithKey(r.URL, currentUser.Username) - if err != nil { - cli, err = remote.ConnectWithPassword(r.URL, currentUser.Username) - if err != nil { - return fmt.Errorf("failed to connect to host: %w", err) - } - } - defer cli.Close() - - - - return nil -} diff --git a/pkg/tools/windows/windows_windows.go b/pkg/tools/windows/windows_windows.go new file mode 100644 index 0000000..497445d --- /dev/null +++ b/pkg/tools/windows/windows_windows.go @@ -0,0 +1,22 @@ +package windows + +import ( + "syscall" + "unsafe" +) + +const ( + NULL = 0 + MB_OK = 0 +) + +// MessageBox of Win32 API. +func MessageBox(hwnd uintptr, caption, title string, flags uint) int { + ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))), + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))), + uintptr(flags)) + + return int(ret) +}