package serve import ( "context" "strings" // "encoding/base64" // "encoding/json" "fmt" "log/slog" "net/http" "os" "os/signal" "strconv" "syscall" "time" "gopkg.awl.red/lx/internal/auth" "gopkg.awl.red/lx/internal/config" "gopkg.awl.red/lx/internal/db" "gopkg.awl.red/lx/internal/h" "gopkg.awl.red/lx/internal/log" ) var msgBadFolder = "site only support folders 1 deep" var msgBadFolderB = []byte(msgBadFolder) var msg404 = "404 not found" var msg404B = []byte(msg404) func wBdTp(w http.ResponseWriter, r *http.Request, err error) { if err == nil { return } slog.Error("could not write route", "route", r.URL.Path, "err", err) w.WriteHeader(http.StatusInternalServerError) } func r___GET(path string, f http.Handler) { http.Handle("GET "+path, f) } func r__POST(path string, f http.Handler) { http.Handle("POST "+path, f) } func Serve() error { chAddrChange := make(chan config.AddrChange, 5) dsn := config.GetDsn() adsn := config.GetAdsn() slog.Debug("dsn", "dsn", dsn, "adsn", adsn) conn, err := db.New(dsn, adsn, chAddrChange) if err != nil { return err } cfg := conn.GetConfig() log.SetFromConfig(cfg) if cfg == nil { return err } r___GET("/favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(nil) })) r___GET("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" || r.URL.Path == "" { cookie, err := r.Cookie("Session") if err != nil { http.Redirect(w, r, "/_ui/login", http.StatusSeeOther) return } c := conn.GetConfig() session, err := conn.GetSession(cookie.Value, c) if err != nil || session == nil { http.Redirect(w, r, "/_ui/login", http.StatusSeeOther) return } http.Redirect(w, r, fmt.Sprintf("/_ui/pn/%d/businesses", session.PersonId), http.StatusSeeOther) } numSlash := strings.Count(r.URL.Path, "/") if numSlash > 1 { w.WriteHeader(http.StatusBadRequest) w.Write(msgBadFolderB) return } host, _, _ := strings.Cut(r.URL.Host, ":") // TODO: check if this works on real domains, and behind a proxy like nginx if host == "" { host = "localhost" } bh, err := conn.GetBackHalf(cfg, host, strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/")) if err != nil { w.WriteHeader(http.StatusNotFound) w.Write(msg404B) return } http.Redirect(w, r, bh.Redirect, http.StatusSeeOther) // TODO do analytics conn.InsertAnalytics(bh.Id, r) })) r___GET("/_ui/site_config", mkMiddlewareSession(conn)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session, hasSession := r.Context().Value("auth").(auth.Session) if !hasSession { w.WriteHeader(http.StatusForbidden) return } if _, isAdmin := session.Roles[auth.RoleAdmin]; !isAdmin { w.WriteHeader(http.StatusForbidden) return } wBdTp(w, r, h.TSiteConfigEdit(w, r.URL.Query(), conn.GetConfig())) }))) r___GET("/_ui/pricing", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wBdTp(w, r, h.TPricing(w, r.URL.Query(), conn.GetConfig())) })) r___GET("/_ui/signup", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wBdTp(w, r, h.TSignup(w, conn.GetConfig())) })) r___GET("/_ui/login", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wBdTp(w, r, h.TLogin(w, conn.GetConfig(), r.URL.Query())) })) r__POST("/login", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { slog.Error("could not parse form", "route", r.URL.Path, "err", err) w.WriteHeader(http.StatusBadRequest) return } f := r.Form username := f.Get("username") password := f.Get("password") email := f.Get("email") cfg := conn.GetConfig() token, session, err := conn.DoLogin(username, password, email, cfg) if err != nil { slog.Warn("could not log in", "err", err) http.Redirect(w, r, "/login?bad_login", http.StatusSeeOther) return } auth.SetSession(w.Header(), token, cfg) http.Redirect(w, r, fmt.Sprintf("/pn/%d/businesses", session.PersonId), http.StatusSeeOther) wBdTp(w, r, h.TLogin(w, conn.GetConfig(), r.URL.Query())) })) lggout := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("Session") if err == nil { err := conn.DeleteSession(cookie.Value) if err != nil { w.Write([]byte("could not log out")) return } } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Location", "/login?did_logout") w.Header().Set("Set-Cookie", auth.DelSessionCookie) w.WriteHeader(http.StatusSeeOther) }) r___GET("/logout", lggout) r__POST("/logout", lggout) r__POST("/_ui/site_config", mkMiddlewareSession(conn)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session, hasSession := r.Context().Value("auth").(auth.Session) if !hasSession { w.WriteHeader(http.StatusForbidden) return } if _, isAdmin := session.Roles[auth.RoleAdmin]; !isAdmin { w.WriteHeader(http.StatusForbidden) return } if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusBadRequest) return } cols, vals, err := config.ParseUpdateForm(r.Form, conn.GetConfig()) if err != nil { w.WriteHeader(http.StatusBadRequest) return } err = conn.SetConfig(cols, vals) if err != nil { http.Redirect(w, r, "/site_config?bad_edit", http.StatusSeeOther) } else { http.Redirect(w, r, "/site_config?edit_saved", http.StatusSeeOther) } }))) addr := cfg.ListenAddress + ":" + strconv.Itoa(int(cfg.Port)) server := &http.Server{Addr: addr} chSig := make(chan os.Signal, 1) signal.Notify(chSig, os.Interrupt, syscall.SIGTERM) go func() { dieSig := <-chSig gracefulTime := 1 * time.Second if dieSig == syscall.SIGTERM { gracefulTime = 0 } ctx, cancel := context.WithTimeout(context.Background(), gracefulTime) slog.Info("signal received, shutting down server", "signal", dieSig, "grace_period", gracefulTime.String()) server.Shutdown(ctx) cancel() }() go func() { for msg := range chAddrChange { slog.Info("got address change") ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) chAddrChange <- msg server.Shutdown(ctx) cancel() } }() stopServeLoop: for { slog.Info("listening", "address", cfg.ListenAddress, "port", cfg.Port) err = server.ListenAndServe() slog.Info("server stopped listening", "err", err) select { case newAddr := <-chAddrChange: addr = newAddr.Address + ":" + strconv.Itoa(int(newAddr.Port)) server = &http.Server{Addr: addr} case <-time.After(10 * time.Millisecond): break stopServeLoop } } slog.Info("quitting") return nil } func mkMiddlewareSession(d *db.DB) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("Session") if err != nil { w.WriteHeader(http.StatusUnauthorized) slog.Info("no session cookie", "err", err) return } session, err := d.GetSession(cookie.Value, d.GetConfig()) if err != nil || session == nil { w.WriteHeader(http.StatusUnauthorized) slog.Info("error middleware session", "err", err) return } next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "auth", *session))) }) } }