116 lines
4.6 KiB
Go
116 lines
4.6 KiB
Go
package webui
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// leAlwaysOverwriteFields are plain (non-secret) [LetsEncrypt] settings — always
|
|
// persisted from the submitted form, same as any other settings.html field.
|
|
var leAlwaysOverwriteFields = []string{
|
|
"enabled", "staging", "contact_email", "domains", "dns_provider",
|
|
"route53_region", "route53_hosted_zone_id", "gcloud_project",
|
|
}
|
|
|
|
// leSecretFields hold DNS provider credentials. They're never rendered back into the
|
|
// form (always blank) and the save handler only overwrites the stored value when the
|
|
// submitted field is non-empty — "leave blank to keep the current value", the same
|
|
// idiom edit_sender.html already uses for its password field.
|
|
var leSecretFields = []string{
|
|
"cloudflare_api_token", "route53_access_key_id", "route53_secret_access_key",
|
|
"digitalocean_api_token", "gcloud_service_account_json_path",
|
|
}
|
|
|
|
// letsEncryptPage shows the current Let's Encrypt status and configuration form.
|
|
// Secret fields are always blank in the rendered form — see leSecretFields.
|
|
func (a *App) letsEncryptPage(w http.ResponseWriter, r *http.Request) {
|
|
sec := a.Cfg.Section("LetsEncrypt")
|
|
kv := M{}
|
|
for _, k := range leAlwaysOverwriteFields {
|
|
kv[k] = sec.Key(k).String()
|
|
}
|
|
for _, k := range leSecretFields {
|
|
kv[k] = ""
|
|
}
|
|
a.render(w, r, "letsencrypt.html", M{"active": "letsencrypt", "le": kv, "status": a.ACME.Status()})
|
|
}
|
|
|
|
// letsEncryptSave is a dedicated handler (not the generic settingsUpdate reflection)
|
|
// specifically because of leSecretFields' blank-means-keep-existing semantics —
|
|
// settingsUpdate would otherwise blank out a stored credential whenever this form is
|
|
// submitted with a secret field left empty.
|
|
func (a *App) letsEncryptSave(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
setFlash(w, "error", "Invalid form data")
|
|
http.Redirect(w, r, Prefix+"/letsencrypt", http.StatusFound)
|
|
return
|
|
}
|
|
sec := a.Cfg.Section("LetsEncrypt")
|
|
for _, k := range leAlwaysOverwriteFields {
|
|
sec.Key(k).SetValue(r.FormValue(k))
|
|
}
|
|
for _, k := range leSecretFields {
|
|
if v := r.FormValue(k); v != "" {
|
|
sec.Key(k).SetValue(v)
|
|
}
|
|
}
|
|
if err := a.Cfg.SaveTo(a.ConfigPath); err != nil {
|
|
setFlash(w, "error", "Error saving settings: "+err.Error())
|
|
http.Redirect(w, r, Prefix+"/letsencrypt", http.StatusFound)
|
|
return
|
|
}
|
|
setFlash(w, "success", `Let's Encrypt settings saved. Use "Obtain / Renew Now" to test the configuration.`)
|
|
http.Redirect(w, r, Prefix+"/letsencrypt", http.StatusFound)
|
|
}
|
|
|
|
// letsEncryptObtainNow triggers an immediate obtain/renew — separate from Save, since
|
|
// saving configuration must never silently kick off an ACME transaction as a side
|
|
// effect. This is also how the very first certificate actually gets obtained.
|
|
func (a *App) letsEncryptObtainNow(w http.ResponseWriter, r *http.Request) {
|
|
if err := a.ACME.ObtainOrRenew(r.Context()); err != nil {
|
|
setFlash(w, "error", "Could not obtain certificate: "+err.Error())
|
|
} else {
|
|
setFlash(w, "success", "Certificate obtained successfully")
|
|
}
|
|
http.Redirect(w, r, Prefix+"/letsencrypt", http.StatusFound)
|
|
}
|
|
|
|
// uploadGCloudServiceAccount mirrors settings.go's uploadTLSFile two-step flow: upload
|
|
// the file, return its saved path as JSON, and the browser fills a sibling text input
|
|
// with that path — the path only actually persists once the surrounding form (Save)
|
|
// is submitted.
|
|
func (a *App) uploadGCloudServiceAccount(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, M{"status": "error", "message": "Invalid upload"})
|
|
return
|
|
}
|
|
file, header, err := r.FormFile("gcloud_key_file")
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, M{"status": "error", "message": "No file provided"})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
if ext := strings.ToLower(filepath.Ext(header.Filename)); ext != ".json" {
|
|
writeJSON(w, http.StatusBadRequest, M{"status": "error", "message": "Expected a .json service account key file"})
|
|
return
|
|
}
|
|
acmeDir := filepath.Join(filepath.Dir(a.ConfigPath), "server_data", "acme")
|
|
os.MkdirAll(acmeDir, 0o755)
|
|
filePath := filepath.Join(acmeDir, fmt.Sprintf("gcloud-sa-%d.json", time.Now().Unix()))
|
|
out, err := os.Create(filePath)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, M{"status": "error", "message": "Could not save file"})
|
|
return
|
|
}
|
|
defer out.Close()
|
|
if _, err := out.ReadFrom(file); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, M{"status": "error", "message": "Could not save file"})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, M{"status": "success", "filepath": filePath})
|
|
}
|