client: define a "dummy" hostname to use for local connections
- fixes https://github.com/moby/moby/issues/45935
- relates to https://github.com/moby/moby/pull/45941


For local communications (npipe://, unix://), the hostname is not used, but we need valid and meaningful hostname.

The current code used the client's `addr` as hostname in some cases, which could contain the path for the unix-socket (`/var/run/docker.sock`), which gets rejected by go1.20.6 and go1.19.11 because of a security fix for [CVE-2023-29406 ][1], which was implemented in  https://go.dev/issue/60374.

Prior versions go Go would clean the host header, and strip slashes in the process, but go1.20.6 and go1.19.11 no longer do, and reject the host header.

This patch introduces a `DummyHost` const, and uses this dummy host for cases where we don't need an actual hostname.

Before this patch (using go1.20.6):

    make GO_VERSION=1.20.6 TEST_FILTER=TestAttach test-integration
    === RUN   TestAttachWithTTY
        attach_test.go:46: assertion failed: error is not nil: http: invalid Host header
    --- FAIL: TestAttachWithTTY (0.11s)
    === RUN   TestAttachWithoutTTy
        attach_test.go:46: assertion failed: error is not nil: http: invalid Host header
    --- FAIL: TestAttachWithoutTTy (0.02s)
    FAIL

With this patch applied:

    make GO_VERSION=1.20.6 TEST_FILTER=TestAttach test-integration
    INFO: Testing against a local daemon
    === RUN   TestAttachWithTTY
    --- PASS: TestAttachWithTTY (0.12s)
    === RUN   TestAttachWithoutTTy
    --- PASS: TestAttachWithoutTTy (0.02s)
    PASS

[1]: https://github.com/advisories/GHSA-f8f7-69v5-w4vx

<!--
Please make sure you've read and understood our contributing guidelines;
https://github.com/moby/moby/blob/master/CONTRIBUTING.md

** Make sure all your commits include a signature generated with `git commit -s` **

For additional information on our contributing process, read our contributing
guide https://docs.docker.com/opensource/code/

If this is a bug fix, make sure your description includes "fixes #xxxx", or
"closes #xxxx"

Please provide the following information:
-->

**- What I did**

**- How I did it**

**- How to verify it**

**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->


**- A picture of a cute animal (not mandatory but encouraged)**


client/request_test.go
package client // import "github.com/docker/docker/client"

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/errdefs"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

// TestSetHostHeader should set fake host for local communications, set real host
// for normal communications.
func TestSetHostHeader(t *testing.T) {
	const testEndpoint = "/test"
	testCases := []struct {
		host            string
		expectedHost    string
		expectedURLHost string
	}{
		{
			"unix:///var/run/docker.sock",
			"docker",
			"/var/run/docker.sock",
		},
		{
			"npipe:////./pipe/docker_engine",
			"docker",
			"//./pipe/docker_engine",
		},
		{
			"tcp://0.0.0.0:4243",
			"",
			"0.0.0.0:4243",
		},
		{
			"tcp://localhost:4243",
			"",
			"localhost:4243",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.host, func(t *testing.T) {
			hostURL, err := ParseHostURL(tc.host)
			assert.Check(t, err)

			client := &Client{
				client: newMockClient(func(req *http.Request) (*http.Response, error) {
					if !strings.HasPrefix(req.URL.Path, testEndpoint) {
						return nil, fmt.Errorf("expected URL %q, got %q", testEndpoint, req.URL)
					}
					if req.Host != tc.expectedHost {
						return nil, fmt.Errorf("wxpected host %q, got %q", tc.expectedHost, req.Host)
					}
					if req.URL.Host != tc.expectedURLHost {
						return nil, fmt.Errorf("expected URL host %q, got %q", tc.expectedURLHost, req.URL.Host)
					}
					return &http.Response{
						StatusCode: http.StatusOK,
						Body:       io.NopCloser(bytes.NewReader([]byte(""))),
					}, nil
				}),

				proto:    hostURL.Scheme,
				addr:     hostURL.Host,
				basePath: hostURL.Path,
			}

			_, err = client.sendRequest(context.Background(), http.MethodGet, testEndpoint, nil, nil, nil)
			assert.Check(t, err)
		})
	}
}

// TestPlainTextError tests the server returning an error in plain text for
// backwards compatibility with API versions <1.24. All other tests use
// errors returned as JSON
func TestPlainTextError(t *testing.T) {
	client := &Client{
		client: newMockClient(plainTextErrorMock(http.StatusInternalServerError, "Server error")),
	}
	_, err := client.ContainerList(context.Background(), types.ContainerListOptions{})
	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
}

func TestInfiniteError(t *testing.T) {
	infinitR := rand.New(rand.NewSource(42))
	client := &Client{
		client: newMockClient(func(req *http.Request) (*http.Response, error) {
			resp := &http.Response{
				StatusCode: http.StatusInternalServerError,
				Header:     http.Header{},
				Body:       io.NopCloser(infinitR),
			}
			return resp, nil
		}),
	}

	_, err := client.Ping(context.Background())
	assert.Check(t, is.ErrorContains(err, "request returned Internal Server Error"))
}

func TestCanceledContext(t *testing.T) {
	const testEndpoint = "/test"

	client := &Client{
		client: newMockClient(func(req *http.Request) (*http.Response, error) {
			assert.Check(t, is.ErrorType(req.Context().Err(), context.Canceled))
			return nil, context.Canceled
		}),
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	_, err := client.sendRequest(ctx, http.MethodGet, testEndpoint, nil, nil, nil)
	assert.Check(t, is.ErrorType(err, errdefs.IsCancelled))
	assert.Check(t, errors.Is(err, context.Canceled))
}

func TestDeadlineExceededContext(t *testing.T) {
	const testEndpoint = "/test"

	client := &Client{
		client: newMockClient(func(req *http.Request) (*http.Response, error) {
			assert.Check(t, is.ErrorType(req.Context().Err(), context.DeadlineExceeded))
			return nil, context.DeadlineExceeded
		}),
	}

	ctx, cancel := context.WithDeadline(context.Background(), time.Now())
	defer cancel()

	<-ctx.Done()

	_, err := client.sendRequest(ctx, http.MethodGet, testEndpoint, nil, nil, nil)
	assert.Check(t, is.ErrorType(err, errdefs.IsDeadline))
	assert.Check(t, errors.Is(err, context.DeadlineExceeded))
}

package client // import "github.com/docker/docker/client"

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/errdefs"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

// TestSetHostHeader should set fake host for local communications, set real host
// for normal communications.
func TestSetHostHeader(t *testing.T) {
	const testEndpoint = "/test"
	testCases := []struct {
		host            string
		expectedHost    string
		expectedURLHost string
	}{
		{
			host:            "unix:///var/run/docker.sock",
			expectedHost:    DummyHost,
			expectedURLHost: "/var/run/docker.sock",
		},
		{
			host:            "npipe:////./pipe/docker_engine",
			expectedHost:    DummyHost,
			expectedURLHost: "//./pipe/docker_engine",
		},
		{
			host:            "tcp://0.0.0.0:4243",
			expectedHost:    "",
			expectedURLHost: "0.0.0.0:4243",
		},
		{
			host:            "tcp://localhost:4243",
			expectedHost:    "",
			expectedURLHost: "localhost:4243",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.host, func(t *testing.T) {
			hostURL, err := ParseHostURL(tc.host)
			assert.Check(t, err)

			client := &Client{
				client: newMockClient(func(req *http.Request) (*http.Response, error) {
					if !strings.HasPrefix(req.URL.Path, testEndpoint) {
						return nil, fmt.Errorf("expected URL %q, got %q", testEndpoint, req.URL)
					}
					if req.Host != tc.expectedHost {
						return nil, fmt.Errorf("wxpected host %q, got %q", tc.expectedHost, req.Host)
					}
					if req.URL.Host != tc.expectedURLHost {
						return nil, fmt.Errorf("expected URL host %q, got %q", tc.expectedURLHost, req.URL.Host)
					}
					return &http.Response{
						StatusCode: http.StatusOK,
						Body:       io.NopCloser(bytes.NewReader([]byte(""))),
					}, nil
				}),

				proto:    hostURL.Scheme,
				addr:     hostURL.Host,
				basePath: hostURL.Path,
			}

			_, err = client.sendRequest(context.Background(), http.MethodGet, testEndpoint, nil, nil, nil)
			assert.Check(t, err)
		})
	}
}

// TestPlainTextError tests the server returning an error in plain text for
// backwards compatibility with API versions <1.24. All other tests use
// errors returned as JSON
func TestPlainTextError(t *testing.T) {
	client := &Client{
		client: newMockClient(plainTextErrorMock(http.StatusInternalServerError, "Server error")),
	}
	_, err := client.ContainerList(context.Background(), types.ContainerListOptions{})
	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
}

func TestInfiniteError(t *testing.T) {
	infinitR := rand.New(rand.NewSource(42))
	client := &Client{
		client: newMockClient(func(req *http.Request) (*http.Response, error) {
			resp := &http.Response{
				StatusCode: http.StatusInternalServerError,
				Header:     http.Header{},
				Body:       io.NopCloser(infinitR),
			}
			return resp, nil
		}),
	}

	_, err := client.Ping(context.Background())
	assert.Check(t, is.ErrorContains(err, "request returned Internal Server Error"))
}

func TestCanceledContext(t *testing.T) {
	const testEndpoint = "/test"

	client := &Client{
		client: newMockClient(func(req *http.Request) (*http.Response, error) {
			assert.Check(t, is.ErrorType(req.Context().Err(), context.Canceled))
			return nil, context.Canceled
		}),
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	_, err := client.sendRequest(ctx, http.MethodGet, testEndpoint, nil, nil, nil)
	assert.Check(t, is.ErrorType(err, errdefs.IsCancelled))
	assert.Check(t, errors.Is(err, context.Canceled))
}

func TestDeadlineExceededContext(t *testing.T) {
	const testEndpoint = "/test"

	client := &Client{
		client: newMockClient(func(req *http.Request) (*http.Response, error) {
			assert.Check(t, is.ErrorType(req.Context().Err(), context.DeadlineExceeded))
			return nil, context.DeadlineExceeded
		}),
	}

	ctx, cancel := context.WithDeadline(context.Background(), time.Now())
	defer cancel()

	<-ctx.Done()

	_, err := client.sendRequest(ctx, http.MethodGet, testEndpoint, nil, nil, nil)
	assert.Check(t, is.ErrorType(err, errdefs.IsDeadline))
	assert.Check(t, errors.Is(err, context.DeadlineExceeded))
}

`expectedHost` is always empty. 
Useless field.