package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "path" "strconv" "sync" "hakurei.app/internal/info" "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" ) // for lazy initialisation of serveInfo var ( infoPayload struct { // Current package count. Count int `json:"count"` // Hakurei version, set at link time. HakureiVersion string `json:"hakurei_version"` } infoPayloadOnce sync.Once ) // handleInfo writes constant system information. func handleInfo(w http.ResponseWriter, _ *http.Request) { infoPayloadOnce.Do(func() { infoPayload.Count = int(rosa.PresetUnexportedStart) infoPayload.HakureiVersion = info.Version() }) // TODO(mae): cache entire response if no additional fields are planned writeAPIPayload(w, infoPayload) } func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := path.Base(r.URL.Path) p, ok := rosa.ResolveName(name) if !ok { http.NotFound(w, r) return } m := rosa.GetMetadata(p) pk, ok := index.names[m.Name] if !ok { http.NotFound(w, r) return } if len(pk.status) > 0 { if disposition { w.Header().Set("Content-Type", "application/octet-stream") } else { w.Header().Set("Content-Type", "text/plain; charset=utf-8") } if disposition { var version string if pk.Version != "\u0000" { version = pk.Version } else { version = "unknown" } w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s-%s.log\"", pk.Name, version, pkg.Encode(pk.ident.Value()))) } w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") _, err := io.Copy(w, bytes.NewReader(pk.status)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } else { http.NotFound(w, r) } } } // handleGet writes a slice of metadata with specified order. func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() limit, err := strconv.Atoi(q.Get("limit")) if err != nil || limit > 100 || limit < 1 { http.Error( w, "limit must be an integer between 1 and 100", http.StatusBadRequest, ) return } i, err := strconv.Atoi(q.Get("index")) if err != nil || i >= len(index.sorts[0]) || i < 0 { http.Error( w, "index must be an integer between 0 and "+ strconv.Itoa(int(rosa.PresetUnexportedStart-1)), http.StatusBadRequest, ) return } sort, err := strconv.Atoi(q.Get("sort")) if err != nil || sort >= len(index.sorts) || sort < 0 { http.Error( w, "sort must be an integer between 0 and "+ strconv.Itoa(sortOrderEnd), http.StatusBadRequest, ) return } values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))] // TODO(mae): remove count field writeAPIPayload(w, &struct { Count int `json:"count"` Values []*metadata `json:"values"` }{len(values), values}) } // apiVersion is the name of the current API revision, as part of the pattern. const apiVersion = "v1" // registerAPI registers API handler functions. func (index *packageIndex) registerAPI(mux *http.ServeMux) { mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo) mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet) mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false)) mux.HandleFunc("GET /status/", index.newStatusHandler(true)) } // writeAPIPayload sets headers common to API responses and encodes payload as // JSON for the response body. func writeAPIPayload(w http.ResponseWriter, payload any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") if err := json.NewEncoder(w).Encode(payload); err != nil { log.Println(err) http.Error( w, "cannot encode payload, contact maintainers", http.StatusInternalServerError, ) } }