diff --git a/.gitignore b/.gitignore index 8db24be..527e3bb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,8 @@ main.db debug *db.lock -/config.json +/config.* /handlers/static.go /handlers/tmpls/tmpls.go -/releases \ No newline at end of file +/releases +/data \ No newline at end of file diff --git a/Makefile b/Makefile index d85fa26..a4f49d3 100644 --- a/Makefile +++ b/Makefile @@ -23,5 +23,5 @@ getGoDependencies: buildProject: @mkdir releases gox -output="releases/{{.Dir}}_{{.OS}}_{{.Arch}}/{{.Dir}}" - find releases -maxdepth 1 -mindepth 1 -type d -exec cp build/config.json {} \; + find releases -maxdepth 1 -mindepth 1 -type d -exec cp build/config.yaml {} \; find releases -maxdepth 1 -mindepth 1 -type d -exec tar -cvjf {}.tar.bz2 {} \; diff --git a/build/config.json b/build/config.json deleted file mode 100644 index 26590ad..0000000 --- a/build/config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Store": { - "DBPath": "main.db", - "ShortedIDLength": 4 - }, - "Handlers": { - "ListenAddr": ":8080", - "BaseURL": "http://localhost:3000", - "EnableGinDebugMode": false, - "OAuth": { - "Google": { - "ClientID": "", - "ClientSecret": "" - } - } - } -} \ No newline at end of file diff --git a/build/config.yaml b/build/config.yaml new file mode 100644 index 0000000..20bc995 --- /dev/null +++ b/build/config.yaml @@ -0,0 +1,11 @@ +http: + ListenAddr: ':8080' + BaseURL: 'http://localhost:3000' +General: + DBPath: main.db + EnableDebugMode: true + ShortedIDLength: 4 +oAuth: + Google: + ClientID: replace me + ClientSecret: replace me diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 38960cb..0000000 --- a/config/config.go +++ /dev/null @@ -1,92 +0,0 @@ -package config - -import ( - "encoding/json" - "io/ioutil" - "os" - "path/filepath" - - "github.com/pkg/errors" -) - -// Configuration holds all the needed parameters use -// the URL Shortener -type Configuration struct { - Store Store - Handlers Handlers -} - -// Store contains the needed fields for the Store package -type Store struct { - DBPath string - ShortedIDLength uint -} - -// Handlers contains the needed fields for the Handlers package -type Handlers struct { - ListenAddr string - BaseURL string - EnableDebugMode bool - Secret []byte - OAuth struct { - Google struct { - ClientID string - ClientSecret string - } - } -} - -var ( - config *Configuration - configPath string -) - -// Get returns the configuration from a given file -func Get() *Configuration { - return config -} - -// Preload loads the configuration file into the memory for further usage -func Preload() error { - var err error - configPath, err = getConfigPath() - if err != nil { - return errors.Wrap(err, "could not get configuration path") - } - if err = updateConfig(); err != nil { - return errors.Wrap(err, "could not update config") - } - return nil -} - -func updateConfig() error { - file, err := ioutil.ReadFile(configPath) - if err != nil { - return errors.Wrap(err, "could not read configuration file") - } - if err = json.Unmarshal(file, &config); err != nil { - return errors.Wrap(err, "could not unmarshal configuration file") - } - return nil -} - -func getConfigPath() (string, error) { - ex, err := os.Executable() - if err != nil { - return "", errors.Wrap(err, "could not get executable path") - } - return filepath.Join(filepath.Dir(ex), "config.json"), nil -} - -// Set replaces the current configuration with the given one -func Set(conf *Configuration) error { - data, err := json.MarshalIndent(conf, "", " ") - if err != nil { - return err - } - if err = ioutil.WriteFile(configPath, data, 0644); err != nil { - return err - } - config = conf - return nil -} diff --git a/handlers/auth.go b/handlers/auth.go index 3688fe1..d5a6937 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -7,6 +7,9 @@ import ( "net/http" "time" + "github.com/maxibanki/golang-url-shortener/util" + "github.com/spf13/viper" + jwt "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" @@ -38,15 +41,15 @@ type checkResponse struct { func (h *Handler) initOAuth() { h.oAuthConf = &oauth2.Config{ - ClientID: h.config.OAuth.Google.ClientID, - ClientSecret: h.config.OAuth.Google.ClientSecret, - RedirectURL: h.config.BaseURL + "/api/v1/callback", + ClientID: viper.GetString("oAuth.Google.ClientID"), + ClientSecret: viper.GetString("oAuth.Google.ClientSecret"), + RedirectURL: viper.GetString("http.BaseURL") + "/api/v1/callback", Scopes: []string{ "https://www.googleapis.com/auth/userinfo.email", }, Endpoint: google.Endpoint, } - h.engine.Use(sessions.Sessions("backend", sessions.NewCookieStore(h.config.Secret))) + h.engine.Use(sessions.Sessions("backend", sessions.NewCookieStore(util.GetPrivateKey()))) h.engine.GET("/api/v1/login", h.handleGoogleRedirect) h.engine.GET("/api/v1/callback", h.handleGoogleCallback) h.engine.POST("/api/v1/check", h.handleGoogleCheck) @@ -67,7 +70,7 @@ func (h *Handler) authMiddleware(c *gin.Context) { return errors.New("'Authorization' header not set") } token, err := jwt.ParseWithClaims(authHeader, &jwtClaims{}, func(token *jwt.Token) (interface{}, error) { - return h.config.Secret, nil + return util.GetPrivateKey(), nil }) if err != nil { return fmt.Errorf("could not parse token: %v", err) @@ -79,7 +82,7 @@ func (h *Handler) authMiddleware(c *gin.Context) { return nil }() if authError != nil { - if h.config.EnableDebugMode { + if viper.GetBool("General.EnableDebugMode") { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": fmt.Sprintf("token is not valid: %v", authError), }) @@ -103,7 +106,7 @@ func (h *Handler) handleGoogleCheck(c *gin.Context) { return } token, err := jwt.ParseWithClaims(data.Token, &jwtClaims{}, func(token *jwt.Token) (interface{}, error) { - return h.config.Secret, nil + return util.GetPrivateKey(), nil }) if claims, ok := token.Claims.(*jwtClaims); ok && token.Valid { c.JSON(http.StatusOK, checkResponse{ @@ -159,7 +162,7 @@ func (h *Handler) handleGoogleCallback(c *gin.Context) { user.Picture, }) - tokenString, err := token.SignedString(h.config.Secret) + tokenString, err := token.SignedString(util.GetPrivateKey()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not sign token: %v", err)}) return diff --git a/handlers/auth_test.go b/handlers/auth_test.go index ee4ef82..640f861 100644 --- a/handlers/auth_test.go +++ b/handlers/auth_test.go @@ -6,16 +6,16 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" "strings" "testing" "time" jwt "github.com/dgrijalva/jwt-go" - "github.com/maxibanki/golang-url-shortener/config" "github.com/maxibanki/golang-url-shortener/store" + "github.com/maxibanki/golang-url-shortener/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/spf13/viper" "golang.org/x/oauth2/google" ) @@ -24,7 +24,7 @@ const ( ) var ( - secret = []byte("our really great secret") + secret []byte server *httptest.Server closeServer func() error handler *Handler @@ -41,18 +41,22 @@ var ( ) func TestCreateBackend(t *testing.T) { - store, err := store.New(config.Store{ - DBPath: testingDBName, - ShortedIDLength: 4, - }, logrus.New()) + secret = util.GetPrivateKey() + viper.SetConfigName("config") + viper.AddConfigPath("../") + util.SetConfigDefaults() + err := viper.ReadInConfig() + if err != nil { + t.Fatalf("could not reload config file: %v", err) + } + if err := util.CheckForDatadir(); err != nil { + t.Fatalf("could not reload config file: %v", err) + } + store, err := store.New(logrus.New()) if err != nil { t.Fatalf("could not create store: %v", err) } - handler, err := New(config.Handlers{ - ListenAddr: ":8080", - Secret: secret, - BaseURL: "http://127.0.0.1", - }, *store, logrus.New(), true) + handler, err := New(*store, logrus.New(), true) if err != nil { t.Fatalf("could not create handler: %v", err) } @@ -62,9 +66,6 @@ func TestCreateBackend(t *testing.T) { if err := handler.CloseStore(); err != nil { return errors.Wrap(err, "could not close store") } - if err := os.Remove(testingDBName); err != nil { - return errors.Wrap(err, "could not remove testing db") - } return nil } } diff --git a/handlers/handlers.go b/handlers/handlers.go index c446817..7243969 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -2,17 +2,17 @@ package handlers import ( - "crypto/rand" "html/template" "net/http" "time" "github.com/gin-gonic/contrib/ginrus" + "github.com/spf13/viper" "github.com/gin-gonic/gin" - "github.com/maxibanki/golang-url-shortener/config" "github.com/maxibanki/golang-url-shortener/handlers/tmpls" "github.com/maxibanki/golang-url-shortener/store" + "github.com/maxibanki/golang-url-shortener/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/oauth2" @@ -21,21 +21,18 @@ import ( // Handler holds the funcs and attributes for the // http communication type Handler struct { - config config.Handlers - store store.Store - engine *gin.Engine - oAuthConf *oauth2.Config - log *logrus.Logger - DoNotCheckConfigViaGet bool // DoNotCheckConfigViaGet is for the unit testing usage + store store.Store + engine *gin.Engine + oAuthConf *oauth2.Config + log *logrus.Logger } // New initializes the http handlers -func New(handlerConfig config.Handlers, store store.Store, log *logrus.Logger, testing bool) (*Handler, error) { - if !handlerConfig.EnableDebugMode { +func New(store store.Store, log *logrus.Logger, testing bool) (*Handler, error) { + if !viper.GetBool("General.EnableDebugMode") { gin.SetMode(gin.ReleaseMode) } h := &Handler{ - config: handlerConfig, store: store, log: log, engine: gin.New(), @@ -44,8 +41,8 @@ func New(handlerConfig config.Handlers, store store.Store, log *logrus.Logger, t return nil, errors.Wrap(err, "could not set handlers") } if !testing { - if err := h.checkIfSecretExist(); err != nil { - return nil, errors.Wrap(err, "could not check if secret exist") + if err := util.CheckForPrivateKey(); err != nil { + return nil, errors.Wrap(err, "could not check for privat key") } } h.initOAuth() @@ -65,23 +62,6 @@ func (h *Handler) setTemplateFromFS(name string) error { return nil } -func (h *Handler) checkIfSecretExist() error { - if !h.DoNotCheckConfigViaGet { - conf := config.Get() - if conf.Handlers.Secret == nil { - b := make([]byte, 128) - if _, err := rand.Read(b); err != nil { - return err - } - conf.Handlers.Secret = b - if err := config.Set(conf); err != nil { - return err - } - } - } - return nil -} - func (h *Handler) setHandlers() error { if err := h.setTemplateFromFS("token.tmpl"); err != nil { return errors.Wrap(err, "could not set template from FS") @@ -98,7 +78,7 @@ func (h *Handler) setHandlers() error { // Listen starts the http server func (h *Handler) Listen() error { - return h.engine.Run(h.config.ListenAddr) + return h.engine.Run(viper.GetString("http.ListenAddr")) } // CloseStore stops the http server and the closes the db gracefully diff --git a/main.go b/main.go index 9f88473..e6a4248 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,11 @@ import ( "github.com/shiena/ansicolor" "github.com/sirupsen/logrus" + "github.com/spf13/viper" - "github.com/maxibanki/golang-url-shortener/config" "github.com/maxibanki/golang-url-shortener/handlers" "github.com/maxibanki/golang-url-shortener/store" + "github.com/maxibanki/golang-url-shortener/util" "github.com/pkg/errors" ) @@ -31,18 +32,17 @@ func main() { } func initShortener(log *logrus.Logger) (func(), error) { - if err := config.Preload(); err != nil { - return nil, errors.Wrap(err, "could not get config") + if err := util.ReadInConfig(); err != nil { + return nil, errors.Wrap(err, "could not reload config file") } - conf := config.Get() - if conf.Handlers.EnableDebugMode { + if viper.GetBool("General.EnableDebugMode") { log.SetLevel(logrus.DebugLevel) } - store, err := store.New(conf.Store, log) + store, err := store.New(log) if err != nil { return nil, errors.Wrap(err, "could not create store") } - handler, err := handlers.New(conf.Handlers, *store, log, false) + handler, err := handlers.New(*store, log, false) if err != nil { return nil, errors.Wrap(err, "could not create handlers") } diff --git a/store/store.go b/store/store.go index aaa6fce..1ca23bd 100644 --- a/store/store.go +++ b/store/store.go @@ -3,13 +3,15 @@ package store import ( "encoding/json" + "path/filepath" "time" + "github.com/maxibanki/golang-url-shortener/util" "github.com/sirupsen/logrus" + "github.com/spf13/viper" "github.com/asaskevich/govalidator" "github.com/boltdb/bolt" - "github.com/maxibanki/golang-url-shortener/config" "github.com/pkg/errors" ) @@ -17,7 +19,7 @@ import ( type Store struct { db *bolt.DB bucketName []byte - idLength uint + idLength int log *logrus.Logger } @@ -47,8 +49,8 @@ var ErrGeneratingIDFailed = errors.New("could not generate unique id, all ten tr var ErrIDIsEmpty = errors.New("the given ID is empty") // New initializes the store with the db -func New(storeConfig config.Store, log *logrus.Logger) (*Store, error) { - db, err := bolt.Open(storeConfig.DBPath, 0644, &bolt.Options{Timeout: 1 * time.Second}) +func New(log *logrus.Logger) (*Store, error) { + db, err := bolt.Open(filepath.Join(util.GetDataDir(), "main.db"), 0644, &bolt.Options{Timeout: 1 * time.Second}) if err != nil { return nil, errors.Wrap(err, "could not open bolt DB database") } @@ -62,7 +64,7 @@ func New(storeConfig config.Store, log *logrus.Logger) (*Store, error) { } return &Store{ db: db, - idLength: storeConfig.ShortedIDLength, + idLength: viper.GetInt("General.ShortedIDLength"), bucketName: bucketName, log: log, }, nil diff --git a/store/store_test.go b/store/store_test.go index 0ad1e9a..c398b83 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -2,27 +2,22 @@ package store import ( "os" - "strings" "testing" "github.com/sirupsen/logrus" - - "github.com/maxibanki/golang-url-shortener/config" + "github.com/spf13/viper" ) const ( testingDBName = "test.db" ) -var validConfig = config.Store{ - DBPath: testingDBName, - ShortedIDLength: 4, -} - func TestGenerateRandomString(t *testing.T) { + viper.SetDefault("General.DataDir", "data") + viper.SetDefault("General.ShortedIDLength", 4) tt := []struct { name string - length uint + length int }{ {"fourtytwo long", 42}, {"sixteen long", 16}, @@ -45,14 +40,8 @@ func TestGenerateRandomString(t *testing.T) { } func TestNewStore(t *testing.T) { - t.Run("create store without file name provided", func(r *testing.T) { - _, err := New(config.Store{}, logrus.New()) - if !strings.Contains(err.Error(), "could not open bolt DB database") { - t.Fatalf("unexpected error: %v", err) - } - }) t.Run("create store with correct arguments", func(r *testing.T) { - store, err := New(validConfig, logrus.New()) + store, err := New(logrus.New()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -61,10 +50,7 @@ func TestNewStore(t *testing.T) { } func TestCreateEntry(t *testing.T) { - store, err := New(config.Store{ - DBPath: testingDBName, - ShortedIDLength: 1, - }, logrus.New()) + store, err := New(logrus.New()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -86,7 +72,7 @@ func TestCreateEntry(t *testing.T) { } func TestGetEntryByID(t *testing.T) { - store, err := New(validConfig, logrus.New()) + store, err := New(logrus.New()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -102,7 +88,7 @@ func TestGetEntryByID(t *testing.T) { } func TestIncreaseVisitCounter(t *testing.T) { - store, err := New(validConfig, logrus.New()) + store, err := New(logrus.New()) if err != nil { t.Fatalf("could not create store: %v", err) } diff --git a/store/util.go b/store/util.go index 060361c..2d00b33 100644 --- a/store/util.go +++ b/store/util.go @@ -48,7 +48,7 @@ func (s *Store) createEntry(entry Entry, givenID string) (string, error) { } // generateRandomString generates a random string with an predefined length -func generateRandomString(length uint) (string, error) { +func generateRandomString(length int) (string, error) { var result string for len(result) < int(length) { num, err := rand.Int(rand.Reader, big.NewInt(int64(127))) diff --git a/util/config.go b/util/config.go new file mode 100644 index 0000000..ad31e8a --- /dev/null +++ b/util/config.go @@ -0,0 +1,54 @@ +package util + +import ( + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +var dataDirPath string + +func ReadInConfig() error { + viper.AutomaticEnv() + viper.SetEnvPrefix("gus") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.SetConfigName("config") + viper.AddConfigPath(".") + SetConfigDefaults() + err := viper.ReadInConfig() + if err != nil { + return errors.Wrap(err, "could not reload config file") + } + return CheckForDatadir() +} + +func SetConfigDefaults() { + viper.SetDefault("http.ListenAddr", ":8080") + viper.SetDefault("http.BaseURL", "http://localhost:3000") + + viper.SetDefault("General.DataDir", "data") + viper.SetDefault("General.EnableDebugMode", true) + viper.SetDefault("General.ShortedIDLength", 4) +} + +func GetDataDir() string { + return dataDirPath +} + +func CheckForDatadir() error { + var err error + dataDirPath, err = filepath.Abs(viper.GetString("General.DataDir")) + if err != nil { + return errors.Wrap(err, "could not get relative data dir path") + } + if _, err = os.Stat(dataDirPath); os.IsNotExist(err) { + err = os.MkdirAll(dataDirPath, 0755) + if err != nil { + return errors.Wrap(err, "could not create config directory") + } + } + return nil +} diff --git a/util/private.go b/util/private.go new file mode 100644 index 0000000..0576f51 --- /dev/null +++ b/util/private.go @@ -0,0 +1,36 @@ +package util + +import ( + "crypto/rand" + "io/ioutil" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +var privateKey []byte + +func CheckForPrivateKey() error { + privateDat := filepath.Join(GetDataDir(), "private.dat") + d, err := ioutil.ReadFile(privateDat) + if err == nil { + privateKey = d + } else if os.IsNotExist(err) { + b := make([]byte, 256) + if _, err := rand.Read(b); err != nil { + return errors.Wrap(err, "could not read random bytes") + } + if err = ioutil.WriteFile(privateDat, b, 0644); err != nil { + return errors.Wrap(err, "could not write private key") + } + privateKey = b + } else if err != nil { + return errors.Wrap(err, "could not read private key") + } + return nil +} + +func GetPrivateKey() []byte { + return privateKey +}