From c5b4eb94338d660cb1fe68c959add16d41c9d1a3 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Tue, 22 Aug 2023 14:46:10 +0200 Subject: [PATCH 1/3] api docs: document stream format Document the attach, exec and logs output stream format. We use the same format as docker. Fixes #19280 Signed-off-by: Paul Holzinger --- pkg/api/server/register_containers.go | 100 +++++++++++++++++++++++++- pkg/api/server/register_exec.go | 4 +- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 630da119c5..ceb1d5be7f 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -526,7 +526,12 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // tags: // - containers (compat) // summary: Attach to a container - // description: Hijacks the connection to forward the container's standard streams to the client. + // description: | + // Attach to a container to read its output or send it input. You can attach + // to the same container multiple times and you can reattach to containers + // that have been detached. + // + // It uses the same stream format as docker, see the libpod attach endpoint for a description of the format. // parameters: // - in: path // name: name @@ -964,7 +969,10 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // tags: // - containers // summary: Get container logs - // description: Get stdout and stderr logs from a container. + // description: | + // Get stdout and stderr logs from a container. + // + // The stream format is the same as described in the attach endpoint. // parameters: // - in: path // name: name @@ -1319,7 +1327,93 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // tags: // - containers // summary: Attach to a container - // description: Hijacks the connection to forward the container's standard streams to the client. + // description: | + // Attach to a container to read its output or send it input. You can attach + // to the same container multiple times and you can reattach to containers + // that have been detached. + // + // ### Hijacking + // + // This endpoint hijacks the HTTP connection to transport `stdin`, `stdout`, + // and `stderr` on the same socket. + // + // This is the response from the service for an attach request: + // + // ``` + // HTTP/1.1 200 OK + // Content-Type: application/vnd.docker.raw-stream + // + // [STREAM] + // ``` + // + // After the headers and two new lines, the TCP connection can now be used + // for raw, bidirectional communication between the client and server. + // + // To inform potential proxies about connection hijacking, the client + // can also optionally send connection upgrade headers. + // + // For example, the client sends this request to upgrade the connection: + // + // ``` + // POST /v4.6.0/libpod/containers/16253994b7c4/attach?stream=1&stdout=1 HTTP/1.1 + // Upgrade: tcp + // Connection: Upgrade + // ``` + // + // The service will respond with a `101 UPGRADED` response, and will + // similarly follow with the raw stream: + // + // ``` + // HTTP/1.1 101 UPGRADED + // Content-Type: application/vnd.docker.raw-stream + // Connection: Upgrade + // Upgrade: tcp + // + // [STREAM] + // ``` + // + // ### Stream format + // + // When the TTY setting is disabled for the container, + // the HTTP Content-Type header is set to application/vnd.docker.multiplexed-stream + // and the stream over the hijacked connected is multiplexed to separate out + // `stdout` and `stderr`. The stream consists of a series of frames, each + // containing a header and a payload. + // + // The header contains the information about the output stream type and the size of + // the payload. + // It is encoded on the first eight bytes like this: + // + // ```go + // header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + // ``` + // + // `STREAM_TYPE` can be: + // + // - 0: `stdin` (is written on `stdout`) + // - 1: `stdout` + // - 2: `stderr` + // + // `SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of the `uint32` size + // encoded as big endian. + // + // Following the header is the payload, which contains the specified number of + // bytes as written in the size. + // + // The simplest way to implement this protocol is the following: + // + // 1. Read 8 bytes. + // 2. Choose `stdout` or `stderr` depending on the first byte. + // 3. Extract the frame size from the last four bytes. + // 4. Read the extracted size and output it on the correct output. + // 5. Goto 1. + // + // ### Stream format when using a TTY + // + // When the TTY setting is enabled for the container, + // the stream is not multiplexed. The data exchanged over the hijacked + // connection is simply the raw data from the process PTY and client's + // `stdin`. // parameters: // - in: path // name: name diff --git a/pkg/api/server/register_exec.go b/pkg/api/server/register_exec.go index cf1fb8c167..34a9171a3e 100644 --- a/pkg/api/server/register_exec.go +++ b/pkg/api/server/register_exec.go @@ -254,7 +254,9 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // tags: // - exec // summary: Start an exec instance - // description: Starts a previously set up exec instance. If detach is true, this endpoint returns immediately after starting the command. Otherwise, it sets up an interactive session with the command. + // description: | + // Starts a previously set up exec instance. If detach is true, this endpoint returns immediately after starting the command. + // Otherwise, it sets up an interactive session with the command. The stream format is the same as the attach endpoint. // parameters: // - in: path // name: id From 243f365aa4bea910c444ebd9d230ccf04ae54f30 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 24 Aug 2023 15:31:54 +0200 Subject: [PATCH 2/3] create apiutils package Move SupportedVersion() and IsLibpodRequest() to separate package to avoid import cycle when using it in libpod. Signed-off-by: Paul Holzinger --- pkg/api/handlers/libpod/manifests.go | 3 +- pkg/api/handlers/utils/apiutil/apiutil.go | 69 +++++++++ .../handlers/utils/apiutil/apiutil_test.go | 140 ++++++++++++++++++ pkg/api/handlers/utils/handler.go | 51 +------ pkg/api/handlers/utils/handler_test.go | 135 ----------------- 5 files changed, 214 insertions(+), 184 deletions(-) create mode 100644 pkg/api/handlers/utils/apiutil/apiutil.go create mode 100644 pkg/api/handlers/utils/apiutil/apiutil_test.go diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index 0434d5fbc0..b2e6272e58 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -17,6 +17,7 @@ import ( "github.com/containers/podman/v4/libpod" "github.com/containers/podman/v4/pkg/api/handlers" "github.com/containers/podman/v4/pkg/api/handlers/utils" + "github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil" api "github.com/containers/podman/v4/pkg/api/types" "github.com/containers/podman/v4/pkg/auth" "github.com/containers/podman/v4/pkg/channel" @@ -80,7 +81,7 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) { } status := http.StatusOK - if _, err := utils.SupportedVersion(r, "< 4.0.0"); err == utils.ErrVersionNotSupported { + if _, err := utils.SupportedVersion(r, "< 4.0.0"); err == apiutil.ErrVersionNotSupported { status = http.StatusCreated } diff --git a/pkg/api/handlers/utils/apiutil/apiutil.go b/pkg/api/handlers/utils/apiutil/apiutil.go new file mode 100644 index 0000000000..b33627e31e --- /dev/null +++ b/pkg/api/handlers/utils/apiutil/apiutil.go @@ -0,0 +1,69 @@ +package apiutil + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/blang/semver/v4" + "github.com/containers/podman/v4/version" + "github.com/gorilla/mux" +) + +var ( + // ErrVersionNotGiven returned when version not given by client + ErrVersionNotGiven = errors.New("version not given in URL path") + // ErrVersionNotSupported returned when given version is too old + ErrVersionNotSupported = errors.New("given version is not supported") +) + +// IsLibpodRequest returns true if the request related to a libpod endpoint +// (e.g., /v2/libpod/...). +func IsLibpodRequest(r *http.Request) bool { + split := strings.Split(r.URL.String(), "/") + return len(split) >= 3 && split[2] == "libpod" +} + +// SupportedVersion validates that the version provided by client is included in the given condition +// https://github.com/blang/semver#ranges provides the details for writing conditions +// If a version is not given in URL path, ErrVersionNotGiven is returned +func SupportedVersion(r *http.Request, condition string) (semver.Version, error) { + version := semver.Version{} + val, ok := mux.Vars(r)["version"] + if !ok { + return version, ErrVersionNotGiven + } + safeVal, err := url.PathUnescape(val) + if err != nil { + return version, fmt.Errorf("unable to unescape given API version: %q: %w", val, err) + } + version, err = semver.ParseTolerant(safeVal) + if err != nil { + return version, fmt.Errorf("unable to parse given API version: %q from %q: %w", safeVal, val, err) + } + + inRange, err := semver.ParseRange(condition) + if err != nil { + return version, err + } + + if inRange(version) { + return version, nil + } + return version, ErrVersionNotSupported +} + +// SupportedVersionWithDefaults validates that the version provided by client valid is supported by server +// minimal API version <= client path version <= maximum API version focused on the endpoint tree from URL +func SupportedVersionWithDefaults(r *http.Request) (semver.Version, error) { + tree := version.Compat + if IsLibpodRequest(r) { + tree = version.Libpod + } + + return SupportedVersion(r, + fmt.Sprintf(">=%s <=%s", version.APIVersion[tree][version.MinimalAPI].String(), + version.APIVersion[tree][version.CurrentAPI].String())) +} diff --git a/pkg/api/handlers/utils/apiutil/apiutil_test.go b/pkg/api/handlers/utils/apiutil/apiutil_test.go new file mode 100644 index 0000000000..b86dc719cd --- /dev/null +++ b/pkg/api/handlers/utils/apiutil/apiutil_test.go @@ -0,0 +1,140 @@ +package apiutil + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/containers/podman/v4/version" + "github.com/gorilla/mux" +) + +func TestSupportedVersion(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, + fmt.Sprintf("/v%s/libpod/testing/versions", version.APIVersion[version.Libpod][version.CurrentAPI]), + nil) + if err != nil { + t.Fatal(err) + } + req = mux.SetURLVars(req, map[string]string{"version": version.APIVersion[version.Libpod][version.CurrentAPI].String()}) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := SupportedVersionWithDefaults(r) + switch { + case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + case errors.Is(err, ErrVersionNotSupported): // version given but not supported + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + case err != nil: + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + default: // all good + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "OK") + } + }) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := `OK` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %q want %q", + rr.Body.String(), expected) + } +} + +func TestUnsupportedVersion(t *testing.T) { + version := "999.999.999" + req, err := http.NewRequest(http.MethodGet, + fmt.Sprintf("/v%s/libpod/testing/versions", version), + nil) + if err != nil { + t.Fatal(err) + } + req = mux.SetURLVars(req, map[string]string{"version": version}) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := SupportedVersionWithDefaults(r) + switch { + case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + case errors.Is(err, ErrVersionNotSupported): // version given but not supported + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + case err != nil: + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + default: // all good + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "OK") + } + }) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusBadRequest) + } + + // Check the response body is what we expect. + expected := ErrVersionNotSupported.Error() + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %q want %q", + rr.Body.String(), expected) + } +} + +func TestEqualVersion(t *testing.T) { + version := "1.30.0" + req, err := http.NewRequest(http.MethodGet, + fmt.Sprintf("/v%s/libpod/testing/versions", version), + nil) + if err != nil { + t.Fatal(err) + } + req = mux.SetURLVars(req, map[string]string{"version": version}) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := SupportedVersion(r, "=="+version) + switch { + case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + case errors.Is(err, ErrVersionNotSupported): // version given but not supported + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + case err != nil: + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + default: // all good + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "OK") + } + }) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := http.StatusText(http.StatusOK) + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %q want %q", + rr.Body.String(), expected) + } +} diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go index 1e5aa2c12d..5ebe3f7d37 100644 --- a/pkg/api/handlers/utils/handler.go +++ b/pkg/api/handlers/utils/handler.go @@ -1,79 +1,34 @@ package utils import ( - "errors" "fmt" "io" "net/http" "net/url" "os" - "strings" "unsafe" "github.com/blang/semver/v4" - "github.com/containers/podman/v4/version" "github.com/gorilla/mux" "github.com/gorilla/schema" jsoniter "github.com/json-iterator/go" "github.com/sirupsen/logrus" + "github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil" api "github.com/containers/podman/v4/pkg/api/types" ) -var ( - // ErrVersionNotGiven returned when version not given by client - ErrVersionNotGiven = errors.New("version not given in URL path") - // ErrVersionNotSupported returned when given version is too old - ErrVersionNotSupported = errors.New("given version is not supported") -) - // IsLibpodRequest returns true if the request related to a libpod endpoint // (e.g., /v2/libpod/...). func IsLibpodRequest(r *http.Request) bool { - split := strings.Split(r.URL.String(), "/") - return len(split) >= 3 && split[2] == "libpod" + return apiutil.IsLibpodRequest(r) } // SupportedVersion validates that the version provided by client is included in the given condition // https://github.com/blang/semver#ranges provides the details for writing conditions // If a version is not given in URL path, ErrVersionNotGiven is returned func SupportedVersion(r *http.Request, condition string) (semver.Version, error) { - version := semver.Version{} - val, ok := mux.Vars(r)["version"] - if !ok { - return version, ErrVersionNotGiven - } - safeVal, err := url.PathUnescape(val) - if err != nil { - return version, fmt.Errorf("unable to unescape given API version: %q: %w", val, err) - } - version, err = semver.ParseTolerant(safeVal) - if err != nil { - return version, fmt.Errorf("unable to parse given API version: %q from %q: %w", safeVal, val, err) - } - - inRange, err := semver.ParseRange(condition) - if err != nil { - return version, err - } - - if inRange(version) { - return version, nil - } - return version, ErrVersionNotSupported -} - -// SupportedVersionWithDefaults validates that the version provided by client valid is supported by server -// minimal API version <= client path version <= maximum API version focused on the endpoint tree from URL -func SupportedVersionWithDefaults(r *http.Request) (semver.Version, error) { - tree := version.Compat - if IsLibpodRequest(r) { - tree = version.Libpod - } - - return SupportedVersion(r, - fmt.Sprintf(">=%s <=%s", version.APIVersion[tree][version.MinimalAPI].String(), - version.APIVersion[tree][version.CurrentAPI].String())) + return apiutil.SupportedVersion(r, condition) } // WriteResponse encodes the given value as JSON or string and renders it for http client diff --git a/pkg/api/handlers/utils/handler_test.go b/pkg/api/handlers/utils/handler_test.go index afa6f17eb1..099f4169b9 100644 --- a/pkg/api/handlers/utils/handler_test.go +++ b/pkg/api/handlers/utils/handler_test.go @@ -1,144 +1,9 @@ package utils import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" "testing" - - "github.com/containers/podman/v4/version" - "github.com/gorilla/mux" ) -func TestSupportedVersion(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, - fmt.Sprintf("/v%s/libpod/testing/versions", version.APIVersion[version.Libpod][version.CurrentAPI]), - nil) - if err != nil { - t.Fatal(err) - } - req = mux.SetURLVars(req, map[string]string{"version": version.APIVersion[version.Libpod][version.CurrentAPI].String()}) - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := SupportedVersionWithDefaults(r) - switch { - case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, err.Error()) - case errors.Is(err, ErrVersionNotSupported): // version given but not supported - w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, err.Error()) - case err != nil: - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, err.Error()) - default: // all good - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "OK") - } - }) - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - // Check the response body is what we expect. - expected := `OK` - if rr.Body.String() != expected { - t.Errorf("handler returned unexpected body: got %q want %q", - rr.Body.String(), expected) - } -} - -func TestUnsupportedVersion(t *testing.T) { - version := "999.999.999" - req, err := http.NewRequest(http.MethodGet, - fmt.Sprintf("/v%s/libpod/testing/versions", version), - nil) - if err != nil { - t.Fatal(err) - } - req = mux.SetURLVars(req, map[string]string{"version": version}) - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := SupportedVersionWithDefaults(r) - switch { - case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, err.Error()) - case errors.Is(err, ErrVersionNotSupported): // version given but not supported - w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, err.Error()) - case err != nil: - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, err.Error()) - default: // all good - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "OK") - } - }) - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := ErrVersionNotSupported.Error() - if rr.Body.String() != expected { - t.Errorf("handler returned unexpected body: got %q want %q", - rr.Body.String(), expected) - } -} - -func TestEqualVersion(t *testing.T) { - version := "1.30.0" - req, err := http.NewRequest(http.MethodGet, - fmt.Sprintf("/v%s/libpod/testing/versions", version), - nil) - if err != nil { - t.Fatal(err) - } - req = mux.SetURLVars(req, map[string]string{"version": version}) - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := SupportedVersion(r, "=="+version) - switch { - case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, err.Error()) - case errors.Is(err, ErrVersionNotSupported): // version given but not supported - w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, err.Error()) - case err != nil: - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, err.Error()) - default: // all good - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "OK") - } - }) - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - // Check the response body is what we expect. - expected := http.StatusText(http.StatusOK) - if rr.Body.String() != expected { - t.Errorf("handler returned unexpected body: got %q want %q", - rr.Body.String(), expected) - } -} - func TestErrorEncoderFuncOmit(t *testing.T) { data, err := json.Marshal(struct { Err error `json:"err,omitempty"` From 7c9c969815d7c99212198b4b87fad435cdd89971 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 24 Aug 2023 16:11:39 +0200 Subject: [PATCH 3/3] API attach: return vnd.docker.multiplexed-stream header The attach API used to always return the Content-Type `vnd.docker.raw-stream`, however docker api v1.42 added the `vnd.docker.multiplexed-stream` type when no tty was used. Follow suit and return the same header for docker api v1.42 and libpod v4.7.0. This technically allows clients to make a small optimization as they no longer need to inspect the container to see if they get a raw or multiplexed stream. Signed-off-by: Paul Holzinger --- libpod/oci_conmon_common.go | 2 +- libpod/oci_conmon_exec_common.go | 2 +- libpod/util.go | 23 +++++++++++++++++++---- pkg/api/server/register_containers.go | 1 + test/apiv2/20-containers.at | 15 +++++++++++++++ 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/libpod/oci_conmon_common.go b/libpod/oci_conmon_common.go index f315d94397..ee20a7c559 100644 --- a/libpod/oci_conmon_common.go +++ b/libpod/oci_conmon_common.go @@ -614,7 +614,7 @@ func (r *ConmonOCIRuntime) HTTPAttach(ctr *Container, req *http.Request, w http. hijackDone <- true - writeHijackHeader(req, httpBuf) + writeHijackHeader(req, httpBuf, isTerminal) // Force a flush after the header is written. if err := httpBuf.Flush(); err != nil { diff --git a/libpod/oci_conmon_exec_common.go b/libpod/oci_conmon_exec_common.go index 24113bd8d2..b1da24445f 100644 --- a/libpod/oci_conmon_exec_common.go +++ b/libpod/oci_conmon_exec_common.go @@ -569,7 +569,7 @@ func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.Resp hijackDone <- true // Write a header to let the client know what happened - writeHijackHeader(r, httpBuf) + writeHijackHeader(r, httpBuf, isTerminal) // Force a flush after the header is written. if err := httpBuf.Flush(); err != nil { diff --git a/libpod/util.go b/libpod/util.go index 94eef3a03c..ce2ce2d96f 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -15,6 +15,7 @@ import ( "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/libpod/define" + "github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux/label" "github.com/sirupsen/logrus" @@ -182,22 +183,36 @@ func makeHTTPAttachHeader(stream byte, length uint32) []byte { // writeHijackHeader writes a header appropriate for the type of HTTP Hijack // that occurred in a hijacked HTTP connection used for attach. -func writeHijackHeader(r *http.Request, conn io.Writer) { +func writeHijackHeader(r *http.Request, conn io.Writer, tty bool) { // AttachHeader is the literal header sent for upgraded/hijacked connections for // attach, sourced from Docker at: // https://raw.githubusercontent.com/moby/moby/b95fad8e51bd064be4f4e58a996924f343846c85/api/server/router/container/container_routes.go // Using literally to ensure compatibility with existing clients. + + // New docker API uses a different header for the non tty case. + // Lets do the same for libpod. Only do this for the new api versions to not break older clients. + header := "application/vnd.docker.raw-stream" + if !tty { + version := "4.7.0" + if !apiutil.IsLibpodRequest(r) { + version = "1.42.0" // docker only used two digest "1.42" but our semver lib needs the extra .0 to work + } + if _, err := apiutil.SupportedVersion(r, ">= "+version); err == nil { + header = "application/vnd.docker.multiplexed-stream" + } + } + c := r.Header.Get("Connection") proto := r.Header.Get("Upgrade") if len(proto) == 0 || !strings.EqualFold(c, "Upgrade") { // OK - can't upgrade if not requested or protocol is not specified fmt.Fprintf(conn, - "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + "HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n", header) } else { // Upgraded fmt.Fprintf(conn, - "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: %s\r\n\r\n", - proto) + "HTTP/1.1 101 UPGRADED\r\nContent-Type: %s\r\nConnection: Upgrade\r\nUpgrade: %s\r\n\r\n", + proto, header) } } diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index ceb1d5be7f..4cc33682e7 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -1376,6 +1376,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // // When the TTY setting is disabled for the container, // the HTTP Content-Type header is set to application/vnd.docker.multiplexed-stream + // (starting with v4.7.0, previously application/vnd.docker.raw-stream was always used) // and the stream over the hijacked connected is multiplexed to separate out // `stdout` and `stderr`. The stream consists of a series of frames, each // containing a header and a payload. diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index 92074b0b28..f790efe78b 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -30,6 +30,21 @@ podman run --rm -d --replace --name foo $IMAGE sh -c "echo $mytext;sleep 42" # Looks like it is missing the required 0 bytes from the message, why? t POST "containers/foo/attach?logs=true&stream=false" 200 \ $'\001\031'$mytext + +# check old docker header +response_headers=$(cat "$WORKDIR/curl.headers.out") +like "$response_headers" ".*Content-Type: application/vnd\.docker\.raw-stream.*" "vnd.docker.raw-stream docker v1.40" +# check new vnd.docker.multiplexed-stream header +t POST "/v1.42/containers/foo/attach?logs=true&stream=false" 200 +response_headers=$(cat "$WORKDIR/curl.headers.out") +like "$response_headers" ".*Content-Type: application/vnd\.docker\.multiplexed-stream.*" "vnd.docker.multiplexed-stream docker v1.42" +t POST "/v4.6.0/libpod/containers/foo/attach?logs=true&stream=false" 200 +response_headers=$(cat "$WORKDIR/curl.headers.out") +like "$response_headers" ".*Content-Type: application/vnd\.docker\.raw-stream.*" "vnd.docker.raw-stream libpod v4.6.0" +t POST "/v4.7.0/libpod/containers/foo/attach?logs=true&stream=false" 200 +response_headers=$(cat "$WORKDIR/curl.headers.out") +like "$response_headers" ".*Content-Type: application/vnd\.docker\.multiplexed-stream.*" "vnd.docker.multiplexed-stream libpod v4.7.0" + t POST "containers/foo/kill" 204 podman run --replace --name=foo -v /tmp:/tmp $IMAGE true