Repository: containrrr/watchtower (go)

Issue:
crash when update image
### Describe the bug

use watchtower for image update on private registry, only update image when triggle http api endpoint, already run docker login for target registry, but it keep crash on http api trigged. 



### Steps to reproduce

see below log

### Expected behavior

update image successful

### Screenshots

_No response_

### Environment

docker-compose.yml

version: "3"
services:
  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: always
    environment:
      - TZ=Asia/Shanghai
      - WATCHTOWER_HTTP_API_TOKEN=token
    ports:
      - ‘8002:8080'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /root/.docker/config.json:/config.json
    command: --label-enable --cleanup --http-api-update —debug

docker info:
Client:
 Context:    default
 Debug Mode: false
 Plugins:
  app: Docker App (Docker Inc., v0.9.1-beta3)
  buildx: Docker Buildx (Docker Inc., v0.7.1-docker)
  scan: Docker Scan (Docker Inc., v0.12.0)

Server:
 Containers: 18
  Running: 18
  Paused: 0
  Stopped: 0
 Images: 19
 Server Version: 20.10.12
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 1
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc version: v1.0.2-0-g52b36a2
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: default
 Kernel Version: 5.4.0-90-generic
 Operating System: Ubuntu 20.04 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 7.275GiB
 Name: VM-0-21-ubuntu
 ID: RSXR:WB7R:I3NH:YER7:IGDE:LZX6:RLS4:BLDT:MLRJ:S3PC:DYSS:P67Q
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Registry Mirrors:
  https://mirror.ccs.tencentyun.com/
 Live Restore Enabled: false

### Your logs

```text
watchtower    | time="2023-04-11T11:34:56+08:00" level=debug msg="Sleeping for a second to ensure the docker api client has been properly initialized."
watchtower    | time="2023-04-11T11:34:57+08:00" level=debug msg="Making sure everything is sane before starting"
watchtower    | time="2023-04-11T11:34:57+08:00" level=debug msg="Retrieving running containers"
watchtower    | time="2023-04-11T11:34:58+08:00" level=debug msg="There are no additional watchtower containers"
watchtower    | time="2023-04-11T11:34:58+08:00" level=info msg="Watchtower 1.5.3"
watchtower    | time="2023-04-11T11:34:58+08:00" level=info msg="Using no notifications"
watchtower    | time="2023-04-11T11:34:58+08:00" level=info msg="Only checking containers using enable label"
watchtower    | time="2023-04-11T11:34:58+08:00" level=info msg="Periodic runs are not enabled."
watchtower    | time="2023-04-11T11:34:58+08:00" level=info msg="The HTTP API is enabled at :8080."
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Valid token found."
watchtower    | time="2023-04-11T11:35:33+08:00" level=info msg="Updates triggered by HTTP API request."
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Checking containers for updated images"
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Retrieving running containers"
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Trying to load authentication credentials." container=/container-name image="ccr.ccs.tencentyun.com/username/container-name:test"
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Loaded auth credentials for user username, on registry ccr.ccs.tencentyun.com/username/container-name:test, from file /config.json"
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Got image name: ccr.ccs.tencentyun.com/username/container-name:test"
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Credentials loaded"
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Checking if pull is needed" container=/container-name image="ccr.ccs.tencentyun.com/username/container-name:test"
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Building challenge URL" URL="https://ccr.ccs.tencentyun.com/v2/"
watchtower    | time="2023-04-11T11:35:33+08:00" level=debug msg="Got response to challenge request" header="bearer realm=\"https://ccr.ccs.tencentyun.com/service/token\",service=\"token-service\"," status="401 Unauthorized"
watchtower    | 2023/04/11 11:35:33 http: panic serving 183.128.233.188:52483: runtime error: index out of range [1] with length 1
watchtower    | goroutine 9 [running]:
watchtower    | net/http.(*conn).serve.func1()
watchtower    | 	/opt/hostedtoolcache/go/1.18.10/x64/src/net/http/server.go:1825 +0xbf
watchtower    | panic({0xb193a0, 0xc0004ce018})
watchtower    | 	/opt/hostedtoolcache/go/1.18.10/x64/src/runtime/panic.go:844 +0x258
watchtower    | github.com/containrrr/watchtower/pkg/registry/auth.GetAuthURL({0xc0003b4ba0?, 0x32?}, {0xc000254180, 0x2d})
watchtower    | 	/home/runner/work/watchtower/watchtower/pkg/registry/auth/auth.go:128 +0x912
watchtower    | github.com/containrrr/watchtower/pkg/registry/auth.GetBearerHeader({0xc0003b4ba0, 0x54}, {0xc000254180, 0x32}, {0xc00024cb40, 0x28})
watchtower    | 	/home/runner/work/watchtower/watchtower/pkg/registry/auth/auth.go:81 +0xf4
watchtower    | github.com/containrrr/watchtower/pkg/registry/auth.GetToken({0xc773d0, 0xc0004d1248}, {0xc00024cb40, 0x28})
watchtower    | 	/home/runner/work/watchtower/watchtower/pkg/registry/auth/auth.go:58 +0x616
watchtower    | github.com/containrrr/watchtower/pkg/registry/digest.CompareDigest({0xc773d0, 0xc0004d1248}, {0xc0002dc360, 0x84})
watchtower    | 	/home/runner/work/watchtower/watchtower/pkg/registry/digest/digest.go:32 +0x8a
watchtower    | github.com/containrrr/watchtower/pkg/container.dockerClient.PullImage({{0xc7a818, 0xc000432100}, {0x1, 0x0, 0x0, 0x0, 0x0, {0x0, 0x0}}}, {0xc731f8, ...}, ...)
watchtower    | 	/home/runner/work/watchtower/watchtower/pkg/container/client.go:338 +0x479
watchtower    | github.com/containrrr/watchtower/pkg/container.dockerClient.IsContainerStale({{0xc7a818, 0xc000432100}, {0x1, 0x0, 0x0, 0x0, 0x0, {0x0, 0x0}}}, {0x0, ...})
watchtower    | 	/home/runner/work/watchtower/watchtower/pkg/container/client.go:285 +0xfb
watchtower    | github.com/containrrr/watchtower/internal/actions.Update({0xc76530, 0xc00041a480}, {0xc000420c40, 0x1, 0x0, 0x2540be400, 0x0, 0x0, 0x0})
watchtower    | 	/home/runner/work/watchtower/watchtower/internal/actions/update.go:37 +0x91c
watchtower    | github.com/containrrr/watchtower/cmd.runUpdatesWithNotifications(0xc000420c40)
watchtower    | 	/home/runner/work/watchtower/watchtower/cmd/root.go:370 +0xe5
watchtower    | github.com/containrrr/watchtower/cmd.Run.func1({0x0?, 0xc00000e018?, 0xb603f7?})
watchtower    | 	/home/runner/work/watchtower/watchtower/cmd/root.go:186 +0x2a
watchtower    | github.com/containrrr/watchtower/pkg/api/update.(*Handler).Handle(0xc000409fc8, {0x17?, 0xc00020e270?}, 0xc0003f8d00)
watchtower    | 	/home/runner/work/watchtower/watchtower/pkg/api/update/update.go:66 +0x2c2
watchtower    | github.com/containrrr/watchtower/pkg/api.(*API).RequireToken.func1({0xc72a68, 0xc0001221c0}, 0xc0003f8d00)
watchtower    | 	/home/runner/work/watchtower/watchtower/pkg/api/api.go:36 +0x148
watchtower    | net/http.HandlerFunc.ServeHTTP(0x7f102fc101b8?, {0xc72a68?, 0xc0001221c0?}, 0x40d425?)
watchtower    | 	/opt/hostedtoolcache/go/1.18.10/x64/src/net/http/server.go:2084 +0x2f
watchtower    | net/http.(*ServeMux).ServeHTTP(0x0?, {0xc72a68, 0xc0001221c0}, 0xc0003f8d00)
watchtower    | 	/opt/hostedtoolcache/go/1.18.10/x64/src/net/http/server.go:2462 +0x149
watchtower    | net/http.serverHandler.ServeHTTP({0xc0001a5d10?}, {0xc72a68, 0xc0001221c0}, 0xc0003f8d00)
watchtower    | 	/opt/hostedtoolcache/go/1.18.10/x64/src/net/http/server.go:2916 +0x43b
watchtower    | net/http.(*conn).serve(0xc000351360, {0xc73268, 0xc0001a5c20})
watchtower    | 	/opt/hostedtoolcache/go/1.18.10/x64/src/net/http/server.go:1966 +0x5d7
watchtower    | created by net/http.(*Server).Serve
watchtower    | 	/opt/hostedtoolcache/go/1.18.10/x64/src/net/http/server.go:3071 +0x4db
```


