Redact the private key from the /servers/:id/keypair response body - the UI never rendered it, but the raw key was still returned over the wire (json:"private_key,omitempty" plus explicit clearing before the JSON response). Add a new import flow: an admin can upload an OPNsense config.xml, preview the WireGuard servers/clients it defines (editable before committing), and confirm to create the corresponding Server/ServerSetting/Client records. Nothing is auto-applied - no wg-quick/systemctl call happens, matching the existing manual "Apply" step for regular server management. Schema verified against OPNsense core (WireGuard has been in core since 22.1, not a plugin) - see opnsense/parse.go for the confirmed tag reference. Public keys are always re-derived from private keys rather than trusted from the export; client public-key collisions against existing store data are skipped and reported per-batch rather than aborting the whole import. Since OPNsense stores DNS/MTU per-server and keepalive per-client, but this fork only had those app-wide (GlobalSetting), extended ServerSetting with DNSServers/MTU and Client with PersistentKeepalive as optional overrides that fall back to the global default when unset - existing single-server behavior is unchanged when the override is empty/zero. Manual UI editing of the per-client keepalive override outside the import flow is left for a later pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VjwLYRA87o8m9a9zztgs3
141 lines
4.8 KiB
Go
141 lines
4.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/gommon/log"
|
|
"github.com/rs/xid"
|
|
|
|
"github.com/ngoduykhanh/wireguard-ui/opnsense"
|
|
"github.com/ngoduykhanh/wireguard-ui/store"
|
|
)
|
|
|
|
// maxOPNsenseUploadSize caps the accepted config.xml upload size.
|
|
const maxOPNsenseUploadSize = 5 << 20 // 5 MiB
|
|
|
|
// PreviewOPNsenseImport handler accepts an uploaded OPNsense config.xml,
|
|
// parses and maps its WireGuard server/client definitions, and returns the
|
|
// staged (admin-editable) preview as JSON. It never touches the store -
|
|
// nothing is written, nothing is applied. Admin-only.
|
|
func PreviewOPNsenseImport(db store.IStore) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
fileHeader, err := c.FormFile("config")
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please upload a config.xml file"})
|
|
}
|
|
if fileHeader.Size > maxOPNsenseUploadSize {
|
|
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "config.xml is too large (max 5 MiB)"})
|
|
}
|
|
|
|
src, err := fileHeader.Open()
|
|
if err != nil {
|
|
log.Error("Cannot open uploaded OPNsense config.xml: ", err)
|
|
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Cannot read uploaded file"})
|
|
}
|
|
defer src.Close()
|
|
|
|
parsed, err := opnsense.Parse(src)
|
|
if err != nil {
|
|
log.Error("Cannot parse OPNsense config.xml: ", err)
|
|
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, fmt.Sprintf("Cannot parse config.xml: %v", err)})
|
|
}
|
|
|
|
existingServers, err := db.GetServers()
|
|
if err != nil {
|
|
log.Error("Cannot list existing servers: ", err)
|
|
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot list existing servers"})
|
|
}
|
|
existingIDs := make([]string, 0, len(existingServers))
|
|
for _, s := range existingServers {
|
|
existingIDs = append(existingIDs, s.ID)
|
|
}
|
|
|
|
preview := opnsense.ToPreview(parsed, existingIDs)
|
|
return c.JSON(http.StatusOK, preview)
|
|
}
|
|
}
|
|
|
|
// CommitOPNsenseImport handler accepts the (possibly admin-edited) preview
|
|
// JSON produced by PreviewOPNsenseImport - not the original XML - and
|
|
// writes the corresponding model.Server/ServerSetting/Client records via
|
|
// the store. Servers/clients that fail validation, or clients whose public
|
|
// key collides with one already in the store, are skipped and reported;
|
|
// the rest of the batch still commits. This never applies the resulting
|
|
// WireGuard config (no wg-quick/systemctl calls) - that remains a separate,
|
|
// manual "Apply" step elsewhere in the app. Admin-only.
|
|
func CommitOPNsenseImport(db store.IStore) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
var payload opnsense.PreviewResult
|
|
if err := c.Bind(&payload); err != nil {
|
|
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
|
|
}
|
|
|
|
existingClients, err := db.GetClients(false)
|
|
if err != nil {
|
|
log.Error("Cannot list existing clients for duplicate check: ", err)
|
|
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot list existing clients"})
|
|
}
|
|
knownPublicKeys := make(map[string]bool, len(existingClients))
|
|
for _, cd := range existingClients {
|
|
knownPublicKeys[cd.Client.PublicKey] = true
|
|
}
|
|
|
|
type serverResult struct {
|
|
ID string `json:"id"`
|
|
Imported bool `json:"imported"`
|
|
Error string `json:"error,omitempty"`
|
|
ClientsCreated int `json:"clients_created"`
|
|
ClientsSkipped []string `json:"clients_skipped,omitempty"`
|
|
}
|
|
results := make([]serverResult, 0, len(payload.Servers))
|
|
|
|
for _, ps := range payload.Servers {
|
|
res := serverResult{ID: ps.ID}
|
|
|
|
server, settings, clients, err := opnsense.ToModel(ps)
|
|
if err != nil {
|
|
res.Error = err.Error()
|
|
results = append(results, res)
|
|
continue
|
|
}
|
|
|
|
if _, err := db.GetServerByID(server.ID); err == nil {
|
|
res.Error = "a server with this ID already exists"
|
|
results = append(results, res)
|
|
continue
|
|
}
|
|
|
|
if err := db.CreateServer(server); err != nil {
|
|
res.Error = fmt.Sprintf("cannot create server: %v", err)
|
|
results = append(results, res)
|
|
continue
|
|
}
|
|
if err := db.SaveServerSettings(server.ID, settings); err != nil {
|
|
log.Errorf("Server %s created but settings failed: %v", server.ID, err)
|
|
}
|
|
|
|
for _, client := range clients {
|
|
if knownPublicKeys[client.PublicKey] {
|
|
res.ClientsSkipped = append(res.ClientsSkipped, fmt.Sprintf("%s (duplicate public key)", client.Name))
|
|
continue
|
|
}
|
|
client.ID = xid.New().String()
|
|
if err := db.SaveClient(client); err != nil {
|
|
res.ClientsSkipped = append(res.ClientsSkipped, fmt.Sprintf("%s (%v)", client.Name, err))
|
|
continue
|
|
}
|
|
knownPublicKeys[client.PublicKey] = true
|
|
res.ClientsCreated++
|
|
}
|
|
|
|
res.Imported = true
|
|
log.Infof("Imported server %s from OPNsense config (%d clients)", server.ID, res.ClientsCreated)
|
|
results = append(results, res)
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, results)
|
|
}
|
|
}
|