Compare commits

..
74 Commits
Author SHA1 Message Date
Sebastiaan van Stijn a82e528901 Merge pull request #7418 from vvoland/7417-29.x
[29.x] Update to go1.26.10
2026-10-20 13:24:07 +02:00
Paweł Gronowski e35bc1b6ae Update to go1.26.10
These releases include 1 security fix following the security policy:

- crypto/x509: mitigation for security concerns with a specific Certificate Authority

  We're blocking trust in a small set of intermediate certificates due to specific security concerns around their operation.

  Verify will now filter out chains which contain these intermediates, preventing them from being returned to the caller.

  We highly recommend immediately upgrading to this release.

  This is CVE-2026-111943 and Go issue https://go.dev/issue/82345.

release notes: https://go.dev/doc/devel/release#go1.26.10

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-20 12:42:59 +02:00
Paweł Gronowski baf6c13889 Bump version
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-20 12:40:09 +02:00
Paweł Gronowski 4051f75f69 Merge tag 'v29.9.1' into 7417-29.x
v29.9.1

# -----BEGIN PGP SIGNATURE-----
#
# iK0EABYKAFUWIQQX90wwlHYgZQhz61ZuYvmhLGnMZgUCas/KIxsUgAAAAAAEAA5t
# YW51MiwyLjUrMS4xMiwwLDMbHHBhd2VsLmdyb25vd3NraUBkb2NrZXIuY29tAAoJ
# EG5i+aEsacxmKWgBAKZGzyE7LzPW1Xy2ETngj+i8n96ZxkRPuzU7b2Rd+XLlAQCc
# YddGBSzdhrlh6EQtYdYMx7ArXfx7g8T10tGORAKqBw==
# =7Syv
# -----END PGP SIGNATURE-----
# gpg: Signature made Wed Oct 14 20:29:55 2026 CEST
# gpg:                using EDDSA key 17F74C30947620650873EB566E62F9A12C69CC66
# gpg:                issuer "pawel.gronowski@docker.com"
# gpg: Good signature from "Paweł Gronowski <pawel.gronowski@docker.com>" [ultimate]
2026-10-20 12:39:56 +02:00
Paweł Gronowski 76321a699d Merge tag 'v29.9.0' into 7417-29.x
v29.9.0

# -----BEGIN PGP SIGNATURE-----
#
# iK0EABYKAFUWIQQX90wwlHYgZQhz61ZuYvmhLGnMZgUCasf+dhsUgAAAAAAEAA5t
# YW51MiwyLjUrMS4xMiwwLDMbHHBhd2VsLmdyb25vd3NraUBkb2NrZXIuY29tAAoJ
# EG5i+aEsacxm3EYBAKRhamCCxqMBniTJ9GtLRVn7G3gbweSztHdAzhCo70C6AP41
# K+a4eqECp+bjqTK1ldegnmdyAPZ8g8AoPMpJgSQ2CA==
# =N5Qp
# -----END PGP SIGNATURE-----
# gpg: Signature made Thu Oct  8 22:35:02 2026 CEST
# gpg:                using EDDSA key 17F74C30947620650873EB566E62F9A12C69CC66
# gpg:                issuer "pawel.gronowski@docker.com"
# gpg: Good signature from "Paweł Gronowski <pawel.gronowski@docker.com>" [ultimate]
2026-10-20 12:39:56 +02:00
Paweł Gronowski b82456c62f Merge tag 'v29.8.2' into 7417-29.x
v29.8.2

# -----BEGIN PGP SIGNATURE-----
#
# iK0EABYKAFUWIQQX90wwlHYgZQhz61ZuYvmhLGnMZgUCar1jPxsUgAAAAAAEAA5t
# YW51MiwyLjUrMS4xMiwwLDMbHHBhd2VsLmdyb25vd3NraUBkb2NrZXIuY29tAAoJ
# EG5i+aEsacxmaQABAKG/rnQXNds4jV6qfRvaOS+tGa3Gy2hTY6o2aOgX3g3LAP4n
# OF3/gfVEcaQAK2dgxYTQnQvZi+/p69M1Bjsmy8ytBg==
# =x2Qn
# -----END PGP SIGNATURE-----
# gpg: Signature made Wed Sep 30 21:30:07 2026 CEST
# gpg:                using EDDSA key 17F74C30947620650873EB566E62F9A12C69CC66
# gpg:                issuer "pawel.gronowski@docker.com"
# gpg: Good signature from "Paweł Gronowski <pawel.gronowski@docker.com>" [ultimate]
2026-10-20 12:39:55 +02:00
Paweł Gronowski a98becc3e3 Bump version
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-20 12:36:49 +02:00
Paweł Gronowski dfc520dcc2 Merge pull request #7390 from renovate-bot/renovate/go-1.x
chore(deps): update dependency go to v1.27.1
2026-10-14 20:09:52 +02:00
Mend Renovate a3a2bb6118 chore(deps): update dependency go to v1.27.1
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-10-14 18:03:30 +00:00
Sebastiaan van Stijn c11295afc4 Merge pull request #7388 from renovate-bot/renovate/tonistiigi-xx-1.x
chore(deps): update tonistiigi/xx docker tag to v1.10.0
2026-10-14 18:11:18 +02:00
Sebastiaan van Stijn 010e125d8b Merge pull request #7386 from renovate-bot/renovate/docker-buildx-bin-0.x
chore(deps): update docker/buildx-bin docker tag to v0.38.0
2026-10-14 18:10:45 +02:00
Sebastiaan van Stijn 5cdf229230 Merge pull request #7387 from renovate-bot/renovate/docker-compose-bin-5.x
chore(deps): update docker/compose-bin docker tag to v5.6.0
2026-10-14 18:10:28 +02:00
Mend Renovate 66f6797582 chore(deps): update tonistiigi/xx docker tag to v1.10.0
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-10-14 16:02:38 +00:00
Mend Renovate cb8870a537 chore(deps): update docker/compose-bin docker tag to v5.6.0
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-10-14 16:02:33 +00:00
Mend Renovate 529f66d2f9 chore(deps): update docker/buildx-bin docker tag to v0.38.0
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-10-14 16:02:28 +00:00
Paweł Gronowski 4d6ff7d646 Merge pull request #7380 from renovate-bot/renovate/actions-upload-artifact-7.x
chore(deps): update actions/upload-artifact action to v7.0.2
2026-10-14 16:53:33 +02:00
Paweł Gronowski b997c5b7aa Merge pull request #7381 from renovate-bot/renovate/docker-actions
chore(deps): update docker/setup-buildx-action action to v4.4.1
2026-10-14 16:49:47 +02:00
Mend Renovate 1822903c1d chore(deps): update docker/setup-buildx-action action to v4.4.1
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-10-14 14:34:17 +00:00
Mend Renovate 37013cb262 chore(deps): update actions/upload-artifact action to v7.0.2
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-10-14 14:34:11 +00:00
Paweł Gronowski 61ee187552 Merge pull request #7251 from vvoland/add-renovate
Add Renovate config
2026-10-14 16:32:20 +02:00
Paweł Gronowski 433b13a249 Add Renovate config
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-14 15:25:17 +02:00
Sebastiaan van Stijn 5c8bfb2844 Merge pull request #7378 from thaJeztah/pr_7371_alt
cli-plugins: Add generic hook messages (alternative)
2026-10-13 17:27:57 +02:00
Paweł Gronowski 528a2ada2e Merge pull request #7375 from thaJeztah/bump_go_winio
vendor: github.com/Microsoft/go-winio v0.6.3
2026-10-13 17:25:25 +02:00
Nicolas Beck de8f4032ad cli-plugins: Add generic hook messages
Allow plugins to return informational messages without the next-steps heading
or indentation. Invoke each hook once and render generic messages before the
shared next-steps block.

Centralize grouping and formatting in PrintMessages, with a callback for
template evaluation and error handling. This avoids the need for an
intermediate EvaluatedMessage type and simplifies hook collection to return
the original responses.

Skip messages containing only whitespace while preserving empty lines
within non-empty messages. Keep PrintNextSteps as a deprecated wrapper
to preserve existing callers and output.

Update the printer tests and remove the obsolete appendMessages helper
and its tests.

Co-authored-by: Nicolas Beck <nicolas.beck@docker.com>
Signed-off-by: Nicolas Beck <nicolas.beck@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-12 16:31:43 +02:00
Sebastiaan van Stijn e4078058de Merge pull request #7373 from TanvirTian/7372-fix-remote-context-download-error
fix: return remote context download errors
2026-10-10 16:03:08 +02:00
Sebastiaan van Stijn 8da8b00555 vendor: github.com/Microsoft/go-winio v0.6.3
- improve interoperability with stdlib sentinel errors and deprecate aliases

full diff: https://github.com/Microsoft/go-winio/compare/f19d9717deb0...v0.6.3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-10 15:28:26 +02:00
Tanvir Tian 067d069f0c fix: return remote context download errors
Signed-off-by: Tanvir Tian <beachpilis@gmail.com>
2026-10-10 18:16:02 +06:00
Sebastiaan van Stijn 02fbbf3eb0 Merge pull request #7369 from nico1510/offload-plugin-guidance
cmd/docker: Add guidance for unavailable offload commands
2026-10-09 18:08:27 +02:00
Sebastiaan van Stijn 890976a13b Merge pull request #7370 from thaJeztah/bump_version
Bump version to 29.9.1
2026-10-09 15:12:08 +02:00
Sebastiaan van Stijn 23c66ccd61 Bump version to 29.9.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-09 13:45:39 +02:00
Nicolas Beck 3e686a8b59 cmd/docker: Add guidance for unavailable offload commands
Show installation guidance and a documentation link when the offload
plugin is missing or invalid. Handle this before command execution so
plugin-specific flags and --help also receive the guidance.

Signed-off-by: Nicolas Beck <nicolas.beck@docker.com>
2026-10-09 10:06:48 +00:00
Sebastiaan van Stijn f415da838b Merge pull request #7367 from thaJeztah/bump_trust_deps
cmd/docker-trust: update dependencies
2026-10-08 22:29:49 +02:00
Paweł Gronowski 45c2317672 Merge pull request #7366 from thaJeztah/bump_runewidth
vendor: github.com/mattn/go-runewidth v0.0.31
2026-10-08 22:27:38 +02:00
Paweł Gronowski 8308969eda Merge pull request #7368 from vvoland/update-docker
vendor: github.com/moby/moby/client v0.6.2
2026-10-08 22:26:44 +02:00
Sebastiaan van Stijn cf809998ab cmd/docker-trust: update dependencies
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-08 22:23:38 +02:00
Paweł Gronowski 730c880b35 vendor: github.com/moby/moby/client v0.6.2
full diff: https://github.com/moby/moby/compare/client/v0.6.1...client/v0.6.2

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-08 22:08:10 +02:00
Sebastiaan van Stijn e63e4fe498 vendor: github.com/mattn/go-runewidth v0.0.31
- Treat the remaining invisible format controls as zero width
- Treat bidi isolates and other invisible format characters as zero width
- Measure ambiguous Latin letters as one cell in East Asian locales
- Fix width mismatches found by the Hegel property tests
- Recognise CJK locales regardless of charset spelling

full diff: https://github.com/mattn/go-runewidth/compare/v0.0.30...v0.0.31

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-08 21:51:14 +02:00
Paweł Gronowski e7df01fe6e Merge pull request #7361 from thaJeztah/bump_go_connections
vendor: github.com/docker/go-connections v0.8.2
2026-10-08 20:31:48 +02:00
Sebastiaan van Stijn 2e51107855 Merge pull request #7363 from vvoland/update-go
Update to go1.26.9
2026-10-08 20:31:01 +02:00
Paweł Gronowski 949bf401dd vendor: golang.org/x/net v0.60.0
full diff: https://golang.org/x/net/compare/v0.59.0...v0.60.0

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-08 20:25:01 +02:00
Paweł Gronowski 9db9802127 Update to go1.26.9
This release includes 15 security fixes following the security policy:

