fix: harden DynDNS recovery and publish v1.5.1
This commit is contained in:
1 parent
3383b91e49
commit
81def77e3c
37 files changed
+2415
-250
No files matched your search
@@ -14,13 +14,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/buildinfo"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/diagnostics"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/model"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/secure"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/store"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/updateprotocol"
|
||||
)
|
||||
|
||||
var ErrCredentialMissing = errors.New("Update-Zugang ist noch nicht eingerichtet")
|
||||
var ErrCredentialMissing = errors.New("Release ist nicht öffentlich abrufbar (privates Repository oder kein veröffentlichtes Release); für ein privates Repository den Update-Zugang hinterlegen")
|
||||
|
||||
const (
|
||||
agentUnavailableRetryInterval = 2 * time.Second
|
||||
@@ -198,18 +199,19 @@ func (m *Manager) CheckNow(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if settings.RegistryUsername == "" || len(settings.TokenCiphertext) == 0 || len(settings.TokenNonce) == 0 {
|
||||
m.setStoredState(settings)
|
||||
return ErrCredentialMissing
|
||||
// Public releases must remain usable even with missing or stale credentials.
|
||||
release, err := m.fetchLatest(ctx, "", "")
|
||||
if errors.Is(err, ErrCredentialMissing) && settings.RegistryUsername != "" && len(settings.TokenCiphertext) > 0 {
|
||||
credential, decryptErr := m.cipher.Decrypt(settings.TokenCiphertext, settings.TokenNonce)
|
||||
if decryptErr != nil {
|
||||
return m.recordFailure(ctx, errors.New("gespeicherter Update-Zugang konnte nicht entschlüsselt werden"))
|
||||
}
|
||||
release, err = m.fetchLatest(ctx, settings.RegistryUsername, credential)
|
||||
}
|
||||
credential, err := m.cipher.Decrypt(settings.TokenCiphertext, settings.TokenNonce)
|
||||
if err != nil {
|
||||
return m.recordFailure(ctx, errors.New("gespeicherter Update-Zugang konnte nicht entschlüsselt werden"))
|
||||
}
|
||||
release, err := m.fetchLatest(ctx, settings.RegistryUsername, credential)
|
||||
if err != nil {
|
||||
return m.recordFailure(ctx, err)
|
||||
}
|
||||
diagnostics.Record(diagnostics.Event{Code: "release_check_ok", Target: "git"})
|
||||
checkedAt := time.Now().UTC()
|
||||
if err := m.store.RecordSoftwareUpdateSuccess(ctx, release.TagName, release.HTMLURL, checkedAt); err != nil {
|
||||
return err
|
||||
@@ -247,14 +249,22 @@ func (m *Manager) RequestUpdate(ctx context.Context, version string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
credential, err := m.cipher.Decrypt(settings.TokenCiphertext, settings.TokenNonce)
|
||||
if err != nil || settings.RegistryUsername == "" {
|
||||
return ErrCredentialMissing
|
||||
var username, credential string
|
||||
if settings.RegistryUsername != "" && len(settings.TokenCiphertext) > 0 {
|
||||
credential, err = m.cipher.Decrypt(settings.TokenCiphertext, settings.TokenNonce)
|
||||
if err == nil {
|
||||
username = settings.RegistryUsername
|
||||
} else if !status.AnonymousPull {
|
||||
return errors.New("gespeicherter Update-Zugang konnte nicht entschlüsselt werden")
|
||||
}
|
||||
}
|
||||
if username == "" && !status.AnonymousPull {
|
||||
return errors.New("für Updates ohne Anmeldung muss der Update-Agent einmal mit Docker Compose aktualisiert werden")
|
||||
}
|
||||
if err := m.store.SetDesiredSoftwareVersion(ctx, version); err != nil {
|
||||
return err
|
||||
}
|
||||
request := updateprotocol.Request{Version: version, CurrentVersion: state.CurrentVersion, RegistryUsername: settings.RegistryUsername, RegistryToken: credential}
|
||||
request := updateprotocol.Request{Version: version, CurrentVersion: state.CurrentVersion, RegistryUsername: username, RegistryToken: credential}
|
||||
if err := m.agent.Start(ctx, request); err != nil {
|
||||
cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
m.clearDesiredVersion(cleanupCtx, version)
|
||||
@@ -278,7 +288,7 @@ func (m *Manager) UpdateStatus(ctx context.Context) (updateprotocol.Status, erro
|
||||
m.state.AgentVersion = status.AgentVersion
|
||||
m.state.AgentProtocol = status.Protocol
|
||||
m.state.AgentCompatible = agentCompatible(status)
|
||||
m.state.ComposeSyncRequired = !m.state.AgentCompatible
|
||||
m.state.ComposeSyncRequired = !m.state.AgentCompatible || (!m.state.CredentialConfigured && !status.AnonymousPull)
|
||||
} else {
|
||||
m.state.AgentCompatible = false
|
||||
m.state.ComposeSyncRequired = false
|
||||
@@ -299,7 +309,7 @@ func (m *Manager) fetchLatest(ctx context.Context, username, credential string)
|
||||
if err != nil {
|
||||
return releaseResponse{}, err
|
||||
}
|
||||
if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
|
||||
if needsAuthentication(response.StatusCode) && username != "" && credential != "" {
|
||||
response.Body.Close()
|
||||
response, err = m.fetchLatestWithAuth(ctx, username, credential, false)
|
||||
if err != nil {
|
||||
@@ -307,11 +317,14 @@ func (m *Manager) fetchLatest(ctx context.Context, username, credential string)
|
||||
}
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
|
||||
return releaseResponse{}, errors.New("Gitea-Zugang wurde abgelehnt")
|
||||
if needsAuthentication(response.StatusCode) {
|
||||
if username == "" || credential == "" {
|
||||
return releaseResponse{}, &releaseStatusError{status: response.StatusCode, err: ErrCredentialMissing}
|
||||
}
|
||||
return releaseResponse{}, &releaseStatusError{status: response.StatusCode, err: errors.New("Gitea-Zugang abgelehnt oder kein sichtbares veröffentlichtes Release vorhanden")}
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return releaseResponse{}, fmt.Errorf("Git-Server antwortet mit HTTP %d", response.StatusCode)
|
||||
return releaseResponse{}, &releaseStatusError{status: response.StatusCode, err: fmt.Errorf("Git-Server antwortet mit HTTP %d", response.StatusCode)}
|
||||
}
|
||||
var release releaseResponse
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 128<<10)).Decode(&release); err != nil {
|
||||
@@ -324,25 +337,46 @@ func (m *Manager) fetchLatest(ctx context.Context, username, credential string)
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func needsAuthentication(status int) bool {
|
||||
// Gitea also conceals private repositories using HTTP 404.
|
||||
return status == http.StatusUnauthorized || status == http.StatusForbidden || status == http.StatusNotFound
|
||||
}
|
||||
|
||||
func (m *Manager) fetchLatestWithAuth(ctx context.Context, username, credential string, basic bool) (*http.Response, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, m.apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("Update-Endpunkt ist ungültig")
|
||||
}
|
||||
request.Header.Set("Accept", "application/json")
|
||||
if basic {
|
||||
request.SetBasicAuth(username, credential)
|
||||
} else {
|
||||
request.Header.Set("Authorization", "token "+credential)
|
||||
if username != "" && credential != "" {
|
||||
if basic {
|
||||
request.SetBasicAuth(username, credential)
|
||||
} else {
|
||||
request.Header.Set("Authorization", "token "+credential)
|
||||
}
|
||||
}
|
||||
request.Header.Set("User-Agent", "domain-control-update-checker/"+buildinfo.Version)
|
||||
response, err := m.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, errors.New("Git-Server ist momentan nicht erreichbar")
|
||||
return nil, &releaseFetchError{err: err}
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
type releaseFetchError struct{ err error }
|
||||
|
||||
func (e *releaseFetchError) Error() string { return "Git-Server ist momentan nicht erreichbar" }
|
||||
func (e *releaseFetchError) Unwrap() error { return e.err }
|
||||
|
||||
type releaseStatusError struct {
|
||||
status int
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *releaseStatusError) Error() string { return e.err.Error() }
|
||||
func (e *releaseStatusError) Unwrap() error { return e.err }
|
||||
func (e *releaseStatusError) HTTPStatusCode() int { return e.status }
|
||||
|
||||
func (m *Manager) safeReleaseURL(raw string) string {
|
||||
apiURL, apiErr := url.Parse(m.apiURL)
|
||||
releaseURL, releaseErr := url.Parse(raw)
|
||||
@@ -353,6 +387,7 @@ func (m *Manager) safeReleaseURL(raw string) string {
|
||||
}
|
||||
|
||||
func (m *Manager) recordFailure(ctx context.Context, checkErr error) error {
|
||||
diagnostics.Record(diagnostics.Event{Code: "release_check_failed", Target: "git", Failure: diagnostics.Failure(checkErr), HTTPStatus: diagnostics.HTTPStatus(checkErr)})
|
||||
checkedAt := time.Now().UTC()
|
||||
if err := m.store.RecordSoftwareUpdateFailure(ctx, checkErr.Error(), checkedAt); err != nil {
|
||||
return err
|
||||
@@ -383,7 +418,7 @@ func (m *Manager) setStoredState(settings model.SoftwareUpdateSettings) {
|
||||
state.AgentStatus = m.state.AgentStatus
|
||||
state.AgentVersion = m.state.AgentVersion
|
||||
state.AgentProtocol = m.state.AgentProtocol
|
||||
state.ComposeSyncRequired = state.AgentAvailable && !state.AgentCompatible
|
||||
state.ComposeSyncRequired = state.AgentAvailable && (!state.AgentCompatible || (!state.CredentialConfigured && !state.AgentStatus.AnonymousPull))
|
||||
m.state = state
|
||||
m.stateMu.Unlock()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user