package api import ( "encoding/json" "errors" "net/http" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/auth" "git.fabledsword.com/bvandeusen/minstrel/internal/netsettings" ) type networkSettingsResp struct { TrustedProxyHops int `json:"trusted_proxy_hops"` MaxHops int `json:"max_hops"` // DetectedClientIP is what the CURRENT setting resolves this very request // to. It's the difference between a number the operator has to reason // about and one they can verify: set the value, reload, and check the // address matches the machine you're sitting at. DetectedClientIP string `json:"detected_client_ip"` // ForwardedChain is the raw X-Forwarded-For as received, so an operator // whose detected address looks wrong can see how many hops actually // arrived and count them rather than guess. ForwardedChain string `json:"forwarded_chain"` RemoteAddr string `json:"remote_addr"` // PublicURL is where users reach Minstrel; reset emails link to it and // are not sent while it is empty. PublicURL string `json:"public_url"` } // Both fields are optional so the proxy card and the public-address card can // each save their own value without overwriting the other's. type updateNetworkSettingsReq struct { TrustedProxyHops *int `json:"trusted_proxy_hops"` PublicURL *string `json:"public_url"` } func (h *handlers) handleGetNetworkSettings(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, h.networkSettingsPayload(r)) } func (h *handlers) handleUpdateNetworkSettings(w http.ResponseWriter, r *http.Request) { var req updateNetworkSettingsReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, apierror.BadRequest("invalid_body", "malformed JSON")) return } if req.TrustedProxyHops == nil && req.PublicURL == nil { writeErr(w, apierror.BadRequest("invalid_body", "nothing to update")) return } // Validate everything before writing anything, so a bad public URL can't // leave the hops half-saved. if req.TrustedProxyHops != nil && (*req.TrustedProxyHops < 0 || *req.TrustedProxyHops > netsettings.MaxTrustedProxyHops) { writeErr(w, apierror.BadRequest("invalid_hops", netsettings.ErrHopsOutOfRange.Error())) return } if req.PublicURL != nil { if _, err := netsettings.NormalizePublicURL(*req.PublicURL); err != nil { writeErr(w, apierror.BadRequest("invalid_public_url", err.Error())) return } } if req.TrustedProxyHops != nil { if err := h.netSettings.SetHops(r.Context(), *req.TrustedProxyHops); err != nil { if errors.Is(err, netsettings.ErrHopsOutOfRange) { writeErr(w, apierror.BadRequest("invalid_hops", err.Error())) return } writeErrWithLog(w, h.logger, "admin network: update failed", apierror.Internal(err)) return } } if req.PublicURL != nil { if err := h.netSettings.SetPublicURL(r.Context(), *req.PublicURL); err != nil { writeErrWithLog(w, h.logger, "admin network: public URL update failed", apierror.Internal(err)) return } } // Echo the payload recomputed under the NEW value, so the card can show // immediately what the change did to this request's own address rather // than making the operator reload to find out. writeJSON(w, http.StatusOK, h.networkSettingsPayload(r)) } func (h *handlers) networkSettingsPayload(r *http.Request) networkSettingsResp { hops := h.netSettings.Hops() return networkSettingsResp{ TrustedProxyHops: hops, MaxHops: netsettings.MaxTrustedProxyHops, DetectedClientIP: auth.ClientIP(r, hops), ForwardedChain: r.Header.Get("X-Forwarded-For"), RemoteAddr: r.RemoteAddr, PublicURL: h.netSettings.PublicURL(), } }