|
|
@@ -1,6 +1,7 @@
|
|
|
package api
|
|
|
|
|
|
import (
|
|
|
+ "context"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"net/http"
|
|
|
@@ -8,6 +9,7 @@ import (
|
|
|
"os"
|
|
|
"os/exec"
|
|
|
"path/filepath"
|
|
|
+ release "proxmox-router"
|
|
|
"proxmox-router/internal/auth"
|
|
|
"regexp"
|
|
|
"strconv"
|
|
|
@@ -19,7 +21,6 @@ var BuildCommit = "unknown"
|
|
|
|
|
|
const updateDir = "/var/lib/debtik/update"
|
|
|
|
|
|
-var branchPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$`)
|
|
|
var pinPattern = regexp.MustCompile(`^sha256//[A-Za-z0-9+/]{43}=$`)
|
|
|
|
|
|
type updateConfig struct {
|
|
|
@@ -41,7 +42,7 @@ func validUpdateConfig(c updateConfig) bool {
|
|
|
return false
|
|
|
}
|
|
|
}
|
|
|
- return (c.TLSKeyPin == "" || pinPattern.MatchString(c.TLSKeyPin)) && u.Host != "" && (u.Scheme == "http" || u.Scheme == "https" || u.Scheme == "ssh") && branchPattern.MatchString(c.Branch) && !strings.Contains(c.Branch, "..") && !strings.HasSuffix(c.Branch, "/")
|
|
|
+ return (c.TLSKeyPin == "" || pinPattern.MatchString(c.TLSKeyPin)) && u.Host != "" && (u.Scheme == "http" || u.Scheme == "https" || u.Scheme == "ssh")
|
|
|
}
|
|
|
func readUpdateConfig() updateConfig {
|
|
|
var c updateConfig
|
|
|
@@ -69,7 +70,7 @@ func (s *Server) handleUpdateStatus(w http.ResponseWriter, r *http.Request) {
|
|
|
if len(logData) > 12000 {
|
|
|
logData = logData[len(logData)-12000:]
|
|
|
}
|
|
|
- writeJSON(w, http.StatusOK, map[string]any{"repository": c.Repository, "branch": c.Branch, "tls_key_pin": c.TLSKeyPin, "installed_commit": BuildCommit, "state": state, "log": string(logData)})
|
|
|
+ writeJSON(w, http.StatusOK, map[string]any{"repository": c.Repository, "branch": c.Branch, "tls_key_pin": c.TLSKeyPin, "installed_commit": BuildCommit, "installed_version": release.Version(), "state": state, "log": string(logData)})
|
|
|
}
|
|
|
func (s *Server) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|
|
if !requireUpdateAdmin(w, r) {
|
|
|
@@ -105,6 +106,22 @@ func (s *Server) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|
|
}
|
|
|
writeJSON(w, 200, c)
|
|
|
}
|
|
|
+func availableRelease(ctx context.Context, c updateConfig) (string, string, error) {
|
|
|
+ ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
|
+ defer cancel()
|
|
|
+ args := []string{}
|
|
|
+ if c.TLSKeyPin != "" {
|
|
|
+ args = append(args, "-c", "http.sslVerify=false", "-c", "http.pinnedpubkey="+c.TLSKeyPin)
|
|
|
+ }
|
|
|
+ args = append(args, "ls-remote", "--tags", c.Repository, "refs/tags/v*")
|
|
|
+ cmd := exec.CommandContext(ctx, "git", args...)
|
|
|
+ cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
|
|
+ data, err := cmd.Output()
|
|
|
+ if err != nil {
|
|
|
+ return "", "", err
|
|
|
+ }
|
|
|
+ return release.LatestTag(string(data))
|
|
|
+}
|
|
|
func (s *Server) handleUpdateCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
if !requireUpdateAdmin(w, r) {
|
|
|
return
|
|
|
@@ -114,24 +131,17 @@ func (s *Server) handleUpdateCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
http.Error(w, "Configure repository first", 400)
|
|
|
return
|
|
|
}
|
|
|
- args := []string{}
|
|
|
- if c.TLSKeyPin != "" {
|
|
|
- args = append(args, "-c", "http.sslVerify=false", "-c", "http.pinnedpubkey="+c.TLSKeyPin)
|
|
|
- }
|
|
|
- args = append(args, "ls-remote", "--exit-code", "--heads", c.Repository, "refs/heads/"+c.Branch)
|
|
|
- cmd := exec.CommandContext(r.Context(), "git", args...)
|
|
|
- cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
|
|
- data, err := cmd.Output()
|
|
|
+ version, commit, err := availableRelease(r.Context(), c)
|
|
|
if err != nil {
|
|
|
- http.Error(w, "Cannot read repository branch", 502)
|
|
|
+ http.Error(w, "Cannot find a stable version tag in repository", 502)
|
|
|
return
|
|
|
}
|
|
|
- fields := strings.Fields(string(data))
|
|
|
- if len(fields) != 2 || fields[1] != "refs/heads/"+c.Branch {
|
|
|
- http.Error(w, "Unexpected Git response", 502)
|
|
|
+ newer, err := release.Newer(version, release.Version())
|
|
|
+ if err != nil {
|
|
|
+ http.Error(w, err.Error(), 500)
|
|
|
return
|
|
|
}
|
|
|
- writeJSON(w, 200, map[string]any{"installed_commit": BuildCommit, "available_commit": fields[0], "update_available": !strings.HasPrefix(fields[0], BuildCommit)})
|
|
|
+ writeJSON(w, 200, map[string]any{"installed_version": release.Version(), "available_version": version, "installed_commit": BuildCommit, "available_commit": commit, "update_available": newer})
|
|
|
}
|
|
|
func (s *Server) handleUpdateAction(w http.ResponseWriter, r *http.Request) {
|
|
|
if !requireUpdateAdmin(w, r) {
|
|
|
@@ -147,6 +157,18 @@ func (s *Server) handleUpdateAction(w http.ResponseWriter, r *http.Request) {
|
|
|
http.Error(w, "Configure repository first", 400)
|
|
|
return
|
|
|
}
|
|
|
+ if action == "install" {
|
|
|
+ candidate, _, err := availableRelease(r.Context(), c)
|
|
|
+ if err != nil {
|
|
|
+ http.Error(w, "Cannot read release versions", 502)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ newer, err := release.Newer(candidate, release.Version())
|
|
|
+ if err != nil || !newer {
|
|
|
+ http.Error(w, "Update requires a version greater than the installed version", 409)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
if _, err := os.Stat("/usr/local/libexec/debtik-update"); err != nil {
|
|
|
http.Error(w, "Update helper is not installed", 503)
|
|
|
return
|
|
|
@@ -157,7 +179,7 @@ func (s *Server) handleUpdateAction(w http.ResponseWriter, r *http.Request) {
|
|
|
}
|
|
|
args := []string{"--unit=debtik-update-" + action + "-" + strconv.FormatInt(time.Now().UnixNano(), 10), "--collect", "/usr/local/libexec/debtik-update", action}
|
|
|
if action == "install" {
|
|
|
- args = append(args, c.Repository, c.Branch, c.TLSKeyPin)
|
|
|
+ args = append(args, c.Repository, "master", c.TLSKeyPin)
|
|
|
}
|
|
|
cmd := exec.CommandContext(r.Context(), "systemd-run", args...)
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|