package main import ( "context" "crypto/tls" "errors" "flag" "fmt" "io" "log/slog" "net/http" "os" "os/signal" "syscall" "time" rdb "git.runcible.io/learning/ratchet/internal/database" "git.runcible.io/learning/ratchet/internal/logging" "git.runcible.io/learning/ratchet/internal/model" "git.runcible.io/learning/ratchet/internal/server" "github.com/alexedwards/scs/sqlite3store" "github.com/alexedwards/scs/v2" _ "github.com/mattn/go-sqlite3" ) // var ( // version string // commit string // ) func run(ctx context.Context, w io.Writer, args []string) error { // CONFIGURATION // Parse command line options flags := flag.NewFlagSet(args[0], flag.ExitOnError) addr := flags.String("addr", "0.0.0.0", "HTTP network address") port := flags.String("port", "5001", "HTTP port") logLevel := flags.String("logging", "INFO", "Logging Level. Valid values [INFO, DEBUG, WARN, ERROR].") dbPath := flags.String("database", "./ratchet.db", "A path to a sqlite3 database") certPath := flags.String("cert", "./tls/cert.pem", "A public cert in .pem format") keyPath := flags.String("key", "./tls/key.pem", "A private key in .pem format") // must call parse or all values will be the defaults if err := flags.Parse(args[1:]); err != nil { return err } // DEPENDENCY INJECTION FOR HANDLERS // Setup Logging logger := logging.InitLogging(*logLevel, w, false) // Setup DB Connection Pool db, err := rdb.OpenSqlite3DB(*dbPath) if err != nil { return err } // Close db connection before exiting main. defer func() { slog.Info("Cleaning up database") _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)") if err != nil { slog.Error(fmt.Sprintf("Error checkpointing database: %v", err)) } db.Close() }() //tc, err := server.InitTemplateCache() tc, err := server.InitFSTemplateCache() if err != nil { return err } // SessionManager sm := scs.New() sm.Store = sqlite3store.New(db) // If you want to change the same sight cookie setting from Lax to Strict // will block the session cookie being sent by the user’s browser for all cross-site usage // including GET and HEAD. That means the cookie won't be sent when clicking on a link in another site // for a GET request, so the user won't be treated as being "logged in" to the app even if they // did in another tab // sm.Cookie.SameSite = http.SameSiteStrictMode app := server.NewRatchetApp(logger, tc, &model.SnippetService{DB: db}, &model.UserService{DB: db}, sm) // these two elliptic curves have assembly implementations tlsConfig := tls.Config{ CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, // example of cipher suites usage. These are ignored if TLS 1.3 is used // CipherSuites: []uint16{ // tls.TLS_AES_128_GCM_SHA256, // tls.TLS_AES_256_GCM_SHA384, // tls.TLS_CHACHA20_POLY1305_SHA256, // tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, // tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, // }, // example of explicitly setting TLS versions supported. // you do this for older browsers or for devices like micros that // have hardware limitations. // MinVersion: tls.VersionTLS10, // MaxVersion: tls.VersionTLS12, } // START SERVING REQUESTS slog.Info(fmt.Sprintf("Listening on http://%s:%s", *addr, *port)) //log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", *addr, *port), server)) // there is no log.Fatal equivalent. This is an approximation of the behavior srv := &http.Server{ Addr: fmt.Sprintf("%s:%s", *addr, *port), Handler: app.Routes(), ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), TLSConfig: &tlsConfig, // Server wide timeouts. // IdleTimeout by default go uses keep-alives on accepted connetions. // this lowers the default timeout. If you set read timeout but not idle // then the idle becomes the readtimeout. Be careful. IdleTimeout: time.Minute, // If the request headers or body are still being read 5 seconds after the // request is first accepted, then Go will close the underlying connection // Care close. User won't recieve any http response // See also http.Server also provides a ReadHeaderTimeout setting. Would allow // per route timeouts. ReadTimeout: 5 * time.Second, // The WriteTimeout setting will close the underlying connection if our server // attempts to write to the connection after a given period. When using TLS // it's sensible to set WriteTimeout above ReadTimeout // It’s important to bear in mind that writes made by a handler are buffered and // written to the connection as one when the handler returns. Therefore, the // idea of WriteTimeout is generally not to prevent long-running handlers, but // to prevent the data that the handler returns from taking too long to write. WriteTimeout: 10 * time.Second, // the maximum number of bytes the server will read when parsing request headers. // default is one mb. Exceeding this returns a 431 Request Header Fields Too Large response // btw go adds 4096 bytes to the number you provide. MaxHeaderBytes: 524288, // 0.5 mb } slog.Info(fmt.Sprintf("Listening on https://%s", srv.Addr)) go func() { if err = srv.ListenAndServeTLS(*certPath, *keyPath); !errors.Is(err, http.ErrServerClosed) { slog.Error(err.Error()) os.Exit(1) } slog.Info("Stopped serving connections") }() // Handle graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // Block until signal is recieved <-sigChan shutdownCtx, shutdownRelease := context.WithTimeout(ctx, 10*time.Second) defer shutdownRelease() if err := srv.Shutdown(shutdownCtx); err != nil { slog.Error("Failed to close within context timeout. Forcing server close.") srv.Close() return err } return nil } func main() { ctx := context.Background() if err := run(ctx, os.Stdout, os.Args); err != nil { slog.Error(err.Error()) os.Exit(1) } }