165 lines
4.5 KiB
Go
165 lines
4.5 KiB
Go
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
|
|
counter, err := NewRequestCounter("request_counts.json", 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)
|
|
}
|
|
}
|