|
|
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)
|
|
|
}
|
|
|
}
|