- net/http: HTTP/2 server crash due to HPACK encoder race

  HTTP/2 servers could end up crashing due to inadvertently
  modifying its HPACK encoder concurrently. This happens because the
  server modifies the HPACK encoder from two goroutines without
  synchronization: one uses the encoder to encode a HEADERS frame as part
  of a response sent to a client and the other modifies the encoder's
  table size when handling a SETTINGS frame containing
  SETTINGS_HEADER_TABLE_SIZE that a client sends. A malicious client can
  repeatedly send a request while changing the header table size to crash
  the server.

  Fix this issue by not applying SETTINGS_HEADER_TABLE_SIZE immediately.
  Instead, buffer any SETTINGS_HEADER_TABLE_SIZE received, and only apply
  the new value prior to the next time the server writes a frame.

  Thanks to RyotaK (https://ryotak.net) of GMO Flatt Security Inc. for reporting this issue.

  This is CVE-2026-97032 and Go issue https://go.dev/issue/81867.

- net/http: HTTP/2 server memory exhaustion due to Trailer headers

  When "Trailer" headers are sent by a client, the HTTP server internally
  uses the header values to populate the Request.Trailer map passed to the
  server handler. Because Request.Trailer is a map, each entry incurs
  memory overhead. For HTTP/2 servers, a malicious client can exploit this
  by sending a "Trailer" header that declares a large number of fields,
  causing the server to allocate a disproportionate amount of memory while
  bypassing Server.MaxHeaderValueCount and Server.MaxHeaderBytes limits.
  This exploit is not applicable for HTTP/1 servers, which do not support
  multiplexing a large number of requests over one TCP connection, and
  whose Server.MaxHeaderBytes are calculated differently.

  Server.MaxHeaderValueCount and Server.MaxHeaderBytes limits are now
  applied towards the trailer fields declared in "Trailer" headers.

  Thanks to RyotaK (https://ryotak.net) of GMO Flatt Security Inc. for reporting this issue.

  This is CVE-2026-78659 and Go issue https://go.dev/issue/81857.

- crypto/tls: reject malformed ECH outer extension references

  Multiple ECH outer extension references are not
  permitted under RFC 9849; previously, a client
  could send a well-crafted packet that could
  trigger memory exhaustion in the server process
  by specifying multiple references.

  We now reject these as malformed and curb the
  memory amplification vector as a result.

  This is CVE-2026-97031 and Go issue https://go.dev/issue/81855.

- cmd/go: checksum bypass for golang.org/fips140

  Previously, a user operating inside of a malicious
  Go project that defines a bogus golang.org/fips140
  and operates a malicious GOMODPROXY the user chooses
  to connect to can serve an arbitrary module in its
  place.

  We now unpack the trusted ziphash for the bundled
  golang.org/fips140 module and construct its entry
  in the GOMODCACHE such that it can be verified by
  the toolchain.

  This is CVE-2026-94444 and Go issue https://go.dev/issue/81833.

- cmd/go: checksum database bypass for golang.org/toolchain

  Previously, a user operating inside of a malicious
  Go project that defines a bogus golang.org/toolchain
  go.sum entry and operates a malicious GOMODPROXY the
  user chooses to use can bypass the intended checksum.

  We now ensure that golang.org/toolchain always goes
  to the network for the canonical checksum.

  This is CVE-2026-94447 and Go issue https://go.dev/issue/81834.

- html/template: reset context tracking on consecutive template expressions

  When a JavaScript template literal contains
  consecutive expressions, the context tracking
  state was not properly reset upon entering a
  new expression.

  We now ensure that template-literal expression
  entries correctly reset context variables so all
  subsequent regular expression literals are
  accurately recognized and escaped.

  This is CVE-2026-94448 and Go issue https://go.dev/issue/81821.

- html/template: recognize yield as regexp preceder keyword

  A trusted template author may have previously
  written a valid template wherein the use of
  the yield keyword would not be correctly
  escaped.

  We now ensure that valid keyword uses are
  escaped and non-keyword uses are not escaped.

  This is CVE-2026-97030 and Go issue https://go.dev/issue/81823.

- net/textproto, mime/multipart: memory limit bypass when parsing MIME headers

  Parsing a multipart form could bypass memory limits and read an
  arbitrarily long line into memory when the remaining limit at the
  start of a part was less than 400 bytes.

  Multipart form memory limits are now properly enforced in this situation.

  Thanks to Jakub Ciolek (https://ciolek.dev) for reporting this issue.

  This is CVE-2026-94440 and Go issue https://go.dev/issue/81741.

- net/http: HTTP/1 client connection desynchronization after CONNECT rejection

  When http.Transport sends an HTTP/1 CONNECT request with a non-empty
  Request.Body, it writes the body directly to the connection without
  framing after the request headers. If the server rejects the CONNECT
  request with a non-2xx keep-alive response, Transport returns the
  connection to the idle pool. Because CONNECT requests do not have a
  request body, the server may interpret the trailing body bytes as a
  subsequent pipelined HTTP/1.1 request on the connection, leaving the
  pooled connection desynchronized and causing the next caller that reuses
  it to read the response to the injected request. In reverse proxies
  (including httputil.ReverseProxy) that forward CONNECT requests through
  a shared Transport, this can lead to cross-user response poisoning.

  The HTTP/1 transport now closes a connection after sending a CONNECT
  request, regardless of the response status.

  In addition, ReverseProxy now rejects incoming CONNECT requests
  with a 405 Method Not Allowed response. ReverseProxy has never handled
  CONNECT requests in a useful fashion (it does not convert the
  connection into a bidirectional tunnel), so we do not expect this
  change to negatively affect any current users.

  Thanks to Xclow3n (Rajat Raghav) for reporting this issue.

  This is CVE-2026-56866 and Go issue https://go.dev/issue/81740.

- net/http: HTTP/1 server connection desynchronization after 2xx CONNECT response

  When an HTTP server handler sent a 2xx response to an HTTP/1 CONNECT request
  and returned without hijacking the connection, the server improperly continued
  to read and serve requests from the connection. Since a 2xx response to an
  HTTP/1 CONNECT converts the connection into a tunnel, the server should not
  treat the connection as continuing to contain HTTP.

  The impact of this misbehavior is mostly limited to potential request smuggling,
  where an intermediate proxy considers the data on the connection to be tunneled
  and the server considers it to be HTTP.

  The HTTP/1 server now always closes a connection after responding to a CONNECT
  request, regardless of the response status.

  Thanks to Jakub Ciolek (https://ciolek.dev) for reporting this issue.

  This is CVE-2026-94439 and Go issue https://go.dev/issue/81744.

- net/http: excessive CPU consumption from repeated initial window changes

  A malicious HTTP/2 peer could cause excessive CPU consumption in the
  client or server by opening a large number of streams and then sending
  many small SETTINGS frames containing SETTINGS_INITIAL_WINDOW_SIZE values.

  The HTTP/2 client and server now efficiently handle changes to the
  initial window size (O(1) rather than O(number of streams)).

  Thanks to Jakub Ciolek (https://ciolek.dev) for reporting this issue.

  This is CVE-2026-78669 and Go issue https://go.dev/issue/81742.

- net/http: HTTP/2 transport accepts malformed framing-related headers

  Historically, we have been rather lax about malformed framing-related
  headers in our HTTP/2 implementation, as they cannot interfere with
  HTTP/2 framing. However, this makes it possible for our HTTP/2
  implementation to forward responses containing such headers to an HTTP/1
  client when acting as a reverse proxy. If the HTTP/1 client also does
  not behave strictly enough, this can result in response smuggling.

  We now delete malformed framing-related headers when received by our
  HTTP/2 transport, so they will not be forwarded to a potentially
  vulnerable HTTP/1 client.

  Thanks to TJ Barton for reporting this issue.

  This is CVE-2026-78660 and Go issue https://go.dev/issue/81115.

- os: Root.Mkdir(All) can follow junctions out of the root on Windows

  On Windows, when the target of Root.Mkdir or Root.MkdirAll was a junction
  pointing to an empty location, the operation would create a directory at
  the junction target even when that target was located outside the root.
  This only applies to operations where the last path component is a
  junction (path/to/junction, but not path/junction/target).

  Root.Mkdir and Root.MkdirAll now correctly avoid resolving junctions.

  Thanks to Daniele Ballarini for reporting this issue.

  This is CVE-2026-56857 and Go issue https://go.dev/issue/81739.

- net/http: lack of limit on size of parsed Range headers

  When parsing a Range header containing a large number of small ranges,
  FileServer(FS), ServeContent, and ServeFile(FS) could consume an excessive
  amount of CPU.

  These functions now ignore Range headers containing more than 200 ranges.

  The limit is controlled by the new httpservecontentmaxranges=
  GODEBUG setting. Setting GODEBUG=httpservecontentmaxranges=0
  disables the limit.

  Thanks to Jakub Ciolek (https://ciolek.dev) for reporting this issue.

  This is CVE-2026-78667 and Go issue https://go.dev/issue/81858.

- net/http: double flow control refund on HTTP/2 server streams

  The HTTP/2 server could refund connection-level flow control twice for the same data:
  Once when a client resets a stream (refunding data for any sent-but-unread portion
  of the stream), and again when a request handler reads the buffered data.
  A malicious client could exploit this to bypass the configured connection-level
  flow control limit (MaxReceiveBufferPerConnection). Total buffered data is still
  limited by the concurrent stream limit and stream-level flow control.

  The HTTP/2 server now waits to refund connection-level flow control for reset
  streams until after the request handler is complete.

  Thanks to Ali Sherif (https://www.linkedin.com/in/ali-sherif-13812b276/) for reporting this issue.

  This is CVE-2026-78663 and Go issue https://go.dev/issue/81743.

release notes: https://go.dev/doc/devel/release#go1.26.9

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-08 20:19:46 +02:00
Sebastiaan van Stijn 63445d6b70 vendor: github.com/docker/go-connections v0.8.2
v0.8.2

- sockets: preserve configured TLS ALPN protocols
- ci: add dependabot to update actions
- ci: update runners to Ubuntu 26.04
- build(deps): bump actions/checkout from 6.0.2 to 7.0.1
- build(deps): bump actions/setup-go from 6.3.0 to 7.0.0
- build(deps): bump golangci/golangci-lint-action from 9.2.0 to 9.3.0

full diff: https://github.com/docker/go-connections/compare/v0.8.1...v0.8.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-08 16:03:13 +02:00
Paweł Gronowski fdb4ee3e75 Merge pull request #7360 from thaJeztah/bump_ansiterm
vendor: github.com/Azure/go-ansiterm 8c912ac31dd4
2026-10-08 14:40:57 +02:00
Sebastiaan van Stijn 4ae247756e vendor: github.com/Azure/go-ansiterm 8c912ac31dd4
- update minimum go version to go1.17
- update minimum golang.org/x/sys to v0.1.0

full diff: https://github.com/Azure/go-ansiterm/compare/e937bb47801a...8c912ac31dd4

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-07 23:56:10 +02:00
Paweł Gronowski eabc65846b Merge tag 'v29.8.1' into 29.x
v29.8.1
2026-09-29 14:09:44 +02:00
Paweł Gronowski b873f57cd0 Merge tag 'v29.8.0' into 29.x
v29.8.0
2026-09-29 14:09:44 +02:00
Paweł Gronowski c8a849c2f9 Merge tag 'v29.8.0-rc.1' into 29.x
v29.8.0-rc.1
2026-09-29 14:09:44 +02:00
Paweł Gronowski f4efc1b596 Merge tag 'v29.7.2' into 29.x
v29.7.2
2026-08-11 13:21:01 +02:00
Paweł Gronowski 2747e2e3bf Merge tag 'v29.7.1' into 29.x
v29.7.1
2026-08-11 13:21:01 +02:00
Paweł Gronowski 329cdf1a9d Merge tag 'v29.7.0' into 29.x
v29.7.0
2026-08-11 13:21:01 +02:00
Paweł Gronowski 0c7e2e7203 Merge tag 'v29.7.0-rc.1' into 29.x
v29.7.0-rc.1
2026-08-11 13:21:00 +02:00
Paweł Gronowski dfc4efb1e2 Merge pull request #7109 from vvoland/update-docker
[29.x] Bump to 29.6.2
2026-07-16 18:09:24 +02:00
Paweł Gronowski dba867594c Bump to 29.6.2
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-16 12:51:30 +02:00
Paweł Gronowski d9aefe565a Merge tag 'v29.6.1' into 29.x
v29.6.1
2026-07-15 17:35:42 +02:00
Paweł Gronowski 9182b351c1 Merge tag 'v29.6.0' into 29.x
v29.6.0
2026-07-15 17:35:42 +02:00
Paweł Gronowski 0ae55a3e6c Merge tag 'v29.5.3' into 29.x
v29.5.3
2026-07-15 17:35:42 +02:00
Paweł Gronowski 029a4b21b3 Merge tag 'v29.5.2' into 29.x
v29.5.2
2026-07-15 17:35:41 +02:00
Paweł Gronowski e596fea454 Merge tag 'v29.5.1' into 29.x
v29.5.1
2026-07-15 17:35:41 +02:00
Paweł Gronowski 0c72f456aa Merge tag 'v29.5.0' into 29.x
v29.5.0
2026-07-15 17:35:41 +02:00
Paweł Gronowski 26f2736a2b Merge tag 'v29.4.1' into 29.x
v29.4.1
2026-07-15 17:35:41 +02:00
Paweł Gronowski ac4c4dfabd Merge tag 'v29.4.0' into 29.x
v29.4.0
2026-07-15 17:35:40 +02:00
Sebastiaan van Stijn 1245054535 Merge pull request #7099 from vvoland/7098-29.x
[29.x backport] scripts/sync-branch: Fix non-interactive merge
2026-07-10 08:34:26 +02:00
Paweł Gronowski f72fb0caf0 scripts/sync-branch: Fix non-interactive merge
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit f57e528457)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-09 16:52:02 +02:00
Sebastiaan van Stijn e4af2bf0d3 Merge pull request #7097 from vvoland/7091-29.x
[29.x backport] gha: Add release branch sync workflow
2026-07-09 16:35:45 +02:00
Paweł Gronowski 49a1af6aaa gha: Add release branch sync workflow
Add a manually dispatched workflow for maintainers to sync a Docker
release branch to a selected release tag.

The sync-release-branch job checks out the release branch, computes
the list of unmerged tags up to the requested tag via
scripts/unmerged-tags, merges them in order via scripts/sync-branch
using git merge --no-ff (resolving conflicts by taking the tag's
content), then pushes the result to a temporary branch.

The push-release-branch job runs after manual approval via the
docker-releases environment. It verifies that neither the release
branch nor the temporary branch moved since the sync job ran before
force-advancing the release branch and deleting the temporary branch.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 182f56fe8c)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-09 14:45:32 +02:00
Paweł Gronowski c2be9ccfc3 Merge pull request #6887 from thaJeztah/29.x_backport_pin_actions
[29.x backport] ci: pin actions to digests
2026-03-25 15:22:32 +01:00
Sebastiaan van Stijn 0da6a5121f ci: pin actions to digests
As a follow-up, we should use the full version (major.minor.patch).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 97b9e04a94)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 15:06:23 +01:00
Sebastiaan van Stijn 6b3ca8fc04 Merge pull request #6884 from thaJeztah/29.x_backport_missing_buildtags
[29.x backport] cli/command: add missing "go:build" comments
2026-03-25 15:04:52 +01:00
Paweł Gronowski f47603c290 Merge pull request #6885 from thaJeztah/29.x_backport_update_actions
[29.x backport] update actions
2026-03-25 14:23:37 +01:00
dependabot[bot] 9709c8fe08 build(deps): bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 39d676c72d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:16:00 +01:00
dependabot[bot] af45a23fa9 build(deps): bump docker/bake-action from 6 to 7
Bumps [docker/bake-action](https://github.com/docker/bake-action) from 6 to 7.
- [Release notes](https://github.com/docker/bake-action/releases)
- [Commits](https://github.com/docker/bake-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/bake-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 6453c4c3a7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:15:48 +01:00
Paweł Gronowski bc97f5ac19 Merge pull request #6883 from thaJeztah/29.x_backport_update_go1.25.8
[29.x backport] update to go1.25.8
2026-03-25 14:14:46 +01:00
Sebastiaan van Stijn 6d71967120 cli/command: add missing "go:build" comments
- commit e8dc2fce32 modernized loops to
  range over int, which requires go1.22 or later.
- commit 85ebca52fd modernized code to
  use stdlib min/max, which requires go1.21 or later.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9a471180cb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:11:15 +01:00
Sebastiaan van Stijn 9b51892b80 update to go1.25.8
go1.25.8 (released 2026-03-05) includes security fixes to the html/template,
net/url, and os packages, as well as bug fixes to the go command, the compiler,
and the os package. See the Go 1.25.8 milestone on our issue tracker for details.

- 1.25.8 https://github.com/golang/go/issues?q=milestone%3AGo1.25.8+label%3ACherryPickApproved
- diff: https://github.com/golang/go/compare/go1.25.7...go1.25.8
- 1.26.1 https://github.com/golang/go/issues?q=milestone%3AGo1.26.1+label%3ACherryPickApproved
- diff: https://github.com/golang/go/compare/go1.26.0...go1.26.1

---

We have just released Go versions 1.26.1 and 1.25.8, minor point releases.

These releases include 5 security fixes following the security policy:

crypto/x509: incorrect enforcement of email constraints

- When verifying a certificate chain which contains a certificate containing
  multiple email address constraints (composed of the full email address) which
  share common local portions (the portion of the address before the '@'
  character) but different domain portions (the portion of the address after the
  '@' character), these constraints will not be properly applied, and only the
  last constraint will be considered.

  This can allow certificates in the chain containing email addresses which are
  either not permitted or excluded by the relevant constraints to be returned by
  calls to Certificate.Verify. Since the name constraint checks happen after chain
  building is complete, this only applies to certificate chains which chain to
  trusted roots (root certificates either in VerifyOptions.Roots or in the system
  root certificate pool), requiring a trusted CA to issue certificates containing
  either not permitted or excluded email addresses.

  This issue only affects Go 1.26.

  Thanks to Jakub Ciolek for reporting this issue.

  This is CVE-2026-27137 and Go issue https://go.dev/issue/77952.

- crypto/x509: panic in name constraint checking for malformed certificates

  Certificate verification can panic when a certificate in the chain has an empty
  DNS name and another certificate in the chain has excluded name constraints.
  This can crash programs that are either directly verifying X.509 certificate
  chains, or those that use TLS.

  Since the name constraint checks happen after chain building is complete, this
  only applies to certificate chains which chain to trusted roots (root
  certificates either in VerifyOptions.Roots or in the system root certificate
  pool), requiring a trusted CA to issue certificates containing malformed DNS
  names.

  This issue only affects Go 1.26.

  Thanks to Jakub Ciolek for reporting this issue.

  This is CVE-2026-27138 and Go issue https://go.dev/issue/77953.

- html/template: URLs in meta content attribute actions are not escaped

  Actions which insert URLs into the content attribute of HTML meta tags are not
  escaped. This can allow XSS if the meta tag also has an http-equiv attribute
  with the value "refresh".

  A new GODEBUG setting has been added, htmlmetacontenturlescape, which can be
  used to disable escaping URLs in actions in the meta content attribute which
  follow "url=" by setting htmlmetacontenturlescape=0.

  This is CVE-2026-27142 and Go issue https://go.dev/issue/77954.

- net/url: reject IPv6 literal not at start of host

  The Go standard library function net/url.Parse insufficiently
  validated the host/authority component and accepted some invalid URLs
  by effectively treating garbage before an IP-literal as ignorable.
  The function should have rejected this as invalid.

  To prevent this behavior, net/url.Parse now rejects IPv6 literals
  that do not appear at the start of the host subcomponent of a URL.

  Thanks to Masaki Hara (https://github.com/qnighy) of Wantedly.

  This is CVE-2026-25679 and Go issue https://go.dev/issue/77578.

- os: FileInfo can escape from a Root

  On Unix platforms, when listing the contents of a directory using
  File.ReadDir or File.Readdir the returned FileInfo could reference
  a file outside of the Root in which the File was opened.

  The contents of the FileInfo were populated using the lstat system
  call, which takes the path to the file as a parameter. If a component
  of the full path of the file described by the FileInfo is replaced with
  a symbolic link, the target of the lstat can be directed to another
  location on the filesystem.

  The impact of this escape is limited to reading metadata provided by
  lstat from arbitrary locations on the filesystem. This could be used
  to probe for the presence or absence of files as well as gleaning
  metadata like file sizes, but does not permit reading or writing files
  outside the root.

  The FileInfo is now populated using fstatat.

  Thank you to Miloslav Trmač of Red Hat for reporting this issue.

  This is CVE-2026-27139 and Go issue https://go.dev/issue/77827.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f7d83cbae8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:08:46 +01:00
44 changed files with 1301 additions and 428 deletions
+59
View File
@@ -0,0 +1,59 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
// https://docs.renovatebot.com/modules/manager/github-actions/
// https://docs.renovatebot.com/presets-helpers/
"helpers:pinGitHubActionDigestsToSemver",
":gitSignOff"
],
"gomod": {
"managerFilePatterns": ["/(^|/)vendor\\.mod$/"]
},
"postUpdateOptions": ["gomodTidy"],
"minimumReleaseAge": "3 days",
"internalChecksFilter": "strict",
"packageRules": [
{
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"semanticCommits": "disabled",
"commitMessagePrefix": "vendor:"
},
{
"matchManagers": ["gomod"],
"matchFileNames": ["cmd/docker-trust/go.mod"],
// The trust module does not vendor dependencies; vendor/ belongs to the root module.
"postUpdateOptions": ["gomodSkipVendor"],
"semanticCommits": "disabled",
"commitMessagePrefix": "cmd/docker-trust:"
},
{
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"groupName": "golang.org/x packages",
"groupSlug": "golang-x",
"matchDepNames": ["/^golang\\.org\\/x\\//"]
},
{
"matchManagers": ["github-actions"],
"minimumReleaseAge": "7 days",
"addLabels": ["area/ci", "status/2-code-review"]
},
{
"matchManagers": ["github-actions"],
"groupName": "docker GitHub Actions",
"groupSlug": "docker-actions",
"matchDepNames": ["/^docker\\//"]
},
{
// Keep all the google.golang.org/genproto modules at the same version.
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"groupName": "google.golang.org/genproto packages",
"groupSlug": "google-genproto",
"matchDepNames": ["/^google\\.golang\\.org\\/genproto(\\/|$)/"],
"schedule": ["every month"]
}
]
}
+3 -3
View File
@@ -65,7 +65,7 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
-
name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
@@ -91,7 +91,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
-
name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/*
@@ -156,7 +156,7 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
-
name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
+1 -1
View File
@@ -64,7 +64,7 @@ jobs:
name: Update Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.8"
go-version: "1.26.10"
cache: false
-
name: Initialize CodeQL
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
docker info
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
-
name: Run ${{ matrix.target }}
run: |
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
fi
- name: Upload context
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: pr-review-context
path: context/
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
-
name: Test
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
@@ -68,7 +68,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.8"
go-version: "1.26.10"
cache: false
-
name: Test
+1 -1
View File
@@ -101,7 +101,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.8"
go-version: "1.26.10"
cache: false
-
name: Run gocompat check
+1 -1
View File
@@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.26.8"
go: "1.26.10"
timeout: 5m
+4 -4
View File
@@ -8,11 +8,11 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.26.8
ARG GO_VERSION=1.26.10
# XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
ARG XX_VERSION=1.9.0
ARG XX_VERSION=1.10.0
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
@@ -25,12 +25,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
# It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.34.1
ARG BUILDX_VERSION=0.38.0
# COMPOSE_VERSION is the version of compose to install in the dev container.
# It must be a tag in the docker.io/docker/compose-bin image repository
# on Docker Hub.
ARG COMPOSE_VERSION=v5.1.3
ARG COMPOSE_VERSION=v5.6.0
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
+1 -1
View File
@@ -1 +1 @@
29.9.0
29.9.2
+4
View File
@@ -30,7 +30,11 @@ package hooks
type ResponseType int
const (
// NextSteps renders suggestions under a shared "What's next:" header.
NextSteps ResponseType = 0
// GenericMessage renders before NextSteps, without a header or indentation.
// CLI versions that do not recognize this type ignore the response.
GenericMessage ResponseType = 1
)
// Request is the type representing the information
+35 -10
View File
@@ -7,17 +7,42 @@ const (
indent = " "
)
// PrintNextSteps renders list of [NextSteps] messages and writes them
// to out. It is a no-op if messages is empty.
func PrintNextSteps(out io.Writer, messages []string) {
if len(messages) == 0 {
return
// PrintMessages groups messages by type and writes them to out. Generic messages
// appear first, without a header or indentation, followed by next steps under a
// shared "What's next:" header. Order within each type is preserved.
// Messages with no lines or an unknown type produce no output.
func PrintMessages(out io.Writer, messages []Response, evaluate func(Response) []string) {
grouped := make(map[ResponseType][]string)
for _, message := range messages {
lines := evaluate(message)
grouped[message.Type] = append(grouped[message.Type], lines...)
}
for _, responseType := range []ResponseType{GenericMessage, NextSteps} {
lines := grouped[responseType]
if len(lines) == 0 {
continue
}
_, _ = io.WriteString(out, whatsNext)
for _, msg := range messages {
_, _ = io.WriteString(out, indent)
_, _ = io.WriteString(out, msg)
_, _ = io.WriteString(out, "\n")
header, prefix := "\n", ""
if responseType == NextSteps {
header, prefix = whatsNext, indent
}
_, _ = io.WriteString(out, header)
for _, line := range lines {
_, _ = io.WriteString(out, prefix)
_, _ = io.WriteString(out, line)
_, _ = io.WriteString(out, "\n")
}
}
}
// PrintNextSteps renders list of [NextSteps] messages and writes them
// to out. It is a no-op if messages is empty.
//
// Deprecated: use [PrintMessages] instead.
func PrintNextSteps(out io.Writer, messages []string) {
PrintMessages(out, []Response{{Type: NextSteps}}, func(Response) []string {
return messages
})
}
+84 -1
View File
@@ -8,7 +8,85 @@ import (
"gotest.tools/v3/assert"
)
func TestPrintHookMessages(t *testing.T) {
func TestPrintMessages(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
for _, tc := range []struct {
doc string
messages []hooks.Response
expected string
}{
{doc: "no messages"},
{
doc: "no lines",
messages: []hooks.Response{
{Type: hooks.GenericMessage},
{Type: hooks.NextSteps},
},
},
{
doc: "generic message",
messages: []hooks.Response{
{Type: hooks.GenericMessage, Template: "Session active"},
},
expected: "\nSession active\n",
},
{
doc: "preserve message formatting",
messages: []hooks.Response{
{Type: hooks.GenericMessage, Template: "Session active\n\n Details"},
},
expected: "\nSession active\n\n Details\n",
},
{
doc: "next steps",
messages: []hooks.Response{
{Type: hooks.NextSteps, Template: "Try another command"},
},
expected: header + " Try another command\n",
},
{
doc: "group interleaved types and preserve order within each type",
messages: []hooks.Response{
{Type: hooks.NextSteps, Template: "First suggestion"},
{Type: hooks.GenericMessage, Template: "Session active"},
{Type: hooks.NextSteps, Template: "Second suggestion\nThird suggestion"},
{Type: hooks.GenericMessage, Template: "Another status"},
},
expected: "\nSession active\nAnother status\n" + header + " First suggestion\n Second suggestion\n Third suggestion\n",
},
{
doc: "unknown type",
messages: []hooks.Response{
{Type: hooks.ResponseType(99), Template: "Ignored"},
},
},
{
doc: "unknown type does not affect known messages",
messages: []hooks.Response{
{Type: hooks.ResponseType(99), Template: "Ignored"},
{Type: hooks.GenericMessage, Template: "Session active"},
{Type: hooks.NextSteps, Template: "Try another command"},
},
expected: "\nSession active\n" + header + " Try another command\n",
},
} {
t.Run(tc.doc, func(t *testing.T) {
var w strings.Builder
hooks.PrintMessages(&w, tc.messages, func(message hooks.Response) []string {
if message.Template == "" {
return nil
}
lines, err := hooks.ParseTemplate(message.Template, nil)
assert.NilError(t, err)
return lines
})
assert.Equal(t, w.String(), tc.expected)
})
}
}
func TestPrintNextSteps(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
tests := []struct {
@@ -34,6 +112,11 @@ func TestPrintHookMessages(t *testing.T) {
" Foo\n" +
" bar\n",
},
{
doc: "preserve empty and indented lines",
messages: []string{"", " Details", ""},
expectedOutput: header + " \n Details\n \n",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
+11 -1
View File
@@ -7,6 +7,7 @@ import (
"bytes"
"errors"
"fmt"
"slices"
"strings"
"text/template"
@@ -43,7 +44,16 @@ func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
if n := strings.Count(out, "\n"); n > maxMessages {
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
}
return strings.SplitN(out, "\n", maxMessages), nil
lines := strings.SplitN(out, "\n", maxMessages)
// Skip messages containing only whitespace, but preserve empty lines
// in messages that have content (e.g., for paragraph spacing).
if !slices.ContainsFunc(lines, func(line string) bool {
return strings.TrimSpace(line) != ""
}) {
return nil, nil
}
return lines, nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
+11 -1
View File
@@ -33,7 +33,17 @@ func TestParseTemplate(t *testing.T) {
{
doc: "empty template",
template: "",
expectedOutput: []string{""},
expectedOutput: nil,
},
{
doc: "only empty lines",
template: "\n\n",
expectedOutput: nil,
},
{
doc: "whitespace-only lines",
template: " \n\t\n ",
expectedOutput: nil,
},
{
doc: "plain message",
+23 -47
View File
@@ -49,11 +49,18 @@ func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, sub
}
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
messages := invokeAndCollectHooks(ctx, cfg, rootCmd, invokedCommand, flags, cmdErrorMessage)
hooks.PrintMessages(subCommand.ErrOrStderr(), messages, func(message hooks.Response) []string {
lines, err := hooks.ParseTemplate(message.Template, subCommand)
if err != nil {
logrus.WithError(err).Debug("Failed to evaluate hook template")
return nil
}
return lines
})
}
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []hooks.Response {
if ctx.Err() != nil {
return nil
}
@@ -64,17 +71,17 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
}
pluginDirs := getPluginDirs(cfg)
nextSteps := make([]string, 0, len(pluginsCfg))
var collected []hooks.Response
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (hooks.Response, error) {
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
if !matched {
return nil, false, nil
return hooks.Response{}, nil
}
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
if err != nil {
return nil, false, err
return hooks.Response{}, err
}
resp, err := p.RunHook(ctx, hooks.Request{
@@ -83,29 +90,23 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
CommandError: cmdErrorMessage,
})
if err != nil {
return nil, false, err
return hooks.Response{}, err
}
var message hooks.Response
if err := json.Unmarshal(resp, &message); err != nil {
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
return hooks.Response{}, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
}
// currently the only hook type
if message.Type != hooks.NextSteps {
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
if message.Type != hooks.NextSteps && message.Type != hooks.GenericMessage {
return hooks.Response{}, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
}
messages, err = hooks.ParseTemplate(message.Template, subCmd)
if err != nil {
return nil, false, err
}
return messages, true, nil
return message, nil
}
for pluginName, pluginCfg := range pluginsCfg {
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
message, err := tryInvokeHook(pluginName, pluginCfg)
if err != nil {
// skip misbehaving plugins, but don't halt execution
logrus.WithFields(logrus.Fields{
@@ -114,38 +115,13 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
}).Debug("Plugin hook invocation failed")
continue
}
if !ok {
if message.Template == "" {
continue
}
var appended bool
nextSteps, appended = appendNextSteps(nextSteps, messages)
if !appended {
logrus.WithFields(logrus.Fields{
"plugin": pluginName,
}).Debug("Plugin responded with an empty hook message; ignoring")
}
collected = append(collected, message)
}
return nextSteps
}
// appendNextSteps appends the processed hook output to the nextSteps slice.
// If the processed hook output is empty, it is not appended.
// Empty lines are not stripped if there's at least one non-empty line.
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
empty := true
for _, l := range processed {
if strings.TrimSpace(l) != "" {
empty = false
break
}
}
if empty {
return nextSteps, false
}
return append(nextSteps, processed...), true
return collected
}
// pluginMatch takes a plugin configuration and a string representing the
+108 -35
View File
@@ -2,6 +2,11 @@ package manager
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/docker/cli/cli/config/configfile"
@@ -258,38 +263,6 @@ func TestMatchHookConfig(t *testing.T) {
}
}
func TestAppendNextSteps(t *testing.T) {
testCases := []struct {
processed []string
expectedOut []string
}{
{
processed: []string{},
expectedOut: []string{},
},
{
processed: []string{"", ""},
expectedOut: []string{},
},
{
processed: []string{"Some hint", "", "Some other hint"},
expectedOut: []string{"Some hint", "", "Some other hint"},
},
{
processed: []string{"Hint 1", "Hint 2"},
expectedOut: []string{"Hint 1", "Hint 2"},
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
got, appended := appendNextSteps([]string{}, tc.processed)
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
assert.Check(t, is.Equal(appended, len(got) > 0))
})
}
}
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
@@ -336,7 +309,7 @@ func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
// On success, error-hooks should not match, so the plugin
// binary is never looked up and no results are returned.
result := invokeAndCollectHooks(
context.Background(), cfg, root, sub,
context.Background(), cfg, root,
"build", map[string]string{}, "",
)
assert.Check(t, is.Len(result, 0))
@@ -349,7 +322,7 @@ func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
root.AddCommand(sub)
result := invokeAndCollectHooks(
context.Background(), cfg, root, sub,
context.Background(), cfg, root,
"build", map[string]string{}, "some error",
)
assert.Check(t, is.Len(result, 0))
@@ -368,8 +341,108 @@ func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
cancel() // cancel immediately
result := invokeAndCollectHooks(
ctx, cfg, root, sub,
ctx, cfg, root,
"build", map[string]string{}, "exit status 1",
)
assert.Check(t, is.Nil(result))
}
func TestRunHooksMessages(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test plugins require a POSIX shell")
}
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
for _, tc := range []struct {
doc string
responses map[string]string
expected string
}{
{
doc: "generic message preserves formatting and expands templates",
responses: map[string]string{
"status": `{"Type":1,"Template":"Status for {{command}}\n\n Details"}`,
},
expected: "\nStatus for build\n\n Details\n",
},
{
doc: "omitted type defaults to next steps",
responses: map[string]string{
"suggestion": `{"Template":"Try another command"}`,
},
expected: header + " Try another command\n",
},
{
doc: "generic messages precede shared next steps header",
responses: map[string]string{
"statusone": `{"Type":1,"Template":"Session active"}`,
"statustwo": `{"Type":1,"Template":"Session active"}`,
"suggestionone": `{"Type":0,"Template":"Try another command"}`,
"suggestiontwo": `{"Type":0,"Template":"Try another command"}`,
},
expected: "\nSession active\nSession active\n" + header + " Try another command\n Try another command\n",
},
{
doc: "empty responses produce no output",
responses: map[string]string{
"status": `{"Type":1,"Template":" \n\t"}`,
"suggestion": `{"Type":0,"Template":""}`,
},
},
{
doc: "empty generic message does not affect next steps",
responses: map[string]string{
"status": `{"Type":1,"Template":""}`,
"suggestion": `{"Type":0,"Template":"Try another command"}`,
},
expected: header + " Try another command\n",
},
{
doc: "invalid responses do not affect other plugins",
responses: map[string]string{
"unknown": `{"Type":99,"Template":"Ignored"}`,
"invalid": `{"Type":1,"Template":"{{"}`,
"status": `{"Type":1,"Template":"Session active"}`,
"suggestion": `{"Type":0,"Template":"Try another command"}`,
},
expected: "\nSession active\n" + header + " Try another command\n",
},
} {
t.Run(tc.doc, func(t *testing.T) {
pluginDir := t.TempDir()
cfg := configfile.New("")
cfg.CLIPluginsExtraDirs = []string{pluginDir}
cfg.Plugins = make(map[string]map[string]string)
for name, response := range tc.responses {
plugin := fmt.Sprintf(`#!/bin/sh
case "$1" in
docker-cli-plugin-metadata)
printf '%%s\n' '{"SchemaVersion":"0.1.0","Vendor":"test"}'
;;
*)
test "$2" = docker-cli-plugin-hooks || exit 1
printf 'called\n' >> "$0.calls"
printf '%%s\n' '%s'
;;
esac
`, response)
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+name), []byte(plugin), 0o755))
cfg.Plugins[name] = map[string]string{"hooks": "build"}
}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
var output strings.Builder
sub.SetErr(&output)
runHooks(context.Background(), cfg, root, sub, "build", nil, "")
assert.Equal(t, output.String(), tc.expected)
for name := range tc.responses {
calls, err := os.ReadFile(filepath.Join(pluginDir, "docker-"+name+".calls"))
assert.NilError(t, err)
assert.Equal(t, string(calls), "called\n", "each plugin hook must run exactly once")
}
})
}
}
+5 -2
View File
@@ -260,8 +260,11 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
contextDir = tempDir
case build.ContextTypeRemote:
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName)
if err != nil && options.quiet {
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
if err != nil {
if options.quiet {
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
}
return err
}
default:
return fmt.Errorf("unable to prepare context: path %q not found", options.context)
+31
View File
@@ -6,6 +6,8 @@ import (
"compress/gzip"
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"slices"
@@ -172,6 +174,35 @@ RUN echo hello world
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
}
// TestRunBuildRemoteContextDownloadError verifies that remote context
// download errors are propagated.
//
// regression test for https://github.com/docker/cli/issues/7372
func TestRunBuildRemoteContextDownloadError(t *testing.T) {
t.Setenv("DOCKER_BUILDKIT", "0")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not found", http.StatusNotFound)
}))
defer server.Close()
imageBuildCalled := false
cli := test.NewFakeCli(&fakeClient{
imageBuildFunc: func(ctx context.Context, buildContext io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) {
imageBuildCalled = true
return client.ImageBuildResult{Body: io.NopCloser(bytes.NewReader(nil))}, nil
},
})
options := newBuildOptions()
options.context = server.URL
err := runBuild(context.Background(), cli, options)
assert.ErrorContains(t, err, "unable to download remote context")
assert.Assert(t, !imageBuildCalled, "ImageBuild should not be called when downloading the remote context fails")
}
type fakeBuild struct {
context *tar.Reader
options client.ImageBuildOptions
+10 -10
View File
@@ -5,13 +5,13 @@ go 1.26.0
require (
github.com/containerd/errdefs v1.0.0
github.com/distribution/reference v0.6.0
github.com/docker/cli v29.7.2+incompatible
github.com/docker/cli v29.8.2+incompatible
github.com/docker/cli-docs-tool v0.11.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/go-connections v0.8.1
github.com/docker/go-connections v0.8.2
github.com/fvbommel/sortorder v1.2.0
github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.5.1
github.com/moby/moby/api v1.56.1
github.com/moby/moby/client v0.6.2
github.com/opencontainers/go-digest v1.0.0
github.com/sirupsen/logrus v1.10.2
github.com/spf13/cobra v1.10.2
@@ -22,7 +22,7 @@ require (
)
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
@@ -66,11 +66,11 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect
go.opentelemetry.io/otel/trace v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/crypto v0.56.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/grpc v1.83.2 // indirect
+20 -21
View File
@@ -1,5 +1,5 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -42,8 +42,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ=
github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -54,8 +54,8 @@ github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw=
@@ -133,10 +133,10 @@ github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KH
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4=
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
@@ -236,27 +236,26 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+7 -3
View File
@@ -571,11 +571,15 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
return nil
}
if !errdefs.IsNotFound(err) {
// For plugin not found we fall through to
// cmd.Execute() which deals with reporting
// "command not found" in a consistent way.
return err
}
if args[0] == "offload" {
// Report this before cmd.ExecuteContext(): plugin-specific flags such as
// --cpus otherwise fail parsing before the root command's RunE.
return errors.New("docker: to use 'docker offload', install the plugin.\nSee https://docs.docker.com/go/cloud-flag/")
}
// Other missing or invalid plugins fall through to cmd.ExecuteContext(),
// which reports the standard unknown-command error.
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.8
ARG GO_VERSION=1.26.10
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
@@ -10,7 +10,7 @@ ARG ALPINE_VERSION=3.23
# BUILDX_VERSION sets the version of buildx to install in the dev container.
# It must be a valid tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.34.1
ARG BUILDX_VERSION=0.38.0
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.8
ARG GO_VERSION=1.26.10
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.8
ARG GO_VERSION=1.26.10
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+6 -6
View File
@@ -22,7 +22,7 @@ require (
github.com/docker/cli-docs-tool v0.11.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker-credential-helpers v0.9.9
github.com/docker/go-connections v0.8.1
github.com/docker/go-connections v0.8.2
github.com/docker/go-units v0.5.0
github.com/fvbommel/sortorder v1.2.0
github.com/go-jose/go-jose/v4 v4.1.5
@@ -31,10 +31,10 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.30
github.com/mattn/go-runewidth v0.0.31
github.com/moby/go-archive v0.3.3
github.com/moby/moby/api v1.56.1
github.com/moby/moby/client v0.6.1
github.com/moby/moby/client v0.6.2
github.com/moby/patternmatcher v0.6.1
github.com/moby/swarmkit/v2 v2.1.2
github.com/moby/sys/atomicwriter v0.1.0
@@ -70,8 +70,8 @@ require (
)
require (
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a // indirect
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0 // indirect; see https://github.com/microsoft/hcsshim/pull/2545
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
github.com/Microsoft/go-winio v0.6.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -102,7 +102,7 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/time v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
+12 -13
View File
@@ -2,10 +2,10 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a h1:4hxax9ktNjSDoFn1tZSeL6gXMTRMpO0FzjdKfT01jgY=
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a/go.mod h1:3EWSSOZ50kb+arhww0qIaEXHToib/3FjBj9Jj5lcP5g=
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0 h1:71VU8kW/LCxJkx41+YYys1EgQO+AUnPa1TrxTSyYUCU=
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -37,8 +37,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w=
github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0=
github.com/docker/go-events v0.1.0/go.mod h1:jwVwMgySJX4PiukIIRrind1wPGU8QhPa8aTEPkreUj4=
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
@@ -82,16 +82,16 @@ github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLM
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo=
github.com/mattn/go-runewidth v0.0.31/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.6.1 h1:gvfKmdcWw+9MzXYP0vAUKQtBTGu1L8475K2nsmL7O98=
github.com/moby/moby/client v0.6.1/go.mod h1:XHgTFqz9NCgS/VuoxXMmEDVmVgXQ4vFEc15wsCIwb24=
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4=
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
@@ -193,8 +193,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -203,7 +203,6 @@ golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+25 -7
View File
@@ -3,8 +3,9 @@
package winio
import (
"errors"
"context"
"io"
"os"
"runtime"
"sync"
"sync/atomic"
@@ -20,16 +21,33 @@ import (
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
var (
ErrFileClosed = errors.New("file has already been closed")
ErrTimeout = &timeoutError{}
)
// ErrFileClosed is returned when an operation is performed on a closed file.
//
// Deprecated: use [os.ErrClosed] instead.
var ErrFileClosed = os.ErrClosed
// ErrTimeout is returned when an I/O operation exceeds its deadline.
//
// It matches both [context.DeadlineExceeded] and [os.ErrDeadlineExceeded]
// with [errors.Is].
var ErrTimeout error = &timeoutError{}
// timeoutError preserves the historical "i/o timeout" error while matching
// both context.DeadlineExceeded and os.ErrDeadlineExceeded through errors.Is.
// This mirrors the behavior of net.errTimeout:
// https://github.com/golang/go/blob/go1.27.1/src/net/net.go
//
// TODO: consider replacing ErrTimeout with os.ErrDeadlineExceeded if os.ErrDeadlineExceeded is changed to match context.DeadlineExceeded.
type timeoutError struct{}
func (*timeoutError) Error() string { return "i/o timeout" }
func (*timeoutError) Timeout() bool { return true }
func (*timeoutError) Temporary() bool { return true }
func (*timeoutError) Unwrap() error { return os.ErrDeadlineExceeded }
func (*timeoutError) Is(err error) bool {
return err == context.DeadlineExceeded
}
type timeoutChan chan struct{}
@@ -143,7 +161,7 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
f.wgLock.RLock()
if f.closing.Load() {
f.wgLock.RUnlock()
return nil, ErrFileClosed
return nil, os.ErrClosed
}
f.wg.Add(1)
f.wgLock.RUnlock()
@@ -192,7 +210,7 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
err = r.err
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
if f.closing.Load() {
err = ErrFileClosed
err = os.ErrClosed
}
} else if err != nil && f.socket {
// err is from Win32. Query the overlapped structure to get the winsock error.
+1 -1
View File
@@ -441,7 +441,7 @@ func canRedial(err error) bool {
func (conn *HvsockConn) opErr(op string, err error) error {
// translate from "file closed" to "socket closed"
if errors.Is(err, ErrFileClosed) {
if errors.Is(err, os.ErrClosed) {
err = socket.ErrSocketClosed
}
return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err}
+11 -12
View File
@@ -102,12 +102,10 @@ func (status ntStatus) Err() error {
return rtlNtStatusToDosError(status)
}
var (
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
ErrPipeListenerClosed = net.ErrClosed
errPipeWriteClosed = errors.New("pipe has been closed for write")
)
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
//
// Deprecated: use [net.ErrClosed] instead.
var ErrPipeListenerClosed = net.ErrClosed
type win32Pipe struct {
*win32File
@@ -144,9 +142,10 @@ func (f *win32Pipe) Disconnect() error {
}
// CloseWrite closes the write side of a message pipe in byte mode.
// It returns [io.ErrClosedPipe] if the write side is already closed.
func (f *win32MessageBytePipe) CloseWrite() error {
if f.writeClosed {
return errPipeWriteClosed
return io.ErrClosedPipe
}
err := f.win32File.Flush()
if err != nil {
@@ -161,10 +160,10 @@ func (f *win32MessageBytePipe) CloseWrite() error {
}
// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
// they are used to implement CloseWrite().
// they are used to implement CloseWrite. It returns [io.ErrClosedPipe] if the write side is closed.
func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
if f.writeClosed {
return 0, errPipeWriteClosed
return 0, io.ErrClosedPipe
}
if len(b) == 0 {
return 0, nil
@@ -454,7 +453,7 @@ func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
_ = p.Close()
p = nil
<-ch
err = ErrPipeListenerClosed
err = net.ErrClosed
}
return p, err
}
@@ -479,7 +478,7 @@ func (l *win32PipeListener) listenerRoutine() {
}
}
responseCh <- acceptResponse{p, err}
closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
closed = err == net.ErrClosed //nolint:errorlint // err is Errno
}
}
windows.Close(l.firstHandle)
@@ -571,7 +570,7 @@ func (l *win32PipeListener) Accept() (net.Conn, error) {
}
return &win32Pipe{win32File: response.f, path: l.path}, nil
case <-l.doneCh:
return nil, ErrPipeListenerClosed
return nil, net.ErrClosed
}
}
+5 -1
View File
@@ -9,13 +9,17 @@ import (
// NewTCPSocket creates a TCP socket listener with the specified address and
// the specified tls configuration. If TLSConfig is set, will encapsulate the
// TCP listener inside a TLS one.
// If no ALPN protocols are configured, it defaults to HTTP/1.1.
func NewTCPSocket(addr string, tlsConfig *tls.Config) (net.Listener, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
if tlsConfig != nil {
tlsConfig.NextProtos = []string{"http/1.1"}
if len(tlsConfig.NextProtos) == 0 {
tlsConfig = tlsConfig.Clone()
tlsConfig.NextProtos = []string{"http/1.1"}
}
l = tls.NewListener(l, tlsConfig)
}
return l, nil
+5
View File
@@ -15,6 +15,11 @@ Usage
runewidth.StringWidth("つのだ☆HIRO") == 12
```
NOTE: Latin letters that Unicode gives ambiguous East Asian width, such as
`ü`, `é` and `ß`, are measured as one cell even when `EastAsianWidth` is
set, as terminals draw them in CJK locales too. Other ambiguous characters
such as `±`, `×` and `○` still take two cells there.
Author
------
+100 -22
View File
@@ -361,9 +361,11 @@ var private = table{
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
{0x061C, 0x061C}, {0x070F, 0x070F}, {0x180B, 0x180E},
{0x200B, 0x200F}, {0x2028, 0x202E}, {0x2060, 0x2064},
{0x2066, 0x206F}, {0xD800, 0xDFFF}, {0xFEFF, 0xFEFF},
{0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, {0x13430, 0x1343F},
{0x1BCA0, 0x1BCA3}, {0x1D173, 0x1D17A}, {0xE0001, 0xE0001},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
@@ -514,19 +516,77 @@ func isJoiner(r rune) bool {
return inTable(r, joiner)
}
// clusterText returns s to segment in place of s. When s ends in a lead
// byte short of the bytes it promises, the segmenter gives up there and
// glues everything from it onto the cluster before, where the two-cell cap
// hides it, although the same bytes followed by anything else form
// clusters of their own. Such a string gets NULs after it, enough for any
// lead byte to be decoded; as controls they are clusters of their own, and
// the loops over the clusters stop at len(s) before reaching them.
func clusterText(s string) string {
if endsShort(s) {
return s + "\x00\x00\x00"
}
return s
}
// endsShort reports whether one of the last three bytes of s is a lead
// byte that needs more bytes than are left, which is where the segmenter
// stops decoding. It only looks at the length the lead byte announces, not
// at whether the bytes after it could continue it, as the segmenter does.
func endsShort(s string) bool {
for i := max(len(s)-3, 0); i < len(s); i++ {
b, left := s[i], len(s)-i
if b >= 0xC2 && b < 0xE0 && left < 2 ||
b >= 0xE0 && b < 0xF0 && left < 3 ||
b >= 0xF0 && b < 0xF8 && left < 4 {
return true
}
}
return false
}
// graphemeWidth returns the width of a single grapheme cluster: the sum of
// the widths of its runes, capped at 2 cells. The cap keeps multi-rune
// sequences that render as a single glyph (ZWJ emoji, flags, Hangul jamo)
// from being counted wider than the two cells terminals give them.
// the widths of its runes, with the part from the first rune that starts a
// single glyph (an emoji, a regional indicator, a Hangul jamo or syllable)
// capped at 2 cells. The cap keeps multi-rune sequences that render as one
// glyph (ZWJ emoji, skin tones, flags, Hangul jamo) from being counted
// wider than the two cells terminals give them. What comes before it, such
// as a Prepend rune, and a cluster with no such rune, like a letter with a
// skin tone modifier after it, are shown rune by rune and summed.
func (c *Condition) graphemeWidth(cluster string) int {
width := 0
if r, size := utf8.DecodeRuneInString(cluster); size == len(cluster) {
return c.RuneWidth(r) // one rune, never over the cap
}
width, glyph := 0, -1
for _, r := range cluster {
width += c.RuneWidth(r)
w := c.RuneWidth(r)
if glyph < 0 && r >= 0x1100 && startsGlyph(r) {
glyph = 0
}
if glyph >= 0 {
glyph += w
} else {
width += w
}
}
if width > 2 {
width = 2
return width + min(max(glyph, 0), 2)
}
// startsGlyph reports whether r begins a sequence that a terminal draws as
// a single glyph however many runes it has.
func startsGlyph(r rune) bool {
switch {
case 0x1100 <= r && r <= 0x11FF, // Hangul jamo
0xA960 <= r && r <= 0xA97F, // Hangul jamo extended-A
0xAC00 <= r && r <= 0xD7FF, // Hangul syllables, jamo extended-B
0x1F1E6 <= r && r <= 0x1F1FF: // regional indicators
return true
case r < 0x203C, 0x3299 < r && r < 0x1F004:
// Outside the emoji table: spares CJK text the search.
return false
}
return width
return inTable(r, emoji)
}
// StringWidth return width as you can see
@@ -536,6 +596,11 @@ func (c *Condition) StringWidth(s string) (width int) {
if b < 0x20 || b == 0x7F {
return 0
}
if b >= 0x80 {
// Not UTF-8 on its own: measure it as the U+FFFD every other
// path decodes it to, which is ambiguous width.
return c.RuneWidth(utf8.RuneError)
}
return 1
}
if len(s) > 0 && len(s) <= utf8.UTFMax {
@@ -565,8 +630,8 @@ graphemes:
return
}
width = 0
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
width += c.graphemeWidth(g.Value())
}
return
@@ -597,8 +662,8 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
}
var width int
pos := len(s)
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
chWidth := c.graphemeWidth(g.Value())
if width+chWidth > w {
pos = g.Start()
@@ -651,8 +716,8 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
var width int
pos := len(s)
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
chWidth := c.graphemeWidth(g.Value())
if width+chWidth > w {
@@ -714,8 +779,8 @@ func (c *Condition) TruncatePrefix(s string, w int, prefix string) string {
}
var width int
var pos int
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
chWidth := c.graphemeWidth(g.Value())
if sw-(width+chWidth) <= w {
pos = g.End()
@@ -791,6 +856,14 @@ func (c *Condition) Wrap(s string, w int) string {
width = 0
continue
}
// CRLF is one cluster that ends the line, as on the
// segmenter, so it is never broken before.
if b == '\r' && i+1 < len(s) && s[i+1] == '\n' {
out.WriteString("\r\n")
width = 0
i++
continue
}
// Same rule as the StringWidth fast path: no ASCII byte is
// wide or ambiguous, so the flags in c cannot change this.
cw := 0
@@ -815,8 +888,8 @@ func (c *Condition) Wrap(s string, w int) string {
width := 0
var out strings.Builder
out.Grow(len(s) + len(s)/max(w, 1) + 1)
g := graphemes.FromString(s)
for g.Next() {
g := graphemes.FromString(clusterText(s))
for g.Next() && g.Start() < len(s) {
cluster := g.Value()
// LF and CRLF are each a single cluster
if strings.HasSuffix(cluster, "\n") {
@@ -862,8 +935,13 @@ func RuneWidth(r rune) int {
}
// IsAmbiguousWidth returns whether is ambiguous width or not.
//
// NOTE: The Latin letters of U+0080..U+017F that Unicode gives ambiguous
// width (ü, é, ß, æ, ø and so on) are reported as ambiguous here, but
// RuneWidth measures them as one cell even with EastAsianWidth, since
// terminals draw them narrow in CJK locales too.
func IsAmbiguousWidth(r rune) bool {
return inTable(r, private) || inTable(r, ambiguous)
return inTable(r, private) || inTable(r, ambiguous) || inTable(r, ambiguousLatin)
}
// IsCombiningWidth returns whether is combining width or not.
+35 -8
View File
@@ -14,9 +14,11 @@ func mblen(charset string) int {
return 6
case "jis":
return 8
case "euctw", "gb18030":
return 4
case "eucjp":
return 3
case "euckr", "euccn", "sjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "gbk", "gb2312":
case "euckr", "euccn", "sjis", "shiftjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "big5hkscs", "gbk", "gb2312":
return 2
}
return 1
@@ -45,9 +47,35 @@ func localeCharset(locale string) string {
return ""
}
// isCJKLanguage reports whether the language code at the start of locale is
// Japanese, Korean or Chinese.
func isCJKLanguage(locale string) bool {
n := 0
for n < len(locale) && locale[n] >= 'a' && locale[n] <= 'z' {
n++
}
switch locale[:n] {
case "ja", "ko", "zh":
return true
}
return false
}
// normalizeCharset drops the hyphens and underscores that charset names are
// spelled with or without, so that "EUC-JP" and "eucjp" compare equal.
func normalizeCharset(charset string) string {
return strings.Map(func(r rune) rune {
if r == '-' || r == '_' {
return -1
}
return r
}, charset)
}
func isEastAsian(locale string) bool {
charset := strings.ToLower(locale)
if cs := localeCharset(locale); cs != "" {
cs := localeCharset(locale)
if cs != "" {
charset = strings.ToLower(cs)
}
@@ -61,14 +89,13 @@ func isEastAsian(locale string) bool {
break
}
}
max := mblen(charset)
if max > 1 && (charset[0] != 'u' ||
strings.HasPrefix(locale, "ja") ||
strings.HasPrefix(locale, "ko") ||
strings.HasPrefix(locale, "zh")) {
cjk := isCJKLanguage(locale)
if cs == "" && cjk {
// A CJK locale without a charset, such as "ja_JP" on macOS.
return true
}
return false
max := mblen(normalizeCharset(charset))
return max > 1 && (charset[0] != 'u' || cjk)
}
// IsEastAsian return true if the current locale is CJK
+58 -52
View File
@@ -173,65 +173,71 @@ var doublewidth = table{
var ambiguous = table{
{0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8},
{0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4},
{0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6},
{0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1},
{0x00AD, 0x00AE}, {0x00B0, 0x00B4}, {0x00B6, 0x00B9},
{0x00BC, 0x00BF}, {0x00D7, 0x00D7}, {0x00F7, 0x00F7},
{0x01CE, 0x01CE}, {0x01D0, 0x01D0}, {0x01D2, 0x01D2},
{0x01D4, 0x01D4}, {0x01D6, 0x01D6}, {0x01D8, 0x01D8},
{0x01DA, 0x01DA}, {0x01DC, 0x01DC}, {0x0251, 0x0251},
{0x0261, 0x0261}, {0x02C4, 0x02C4}, {0x02C7, 0x02C7},
{0x02C9, 0x02CB}, {0x02CD, 0x02CD}, {0x02D0, 0x02D0},
{0x02D8, 0x02DB}, {0x02DD, 0x02DD}, {0x02DF, 0x02DF},
{0x0300, 0x036F}, {0x0391, 0x03A1}, {0x03A3, 0x03A9},
{0x03B1, 0x03C1}, {0x03C3, 0x03C9}, {0x0401, 0x0401},
{0x0410, 0x044F}, {0x0451, 0x0451}, {0x2010, 0x2010},
{0x2013, 0x2016}, {0x2018, 0x2019}, {0x201C, 0x201D},
{0x2020, 0x2022}, {0x2024, 0x2027}, {0x2030, 0x2030},
{0x2032, 0x2033}, {0x2035, 0x2035}, {0x203B, 0x203B},
{0x203E, 0x203E}, {0x2074, 0x2074}, {0x207F, 0x207F},
{0x2081, 0x2084}, {0x20AC, 0x20AC}, {0x2103, 0x2103},
{0x2105, 0x2105}, {0x2109, 0x2109}, {0x2113, 0x2113},
{0x2116, 0x2116}, {0x2121, 0x2122}, {0x2126, 0x2126},
{0x212B, 0x212B}, {0x2153, 0x2154}, {0x215B, 0x215E},
{0x2160, 0x216B}, {0x2170, 0x2179}, {0x2189, 0x2189},
{0x2190, 0x2199}, {0x21B8, 0x21B9}, {0x21D2, 0x21D2},
{0x21D4, 0x21D4}, {0x21E7, 0x21E7}, {0x2200, 0x2200},
{0x2202, 0x2203}, {0x2207, 0x2208}, {0x220B, 0x220B},
{0x220F, 0x220F}, {0x2211, 0x2211}, {0x2215, 0x2215},
{0x221A, 0x221A}, {0x221D, 0x2220}, {0x2223, 0x2223},
{0x2225, 0x2225}, {0x2227, 0x222C}, {0x222E, 0x222E},
{0x2234, 0x2237}, {0x223C, 0x223D}, {0x2248, 0x2248},
{0x224C, 0x224C}, {0x2252, 0x2252}, {0x2260, 0x2261},
{0x2264, 0x2267}, {0x226A, 0x226B}, {0x226E, 0x226F},
{0x2282, 0x2283}, {0x2286, 0x2287}, {0x2295, 0x2295},
{0x2299, 0x2299}, {0x22A5, 0x22A5}, {0x22BF, 0x22BF},
{0x2312, 0x2312}, {0x2460, 0x24E9}, {0x24EB, 0x254B},
{0x2550, 0x2573}, {0x2580, 0x258F}, {0x2592, 0x2595},
{0x25A0, 0x25A1}, {0x25A3, 0x25A9}, {0x25B2, 0x25B3},
{0x25B6, 0x25B7}, {0x25BC, 0x25BD}, {0x25C0, 0x25C1},
{0x25C6, 0x25C8}, {0x25CB, 0x25CB}, {0x25CE, 0x25D1},
{0x25E2, 0x25E5}, {0x25EF, 0x25EF}, {0x2605, 0x2606},
{0x2609, 0x2609}, {0x260E, 0x260F}, {0x261C, 0x261C},
{0x261E, 0x261E}, {0x2640, 0x2640}, {0x2642, 0x2642},
{0x2660, 0x2661}, {0x2663, 0x2665}, {0x2667, 0x266A},
{0x266C, 0x266D}, {0x266F, 0x266F}, {0x269E, 0x269F},
{0x26BF, 0x26BF}, {0x26C6, 0x26CD}, {0x26CF, 0x26D3},
{0x26D5, 0x26E1}, {0x26E3, 0x26E3}, {0x26E8, 0x26E9},
{0x26EB, 0x26F1}, {0x26F4, 0x26F4}, {0x26F6, 0x26F9},
{0x26FB, 0x26FC}, {0x26FE, 0x26FF}, {0x273D, 0x273D},
{0x2776, 0x277F}, {0x2B56, 0x2B59}, {0x3248, 0x324F},
{0xE000, 0xF8FF}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A},
{0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D},
{0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xF0000, 0xFFFFD},
{0x100000, 0x10FFFD},
}
var ambiguousLatin = table{
{0x00AA, 0x00AA}, {0x00BA, 0x00BA}, {0x00C6, 0x00C6},
{0x00D0, 0x00D0}, {0x00D8, 0x00D8}, {0x00DE, 0x00E1},
{0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED},
{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA},
{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F8, 0x00FA},
{0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101},
{0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B},
{0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133},
{0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144},
{0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153},
{0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE},
{0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4},
{0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA},
{0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261},
{0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB},
{0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB},
{0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F},
{0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1},
{0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F},
{0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016},
{0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022},
{0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033},
{0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E},
{0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084},
{0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105},
{0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116},
{0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B},
{0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B},
{0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199},
{0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4},
{0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203},
{0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F},
{0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A},
{0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225},
{0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237},
{0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C},
{0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267},
{0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283},
{0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299},
{0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312},
{0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573},
{0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1},
{0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7},
{0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8},
{0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5},
{0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609},
{0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E},
{0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661},
{0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D},
{0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF},
{0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1},
{0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1},
{0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC},
{0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F},
{0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF},
{0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A}, {0x1F110, 0x1F12D},
{0x1F130, 0x1F169}, {0x1F170, 0x1F18D}, {0x1F18F, 0x1F190},
{0x1F19B, 0x1F1AC}, {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD},
{0x0166, 0x0167}, {0x016B, 0x016B},
}
var narrow = table{
{0x0020, 0x007E}, {0x00A2, 0x00A3}, {0x00A5, 0x00A6},
{0x00AC, 0x00AC}, {0x00AF, 0x00AF}, {0x27E6, 0x27ED},
+15 -1
View File
@@ -42,7 +42,7 @@ func (cli *Client) DialHijack(ctx context.Context, url, proto string, meta map[s
return conn, err
}
func (cli *Client) setupHijackConn(req *http.Request, proto string) (_ net.Conn, _ string, retErr error) {
func (cli *Client) setupHijackConn(req *http.Request, proto string) (retConn net.Conn, mediaType string, retErr error) {
ctx := req.Context()
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", proto)
@@ -70,6 +70,20 @@ func (cli *Client) setupHijackConn(req *http.Request, proto string) (_ net.Conn,
hc := &hijackedConn{conn, bufio.NewReader(conn)}
// Cancel the handshake without tying the returned connection to ctx.
cancelled := make(chan struct{})
stop := context.AfterFunc(ctx, func() {
_ = hc.Conn.Close()
close(cancelled)
})
defer func() {
if !stop() {
// Cancellation won the race; don't return a connection being closed.
<-cancelled
retConn, mediaType, retErr = nil, "", ctx.Err()
}
}()
cfg := &cli.clientConfig
var rt http.RoundTripper = otelhttp.NewTransport(hc, cli.traceOpts...)
+79 -24
View File
@@ -10,6 +10,9 @@ package http2
// flow control window update.
const inflowMinRefresh = 4 << 10
// maxFlowWindow is the maximum size of a flow control window.
const maxFlowWindow = (1 << 31) - 1
// inflow accounts for an inbound flow control window.
// It tracks both the latest window sent to the peer (used for enforcement)
// and the accumulated unsent window.
@@ -74,47 +77,99 @@ func takeInflows(f1, f2 *inflow, n uint32) bool {
return true
}
// outflow is the outbound flow control window's size.
// connOutflow is connection-level outbound flow control.
type connOutflow struct {
initial int32 // SETTINGS_INITIAL_WINDOW_SIZE, changes with settings updates
n int32 // connection-level flow control window
flowErr bool // set when a flow control error is encountered
}
func (f *connOutflow) init() {
f.initial = initialWindowSize // initial stream window size
f.n = initialWindowSize // current connection window size
}
func (f *connOutflow) changeInitialWindowSize(size int64) bool {
if size > maxFlowWindow {
f.flowErr = true
return false
}
f.initial = int32(size)
return true
}
func (f *connOutflow) add(n int32) bool {
sum := int64(f.n) + int64(n)
if sum > maxFlowWindow {
f.flowErr = true
return false
}
f.n += n
return true
}
// outflow is the stream-level outbound flow control window's size.
type outflow struct {
_ incomparable
// n is the number of DATA bytes we're allowed to send.
// An outflow is kept both on a conn and a per-stream.
n int32
// delta is the difference between the stream's flow control window and
// the connection's initial window size (conn.initial).
//
// Another view is that delta is the number of flow control bytes provided to this
// stream in WINDOW_UPDATE frames, less the number of bytes sent on the stream.
delta int32
// conn points to the shared connection-level outflow that is
// shared by all streams on that conn. It is nil for the outflow
// that's on the conn directly.
conn *outflow
// shared by all streams on that conn.
conn *connOutflow
}
func (f *outflow) setConnFlow(cf *outflow) { f.conn = cf }
func (f *outflow) available() int32 {
n := f.n
if f.conn != nil && f.conn.n < n {
n = f.conn.n
func (f *outflow) available() (int32, bool) {
if f.conn == nil {
return maxFlowWindow, true // only happens in tests
}
return n
if f.conn.flowErr {
// Block all sending once any stream observes a flow control error.
return 0, false
}
n := int64(f.conn.initial) + int64(f.delta)
if n > maxFlowWindow {
f.conn.flowErr = true
return 0, false
}
return min(int32(n), f.conn.n), true
}
func (f *outflow) take(n int32) {
if n > f.available() {
if f.conn == nil {
return // only happens in tests
}
avail, _ := f.available()
if n > avail {
panic("internal error: took too much")
}
f.n -= n
if f.conn != nil {
f.conn.n -= n
}
f.delta -= n
f.conn.n -= n
}
// add adds n bytes (positive or negative) to the flow control window.
// It returns false if the sum would exceed 2^31-1.
func (f *outflow) add(n int32) bool {
sum := f.n + n
if (sum > n) == (f.n > 0) {
f.n = sum
return true
if f.conn == nil {
return true // only happens in tests
}
return false
avail := int64(f.conn.initial) + int64(f.delta)
if avail > maxFlowWindow {
// An earlier change to the initial window pushed this stream over the limit.
// This is a connection-level flow control error.
f.conn.flowErr = true
return false
}
if avail+int64(n) > maxFlowWindow {
// This update would push the stream over the limit.
// This is a stream-level flow control error.
return false
}
f.delta += n
return true
}
+25 -9
View File
@@ -1711,7 +1711,8 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (Frame, error) {
mh := &MetaHeadersFrame{
HeadersFrame: hf,
}
var remainSize = fr.maxHeaderListSize()
var headersRemainSize = fr.maxHeaderListSize()
var trailersRemainSize = fr.maxHeaderListSize()
var sawRegular bool
var invalid error // pseudo header field errors
@@ -1743,14 +1744,29 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (Frame, error) {
return
}
size := hf.Size()
if size > remainSize {
var remainSize *uint32
var size uint32
if hf.Name == "trailer" {
remainSize = &trailersRemainSize
fieldCount := strings.Count(hf.Value, ",") + 1
// Rather than actually constructing hpack.HeaderField for each
// trailer field and cumulatively adding its Size, just do the math
// manually to avoid unnecessary work. This does make it so
// whitespaces after comma are counted against the budget, but that
// should be innocuous.
size = uint32(len(hf.Value)-fieldCount+1) + uint32(fieldCount)*hpack.HeaderField{}.Size()
} else {
remainSize = &headersRemainSize
size = hf.Size()
}
if size > *remainSize {
hdec.SetEmitEnabled(false)
mh.Truncated = true
remainSize = 0
headersRemainSize = 0
trailersRemainSize = 0
return
}
remainSize -= size
*remainSize -= size
mh.Fields = append(mh.Fields, hf)
})
@@ -1766,10 +1782,10 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (Frame, error) {
// skip parsing the fragment and close the connection.
//
// "Too much" is either any CONTINUATION frame after we've already
// exceeded the max header list size (in which case remainSize is 0),
// or a frame whose encoded size is more than twice the remaining
// header list bytes we're willing to accept.
if int64(len(frag)) > int64(2*remainSize) {
// exceeded the max header list size (if so, both budgets are 0), or a
// frame whose encoded size is more than twice the remaining header
// list bytes we're willing to accept.
if int64(len(frag)) > 2*int64(headersRemainSize+trailersRemainSize) {
if VerboseLogs {
log.Printf("http2: header list too large")
}
+66 -44
View File
@@ -280,7 +280,6 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
doneServing: make(chan struct{}),
clientMaxStreams: math.MaxUint32, // Section 6.5.2: "Initially, there is no limit to this value"
advMaxStreams: conf.MaxConcurrentStreams,
initialStreamSendWindowSize: initialWindowSize,
initialStreamRecvWindowSize: conf.MaxUploadBufferPerStream,
maxFrameSize: initialMaxFrameSize,
pingTimeout: conf.PingTimeout,
@@ -320,7 +319,7 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
// These start at the RFC-specified defaults. If there is a higher
// configured value for inflow, that will be updated when we send a
// WINDOW_UPDATE shortly after sending SETTINGS.
sc.flow.add(initialWindowSize)
sc.flow.init()
sc.inflow.init(initialWindowSize)
sc.hpackEncoder = hpack.NewEncoder(&sc.headerWriteBuf)
sc.hpackEncoder.SetMaxDynamicTableSizeLimit(conf.MaxEncoderHeaderTableSize)
@@ -427,7 +426,7 @@ type serverConn struct {
wroteFrameCh chan frameWriteResult // from writeFrameAsync -> serve, tickles more frame writes
bodyReadCh chan bodyReadMsg // from handlers -> serve
serveMsgCh chan interface{} // misc messages & code to send to / run on the serve loop
flow outflow // conn-wide (not stream-specific) outbound flow control
flow connOutflow // conn-wide (not stream-specific) outbound flow control
inflow inflow // conn-wide inbound flow control
tlsState *tls.ConnectionState // shared by all handlers, like net/http
remoteAddrStr string
@@ -441,6 +440,9 @@ type serverConn struct {
sawFirstSettings bool // got the initial SETTINGS frame after the preface
needToSendSettingsAck bool
unackedSettings int // how many SETTINGS have we sent without ACKs?
pendingEncoderTableSize bool // peer changed SETTINGS_HEADER_TABLE_SIZE; apply to hpackEncoder before the next frame write
encoderTableSizeMin uint32 // smallest SETTINGS_HEADER_TABLE_SIZE since the last apply
encoderTableSize uint32 // latest SETTINGS_HEADER_TABLE_SIZE
queuedControlFrames int // control frames in the writeSched queue
clientMaxStreams uint32 // SETTINGS_MAX_CONCURRENT_STREAMS from client (our PUSH_PROMISE limit)
advMaxStreams uint32 // our SETTINGS_MAX_CONCURRENT_STREAMS advertised the client
@@ -451,7 +453,6 @@ type serverConn struct {
maxPushPromiseID uint32 // ID of the last push promise (even), or 0 if there have been no pushes
streams map[uint32]*stream
unstartedHandlers []unstartedHandler
initialStreamSendWindowSize int32
initialStreamRecvWindowSize int32
maxFrameSize int32
peerMaxHeaderListSize uint32 // zero means unknown (default)
@@ -521,7 +522,8 @@ type stream struct {
// immutable:
sc *serverConn
id uint32
body *pipe // non-nil if expecting DATA frames
body *pipe // non-nil if expecting DATA frames
reqBody *requestBody
cw closeWaiter // closed wait stream transitions to closed state
ctx context.Context
cancelCtx func()
@@ -1172,6 +1174,16 @@ func (sc *serverConn) startFrameWrite(wr FrameWriteRequest) {
sc.writingFrame = true
sc.needsFrameFlush = true
if sc.pendingEncoderTableSize {
// hpackEncoder may be in use by writeFrameAsync, so SETTINGS
// changes to it are deferred until no frame is being written.
// Replaying the smallest size before the latest one keeps the
// encoder's view identical to having applied every change
// (RFC 7541, Section 4.2).
sc.pendingEncoderTableSize = false
sc.hpackEncoder.SetMaxDynamicTableSize(sc.encoderTableSizeMin)
sc.hpackEncoder.SetMaxDynamicTableSize(sc.encoderTableSize)
}
if wr.write.staysWithinBuffer(sc.bw.Available()) {
sc.writingFrameAsync = false
err := wr.write.writeFrame(sc)
@@ -1271,6 +1283,11 @@ func (sc *serverConn) scheduleFrameWrite() {
}
sc.inFrameScheduleLoop = true
for !sc.writingFrameAsync {
if sc.flow.flowErr && (!sc.inGoAway || sc.goAwayCode == ErrCodeNo) {
sc.inGoAway = true
sc.needToSendGoAway = true
sc.goAwayCode = ErrCodeFlowControl
}
if sc.needToSendGoAway {
sc.needToSendGoAway = false
sc.startFrameWrite(FrameWriteRequest{
@@ -1294,6 +1311,9 @@ func (sc *serverConn) scheduleFrameWrite() {
sc.startFrameWrite(wr)
continue
}
if sc.flow.flowErr {
continue
}
}
if sc.needsFrameFlush {
sc.startFrameWrite(FrameWriteRequest{write: flushFrameWriter{}})
@@ -1526,6 +1546,10 @@ func (sc *serverConn) processWindowUpdate(f *WindowUpdateFrame) error {
return nil
}
if !st.flow.add(int32(f.Increment)) {
if st.flow.conn.flowErr {
// This is a lazily-detected connection-level flow control error.
return sc.countError("bad_flow", ConnectionError(ErrCodeFlowControl))
}
return sc.countError("bad_flow", streamError(f.StreamID, ErrCodeFlowControl))
}
default: // connection-level flow control
@@ -1584,10 +1608,6 @@ func (sc *serverConn) closeStream(st *stream, err error) {
}
}
if p := st.body; p != nil {
// Return any buffered unread bytes worth of conn-level flow control.
// See golang.org/issue/16481
sc.sendWindowUpdate(nil, p.Len())
p.CloseWithError(err)
}
if e, ok := err.(StreamError); ok {
@@ -1641,7 +1661,12 @@ func (sc *serverConn) processSetting(s Setting) error {
}
switch s.ID {
case SettingHeaderTableSize:
sc.hpackEncoder.SetMaxDynamicTableSize(s.Val)
// Applied by startFrameWrite; see comment there.
if !sc.pendingEncoderTableSize || s.Val < sc.encoderTableSizeMin {
sc.encoderTableSizeMin = s.Val
}
sc.encoderTableSize = s.Val
sc.pendingEncoderTableSize = true
case SettingEnablePush:
sc.pushEnabled = s.Val != 0
case SettingMaxConcurrentStreams:
@@ -1672,28 +1697,14 @@ func (sc *serverConn) processSetting(s Setting) error {
func (sc *serverConn) processSettingInitialWindowSize(val uint32) error {
sc.serveG.check()
// Note: val already validated to be within range by
// processSetting's Valid call.
// "A SETTINGS frame can alter the initial flow control window
// size for all current streams. When the value of
// SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST
// adjust the size of all stream flow control windows that it
// maintains by the difference between the new value and the
// old value."
old := sc.initialStreamSendWindowSize
sc.initialStreamSendWindowSize = int32(val)
growth := int32(val) - old // may be negative
for _, st := range sc.streams {
if !st.flow.add(growth) {
// 6.9.2 Initial Flow Control Window Size
// "An endpoint MUST treat a change to
// SETTINGS_INITIAL_WINDOW_SIZE that causes any flow
// control window to exceed the maximum size as a
// connection error (Section 5.4.1) of type
// FLOW_CONTROL_ERROR."
return sc.countError("setting_win_size", ConnectionError(ErrCodeFlowControl))
}
if !sc.flow.changeInitialWindowSize(int64(val)) {
// 6.9.2 Initial Flow Control Window Size
// "An endpoint MUST treat a change to
// SETTINGS_INITIAL_WINDOW_SIZE that causes any flow
// control window to exceed the maximum size as a
// connection error (Section 5.4.1) of type
// FLOW_CONTROL_ERROR."
return sc.countError("setting_win_size", ConnectionError(ErrCodeFlowControl))
}
return nil
}
@@ -1966,7 +1977,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
if st.reqTrailer != nil {
st.trailer = make(http.Header)
}
st.body = req.Body.(*requestBody).pipe // may be nil
st.body = st.reqBody.pipe // may be nil
st.declBodyBytes = req.ContentLength
handler := sc.handler.ServeHTTP
@@ -1989,7 +2000,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout)
}
return sc.scheduleHandler(id, rw, req, handler)
return sc.scheduleHandler(st, rw, req, handler)
}
func (sc *serverConn) upgradeRequest(req *http.Request) {
@@ -2102,7 +2113,6 @@ func (sc *serverConn) newStream(id, pusherID uint32, state streamState, priority
}
st.cw.Init()
st.flow.conn = &sc.flow // link to conn-level counter
st.flow.add(sc.initialStreamSendWindowSize)
st.inflow.init(sc.initialStreamRecvWindowSize)
if sc.hs.WriteTimeout > 0 {
st.writeDeadline = time.AfterFunc(sc.hs.WriteTimeout, st.onWriteTimeout)
@@ -2184,7 +2194,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
} else {
req.ContentLength = -1
}
req.Body.(*requestBody).pipe = &pipe{
st.reqBody.pipe = &pipe{
b: &dataBuffer{expected: req.ContentLength},
}
}
@@ -2204,7 +2214,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp httpcommon.Server
return nil, nil, sc.countError(res.InvalidReason, streamError(st.id, ErrCodeProtocol))
}
body := &requestBody{
st.reqBody = &requestBody{
conn: sc,
stream: st,
needsContinue: res.NeedsContinue,
@@ -2220,7 +2230,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp httpcommon.Server
ProtoMinor: 0,
TLS: tlsState,
Host: rp.Authority,
Body: body,
Body: st.reqBody,
Trailer: res.Trailer,
}).WithContext(st.ctx)
rw := sc.newResponseWriter(st, req)
@@ -2244,11 +2254,12 @@ type unstartedHandler struct {
rw *responseWriter
req *http.Request
handler func(http.ResponseWriter, *http.Request)
body *pipe
}
// scheduleHandler starts a handler goroutine,
// or schedules one to start as soon as an existing handler finishes.
func (sc *serverConn) scheduleHandler(streamID uint32, rw *responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) error {
func (sc *serverConn) scheduleHandler(st *stream, rw *responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) error {
sc.serveG.check()
maxHandlers := sc.advMaxStreams
if sc.curHandlers < maxHandlers {
@@ -2260,10 +2271,11 @@ func (sc *serverConn) scheduleHandler(streamID uint32, rw *responseWriter, req *
return sc.countError("too_many_early_resets", ConnectionError(ErrCodeEnhanceYourCalm))
}
sc.unstartedHandlers = append(sc.unstartedHandlers, unstartedHandler{
streamID: streamID,
streamID: st.id,
rw: rw,
req: req,
handler: handler,
body: st.body,
})
return nil
}
@@ -2277,6 +2289,10 @@ func (sc *serverConn) handlerDone() {
u := sc.unstartedHandlers[i]
if sc.streams[u.streamID] == nil {
// This stream was reset before its goroutine had a chance to start.
if u.body != nil {
u.body.BreakWithError(errClosedBody)
sc.sendWindowUpdate(nil, u.body.Len())
}
continue
}
if sc.curHandlers >= maxHandlers {
@@ -2298,6 +2314,12 @@ func (sc *serverConn) runHandler(rw *responseWriter, req *http.Request, handler
didPanic := true
defer func() {
rw.rws.stream.cancelCtx()
if b := rw.rws.stream.reqBody; b != nil {
// Closing the body refunds flow control credit for any unconsumed data.
// (reqBody is nil for Upgrade: h2c requests, but those do not use flow
// control for the request body.)
b.Close()
}
if req.MultipartForm != nil {
req.MultipartForm.RemoveAll()
}
@@ -2396,7 +2418,7 @@ func (sc *serverConn) noteBodyReadFromHandler(st *stream, n int, err error) {
func (sc *serverConn) noteBodyRead(st *stream, n int) {
sc.serveG.check()
sc.sendWindowUpdate(nil, n) // conn-level
if st.state != stateHalfClosedRemote && st.state != stateClosed {
if st != nil && st.state != stateHalfClosedRemote && st.state != stateClosed {
// Don't send this WINDOW_UPDATE if the stream is closed
// remotely.
sc.sendWindowUpdate(st, n)
@@ -2444,6 +2466,9 @@ func (b *requestBody) Close() error {
b.closeOnce.Do(func() {
if b.pipe != nil {
b.pipe.BreakWithError(errClosedBody)
if unread := b.pipe.Len(); unread > 0 {
b.conn.noteBodyReadFromHandler(nil, unread, errClosedBody)
}
}
})
return nil
@@ -2461,9 +2486,6 @@ func (b *requestBody) Read(p []byte) (n int, err error) {
if err == io.EOF {
b.sawEOF = true
}
if b.conn == nil {
return
}
b.conn.noteBodyReadFromHandler(b.stream, n, err)
return
}
+94 -48
View File
@@ -27,6 +27,7 @@ import (
"net/http"
"net/http/httptrace"
"net/textproto"
"slices"
"strconv"
"sync"
"sync/atomic"
@@ -203,7 +204,7 @@ type ClientConn struct {
mu sync.Mutex // guards following
cond *sync.Cond // hold mu; broadcast on flow/closed changes
flow outflow // our conn-level flow control quota (cs.outflow is per stream)
flow connOutflow // our conn-level flow control quota (cs.outflow is per stream)
inflow inflow // peer's conn-level flow control
doNotReuse bool // whether conn is marked to not be reused for any future requests
closing bool
@@ -227,7 +228,6 @@ type ClientConn struct {
maxConcurrentStreams uint32
peerMaxHeaderListSize uint64
peerMaxHeaderTableSize uint32
initialWindowSize uint32
initialStreamRecvWindowSize int32
readIdleTimeout time.Duration
pingTimeout time.Duration
@@ -472,7 +472,6 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool, internalStateHook
readerDone: make(chan struct{}),
nextStreamID: 1,
maxFrameSize: 16 << 10, // spec default
initialWindowSize: 65535, // spec default
initialStreamRecvWindowSize: conf.MaxUploadBufferPerStream,
maxConcurrentStreams: initialMaxConcurrentStreams, // "infinite", per spec. Use a smaller value until we have received server settings.
strictMaxConcurrentStreams: conf.StrictMaxConcurrentRequests,
@@ -497,7 +496,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool, internalStateHook
}
cc.cond = sync.NewCond(&cc.mu)
cc.flow.add(int32(initialWindowSize))
cc.flow.init()
// TODO: adjust this writer size to account for frame size +
// MTU + crypto/tls record padding.
@@ -876,6 +875,33 @@ func (cc *ClientConn) sendGoAway() error {
return nil
}
func (cc *ClientConn) goAwayAndClose(code ErrCode) {
cc.mu.Lock()
closed := cc.closed
cc.closing = true
cc.closed = true
cc.mu.Unlock()
if closed {
return
}
if f := cc.fr.countError; f != nil {
f(fmt.Sprintf("conn_close_error_%s", code.stringToken()))
}
done := make(chan struct{})
go func() {
defer close(done)
cc.wmu.Lock()
cc.fr.WriteGoAway(0, code, nil)
cc.bw.Flush()
cc.wmu.Unlock()
}()
select {
case <-done:
case <-time.After(250 * time.Millisecond):
}
cc.closeForError(fmt.Errorf("http2: closing connection with %v", code))
}
// closes the client connection immediately. In-flight requests are interrupted.
// err is sent to streams.
func (cc *ClientConn) closeForError(err error) {
@@ -1319,7 +1345,9 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
}
if err != nil {
cs.abortStream(err) // possibly redundant, but harmless
if cs.sentHeaders {
if ce, ok := err.(ConnectionError); ok {
cc.goAwayAndClose(ErrCode(ce))
} else if cs.sentHeaders {
if se, ok := err.(StreamError); ok {
if se.Cause != errFromPeer {
cc.writeStreamReset(cs.ID, se.Code, false, err)
@@ -1648,8 +1676,12 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
return 0, errRequestCanceled
default:
}
if a := cs.flow.available(); a > 0 {
take := a
avail, ok := cs.flow.available()
if !ok {
return 0, ConnectionError(ErrCodeFlowControl)
}
if avail > 0 {
take := avail
if int(take) > maxBytes {
take = int32(maxBytes) // can't truncate int; take is int32
@@ -1710,8 +1742,7 @@ type resAndError struct {
// requires cc.mu be held.
func (cc *ClientConn) addStreamLocked(cs *clientStream) {
cs.flow.add(int32(cc.initialWindowSize))
cs.flow.setConnFlow(&cc.flow)
cs.flow.conn = &cc.flow
cs.inflow.init(cc.initialStreamRecvWindowSize)
cs.ID = cc.nextStreamID
cc.nextStreamID += 2
@@ -2095,18 +2126,42 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
return nil, nil
}
// Delete various headers that might mess up framing for HTTP/1. This is
// not a problem for HTTP/2, but someone might use HTTP/2 transport as a
// reverse proxy which forwards the response to an HTTP/1 client. Our
// HTTP/1 client transport will properly reject improper headers such as
// multiple conflicting Content-Length headers, but other implementations
// might not.
// TODO: just reject such responses? We deleted them for compatibility
// since this was done in a security fix (go.dev/issue/81115). However,
// rejecting them seems entirely reasonable and relatively safe.
// Connection-specific header fields must not appear in an HTTP/2 message,
// and any message containing them is malformed. RFC 9113, Section 8.2.2.
for _, k := range connHeaders {
delete(res.Header, k)
}
res.ContentLength = -1
if clens := res.Header["Content-Length"]; len(clens) == 1 {
if cl, err := strconv.ParseUint(clens[0], 10, 63); err == nil {
res.ContentLength = int64(cl)
if clens, ok := res.Header["Content-Length"]; ok {
// Repeated Content-Length values may be collapsed into one only if
// they are identical per RFC 9110 Section 8.6.
// No need to trim whitespace, HTTP/2 header values must not have
// extraneous whitespace per RFC 9113 Section 8.2.1.
// Non-canonical headers are already rejected by our framer at this
// point.
conflicting := slices.ContainsFunc(clens[1:], func(clen string) bool {
return clen != clens[0]
})
cl, err := strconv.ParseUint(clens[0], 10, 63)
if conflicting || err != nil {
delete(res.Header, "Content-Length")
} else {
// TODO: care? unlike http/1, it won't mess up our framing, so it's
// more safe smuggling-wise to ignore.
res.Header["Content-Length"] = clens[:1]
res.ContentLength = int64(cl)
}
} else if len(clens) > 1 {
// TODO: care? unlike http/1, it won't mess up our framing, so it's
// more safe smuggling-wise to ignore.
} else if f.StreamEnded() && !cs.isHead {
}
if res.ContentLength < 0 && f.StreamEnded() && !cs.isHead {
res.ContentLength = 0
}
@@ -2417,6 +2472,10 @@ const (
func (rl *clientConnReadLoop) streamByID(id uint32, headerOrData bool) *clientStream {
rl.cc.mu.Lock()
defer rl.cc.mu.Unlock()
return rl.streamByIDLocked(id, headerOrData)
}
func (rl *clientConnReadLoop) streamByIDLocked(id uint32, headerOrData bool) *clientStream {
if headerOrData {
// Work around an unfortunate gRPC behavior.
// See comment on ClientConn.rstStreamPingsBlocked for details.
@@ -2499,24 +2558,10 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
case SettingMaxHeaderListSize:
cc.peerMaxHeaderListSize = uint64(s.Val)
case SettingInitialWindowSize:
// Values above the maximum flow-control
// window size of 2^31-1 MUST be treated as a
// connection error (Section 5.4.1) of type
// FLOW_CONTROL_ERROR.
if s.Val > math.MaxInt32 {
if !cc.flow.changeInitialWindowSize(int64(s.Val)) {
return ConnectionError(ErrCodeFlowControl)
}
// Adjust flow control of currently-open
// frames by the difference of the old initial
// window size and this one.
delta := int32(s.Val) - int32(cc.initialWindowSize)
for _, cs := range cc.streams {
cs.flow.add(delta)
}
cc.cond.Broadcast()
cc.initialWindowSize = s.Val
case SettingHeaderTableSize:
cc.henc.SetMaxDynamicTableSize(s.Val)
cc.peerMaxHeaderTableSize = s.Val
@@ -2558,29 +2603,30 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error {
cc := rl.cc
cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame)
if f.StreamID != 0 && cs == nil {
return nil
}
cc.mu.Lock()
defer cc.mu.Unlock()
fl := &cc.flow
if cs != nil {
fl = &cs.flow
}
if !fl.add(int32(f.Increment)) {
// For stream, the sender sends RST_STREAM with an error code of FLOW_CONTROL_ERROR
if cs != nil {
if f.StreamID == 0 {
if !cc.flow.add(int32(f.Increment)) {
return ConnectionError(ErrCodeFlowControl)
}
} else {
cs := rl.streamByIDLocked(f.StreamID, notHeaderOrDataFrame)
if cs == nil {
return nil
}
if !cs.flow.add(int32(f.Increment)) {
if cs.flow.conn.flowErr {
// This is a lazily-detected connection-level flow control error.
return ConnectionError(ErrCodeFlowControl)
}
// For stream, the sender sends RST_STREAM with
// an error code of FLOW_CONTROL_ERROR.
rl.endStreamErrorLocked(cs, StreamError{
StreamID: f.StreamID,
Code: ErrCodeFlowControl,
})
return nil
}
return ConnectionError(ErrCodeFlowControl)
}
cc.cond.Broadcast()
return nil
+194 -20
View File
@@ -119,33 +119,27 @@ type http2TransportContextKey struct{}
// DialFromContext dials a new connection using the http2.Transport's DialTLS/DialTLSContext.
func (t transportConfig) DialFromContext(ctx context.Context, network, address string) (net.Conn, error) {
if ctx.Value(http2TransportContextKey{}) == nil {
dial, _ := ctx.Value(http2TransportContextKey{}).(*transportRoundTripState)
if dial == nil {
// We're being called from a RoundTrip that did not start with an http2.Transport.
// Use the http.Transport's dialer.
return nil, errors.ErrUnsupported
}
tlsConf := t.t.TLSClientConfig
if tlsConf == nil {
tlsConf = &tls.Config{}
} else {
tlsConf = tlsConf.Clone()
}
if !slices.Contains(tlsConf.NextProtos, "h2") {
tlsConf.NextProtos = append([]string{"h2"}, tlsConf.NextProtos...)
}
if tlsConf.ServerName == "" {
host, _, err := net.SplitHostPort(address)
if err == nil {
tlsConf.ServerName = host
}
}
return t.t.dialTLS(ctx, network, address, tlsConf)
return t.t.dialForRoundTrip(ctx, dial, network, address)
}
type transportInternal struct {
initOnce sync.Once
lazyt1 *http.Transport
dialMu sync.Mutex
dials map[string]*transportDialState
}
type transportDialState struct {
dialc chan struct{} // close when dial returns
dialErr error // dial result, set before dialc is closed
rtdonec chan struct{} // closed when RoundTrip initiating the dial returns
}
func (t *Transport) init() *http.Transport {
@@ -167,6 +161,52 @@ func (t *Transport) configure(t1 *http.Transport) {
}
}
// transportRoundTripState is the state of the dial for an http2.Transport.RoundTrip.
type transportRoundTripState struct {
mu sync.Mutex
rtdone bool // set when RoundTrip returns
rtdonec chan struct{} // closed when RoundTrip returns
gotconnc chan struct{} // closed when GotConn hook is called
}
// startDial is called when a dial starts.
//
// It returns done=true if the RoundTrip has already returned,
// in which case we should skip dialing.
func (dial *transportRoundTripState) startDial() (gotconnc chan struct{}, done bool) {
dial.mu.Lock()
defer dial.mu.Unlock()
if dial.rtdone {
return nil, true
}
if dial.rtdonec == nil {
dial.rtdonec = make(chan struct{})
}
dial.gotconnc = make(chan struct{})
return dial.gotconnc, false
}
// gotConn is called when RoundTrip gets a connection.
// This may happen multiple times per RoundTrip, if a request is retried.
func (dial *transportRoundTripState) gotConn() {
dial.mu.Lock()
defer dial.mu.Unlock()
if dial.gotconnc != nil {
close(dial.gotconnc)
dial.gotconnc = nil
}
}
// roundTripDone is called when RoundTrip returns.
func (dial *transportRoundTripState) roundTripDone() {
dial.mu.Lock()
defer dial.mu.Unlock()
dial.rtdone = true
if dial.rtdonec != nil {
close(dial.rtdonec)
}
}
func (t *Transport) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
t1 := t.init()
@@ -186,12 +226,146 @@ func (t *Transport) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
// Both http.Transport and http2.Transport allow the user to provide a custom
// dial function, and historically you only get the dial function from the
// Transport you're calling RoundTrip on.
ctx := context.WithValue(req.Context(), http2TransportContextKey{}, t)
//
// In addition, http2.Transport coalesces dials, which http.Transport historically
// has not.
dial := &transportRoundTripState{}
defer dial.roundTripDone()
ctx := context.WithValue(req.Context(), http2TransportContextKey{}, dial)
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
GotConn: func(httptrace.GotConnInfo) {
// Tell dialForRoundTrip that we have received a connection.
dial.gotConn()
},
})
req = req.WithContext(ctx)
return t1.RoundTrip(req)
}
var errCoalescedDialAbandoned = errors.New("http2: abandoned coalesced dial")
const coalescedDialRetryTimeout = 50 * time.Millisecond
// dialForRoundTrip is called (indirectly) by net/http when dialing a new connection
// for a request call which originated as an http2.Transport.RoundTrip call.
//
// It uses the http2.Transport's Dial hooks and coalesces dials.
func (t *Transport) dialForRoundTrip(ctx context.Context, dial *transportRoundTripState, network, address string) (net.Conn, error) {
gotconnc, rtdone := dial.startDial()
if rtdone {
// RoundTrip returned, no need for this dial to proceed.
return nil, errCoalescedDialAbandoned
}
var state *transportDialState
for {
// The first dial to an address registers itself with t.dials.
// When the dial finishes, it records the outcome and removes itself from t.dials
// so future dials will start a new round of coalescing.
//
// Subsequent dials observe that a dial is in progress, and skip dialing.
t.dialMu.Lock()
if t.dials == nil {
t.dials = make(map[string]*transportDialState)
}
leader := t.dials[address]
if leader == nil {
// No entry in t.dials to coalesce with. Add ourselves as leader.
state = &transportDialState{
dialc: make(chan struct{}),
rtdonec: dial.rtdonec,
}
t.dials[address] = state
}
t.dialMu.Unlock()
if leader == nil {
// We are the leader, so we should dial for real.
break
}
// Coalesce with a previous dial.
var (
done = false
leaderdialc = leader.dialc
leaderrtdonec = leader.rtdonec
timerc <-chan time.Time
)
for !done {
if leaderdialc == nil && leaderrtdonec == nil && timerc == nil {
// The leader finished dialing, and its RoundTrip finished.
//
// There are several possibilities, which reduce to:
// - The RoundTrip we are dialing for is about to receive
// a connection (possibly the one the leader just dialed),
// but we haven't observed it yet.
// - There's something wrong with the leader's connection,
// such as a TLS handshake failure.
//
// We have no good way to distinguish between these cases.
//
// Wait a short time for the leader's connection (or some
// other usable connection) to be delivered to our RoundTrip.
// If it is not, start a dial of our own.
timerc = time.After(coalescedDialRetryTimeout)
}
select {
case <-leaderdialc:
// The leader's dial completed.
if leader.dialErr != nil {
return nil, leader.dialErr
}
leaderdialc = nil
case <-leaderrtdonec:
// The leader's RoundTrip completed.
leaderrtdonec = nil
case <-timerc:
// The leader's dial and RoundTrip completed, and some time
// has passed. Assume we're not getting a connection and
// restart the dial process.
done = true
case <-dial.rtdonec:
// Our RoundTrip returned.
return nil, errCoalescedDialAbandoned
case <-gotconnc:
// Our RoundTrip got a connection.
return nil, errCoalescedDialAbandoned
case <-ctx.Done():
// net/http abandoned the dial.
return nil, ctx.Err()
}
}
}
// Dial for real.
tlsConf := t.TLSClientConfig
if tlsConf == nil {
tlsConf = &tls.Config{}
} else {
tlsConf = tlsConf.Clone()
}
if !slices.Contains(tlsConf.NextProtos, "h2") {
tlsConf.NextProtos = append([]string{"h2"}, tlsConf.NextProtos...)
}
if tlsConf.ServerName == "" {
host, _, err := net.SplitHostPort(address)
if err == nil {
tlsConf.ServerName = host
}
}
nc, err := t.dialTLS(ctx, network, address, tlsConf)
// Remove this dial from t.dials, so future dial attempts will not coalesce with it.
t.dialMu.Lock()
delete(t.dials, address)
t.dialMu.Unlock()
// Notify any dial coalesced with this one that we are done.
state.dialErr = err
close(state.dialc)
return nc, err
}
func (t *Transport) closeIdleConnections() {
t1 := t.init()
t1.CloseIdleConnections()
+4 -3
View File
@@ -79,10 +79,11 @@ func (wr FrameWriteRequest) Consume(n int32) (FrameWriteRequest, FrameWriteReque
}
// Might need to split after applying limits.
allowed := wr.stream.flow.available()
if n < allowed {
allowed = n
avail, ok := wr.stream.flow.available()
if !ok {
return empty, empty, 0
}
allowed := min(n, avail)
if wr.stream.sc.maxFrameSize < allowed {
allowed = wr.stream.sc.maxFrameSize
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package httpcommon
import (
"compress/flate"
"compress/gzip"
"errors"
"io"
"io/fs"
"sync"
)
var errConcurrentRead = errors.New("http: concurrent read on response body")
// incomparable is a zero-width, non-comparable type. Adding it to a struct
// makes that struct also non-comparable, and generally doesn't add
// any size (as long as it's first).
type incomparable [0]func()
// GzipReader wraps a response body so it can lazily
// get gzip.Reader from the pool on the first call to Read.
// After Close is called it puts gzip.Reader to the pool immediately
// if there is no Read in progress or later when Read completes.
type GzipReader struct {
_ incomparable
Body io.ReadCloser // underlying Response.Body
mu sync.Mutex // guards zr and zerr
zr *gzip.Reader // stores gzip reader from the pool between reads
zerr error // sticky gzip reader init error or sentinel value to detect concurrent read and read after close
}
type eofReader struct{}
func (eofReader) Read([]byte) (int, error) { return 0, io.EOF }
func (eofReader) ReadByte() (byte, error) { return 0, io.EOF }
var gzipPool = sync.Pool{New: func() any { return new(gzip.Reader) }}
// gzipPoolGet gets a gzip.Reader from the pool and resets it to read from r.
func gzipPoolGet(r io.Reader) (*gzip.Reader, error) {
zr := gzipPool.Get().(*gzip.Reader)
if err := zr.Reset(r); err != nil {
gzipPoolPut(zr)
return nil, err
}
return zr, nil
}
// gzipPoolPut puts a gzip.Reader back into the pool.
func gzipPoolPut(zr *gzip.Reader) {
// Reset will allocate bufio.Reader if we pass it anything
// other than a flate.Reader, so ensure that it's getting one.
var r flate.Reader = eofReader{}
zr.Reset(r)
gzipPool.Put(zr)
}
// acquire returns a gzip.Reader for reading response body.
// The reader must be released after use.
func (gz *GzipReader) acquire() (*gzip.Reader, error) {
gz.mu.Lock()
defer gz.mu.Unlock()
if gz.zerr != nil {
return nil, gz.zerr
}
if gz.zr == nil {
// gzipPoolGet might block indefinitely since it reads the gzip header.
// Therefore, drop mu temporarily when using gzipPoolGet.
// We set zerr to errConcurrentRead to prevent concurrent read
// even when mu is temporarily dropped.
gz.zerr = errConcurrentRead
gz.mu.Unlock()
zr, err := gzipPoolGet(gz.Body)
gz.mu.Lock()
// Guard against Close being called while gzipPoolGet is running.
if gz.zerr != errConcurrentRead {
if zr != nil {
gzipPoolPut(zr)
}
return nil, gz.zerr
}
gz.zr, gz.zerr = zr, err
if gz.zerr != nil {
return nil, gz.zerr
}
}
ret := gz.zr
gz.zr, gz.zerr = nil, errConcurrentRead
return ret, nil
}
// release returns the gzip.Reader to the pool if Close was called during Read.
func (gz *GzipReader) release(zr *gzip.Reader) {
gz.mu.Lock()
defer gz.mu.Unlock()
if gz.zerr == errConcurrentRead {
gz.zr, gz.zerr = zr, nil
} else { // fs.ErrClosed
gzipPoolPut(zr)
}
}
// close returns the gzip.Reader to the pool immediately or
// signals release to do so after Read completes.
func (gz *GzipReader) close() {
gz.mu.Lock()
defer gz.mu.Unlock()
if gz.zerr == nil && gz.zr != nil {
gzipPoolPut(gz.zr)
gz.zr = nil
}
gz.zerr = fs.ErrClosed
}
func (gz *GzipReader) Read(p []byte) (n int, err error) {
zr, err := gz.acquire()
if err != nil {
return 0, err
}
defer gz.release(zr)
return zr.Read(p)
}
func (gz *GzipReader) Close() error {
gz.close()
return gz.Body.Close()
}
+7 -7
View File
@@ -1,11 +1,11 @@
# dario.cat/mergo v1.0.2
## explicit; go 1.13
dario.cat/mergo
# github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a
## explicit; go 1.16
# github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4
## explicit; go 1.17
github.com/Azure/go-ansiterm
github.com/Azure/go-ansiterm/winterm
# github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0
# github.com/Microsoft/go-winio v0.6.3
## explicit; go 1.26.0
github.com/Microsoft/go-winio
github.com/Microsoft/go-winio/internal/fs
@@ -71,7 +71,7 @@ github.com/docker/distribution/registry/storage/cache/memory
## explicit; go 1.21
github.com/docker/docker-credential-helpers/client
github.com/docker/docker-credential-helpers/credentials
# github.com/docker/go-connections v0.8.1
# github.com/docker/go-connections v0.8.2
## explicit; go 1.23
github.com/docker/go-connections/nat
github.com/docker/go-connections/sockets
@@ -155,7 +155,7 @@ github.com/klauspost/compress/internal/le
github.com/klauspost/compress/internal/snapref
github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd/internal/xxhash
# github.com/mattn/go-runewidth v0.0.30
# github.com/mattn/go-runewidth v0.0.31
## explicit; go 1.23
github.com/mattn/go-runewidth
# github.com/moby/docker-image-spec v1.3.1
@@ -189,7 +189,7 @@ github.com/moby/moby/api/types/storage
github.com/moby/moby/api/types/swarm
github.com/moby/moby/api/types/system
github.com/moby/moby/api/types/volume
# github.com/moby/moby/client v0.6.1
# github.com/moby/moby/client v0.6.2
## explicit; go 1.24
github.com/moby/moby/client
github.com/moby/moby/client/internal
@@ -397,7 +397,7 @@ golang.org/x/mod/internal/lazyregexp
golang.org/x/mod/modfile
golang.org/x/mod/module
golang.org/x/mod/semver
# golang.org/x/net v0.59.0
# golang.org/x/net v0.60.0
## explicit; go 1.26.0
golang.org/x/net/http/httpguts
golang.org/x/net/http2