### Additional context

_No response_

Pull request:
fix(registry): ignore empty challenge fields
The `GetAuthURL` method splits challenge response header fields by `=` and then stores the key and value in a map. If an empty field (or a key without a value) is encountered, this causes a "index out of range" panic.

This PR replaces the unchecked `strings.Split` with `strings.Cut`, and skips any fields that does not contain a value.

Fixes #1624.

Patch:
diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go
index 23aef60..5056cb3 100644
--- a/pkg/registry/auth/auth.go
+++ b/pkg/registry/auth/auth.go
@@ -123,10 +123,9 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
 
 	for _, pair := range pairs {
 		trimmed := strings.Trim(pair, " ")
-		kv := strings.Split(trimmed, "=")
-		key := kv[0]
-		val := strings.Trim(kv[1], "\"")
-		values[key] = val
+		if key, val, ok := strings.Cut(trimmed, "="); ok {
+			values[key] = strings.Trim(val, `"`)
+		}
 	}
 	logrus.WithFields(logrus.Fields{
 		"realm":   values["realm"],


Test patch:
diff --git a/pkg/registry/auth/auth_test.go b/pkg/registry/auth/auth_test.go
index 6ad2307..e276dda 100644
--- a/pkg/registry/auth/auth_test.go
+++ b/pkg/registry/auth/auth_test.go
@@ -2,13 +2,14 @@ package auth_test
 
 import (
 	"fmt"
-	"github.com/containrrr/watchtower/internal/actions/mocks"
-	"github.com/containrrr/watchtower/pkg/registry/auth"
 	"net/url"
 	"os"
 	"testing"
 	"time"
 
+	"github.com/containrrr/watchtower/internal/actions/mocks"
+	"github.com/containrrr/watchtower/pkg/registry/auth"
+
 	wtTypes "github.com/containrrr/watchtower/pkg/types"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -79,6 +80,18 @@ var _ = Describe("the auth module", func() {
 			Expect(err).To(HaveOccurred())
 			Expect(res).To(BeNil())
 		})
+		It("should not crash when an empty field is recieved", func() {
+			input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",`
+			res, err := auth.GetAuthURL(input, "containrrr/watchtower")
+			Expect(err).NotTo(HaveOccurred())
+			Expect(res).NotTo(BeNil())
+		})
+		It("should not crash when a field without a value is recieved", func() {
+			input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",valuelesskey`
+			res, err := auth.GetAuthURL(input, "containrrr/watchtower")
+			Expect(err).NotTo(HaveOccurred())
+			Expect(res).NotTo(BeNil())
+		})
 	})
 	When("getting a challenge url", func() {
 		It("should create a valid challenge url object based on the image ref supplied", func() {