personal-gemini-capsule/main.go
2025-10-07 23:26:37 +02:00

147 lines
3.9 KiB
Go

package main
import (
"database/sql"
"errors"
"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
}
func (h MainHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) {
slog.Info("gemini request",
"path", req.URL.Path,
"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 "/":
gemini.ServeFileName("pages/home.gmi", "text/gemini")(w, req)
// err := w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
// requireNoError(err)
// _, err = w.WriteBody([]byte("Hello, world!"))
// requireNoError(err)
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()
pbClient := pocketbase.NewPocketBaseClient()
handler := MainHandler{
blog: microblog.NewHandler(pbClient),
gemlog: gemlog.NewHandler(),
guestbook: guestbook.NewGuestBook(db),
}
err = gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini))
if err != nil {
slog.Error("server failed to start", "error", err)
os.Exit(1)
}
}