diff --git a/cmd/client/actions/actions.go b/cmd/client/actions/actions.go new file mode 100644 index 0000000..1954b76 --- /dev/null +++ b/cmd/client/actions/actions.go @@ -0,0 +1,188 @@ +package actions + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "strings" + "syscall" + "time" + + "git.t-juice.club/torjus/gpaste/api" + "github.com/google/uuid" + "github.com/urfave/cli/v2" + "golang.org/x/term" +) + +func ActionUpload(c *cli.Context) error { + url := fmt.Sprintf("%s/api/file", c.String("url")) + client := &http.Client{} + // TODO: Change timeout + ctx, cancel := context.WithTimeout(c.Context, 10*time.Minute) + defer cancel() + + buf := &bytes.Buffer{} + mw := multipart.NewWriter(buf) + + for _, arg := range c.Args().Slice() { + f, err := os.Open(arg) + if err != nil { + return err + } + defer f.Close() + fw, err := mw.CreateFormFile(uuid.Must(uuid.NewRandom()).String(), arg) + if err != nil { + return err + } + if _, err := io.Copy(fw, f); err != nil { + return err + } + } + mw.Close() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, buf) + if err != nil { + return err + } + req.Header.Add("Content-Type", mw.FormDataContentType()) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + var expectedResp []struct { + Message string `json:"message"` + ID string `json:"id"` + URL string `json:"url"` + } + + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&expectedResp); err != nil { + return fmt.Errorf("error decoding response: %w", err) + } + + for _, r := range expectedResp { + fmt.Printf("Uploaded file %s\n", r.ID) + } + return nil +} + +func ActionLogin(c *cli.Context) error { + username := c.Args().First() + if username == "" { + return cli.Exit("USERNAME not supplied.", 1) + } + password, err := readPassword() + if err != nil { + return fmt.Errorf("error reading password: %w", err) + } + + url := fmt.Sprintf("%s/api/login", c.String("url")) + client := &http.Client{} + // TODO: Change timeout + ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) + defer cancel() + + body := new(bytes.Buffer) + requestData := struct { + Username string `json:"username"` + Password string `json:"password"` + }{ + Username: username, + Password: password, + } + encoder := json.NewEncoder(body) + if err := encoder.Encode(&requestData); err != nil { + return fmt.Errorf("error encoding response: %w", err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("unable to perform request: %s", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return cli.Exit("got non-ok response from server", 0) + } + + responseData := struct { + Token string `json:"token"` + }{} + + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&responseData); err != nil { + return fmt.Errorf("unable to parse response: %s", err) + } + + fmt.Printf("Token: %s", responseData.Token) + + return nil +} + +func ActionUserCreate(c *cli.Context) error { + // TODO: Needs to supply auth token to actually work + username := c.Args().First() + if username == "" { + return cli.Exit("USERNAME not supplied.", 1) + } + password, err := readPassword() + if err != nil { + return fmt.Errorf("error reading password: %w", err) + } + + url := fmt.Sprintf("%s/api/user", c.String("url")) + client := &http.Client{} + // TODO: Change timeout + ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) + defer cancel() + + body := new(bytes.Buffer) + requestData := &api.RequestAPIUserCreate{ + Username: username, + Password: password, + } + encoder := json.NewEncoder(body) + if err := encoder.Encode(requestData); err != nil { + return fmt.Errorf("error encoding response: %w", err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("unable to perform request: %s", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusAccepted { + return cli.Exit("got non-ok response from server", 0) + } + + fmt.Printf("Created user %s\n", username) + + return nil +} + +func readPassword() (string, error) { + fmt.Print("Enter Password: ") + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", err + } + + password := string(bytePassword) + return strings.TrimSpace(password), nil +} diff --git a/cmd/client/client.go b/cmd/client/client.go index 538a88c..3274bcc 100644 --- a/cmd/client/client.go +++ b/cmd/client/client.go @@ -1,22 +1,11 @@ package main import ( - "bytes" - "context" - "encoding/json" "fmt" - "io" - "mime/multipart" - "net/http" "os" - "strings" - "syscall" - "time" - "git.t-juice.club/torjus/gpaste/api" - "github.com/google/uuid" + "git.t-juice.club/torjus/gpaste/cmd/client/actions" "github.com/urfave/cli/v2" - "golang.org/x/term" ) var ( @@ -46,13 +35,13 @@ func main() { Name: "upload", Usage: "Upload file(s)", ArgsUsage: "FILE [FILE]...", - Action: ActionUpload, + Action: actions.ActionUpload, }, { Name: "login", Usage: "Login to gpaste server", ArgsUsage: "USERNAME", - Action: ActionLogin, + Action: actions.ActionLogin, }, { Name: "admin", @@ -62,7 +51,7 @@ func main() { Name: "create-user", Usage: "Create a new user", ArgsUsage: "USERNAME", - Action: ActionUserCreate, + Action: actions.ActionUserCreate, }, }, }, @@ -71,171 +60,3 @@ func main() { app.Run(os.Args) } - -func ActionUpload(c *cli.Context) error { - url := fmt.Sprintf("%s/api/file", c.String("url")) - client := &http.Client{} - // TODO: Change timeout - ctx, cancel := context.WithTimeout(c.Context, 10*time.Minute) - defer cancel() - - buf := &bytes.Buffer{} - mw := multipart.NewWriter(buf) - - for _, arg := range c.Args().Slice() { - f, err := os.Open(arg) - if err != nil { - return err - } - defer f.Close() - fw, err := mw.CreateFormFile(uuid.Must(uuid.NewRandom()).String(), arg) - if err != nil { - return err - } - if _, err := io.Copy(fw, f); err != nil { - return err - } - } - mw.Close() - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, buf) - if err != nil { - return err - } - req.Header.Add("Content-Type", mw.FormDataContentType()) - - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - var expectedResp []struct { - Message string `json:"message"` - ID string `json:"id"` - URL string `json:"url"` - } - - decoder := json.NewDecoder(resp.Body) - if err := decoder.Decode(&expectedResp); err != nil { - return fmt.Errorf("error decoding response: %w", err) - } - - for _, r := range expectedResp { - fmt.Printf("Uploaded file %s\n", r.ID) - } - return nil -} - -func ActionLogin(c *cli.Context) error { - username := c.Args().First() - if username == "" { - return cli.Exit("USERNAME not supplied.", 1) - } - password, err := readPassword() - if err != nil { - return fmt.Errorf("error reading password: %w", err) - } - - url := fmt.Sprintf("%s/api/login", c.String("url")) - client := &http.Client{} - // TODO: Change timeout - ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) - defer cancel() - - body := new(bytes.Buffer) - requestData := struct { - Username string `json:"username"` - Password string `json:"password"` - }{ - Username: username, - Password: password, - } - encoder := json.NewEncoder(body) - if err := encoder.Encode(&requestData); err != nil { - return fmt.Errorf("error encoding response: %w", err) - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) - if err != nil { - return fmt.Errorf("error creating request: %w", err) - } - - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("unable to perform request: %s", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return cli.Exit("got non-ok response from server", 0) - } - - responseData := struct { - Token string `json:"token"` - }{} - - decoder := json.NewDecoder(resp.Body) - if err := decoder.Decode(&responseData); err != nil { - return fmt.Errorf("unable to parse response: %s", err) - } - - fmt.Printf("Token: %s", responseData.Token) - - return nil -} - -func ActionUserCreate(c *cli.Context) error { - // TODO: Needs to supply auth token to actually work - username := c.Args().First() - if username == "" { - return cli.Exit("USERNAME not supplied.", 1) - } - password, err := readPassword() - if err != nil { - return fmt.Errorf("error reading password: %w", err) - } - - url := fmt.Sprintf("%s/api/user", c.String("url")) - client := &http.Client{} - // TODO: Change timeout - ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) - defer cancel() - - body := new(bytes.Buffer) - requestData := &api.RequestAPIUserCreate{ - Username: username, - Password: password, - } - encoder := json.NewEncoder(body) - if err := encoder.Encode(requestData); err != nil { - return fmt.Errorf("error encoding response: %w", err) - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) - if err != nil { - return fmt.Errorf("error creating request: %w", err) - } - - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("unable to perform request: %s", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusAccepted { - return cli.Exit("got non-ok response from server", 0) - } - - fmt.Printf("Created user %s\n", username) - - return nil -} - -func readPassword() (string, error) { - fmt.Print("Enter Password: ") - bytePassword, err := term.ReadPassword(int(syscall.Stdin)) - if err != nil { - return "", err - } - - password := string(bytePassword) - return strings.TrimSpace(password), nil -} diff --git a/cmd/server/actions/actions.go b/cmd/server/actions/actions.go new file mode 100644 index 0000000..e538605 --- /dev/null +++ b/cmd/server/actions/actions.go @@ -0,0 +1,105 @@ +package actions + +import ( + "context" + "net/http" + "os" + "os/signal" + "strings" + "time" + + "git.t-juice.club/torjus/gpaste" + "git.t-juice.club/torjus/gpaste/api" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func ActionServe(c *cli.Context) error { + configPath := "gpaste-server.toml" + if c.IsSet("config") { + configPath = c.String("config") + } + + f, err := os.Open(configPath) + if err != nil { + return cli.Exit(err, 1) + } + defer f.Close() + cfg, err := gpaste.ServerConfigFromReader(f) + if err != nil { + return cli.Exit(err, 1) + } + // Setup loggers + rootLogger := getRootLogger(cfg.LogLevel) + serverLogger := rootLogger.Named("SERV") + accessLogger := rootLogger.Named("ACCS") + + // Setup contexts for clean shutdown + rootCtx, rootCancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer rootCancel() + httpCtx, httpCancel := context.WithCancel(rootCtx) + defer httpCancel() + httpShutdownCtx, httpShutdownCancel := context.WithCancel(context.Background()) + defer httpShutdownCancel() + + go func() { + srv := api.NewHTTPServer(cfg) + srv.Addr = cfg.ListenAddr + srv.Logger = serverLogger + srv.AccessLogger = accessLogger + + // Wait for cancel + go func() { + <-httpCtx.Done() + timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + srv.Shutdown(timeoutCtx) + }() + serverLogger.Infow("Starting HTTP server.", "addr", cfg.ListenAddr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + serverLogger.Errorw("Error during shutdown.", "error", err) + } + serverLogger.Infow("HTTP server shutdown complete.", "addr", cfg.ListenAddr) + httpShutdownCancel() + }() + <-httpShutdownCtx.Done() + + return nil +} + +func getRootLogger(level string) *zap.SugaredLogger { + logEncoderConfig := zap.NewProductionEncoderConfig() + logEncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder + logEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + logEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + logEncoderConfig.EncodeDuration = zapcore.StringDurationEncoder + + rootLoggerConfig := &zap.Config{ + Level: zap.NewAtomicLevelAt(zap.DebugLevel), + OutputPaths: []string{"stdout"}, + ErrorOutputPaths: []string{"stdout"}, + Encoding: "console", + EncoderConfig: logEncoderConfig, + DisableCaller: true, + } + + switch strings.ToUpper(level) { + case "DEBUG": + rootLoggerConfig.DisableCaller = false + rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) + case "INFO": + rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) + case "WARN", "WARNING": + rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.WarnLevel) + case "ERR", "ERROR": + rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) + } + + rootLogger, err := rootLoggerConfig.Build() + if err != nil { + panic(err) + } + + return rootLogger.Sugar() +} diff --git a/cmd/server/server.go b/cmd/server/server.go index 2ba9aac..d4d5c73 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -1,19 +1,11 @@ package main import ( - "context" "fmt" - "net/http" "os" - "os/signal" - "strings" - "time" - "git.t-juice.club/torjus/gpaste" - "git.t-juice.club/torjus/gpaste/api" + "git.t-juice.club/torjus/gpaste/cmd/server/actions" "github.com/urfave/cli/v2" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" ) var ( @@ -34,97 +26,8 @@ func main() { Usage: "Path to config-file.", }, }, - Action: ActionServe, + Action: actions.ActionServe, } app.Run(os.Args) } - -func ActionServe(c *cli.Context) error { - configPath := "gpaste-server.toml" - if c.IsSet("config") { - configPath = c.String("config") - } - - f, err := os.Open(configPath) - if err != nil { - return cli.Exit(err, 1) - } - defer f.Close() - cfg, err := gpaste.ServerConfigFromReader(f) - if err != nil { - return cli.Exit(err, 1) - } - // Setup loggers - rootLogger := getRootLogger(cfg.LogLevel) - serverLogger := rootLogger.Named("SERV") - accessLogger := rootLogger.Named("ACCS") - - // Setup contexts for clean shutdown - rootCtx, rootCancel := signal.NotifyContext(context.Background(), os.Interrupt) - defer rootCancel() - httpCtx, httpCancel := context.WithCancel(rootCtx) - defer httpCancel() - httpShutdownCtx, httpShutdownCancel := context.WithCancel(context.Background()) - defer httpShutdownCancel() - - go func() { - srv := api.NewHTTPServer(cfg) - srv.Addr = cfg.ListenAddr - srv.Logger = serverLogger - srv.AccessLogger = accessLogger - - // Wait for cancel - go func() { - <-httpCtx.Done() - timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - srv.Shutdown(timeoutCtx) - }() - serverLogger.Infow("Starting HTTP server.", "addr", cfg.ListenAddr) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - serverLogger.Errorw("Error during shutdown.", "error", err) - } - serverLogger.Infow("HTTP server shutdown complete.", "addr", cfg.ListenAddr) - httpShutdownCancel() - }() - <-httpShutdownCtx.Done() - - return nil -} - -func getRootLogger(level string) *zap.SugaredLogger { - logEncoderConfig := zap.NewProductionEncoderConfig() - logEncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder - logEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder - logEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - logEncoderConfig.EncodeDuration = zapcore.StringDurationEncoder - - rootLoggerConfig := &zap.Config{ - Level: zap.NewAtomicLevelAt(zap.DebugLevel), - OutputPaths: []string{"stdout"}, - ErrorOutputPaths: []string{"stdout"}, - Encoding: "console", - EncoderConfig: logEncoderConfig, - DisableCaller: true, - } - - switch strings.ToUpper(level) { - case "DEBUG": - rootLoggerConfig.DisableCaller = false - rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) - case "INFO": - rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) - case "WARN", "WARNING": - rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.WarnLevel) - case "ERR", "ERROR": - rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) - } - - rootLogger, err := rootLoggerConfig.Build() - if err != nil { - panic(err) - } - - return rootLogger.Sugar() -}