go-help/server.go

163 lines
4.3 KiB
Go
Raw Normal View History

// SPDX-Licence-Identifier: EUPL-1.2
package help
import (
"encoding/json"
"net/http"
)
// Server serves the help catalog over HTTP.
type Server struct {
catalog *Catalog
addr string
mux *http.ServeMux
}
// NewServer creates an HTTP server for the given catalog.
// Routes are registered on creation; the caller can use ServeHTTP as
// an http.Handler or call ListenAndServe to start listening.
func NewServer(catalog *Catalog, addr string) *Server {
s := &Server{
catalog: catalog,
addr: addr,
mux: http.NewServeMux(),
}
// HTML routes
s.mux.HandleFunc("GET /", s.handleIndex)
s.mux.HandleFunc("GET /topics/{id}", s.handleTopic)
s.mux.HandleFunc("GET /search", s.handleSearch)
// JSON API routes
s.mux.HandleFunc("GET /api/topics", s.handleAPITopics)
s.mux.HandleFunc("GET /api/topics/{id}", s.handleAPITopic)
s.mux.HandleFunc("GET /api/search", s.handleAPISearch)
return s
}
// ServeHTTP implements http.Handler, delegating to the internal mux.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
}
// ListenAndServe starts the HTTP server.
func (s *Server) ListenAndServe() error {
srv := &http.Server{
Addr: s.addr,
Handler: s.mux,
}
return srv.ListenAndServe()
}
// setSecurityHeaders sets common security headers.
func setSecurityHeaders(w http.ResponseWriter) {
w.Header().Set("X-Content-Type-Options", "nosniff")
}
// --- HTML handlers ---
func (s *Server) handleIndex(w http.ResponseWriter, _ *http.Request) {
setSecurityHeaders(w)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
topics := s.catalog.List()
data := indexData{
Topics: topics,
Groups: groupTopicsByTag(topics),
}
if err := renderPage(w, "index.html", data); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) {
setSecurityHeaders(w)
id := r.PathValue("id")
topic, err := s.catalog.Get(id)
if err != nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
_ = renderPage(w, "404.html", nil)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := renderPage(w, "topic.html", topicData{Topic: topic}); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
setSecurityHeaders(w)
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "Missing search query parameter 'q'", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
results := s.catalog.Search(query)
data := searchData{
Query: query,
Results: results,
}
if err := renderPage(w, "search.html", data); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// --- JSON API handlers ---
func (s *Server) handleAPITopics(w http.ResponseWriter, _ *http.Request) {
setSecurityHeaders(w)
w.Header().Set("Content-Type", "application/json")
topics := s.catalog.List()
if err := json.NewEncoder(w).Encode(topics); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
func (s *Server) handleAPITopic(w http.ResponseWriter, r *http.Request) {
setSecurityHeaders(w)
id := r.PathValue("id")
topic, err := s.catalog.Get(id)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "topic not found"})
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(topic); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
func (s *Server) handleAPISearch(w http.ResponseWriter, r *http.Request) {
setSecurityHeaders(w)
query := r.URL.Query().Get("q")
if query == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "missing query parameter 'q'"})
return
}
w.Header().Set("Content-Type", "application/json")
results := s.catalog.Search(query)
if err := json.NewEncoder(w).Encode(results); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}