From 1f50b8621e18782e66a19cc93c198e2a7bf390a1 Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Sun, 13 Oct 2024 16:16:19 +0300 Subject: [PATCH] Move to Cobra for CLI --- Makefile | 2 +- auth/auth.go | 12 ++++---- auth/jwt.go | 6 ++++ cmd/find_user.go | 45 ++++++++++++++++++++++++++++ cmd/find_user/main.go | 29 ------------------ cmd/main.go | 68 ------------------------------------------- cmd/root.go | 54 ++++++++++++++++++++++++++++++++++ cmd/serve.go | 42 ++++++++++++++++++++++++++ cmd/serve/main.go | 33 --------------------- cmd/verify.go | 54 ++++++++++++++++++++++++++++++++++ cmd/verify/main.go | 17 ----------- go.mod | 6 ++++ go.sum | 10 +++++++ main.go | 2 +- storage/storage.go | 5 ++++ 15 files changed, 230 insertions(+), 155 deletions(-) create mode 100644 cmd/find_user.go delete mode 100644 cmd/find_user/main.go delete mode 100644 cmd/main.go create mode 100644 cmd/root.go create mode 100644 cmd/serve.go delete mode 100644 cmd/serve/main.go create mode 100644 cmd/verify.go delete mode 100644 cmd/verify/main.go diff --git a/Makefile b/Makefile index 5661dae..926f532 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ build: go build -serve: +run: go build && ./pye serve dev: diff --git a/auth/auth.go b/auth/auth.go index 76af146..0471b6e 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -19,21 +19,21 @@ func validPass(pass string) bool { } // Register creates a new user with credentials provided through Basic Auth -func Register(w http.ResponseWriter, r *http.Request, data storage.Storage) { +func Register(w http.ResponseWriter, r *http.Request) { email, password, ok := r.BasicAuth() if ok { email = strings.TrimSpace(email) password = strings.TrimSpace(password) - if !(validEmail(email) && validPass(password) && !data.EmailExists(email)) { + if !(validEmail(email) && validPass(password) && !storage.Data.EmailExists(email)) { slog.Debug("Outcome", "email", validEmail(email), "pass", validPass(password), - "taken", !data.EmailExists(email)) + "taken", !storage.Data.EmailExists(email)) http.Error(w, "invalid auth credentials", http.StatusBadRequest) return } - err := data.AddUser(email, password) + err := storage.Data.AddUser(email, password) if err != nil { slog.Error("error adding a new user", "error", err) http.Error(w, "error adding a new user", http.StatusInternalServerError) @@ -50,13 +50,13 @@ func Register(w http.ResponseWriter, r *http.Request, data storage.Storage) { } // Login returns JWT for a registered user through Basic Auth -func Login(w http.ResponseWriter, r *http.Request, data storage.Storage) { +func Login(w http.ResponseWriter, r *http.Request) { email, password, ok := r.BasicAuth() if ok { email = strings.TrimSpace(email) password = strings.TrimSpace(password) - user, ok := data.ByEmail(email) + user, ok := storage.Data.ByEmail(email) if !ok || !user.PasswordFits(password) { w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) http.Error(w, "you did something wrong", http.StatusUnauthorized) diff --git a/auth/jwt.go b/auth/jwt.go index 452aafe..1917720 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -101,3 +101,9 @@ func VerifyJWT(token string, publicKey []byte) (*jwt.Token, error) { }) return t, err } + +func VerifyLocalJWT(token string) (*jwt.Token, error) { + key_marshalled := x509.MarshalPKCS1PublicKey(&key.PublicKey) + block := pem.Block{Bytes: key_marshalled, Type: "RSA PUBLIC KEY"} + return VerifyJWT(token, pem.EncodeToMemory(&block)) +} \ No newline at end of file diff --git a/cmd/find_user.go b/cmd/find_user.go new file mode 100644 index 0000000..945705d --- /dev/null +++ b/cmd/find_user.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "fmt" + + "git.a71.su/Andrew71/pye/storage" + "github.com/spf13/cobra" +) + +var ( + findMethod string + findQuery string +) + +func init() { + rootCmd.AddCommand(findUserCmd) +} + +var findUserCmd = &cobra.Command{ + Use: "find ", + Short: "Find a user", + Long: `Find information about a user from their UUID or email`, + Args: cobra.ExactArgs(2), + Run: findUser, +} + +// TODO: Better name. +func findUser(cmd *cobra.Command, args []string) { + var user storage.User + var ok bool + if args[0] == "email" { + user, ok = storage.Data.ByEmail(args[1]) + } else if args[0] == "uuid" { + user, ok = storage.Data.ById(args[1]) + } else { + fmt.Println("expected email or uuid") + return + } + if !ok { + fmt.Println("User not found") + } else { + fmt.Printf("Information for user:\nuuid\t- %s\nemail\t- %s\nhash\t- %s\n", + user.Uuid, user.Email, user.Hash) + } +} diff --git a/cmd/find_user/main.go b/cmd/find_user/main.go deleted file mode 100644 index 0b3067a..0000000 --- a/cmd/find_user/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package find_user - -import ( - "fmt" - - "git.a71.su/Andrew71/pye/config" - "git.a71.su/Andrew71/pye/storage" - "git.a71.su/Andrew71/pye/storage/sqlite" -) - -func FindUser(mode, query string) { - data := sqlite.MustLoadSQLite(config.Cfg.SQLiteFile) - var user storage.User - var ok bool - if mode == "email" { - user, ok = data.ByEmail(query) - } else if mode == "uuid" { - user, ok = data.ById(query) - } else { - fmt.Println("expected email or uuid") - return - } - if !ok { - fmt.Println("User not found") - } else { - fmt.Printf("Information for user:\nuuid\t- %s\nemail\t- %s\nhash\t- %s\n", - user.Uuid, user.Email, user.Hash) - } -} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 9f3378a..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,68 +0,0 @@ -package cmd - -import ( - "flag" - "fmt" - "log/slog" - "os" - - "git.a71.su/Andrew71/pye/cmd/find_user" - "git.a71.su/Andrew71/pye/cmd/serve" - "git.a71.su/Andrew71/pye/cmd/verify" - "git.a71.su/Andrew71/pye/config" - "git.a71.su/Andrew71/pye/logging" -) - -func Run() { - - serveCmd := flag.NewFlagSet("serve", flag.ExitOnError) - serveConfig := serveCmd.String("config", "", "override config file") - servePort := serveCmd.Int("port", 0, "override port") - serveDb := serveCmd.String("db", "", "override sqlite database") - serveDebug := serveCmd.Bool("debug", false, "debug logging") - - verifyCmd := flag.NewFlagSet("verify", flag.ExitOnError) - verifyDebug := verifyCmd.Bool("debug", false, "debug logging") - - if len(os.Args) < 2 { - fmt.Println("expected 'serve' or 'verify' subcommands") - os.Exit(0) - } - - switch os.Args[1] { - case "serve": - serveCmd.Parse(os.Args[2:]) - logging.LogInit(*serveDebug) - if *serveConfig != "" { - err := config.LoadConfig(*serveConfig) - if err != nil { - slog.Error("error loading custom config", "error", err) - } - } - if *servePort != 0 { - config.Cfg.Port = *servePort - } - if *serveDb != "" { - config.Cfg.SQLiteFile = *serveDb - } - serve.Serve() - case "verify": - verifyCmd.Parse(os.Args[2:]) - logging.LogInit(*verifyDebug) - if len(os.Args) < 4 { - fmt.Println("Usage: [--debug]") - } else { - verify.Verify(os.Args[2], os.Args[3]) - } - case "user": - if len(os.Args) !=4 { - fmt.Println("Usage: ") - } else { - find_user.FindUser(os.Args[2], os.Args[3]) - } - - default: - fmt.Println("expected 'serve'/'verify'/'user' subcommands") - os.Exit(0) - } -} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..58a75c5 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + "log/slog" + "os" + + "git.a71.su/Andrew71/pye/config" + "git.a71.su/Andrew71/pye/logging" + "git.a71.su/Andrew71/pye/storage" + "git.a71.su/Andrew71/pye/storage/sqlite" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "pye", + Short: "Pye is a simple JWT system", + Long: `A bare-bones authentication system built by Andrew71 as an assignment`, +} + +var ( + cfgFile string + cfgDb string + debugMode *bool +) + +func initConfig() { + logging.LogInit(*debugMode) + if cfgFile != "" { + err := config.LoadConfig(cfgFile) + if err != nil { + slog.Error("error loading custom config", "error", err) + } + } + if cfgDb != "" { + config.Cfg.SQLiteFile = cfgDb + } + + storage.Data = sqlite.MustLoadSQLite(config.Cfg.SQLiteFile) +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.json", "config file") + rootCmd.PersistentFlags().StringVar(&cfgDb, "db", "", "database to use") + debugMode = rootCmd.PersistentFlags().BoolP("debug", "d", false, "enable debug mode") +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..7706c0d --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "log/slog" + "net/http" + "strconv" + + "git.a71.su/Andrew71/pye/auth" + "git.a71.su/Andrew71/pye/config" + "github.com/spf13/cobra" +) + +var port int + +func init() { + serveCmd.Flags().IntVarP(&port, "port", "p", config.Cfg.Port, "port to use") + rootCmd.AddCommand(serveCmd) +} + +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Start JWT service", + Long: `Start a simple authentication service`, + Run: serveAuth, +} + +func serveAuth(cmd *cobra.Command, args []string) { + router := http.NewServeMux() + + router.HandleFunc("GET /pem", auth.PublicKey) + + router.HandleFunc("POST /register", auth.Register) + router.HandleFunc("POST /login", auth.Login) + + // Note: likely temporary, possibly to be replaced by a fake "frontend" + router.HandleFunc("GET /register", auth.Register) + router.HandleFunc("GET /login", auth.Login) + + slog.Info("🪐 pye started", "port", port) + slog.Debug("debug mode active") + http.ListenAndServe(":"+strconv.Itoa(port), router) +} diff --git a/cmd/serve/main.go b/cmd/serve/main.go deleted file mode 100644 index 8f56ab2..0000000 --- a/cmd/serve/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package serve - -import ( - "log/slog" - "net/http" - "strconv" - - "git.a71.su/Andrew71/pye/auth" - "git.a71.su/Andrew71/pye/config" - "git.a71.su/Andrew71/pye/storage" - "git.a71.su/Andrew71/pye/storage/sqlite" -) - -var data storage.Storage - -func Serve() { - data = sqlite.MustLoadSQLite(config.Cfg.SQLiteFile) - - router := http.NewServeMux() - - router.HandleFunc("GET /pem", auth.PublicKey) - - router.HandleFunc("POST /register", func(w http.ResponseWriter, r *http.Request) { auth.Register(w, r, data) }) - router.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) { auth.Login(w, r, data) }) - - // Note: likely temporary, possibly to be replaced by a fake "frontend" - router.HandleFunc("GET /register", func(w http.ResponseWriter, r *http.Request) { auth.Register(w, r, data) }) - router.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { auth.Login(w, r, data) }) - - slog.Info("🪐 pye started", "port", config.Cfg.Port) - slog.Debug("debug mode active") - http.ListenAndServe(":"+strconv.Itoa(config.Cfg.Port), router) -} diff --git a/cmd/verify.go b/cmd/verify.go new file mode 100644 index 0000000..9b38c16 --- /dev/null +++ b/cmd/verify.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + "log/slog" + "os" + + "git.a71.su/Andrew71/pye/auth" + "github.com/golang-jwt/jwt/v5" + "github.com/spf13/cobra" +) + +var ( + verifyToken string + verifyFile string +) + +func init() { + verifyCmd.Flags().StringVarP(&verifyToken, "token", "t", "", "token to verify") + verifyCmd.MarkFlagRequired("token") + verifyCmd.Flags().StringVarP(&verifyFile, "file", "f", "", "file to use") + rootCmd.AddCommand(verifyCmd) +} + +var verifyCmd = &cobra.Command{ + Use: "verify", + Short: "Verify a JWT token", + Long: `Pass a JWT token and a path to PEM-encoded file with a public key + to verify whether it is legit.`, + Run: verifyFunc, +} + +// TODO: Better name. +func verifyFunc(cmd *cobra.Command, args []string) { + if verifyToken == "" { + fmt.Println("Empty token supplied!") + return + } + + var t *jwt.Token + var err error + if verifyFile == "" { + fmt.Println("No PEM file supplied, assuming local") + t, err = auth.VerifyLocalJWT(verifyToken) + } else { + key, err_k := os.ReadFile(verifyFile) + if err_k != nil { + slog.Error("error reading file", "error", err, "file", verifyFile) + return + } + t, err = auth.VerifyJWT(verifyToken, key) + } + slog.Info("result", "token", t, "error", err, "ok", err == nil) +} diff --git a/cmd/verify/main.go b/cmd/verify/main.go deleted file mode 100644 index e66e209..0000000 --- a/cmd/verify/main.go +++ /dev/null @@ -1,17 +0,0 @@ -package verify - -import ( - "log/slog" - "os" - - "git.a71.su/Andrew71/pye/auth" -) - -func Verify(token, filename string) { - key, err := os.ReadFile(filename) - if err != nil { - slog.Error("error reading file", "error", err, "file", filename) - } - t, err := auth.VerifyJWT(token, key) - slog.Info("result", "token", t, "error", err, "ok", err == nil) -} diff --git a/go.mod b/go.mod index f1a85ee..002bb97 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,11 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/mattn/go-sqlite3 v1.14.24 + github.com/spf13/cobra v1.8.1 golang.org/x/crypto v0.28.0 ) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum index 67dd0db..f8cf678 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,18 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 1d44e82..957ff80 100644 --- a/main.go +++ b/main.go @@ -3,5 +3,5 @@ package main import "git.a71.su/Andrew71/pye/cmd" func main() { - cmd.Run() + cmd.Execute() } diff --git a/storage/storage.go b/storage/storage.go index dcee5fe..ac974a0 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -1,8 +1,13 @@ package storage +// Storage is an arbitrary storage interface type Storage interface { AddUser(email, password string) error ById(uuid string) (User, bool) ByEmail(uuid string) (User, bool) EmailExists(email string) bool } + +// Data stores active information for the app +// It should be populated at app startup +var Data Storage