diff --git a/internal/cmd/root.go b/internal/cmd/root.go deleted file mode 100644 index 44336e9..0000000 --- a/internal/cmd/root.go +++ /dev/null @@ -1,87 +0,0 @@ -package cmd - -import ( - "log" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "source.toby3d.me/website/indieauth/internal/domain" -) - -//nolint: gochecknoglobals -var ( - rootCmd = &cobra.Command{ - Use: "indieauth", - Short: "", - Long: "", - } - client = new(domain.Client) - config = new(domain.Config) -) - -//nolint: gochecknoglobals -var configPath string - -//nolint: gochecknoinits -func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&configPath, "config", filepath.Join(".", "config.yaml"), "config file") - viper.BindPFlag("port", startCmd.PersistentFlags().Lookup("port")) -} - -func Execute() { - if err := rootCmd.Execute(); err != nil { - log.Fatalln(err) - } -} - -func initConfig() { - viper.AddConfigPath(filepath.Join(".", "configs")) - viper.SetConfigName("config") - - if configPath != "" { - viper.SetConfigFile(configPath) - } - - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AutomaticEnv() - - var err error - if err = viper.ReadInConfig(); err != nil { - log.Fatalf("cannot load config from file %s: %v", viper.ConfigFileUsed(), err) - } - - if err = viper.Unmarshal(config); err != nil { - log.Fatalln("failed to read config:", err) - } - - // NOTE(toby3d): The server instance itself can be as a client. - rootURL := config.Server.GetRootURL() - client.Name = []string{config.Name} - - if client.ID, err = domain.NewClientID(rootURL); err != nil { - log.Fatalln("fail to read config:", err) - } - - url, err := domain.NewURL(rootURL) - if err != nil { - log.Fatalln("cannot parse root URL as client URL:", err) - } - - logo, err := domain.NewURL(rootURL + config.Server.StaticURLPrefix + "/icon.svg") - if err != nil { - log.Fatalln("cannot parse root URL as client URL:", err) - } - - redirectURI, err := domain.NewURL(rootURL + "/callback") - if err != nil { - log.Fatalln("cannot parse root URL as client URL:", err) - } - - client.URL = []*domain.URL{url} - client.Logo = []*domain.URL{logo} - client.RedirectURI = []*domain.URL{redirectURI} -} diff --git a/internal/cmd/start.go b/internal/cmd/start.go deleted file mode 100644 index 8110dfa..0000000 --- a/internal/cmd/start.go +++ /dev/null @@ -1,159 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "log" - "os" - "os/signal" - "path" - "runtime" - "runtime/pprof" - "syscall" - "time" - - "github.com/fasthttp/router" - "github.com/spf13/cobra" - http "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/pprofhandler" - "golang.org/x/text/language" - "golang.org/x/text/message" - - // bolt "go.etcd.io/bbolt". - clienthttpdelivery "source.toby3d.me/website/indieauth/internal/client/delivery/http" - healthhttpdelivery "source.toby3d.me/website/indieauth/internal/health/delivery/http" - metadatahttpdelivery "source.toby3d.me/website/indieauth/internal/metadata/delivery/http" - tickethttpdelivery "source.toby3d.me/website/indieauth/internal/ticket/delivery/http" - ticketrepo "source.toby3d.me/website/indieauth/internal/ticket/repository/http" - ticketucase "source.toby3d.me/website/indieauth/internal/ticket/usecase" -) - -const ( - DefaultCacheDuration time.Duration = 8760 * time.Hour // NOTE(toby3d): year - DefaultPort int = 3000 -) - -//nolint: gochecknoglobals -var startCmd = &cobra.Command{ - Use: "server", - Short: "start server", - Long: "", - Run: startServer, -} - -//nolint: gochecknoglobals -var ( - cpuProfilePath string - memProfilePath string - enablePprof bool -) - -//nolint: gochecknoinits -func init() { - rootCmd.AddCommand(startCmd) - startCmd.PersistentFlags().IntP("port", "p", DefaultPort, "port to run server on") - startCmd.PersistentFlags().BoolVar(&enablePprof, "pprof", false, "enable pprof mode") - startCmd.PersistentFlags().StringVar(&cpuProfilePath, "cpuprofile", "", "write cpu profile to file") - startCmd.PersistentFlags().StringVar(&memProfilePath, "memprofile", "", "write memory profile to file") -} - -func startServer(cmd *cobra.Command, args []string) { - /* - store, err := bolt.Open(config.Database.Path, os.ModePerm, nil) - if err != nil { - log.Fatalln("failed to open database connection:", err) - } - defer store.Close() - */ - - httpClient := &http.Client{ - Name: fmt.Sprintf("%s/0.1 (+%s)", config.Name, config.Server.GetAddress()), - } - r := router.New() - matcher := language.NewMatcher(message.DefaultCatalog.Languages()) - - r.ServeFilesCustom(path.Join(config.Server.StaticURLPrefix, "{filepath:*}"), &http.FS{ - Root: config.Server.StaticRootPath, - CacheDuration: DefaultCacheDuration, - AcceptByteRange: true, - Compress: true, - CompressBrotli: true, - GenerateIndexPages: true, - }) - healthhttpdelivery.NewRequestHandler().Register(r) - metadatahttpdelivery.NewRequestHandler(config).Register(r) - clienthttpdelivery.NewRequestHandler(config, client, matcher).Register(r) - /* - serverhttpdelivery.NewRequestHandler(serverhttpdelivery.Config{ - Config: config, - Clients: clientucase.NewClientUseCase(clientrepo.NewHTTPClientRepository(httpClient)), - Matcher: matcher, - }).Register(r) - */ - tickethttpdelivery.NewRequestHandler( - ticketucase.NewTicketUseCase(ticketrepo.NewHTTPTicketRepository(httpClient), httpClient), - ).Register(r) - - if enablePprof { - r.GET("/debug/pprof/{filepath:*}", pprofhandler.PprofHandler) - } - - server := &http.Server{ - CloseOnShutdown: true, - DisableKeepalive: true, - Handler: r.Handler, - Logger: log.New(os.Stdout, config.Name+"\t", log.Lmsgprefix|log.LstdFlags|log.LUTC), - Name: fmt.Sprintf("%s/0.1 (+%s)", config.Name, config.Server.GetAddress()), - } - - done := make(chan os.Signal, 1) - signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - - if cpuProfilePath != "" { - cpuProfile, err := os.Create(cpuProfilePath) - if err != nil { - log.Fatalln("could not create CPU profile:", err) - } - defer cpuProfile.Close() - - if err = pprof.StartCPUProfile(cpuProfile); err != nil { - log.Fatalln("could not start CPU profile:", err) - } - defer pprof.StopCPUProfile() - } - - go func() { - server.Logger.Printf( - "started at %s, available at %s", - config.Server.GetAddress(), - config.Server.GetRootURL(), - ) - - err := server.ListenAndServe(config.Server.GetAddress()) - if err != nil && !errors.Is(err, http.ErrConnectionClosed) { - log.Fatalln("cannot listen and serve:", err) - } - }() - - <-done - - if err := server.Shutdown(); err != nil { - log.Fatalln("failed shutdown of server:", err) - } - - if memProfilePath == "" { - return - } - - memProfile, err := os.Create(memProfilePath) - if err != nil { - log.Fatalln("could not create memory profile:", err) - } - defer memProfile.Close() - - runtime.GC() // NOTE(toby3d): get up-to-date statistics - - if err = pprof.WriteHeapProfile(memProfile); err != nil { - log.Fatalln("could not write memory profile:", err) - } -} diff --git a/internal/cmd/version.go b/internal/cmd/version.go deleted file mode 100644 index 7bff013..0000000 --- a/internal/cmd/version.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "fmt" - "runtime" - - "github.com/spf13/cobra" -) - -//nolint: gochecknoglobals -var versionCmd = &cobra.Command{ - Use: "version", - Short: "print version", - Long: "prints the build information", - Run: printVersion, -} - -//nolint: gochecknoinits -func init() { - rootCmd.AddCommand(versionCmd) -} - -func printVersion(cmd *cobra.Command, args []string) { - fmt.Println("IndieAuth version", runtime.Version(), runtime.GOOS) //nolint: forbidigo -} diff --git a/main.go b/main.go index df7ecaf..e6e382d 100644 --- a/main.go +++ b/main.go @@ -6,8 +6,256 @@ package main import ( _ "embed" + "errors" + "flag" + "fmt" + "log" + "os" + "os/signal" + "path" + "path/filepath" + "runtime" + "runtime/pprof" + "strings" + "sync" + "syscall" + "time" - "source.toby3d.me/website/indieauth/internal/cmd" + "github.com/fasthttp/router" + "github.com/jmoiron/sqlx" + "github.com/spf13/viper" + http "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/pprofhandler" + "golang.org/x/text/language" + "golang.org/x/text/message" + _ "modernc.org/sqlite" + + authhttpdelivery "source.toby3d.me/website/indieauth/internal/auth/delivery/http" + authucase "source.toby3d.me/website/indieauth/internal/auth/usecase" + clienthttpdelivery "source.toby3d.me/website/indieauth/internal/client/delivery/http" + clientrepo "source.toby3d.me/website/indieauth/internal/client/repository/http" + clientucase "source.toby3d.me/website/indieauth/internal/client/usecase" + "source.toby3d.me/website/indieauth/internal/domain" + healthhttpdelivery "source.toby3d.me/website/indieauth/internal/health/delivery/http" + metadatahttpdelivery "source.toby3d.me/website/indieauth/internal/metadata/delivery/http" + "source.toby3d.me/website/indieauth/internal/session" + sessionmemoryrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory" + sessionsqlite3repo "source.toby3d.me/website/indieauth/internal/session/repository/sqlite3" + "source.toby3d.me/website/indieauth/internal/ticket" + tickethttpdelivery "source.toby3d.me/website/indieauth/internal/ticket/delivery/http" + ticketmemoryrepo "source.toby3d.me/website/indieauth/internal/ticket/repository/memory" + ticketsqlite3repo "source.toby3d.me/website/indieauth/internal/ticket/repository/sqlite3" + ticketucase "source.toby3d.me/website/indieauth/internal/ticket/usecase" + "source.toby3d.me/website/indieauth/internal/token" + tokenhttpdelivery "source.toby3d.me/website/indieauth/internal/token/delivery/http" + tokenmemoryrepo "source.toby3d.me/website/indieauth/internal/token/repository/memory" + tokensqlite3repo "source.toby3d.me/website/indieauth/internal/token/repository/sqlite3" + tokenucase "source.toby3d.me/website/indieauth/internal/token/usecase" ) -func main() { cmd.Execute() } +const ( + DefaultCacheDuration time.Duration = 8760 * time.Hour // NOTE(toby3d): year + DefaultPort int = 3000 +) + +//nolint: gochecknoglobals +var ( + // NOTE(toby3d): write logs in stdout, see: https://12factor.net/logs + logger = log.New(os.Stdout, "IndieAuth\t", log.Lmsgprefix|log.LstdFlags|log.LUTC) + client = new(domain.Client) + config = new(domain.Config) + + configPath string + cpuProfilePath string + memProfilePath string + enablePprof bool +) + +//nolint: gochecknoinits +func init() { + flag.StringVar(&configPath, "config", filepath.Join(".", "config.yml"), "load specific config") + flag.BoolVar(&enablePprof, "pprof", false, "enable pprof mode") + flag.Parse() + + viper.AddConfigPath(filepath.Join(".", "configs")) + viper.SetConfigName("config") + + if configPath != "" { + viper.SetConfigFile(configPath) + } + + viper.SetEnvPrefix("INDIEAUTH_") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + viper.WatchConfig() + + var err error + if err = viper.ReadInConfig(); err != nil { + logger.Fatalf("cannot load config from file %s: %v", viper.ConfigFileUsed(), err) + } + + if err = viper.Unmarshal(&config); err != nil { + logger.Fatalln("failed to read config:", err) + } + + // NOTE(toby3d): The server instance itself can be as a client. + rootURL := config.Server.GetRootURL() + client.Name = []string{config.Name} + + if client.ID, err = domain.NewClientID(rootURL); err != nil { + logger.Fatalln("fail to read config:", err) + } + + url, err := domain.NewURL(rootURL) + if err != nil { + logger.Fatalln("cannot parse root URL as client URL:", err) + } + + logo, err := domain.NewURL(rootURL + config.Server.StaticURLPrefix + "/icon.svg") + if err != nil { + logger.Fatalln("cannot parse root URL as client URL:", err) + } + + redirectURI, err := domain.NewURL(rootURL + "/callback") + if err != nil { + logger.Fatalln("cannot parse root URL as client URL:", err) + } + + client.URL = []*domain.URL{url} + client.Logo = []*domain.URL{logo} + client.RedirectURI = []*domain.URL{redirectURI} +} + +//nolint: funlen +func main() { + var ( + tokens token.Repository + sessions session.Repository + tickets ticket.Repository + ) + + switch strings.ToLower(config.Database.Type) { + case "sqlite3": + store, err := sqlx.Open("sqlite", config.Database.Path) + if err != nil { + panic(err) + } + + if err = store.Ping(); err != nil { + logger.Fatalf("cannot ping %s database: %v", config.Database.Type, err) + } + + tokens = tokensqlite3repo.NewSQLite3TokenRepository(store) + sessions = sessionsqlite3repo.NewSQLite3SessionRepository(config, store) + tickets = ticketsqlite3repo.NewSQLite3TicketRepository(store, config) + case "memory": + store := new(sync.Map) + tokens = tokenmemoryrepo.NewMemoryTokenRepository(store) + sessions = sessionmemoryrepo.NewMemorySessionRepository(config, store) + tickets = ticketmemoryrepo.NewMemoryTicketRepository(store, config) + default: + log.Fatalln("unsupported database type, use 'memory' or 'sqlite3'") + } + + go sessions.GC() + + matcher := language.NewMatcher(message.DefaultCatalog.Languages()) + httpClient := &http.Client{ + Name: fmt.Sprintf("%s/0.1 (+%s)", config.Name, config.Server.GetAddress()), + MaxConnDuration: 10 * time.Second, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxConnWaitTimeout: 10 * time.Second, + } + ticketService := ticketucase.NewTicketUseCase(tickets, httpClient) + tokenService := tokenucase.NewTokenUseCase(tokens, sessions, config) + + r := router.New() + tickethttpdelivery.NewRequestHandler(ticketService, matcher, config).Register(r) + healthhttpdelivery.NewRequestHandler().Register(r) + metadatahttpdelivery.NewRequestHandler(config).Register(r) + tokenhttpdelivery.NewRequestHandler(tokenService).Register(r) + clienthttpdelivery.NewRequestHandler(clienthttpdelivery.NewRequestHandlerOptions{ + Client: client, + Config: config, + Matcher: matcher, + Tokens: tokenService, + }).Register(r) + authhttpdelivery.NewRequestHandler(authhttpdelivery.NewRequestHandlerOptions{ + Clients: clientucase.NewClientUseCase(clientrepo.NewHTTPClientRepository(httpClient)), + Auth: authucase.NewAuthUseCase(sessions, config), + Matcher: matcher, + Config: config, + }).Register(r) + r.ServeFilesCustom(path.Join(config.Server.StaticURLPrefix, "{filepath:*}"), &http.FS{ + Root: config.Server.StaticRootPath, + CacheDuration: DefaultCacheDuration, + AcceptByteRange: true, + Compress: true, + CompressBrotli: true, + GenerateIndexPages: true, + }) + + if enablePprof { + r.GET("/debug/pprof/{filepath:*}", pprofhandler.PprofHandler) + } + + server := &http.Server{ + Name: fmt.Sprintf("IndieAuth/0.1 (+%s)", config.Server.GetAddress()), + Handler: r.Handler, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + DisableKeepalive: true, + ReduceMemoryUsage: true, + SecureErrorLogMessage: true, + CloseOnShutdown: true, + } + + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) + + if cpuProfilePath != "" { + cpuProfile, err := os.Create(cpuProfilePath) + if err != nil { + logger.Fatalln("could not create CPU profile:", err) + } + defer cpuProfile.Close() + + if err = pprof.StartCPUProfile(cpuProfile); err != nil { + logger.Fatalln("could not start CPU profile:", err) + } + defer pprof.StopCPUProfile() + } + + go func() { + logger.Printf("started at %s, available at %s", config.Server.GetAddress(), + config.Server.GetRootURL()) + + err := server.ListenAndServe(config.Server.GetAddress()) + if err != nil && !errors.Is(err, http.ErrConnectionClosed) { + logger.Fatalln("cannot listen and serve:", err) + } + }() + + <-done + + if err := server.Shutdown(); err != nil { + logger.Fatalln("failed shutdown of server:", err) + } + + if memProfilePath == "" { + return + } + + memProfile, err := os.Create(memProfilePath) + if err != nil { + logger.Fatalln("could not create memory profile:", err) + } + defer memProfile.Close() + + runtime.GC() // NOTE(toby3d): get up-to-date statistics + + if err = pprof.WriteHeapProfile(memProfile); err != nil { + logger.Fatalln("could not write memory profile:", err) + } +}