package server import ( "context" "crypto/rand" "crypto/sha1" "errors" "fmt" "net/http" "os" "os/signal" "path/filepath" "strconv" "syscall" "time" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" "github.com/ihexxa/gocfg" "github.com/natefinch/lumberjack" "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/crypto/bcrypt" "github.com/ihexxa/quickshare/src/cryptoutil/jwt" "github.com/ihexxa/quickshare/src/depidx" "github.com/ihexxa/quickshare/src/fileinfostore" "github.com/ihexxa/quickshare/src/fs" "github.com/ihexxa/quickshare/src/fs/local" "github.com/ihexxa/quickshare/src/handlers/fileshdr" "github.com/ihexxa/quickshare/src/handlers/multiusers" "github.com/ihexxa/quickshare/src/handlers/settings" "github.com/ihexxa/quickshare/src/idgen/simpleidgen" "github.com/ihexxa/quickshare/src/iolimiter" "github.com/ihexxa/quickshare/src/kvstore" "github.com/ihexxa/quickshare/src/kvstore/boltdbpvd" "github.com/ihexxa/quickshare/src/userstore" "github.com/ihexxa/quickshare/src/worker/localworker" ) type Server struct { server *http.Server cfg gocfg.ICfg deps *depidx.Deps signalChan chan os.Signal } func NewServer(cfg gocfg.ICfg) (*Server, error) { if !cfg.BoolOr("Server.Debug", false) { gin.SetMode(gin.ReleaseMode) } deps := initDeps(cfg) router := gin.Default() router, err := initHandlers(router, cfg, deps) if err != nil { return nil, fmt.Errorf("init handlers error: %w", err) } port := cfg.GrabInt("Server.Port") portStr, ok := cfg.String("ENV.PORT") if ok && portStr != "" { port, err = strconv.Atoi(portStr) if err != nil { panic(fmt.Sprintf("invalid port: %s", portStr)) } cfg.SetInt("Server.Port", port) } srv := &http.Server{ Addr: fmt.Sprintf("%s:%d", cfg.GrabString("Server.Host"), port), Handler: router, ReadTimeout: time.Duration(cfg.GrabInt("Server.ReadTimeout")) * time.Millisecond, WriteTimeout: time.Duration(cfg.GrabInt("Server.WriteTimeout")) * time.Millisecond, MaxHeaderBytes: cfg.GrabInt("Server.MaxHeaderBytes"), } return &Server{ server: srv, deps: deps, cfg: cfg, }, nil } func mkRoot(rootPath string) { info, err := os.Stat(rootPath) if err != nil { if os.IsNotExist(err) { err = os.MkdirAll(rootPath, 0760) if err != nil { panic(fmt.Sprintf("mk root path error: %s", err)) } } else { panic(fmt.Sprintf("stat root Path error: %s", err)) } } else if !info.IsDir() { panic(fmt.Sprintf("can not create %s folder: there is a file with same name", rootPath)) } } func initDeps(cfg gocfg.ICfg) *depidx.Deps { logger := initLogger(cfg) secret, ok := cfg.String("ENV.TOKENSECRET") if !ok { secret = makeRandToken() logger.Info("warning: TOKENSECRET is not given, using generated token") } rootPath := cfg.GrabString("Fs.Root") mkRoot(rootPath) opensLimit := cfg.GrabInt("Fs.OpensLimit") openTTL := cfg.GrabInt("Fs.OpenTTL") readerTTL := cfg.GrabInt("Server.WriteTimeout") / 1000 // millisecond -> second ider := simpleidgen.New() filesystem := local.NewLocalFS(rootPath, 0660, opensLimit, openTTL, readerTTL, ider) jwtEncDec := jwt.NewJWTEncDec(secret) kv := boltdbpvd.New(rootPath, 1024) users, err := userstore.NewKVUserStore(kv) if err != nil { panic(fmt.Sprintf("fail to init user store: %s", err)) } fileInfos, err := fileinfostore.NewFileInfoStore(kv) if err != nil { panic(fmt.Sprintf("fail to init file info store: %s", err)) } limiterCap := cfg.IntOr("Users.LimiterCapacity", 10000) limiterCyc := cfg.IntOr("Users.LimiterCyc", 1000) limiter := iolimiter.NewIOLimiter(limiterCap, limiterCyc, users) deps := depidx.NewDeps(cfg) deps.SetFS(filesystem) deps.SetToken(jwtEncDec) deps.SetKV(kv) deps.SetUsers(users) deps.SetFileInfos(fileInfos) deps.SetID(ider) deps.SetLog(logger) deps.SetLimiter(limiter) queueSize := cfg.GrabInt("Workers.QueueSize") sleepCyc := cfg.GrabInt("Workers.SleepCyc") workerCount := cfg.GrabInt("Workers.WorkerCount") workers := localworker.NewWorkerPool(queueSize, sleepCyc, workerCount, logger) workers.Start() deps.SetWorkers(workers) return deps } func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.Engine, error) { // handlers userHdrs, err := multiusers.NewMultiUsersSvc(cfg, deps) if err != nil { return nil, fmt.Errorf("new users svc error: %w", err) } if cfg.BoolOr("Users.EnableAuth", true) && !userHdrs.IsInited() { adminName, ok := cfg.String("ENV.DEFAULTADMIN") if !ok || adminName == "" { // only write to stdout deps.Log().Info("Please input admin name: ") fmt.Scanf("%s", &adminName) } adminPwd, _ := cfg.String("ENV.DEFAULTADMINPWD") if adminPwd == "" { adminPwd, err = generatePwd() if err != nil { return nil, fmt.Errorf("generate pwd error: %w", err) } // only write to stdout fmt.Printf("password is generated: %s, please update it after login\n", adminPwd) } pwdHash, err := bcrypt.GenerateFromPassword([]byte(adminPwd), 10) if err != nil { return nil, fmt.Errorf("generate pwd error: %w", err) } if _, err := userHdrs.Init(adminName, string(pwdHash)); err != nil { return nil, fmt.Errorf("init admin error: %w", err) } deps.Log().Infof("admin(%s) is created", adminName) } fileHdrs, err := fileshdr.NewFileHandlers(cfg, deps) if err != nil { return nil, fmt.Errorf("new files service error: %w", err) } settingsSvc, err := settings.NewSettingsSvc(cfg, deps) if err != nil { return nil, fmt.Errorf("new setting service error: %w", err) } publicPath, ok := cfg.String("Server.PublicPath") if !ok || publicPath == "" { return nil, errors.New("publicPath not found or empty") } // middleware router.Use(userHdrs.AuthN()) router.Use(userHdrs.APIAccessControl()) // tmp static server router.Use(static.Serve("/", static.LocalFile(publicPath, false))) // handler v1 := router.Group("/v1") usersAPI := v1.Group("/users") usersAPI.POST("/login", userHdrs.Login) usersAPI.POST("/logout", userHdrs.Logout) usersAPI.GET("/isauthed", userHdrs.IsAuthed) usersAPI.PATCH("/pwd", userHdrs.SetPwd) usersAPI.PATCH("/pwd/force-set", userHdrs.ForceSetPwd) usersAPI.POST("/", userHdrs.AddUser) usersAPI.DELETE("/", userHdrs.DelUser) usersAPI.GET("/list", userHdrs.ListUsers) usersAPI.GET("/self", userHdrs.Self) usersAPI.PATCH("/", userHdrs.SetUser) rolesAPI := v1.Group("/roles") rolesAPI.POST("/", userHdrs.AddRole) rolesAPI.DELETE("/", userHdrs.DelRole) rolesAPI.GET("/list", userHdrs.ListRoles) captchaAPI := v1.Group("/captchas") captchaAPI.GET("/", userHdrs.GetCaptchaID) captchaAPI.GET("/imgs", userHdrs.GetCaptchaImg) filesAPI := v1.Group("/fs") filesAPI.POST("/files", fileHdrs.Create) filesAPI.DELETE("/files", fileHdrs.Delete) filesAPI.GET("/files", fileHdrs.Download) filesAPI.PATCH("/files/chunks", fileHdrs.UploadChunk) filesAPI.GET("/files/chunks", fileHdrs.UploadStatus) filesAPI.PATCH("/files/copy", fileHdrs.Copy) filesAPI.PATCH("/files/move", fileHdrs.Move) filesAPI.GET("/dirs", fileHdrs.List) filesAPI.GET("/dirs/home", fileHdrs.ListHome) filesAPI.POST("/dirs", fileHdrs.Mkdir) // files.POST("/dirs/copy", fileHdrs.CopyDir) filesAPI.GET("/uploadings", fileHdrs.ListUploadings) filesAPI.DELETE("/uploadings", fileHdrs.DelUploading) filesAPI.POST("/sharings", fileHdrs.AddSharing) filesAPI.DELETE("/sharings", fileHdrs.DelSharing) filesAPI.GET("/sharings", fileHdrs.ListSharings) filesAPI.GET("/sharings/exist", fileHdrs.IsSharing) filesAPI.GET("/metadata", fileHdrs.Metadata) filesAPI.POST("/hashes/sha1", fileHdrs.GenerateHash) settingsAPI := v1.Group("/settings") settingsAPI.OPTIONS("/health", settingsSvc.Health) return router, nil } func initLogger(cfg gocfg.ICfg) *zap.SugaredLogger { fileWriter := zapcore.AddSync(&lumberjack.Logger{ Filename: filepath.Join(cfg.GrabString("Fs.Root"), "quickshare.log"), MaxSize: cfg.IntOr("Log.MaxSize", 50), // megabytes MaxBackups: cfg.IntOr("Log.MaxBackups", 2), MaxAge: cfg.IntOr("Log.MaxAge", 31), // days }) stdoutWriter := zapcore.AddSync(os.Stdout) multiWriter := zapcore.NewMultiWriteSyncer(fileWriter, stdoutWriter) gin.DefaultWriter = multiWriter core := zapcore.NewCore( zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), multiWriter, zap.InfoLevel, ) return zap.New(core).Sugar() } func (s *Server) Start() error { s.signalChan = make(chan os.Signal, 4) signal.Notify(s.signalChan, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-s.signalChan if sig != nil { s.deps.Log().Infow( fmt.Sprintf("received signal %s: shutting down", sig.String()), ) } s.Shutdown() }() s.deps.Log().Infow( "quickshare is starting", "hostname:port", fmt.Sprintf( "%s:%d", s.cfg.GrabString("Server.Host"), s.cfg.GrabInt("Server.Port"), ), ) err := s.server.ListenAndServe() if err != http.ErrServerClosed { return fmt.Errorf("listen error: %w", err) } return nil } func (s *Server) Shutdown() error { // TODO: add timeout s.deps.Workers().Stop() s.deps.FS().Close() s.deps.Log().Sync() return s.server.Shutdown(context.Background()) } func (s *Server) depsFS() fs.ISimpleFS { return s.deps.FS() } func (s *Server) depsKVStore() kvstore.IKVStore { return s.deps.KV() } func makeRandToken() string { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { panic(fmt.Sprintf("make rand token error: %s", err)) } return string(b) } func generatePwd() (string, error) { size := 10 buf := make([]byte, size) size, err := rand.Read(buf) if err != nil { return "", fmt.Errorf("generate pwd error: %w", err) } return fmt.Sprintf("%x", sha1.Sum(buf[:size]))[:6], nil }