mirror of
https://github.com/Pacerino/CaddyProxyManager.git
synced 2026-07-22 13:33:36 -04:00
Introduce per-host plugin support: Caddy module discovery, module config endpoints, plugin schemas (basicauth, cloudflare) and the host plugins API. On the frontend, add the Plugins page, plugin/host plugin dialogs and schema-driven form fields.
142 lines
4.4 KiB
Go
142 lines
4.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
h "github.com/Pacerino/CaddyProxyManager/internal/api/http"
|
|
"github.com/Pacerino/CaddyProxyManager/internal/caddy"
|
|
"github.com/Pacerino/CaddyProxyManager/internal/caddy/schema"
|
|
"github.com/Pacerino/CaddyProxyManager/internal/database"
|
|
"github.com/Pacerino/CaddyProxyManager/internal/jobqueue"
|
|
)
|
|
|
|
// GetHostScopedSchemas returns the typed schemas for plugins configurable per
|
|
// host. The frontend filters these against the host's Caddy build.
|
|
// Route: GET /caddy/host-schemas
|
|
func (s Handler) GetHostScopedSchemas() func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
h.ResultResponseJSON(w, r, http.StatusOK, schema.WithScope(schema.ScopeHost))
|
|
}
|
|
}
|
|
|
|
// hostPluginRequest is the body accepted by SetHostPlugin.
|
|
type hostPluginRequest struct {
|
|
ModuleID string `json:"moduleId"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
Values map[string]any `json:"values"`
|
|
}
|
|
|
|
// SetHostPlugin creates or updates a per-host plugin configuration and
|
|
// re-applies the host's route.
|
|
// Route: PUT /hosts/{hostID}/plugins
|
|
func (s Handler) SetHostPlugin() func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
hostID, err := getURLParamInt(r, "hostID")
|
|
if err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
var req hostPluginRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
sc, found := schema.Get(req.ModuleID)
|
|
if !found || !sc.HasScope(schema.ScopeHost) {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, "module is not configurable per host", nil)
|
|
return
|
|
}
|
|
|
|
handler, err := sc.BuildHandler(req.Values)
|
|
if err != nil {
|
|
if ve, ok := err.(*schema.ValidationError); ok {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, "schema validation failed", ve.Fields)
|
|
return
|
|
}
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
handlerJSON, err := json.Marshal(handler)
|
|
if err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
plugin := database.HostPlugin{
|
|
HostID: uint(hostID),
|
|
ModuleID: req.ModuleID,
|
|
Handler: database.JSON(handlerJSON),
|
|
Enabled: true,
|
|
}
|
|
if req.Enabled != nil {
|
|
plugin.Enabled = *req.Enabled
|
|
}
|
|
|
|
// Upsert by (host, module).
|
|
var existing database.HostPlugin
|
|
if err := s.DB.Where("host_id = ? AND module_id = ?", hostID, req.ModuleID).First(&existing).Error; err == nil {
|
|
plugin.ID = existing.ID
|
|
}
|
|
if err := s.DB.Save(&plugin).Error; err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
if err := s.reapplyHost(uint(hostID)); err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
h.ResultResponseJSON(w, r, http.StatusOK, plugin)
|
|
}
|
|
}
|
|
|
|
// DeleteHostPlugin removes a per-host plugin and re-applies the host's route.
|
|
// Route: DELETE /hosts/{hostID}/plugins/{pluginID}
|
|
func (s Handler) DeleteHostPlugin() func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
hostID, err := getURLParamInt(r, "hostID")
|
|
if err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
pluginID, err := getURLParamInt(r, "pluginID")
|
|
if err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
if err := s.DB.Where("host_id = ?", hostID).Delete(&database.HostPlugin{}, pluginID).Error; err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
if err := s.reapplyHost(uint(hostID)); err != nil {
|
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
h.ResultResponseJSON(w, r, http.StatusOK, true)
|
|
}
|
|
}
|
|
|
|
// reapplyHost reloads the host with its associations and re-writes its
|
|
// configuration through the configured provider via the job queue.
|
|
func (s Handler) reapplyHost(hostID uint) error {
|
|
var host database.Host
|
|
if err := s.DB.Where("id = ?", hostID).Preload("Upstreams").Preload("Plugins").First(&host).Error; err != nil {
|
|
return err
|
|
}
|
|
provider, err := caddy.GetProvider()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return jobqueue.AddJob(jobqueue.Job{
|
|
Name: "CaddyConfigureHost",
|
|
Action: func() error { return provider.WriteHost(host) },
|
|
})
|
|
}
|