package main import ( "database/sql" "flag" "fmt" "log/slog" "os" "strconv" "strings" "time" _ "github.com/mattn/go-sqlite3" codeview "gemini_site/internal/codeview" gemlog "gemini_site/internal/gemlog" "gemini_site/internal/guestbook" microblog "gemini_site/internal/microblog" "gemini_site/internal/pocketbase" gemini "github.com/kulak/gemini" ) type MainHandler struct { blog microblog.Handler gemlog gemlog.Handler guestbook guestbook.Handler db *sql.DB counter *RequestCounter } func (h MainHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) { // Increment request counter count := h.counter.Increment(req.URL.Path) slog.Info("gemini request", "path", req.URL.Path, "count", count, "user", strings.Join(userName(req), " ")) // Check if this is a blog request switch { case strings.HasPrefix(req.URL.Path, "/microblog"): h.blog.HandleBlogRequest(w, req) return case strings.HasPrefix(req.URL.Path, "/gemlog"): h.gemlog.HandleRequest(w, req) return case strings.HasPrefix(req.URL.Path, "/guestbook"): h.guestbook.HandleRequest(w, req) return case strings.HasPrefix(req.URL.Path, "/codeview"): codeview.HandleRequest(w, req) return } switch req.URL.Path { case "/": w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") data, err := os.ReadFile("pages/home.gmi") requireNoError(err) page := string(data) var content strings.Builder content.WriteString(page) content.WriteString(fmt.Sprintf("\n\n------ stats: total requests served %d, this page %d ------", h.counter.GetTotal(), h.counter.Get(req.URL.Path))) w.WriteBody([]byte(content.String())) // case "/user": // if req.Certificate() == nil { // w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required") // return // } // w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") // w.WriteBody([]byte(req.Certificate().Subject.CommonName)) // case "/die": // requireNoError(errors.New("must die")) // case "/file": // gemini.ServeFileName("cmd/example/hello.gmi", "text/gemini")(w, req) // case "/post": // if req.URL.Scheme != gemini.SchemaTitan { // w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") // w.WriteBody([]byte("Use titan scheme to upload data")) // return // } // payload, err := req.ReadTitanPayload() // requireNoError(err) // w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") // w.WriteBody([]byte("Titan Upload Parameters\r\n")) // w.WriteBody([]byte(fmt.Sprintf("Upload MIME Type: %s\r\n", req.Titan.Mime))) // w.WriteBody([]byte(fmt.Sprintf("Token: %s\r\n", req.Titan.Token))) // w.WriteBody([]byte(fmt.Sprintf("Size: %v\r\n", req.Titan.Size))) // w.WriteBody([]byte("Payload:\r\n")) // w.WriteBody(payload) default: w.WriteStatusMsg(gemini.StatusNotFound, req.URL.Path) } } func requireNoError(err error) { if err != nil { panic(err) } } func dateToStr(t time.Time) string { return strconv.FormatInt(t.Unix(), 36) } func userName(r *gemini.Request) []string { cert := r.Certificate() if cert == nil { return []string{""} } return []string{cert.Subject.CommonName, cert.SerialNumber.String(), dateToStr(cert.NotBefore), dateToStr(cert.NotAfter)} } func main() { // Set up structured JSON logging logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) slog.SetDefault(logger) // Setup DB sqlitePath := os.Getenv("SQLITE_PATH") if sqlitePath == "" { panic("SQLITE_PATH environment variable must be set and non-empty") } db, err := sql.Open("sqlite3", sqlitePath) if err != nil { panic(err) } defer db.Close() slog.Info("Starting gemini server") var host, cert, key string flag.StringVar(&host, "host", ":1965", "listen on host and port. Example: hostname:1965") flag.StringVar(&cert, "cert", "server.crt.pem", "certificate file") flag.StringVar(&key, "key", "server.key.pem", "private key associated with certificate file") flag.Parse() // Initialize request counter with 30-second snapshot interval requestCountsPath := os.Getenv("REQUEST_COUNTS_PATH") if requestCountsPath == "" { err = errors.New("REQUEST_COUNTS_PATH environment variable must be set and non-empty") slog.Error("failed to initialize request counter", "error", err) os.Exit(1) } counter, err := NewRequestCounter(30 * time.Second) if err != nil { slog.Error("failed to initialize request counter", "error", err) os.Exit(1) } defer counter.Close() pbClient := pocketbase.NewPocketBaseClient() handler := MainHandler{ blog: microblog.NewHandler(pbClient), gemlog: gemlog.NewHandler(), guestbook: guestbook.NewGuestBook(db), db: db, counter: counter, } err = gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini)) if err != nil { slog.Error("server failed to start", "error", err) os.Exit(1) } }