Merge branch 'distribution:main' into dev/mutil-proxy

This commit is contained in:
smartyhero 2025-02-25 11:53:36 +08:00 committed by GitHub
commit a709eca696
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 869 additions and 186 deletions

View File

@ -33,8 +33,8 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
go: go:
- 1.22.8 - 1.22.12
- 1.23.2 - 1.23.6
target: target:
- test-coverage - test-coverage
- test-cloud-storage - test-cloud-storage
@ -118,8 +118,9 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- -
name: Build artifacts name: Build artifacts
uses: docker/bake-action@v5 uses: docker/bake-action@v6
with: with:
source: .
targets: artifact-all targets: artifact-all
- -
name: Rename provenance name: Rename provenance
@ -139,15 +140,16 @@ jobs:
tree -nh ./bin tree -nh ./bin
- -
name: Upload artifacts name: Upload artifacts
uses: actions/upload-artifact@v4.3.6 uses: actions/upload-artifact@v4.6.0
with: with:
name: registry name: registry
path: ./bin/* path: ./bin/*
if-no-files-found: error if-no-files-found: error
- -
name: Build image name: Build image
uses: docker/bake-action@v5 uses: docker/bake-action@v6
with: with:
source: .
files: | files: |
./docker-bake.hcl ./docker-bake.hcl
${{ steps.meta.outputs.bake-file }} ${{ steps.meta.outputs.bake-file }}

View File

@ -15,14 +15,9 @@ jobs:
run-conformance-test: run-conformance-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- -
name: Build image name: Build image
uses: docker/bake-action@v5 uses: docker/bake-action@v6
with: with:
targets: image-local targets: image-local
- -
@ -46,11 +41,11 @@ jobs:
OCI_HIDE_SKIPPED_WORKFLOWS: 1 OCI_HIDE_SKIPPED_WORKFLOWS: 1
- -
name: Move test results name: Move test results
run: mkdir -p .out/ && mv {report.html,junit.xml} .out/ run: mkdir -p out/ && mv {report.html,junit.xml} out/
- -
name: Upload test results name: Upload test results
uses: actions/upload-artifact@v4.3.6 uses: actions/upload-artifact@v4.6.0
with: with:
name: oci-test-results-${{ github.sha }} name: oci-test-results-${{ github.sha }}
path: .out/ path: out/
if-no-files-found: error if-no-files-found: error

View File

@ -13,6 +13,7 @@ on:
- dockerfiles/docs.Dockerfile - dockerfiles/docs.Dockerfile
- docs/** - docs/**
workflow_dispatch: workflow_dispatch:
pull_request:
jobs: jobs:
# Build job # Build job
@ -22,28 +23,28 @@ jobs:
contents: read contents: read
# Build the site and upload artifacts using actions/upload-pages-artifact # Build the site and upload artifacts using actions/upload-pages-artifact
steps: steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages - name: Setup Pages
id: pages id: pages
uses: actions/configure-pages@v5 uses: actions/configure-pages@v5
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build docs - name: Build docs
uses: docker/bake-action@v5 uses: docker/bake-action@v6
with: with:
files: |
docker-bake.hcl
targets: docs-export targets: docs-export
provenance: false provenance: false
set: | set: |
*.cache-from=type=gha,scope=docs *.cache-from=type=gha,scope=docs
*.cache-to=type=gha,scope=docs,mode=max *.cache-to=type=gha,scope=docs,mode=max
- name: Fix permissions - name: Fix permissions
run: | run: |
chmod -c -R +rX "./build/docs" | while read line; do chmod -c -R +rX "./build/docs" | while read line; do
echo "::warning title=Invalid file permissions automatically fixed::$line" echo "::warning title=Invalid file permissions automatically fixed::$line"
done done
- name: Upload Pages artifact - name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v3
with: with:
@ -51,6 +52,7 @@ jobs:
# Deploy job # Deploy job
deploy: deploy:
if: github.event_name != 'pull_request'
# Add a dependency to the build job # Add a dependency to the build job
needs: build needs: build

View File

@ -25,8 +25,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- -
name: Build image name: Build image
uses: docker/bake-action@v5 uses: docker/bake-action@v6
with: with:
source: .
targets: image-local targets: image-local
- -
name: Start distribution server name: Start distribution server

View File

@ -46,7 +46,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # tag=v4.3.6 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # tag=v4.6.0
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif

View File

@ -93,6 +93,7 @@ Justin Cormack <justin.cormack@docker.com> <justin@specialbusservice.com>
Kirat Singh <kirat.singh@gmail.com> Kirat Singh <kirat.singh@gmail.com>
Kirat Singh <kirat.singh@gmail.com> <kirat.singh@beacon.io> Kirat Singh <kirat.singh@gmail.com> <kirat.singh@beacon.io>
Kirat Singh <kirat.singh@gmail.com> <kirat.singh@wsq.io> Kirat Singh <kirat.singh@gmail.com> <kirat.singh@wsq.io>
krynju <krystian.gulinski@juliahub.com>
Kyle Squizzato <ksquizz@gmail.com> Kyle Squizzato <ksquizz@gmail.com>
Liang Zheng <zhengliang0901@gmail.com> Liang Zheng <zhengliang0901@gmail.com>
Luca Bruno <lucab@debian.org> Luca Bruno <lucab@debian.org>
@ -167,6 +168,7 @@ Tibor <tiborcasteleijn@gmail.com>
Tibor Vass <teabee89@gmail.com> Tibor Vass <teabee89@gmail.com>
Tibor Vass <teabee89@gmail.com> <tibor@docker.com> Tibor Vass <teabee89@gmail.com> <tibor@docker.com>
Tibor Vass <teabee89@gmail.com> <tiborvass@users.noreply.github.com> Tibor Vass <teabee89@gmail.com> <tiborvass@users.noreply.github.com>
Victor Barbu <git.zqk3n@vicb.cc>
Victor Vieux <victorvieux@gmail.com> Victor Vieux <victorvieux@gmail.com>
Victor Vieux <victorvieux@gmail.com> <dev@vvieux.com> Victor Vieux <victorvieux@gmail.com> <dev@vvieux.com>
Victor Vieux <victorvieux@gmail.com> <victor.vieux@docker.com> Victor Vieux <victorvieux@gmail.com> <victor.vieux@docker.com>
@ -185,6 +187,7 @@ Vincent Demeester <vincent.demeester@docker.com> <vincent@demeester.fr>
Vincent Demeester <vincent.demeester@docker.com> <vincent@sbr.pm> Vincent Demeester <vincent.demeester@docker.com> <vincent@sbr.pm>
Vincent Giersch <vincent@giersch.fr> Vincent Giersch <vincent@giersch.fr>
Vincent Giersch <vincent@giersch.fr> <vincent.giersch@ovh.net> Vincent Giersch <vincent@giersch.fr> <vincent.giersch@ovh.net>
vitshev <vitali.shevchenko@gmail.com>
Wang Yan <wangyan@vmware.com> Wang Yan <wangyan@vmware.com>
Wen-Quan Li <legendarilylwq@gmail.com> Wen-Quan Li <legendarilylwq@gmail.com>
Wen-Quan Li <legendarilylwq@gmail.com> <wenquan.li@hp.com> Wen-Quan Li <legendarilylwq@gmail.com> <wenquan.li@hp.com>

37
AUTHORS
View File

@ -30,6 +30,7 @@ allencloud <allen.sun@daocloud.io>
Alvin Feng <alvin4feng@yahoo.com> Alvin Feng <alvin4feng@yahoo.com>
amitshukla <ashukla73@hotmail.com> amitshukla <ashukla73@hotmail.com>
Amy Lindburg <amy.lindburg@docker.com> Amy Lindburg <amy.lindburg@docker.com>
Anders Ingemann <aim@orbit.online>
Andreas Hassing <andreas@famhassing.dk> Andreas Hassing <andreas@famhassing.dk>
Andrew Bulford <andrew.bulford@redmatter.com> Andrew Bulford <andrew.bulford@redmatter.com>
Andrew Hsu <andrewhsu@acm.org> Andrew Hsu <andrewhsu@acm.org>
@ -40,14 +41,17 @@ Andrew Meredith <andymeredith@gmail.com>
Andrew T Nguyen <andrew.nguyen@docker.com> Andrew T Nguyen <andrew.nguyen@docker.com>
Andrews Medina <andrewsmedina@gmail.com> Andrews Medina <andrewsmedina@gmail.com>
Andrey Kostov <kostov.andrey@gmail.com> Andrey Kostov <kostov.andrey@gmail.com>
Andrey Smirnov <andrey.smirnov@siderolabs.com>
Andrii Soldatenko <andrii.soldatenko@gmail.com> Andrii Soldatenko <andrii.soldatenko@gmail.com>
Andy Goldstein <agoldste@redhat.com> Andy Goldstein <agoldste@redhat.com>
andyzhangx <xiazhang@microsoft.com> andyzhangx <xiazhang@microsoft.com>
Anian Z <ziegler@sicony.de> Anian Z <ziegler@sicony.de>
Anil Belur <askb23@gmail.com> Anil Belur <askb23@gmail.com>
Anis Elleuch <vadmeste@gmail.com> Anis Elleuch <vadmeste@gmail.com>
Ankur Kothiwal <ankur.kothiwal@cern.com>
Ankush Agarwal <ankushagarwal11@gmail.com> Ankush Agarwal <ankushagarwal11@gmail.com>
Anne Henmi <41210220+ahh-docker@users.noreply.github.com> Anne Henmi <41210220+ahh-docker@users.noreply.github.com>
Anthony Ramahay <thewolt@gmail.com>
Anton Tiurin <noxiouz@yandex.ru> Anton Tiurin <noxiouz@yandex.ru>
Antonio Mercado <amercado@thinknode.com> Antonio Mercado <amercado@thinknode.com>
Antonio Murdaca <antonio.murdaca@gmail.com> Antonio Murdaca <antonio.murdaca@gmail.com>
@ -59,6 +63,7 @@ Arnaud Porterie <arnaud.porterie@docker.com>
Arthur Baars <arthur@semmle.com> Arthur Baars <arthur@semmle.com>
Arthur Gautier <baloo@gandi.net> Arthur Gautier <baloo@gandi.net>
Asuka Suzuki <hello@tanksuzuki.com> Asuka Suzuki <hello@tanksuzuki.com>
Austin Vazquez <macedonv@amazon.com>
Avi Miller <avi.miller@oracle.com> Avi Miller <avi.miller@oracle.com>
Aviral Takkar <aviral26@users.noreply.github.com> Aviral Takkar <aviral26@users.noreply.github.com>
Ayose Cazorla <ayosec@gmail.com> Ayose Cazorla <ayosec@gmail.com>
@ -70,6 +75,7 @@ Ben Emamian <ben@ictace.com>
Ben Firshman <ben@firshman.co.uk> Ben Firshman <ben@firshman.co.uk>
Ben Kochie <superq@gmail.com> Ben Kochie <superq@gmail.com>
Ben Manuel <ben.manuel@procore.com> Ben Manuel <ben.manuel@procore.com>
Benjamin Schanzel <benjamin.schanzel@bmw.de>
Bhavin Gandhi <bhavin192@users.noreply.github.com> Bhavin Gandhi <bhavin192@users.noreply.github.com>
Bill <NonCreature0714@users.noreply.github.com> Bill <NonCreature0714@users.noreply.github.com>
bin liu <liubin0329@gmail.com> bin liu <liubin0329@gmail.com>
@ -96,6 +102,7 @@ Chris Patterson <chrispat@github.com>
Christopher Yeleighton <ne01026@shark.2a.pl> Christopher Yeleighton <ne01026@shark.2a.pl>
Christy Perez <christy@linux.vnet.ibm.com> Christy Perez <christy@linux.vnet.ibm.com>
Chuanying Du <cydu@google.com> Chuanying Du <cydu@google.com>
Chun-Hung Hsiao <chhsiao@google.com>
Clayton Coleman <ccoleman@redhat.com> Clayton Coleman <ccoleman@redhat.com>
Collin Shoop <cshoop@digitalocean.com> Collin Shoop <cshoop@digitalocean.com>
Corey Quon <corey.quon@gmail.com> Corey Quon <corey.quon@gmail.com>
@ -158,8 +165,10 @@ Elliot Pahl <elliot.pahl@gmail.com>
elsanli(李楠) <elsanli@tencent.com> elsanli(李楠) <elsanli@tencent.com>
Elton Stoneman <elton@sixeyed.com> Elton Stoneman <elton@sixeyed.com>
Emmanuel Briney <emmanuel.briney@docker.com> Emmanuel Briney <emmanuel.briney@docker.com>
Emmanuel Ferdman <emmanuelferdman@gmail.com>
Eng Zer Jun <engzerjun@gmail.com> Eng Zer Jun <engzerjun@gmail.com>
Eohyung Lee <liquidnuker@gmail.com> Eohyung Lee <liquidnuker@gmail.com>
erezrokah <erezrokah@users.noreply.github.com>
Eric Yang <windfarer@gmail.com> Eric Yang <windfarer@gmail.com>
Erica Windisch <erica@windisch.us> Erica Windisch <erica@windisch.us>
Erik Hollensbe <github@hollensbe.org> Erik Hollensbe <github@hollensbe.org>
@ -194,12 +203,14 @@ Gleb M Borisov <borisov.gleb@gmail.com>
Gleb Schukin <gschukin@ptsecurity.com> Gleb Schukin <gschukin@ptsecurity.com>
glefloch <glfloch@gmail.com> glefloch <glfloch@gmail.com>
Glyn Owen Hanmer <1295698+glynternet@users.noreply.github.com> Glyn Owen Hanmer <1295698+glynternet@users.noreply.github.com>
goodactive <goodactive@qq.com>
gotgelf <gotgelf@gmail.com> gotgelf <gotgelf@gmail.com>
Grachev Mikhail <work@mgrachev.com> Grachev Mikhail <work@mgrachev.com>
Grant Watters <grant.watters@docker.com> Grant Watters <grant.watters@docker.com>
Greg Rebholz <gregrebholz@gmail.com> Greg Rebholz <gregrebholz@gmail.com>
Guillaume J. Charmes <charmes.guillaume@gmail.com> Guillaume J. Charmes <charmes.guillaume@gmail.com>
Guillaume Rose <guillaume.rose@docker.com> Guillaume Rose <guillaume.rose@docker.com>
guoguangwu <guoguangwug@gmail.com>
Gábor Lipták <gliptak@gmail.com> Gábor Lipták <gliptak@gmail.com>
harche <p.harshal@gmail.com> harche <p.harshal@gmail.com>
hasheddan <georgedanielmangum@gmail.com> hasheddan <georgedanielmangum@gmail.com>
@ -211,9 +222,9 @@ Hu Keping <hukeping@huawei.com>
Hua Wang <wanghua.humble@gmail.com> Hua Wang <wanghua.humble@gmail.com>
HuKeping <hukeping@huawei.com> HuKeping <hukeping@huawei.com>
Huu Nguyen <whoshuu@gmail.com> Huu Nguyen <whoshuu@gmail.com>
ialidzhikov <i.alidjikov@gmail.com>
Ian Babrou <ibobrik@gmail.com> Ian Babrou <ibobrik@gmail.com>
iasoon <ilion.beyst@gmail.com> iasoon <ilion.beyst@gmail.com>
icefed <zlwangel@gmail.com>
igayoso <igayoso@gmail.com> igayoso <igayoso@gmail.com>
Igor Dolzhikov <bluesriverz@gmail.com> Igor Dolzhikov <bluesriverz@gmail.com>
Igor Morozov <igmorv@gmail.com> Igor Morozov <igmorv@gmail.com>
@ -225,12 +236,14 @@ Ismail Alidzhikov <i.alidjikov@gmail.com>
Jack Baines <jack.baines@uk.ibm.com> Jack Baines <jack.baines@uk.ibm.com>
Jack Griffin <jackpg14@gmail.com> Jack Griffin <jackpg14@gmail.com>
Jacob Atzen <jatzen@gmail.com> Jacob Atzen <jatzen@gmail.com>
Jaime Martinez <jmartinez@gitlab.com>
Jake Moshenko <jake@devtable.com> Jake Moshenko <jake@devtable.com>
Jakob Ackermann <das7pad@outlook.com> Jakob Ackermann <das7pad@outlook.com>
Jakub Mikulas <jakub@mikul.as> Jakub Mikulas <jakub@mikul.as>
James Findley <jfindley@fastmail.com> James Findley <jfindley@fastmail.com>
James Hewitt <james.hewitt@uk.ibm.com> James Hewitt <james.hewitt@uk.ibm.com>
James Lal <james@lightsofapollo.com> James Lal <james@lightsofapollo.com>
Jan-Otto Kröpke <github@jkroepke.de>
Jason Freidman <jason.freidman@gmail.com> Jason Freidman <jason.freidman@gmail.com>
Jason Heiss <jheiss@aput.net> Jason Heiss <jheiss@aput.net>
Javier Palomo Almena <javier.palomo.almena@gmail.com> Javier Palomo Almena <javier.palomo.almena@gmail.com>
@ -283,6 +296,9 @@ Kevin Lin <kevin@kelda.io>
Kevin Robatel <kevinrob2@gmail.com> Kevin Robatel <kevinrob2@gmail.com>
Kira <me@imkira.com> Kira <me@imkira.com>
Kirat Singh <kirat.singh@gmail.com> Kirat Singh <kirat.singh@gmail.com>
krynju <krystian.gulinski@juliahub.com>
Kyle Squizzato <ksquizz@gmail.com>
Kyle Squizzato <kyle@replicated.com>
L-Hudson <44844738+L-Hudson@users.noreply.github.com> L-Hudson <44844738+L-Hudson@users.noreply.github.com>
Lachlan Cooper <lachlancooper@gmail.com> Lachlan Cooper <lachlancooper@gmail.com>
Laura Brehm <laurabrehm@hey.com> Laura Brehm <laurabrehm@hey.com>
@ -292,6 +308,7 @@ Leonardo Azize Martins <lazize@users.noreply.github.com>
leonstrand <leonstrand@gmail.com> leonstrand <leonstrand@gmail.com>
Li Yi <denverdino@gmail.com> Li Yi <denverdino@gmail.com>
Liam White <liamwhite@uk.ibm.com> Liam White <liamwhite@uk.ibm.com>
Liang Zheng <zhengliang0901@gmail.com>
libo.huang <huanglibo2010@gmail.com> libo.huang <huanglibo2010@gmail.com>
LingFaKe <lingfake@huawei.com> LingFaKe <lingfake@huawei.com>
Liron Levin <liron@twistlock.com> Liron Levin <liron@twistlock.com>
@ -309,6 +326,7 @@ Lucas Santos <lhs.santoss@gmail.com>
Luis Lobo Borobia <luislobo@gmail.com> Luis Lobo Borobia <luislobo@gmail.com>
Luke Carpenter <x@rubynerd.net> Luke Carpenter <x@rubynerd.net>
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com> Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
Mahmoud Kandil <47168819+MahmoudKKandil@users.noreply.github.com>
Makoto Oda <truth_jp_4133@yahoo.co.jp> Makoto Oda <truth_jp_4133@yahoo.co.jp>
mallchin <mallchin@mac.com> mallchin <mallchin@mac.com>
Manish Tomar <manish.tomar@docker.com> Manish Tomar <manish.tomar@docker.com>
@ -316,8 +334,10 @@ Marco Hennings <marco.hennings@freiheit.com>
Marcus Martins <marcus@docker.com> Marcus Martins <marcus@docker.com>
Maria Bermudez <bermudez.mt@gmail.com> Maria Bermudez <bermudez.mt@gmail.com>
Mark Sagi-Kazar <mark.sagikazar@gmail.com> Mark Sagi-Kazar <mark.sagikazar@gmail.com>
Markus Thömmes <markusthoemmes@me.com>
Mary Anthony <mary@docker.com> Mary Anthony <mary@docker.com>
Masataka Mizukoshi <m.mizukoshi.wakuwaku@gmail.com> Masataka Mizukoshi <m.mizukoshi.wakuwaku@gmail.com>
Matheus Macabu <macabu.matheus@gmail.com>
Matin Rahmanian <itsmatinx@gmail.com> Matin Rahmanian <itsmatinx@gmail.com>
MATSUMOTO TAKEAKI <takeaki.matsumoto@linecorp.com> MATSUMOTO TAKEAKI <takeaki.matsumoto@linecorp.com>
Matt Bentley <mbentley@mbentley.net> Matt Bentley <mbentley@mbentley.net>
@ -342,6 +362,8 @@ Michal Minar <miminar@redhat.com>
Mike Brown <brownwm@us.ibm.com> Mike Brown <brownwm@us.ibm.com>
Mike Lundy <mike@fluffypenguin.org> Mike Lundy <mike@fluffypenguin.org>
Mike Truman <miketruman42@gmail.com> Mike Truman <miketruman42@gmail.com>
Mikel Rychliski <mikel@mikelr.com>
Mikhail f. Shiryaev <mr.felixoid@gmail.com>
Milos Gajdos <milosthegajdos@gmail.com> Milos Gajdos <milosthegajdos@gmail.com>
Miquel Sabaté <msabate@suse.com> Miquel Sabaté <msabate@suse.com>
mlmhl <409107750@qq.com> mlmhl <409107750@qq.com>
@ -368,6 +390,7 @@ Nycholas de Oliveira e Oliveira <nycholas@gmail.com>
Oilbeater <liumengxinfly@gmail.com> Oilbeater <liumengxinfly@gmail.com>
Oleg Bulatov <oleg@bulatov.me> Oleg Bulatov <oleg@bulatov.me>
olegburov <oleg.burov@outlook.com> olegburov <oleg.burov@outlook.com>
oliver-goetz <o.goetz@sap.com>
Olivier <o+github@gambier.email> Olivier <o+github@gambier.email>
Olivier Gambier <olivier@docker.com> Olivier Gambier <olivier@docker.com>
Olivier Jacques <olivier.jacques@hp.com> Olivier Jacques <olivier.jacques@hp.com>
@ -381,6 +404,7 @@ Pascal Borreli <pascal@borreli.com>
Patrick Devine <patrick.devine@docker.com> Patrick Devine <patrick.devine@docker.com>
Patrick Easters <peasters@redhat.com> Patrick Easters <peasters@redhat.com>
Paul Cacheux <paul.cacheux@datadoghq.com> Paul Cacheux <paul.cacheux@datadoghq.com>
Paul Meyer <49727155+katexochen@users.noreply.github.com>
Pavel Antonov <ddc67cd@gmail.com> Pavel Antonov <ddc67cd@gmail.com>
Paweł Gronowski <pawel.gronowski@docker.com> Paweł Gronowski <pawel.gronowski@docker.com>
Per Lundberg <perlun@gmail.com> Per Lundberg <perlun@gmail.com>
@ -391,6 +415,7 @@ Phil Estes <estesp@gmail.com>
Philip Misiowiec <philip@atlashealth.com> Philip Misiowiec <philip@atlashealth.com>
Pierre-Yves Ritschard <pyr@spootnik.org> Pierre-Yves Ritschard <pyr@spootnik.org>
Pieter Scheffers <pieter.scheffers@gmail.com> Pieter Scheffers <pieter.scheffers@gmail.com>
Pratik <pratikgparikh@gmail.com>
Qiang Huang <h.huangqiang@huawei.com> Qiang Huang <h.huangqiang@huawei.com>
Qiao Anran <qiaoanran@gmail.com> Qiao Anran <qiaoanran@gmail.com>
Radon Rosborough <radon.neon@gmail.com> Radon Rosborough <radon.neon@gmail.com>
@ -405,6 +430,7 @@ Rober Morales-Chaparro <rober.morales@rstor.io>
Robert Kaussow <mail@geeklabor.de> Robert Kaussow <mail@geeklabor.de>
Robert Steward <speaktorob@users.noreply.github.com> Robert Steward <speaktorob@users.noreply.github.com>
Roberto G. Hashioka <roberto.hashioka@docker.com> Roberto G. Hashioka <roberto.hashioka@docker.com>
Robin Ketelbuters <robin.ketelbuters@gmail.com>
Rodolfo Carvalho <rhcarvalho@gmail.com> Rodolfo Carvalho <rhcarvalho@gmail.com>
ROY <qqbuby@gmail.com> ROY <qqbuby@gmail.com>
Rui Cao <ruicao@alauda.io> Rui Cao <ruicao@alauda.io>
@ -459,9 +485,11 @@ sun jian <cnhttpd@gmail.com>
Sungho Moon <sungho.moon@navercorp.com> Sungho Moon <sungho.moon@navercorp.com>
Sven Dowideit <SvenDowideit@home.org.au> Sven Dowideit <SvenDowideit@home.org.au>
Sylvain Baubeau <sbaubeau@redhat.com> Sylvain Baubeau <sbaubeau@redhat.com>
Sylvain DESGRAIS <sylvain.desgrais@gmail.com>
syntaxkim <40621244+syntaxkim@users.noreply.github.com> syntaxkim <40621244+syntaxkim@users.noreply.github.com>
T N <tnir@users.noreply.github.com> T N <tnir@users.noreply.github.com>
t-eimizu <t-eimizu@aim.ac> t-eimizu <t-eimizu@aim.ac>
Tadeusz Dudkiewicz <tadeusz.dudkiewicz@rtbhouse.com>
Tariq Ibrahim <tariq181290@gmail.com> Tariq Ibrahim <tariq181290@gmail.com>
TaylorKanper <tony_kanper@hotmail.com> TaylorKanper <tony_kanper@hotmail.com>
Ted Reed <ted.reed@gmail.com> Ted Reed <ted.reed@gmail.com>
@ -469,7 +497,9 @@ Terin Stock <terinjokes@gmail.com>
tgic <farmer1992@gmail.com> tgic <farmer1992@gmail.com>
Thomas Berger <loki@lokis-chaos.de> Thomas Berger <loki@lokis-chaos.de>
Thomas Sjögren <konstruktoid@users.noreply.github.com> Thomas Sjögren <konstruktoid@users.noreply.github.com>
Thomas Way <thomas@6f.io>
Tianon Gravi <admwiggin@gmail.com> Tianon Gravi <admwiggin@gmail.com>
Tibor <tiborcasteleijn@gmail.com>
Tibor Vass <teabee89@gmail.com> Tibor Vass <teabee89@gmail.com>
tifayuki <tifayuki@gmail.com> tifayuki <tifayuki@gmail.com>
Tiger Kaovilai <tkaovila@redhat.com> Tiger Kaovilai <tkaovila@redhat.com>
@ -477,6 +507,7 @@ Tobias Fuhrimann <mastertinner@users.noreply.github.com>
Tobias Schwab <tobias.schwab@dynport.de> Tobias Schwab <tobias.schwab@dynport.de>
Tom Hayward <thayward@infoblox.com> Tom Hayward <thayward@infoblox.com>
Tom Hu <tomhu1096@gmail.com> Tom Hu <tomhu1096@gmail.com>
tomoya-kawaguchi <yamo7yamoto@gmail.com>
Tonis Tiigi <tonistiigi@gmail.com> Tonis Tiigi <tonistiigi@gmail.com>
Tony Holdstock-Brown <tony@docker.com> Tony Holdstock-Brown <tony@docker.com>
Tosone <i@tosone.cn> Tosone <i@tosone.cn>
@ -490,6 +521,7 @@ Usha Mandya <usha.mandya@docker.com>
Vaidas Jablonskis <jablonskis@gmail.com> Vaidas Jablonskis <jablonskis@gmail.com>
Vega Chou <VegeChou@users.noreply.github.com> Vega Chou <VegeChou@users.noreply.github.com>
Veres Lajos <vlajos@gmail.com> Veres Lajos <vlajos@gmail.com>
Victor Barbu <git.zqk3n@vicb.cc>
Victor Vieux <victorvieux@gmail.com> Victor Vieux <victorvieux@gmail.com>
Victoria Bialas <victoria.bialas@docker.com> Victoria Bialas <victoria.bialas@docker.com>
Vidar <vl@ez.no> Vidar <vl@ez.no>
@ -498,6 +530,8 @@ Vincent Batts <vbatts@redhat.com>
Vincent Demeester <vincent.demeester@docker.com> Vincent Demeester <vincent.demeester@docker.com>
Vincent Giersch <vincent@giersch.fr> Vincent Giersch <vincent@giersch.fr>
Vishesh Jindal <vishesh92@gmail.com> Vishesh Jindal <vishesh92@gmail.com>
vitshev <vitali.shevchenko@gmail.com>
Vitshev <vitshev@tracto.ai>
W. Trevor King <wking@tremily.us> W. Trevor King <wking@tremily.us>
Wang Jie <wangjie5@chinaskycloud.com> Wang Jie <wangjie5@chinaskycloud.com>
Wang Yan <wangyan@vmware.com> Wang Yan <wangyan@vmware.com>
@ -510,6 +544,7 @@ Wen-Quan Li <legendarilylwq@gmail.com>
Wenkai Yin <yinw@vmware.com> Wenkai Yin <yinw@vmware.com>
william wei <1342247033@qq.com> william wei <1342247033@qq.com>
xg.song <xg.song@venusource.com> xg.song <xg.song@venusource.com>
xiaoxiangxianzi <zhaoyizheng@outlook.com>
xiekeyang <xiekeyang@huawei.com> xiekeyang <xiekeyang@huawei.com>
Xueshan Feng <xueshan.feng@gmail.com> Xueshan Feng <xueshan.feng@gmail.com>
Yann ROBERT <yann.robert@anantaplex.fr> Yann ROBERT <yann.robert@anantaplex.fr>

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.23.2 ARG GO_VERSION=1.23.6
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.21
ARG XX_VERSION=1.6.1 ARG XX_VERSION=1.6.1
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx

View File

@ -11,7 +11,7 @@
"heww","He Weiwei","hweiwei@vmware.com" "heww","He Weiwei","hweiwei@vmware.com"
"joaodrp","João Pereira","jpereira@gitlab.com" "joaodrp","João Pereira","jpereira@gitlab.com"
"justincormack","Justin Cormack","justin.cormack@docker.com" "justincormack","Justin Cormack","justin.cormack@docker.com"
"squizzi","Kyle Squizzato","ksquizzato@mirantis.com" "squizzi","Kyle Squizzato","ksquizz@gmail.com"
"milosgajdos","Milos Gajdos","milosthegajdos@gmail.com" "milosgajdos","Milos Gajdos","milosthegajdos@gmail.com"
"sargun","Sargun Dhillon","sargun@sargun.me" "sargun","Sargun Dhillon","sargun@sargun.me"
"wy65701436","Wang Yan","wangyan@vmware.com" "wy65701436","Wang Yan","wangyan@vmware.com"

View File

@ -109,6 +109,10 @@ type Configuration struct {
// A file may contain multiple CA certificates encoded as PEM // A file may contain multiple CA certificates encoded as PEM
ClientCAs []string `yaml:"clientcas,omitempty"` ClientCAs []string `yaml:"clientcas,omitempty"`
// Client certificate authentication mode
// One of: request-client-cert, require-any-client-cert, verify-client-cert-if-given, require-and-verify-client-cert
ClientAuth ClientAuth `yaml:"clientauth,omitempty"`
// Specifies the lowest TLS version allowed // Specifies the lowest TLS version allowed
MinimumTLS string `yaml:"minimumtls,omitempty"` MinimumTLS string `yaml:"minimumtls,omitempty"`
@ -899,3 +903,35 @@ func setFieldValue(field reflect.Value, value interface{}) error {
} }
return nil return nil
} }
const (
ClientAuthRequestClientCert = "request-client-cert"
ClientAuthRequireAnyClientCert = "require-any-client-cert"
ClientAuthVerifyClientCertIfGiven = "verify-client-cert-if-given"
ClientAuthRequireAndVerifyClientCert = "require-and-verify-client-cert"
)
type ClientAuth string
// UnmarshalYAML implements the yaml.Umarshaler interface
// Unmarshals a string into a ClientAuth, validating that it represents a valid ClientAuth mod
func (clientAuth *ClientAuth) UnmarshalYAML(unmarshal func(interface{}) error) error {
var clientAuthString string
err := unmarshal(&clientAuthString)
if err != nil {
return err
}
switch clientAuthString {
case ClientAuthRequestClientCert:
case ClientAuthRequireAnyClientCert:
case ClientAuthVerifyClientCertIfGiven:
case ClientAuthRequireAndVerifyClientCert:
default:
return fmt.Errorf("invalid ClientAuth %s Must be one of: %s, %s, %s, %s", clientAuthString, ClientAuthRequestClientCert, ClientAuthRequireAnyClientCert, ClientAuthVerifyClientCertIfGiven, ClientAuthRequireAndVerifyClientCert)
}
*clientAuth = ClientAuth(clientAuthString)
return nil
}

View File

@ -78,11 +78,12 @@ var configStruct = Configuration{
RelativeURLs bool `yaml:"relativeurls,omitempty"` RelativeURLs bool `yaml:"relativeurls,omitempty"`
DrainTimeout time.Duration `yaml:"draintimeout,omitempty"` DrainTimeout time.Duration `yaml:"draintimeout,omitempty"`
TLS struct { TLS struct {
Certificate string `yaml:"certificate,omitempty"` Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"` Key string `yaml:"key,omitempty"`
ClientCAs []string `yaml:"clientcas,omitempty"` ClientCAs []string `yaml:"clientcas,omitempty"`
MinimumTLS string `yaml:"minimumtls,omitempty"` ClientAuth ClientAuth `yaml:"clientauth,omitempty"`
CipherSuites []string `yaml:"ciphersuites,omitempty"` MinimumTLS string `yaml:"minimumtls,omitempty"`
CipherSuites []string `yaml:"ciphersuites,omitempty"`
LetsEncrypt struct { LetsEncrypt struct {
CacheFile string `yaml:"cachefile,omitempty"` CacheFile string `yaml:"cachefile,omitempty"`
Email string `yaml:"email,omitempty"` Email string `yaml:"email,omitempty"`
@ -106,11 +107,12 @@ var configStruct = Configuration{
} `yaml:"h2c,omitempty"` } `yaml:"h2c,omitempty"`
}{ }{
TLS: struct { TLS: struct {
Certificate string `yaml:"certificate,omitempty"` Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"` Key string `yaml:"key,omitempty"`
ClientCAs []string `yaml:"clientcas,omitempty"` ClientCAs []string `yaml:"clientcas,omitempty"`
MinimumTLS string `yaml:"minimumtls,omitempty"` ClientAuth ClientAuth `yaml:"clientauth,omitempty"`
CipherSuites []string `yaml:"ciphersuites,omitempty"` MinimumTLS string `yaml:"minimumtls,omitempty"`
CipherSuites []string `yaml:"ciphersuites,omitempty"`
LetsEncrypt struct { LetsEncrypt struct {
CacheFile string `yaml:"cachefile,omitempty"` CacheFile string `yaml:"cachefile,omitempty"`
Email string `yaml:"email,omitempty"` Email string `yaml:"email,omitempty"`
@ -118,7 +120,8 @@ var configStruct = Configuration{
DirectoryURL string `yaml:"directoryurl,omitempty"` DirectoryURL string `yaml:"directoryurl,omitempty"`
} `yaml:"letsencrypt,omitempty"` } `yaml:"letsencrypt,omitempty"`
}{ }{
ClientCAs: []string{"/path/to/ca.pem"}, ClientCAs: []string{"/path/to/ca.pem"},
ClientAuth: ClientAuthVerifyClientCertIfGiven,
}, },
Headers: http.Header{ Headers: http.Header{
"X-Content-Type-Options": []string{"nosniff"}, "X-Content-Type-Options": []string{"nosniff"},
@ -202,6 +205,7 @@ http:
tls: tls:
clientcas: clientcas:
- /path/to/ca.pem - /path/to/ca.pem
clientauth: verify-client-cert-if-given
headers: headers:
X-Content-Type-Options: [nosniff] X-Content-Type-Options: [nosniff]
redis: redis:
@ -297,6 +301,7 @@ func (suite *ConfigSuite) TestParseInmemory() {
suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}} suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
suite.expectedConfig.Log.Fields = nil suite.expectedConfig.Log.Fields = nil
suite.expectedConfig.HTTP.TLS.ClientCAs = nil suite.expectedConfig.HTTP.TLS.ClientCAs = nil
suite.expectedConfig.HTTP.TLS.ClientAuth = ""
suite.expectedConfig.Redis = Redis{} suite.expectedConfig.Redis = Redis{}
config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1))) config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
@ -318,6 +323,7 @@ func (suite *ConfigSuite) TestParseIncomplete() {
suite.expectedConfig.Notifications = Notifications{} suite.expectedConfig.Notifications = Notifications{}
suite.expectedConfig.HTTP.Headers = nil suite.expectedConfig.HTTP.Headers = nil
suite.expectedConfig.HTTP.TLS.ClientCAs = nil suite.expectedConfig.HTTP.TLS.ClientCAs = nil
suite.expectedConfig.HTTP.TLS.ClientAuth = ""
suite.expectedConfig.Redis = Redis{} suite.expectedConfig.Redis = Redis{}
suite.expectedConfig.Validation.Manifests.Indexes.Platforms = "" suite.expectedConfig.Validation.Manifests.Indexes.Platforms = ""
@ -590,6 +596,7 @@ func copyConfig(config Configuration) *Configuration {
} }
configCopy.HTTP.TLS.ClientCAs = make([]string, 0, len(config.HTTP.TLS.ClientCAs)) configCopy.HTTP.TLS.ClientCAs = make([]string, 0, len(config.HTTP.TLS.ClientCAs))
configCopy.HTTP.TLS.ClientCAs = append(configCopy.HTTP.TLS.ClientCAs, config.HTTP.TLS.ClientCAs...) configCopy.HTTP.TLS.ClientCAs = append(configCopy.HTTP.TLS.ClientCAs, config.HTTP.TLS.ClientCAs...)
configCopy.HTTP.TLS.ClientAuth = config.HTTP.TLS.ClientAuth
configCopy.Redis = config.Redis configCopy.Redis = config.Redis
configCopy.Redis.TLS.Certificate = config.Redis.TLS.Certificate configCopy.Redis.TLS.Certificate = config.Redis.TLS.Certificate

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.21
FROM alpine:${ALPINE_VERSION} AS gen FROM alpine:${ALPINE_VERSION} AS gen
RUN apk add --no-cache git RUN apk add --no-cache git

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.23.2 ARG GO_VERSION=1.23.6
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.21
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
RUN apk add --no-cache git RUN apk add --no-cache git

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.23.2 ARG GO_VERSION=1.23.6
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.21
FROM alpine:${ALPINE_VERSION} AS base FROM alpine:${ALPINE_VERSION} AS base
RUN apk add --no-cache git gpg RUN apk add --no-cache git gpg

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.23.2 ARG GO_VERSION=1.23.6
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.21
ARG GOLANGCI_LINT_VERSION=v1.61.0 ARG GOLANGCI_LINT_VERSION=v1.61.0
ARG BUILDTAGS="" ARG BUILDTAGS=""

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.23.2 ARG GO_VERSION=1.23.6
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.21
ARG MODOUTDATED_VERSION=v0.8.0 ARG MODOUTDATED_VERSION=v0.8.0
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base

View File

@ -229,6 +229,7 @@ http:
clientcas: clientcas:
- /path/to/ca.pem - /path/to/ca.pem
- /path/to/another/ca.pem - /path/to/another/ca.pem
clientauth: require-and-verify-client-cert
letsencrypt: letsencrypt:
cachefile: /path/to/cache-file cachefile: /path/to/cache-file
email: emailused@letsencrypt.com email: emailused@letsencrypt.com
@ -808,6 +809,7 @@ http:
clientcas: clientcas:
- /path/to/ca.pem - /path/to/ca.pem
- /path/to/another/ca.pem - /path/to/another/ca.pem
clientauth: require-and-verify-client-cert
minimumtls: tls1.2 minimumtls: tls1.2
ciphersuites: ciphersuites:
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
@ -848,13 +850,14 @@ for the server. If you already have a web server running on
the same host as the registry, you may prefer to configure TLS on that web server the same host as the registry, you may prefer to configure TLS on that web server
and proxy connections to the registry server. and proxy connections to the registry server.
| Parameter | Required | Description | | Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------| |----------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `certificate` | yes | Absolute path to the x509 certificate file. | | `certificate` | yes | Absolute path to the x509 certificate file. |
| `key` | yes | Absolute path to the x509 private key file. | | `key` | yes | Absolute path to the x509 private key file. |
| `clientcas` | no | An array of absolute paths to x509 CA files. | | `clientcas` | no | An array of absolute paths to x509 CA files. |
| `minimumtls` | no | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2, tls1.3). Defaults to tls1.2 | | `clientauth` | no | Client certificate authentication mode. This setting determines how the server handles client certificates during the TLS handshake. If clientcas is not provided, TLS Client Authentication is disabled, and the mode is ignored. Allowed (request-client-cert, require-any-client-cert, verify-client-cert-if-given, require-and-verify-client-cert). Defaults to require-and-verify-client-cert |
| `ciphersuites` | no | Cipher suites allowed. Please see below for allowed values and default. | | `minimumtls` | no | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2, tls1.3). Defaults to tls1.2 |
| `ciphersuites` | no | Cipher suites allowed. Please see below for allowed values and default. |
Available cipher suites: Available cipher suites:
- TLS_RSA_WITH_RC4_128_SHA - TLS_RSA_WITH_RC4_128_SHA

View File

@ -90,7 +90,7 @@ This type of garbage collection is known as stop-the-world garbage collection.
Garbage collection can be run as follows Garbage collection can be run as follows
`bin/registry garbage-collect [--dry-run] /path/to/config.yml` `bin/registry garbage-collect [--dry-run] [--delete-untagged] [--quiet] /path/to/config.yml`
The garbage-collect command accepts a `--dry-run` parameter, which prints the progress The garbage-collect command accepts a `--dry-run` parameter, which prints the progress
of the mark and sweep phases without removing any data. Running with a log level of `info` of the mark and sweep phases without removing any data. Running with a log level of `info`
@ -122,3 +122,8 @@ blob eligible for deletion: sha256:87192bdbe00f8f2a62527f36bb4c7c7f4eaf9307e4b87
blob eligible for deletion: sha256:b549a9959a664038fc35c155a95742cf12297672ca0ae35735ec027d55bf4e97 blob eligible for deletion: sha256:b549a9959a664038fc35c155a95742cf12297672ca0ae35735ec027d55bf4e97
blob eligible for deletion: sha256:f251d679a7c61455f06d793e43c06786d7766c88b8c24edf242b2c08e3c3f599 blob eligible for deletion: sha256:f251d679a7c61455f06d793e43c06786d7766c88b8c24edf242b2c08e3c3f599
``` ```
The `--delete-untagged` option can be used to delete manifests that are not currently referenced by a tag.
The `--quiet` option suppresses any output from being printed.

6
go.mod
View File

@ -2,7 +2,7 @@ module github.com/distribution/distribution/v3
go 1.22.7 go 1.22.7
toolchain go1.23.2 toolchain go1.23.4
require ( require (
cloud.google.com/go/storage v1.45.0 cloud.google.com/go/storage v1.45.0
@ -38,8 +38,9 @@ require (
go.opentelemetry.io/otel/sdk v1.32.0 go.opentelemetry.io/otel/sdk v1.32.0
go.opentelemetry.io/otel/trace v1.32.0 go.opentelemetry.io/otel/trace v1.32.0
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.31.0
golang.org/x/net v0.30.0 golang.org/x/net v0.33.0
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.10.0
google.golang.org/api v0.197.0 google.golang.org/api v0.197.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@ -109,7 +110,6 @@ require (
go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.6.0 // indirect golang.org/x/time v0.6.0 // indirect

4
go.sum
View File

@ -322,8 +322,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=

View File

@ -197,7 +197,7 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
vals = append(vals, "") vals = append(vals, "")
continue continue
} }
return opts, fmt.Errorf("token auth requires a valid option string: %q", key) return tokenAccessOptions{}, fmt.Errorf("token auth requires a valid option string: %q", key)
} }
vals = append(vals, val) vals = append(vals, val)
} }
@ -208,7 +208,7 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
if ok { if ok {
autoRedirect, ok := autoRedirectVal.(bool) autoRedirect, ok := autoRedirectVal.(bool)
if !ok { if !ok {
return opts, errors.New("token auth requires a valid option bool: autoredirect") return tokenAccessOptions{}, errors.New("token auth requires a valid option bool: autoredirect")
} }
opts.autoRedirect = autoRedirect opts.autoRedirect = autoRedirect
} }
@ -217,7 +217,7 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
if ok { if ok {
autoRedirectPath, ok := autoRedirectPathVal.(string) autoRedirectPath, ok := autoRedirectPathVal.(string)
if !ok { if !ok {
return opts, errors.New("token auth requires a valid option string: autoredirectpath") return tokenAccessOptions{}, errors.New("token auth requires a valid option string: autoredirectpath")
} }
opts.autoRedirectPath = autoRedirectPath opts.autoRedirectPath = autoRedirectPath
} }
@ -228,11 +228,19 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
signingAlgos, ok := options["signingalgorithms"] signingAlgos, ok := options["signingalgorithms"]
if ok { if ok {
signingAlgorithmsVals, ok := signingAlgos.([]string) signingAlgorithmsVals, ok := signingAlgos.([]interface{})
if !ok { if !ok {
return opts, errors.New("signingalgorithms must be a list of signing algorithms") return tokenAccessOptions{}, errors.New("signingalgorithms must be a list of signing algorithms")
}
for _, signingAlgorithmVal := range signingAlgorithmsVals {
signingAlgorithm, ok := signingAlgorithmVal.(string)
if !ok {
return tokenAccessOptions{}, errors.New("signingalgorithms must be a list of signing algorithms")
}
opts.signingAlgorithms = append(opts.signingAlgorithms, signingAlgorithm)
} }
opts.signingAlgorithms = signingAlgorithmsVals
} }
return opts, nil return opts, nil
@ -298,11 +306,11 @@ func getJwks(path string) (*jose.JSONWebKeySet, error) {
func getSigningAlgorithms(algos []string) ([]jose.SignatureAlgorithm, error) { func getSigningAlgorithms(algos []string) ([]jose.SignatureAlgorithm, error) {
signAlgVals := make([]jose.SignatureAlgorithm, 0, len(algos)) signAlgVals := make([]jose.SignatureAlgorithm, 0, len(algos))
for _, alg := range algos { for _, alg := range algos {
alg, ok := signingAlgorithms[alg] signAlg, ok := signingAlgorithms[alg]
if !ok { if !ok {
return nil, fmt.Errorf("unsupported signing algorithm: %s", alg) return nil, fmt.Errorf("unsupported signing algorithm: %s", alg)
} }
signAlgVals = append(signAlgVals, alg) signAlgVals = append(signAlgVals, signAlg)
} }
return signAlgVals, nil return signAlgVals, nil
} }

View File

@ -212,18 +212,18 @@ func verifyCertChain(header jose.Header, roots *x509.CertPool) (signingKey crypt
return return
} }
func verifyJWK(header jose.Header, verifyOpts VerifyOptions) (signingKey crypto.PublicKey, err error) { func verifyJWK(header jose.Header, verifyOpts VerifyOptions) (crypto.PublicKey, error) {
jwk := header.JSONWebKey jwk := header.JSONWebKey
signingKey = jwk.Key
// Check to see if the key includes a certificate chain. // Check to see if the key includes a certificate chain.
if len(jwk.Certificates) == 0 { if len(jwk.Certificates) == 0 {
// The JWK should be one of the trusted root keys. // The JWK should be one of the trusted root keys.
if _, trusted := verifyOpts.TrustedKeys[jwk.KeyID]; !trusted { key, trusted := verifyOpts.TrustedKeys[jwk.KeyID]
if !trusted {
return nil, errors.New("untrusted JWK with no certificate chain") return nil, errors.New("untrusted JWK with no certificate chain")
} }
// The JWK is one of the trusted keys. // The JWK is one of the trusted keys.
return return key, nil
} }
opts := x509.VerifyOptions{ opts := x509.VerifyOptions{
@ -245,9 +245,8 @@ func verifyJWK(header jose.Header, verifyOpts VerifyOptions) (signingKey crypto.
if err != nil { if err != nil {
return nil, err return nil, err
} }
signingKey = getCertPubKey(chains)
return return getCertPubKey(chains), nil
} }
func getCertPubKey(chains [][]*x509.Certificate) crypto.PublicKey { func getCertPubKey(chains [][]*x509.Certificate) crypto.PublicKey {

View File

@ -646,3 +646,57 @@ func TestNewAccessControllerPemBlock(t *testing.T) {
t.Fatal("accessController has the wrong number of certificates") t.Fatal("accessController has the wrong number of certificates")
} }
} }
// This test makes sure the untrusted key can not be used in token verification.
func TestVerifyJWKWithTrustedKey(t *testing.T) {
// Generate a test key pair
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
pubKey := privKey.Public()
// Create a JWK with no certificates
jwk := &jose.JSONWebKey{
Key: privKey,
KeyID: "test-key-id",
Use: "sig",
Algorithm: string(jose.ES256),
}
// Create verify options with our public key as trusted
verifyOpts := VerifyOptions{
TrustedKeys: map[string]crypto.PublicKey{
"test-key-id": pubKey,
},
}
// Create test header
header := jose.Header{
JSONWebKey: jwk,
}
// Test the verifyJWK function
returnedKey, err := verifyJWK(header, verifyOpts)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
// Verify the returned key matches our trusted key
if returnedKey != pubKey {
t.Error("Returned key does not match the trusted key")
}
// Test with untrusted key
verifyOpts.TrustedKeys = map[string]crypto.PublicKey{
"different-key-id": pubKey,
}
_, err = verifyJWK(header, verifyOpts)
if err == nil {
t.Error("Expected error for untrusted key, got none")
}
if err.Error() != "untrusted JWK with no certificate chain" {
t.Errorf("Expected 'untrusted JWK with no certificate chain' error, got: %v", err)
}
}

View File

@ -79,6 +79,14 @@ var tlsVersions = map[string]uint16{
"tls1.3": tls.VersionTLS13, "tls1.3": tls.VersionTLS13,
} }
// tlsClientAuth maps user-specified values to TLS Client Authentication constants.
var tlsClientAuth = map[string]tls.ClientAuthType{
configuration.ClientAuthRequestClientCert: tls.RequestClientCert,
configuration.ClientAuthRequireAnyClientCert: tls.RequireAnyClientCert,
configuration.ClientAuthVerifyClientCertIfGiven: tls.VerifyClientCertIfGiven,
configuration.ClientAuthRequireAndVerifyClientCert: tls.RequireAndVerifyClientCert,
}
// defaultLogFormatter is the default formatter to use for logs. // defaultLogFormatter is the default formatter to use for logs.
const defaultLogFormatter = "text" const defaultLogFormatter = "text"
@ -298,7 +306,18 @@ func (registry *Registry) ListenAndServe() error {
dcontext.GetLogger(registry.app).Debugf("CA Subject: %s", string(subj)) dcontext.GetLogger(registry.app).Debugf("CA Subject: %s", string(subj))
} }
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert if config.HTTP.TLS.ClientAuth != "" {
tlsClientAuthMod, ok := tlsClientAuth[string(config.HTTP.TLS.ClientAuth)]
if !ok {
return fmt.Errorf("unknown client auth mod '%s' specified for http.tls.clientauth", config.HTTP.TLS.ClientAuth)
}
tlsConf.ClientAuth = tlsClientAuthMod
} else {
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
}
tlsConf.ClientCAs = pool tlsConf.ClientCAs = pool
} }

View File

@ -18,6 +18,7 @@ func init() {
RootCmd.AddCommand(GCCmd) RootCmd.AddCommand(GCCmd)
GCCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "do everything except remove the blobs") GCCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "do everything except remove the blobs")
GCCmd.Flags().BoolVarP(&removeUntagged, "delete-untagged", "m", false, "delete manifests that are not currently referenced via tag") GCCmd.Flags().BoolVarP(&removeUntagged, "delete-untagged", "m", false, "delete manifests that are not currently referenced via tag")
GCCmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "silence output")
RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit") RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit")
} }
@ -39,6 +40,7 @@ var RootCmd = &cobra.Command{
var ( var (
dryRun bool dryRun bool
removeUntagged bool removeUntagged bool
quiet bool
) )
// GCCmd is the cobra command that corresponds to the garbage-collect subcommand // GCCmd is the cobra command that corresponds to the garbage-collect subcommand
@ -77,6 +79,7 @@ var GCCmd = &cobra.Command{
err = storage.MarkAndSweep(ctx, driver, registry, storage.GCOpts{ err = storage.MarkAndSweep(ctx, driver, registry, storage.GCOpts{
DryRun: dryRun, DryRun: dryRun,
RemoveUntagged: removeUntagged, RemoveUntagged: removeUntagged,
Quiet: quiet,
}) })
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to garbage collect: %v", err) fmt.Fprintf(os.Stderr, "failed to garbage collect: %v", err)

View File

@ -20,6 +20,7 @@ func emit(format string, a ...interface{}) {
type GCOpts struct { type GCOpts struct {
DryRun bool DryRun bool
RemoveUntagged bool RemoveUntagged bool
Quiet bool
} }
// ManifestDel contains manifest structure which will be deleted // ManifestDel contains manifest structure which will be deleted
@ -41,7 +42,9 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
deleteLayerSet := make(map[string][]digest.Digest) deleteLayerSet := make(map[string][]digest.Digest)
manifestArr := make([]ManifestDel, 0) manifestArr := make([]ManifestDel, 0)
err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error { err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error {
emit(repoName) if !opts.Quiet {
emit(repoName)
}
var err error var err error
named, err := reference.WithName(repoName) named, err := reference.WithName(repoName)
@ -77,7 +80,9 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
allTags, err := repository.Tags(ctx).All(ctx) allTags, err := repository.Tags(ctx).All(ctx)
if err != nil { if err != nil {
if _, ok := err.(distribution.ErrRepositoryUnknown); ok { if _, ok := err.(distribution.ErrRepositoryUnknown); ok {
emit("manifest tags path of repository %s does not exist", repoName) if !opts.Quiet {
emit("manifest tags path of repository %s does not exist", repoName)
}
return nil return nil
} }
return fmt.Errorf("failed to retrieve tags %v", err) return fmt.Errorf("failed to retrieve tags %v", err)
@ -87,14 +92,18 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
} }
} }
// Mark the manifest's blob // Mark the manifest's blob
emit("%s: marking manifest %s ", repoName, dgst) if !opts.Quiet {
emit("%s: marking manifest %s ", repoName, dgst)
}
markSet[dgst] = struct{}{} markSet[dgst] = struct{}{}
return markManifestReferences(dgst, manifestService, ctx, func(d digest.Digest) bool { return markManifestReferences(dgst, manifestService, ctx, func(d digest.Digest) bool {
_, marked := markSet[d] _, marked := markSet[d]
if !marked { if !marked {
markSet[d] = struct{}{} markSet[d] = struct{}{}
emit("%s: marking blob %s", repoName, d) if !opts.Quiet {
emit("%s: marking blob %s", repoName, d)
}
} }
return marked return marked
}) })
@ -132,7 +141,7 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
return fmt.Errorf("failed to mark: %v", err) return fmt.Errorf("failed to mark: %v", err)
} }
manifestArr = unmarkReferencedManifest(manifestArr, markSet) manifestArr = unmarkReferencedManifest(manifestArr, markSet, opts.Quiet)
// sweep // sweep
vacuum := NewVacuum(ctx, storageDriver) vacuum := NewVacuum(ctx, storageDriver)
@ -156,9 +165,13 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
if err != nil { if err != nil {
return fmt.Errorf("error enumerating blobs: %v", err) return fmt.Errorf("error enumerating blobs: %v", err)
} }
emit("\n%d blobs marked, %d blobs and %d manifests eligible for deletion", len(markSet), len(deleteSet), len(manifestArr)) if !opts.Quiet {
emit("\n%d blobs marked, %d blobs and %d manifests eligible for deletion", len(markSet), len(deleteSet), len(manifestArr))
}
for dgst := range deleteSet { for dgst := range deleteSet {
emit("blob eligible for deletion: %s", dgst) if !opts.Quiet {
emit("blob eligible for deletion: %s", dgst)
}
if opts.DryRun { if opts.DryRun {
continue continue
} }
@ -170,7 +183,9 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
for repo, dgsts := range deleteLayerSet { for repo, dgsts := range deleteLayerSet {
for _, dgst := range dgsts { for _, dgst := range dgsts {
emit("%s: layer link eligible for deletion: %s", repo, dgst) if !opts.Quiet {
emit("%s: layer link eligible for deletion: %s", repo, dgst)
}
if opts.DryRun { if opts.DryRun {
continue continue
} }
@ -185,11 +200,14 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
} }
// unmarkReferencedManifest filters out manifest present in markSet // unmarkReferencedManifest filters out manifest present in markSet
func unmarkReferencedManifest(manifestArr []ManifestDel, markSet map[digest.Digest]struct{}) []ManifestDel { func unmarkReferencedManifest(manifestArr []ManifestDel, markSet map[digest.Digest]struct{}, quietOutput bool) []ManifestDel {
filtered := make([]ManifestDel, 0) filtered := make([]ManifestDel, 0)
for _, obj := range manifestArr { for _, obj := range manifestArr {
if _, ok := markSet[obj.Digest]; !ok { if _, ok := markSet[obj.Digest]; !ok {
emit("manifest eligible for deletion: %s", obj) if !quietOutput {
emit("manifest eligible for deletion: %s", obj)
}
filtered = append(filtered, obj) filtered = append(filtered, obj)
} }
} }

104
releases/v3.0.0-rc.2.toml Normal file
View File

@ -0,0 +1,104 @@
# commit to be tagged for new release
commit = "HEAD"
project_name = "registry"
github_repo = "distribution/distribution"
# previous release
previous = "v3.0.0-rc.1"
pre_release = true
preface = """\
registry 3.0.0-rc.2
Welcome to the v3.0.0-rc.2 release of registry!
*This is a pre-release of registry*
This is the second stable release candidate of registry.
It contains various updates and bug fixes with a few minor feature additions.
See the changelog below for the full list of changes.
### Notable Changes
* Upgrade Go OpenTelemetry
* Add support for mtls auth
* Update Go runtime and Alpine image
### Changes
<details><summary>20 commits</summary>
<p>
* [`1c628981`](https://github.com/distribution/distribution/commit/1c628981442961ee349bf7c9d1a576fab2bdbe67) feat(configuration): support mtls auth mod (#4537)
* [`41a906f0`](https://github.com/distribution/distribution/commit/41a906f0c670b5bdc7f5fc9d00f02219a5d4532b) fix(configuration): replace string literals with constants in tests
* [`96c9a85b`](https://github.com/distribution/distribution/commit/96c9a85b62ee6ffbe7df85b6fb95f054e6a6399b) fix(configuration): replace string literals with constants in error
* [`328f802b`](https://github.com/distribution/distribution/commit/328f802b8e87ae1f6bd8319be2f427e588043397) fix(configuration): replace string literals with constants
* [`916bdeae`](https://github.com/distribution/distribution/commit/916bdeae94f200d85603a9dcec2fa1bb22b755a8) feat(configuration): support mtls auth mod
* [`258144d7`](https://github.com/distribution/distribution/commit/258144d70f2718d4b4086e937621cff3e8cd18e4) Update squizzi maintainer email (#4530)
* [`183919ce`](https://github.com/distribution/distribution/commit/183919cee521026dd602f8305de56a5ba900ef2f) Update squizzi maintainer email
* [`3241bc21`](https://github.com/distribution/distribution/commit/3241bc213cdf67ba53dfc5e7033549ff6e0d37ab) chore: Bump alpine image version (#4532)
* [`96a3daaf`](https://github.com/distribution/distribution/commit/96a3daafe907ac077d695358359f76b743410eef) Move a direct dependency to direct deps required modules
* [`fb3ba302`](https://github.com/distribution/distribution/commit/fb3ba302d2081082cf040a2f15f68ea2e0f5297a) chore: Bump alpine and Go versions
* [`b2ae9e39`](https://github.com/distribution/distribution/commit/b2ae9e398c3871f9340cb825cf507535bc2f2fdc) update xx to v1.6.1 for compatibility with alpine 3.21 and file 5.46+ (#4527)
* [`179e902f`](https://github.com/distribution/distribution/commit/179e902fe94d8a16119dffce6044c11bb5dedf43) update xx to v1.6.1 for compatibility with alpine 3.21 and file 5.46+
* [`61ee6994`](https://github.com/distribution/distribution/commit/61ee69943b5197e158bfe5518dc7240bc5ce052f) build(deps): bump golang.org/x/crypto from 0.28.0 to 0.31.0 (#4531)
* [`6eba54be`](https://github.com/distribution/distribution/commit/6eba54be6041f3e760abc2eb99aa199b98c407b3) build(deps): bump golang.org/x/crypto from 0.28.0 to 0.31.0
* [`f2658eeb`](https://github.com/distribution/distribution/commit/f2658eeb0b21dbaf77b9117242d2200aaff90a0b) docs: Explain how to configure a list through env variables (#4522)
* [`1d104a93`](https://github.com/distribution/distribution/commit/1d104a93991d71a14e3342bae7481032ff222d61) [docs] Explain how to configure a list through env variables
* [`e3007cd2`](https://github.com/distribution/distribution/commit/e3007cd2bcd9526c1c7c92edad419bd0e67531d1) Upgrade `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` (#4507)
* [`abbe03ef`](https://github.com/distribution/distribution/commit/abbe03efefa2f49c1dd7f73370aaa16f423eb3f3) Upgrade go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
* [`a44f1fb0`](https://github.com/distribution/distribution/commit/a44f1fb058533f3f840037ebf29c0fea377ebbc6) build(deps): bump codecov/codecov-action from 4 to 5 (#4508)
* [`4dfab838`](https://github.com/distribution/distribution/commit/4dfab838b75c9ac3a95de044e1783554b312b756) build(deps): bump codecov/codecov-action from 4 to 5
</p>
</details>
### Contributors
* Milos Gajdos
* Wang Yan
* vitshev
* Kyle Squizzato
* Sebastiaan van Stijn
* Victor Barbu
* krynju
### Dependency Changes
* **github.com/grpc-ecosystem/grpc-gateway/v2** v2.22.0 -> v2.23.0
* **github.com/klauspost/compress** v1.17.9 -> v1.17.11
* **github.com/prometheus/client_golang** v1.20.1 -> v1.20.5
* **github.com/prometheus/common** v0.55.0 -> v0.60.1
* **go.opentelemetry.io/contrib/bridges/prometheus** v0.54.0 -> v0.57.0
* **go.opentelemetry.io/contrib/exporters/autoexport** v0.54.0 -> v0.57.0
* **go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp** v0.54.0 -> v0.57.0
* **go.opentelemetry.io/otel** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc** v0.8.0 **_new_**
* **go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp** v0.5.0 -> v0.8.0
* **go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/exporters/otlp/otlptrace** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/exporters/prometheus** v0.51.0 -> v0.54.0
* **go.opentelemetry.io/otel/exporters/stdout/stdoutlog** v0.5.0 -> v0.8.0
* **go.opentelemetry.io/otel/exporters/stdout/stdoutmetric** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/exporters/stdout/stdouttrace** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/log** v0.5.0 -> v0.8.0
* **go.opentelemetry.io/otel/metric** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/sdk** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/sdk/log** v0.5.0 -> v0.8.0
* **go.opentelemetry.io/otel/sdk/metric** v1.29.0 -> v1.32.0
* **go.opentelemetry.io/otel/trace** v1.29.0 -> v1.32.0
* **golang.org/x/crypto** v0.27.0 -> v0.31.0
* **golang.org/x/net** v0.29.0 -> v0.30.0
* **golang.org/x/sync** v0.8.0 -> v0.10.0
* **golang.org/x/sys** v0.25.0 -> v0.28.0
* **golang.org/x/text** v0.18.0 -> v0.21.0
* **google.golang.org/genproto/googleapis/api** 8af14fe29dc1 -> dd2ea8efbc28
* **google.golang.org/genproto/googleapis/rpc** 8af14fe29dc1 -> dd2ea8efbc28
* **google.golang.org/grpc** v1.66.2 -> v1.68.0
* **google.golang.org/protobuf** v1.34.2 -> v1.35.1
Previous release can be found at [v3.0.0-rc.1](https://github.com/distribution/distribution/releases/tag/v3.0.0-rc.1)
"""

63
releases/v3.0.0-rc.3.toml Normal file
View File

@ -0,0 +1,63 @@
# commit to be tagged for new release
commit = "HEAD"
project_name = "registry"
github_repo = "distribution/distribution"
# previous release
previous = "v3.0.0-rc.2"
pre_release = true
preface = """\
registry 3.0.0-rc.3
Welcome to the v3.0.0-rc.3 release of registry!
*This is a pre-release!*
This is the third stable release candidate of registry which fixes
Please try out the release binaries and report any issues at
https://github.com/distribution/distribution/issues.
See the changelog below for the full list of changes.
### Notable Changes
* Fixes CVE-2025-24976
### Contributors
* Milos Gajdos
* CrazyMax
* Wang Yan
### Changes
<details><summary>17 commits</summary>
<p>
* [`5ea9aa02`](https://github.com/distribution/distribution/commit/5ea9aa028db65ca5665f6af2c20ecf9dc34e5fcd) Merge commit from fork
* [`6ed60b0f`](https://github.com/distribution/distribution/commit/6ed60b0f4892685fc9bc5924ff2e2788d7dbbab7) Apply suggestions from code review
* [`53c38264`](https://github.com/distribution/distribution/commit/53c382641c9223aaa2b79793b05d444bebff0587) Remove named returns and fix linting woes
* [`f4a500ca`](https://github.com/distribution/distribution/commit/f4a500caf68169dccb0b54cb90523e68ee1ac2be) Fix registry token authentication bug
* [`939a525d`](https://github.com/distribution/distribution/commit/939a525dd5293903d82f329dc3c33c0228793c3d) Bump Go version (#4566)
* [`7098b3f4`](https://github.com/distribution/distribution/commit/7098b3f42c4f8907dcc6e63ea209572c0f9dd210) Bump Go version
* [`7271d882`](https://github.com/distribution/distribution/commit/7271d882c06246d7f91802b95f5708035aa83908) ci: fix bake build (#4555)
* [`4c5e3945`](https://github.com/distribution/distribution/commit/4c5e3945612b26927cfd8d500da128fb602522a4) ci: fix bake build
* [`3270367d`](https://github.com/distribution/distribution/commit/3270367d89f572883be9a3ac2c28dd4222df5bf7) (security): Bump golang.org/x/net module (#4542)
* [`38fd91a4`](https://github.com/distribution/distribution/commit/38fd91a49e13811735941ecb9f3cd3b657f6e463) (security): Bump golang.org/x/net module
* [`17550ead`](https://github.com/distribution/distribution/commit/17550ead969a32874a8f656d2b91e7b424c31cc2) build(deps): bump actions/upload-artifact from 4.5.0 to 4.6.0 (#4553)
* [`825eeb03`](https://github.com/distribution/distribution/commit/825eeb039d80de6289b97820a90b658af2312c18) build(deps): bump actions/upload-artifact from 4.5.0 to 4.6.0
* [`9dff0cbf`](https://github.com/distribution/distribution/commit/9dff0cbf9fa59afe07c02ba53405bbcf0ce40d8a) ci: update bake-action to v6 (#4554)
* [`808f0b89`](https://github.com/distribution/distribution/commit/808f0b8961e899901218c9b2bcf7a0d01e2def8f) ci: update bake-action to v6
* [`43291261`](https://github.com/distribution/distribution/commit/43291261fab610a7347215310dc4370e5e9a6c25) build(deps): bump actions/upload-artifact from 4.3.6 to 4.5.0 (#4538)
* [`f1e33060`](https://github.com/distribution/distribution/commit/f1e33060cbcccc8aa38730bf0786bdd935f2056f) Fix conformance upload issue:
* [`d85819c0`](https://github.com/distribution/distribution/commit/d85819c08e852dfcde5cb15834d7b79eca89428e) build(deps): bump actions/upload-artifact from 4.3.6 to 4.5.0
</p>
</details>
### Dependency Changes
* **golang.org/x/net** v0.30.0 -> v0.33.0
Previous release can be found at [v3.0.0-rc.2](https://github.com/distribution/distribution/releases/tag/v3.0.0-rc.2)
"""

View File

@ -8,8 +8,8 @@ package http2
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"net"
"net/http" "net/http"
"sync" "sync"
) )
@ -158,7 +158,7 @@ func (c *dialCall) dial(ctx context.Context, addr string) {
// This code decides which ones live or die. // This code decides which ones live or die.
// The return value used is whether c was used. // The return value used is whether c was used.
// c is never closed. // c is never closed.
func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn) (used bool, err error) { func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c net.Conn) (used bool, err error) {
p.mu.Lock() p.mu.Lock()
for _, cc := range p.conns[key] { for _, cc := range p.conns[key] {
if cc.CanTakeNewRequest() { if cc.CanTakeNewRequest() {
@ -194,8 +194,8 @@ type addConnCall struct {
err error err error
} }
func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) { func (c *addConnCall) run(t *Transport, key string, nc net.Conn) {
cc, err := t.NewClientConn(tc) cc, err := t.NewClientConn(nc)
p := c.p p := c.p
p.mu.Lock() p.mu.Lock()

View File

@ -1490,7 +1490,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error {
pf := mh.PseudoFields() pf := mh.PseudoFields()
for i, hf := range pf { for i, hf := range pf {
switch hf.Name { switch hf.Name {
case ":method", ":path", ":scheme", ":authority": case ":method", ":path", ":scheme", ":authority", ":protocol":
isRequest = true isRequest = true
case ":status": case ":status":
isResponse = true isResponse = true
@ -1498,7 +1498,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error {
return pseudoHeaderError(hf.Name) return pseudoHeaderError(hf.Name)
} }
// Check for duplicates. // Check for duplicates.
// This would be a bad algorithm, but N is 4. // This would be a bad algorithm, but N is 5.
// And this doesn't allocate. // And this doesn't allocate.
for _, hf2 := range pf[:i] { for _, hf2 := range pf[:i] {
if hf.Name == hf2.Name { if hf.Name == hf2.Name {

View File

@ -34,10 +34,11 @@ import (
) )
var ( var (
VerboseLogs bool VerboseLogs bool
logFrameWrites bool logFrameWrites bool
logFrameReads bool logFrameReads bool
inTests bool inTests bool
disableExtendedConnectProtocol bool
) )
func init() { func init() {
@ -50,6 +51,9 @@ func init() {
logFrameWrites = true logFrameWrites = true
logFrameReads = true logFrameReads = true
} }
if strings.Contains(e, "http2xconnect=0") {
disableExtendedConnectProtocol = true
}
} }
const ( const (
@ -141,6 +145,10 @@ func (s Setting) Valid() error {
if s.Val < 16384 || s.Val > 1<<24-1 { if s.Val < 16384 || s.Val > 1<<24-1 {
return ConnectionError(ErrCodeProtocol) return ConnectionError(ErrCodeProtocol)
} }
case SettingEnableConnectProtocol:
if s.Val != 1 && s.Val != 0 {
return ConnectionError(ErrCodeProtocol)
}
} }
return nil return nil
} }
@ -150,21 +158,23 @@ func (s Setting) Valid() error {
type SettingID uint16 type SettingID uint16
const ( const (
SettingHeaderTableSize SettingID = 0x1 SettingHeaderTableSize SettingID = 0x1
SettingEnablePush SettingID = 0x2 SettingEnablePush SettingID = 0x2
SettingMaxConcurrentStreams SettingID = 0x3 SettingMaxConcurrentStreams SettingID = 0x3
SettingInitialWindowSize SettingID = 0x4 SettingInitialWindowSize SettingID = 0x4
SettingMaxFrameSize SettingID = 0x5 SettingMaxFrameSize SettingID = 0x5
SettingMaxHeaderListSize SettingID = 0x6 SettingMaxHeaderListSize SettingID = 0x6
SettingEnableConnectProtocol SettingID = 0x8
) )
var settingName = map[SettingID]string{ var settingName = map[SettingID]string{
SettingHeaderTableSize: "HEADER_TABLE_SIZE", SettingHeaderTableSize: "HEADER_TABLE_SIZE",
SettingEnablePush: "ENABLE_PUSH", SettingEnablePush: "ENABLE_PUSH",
SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS", SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS",
SettingInitialWindowSize: "INITIAL_WINDOW_SIZE", SettingInitialWindowSize: "INITIAL_WINDOW_SIZE",
SettingMaxFrameSize: "MAX_FRAME_SIZE", SettingMaxFrameSize: "MAX_FRAME_SIZE",
SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE", SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE",
SettingEnableConnectProtocol: "ENABLE_CONNECT_PROTOCOL",
} }
func (s SettingID) String() string { func (s SettingID) String() string {

View File

@ -306,7 +306,7 @@ func ConfigureServer(s *http.Server, conf *Server) error {
if s.TLSNextProto == nil { if s.TLSNextProto == nil {
s.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){} s.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){}
} }
protoHandler := func(hs *http.Server, c *tls.Conn, h http.Handler) { protoHandler := func(hs *http.Server, c net.Conn, h http.Handler, sawClientPreface bool) {
if testHookOnConn != nil { if testHookOnConn != nil {
testHookOnConn() testHookOnConn()
} }
@ -323,12 +323,31 @@ func ConfigureServer(s *http.Server, conf *Server) error {
ctx = bc.BaseContext() ctx = bc.BaseContext()
} }
conf.ServeConn(c, &ServeConnOpts{ conf.ServeConn(c, &ServeConnOpts{
Context: ctx, Context: ctx,
Handler: h, Handler: h,
BaseConfig: hs, BaseConfig: hs,
SawClientPreface: sawClientPreface,
}) })
} }
s.TLSNextProto[NextProtoTLS] = protoHandler s.TLSNextProto[NextProtoTLS] = func(hs *http.Server, c *tls.Conn, h http.Handler) {
protoHandler(hs, c, h, false)
}
// The "unencrypted_http2" TLSNextProto key is used to pass off non-TLS HTTP/2 conns.
//
// A connection passed in this method has already had the HTTP/2 preface read from it.
s.TLSNextProto[nextProtoUnencryptedHTTP2] = func(hs *http.Server, c *tls.Conn, h http.Handler) {
nc, err := unencryptedNetConnFromTLSConn(c)
if err != nil {
if lg := hs.ErrorLog; lg != nil {
lg.Print(err)
} else {
log.Print(err)
}
go c.Close()
return
}
protoHandler(hs, nc, h, true)
}
return nil return nil
} }
@ -913,14 +932,18 @@ func (sc *serverConn) serve(conf http2Config) {
sc.vlogf("http2: server connection from %v on %p", sc.conn.RemoteAddr(), sc.hs) sc.vlogf("http2: server connection from %v on %p", sc.conn.RemoteAddr(), sc.hs)
} }
settings := writeSettings{
{SettingMaxFrameSize, conf.MaxReadFrameSize},
{SettingMaxConcurrentStreams, sc.advMaxStreams},
{SettingMaxHeaderListSize, sc.maxHeaderListSize()},
{SettingHeaderTableSize, conf.MaxDecoderHeaderTableSize},
{SettingInitialWindowSize, uint32(sc.initialStreamRecvWindowSize)},
}
if !disableExtendedConnectProtocol {
settings = append(settings, Setting{SettingEnableConnectProtocol, 1})
}
sc.writeFrame(FrameWriteRequest{ sc.writeFrame(FrameWriteRequest{
write: writeSettings{ write: settings,
{SettingMaxFrameSize, conf.MaxReadFrameSize},
{SettingMaxConcurrentStreams, sc.advMaxStreams},
{SettingMaxHeaderListSize, sc.maxHeaderListSize()},
{SettingHeaderTableSize, conf.MaxDecoderHeaderTableSize},
{SettingInitialWindowSize, uint32(sc.initialStreamRecvWindowSize)},
},
}) })
sc.unackedSettings++ sc.unackedSettings++
@ -1782,6 +1805,9 @@ func (sc *serverConn) processSetting(s Setting) error {
sc.maxFrameSize = int32(s.Val) // the maximum valid s.Val is < 2^31 sc.maxFrameSize = int32(s.Val) // the maximum valid s.Val is < 2^31
case SettingMaxHeaderListSize: case SettingMaxHeaderListSize:
sc.peerMaxHeaderListSize = s.Val sc.peerMaxHeaderListSize = s.Val
case SettingEnableConnectProtocol:
// Receipt of this parameter by a server does not
// have any impact
default: default:
// Unknown setting: "An endpoint that receives a SETTINGS // Unknown setting: "An endpoint that receives a SETTINGS
// frame with any unknown or unsupported identifier MUST // frame with any unknown or unsupported identifier MUST
@ -2212,11 +2238,17 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
scheme: f.PseudoValue("scheme"), scheme: f.PseudoValue("scheme"),
authority: f.PseudoValue("authority"), authority: f.PseudoValue("authority"),
path: f.PseudoValue("path"), path: f.PseudoValue("path"),
protocol: f.PseudoValue("protocol"),
}
// extended connect is disabled, so we should not see :protocol
if disableExtendedConnectProtocol && rp.protocol != "" {
return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol))
} }
isConnect := rp.method == "CONNECT" isConnect := rp.method == "CONNECT"
if isConnect { if isConnect {
if rp.path != "" || rp.scheme != "" || rp.authority == "" { if rp.protocol == "" && (rp.path != "" || rp.scheme != "" || rp.authority == "") {
return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol)) return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol))
} }
} else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && rp.scheme != "http") { } else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && rp.scheme != "http") {
@ -2240,6 +2272,9 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
if rp.authority == "" { if rp.authority == "" {
rp.authority = rp.header.Get("Host") rp.authority = rp.header.Get("Host")
} }
if rp.protocol != "" {
rp.header.Set(":protocol", rp.protocol)
}
rw, req, err := sc.newWriterAndRequestNoBody(st, rp) rw, req, err := sc.newWriterAndRequestNoBody(st, rp)
if err != nil { if err != nil {
@ -2266,6 +2301,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
type requestParam struct { type requestParam struct {
method string method string
scheme, authority, path string scheme, authority, path string
protocol string
header http.Header header http.Header
} }
@ -2307,7 +2343,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*r
var url_ *url.URL var url_ *url.URL
var requestURI string var requestURI string
if rp.method == "CONNECT" { if rp.method == "CONNECT" && rp.protocol == "" {
url_ = &url.URL{Host: rp.authority} url_ = &url.URL{Host: rp.authority}
requestURI = rp.authority // mimic HTTP/1 server behavior requestURI = rp.authority // mimic HTTP/1 server behavior
} else { } else {
@ -2880,6 +2916,11 @@ func (w *responseWriter) SetWriteDeadline(deadline time.Time) error {
return nil return nil
} }
func (w *responseWriter) EnableFullDuplex() error {
// We always support full duplex responses, so this is a no-op.
return nil
}
func (w *responseWriter) Flush() { func (w *responseWriter) Flush() {
w.FlushError() w.FlushError()
} }

View File

@ -202,6 +202,20 @@ func (t *Transport) markNewGoroutine() {
} }
} }
func (t *Transport) now() time.Time {
if t != nil && t.transportTestHooks != nil {
return t.transportTestHooks.group.Now()
}
return time.Now()
}
func (t *Transport) timeSince(when time.Time) time.Duration {
if t != nil && t.transportTestHooks != nil {
return t.now().Sub(when)
}
return time.Since(when)
}
// newTimer creates a new time.Timer, or a synthetic timer in tests. // newTimer creates a new time.Timer, or a synthetic timer in tests.
func (t *Transport) newTimer(d time.Duration) timer { func (t *Transport) newTimer(d time.Duration) timer {
if t.transportTestHooks != nil { if t.transportTestHooks != nil {
@ -281,8 +295,8 @@ func configureTransports(t1 *http.Transport) (*Transport, error) {
if !strSliceContains(t1.TLSClientConfig.NextProtos, "http/1.1") { if !strSliceContains(t1.TLSClientConfig.NextProtos, "http/1.1") {
t1.TLSClientConfig.NextProtos = append(t1.TLSClientConfig.NextProtos, "http/1.1") t1.TLSClientConfig.NextProtos = append(t1.TLSClientConfig.NextProtos, "http/1.1")
} }
upgradeFn := func(authority string, c *tls.Conn) http.RoundTripper { upgradeFn := func(scheme, authority string, c net.Conn) http.RoundTripper {
addr := authorityAddr("https", authority) addr := authorityAddr(scheme, authority)
if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil { if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil {
go c.Close() go c.Close()
return erringRoundTripper{err} return erringRoundTripper{err}
@ -293,18 +307,37 @@ func configureTransports(t1 *http.Transport) (*Transport, error) {
// was unknown) // was unknown)
go c.Close() go c.Close()
} }
if scheme == "http" {
return (*unencryptedTransport)(t2)
}
return t2 return t2
} }
if m := t1.TLSNextProto; len(m) == 0 { if t1.TLSNextProto == nil {
t1.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{ t1.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper)
"h2": upgradeFn, }
t1.TLSNextProto[NextProtoTLS] = func(authority string, c *tls.Conn) http.RoundTripper {
return upgradeFn("https", authority, c)
}
// The "unencrypted_http2" TLSNextProto key is used to pass off non-TLS HTTP/2 conns.
t1.TLSNextProto[nextProtoUnencryptedHTTP2] = func(authority string, c *tls.Conn) http.RoundTripper {
nc, err := unencryptedNetConnFromTLSConn(c)
if err != nil {
go c.Close()
return erringRoundTripper{err}
} }
} else { return upgradeFn("http", authority, nc)
m["h2"] = upgradeFn
} }
return t2, nil return t2, nil
} }
// unencryptedTransport is a Transport with a RoundTrip method that
// always permits http:// URLs.
type unencryptedTransport Transport
func (t *unencryptedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return (*Transport)(t).RoundTripOpt(req, RoundTripOpt{allowHTTP: true})
}
func (t *Transport) connPool() ClientConnPool { func (t *Transport) connPool() ClientConnPool {
t.connPoolOnce.Do(t.initConnPool) t.connPoolOnce.Do(t.initConnPool)
return t.connPoolOrDef return t.connPoolOrDef
@ -324,7 +357,7 @@ type ClientConn struct {
t *Transport t *Transport
tconn net.Conn // usually *tls.Conn, except specialized impls tconn net.Conn // usually *tls.Conn, except specialized impls
tlsState *tls.ConnectionState // nil only for specialized impls tlsState *tls.ConnectionState // nil only for specialized impls
reused uint32 // whether conn is being reused; atomic atomicReused uint32 // whether conn is being reused; atomic
singleUse bool // whether being used for a single http.Request singleUse bool // whether being used for a single http.Request
getConnCalled bool // used by clientConnPool getConnCalled bool // used by clientConnPool
@ -335,25 +368,26 @@ type ClientConn struct {
idleTimeout time.Duration // or 0 for never idleTimeout time.Duration // or 0 for never
idleTimer timer idleTimer timer
mu sync.Mutex // guards following mu sync.Mutex // guards following
cond *sync.Cond // hold mu; broadcast on flow/closed changes cond *sync.Cond // hold mu; broadcast on flow/closed changes
flow outflow // our conn-level flow control quota (cs.outflow is per stream) flow outflow // our conn-level flow control quota (cs.outflow is per stream)
inflow inflow // peer's conn-level flow control inflow inflow // peer's conn-level flow control
doNotReuse bool // whether conn is marked to not be reused for any future requests doNotReuse bool // whether conn is marked to not be reused for any future requests
closing bool closing bool
closed bool closed bool
seenSettings bool // true if we've seen a settings frame, false otherwise seenSettings bool // true if we've seen a settings frame, false otherwise
wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back seenSettingsChan chan struct{} // closed when seenSettings is true or frame reading fails
goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
goAwayDebug string // goAway frame's debug data, retained as a string goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received
streams map[uint32]*clientStream // client-initiated goAwayDebug string // goAway frame's debug data, retained as a string
streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip streams map[uint32]*clientStream // client-initiated
nextStreamID uint32 streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip
pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams nextStreamID uint32
pings map[[8]byte]chan struct{} // in flight ping data to notification channel pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams
br *bufio.Reader pings map[[8]byte]chan struct{} // in flight ping data to notification channel
lastActive time.Time br *bufio.Reader
lastIdle time.Time // time last idle lastActive time.Time
lastIdle time.Time // time last idle
// Settings from peer: (also guarded by wmu) // Settings from peer: (also guarded by wmu)
maxFrameSize uint32 maxFrameSize uint32
maxConcurrentStreams uint32 maxConcurrentStreams uint32
@ -363,6 +397,25 @@ type ClientConn struct {
initialStreamRecvWindowSize int32 initialStreamRecvWindowSize int32
readIdleTimeout time.Duration readIdleTimeout time.Duration
pingTimeout time.Duration pingTimeout time.Duration
extendedConnectAllowed bool
// rstStreamPingsBlocked works around an unfortunate gRPC behavior.
// gRPC strictly limits the number of PING frames that it will receive.
// The default is two pings per two hours, but the limit resets every time
// the gRPC endpoint sends a HEADERS or DATA frame. See golang/go#70575.
//
// rstStreamPingsBlocked is set after receiving a response to a PING frame
// bundled with an RST_STREAM (see pendingResets below), and cleared after
// receiving a HEADERS or DATA frame.
rstStreamPingsBlocked bool
// pendingResets is the number of RST_STREAM frames we have sent to the peer,
// without confirming that the peer has received them. When we send a RST_STREAM,
// we bundle it with a PING frame, unless a PING is already in flight. We count
// the reset stream against the connection's concurrency limit until we get
// a PING response. This limits the number of requests we'll try to send to a
// completely unresponsive connection.
pendingResets int
// reqHeaderMu is a 1-element semaphore channel controlling access to sending new requests. // reqHeaderMu is a 1-element semaphore channel controlling access to sending new requests.
// Write to reqHeaderMu to lock it, read from it to unlock. // Write to reqHeaderMu to lock it, read from it to unlock.
@ -420,12 +473,12 @@ type clientStream struct {
sentHeaders bool sentHeaders bool
// owned by clientConnReadLoop: // owned by clientConnReadLoop:
firstByte bool // got the first response byte firstByte bool // got the first response byte
pastHeaders bool // got first MetaHeadersFrame (actual headers) pastHeaders bool // got first MetaHeadersFrame (actual headers)
pastTrailers bool // got optional second MetaHeadersFrame (trailers) pastTrailers bool // got optional second MetaHeadersFrame (trailers)
num1xx uint8 // number of 1xx responses seen readClosed bool // peer sent an END_STREAM flag
readClosed bool // peer sent an END_STREAM flag readAborted bool // read loop reset the stream
readAborted bool // read loop reset the stream totalHeaderSize int64 // total size of 1xx headers seen
trailer http.Header // accumulated trailers trailer http.Header // accumulated trailers
resTrailer *http.Header // client's Response.Trailer resTrailer *http.Header // client's Response.Trailer
@ -530,6 +583,8 @@ type RoundTripOpt struct {
// no cached connection is available, RoundTripOpt // no cached connection is available, RoundTripOpt
// will return ErrNoCachedConn. // will return ErrNoCachedConn.
OnlyCachedConn bool OnlyCachedConn bool
allowHTTP bool // allow http:// URLs
} }
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
@ -562,7 +617,14 @@ func authorityAddr(scheme string, authority string) (addr string) {
// RoundTripOpt is like RoundTrip, but takes options. // RoundTripOpt is like RoundTrip, but takes options.
func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) { func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
if !(req.URL.Scheme == "https" || (req.URL.Scheme == "http" && t.AllowHTTP)) { switch req.URL.Scheme {
case "https":
// Always okay.
case "http":
if !t.AllowHTTP && !opt.allowHTTP {
return nil, errors.New("http2: unencrypted HTTP/2 not enabled")
}
default:
return nil, errors.New("http2: unsupported scheme") return nil, errors.New("http2: unsupported scheme")
} }
@ -573,7 +635,7 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
t.vlogf("http2: Transport failed to get client conn for %s: %v", addr, err) t.vlogf("http2: Transport failed to get client conn for %s: %v", addr, err)
return nil, err return nil, err
} }
reused := !atomic.CompareAndSwapUint32(&cc.reused, 0, 1) reused := !atomic.CompareAndSwapUint32(&cc.atomicReused, 0, 1)
traceGotConn(req, cc, reused) traceGotConn(req, cc, reused)
res, err := cc.RoundTrip(req) res, err := cc.RoundTrip(req)
if err != nil && retry <= 6 { if err != nil && retry <= 6 {
@ -598,6 +660,22 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
} }
} }
} }
if err == errClientConnNotEstablished {
// This ClientConn was created recently,
// this is the first request to use it,
// and the connection is closed and not usable.
//
// In this state, cc.idleTimer will remove the conn from the pool
// when it fires. Stop the timer and remove it here so future requests
// won't try to use this connection.
//
// If the timer has already fired and we're racing it, the redundant
// call to MarkDead is harmless.
if cc.idleTimer != nil {
cc.idleTimer.Stop()
}
t.connPool().MarkDead(cc)
}
if err != nil { if err != nil {
t.vlogf("RoundTrip failure: %v", err) t.vlogf("RoundTrip failure: %v", err)
return nil, err return nil, err
@ -616,9 +694,10 @@ func (t *Transport) CloseIdleConnections() {
} }
var ( var (
errClientConnClosed = errors.New("http2: client conn is closed") errClientConnClosed = errors.New("http2: client conn is closed")
errClientConnUnusable = errors.New("http2: client conn not usable") errClientConnUnusable = errors.New("http2: client conn not usable")
errClientConnGotGoAway = errors.New("http2: Transport received Server's graceful shutdown GOAWAY") errClientConnNotEstablished = errors.New("http2: client conn could not be established")
errClientConnGotGoAway = errors.New("http2: Transport received Server's graceful shutdown GOAWAY")
) )
// shouldRetryRequest is called by RoundTrip when a request fails to get // shouldRetryRequest is called by RoundTrip when a request fails to get
@ -752,11 +831,13 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead. peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead.
streams: make(map[uint32]*clientStream), streams: make(map[uint32]*clientStream),
singleUse: singleUse, singleUse: singleUse,
seenSettingsChan: make(chan struct{}),
wantSettingsAck: true, wantSettingsAck: true,
readIdleTimeout: conf.SendPingTimeout, readIdleTimeout: conf.SendPingTimeout,
pingTimeout: conf.PingTimeout, pingTimeout: conf.PingTimeout,
pings: make(map[[8]byte]chan struct{}), pings: make(map[[8]byte]chan struct{}),
reqHeaderMu: make(chan struct{}, 1), reqHeaderMu: make(chan struct{}, 1),
lastActive: t.now(),
} }
var group synctestGroupInterface var group synctestGroupInterface
if t.transportTestHooks != nil { if t.transportTestHooks != nil {
@ -960,7 +1041,7 @@ func (cc *ClientConn) State() ClientConnState {
return ClientConnState{ return ClientConnState{
Closed: cc.closed, Closed: cc.closed,
Closing: cc.closing || cc.singleUse || cc.doNotReuse || cc.goAway != nil, Closing: cc.closing || cc.singleUse || cc.doNotReuse || cc.goAway != nil,
StreamsActive: len(cc.streams), StreamsActive: len(cc.streams) + cc.pendingResets,
StreamsReserved: cc.streamsReserved, StreamsReserved: cc.streamsReserved,
StreamsPending: cc.pendingRequests, StreamsPending: cc.pendingRequests,
LastIdle: cc.lastIdle, LastIdle: cc.lastIdle,
@ -992,16 +1073,38 @@ func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) {
// writing it. // writing it.
maxConcurrentOkay = true maxConcurrentOkay = true
} else { } else {
maxConcurrentOkay = int64(len(cc.streams)+cc.streamsReserved+1) <= int64(cc.maxConcurrentStreams) // We can take a new request if the total of
// - active streams;
// - reservation slots for new streams; and
// - streams for which we have sent a RST_STREAM and a PING,
// but received no subsequent frame
// is less than the concurrency limit.
maxConcurrentOkay = cc.currentRequestCountLocked() < int(cc.maxConcurrentStreams)
} }
st.canTakeNewRequest = cc.goAway == nil && !cc.closed && !cc.closing && maxConcurrentOkay && st.canTakeNewRequest = cc.goAway == nil && !cc.closed && !cc.closing && maxConcurrentOkay &&
!cc.doNotReuse && !cc.doNotReuse &&
int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32 && int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32 &&
!cc.tooIdleLocked() !cc.tooIdleLocked()
// If this connection has never been used for a request and is closed,
// then let it take a request (which will fail).
//
// This avoids a situation where an error early in a connection's lifetime
// goes unreported.
if cc.nextStreamID == 1 && cc.streamsReserved == 0 && cc.closed {
st.canTakeNewRequest = true
}
return return
} }
// currentRequestCountLocked reports the number of concurrency slots currently in use,
// including active streams, reserved slots, and reset streams waiting for acknowledgement.
func (cc *ClientConn) currentRequestCountLocked() int {
return len(cc.streams) + cc.streamsReserved + cc.pendingResets
}
func (cc *ClientConn) canTakeNewRequestLocked() bool { func (cc *ClientConn) canTakeNewRequestLocked() bool {
st := cc.idleStateLocked() st := cc.idleStateLocked()
return st.canTakeNewRequest return st.canTakeNewRequest
@ -1014,7 +1117,7 @@ func (cc *ClientConn) tooIdleLocked() bool {
// times are compared based on their wall time. We don't want // times are compared based on their wall time. We don't want
// to reuse a connection that's been sitting idle during // to reuse a connection that's been sitting idle during
// VM/laptop suspend if monotonic time was also frozen. // VM/laptop suspend if monotonic time was also frozen.
return cc.idleTimeout != 0 && !cc.lastIdle.IsZero() && time.Since(cc.lastIdle.Round(0)) > cc.idleTimeout return cc.idleTimeout != 0 && !cc.lastIdle.IsZero() && cc.t.timeSince(cc.lastIdle.Round(0)) > cc.idleTimeout
} }
// onIdleTimeout is called from a time.AfterFunc goroutine. It will // onIdleTimeout is called from a time.AfterFunc goroutine. It will
@ -1376,6 +1479,8 @@ func (cs *clientStream) doRequest(req *http.Request, streamf func(*clientStream)
cs.cleanupWriteRequest(err) cs.cleanupWriteRequest(err)
} }
var errExtendedConnectNotSupported = errors.New("net/http: extended connect not supported by peer")
// writeRequest sends a request. // writeRequest sends a request.
// //
// It returns nil after the request is written, the response read, // It returns nil after the request is written, the response read,
@ -1391,12 +1496,31 @@ func (cs *clientStream) writeRequest(req *http.Request, streamf func(*clientStre
return err return err
} }
// wait for setting frames to be received, a server can change this value later,
// but we just wait for the first settings frame
var isExtendedConnect bool
if req.Method == "CONNECT" && req.Header.Get(":protocol") != "" {
isExtendedConnect = true
}
// Acquire the new-request lock by writing to reqHeaderMu. // Acquire the new-request lock by writing to reqHeaderMu.
// This lock guards the critical section covering allocating a new stream ID // This lock guards the critical section covering allocating a new stream ID
// (requires mu) and creating the stream (requires wmu). // (requires mu) and creating the stream (requires wmu).
if cc.reqHeaderMu == nil { if cc.reqHeaderMu == nil {
panic("RoundTrip on uninitialized ClientConn") // for tests panic("RoundTrip on uninitialized ClientConn") // for tests
} }
if isExtendedConnect {
select {
case <-cs.reqCancel:
return errRequestCanceled
case <-ctx.Done():
return ctx.Err()
case <-cc.seenSettingsChan:
if !cc.extendedConnectAllowed {
return errExtendedConnectNotSupported
}
}
}
select { select {
case cc.reqHeaderMu <- struct{}{}: case cc.reqHeaderMu <- struct{}{}:
case <-cs.reqCancel: case <-cs.reqCancel:
@ -1578,6 +1702,7 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
cs.reqBodyClosed = make(chan struct{}) cs.reqBodyClosed = make(chan struct{})
} }
bodyClosed := cs.reqBodyClosed bodyClosed := cs.reqBodyClosed
closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() || cc.goAway != nil
cc.mu.Unlock() cc.mu.Unlock()
if mustCloseBody { if mustCloseBody {
cs.reqBody.Close() cs.reqBody.Close()
@ -1602,16 +1727,44 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
if cs.sentHeaders { if cs.sentHeaders {
if se, ok := err.(StreamError); ok { if se, ok := err.(StreamError); ok {
if se.Cause != errFromPeer { if se.Cause != errFromPeer {
cc.writeStreamReset(cs.ID, se.Code, err) cc.writeStreamReset(cs.ID, se.Code, false, err)
} }
} else { } else {
cc.writeStreamReset(cs.ID, ErrCodeCancel, err) // We're cancelling an in-flight request.
//
// This could be due to the server becoming unresponsive.
// To avoid sending too many requests on a dead connection,
// we let the request continue to consume a concurrency slot
// until we can confirm the server is still responding.
// We do this by sending a PING frame along with the RST_STREAM
// (unless a ping is already in flight).
//
// For simplicity, we don't bother tracking the PING payload:
// We reset cc.pendingResets any time we receive a PING ACK.
//
// We skip this if the conn is going to be closed on idle,
// because it's short lived and will probably be closed before
// we get the ping response.
ping := false
if !closeOnIdle {
cc.mu.Lock()
// rstStreamPingsBlocked works around a gRPC behavior:
// see comment on the field for details.
if !cc.rstStreamPingsBlocked {
if cc.pendingResets == 0 {
ping = true
}
cc.pendingResets++
}
cc.mu.Unlock()
}
cc.writeStreamReset(cs.ID, ErrCodeCancel, ping, err)
} }
} }
cs.bufPipe.CloseWithError(err) // no-op if already closed cs.bufPipe.CloseWithError(err) // no-op if already closed
} else { } else {
if cs.sentHeaders && !cs.sentEndStream { if cs.sentHeaders && !cs.sentEndStream {
cc.writeStreamReset(cs.ID, ErrCodeNo, nil) cc.writeStreamReset(cs.ID, ErrCodeNo, false, nil)
} }
cs.bufPipe.CloseWithError(errRequestCanceled) cs.bufPipe.CloseWithError(errRequestCanceled)
} }
@ -1633,12 +1786,17 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
// Must hold cc.mu. // Must hold cc.mu.
func (cc *ClientConn) awaitOpenSlotForStreamLocked(cs *clientStream) error { func (cc *ClientConn) awaitOpenSlotForStreamLocked(cs *clientStream) error {
for { for {
cc.lastActive = time.Now() if cc.closed && cc.nextStreamID == 1 && cc.streamsReserved == 0 {
// This is the very first request sent to this connection.
// Return a fatal error which aborts the retry loop.
return errClientConnNotEstablished
}
cc.lastActive = cc.t.now()
if cc.closed || !cc.canTakeNewRequestLocked() { if cc.closed || !cc.canTakeNewRequestLocked() {
return errClientConnUnusable return errClientConnUnusable
} }
cc.lastIdle = time.Time{} cc.lastIdle = time.Time{}
if int64(len(cc.streams)) < int64(cc.maxConcurrentStreams) { if cc.currentRequestCountLocked() < int(cc.maxConcurrentStreams) {
return nil return nil
} }
cc.pendingRequests++ cc.pendingRequests++
@ -1910,7 +2068,7 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
func validateHeaders(hdrs http.Header) string { func validateHeaders(hdrs http.Header) string {
for k, vv := range hdrs { for k, vv := range hdrs {
if !httpguts.ValidHeaderFieldName(k) { if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
return fmt.Sprintf("name %q", k) return fmt.Sprintf("name %q", k)
} }
for _, v := range vv { for _, v := range vv {
@ -1926,6 +2084,10 @@ func validateHeaders(hdrs http.Header) string {
var errNilRequestURL = errors.New("http2: Request.URI is nil") var errNilRequestURL = errors.New("http2: Request.URI is nil")
func isNormalConnect(req *http.Request) bool {
return req.Method == "CONNECT" && req.Header.Get(":protocol") == ""
}
// requires cc.wmu be held. // requires cc.wmu be held.
func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) { func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) {
cc.hbuf.Reset() cc.hbuf.Reset()
@ -1946,7 +2108,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
} }
var path string var path string
if req.Method != "CONNECT" { if !isNormalConnect(req) {
path = req.URL.RequestURI() path = req.URL.RequestURI()
if !validPseudoPath(path) { if !validPseudoPath(path) {
orig := path orig := path
@ -1983,7 +2145,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
m = http.MethodGet m = http.MethodGet
} }
f(":method", m) f(":method", m)
if req.Method != "CONNECT" { if !isNormalConnect(req) {
f(":path", path) f(":path", path)
f(":scheme", req.URL.Scheme) f(":scheme", req.URL.Scheme)
} }
@ -2180,10 +2342,10 @@ func (cc *ClientConn) forgetStreamID(id uint32) {
if len(cc.streams) != slen-1 { if len(cc.streams) != slen-1 {
panic("forgetting unknown stream id") panic("forgetting unknown stream id")
} }
cc.lastActive = time.Now() cc.lastActive = cc.t.now()
if len(cc.streams) == 0 && cc.idleTimer != nil { if len(cc.streams) == 0 && cc.idleTimer != nil {
cc.idleTimer.Reset(cc.idleTimeout) cc.idleTimer.Reset(cc.idleTimeout)
cc.lastIdle = time.Now() cc.lastIdle = cc.t.now()
} }
// Wake up writeRequestBody via clientStream.awaitFlowControl and // Wake up writeRequestBody via clientStream.awaitFlowControl and
// wake up RoundTrip if there is a pending request. // wake up RoundTrip if there is a pending request.
@ -2243,7 +2405,6 @@ func isEOFOrNetReadError(err error) bool {
func (rl *clientConnReadLoop) cleanup() { func (rl *clientConnReadLoop) cleanup() {
cc := rl.cc cc := rl.cc
cc.t.connPool().MarkDead(cc)
defer cc.closeConn() defer cc.closeConn()
defer close(cc.readerDone) defer close(cc.readerDone)
@ -2267,6 +2428,24 @@ func (rl *clientConnReadLoop) cleanup() {
} }
cc.closed = true cc.closed = true
// If the connection has never been used, and has been open for only a short time,
// leave it in the connection pool for a little while.
//
// This avoids a situation where new connections are constantly created,
// added to the pool, fail, and are removed from the pool, without any error
// being surfaced to the user.
const unusedWaitTime = 5 * time.Second
idleTime := cc.t.now().Sub(cc.lastActive)
if atomic.LoadUint32(&cc.atomicReused) == 0 && idleTime < unusedWaitTime {
cc.idleTimer = cc.t.afterFunc(unusedWaitTime-idleTime, func() {
cc.t.connPool().MarkDead(cc)
})
} else {
cc.mu.Unlock() // avoid any deadlocks in MarkDead
cc.t.connPool().MarkDead(cc)
cc.mu.Lock()
}
for _, cs := range cc.streams { for _, cs := range cc.streams {
select { select {
case <-cs.peerClosed: case <-cs.peerClosed:
@ -2324,7 +2503,7 @@ func (rl *clientConnReadLoop) run() error {
cc.vlogf("http2: Transport readFrame error on conn %p: (%T) %v", cc, err, err) cc.vlogf("http2: Transport readFrame error on conn %p: (%T) %v", cc, err, err)
} }
if se, ok := err.(StreamError); ok { if se, ok := err.(StreamError); ok {
if cs := rl.streamByID(se.StreamID); cs != nil { if cs := rl.streamByID(se.StreamID, notHeaderOrDataFrame); cs != nil {
if se.Cause == nil { if se.Cause == nil {
se.Cause = cc.fr.errDetail se.Cause = cc.fr.errDetail
} }
@ -2370,13 +2549,16 @@ func (rl *clientConnReadLoop) run() error {
if VerboseLogs { if VerboseLogs {
cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err) cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err)
} }
if !cc.seenSettings {
close(cc.seenSettingsChan)
}
return err return err
} }
} }
} }
func (rl *clientConnReadLoop) processHeaders(f *MetaHeadersFrame) error { func (rl *clientConnReadLoop) processHeaders(f *MetaHeadersFrame) error {
cs := rl.streamByID(f.StreamID) cs := rl.streamByID(f.StreamID, headerOrDataFrame)
if cs == nil { if cs == nil {
// We'd get here if we canceled a request while the // We'd get here if we canceled a request while the
// server had its response still in flight. So if this // server had its response still in flight. So if this
@ -2494,15 +2676,34 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
if f.StreamEnded() { if f.StreamEnded() {
return nil, errors.New("1xx informational response with END_STREAM flag") return nil, errors.New("1xx informational response with END_STREAM flag")
} }
cs.num1xx++
const max1xxResponses = 5 // arbitrary bound on number of informational responses, same as net/http
if cs.num1xx > max1xxResponses {
return nil, errors.New("http2: too many 1xx informational responses")
}
if fn := cs.get1xxTraceFunc(); fn != nil { if fn := cs.get1xxTraceFunc(); fn != nil {
// If the 1xx response is being delivered to the user,
// then they're responsible for limiting the number
// of responses.
if err := fn(statusCode, textproto.MIMEHeader(header)); err != nil { if err := fn(statusCode, textproto.MIMEHeader(header)); err != nil {
return nil, err return nil, err
} }
} else {
// If the user didn't examine the 1xx response, then we
// limit the size of all 1xx headers.
//
// This differs a bit from the HTTP/1 implementation, which
// limits the size of all 1xx headers plus the final response.
// Use the larger limit of MaxHeaderListSize and
// net/http.Transport.MaxResponseHeaderBytes.
limit := int64(cs.cc.t.maxHeaderListSize())
if t1 := cs.cc.t.t1; t1 != nil && t1.MaxResponseHeaderBytes > limit {
limit = t1.MaxResponseHeaderBytes
}
for _, h := range f.Fields {
cs.totalHeaderSize += int64(h.Size())
}
if cs.totalHeaderSize > limit {
if VerboseLogs {
log.Printf("http2: 1xx informational responses too large")
}
return nil, errors.New("header list too large")
}
} }
if statusCode == 100 { if statusCode == 100 {
traceGot100Continue(cs.trace) traceGot100Continue(cs.trace)
@ -2686,7 +2887,7 @@ func (b transportResponseBody) Close() error {
func (rl *clientConnReadLoop) processData(f *DataFrame) error { func (rl *clientConnReadLoop) processData(f *DataFrame) error {
cc := rl.cc cc := rl.cc
cs := rl.streamByID(f.StreamID) cs := rl.streamByID(f.StreamID, headerOrDataFrame)
data := f.Data() data := f.Data()
if cs == nil { if cs == nil {
cc.mu.Lock() cc.mu.Lock()
@ -2821,9 +3022,22 @@ func (rl *clientConnReadLoop) endStreamError(cs *clientStream, err error) {
cs.abortStream(err) cs.abortStream(err)
} }
func (rl *clientConnReadLoop) streamByID(id uint32) *clientStream { // Constants passed to streamByID for documentation purposes.
const (
headerOrDataFrame = true
notHeaderOrDataFrame = false
)
// streamByID returns the stream with the given id, or nil if no stream has that id.
// If headerOrData is true, it clears rst.StreamPingsBlocked.
func (rl *clientConnReadLoop) streamByID(id uint32, headerOrData bool) *clientStream {
rl.cc.mu.Lock() rl.cc.mu.Lock()
defer rl.cc.mu.Unlock() defer rl.cc.mu.Unlock()
if headerOrData {
// Work around an unfortunate gRPC behavior.
// See comment on ClientConn.rstStreamPingsBlocked for details.
rl.cc.rstStreamPingsBlocked = false
}
cs := rl.cc.streams[id] cs := rl.cc.streams[id]
if cs != nil && !cs.readAborted { if cs != nil && !cs.readAborted {
return cs return cs
@ -2917,6 +3131,21 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
case SettingHeaderTableSize: case SettingHeaderTableSize:
cc.henc.SetMaxDynamicTableSize(s.Val) cc.henc.SetMaxDynamicTableSize(s.Val)
cc.peerMaxHeaderTableSize = s.Val cc.peerMaxHeaderTableSize = s.Val
case SettingEnableConnectProtocol:
if err := s.Valid(); err != nil {
return err
}
// If the peer wants to send us SETTINGS_ENABLE_CONNECT_PROTOCOL,
// we require that it do so in the first SETTINGS frame.
//
// When we attempt to use extended CONNECT, we wait for the first
// SETTINGS frame to see if the server supports it. If we let the
// server enable the feature with a later SETTINGS frame, then
// users will see inconsistent results depending on whether we've
// seen that frame or not.
if !cc.seenSettings {
cc.extendedConnectAllowed = s.Val == 1
}
default: default:
cc.vlogf("Unhandled Setting: %v", s) cc.vlogf("Unhandled Setting: %v", s)
} }
@ -2934,6 +3163,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
// connection can establish to our default. // connection can establish to our default.
cc.maxConcurrentStreams = defaultMaxConcurrentStreams cc.maxConcurrentStreams = defaultMaxConcurrentStreams
} }
close(cc.seenSettingsChan)
cc.seenSettings = true cc.seenSettings = true
} }
@ -2942,7 +3172,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error { func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error {
cc := rl.cc cc := rl.cc
cs := rl.streamByID(f.StreamID) cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame)
if f.StreamID != 0 && cs == nil { if f.StreamID != 0 && cs == nil {
return nil return nil
} }
@ -2971,7 +3201,7 @@ func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error {
} }
func (rl *clientConnReadLoop) processResetStream(f *RSTStreamFrame) error { func (rl *clientConnReadLoop) processResetStream(f *RSTStreamFrame) error {
cs := rl.streamByID(f.StreamID) cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame)
if cs == nil { if cs == nil {
// TODO: return error if server tries to RST_STREAM an idle stream // TODO: return error if server tries to RST_STREAM an idle stream
return nil return nil
@ -3046,6 +3276,12 @@ func (rl *clientConnReadLoop) processPing(f *PingFrame) error {
close(c) close(c)
delete(cc.pings, f.Data) delete(cc.pings, f.Data)
} }
if cc.pendingResets > 0 {
// See clientStream.cleanupWriteRequest.
cc.pendingResets = 0
cc.rstStreamPingsBlocked = true
cc.cond.Broadcast()
}
return nil return nil
} }
cc := rl.cc cc := rl.cc
@ -3068,13 +3304,20 @@ func (rl *clientConnReadLoop) processPushPromise(f *PushPromiseFrame) error {
return ConnectionError(ErrCodeProtocol) return ConnectionError(ErrCodeProtocol)
} }
func (cc *ClientConn) writeStreamReset(streamID uint32, code ErrCode, err error) { // writeStreamReset sends a RST_STREAM frame.
// When ping is true, it also sends a PING frame with a random payload.
func (cc *ClientConn) writeStreamReset(streamID uint32, code ErrCode, ping bool, err error) {
// TODO: map err to more interesting error codes, once the // TODO: map err to more interesting error codes, once the
// HTTP community comes up with some. But currently for // HTTP community comes up with some. But currently for
// RST_STREAM there's no equivalent to GOAWAY frame's debug // RST_STREAM there's no equivalent to GOAWAY frame's debug
// data, and the error codes are all pretty vague ("cancel"). // data, and the error codes are all pretty vague ("cancel").
cc.wmu.Lock() cc.wmu.Lock()
cc.fr.WriteRSTStream(streamID, code) cc.fr.WriteRSTStream(streamID, code)
if ping {
var payload [8]byte
rand.Read(payload[:])
cc.fr.WritePing(false, payload)
}
cc.bw.Flush() cc.bw.Flush()
cc.wmu.Unlock() cc.wmu.Unlock()
} }
@ -3228,7 +3471,7 @@ func traceGotConn(req *http.Request, cc *ClientConn, reused bool) {
cc.mu.Lock() cc.mu.Lock()
ci.WasIdle = len(cc.streams) == 0 && reused ci.WasIdle = len(cc.streams) == 0 && reused
if ci.WasIdle && !cc.lastActive.IsZero() { if ci.WasIdle && !cc.lastActive.IsZero() {
ci.IdleTime = time.Since(cc.lastActive) ci.IdleTime = cc.t.timeSince(cc.lastActive)
} }
cc.mu.Unlock() cc.mu.Unlock()

32
vendor/golang.org/x/net/http2/unencrypted.go generated vendored Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2024 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 http2
import (
"crypto/tls"
"errors"
"net"
)
const nextProtoUnencryptedHTTP2 = "unencrypted_http2"
// unencryptedNetConnFromTLSConn retrieves a net.Conn wrapped in a *tls.Conn.
//
// TLSNextProto functions accept a *tls.Conn.
//
// When passing an unencrypted HTTP/2 connection to a TLSNextProto function,
// we pass a *tls.Conn with an underlying net.Conn containing the unencrypted connection.
// To be extra careful about mistakes (accidentally dropping TLS encryption in a place
// where we want it), the tls.Conn contains a net.Conn with an UnencryptedNetConn method
// that returns the actual connection we want to use.
func unencryptedNetConnFromTLSConn(tc *tls.Conn) (net.Conn, error) {
conner, ok := tc.NetConn().(interface {
UnencryptedNetConn() net.Conn
})
if !ok {
return nil, errors.New("http2: TLS conn unexpectedly found in unencrypted handoff")
}
return conner.UnencryptedNetConn(), nil
}

2
vendor/modules.txt vendored
View File

@ -628,7 +628,7 @@ golang.org/x/crypto/internal/poly1305
golang.org/x/crypto/pbkdf2 golang.org/x/crypto/pbkdf2
golang.org/x/crypto/pkcs12 golang.org/x/crypto/pkcs12
golang.org/x/crypto/pkcs12/internal/rc2 golang.org/x/crypto/pkcs12/internal/rc2
# golang.org/x/net v0.30.0 # golang.org/x/net v0.33.0
## explicit; go 1.18 ## explicit; go 1.18
golang.org/x/net/http/httpguts golang.org/x/net/http/httpguts
golang.org/x/net/http2 golang.org/x/net/http2

View File

@ -8,7 +8,7 @@ var mainpkg = "github.com/distribution/distribution/v3"
// the latest release tag by hand, always suffixed by "+unknown". During // the latest release tag by hand, always suffixed by "+unknown". During
// build, it will be replaced by the actual version. The value here will be // build, it will be replaced by the actual version. The value here will be
// used if the registry is run after a go get based install. // used if the registry is run after a go get based install.
var version = "v3.0.0-rc.1.m+unknown" var version = "v3.0.0-rc.3+unknown"
// revision is filled with the VCS (e.g. git) revision being used to build // revision is filled with the VCS (e.g. git) revision being used to build
// the program at linking time. // the program at linking time.