protondrive: log Proton's announced drive requirements at debug level

Proton announces requirements it will enforce, such as a minimum client
version, in x-pm-drive-requirements response headers. They are now
logged at debug level whenever they change, so they show in -vv bug
reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-11-12 14:41:19 +01:00
1 parent 5dbf5b04c6
commit b4a23a31ed
3 files changed
+63

No files matched your search

+15
View File
@@ -102,6 +102,9 @@ type Server struct {
requests map[string]int // count per "METHOD pattern"
faults []*fault
clockSkew time.Duration
// driveRequirements is sent as x-pm-drive-requirements headers if
// not empty; see SetDriveRequirements.
driveRequirements []string
users map[string]*User // by username
sessions map[string]*session // by UID
srp map[string]*srpLogin // by SRPSession
@@ -276,6 +279,15 @@ func (s *Server) SetClockSkew(d time.Duration) {
s.clockSkew = d
}
// SetDriveRequirements makes every response carry an
// x-pm-drive-requirements header with each of values, as Proton
// announces upcoming requirements; none if values is empty.
func (s *Server) SetDriveRequirements(values ...string) {
s.mu.Lock()
defer s.mu.Unlock()
s.driveRequirements = values
}
// handle registers h for method and an http.ServeMux path pattern.
func (s *Server) handle(method, pattern string, h http.HandlerFunc) {
s.mux.HandleFunc(method+" "+pattern, h)
@@ -294,6 +306,9 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
s.requests[key]++
w.Header().Set("Date", time.Now().Add(s.clockSkew).UTC().Format(http.TimeFormat))
for _, v := range s.driveRequirements {
w.Header().Add("x-pm-drive-requirements", v)
}
if pattern == storageUploadPattern {
s.storageUploads++
s.storagePeak = max(s.storagePeak, s.storageUploads)
+23
View File
@@ -936,6 +936,29 @@ func runSignedInSession(t *testing.T, dump fs.DumpFlags) secretRun {
// TestNoSecretsInLogs checks no secret reaches rclone's log at debug
// level across sign-in, upload, download and Disconnect.
func TestDriveRequirementsLogged(t *testing.T) {
ctx := context.Background()
remote, srv, u := setupFakeRemote(t)
u.SeedFile(u.RootID(), "a.txt", []byte("a"), time.Now())
srv.SetDriveRequirements("min-version=1.2.3", "crypto=v2")
capture := captureLog(t, 0)
f := newFakeFs(t, remote, "")
for range 3 {
mustList(ctx, t, f, "")
}
srv.SetDriveRequirements("min-version=2.0.0")
for range 2 {
_, err := f.Features().About(ctx)
require.NoError(t, err)
}
log := capture.String()
first := `DEBUG : protondrive: Proton announces drive requirements: "min-version=1.2.3, crypto=v2"`
assert.Equal(t, 1, strings.Count(log, first), "%q logged other than once in:\n%s", first, log)
second := `DEBUG : protondrive: Proton announces drive requirements: "min-version=2.0.0"`
assert.Equal(t, 1, strings.Count(log, second), "%q logged other than once in:\n%s", second, log)
}
func TestNoSecretsInLogs(t *testing.T) {
t.Run("debug", func(t *testing.T) {
run := runSignedInSession(t, 0)
+25
View File
@@ -143,6 +143,7 @@ type session struct {
deadRefresh string // refresh token the server rejected
serverDate time.Time // Date of the last response
serverDateLocal time.Time // local time serverDate was received
requirements string // x-pm-drive-requirements last logged
}
// newSession returns a session for the API at baseURL using the
@@ -252,6 +253,7 @@ func (s *session) do(ctx context.Context, opts *rest.Opts, request, response any
resp, err := s.srv.CallJSON(ctx, opts, request, &body)
if resp != nil {
s.observeDate(resp.Header.Get("Date"))
s.observeRequirements(resp.Header.Values("x-pm-drive-requirements"))
}
if err != nil || opts.NoResponse {
return resp, err
@@ -288,6 +290,29 @@ func (s *session) observeDate(date string) {
s.serverDateLocal = time.Now()
}
// maxRequirementsLog bounds the x-pm-drive-requirements text logged.
const maxRequirementsLog = 1024
// observeRequirements logs at debug level the x-pm-drive-requirements
// header values of a response, by which Proton announces requirements
// it will enforce, when they differ from those last logged.
func (s *session) observeRequirements(values []string) {
if len(values) == 0 {
return
}
v := strings.Join(values, ", ")
if len(v) > maxRequirementsLog {
v = v[:maxRequirementsLog]
}
s.mu.Lock()
changed := v != s.requirements
s.requirements = v
s.mu.Unlock()
if changed {
fs.Debugf(nil, "protondrive: Proton announces drive requirements: %q", v)
}
}
// refresh replaces the access token rejected with a working one.
//
// It uses tokens another goroutine or another user of the store has